Remark(1)何ができる?

about remark/unified framework


2020/08/26
藤田昭人


またまたご無沙汰してしまってますが…
本稿からしばらく、いつもとは趣向を変えて remarkについて集中的に紹介してみたいと思います。

実は本稿はこのブログにポストした30番目の記事になります*1。 で、何よりも問題なのは、過去のブログ記事で何を書いたのか忘れてしまう事です。 最近では「あれ?これ、書いた気がするなぁ」とは思い出すのですが、 肝心の「ブログのどの回で、どのように触れたのか?」を なかなか思い出せないことが増えてます。

こういう時の「書いた気がする」という曖昧な記憶から、 サーチエンジンの検索条件を捻り出すのは案外難しい作業だったりします。 止むなく、過去の記事を順番に流し見てみるのですが…これが結構、イライラさせられます。 そこで「僕の曖昧な記憶に沿った検索」を実現できないか?などと考え始めました。

今もって具体的なアイデアは思い付いてないのですが、 まずは過去のブログ記事を普通に検索できる仕組みを用意することにしました。


Remark: Javascript による Markdown プロセッサ

僕のブログ記事は「はてなブログ」の Markdown で記述しているので、 まずは実績のある Markdown の管理ツール・ライブラリを探してみました。 もちろん Markdown といえば Pandoc があることは承知しているのですが、 前述のとおり、そのうち「曖昧な記憶に沿った検索」を実装したいので、 馴染みのある Javascript ベースの Markdown 管理ツール・ライブラリが欲しいところです。

調べてみたところ Remark なるオープンソースが見つかりました。 次のような 概要 が説明されています。

remark is a Markdown processor powered by plugins part of the unified collective. The project parses and compiles Markdown, and lets programs process Markdown without ever compiling to HTML (it can though). Powered by plugins to do all kinds of things: check Markdown code style, transform safely to React, add a table of contents, or compile to man pages.

remarkは、unified コレクティブのプラグインの一部である Markdown 用のプロセッサです。 このプロジェクトは Markdown をパースしコンパイルして、 HTMLにコンパイルすることなくプログラムがMarkdownを処理することを可能にします。 Markdownのコードスタイルのチェック、React への安全な変換、 目次の追加、あるいはマニュアルページのコンパイルなど、 あらゆる種類の処理を行うためのプラグインを搭載しています。

Pandoc ほどメジャーではなさそうですが、よく似た目標で開発されていて、 もう少しプログラマブルな側面に重点が置かれているような感じです。

実は、当初は甘く見てクイック(ダーティ)ハックで、 ちょこちょこと簡易ツールを作ることを目論んだのですが、 このツールが構築されている Unified なるフレームワークは かなり大掛かりなようで敢なく玉砕。 止むなく正攻法で攻めることにしました。


Remark は何ができるの?

で、チュートリアルを探し回ったのですが…

現時点での Remark の難点の1つはドキュメントの整備が行き届いてないこと。 公式のチュートリアルに相当するのは Using unified という 短めのドキュメント(メモ書きとも言う)あたりになるようです*2

ここでは、このメモ書きをさらに端折って紹介します。


基本的な Markdown から HTML への変換

まず、もっとも基本的な Markdown to HTML のコンバーター index.js は次のとおりです。

var unified = require('unified')
var stream = require('unified-stream')
var markdown = require('remark-parse')
var remark2rehype = require('remark-rehype')
var html = require('rehype-stringify')

var processor = unified().use(markdown).use(remark2rehype).use(html)

process.stdin.pipe(stream(processor)).pipe(process.stdout)

JavascriptPromise を使って、 Unix のパイプのような記法で処理が記述できるのが(僕には)わかり易い。 このコンバーターUnix のフィルターのような使い方をするようです。

node index.js < example.md > example.html

ちなみに次のようなファイル example.md を入力すると…

# Hello World

## Table of Content

## Install

A **example**.

## Use

More `text`.

## License

MIT

次のようなファイル example.html が出力されます。

<h1>Hello World</h1>
<h2>Table of Content</h2>
<h2>Install</h2>
<p>A <strong>example</strong>.</p>
<h2>Use</h2>
<p>More <code>text</code>.</p>
<h2>License</h2>
<p>MIT</p>


もう少しリッチな HTML への変換

さらにプラグインを使うように index.js を修正すると…

--- ../Tree transformations/index.js 2020-08-20 13:52:22.000000000 +0900
+++ index.js  2020-08-20 14:11:07.000000000 +0900
@@ -1,9 +1,18 @@
 var unified = require('unified')
 var stream = require('unified-stream')
 var markdown = require('remark-parse')
+var slug = require('remark-slug')
+var toc = require('remark-toc')
 var remark2rehype = require('remark-rehype')
+var doc = require('rehype-document')
 var html = require('rehype-stringify')
 
-var processor = unified().use(markdown).use(remark2rehype).use(html)
+var processor = unified()
+  .use(markdown)
+  .use(slug)
+  .use(toc)
+  .use(remark2rehype)
+  .use(doc, {title: 'Contents'})
+  .use(html)
 
 process.stdin.pipe(stream(processor)).pipe(process.stdout)

出力される example.html は次のように変わります。

<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Contents</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
</head>
<body>
<h1 id="hello-world">Hello World</h1>
<h2 id="table-of-content">Table of Content</h2>
<ul>
<li><a href="#install">Install</a></li>
<li><a href="#use">Use</a></li>
<li><a href="#license">License</a></li>
</ul>
<h2 id="install">Install</h2>
<p>A <strong>example</strong>.</p>
<h2 id="use">Use</h2>
<p>More <code>text</code>.</p>
<h2 id="license">License</h2>
<p>MIT</p>
</body>
</html>


ファイル名を明示的に与える場合

さらにファイル名を明示的に渡すには…

--- ../Plugins/index.js  2020-08-20 14:11:07.000000000 +0900
+++ index.js  2020-08-20 14:31:16.000000000 +0900
@@ -1,5 +1,6 @@
 var unified = require('unified')
-var stream = require('unified-stream')
+var vfile = require('to-vfile')
+var report = require('vfile-reporter')
 var markdown = require('remark-parse')
 var slug = require('remark-slug')
 var toc = require('remark-toc')
@@ -15,4 +16,9 @@
   .use(doc, {title: 'Contents'})
   .use(html)
 
-process.stdin.pipe(stream(processor)).pipe(process.stdout)
+processor.process(vfile.readSync('example.md'), function (error, file) {
+  if (error) throw error
+  console.error(report(file))
+  file.extname = '.html'
+  vfile.writeSync(file)
+})

…と修正します。 この例では入出力ファイルをソースにハードコードしているので…

$ node index.js
example.md: no issues found

となります。


Markdown のテキスト部分を取り出してチェック

最後に僕的には興味深い機能ですが…
Markdown ファイルのテキスト部分だけを取り出して 構文チェックをする例も紹介されてます。

--- ../Reporting/index.js    2020-08-20 14:31:16.000000000 +0900
+++ index.js  2020-08-20 15:45:39.000000000 +0900
@@ -4,12 +4,16 @@
 var markdown = require('remark-parse')
 var slug = require('remark-slug')
 var toc = require('remark-toc')
+var remark2retext = require('remark-retext')
+var english = require('retext-english')
+var indefiniteArticle = require('retext-indefinite-article')
 var remark2rehype = require('remark-rehype')
 var doc = require('rehype-document')
 var html = require('rehype-stringify')
 
 var processor = unified()
   .use(markdown)
+  .use(remark2retext, unified().use(english).use(indefiniteArticle))
   .use(slug)
   .use(toc)
   .use(remark2rehype)

…と修正して、実行すると…

$ node index.js
example.md
  7:1-7:2  warning  Use `An` before `example`, not `A`  retext-indefinite-article  retext-indefinite-article

⚠ 1 warning

…と「example の冠詞は An ですよ」とワーニングが表示されます。


まとめ

…とまぁ、本稿では Remark のチュートリアルにあるサンプルを紹介しました。

どうやら Remark は JavascriptPromise を活用して、 Markdown ドキュメントを対象に(Unixのパイプのように) 様々な機能ブロックを継ぎ足して処理を行うツールのようです*3

次回は機能ブロックを実現している Plugin について調べてみたいと思います。

以上

*1:2019年2月から1年半の間に30本ですので、 月刊連載の頃に比べれば大幅にペースアップしてますが、 当初の目標だった月2本のペースには届いていません。 ちなみに『Unix考古学』は連載26ヶ月分だったので、 分量的には1冊分は書いてることになるのですが、 今の草稿は各回の内容が散漫過ぎるので これをベースに直ちに単行本を執筆するのはなかなか厳しそうです。

*2:"Unified Handbook" なるドキュメントも見つけましたが…

github.com

これまた "This is a work in progress" なんだそうです。

*3:このビルディング・ブロックを継ぎ足す機能は Unified フレームワークが 提供しているのでしょうが…