Remark(3)remark-retext: プラグイン・チェインの接続点

remark-retext: connection points for plugin chains


2020/08/30
藤田昭人


前回、 ファイルから Markdown データを読み込み(remark-parse)、 Text データへと変換したのち(remark-retext)、 文字列化する(retext-stringify)プログラムを紹介しました。 各々が生成する構文木mdastnlcst)を表示するプラグインを作って データ構造を確認してみましたが、 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])

remarkmdastプラグインbridgeするか、 あるいはretextnlcst)にmutate します。

destination

destination はパーサまたはプロセッサのいずれかです。

Unified プロセッサが与えられた場合、 新しい nlcst ツリーで destination プロセッサを実行し、 実行後にそのツリーを破棄して元のツリーで元のプロセッサを実行し続けます (bridge モード)。

パーサー (parse-latinparse-englishparse-dutch など)が指定されている場合、 構文木を他のプラグインに渡します (mutate モード)。

この「destination はパーサーまたはプロセッサのいずれかです」が分かりにくいのですが…

つまり remark-retext は、単純な構文木の変換を行うmutate モードと、 プロセッサーを新たに生成しプラグイン・チェインを分岐させる bridge モードの 2つのモードをサポートしているということのようです。

実は上記の3種類のパーサー parse-latinparse-englishparse-dutch には各々、 プラグイン形式(すなわち use を使ってプラグイン・チェインに登録できる形式) retext-latinretext-englishretext-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点です。


どうやら remark-retextmdast 構文木から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)
$