再帰的にMarkdownを処理

このウェブログではHTMLを直接ぺちぺち書くことが多いが、たまにiPhoneで書く時にはMarkdownで書いている。Markdownの処理系にはここ数年はmarkedを使っており(宗教上の理由でCommonMark系は使うことができない)、あまり困ってはいないのだが、sectionfigure要素を使うとその中のMarkdown文字列がうまく処理できないという問題がある。

例えばfigure要素を使ってtable要素を図としてマークアップしたいとする。その場合、markedでは表の記法が拡張されているので、以下のように書くことになる。

<figure>
|Name|Num|
|-|-|
|foo|128|
|bar|256|

<figcaption>表1</figcaption>
</figure>

このMarkdown文字列をmarkedで処理しても何も起きず、そのまま出てくる。つまり何らかのHTMLタグで括られた間はインライン系の記法しか処理されない。


markedには処理をフックする機構が用意されているので、それを使うとうまく処理することできる。こういったHTML文字列の場合、Renderer.htmlでそういった文字列を処理する関数を指定してやれば良い。

// node this.js < test.md
var marked = require("marked");
var renderer = new marked.Renderer();

function processHTML(html) {
	var attributes;
	var contents;
	var tag;
	var tags = ["aside", "figure", "section"];
	var tokens = html.trim().match(/^<(\w+)(.*?)>([\s\S]*)<\/\1>/);

	if (!tokens) {
		return html;
	}

	tag = tokens[1];

	if (tags.indexOf(tag) === -1) {
		return html;
	}

	attributes = tokens[2];
	contents = marked(tokens[3].replace(/&gt;/g, ">"), {
		renderer: renderer
	}).trim();

	return "<" + tag + attributes + ">\n" + contents + "\n</" + tag + ">\n";
}

renderer.html = processHTML;
console.log(marked(fs.readFileSync(process.stdin.fd, "utf8"), {
	renderer: renderer
}));

空白文字を削った一番外側のHTMLタグを拾い、その内容を更にmarkedで処理するようにしているわけだ。インライン記法は処理できるので、markedがサポートしていないfiguresectionaside要素のみを処理するようにしている。>の文字参照だけを解除しているのは、引用記法がうまく処理できなくなるからで、そもそもの正規表現も含め、この辺りはもうちょっとちゃんとした方が良さそうだ。

この関数を利用すると、最初に挙げたMarkdown文字列の例は以下のように期待通りに処理される(インデントは読みやすいように追加した)。

<figure>
	<table>
		<thead>
			<tr>
				<th>Name</th>
				<th>Num</th>
			</tr>
		</thead>
		<tbody>
			<tr>
				<td>foo</td>
				<td>128</td>
			</tr>
			<tr>
				<td>bar</td>
				<td>256</td>
			</tr>
		</tbody>
	</table>

	<figcaption>表1</figcaption>
</figure>

本当はこういったアドホックなプラグイン志向でMarkdownを拡張するのは好きではない。Markdownは拡張性なしの安定志向のもの(それこそオリジナルで良い)を使い、プリプロセッサーでtabledl要素の独自記法を処理して渡す方が良いと考えている。そうしてプリプロセッサーで処理済みのものをデータとして保存しておけば、ほぼあらゆるMarkdown処理系で正常に処理できることが期待できるからだ。