markdown で記述されたテキストをパースしてあれこれしたい場合。 markdown-to-ast が便利そうなので、使ってみた。 これはすごい便利。
2022-04-04 更新: commonmark 編を書きました。
$ mkdir mast
$ cd mast
$ npm init -y
依存するライブラリをインストール:
$ npm install @textlint/markdown-to-ast
$ npm install underscore
index.js を書く:
const parse = require("@textlint/markdown-to-ast").parse;
const markdownText = "It's a *text*.";
const ast = parse(markdownText)
console.log(ast);
実行する:
$ node index.js
標準出力:
{
type: 'Document',
children: [
{
type: 'Paragraph',
children: [Array],
loc: [Object],
range: [Array],
raw: "It's a *text*."
}
],
loc: { start: { line: 1, column: 0 }, end: { line: 1, column: 14 } },
range: [ 0, 14 ],
raw: "It's a *text*."
}
与えたマークダウンテキストがASTとしてオブジェクト化されました。
次に、2つのパラグラフのあるマークダウンテキストをパースしてみます。
Hello, World!
Hello, *World*!
index.js :
const parse = require("@textlint/markdown-to-ast").parse;
const markdownList = [];
markdownList.push("Hello, World!");
markdownList.push("");
markdownList.push("Hello, *World*!");
const markdownText = markdownList.join("\n");
const ast = parse(markdownText)
console.log(ast);
実行:
{
type: 'Document',
children: [
{
type: 'Paragraph',
children: [Array],
loc: [Object],
range: [Array],
raw: 'Hello, World!'
},
{
type: 'Paragraph',
children: [Array],
loc: [Object],
range: [Array],
raw: 'Hello, *World*!'
}
],
loc: { start: { line: 1, column: 0 }, end: { line: 3, column: 15 } },
range: [ 0, 30 ],
raw: 'Hello, World!\n\nHello, *World*!'
}
children の部分で2つの Paragraph が出現しています。
次に、 parseDoc という ドキュメント構造を調べる関数を書きます。
const _ = require('underscore');
const parseDoc = (ast)=> {
console.log("- " + ast.type);
if( ast.type == 'Paragraph' ){
_.each(ast.children, (it)=>{
console.log(" - " + it.type);
});
}
else {
_.each(ast.children, (it)=>{ parseDoc(it); });
}
};
const markdownList = [];
markdownList.push("Hello, World!");
markdownList.push("");
markdownList.push("Hello, *World*!");
const markdownText = markdownList.join("\n");
const parse = require("@textlint/markdown-to-ast").parse;
const ast = parse(markdownText)
parseDoc(ast);
実行する:
$ node index.js
- Document
- Paragraph
- Str
- Paragraph
- Str
- Emphasis
- Str
ドキュメントの中にパラグラフが2つあり、パラグラフの中に Str や Emphasis が出現する、という雰囲気がわかりました。
これを XMLというか HTML っぽい出力にしてみましょう。
const _ = require('underscore');
const parseEmp = (node, list)=> {
if( node.type == 'Str' ){
list.push('<str>')
list.push(node.value);
list.push('</str>')
}
};
const parseStrOrEmp = (node, list)=> {
if( node.type == 'Str' ){
list.push('<str>')
list.push(node.value);
list.push('</str>')
}
else if( node.type == 'Emphasis' ){
list.push('<emp>')
_.each(node.children, (it)=>{ parseEmp(it, list); });
list.push('</emp>')
}
};
const parsePara = (node, list)=> {
if( node.type == 'Paragraph' ){
list.push("<p>");
_.each(node.children, (it)=>{ parseStrOrEmp(it, list); });
list.push("</p>");
}
};
const parseDoc = (node, list)=> {
if( node.type == 'Document' ){
list.push("<doc>");
_.each(node.children, (it)=>{ parsePara(it, list); });
list.push("</doc>");
}
};
const markdownList = [];
markdownList.push("Hello, World!");
markdownList.push("");
markdownList.push("Hello, *World*!");
const markdownText = markdownList.join("\n");
const parse = require("@textlint/markdown-to-ast").parse;
const ast = parse(markdownText)
const list = [];
parseDoc(ast, list);
console.log( list.join('') );
を前提としてパースする関数を作成した。
実行すると以下の文字列が得られます:
<doc><p><str>Hello, World!</str></p><p><str>Hello, </str><emp><str>World</str></emp><str>!</str></p></doc>
コードを良く見てみると同じコードを繰り返しているだけなので、そこをまとめる形でリファクタリングした。
const _ = require('underscore');
const parseNode = (node)=> {
// _.foldl で使う関数:
const f = (acc, subNode) => {
return acc + parseNode(subNode);
};
if( node.type == 'Document' ){
return '<doc>' + _.foldl(node.children, f, '') + '</doc>';
}
else if( node.type == 'Paragraph' ){
return '<p>' + _.foldl(node.children, f, '') + '</p>';
}
else if( node.type == 'Emphasis' ){
return '<emp>' + _.foldl(node.children, f, '') + '</emp>';
}
else if( node.type == 'Str' ){
return '<str>' + node.value + '</str>';
}
};
const markdownList = [];
markdownList.push("Hello, World!");
markdownList.push("");
markdownList.push("Hello, *World*!");
const markdownText = markdownList.join("\n");
const parse = require("@textlint/markdown-to-ast").parse;
const ast = parse(markdownText)
const xml = parseNode(ast);
console.log( xml );
parseNode 関数で再帰的に AST を調べて適切な文字列を出力している。
Document, Paragraph, Emphasis はコンテナ要素なのでサブノードがある(children が存在)。 したがって、 foldl で子要素が作り出す文字列を全部足し合わせている。
Str はリーフ要素(末端)で文字列が node.value に入っているだけなので、それを文字列として返すだけ。
実行した結果:
<doc><p><str>Hello, World!</str></p><p><str>Hello, </str><emp><str>World</str></emp><str>!</str></p></doc>
当たり前だが先ほどと同じ結果になる。
単にmarkdownテキストをHTMLに変換したいだけならば かならずしも markdow-to-ast を使う必要はありませんが、 自在に出力をカスタマイズしたい場合には重宝します。