Remark(3)remark-retext: プラグイン・チェインの接続点
藤田昭人
前回、
ファイルから Markdown データを読み込み(remark-parse
)、
Text データへと変換したのち(remark-retext
)、
文字列化する(retext-stringify
)プログラムを紹介しました。
各々が生成する構文木( mdast
とnlcst
)を表示するプラグインを作って
データ構造を確認してみましたが、
Markdown データの構文木はパラグラフ単位であるのに対し、
Text データの構文木はトークン単位で管理していることがわかりました。
本稿では mdast
からnlcst
、
すなわち Markdown から Text への変換を行っている
remark-retext について深堀りしてみます。
というのも、この remark-retext のどこかに、 パラグラフ・トークン変換を行うトークナイザーが組み込まれているはずだからです。 ご存知のとおり、単語間に空白が存在する英文のトークナイザーは単純に実装できますが、 日本文の場合には形態素解析が必要となります。 形態素解析機そのものは MeCab などのオープンソース・ソフトウェアが流通してますが、 それを Remark に組み込んで日本文を含むドキュメントでも Remark を齟齬なく動作させるには、 Remark でのパラグラフ -- トークン変換の仕組みを理解しておく必要があります。
つまり、本稿は Remark 日本語化の第1段階ということになります。
unist-util-inspect:構文木をコンパクトに
本論に入るまでに Tips をひとつ。
構文木のデバッグ・プリントは非常に冗長な表示になってしましますが、
unist-util-inspect
を使うと多少マシになります。
例えばこんな感じ…
var inspect = require('unist-util-inspect'); function attacher0() { return transformer; function transformer(tree, file) { console.log("\n### remark"); console.log(inspect(tree)); } } function attacher1() { return transformer; function transformer(tree, file) { console.log("\n### retext"); console.log(inspect(tree)); } } function attacher2() { return transformer; function transformer(tree, file) { console.log("\n### after retext-pos"); console.log(inspect(tree)); } } var unified = require('unified'); var parse = require('remark-parse'); var remark2retext = require('remark-retext'); var english = require('parse-english'); var stringify = require('retext-stringify'); var vfile = require('to-vfile'); var report = require('vfile-reporter'); var pos = require('retext-pos'); var processor = unified() .use(parse) .use(attacher0) .use(remark2retext, english) .use(attacher1) .use(pos) .use(attacher2) .use(stringify); processor.process(vfile.readSync('english.md'), done); function done(err, file) { //console.error(report(err || file)); //console.log(String(file)); }
このソースを動かすには必要なパッケージをインストールしてもらって…
$ npm install unist-util-inspect unified remark-parse remark-retext parse-english retext-stringify to-vfile vfile-reporter retext-pos ・・・ $ node example.js
…このソースの実行例は長いので 末尾 に掲載します。
ソース自体は
前回
紹介したものと基本的には同じですが、プラグイン・チェインに
品詞タグを付与するretext-pos
を追加しています。詳細は
末尾
の実行例を見てもらうとわかると思います。
remark-retext:Markdown から Text への変換
それでは本題のremark-retext
を紹介しましょう。
このプラグインが用意しているAPIは次に示すエントリーだけです。
origin.use(remark2retext, destination[, options])
remark(mdast)プラグインをbridgeするか、 あるいはretext(nlcst)にmutate します。
destination
destination はパーサまたはプロセッサのいずれかです。
Unified
プロセッサが与えられた場合、 新しい nlcst ツリーで destination プロセッサを実行し、 実行後にそのツリーを破棄して元のツリーで元のプロセッサを実行し続けます (bridge モード)。パーサー (
parse-latin
、parse-english
、parse-dutch
など)が指定されている場合、 構文木を他のプラグインに渡します (mutate モード)。
この「destination はパーサーまたはプロセッサのいずれかです」が分かりにくいのですが…
つまり remark-retext は、単純な構文木の変換を行うmutate モードと、 プロセッサーを新たに生成しプラグイン・チェインを分岐させる bridge モードの 2つのモードをサポートしているということのようです。
実は上記の3種類のパーサー parse-latin
、parse-english
、parse-dutch
には各々、
プラグイン形式(すなわち use を使ってプラグイン・チェインに登録できる形式)
retext-latin
、retext-english
、retext-dutch
も用意されています(前回を参照)。
mutate モードで使用する場合には第2引数の destination にパーサーそのものを渡して構文木の変換のみを行いますが、 bridge モードで使用する場合には新たなプロセッサーを生成して 第2引数の destination に渡し新たなプラグイン・チェインを生成します。 その場合は processor.use にプラグイン形式のパーサーを設定します。
例えば、前述のソースはmutate モードを使ってますが、 これをbridge モードに書き直すと…
var unified = require('unified') var markdown = require('remark-parse') var remark2retext = require('remark-retext') var english = require('retext-english') var stringify = require('retext-stringify'); var inspect = require('unist-util-inspect'); function attacher() { return transformer; function transformer(tree, file) { console.log(inspect(tree)); } } var processor = unified() .use(markdown) .use(remark2retext, unified() .use(english) .use(attacher)) .use(stringify); processor.process(vfile.readSync('example.md'), function (error, file) { });
…といった感じになります。
ソースコード
幾つかテスト・プログラムを書いて上記の仕様を確認したのですが、 結局プラグインの間で引き渡される構文木の扱い方がよくわからなかったので remark-retext のソースコードをあたってみました。 このプラグインの実装は非常にコンパクトなので、 以下に全文を引用します。
'use strict' var mdast2nlcst = require('mdast-util-to-nlcst') module.exports = remark2retext // Attacher. // If a destination processor is given, runs the destination // with the new nlcst tree (bridge mode). // If a parser is given, returns the nlcst tree: // further plugins run on that tree (mutate mode). function remark2retext(destination, options) { var fn = destination && destination.run ? bridge : mutate return fn(destination, options) } // Mutate mode. // Further transformers run on the nlcst tree. function mutate(parser, options) { return transformer function transformer(node, file) { return mdast2nlcst(node, file, parser, options) } } // Bridge mode. // Runs the destination with the new nlcst tree. function bridge(destination, options) { return transformer function transformer(node, file, next) { var Parser = destination.freeze().Parser var tree = mdast2nlcst(node, file, Parser, options) destination.run(tree, file, done) function done(err) { next(err) } } }
ソースコード から分かることは以下の3点です。
- mdast構文木からnlcst構文木への複製は
mdast-util-to-nlcst
で実装されている。 - mutate モードの場合、複製後は nlcst 構文木がプラグイン・チェインを伝搬し、mdast 構文木は直ちに破棄される。
- bridge モードの場合、複製後もmdast 構文木が元のプラグイン・チェインを伝搬し、 nlcst 構文木は新たに生成されたプラグイン・チェインを伝搬する。
どうやら remark-retext は mdast 構文木からnlcst 構文木への複製を行う
mdast-util-to-nlcst を unified フレームワークに組み入れるインターフェースのようです。
次は、構文木の複製を実装している mdast-util-to-nlcst を追っかけてみます。
以上
$ node example.js ### remark root[2] (1:1-4:1, 0-140) ├─0 heading[1] (1:1-1:24, 0-23) │ │ depth: 1 │ └─0 text "in the United States" (1:3-1:23, 2-22) └─1 paragraph[1] (3:1-3:115, 25-139) └─0 text "The project will cost between $5,000 and $30,000 dollars, depending on how fancy you want the final product to be." (3:1-3:115, 25-139) ### retext RootNode[3] (1:3-3:115, 2-139) ├─0 ParagraphNode[1] (1:3-1:23, 2-22) │ └─0 SentenceNode[7] (1:3-1:23, 2-22) │ ├─0 WordNode[1] (1:3-1:5, 2-4) │ │ └─0 TextNode "in" (1:3-1:5, 2-4) │ ├─1 WhiteSpaceNode " " (1:5-1:6, 4-5) │ ├─2 WordNode[1] (1:6-1:9, 5-8) │ │ └─0 TextNode "the" (1:6-1:9, 5-8) │ ├─3 WhiteSpaceNode " " (1:9-1:10, 8-9) │ ├─4 WordNode[1] (1:10-1:16, 9-15) │ │ └─0 TextNode "United" (1:10-1:16, 9-15) │ ├─5 WhiteSpaceNode " " (1:16-1:17, 15-16) │ └─6 WordNode[1] (1:17-1:23, 16-22) │ └─0 TextNode "States" (1:17-1:23, 16-22) ├─1 WhiteSpaceNode "\n\n" (1:24-3:1, 23-25) └─2 ParagraphNode[1] (3:1-3:115, 25-139) └─0 SentenceNode[47] (3:1-3:115, 25-139) ├─0 WordNode[1] (3:1-3:4, 25-28) │ └─0 TextNode "The" (3:1-3:4, 25-28) ├─1 WhiteSpaceNode " " (3:4-3:5, 28-29) ├─2 WordNode[1] (3:5-3:12, 29-36) │ └─0 TextNode "project" (3:5-3:12, 29-36) ├─3 WhiteSpaceNode " " (3:12-3:13, 36-37) ├─4 WordNode[1] (3:13-3:17, 37-41) │ └─0 TextNode "will" (3:13-3:17, 37-41) ├─5 WhiteSpaceNode " " (3:17-3:18, 41-42) ├─6 WordNode[1] (3:18-3:22, 42-46) │ └─0 TextNode "cost" (3:18-3:22, 42-46) ├─7 WhiteSpaceNode " " (3:22-3:23, 46-47) ├─8 WordNode[1] (3:23-3:30, 47-54) │ └─0 TextNode "between" (3:23-3:30, 47-54) ├─9 WhiteSpaceNode " " (3:30-3:31, 54-55) ├─10 SymbolNode "$" (3:31-3:32, 55-56) ├─11 WordNode[1] (3:32-3:33, 56-57) │ └─0 TextNode "5" (3:32-3:33, 56-57) ├─12 PunctuationNode "," (3:33-3:34, 57-58) ├─13 WordNode[1] (3:34-3:37, 58-61) │ └─0 TextNode "000" (3:34-3:37, 58-61) ├─14 WhiteSpaceNode " " (3:37-3:38, 61-62) ├─15 WordNode[1] (3:38-3:41, 62-65) │ └─0 TextNode "and" (3:38-3:41, 62-65) ├─16 WhiteSpaceNode " " (3:41-3:42, 65-66) ├─17 SymbolNode "$" (3:42-3:43, 66-67) ├─18 WordNode[1] (3:43-3:45, 67-69) │ └─0 TextNode "30" (3:43-3:45, 67-69) ├─19 PunctuationNode "," (3:45-3:46, 69-70) ├─20 WordNode[1] (3:46-3:49, 70-73) │ └─0 TextNode "000" (3:46-3:49, 70-73) ├─21 WhiteSpaceNode " " (3:49-3:50, 73-74) ├─22 WordNode[1] (3:50-3:57, 74-81) │ └─0 TextNode "dollars" (3:50-3:57, 74-81) ├─23 PunctuationNode "," (3:57-3:58, 81-82) ├─24 WhiteSpaceNode " " (3:58-3:59, 82-83) ├─25 WordNode[1] (3:59-3:68, 83-92) │ └─0 TextNode "depending" (3:59-3:68, 83-92) ├─26 WhiteSpaceNode " " (3:68-3:69, 92-93) ├─27 WordNode[1] (3:69-3:71, 93-95) │ └─0 TextNode "on" (3:69-3:71, 93-95) ├─28 WhiteSpaceNode " " (3:71-3:72, 95-96) ├─29 WordNode[1] (3:72-3:75, 96-99) │ └─0 TextNode "how" (3:72-3:75, 96-99) ├─30 WhiteSpaceNode " " (3:75-3:76, 99-100) ├─31 WordNode[1] (3:76-3:81, 100-105) │ └─0 TextNode "fancy" (3:76-3:81, 100-105) ├─32 WhiteSpaceNode " " (3:81-3:82, 105-106) ├─33 WordNode[1] (3:82-3:85, 106-109) │ └─0 TextNode "you" (3:82-3:85, 106-109) ├─34 WhiteSpaceNode " " (3:85-3:86, 109-110) ├─35 WordNode[1] (3:86-3:90, 110-114) │ └─0 TextNode "want" (3:86-3:90, 110-114) ├─36 WhiteSpaceNode " " (3:90-3:91, 114-115) ├─37 WordNode[1] (3:91-3:94, 115-118) │ └─0 TextNode "the" (3:91-3:94, 115-118) ├─38 WhiteSpaceNode " " (3:94-3:95, 118-119) ├─39 WordNode[1] (3:95-3:100, 119-124) │ └─0 TextNode "final" (3:95-3:100, 119-124) ├─40 WhiteSpaceNode " " (3:100-3:101, 124-125) ├─41 WordNode[1] (3:101-3:108, 125-132) │ └─0 TextNode "product" (3:101-3:108, 125-132) ├─42 WhiteSpaceNode " " (3:108-3:109, 132-133) ├─43 WordNode[1] (3:109-3:111, 133-135) │ └─0 TextNode "to" (3:109-3:111, 133-135) ├─44 WhiteSpaceNode " " (3:111-3:112, 135-136) ├─45 WordNode[1] (3:112-3:114, 136-138) │ └─0 TextNode "be" (3:112-3:114, 136-138) └─46 PunctuationNode "." (3:114-3:115, 138-139) ### after retext-pos RootNode[3] (1:3-3:115, 2-139) ├─0 ParagraphNode[1] (1:3-1:23, 2-22) │ └─0 SentenceNode[7] (1:3-1:23, 2-22) │ ├─0 WordNode[1] (1:3-1:5, 2-4) │ │ │ data: {"partOfSpeech":"IN"} │ │ └─0 TextNode "in" (1:3-1:5, 2-4) │ ├─1 WhiteSpaceNode " " (1:5-1:6, 4-5) │ ├─2 WordNode[1] (1:6-1:9, 5-8) │ │ │ data: {"partOfSpeech":"DT"} │ │ └─0 TextNode "the" (1:6-1:9, 5-8) │ ├─3 WhiteSpaceNode " " (1:9-1:10, 8-9) │ ├─4 WordNode[1] (1:10-1:16, 9-15) │ │ │ data: {"partOfSpeech":"VBN"} │ │ └─0 TextNode "United" (1:10-1:16, 9-15) │ ├─5 WhiteSpaceNode " " (1:16-1:17, 15-16) │ └─6 WordNode[1] (1:17-1:23, 16-22) │ │ data: {"partOfSpeech":"NNPS"} │ └─0 TextNode "States" (1:17-1:23, 16-22) ├─1 WhiteSpaceNode "\n\n" (1:24-3:1, 23-25) └─2 ParagraphNode[1] (3:1-3:115, 25-139) └─0 SentenceNode[47] (3:1-3:115, 25-139) ├─0 WordNode[1] (3:1-3:4, 25-28) │ │ data: {"partOfSpeech":"DT"} │ └─0 TextNode "The" (3:1-3:4, 25-28) ├─1 WhiteSpaceNode " " (3:4-3:5, 28-29) ├─2 WordNode[1] (3:5-3:12, 29-36) │ │ data: {"partOfSpeech":"NN"} │ └─0 TextNode "project" (3:5-3:12, 29-36) ├─3 WhiteSpaceNode " " (3:12-3:13, 36-37) ├─4 WordNode[1] (3:13-3:17, 37-41) │ │ data: {"partOfSpeech":"MD"} │ └─0 TextNode "will" (3:13-3:17, 37-41) ├─5 WhiteSpaceNode " " (3:17-3:18, 41-42) ├─6 WordNode[1] (3:18-3:22, 42-46) │ │ data: {"partOfSpeech":"NN"} │ └─0 TextNode "cost" (3:18-3:22, 42-46) ├─7 WhiteSpaceNode " " (3:22-3:23, 46-47) ├─8 WordNode[1] (3:23-3:30, 47-54) │ │ data: {"partOfSpeech":"IN"} │ └─0 TextNode "between" (3:23-3:30, 47-54) ├─9 WhiteSpaceNode " " (3:30-3:31, 54-55) ├─10 SymbolNode "$" (3:31-3:32, 55-56) ├─11 WordNode[1] (3:32-3:33, 56-57) │ │ data: {"partOfSpeech":"CD"} │ └─0 TextNode "5" (3:32-3:33, 56-57) ├─12 PunctuationNode "," (3:33-3:34, 57-58) ├─13 WordNode[1] (3:34-3:37, 58-61) │ │ data: {"partOfSpeech":"CD"} │ └─0 TextNode "000" (3:34-3:37, 58-61) ├─14 WhiteSpaceNode " " (3:37-3:38, 61-62) ├─15 WordNode[1] (3:38-3:41, 62-65) │ │ data: {"partOfSpeech":"CC"} │ └─0 TextNode "and" (3:38-3:41, 62-65) ├─16 WhiteSpaceNode " " (3:41-3:42, 65-66) ├─17 SymbolNode "$" (3:42-3:43, 66-67) ├─18 WordNode[1] (3:43-3:45, 67-69) │ │ data: {"partOfSpeech":"CD"} │ └─0 TextNode "30" (3:43-3:45, 67-69) ├─19 PunctuationNode "," (3:45-3:46, 69-70) ├─20 WordNode[1] (3:46-3:49, 70-73) │ │ data: {"partOfSpeech":"CD"} │ └─0 TextNode "000" (3:46-3:49, 70-73) ├─21 WhiteSpaceNode " " (3:49-3:50, 73-74) ├─22 WordNode[1] (3:50-3:57, 74-81) │ │ data: {"partOfSpeech":"NNS"} │ └─0 TextNode "dollars" (3:50-3:57, 74-81) ├─23 PunctuationNode "," (3:57-3:58, 81-82) ├─24 WhiteSpaceNode " " (3:58-3:59, 82-83) ├─25 WordNode[1] (3:59-3:68, 83-92) │ │ data: {"partOfSpeech":"VBG"} │ └─0 TextNode "depending" (3:59-3:68, 83-92) ├─26 WhiteSpaceNode " " (3:68-3:69, 92-93) ├─27 WordNode[1] (3:69-3:71, 93-95) │ │ data: {"partOfSpeech":"IN"} │ └─0 TextNode "on" (3:69-3:71, 93-95) ├─28 WhiteSpaceNode " " (3:71-3:72, 95-96) ├─29 WordNode[1] (3:72-3:75, 96-99) │ │ data: {"partOfSpeech":"WRB"} │ └─0 TextNode "how" (3:72-3:75, 96-99) ├─30 WhiteSpaceNode " " (3:75-3:76, 99-100) ├─31 WordNode[1] (3:76-3:81, 100-105) │ │ data: {"partOfSpeech":"JJ"} │ └─0 TextNode "fancy" (3:76-3:81, 100-105) ├─32 WhiteSpaceNode " " (3:81-3:82, 105-106) ├─33 WordNode[1] (3:82-3:85, 106-109) │ │ data: {"partOfSpeech":"PRP"} │ └─0 TextNode "you" (3:82-3:85, 106-109) ├─34 WhiteSpaceNode " " (3:85-3:86, 109-110) ├─35 WordNode[1] (3:86-3:90, 110-114) │ │ data: {"partOfSpeech":"VBP"} │ └─0 TextNode "want" (3:86-3:90, 110-114) ├─36 WhiteSpaceNode " " (3:90-3:91, 114-115) ├─37 WordNode[1] (3:91-3:94, 115-118) │ │ data: {"partOfSpeech":"DT"} │ └─0 TextNode "the" (3:91-3:94, 115-118) ├─38 WhiteSpaceNode " " (3:94-3:95, 118-119) ├─39 WordNode[1] (3:95-3:100, 119-124) │ │ data: {"partOfSpeech":"JJ"} │ └─0 TextNode "final" (3:95-3:100, 119-124) ├─40 WhiteSpaceNode " " (3:100-3:101, 124-125) ├─41 WordNode[1] (3:101-3:108, 125-132) │ │ data: {"partOfSpeech":"NN"} │ └─0 TextNode "product" (3:101-3:108, 125-132) ├─42 WhiteSpaceNode " " (3:108-3:109, 132-133) ├─43 WordNode[1] (3:109-3:111, 133-135) │ │ data: {"partOfSpeech":"TO"} │ └─0 TextNode "to" (3:109-3:111, 133-135) ├─44 WhiteSpaceNode " " (3:111-3:112, 135-136) ├─45 WordNode[1] (3:112-3:114, 136-138) │ │ data: {"partOfSpeech":"VB"} │ └─0 TextNode "be" (3:112-3:114, 136-138) └─46 PunctuationNode "." (3:114-3:115, 138-139) $