Remark(2)プラグイン の作り方

Creating a plugin with unified


2020/08/27
藤田昭人


Remark の話を続けます。

ビルディング・ブロックを積み上げて所望の機能を得る Remark では、 やはりプラグインを作らなければ、 その有り難みを本格的に感じることはできないのだろうなぁ…と思います。

ということで、本稿ではプラグインの作り方を調べてみました。


Creating a plugin with unified

Remark のプラグインの作り方は Creating a plugin with unified で紹介されています。

が、このチュートリアルも、 ステップ・バイ・ステップで進行するので少々まどろっこしいところがありますので、 その要点を網羅して1本のソースにまとめました。 見ての通り、前半がプラグイン、後半がそれを呼び出す本体のコードです。

/* 
 * 以降がプラグイン
 */
var visit = require('unist-util-visit');
var is = require('unist-util-is');

function attacher() {
    return transformer;

  function transformer(tree, file) {
    visit(tree, 'ParagraphNode', visitor);

    function visitor(node) {
      var children = node.children;

      children.forEach(function(child, index) {
        if (is(children[index - 1], 'SentenceNode') &&
            is(child, 'WhiteSpaceNode') &&
            is(children[index + 1], 'SentenceNode')) {
          if (child.value.length !== 1) {
              file.message('Expected 1 space between sentences, not '
                           +child.value.length, child);
          }
        }
      });
    }
  }
}

var spacing = attacher;

/* 
 * 以降が本体
 */

var fs = require('fs');
var retext = require('retext');
var report = require('vfile-reporter');

var doc = fs.readFileSync('example.md');

retext()
  .use(spacing)
  .process(doc, function(err, file) {
    console.error(report(err || file));
  });

実行すると…

$ node example.js
3:14-3:16  warning  Expected 1 space between sentences, not 21 warning

このプログラムは「文と文の間の空白をチェックし、空白1文字でなければワーニングメッセージを表示」します。プラグインのデモンストレーション以上の意味は無さそうです。


プラグインのインターフェースについて

JavaScriptは引数の記述の自由度プラグインのインターフェース仕様がきになったので、調べてみました。Remark のプラグインのインターフェースはUnifiedのプラグインの項目で確認することができます。

プラグインのインターフェースに関連することだけを次に抜粋しておきます。

プラグイン は、以下の方法で適用されるプロセッサを設定します。

プラグインはコンセプトです。 それらはattacherとして具現化します。


function attacher([options])

Attacher は実体化されたプラグインです。 attacher は、オプションを受け取り、プロセッサを設定することができる関数です。

Attacherは、パーサコンパイラ、設定データ構文木ファイルの処理方法を指定することで、 プロセッサを変更します。

コンテキスト

コンテキスト・オブジェクト(this)は、attacher が適用されているプロセッサに設定されます。

パラメーター
  • options (*, optional) - コンフィグレーション
リターン

transformer - オプション

ノート

Attachers are called when the processor is frozen, not when they are applied.

Attacherは、プロセッサがフリーズしたときに呼び出されるのであって、 適用されたときに呼び出されるのではありません。


function transformer(node, file[, next])

Transformersは、構文木ファイルを処理します。 transformer は、構文木とファイルが実行フェーズに渡されるたびに呼び出される関数です。 エラーが発生した場合(thrown、returned、rejected、passed to next、のいずれかの理由で) プロセスは停止します。

trough による 実行フェーズ は処理されます。 これらの関数の正確なセマンティクスについては、そのドキュメントを参照してください。

パラメーター
リターン
  • void — 何も返されない場合,次の変換器は同じ木を使い続けます.
  • Error — 致命的なエラーでプロセスを停止します。
  • node (Node) — 新しい構文木。 返された場合、次の変換器にはこの新しい木が与えられます。
  • Promise — 非同期操作を実行するために返されます。 promiseは(オプションでNodeを使って)解決されるか、 (オプションでErrorを使って)拒否されなければなりません


function next(err[, tree[, file]])

transformerシグネチャnext (第三引数) が含まれている場合、 transformer は非同期操作を行うことができます。 その場合は next() を呼び出す必要があります

パラメーター
  • err (Error, optional) — プロセスを停止するための致命的なエラー。
  • node (Node, optional) — 新しい構文木。 与えられた場合,次の変換器にはこの新しいツリーが与えられます。
  • file (VFile, optional) — 新しい[ファイル]file]。 与えられた場合,次のtransformerにはこの新しいファイルが与えられます。

どうやら構文木ファイルがセットで引き渡って来るようです。動的に確保され自由度の高いJavaScriptの配列を使った構文木は、もう少し調べてみないと…


構文木についてもう少し深掘り

という事で次のようなデバッグプリントみたいなプラグインを作ってみました。

var visit = require('unist-util-visit')

function attacher() {
    return transformer;
    function transformer(tree, file) {
    visit(tree, visitor);
    function visitor(node) {
        console.log(node);
    }
    }
}

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 processor = unified()
    .use(parse)
    .use(attacher)
    .use(remark2retext, english)
    .use(stringify);

processor.process(vfile.readSync('english.md'), done);

function done(err, file) {
    //console.error(report(err || file));
    //console.log(String(file));
}

入力ファイルenglish.mdも用意して…

# in the United States

The project will cost between $5,000 and $30,000 dollars, depending on how fancy you want the final product to be.

…実行してみました。

$ node example.js
{ type: 'root',
  children:
   [ { type: 'heading',
       depth: 1,
       children: [Array],
       position: [Position] },
     { type: 'paragraph', children: [Array], position: [Position] } ],
  position:
   { start: { line: 1, column: 1, offset: 0 },
     end: { line: 4, column: 1, offset: 140 } } }
{ type: 'heading',
  depth: 1,
  children:
   [ { type: 'text',
       value: 'in the United States',
       position: [Position] } ],
  position:
   Position {
     start: { line: 1, column: 1, offset: 0 },
     end: { line: 1, column: 24, offset: 23 },
     indent: [] } }
{ type: 'text',
  value: 'in the United States',
  position:
   Position {
     start: { line: 1, column: 3, offset: 2 },
     end: { line: 1, column: 23, offset: 22 },
     indent: [] } }
{ type: 'paragraph',
  children:
   [ { type: 'text',
       value:
        'The project will cost between $5,000 and $30,000 dollars, depending on how fancy you want the final product to be.',
       position: [Position] } ],
  position:
   Position {
     start: { line: 3, column: 1, offset: 25 },
     end: { line: 3, column: 115, offset: 139 },
     indent: [] } }
{ type: 'text',
  value:
   'The project will cost between $5,000 and $30,000 dollars, depending on how fancy you want the final product to be.',
  position:
   Position {
     start: { line: 3, column: 1, offset: 25 },
     end: { line: 3, column: 115, offset: 139 },
     indent: [] } }
$

Markdownの記法に従って、テキストを切り出してくれるようです。プラグインを挿入するポイントをズラしてみます。

--- example.js-  2020-08-28 08:24:40.000000000 +0900
+++ example.js    2020-08-28 08:24:50.000000000 +0900
@@ -20,8 +20,8 @@

 var processor = unified()
     .use(parse)
-    .use(attacher)
     .use(remark2retext, english)
+    .use(attacher)
     .use(stringify);

 processor.process(vfile.readSync('english.md'), done);

このプログラムを実行すると retext に変換された構文木をダンプすることができます (トークン化された長い構文木が出力されるので末尾に回します)。 テキスト部分がトークン化された長大なツリーが表示されました。が、もちろん、これは英文の場合。 日本文の場合は形態素解析を組み込まないといけませんねぇ。

以上



いや、いきなりトークン化された構文木が出力されたので、 思わず「長げ〜よ」ってツッコンでしまった。

$ node example.js
{ type: 'RootNode',
  children:
   [ { type: 'ParagraphNode', children: [Array], position: [Object] },
     { type: 'WhiteSpaceNode', value: '\n\n', position: [Object] },
     { type: 'ParagraphNode', children: [Array], position: [Object] } ],
  position:
   { start: { line: 1, column: 3, offset: 2 },
     end: { line: 3, column: 115, offset: 139 } } }
{ type: 'ParagraphNode',
  children:
   [ { type: 'SentenceNode', children: [Array], position: [Object] } ],
  position:
   { start: { line: 1, column: 3, offset: 2 },
     end: { line: 1, column: 23, offset: 22 } } }
{ type: 'SentenceNode',
  children:
   [ { type: 'WordNode', children: [Array], position: [Object] },
     { type: 'WhiteSpaceNode', value: ' ', position: [Object] },
     { type: 'WordNode', children: [Array], position: [Object] },
     { type: 'WhiteSpaceNode', value: ' ', position: [Object] },
     { type: 'WordNode', children: [Array], position: [Object] },
     { type: 'WhiteSpaceNode', value: ' ', position: [Object] },
     { type: 'WordNode', children: [Array], position: [Object] } ],
  position:
   { start: { line: 1, column: 3, offset: 2 },
     end: { line: 1, column: 23, offset: 22 } } }
{ type: 'WordNode',
  children: [ { type: 'TextNode', value: 'in', position: [Object] } ],
  position:
   { start: { line: 1, column: 3, offset: 2 },
     end: { line: 1, column: 5, offset: 4 } } }
{ type: 'TextNode',
  value: 'in',
  position:
   { start: { line: 1, column: 3, offset: 2 },
     end: { line: 1, column: 5, offset: 4 } } }
{ type: 'WhiteSpaceNode',
  value: ' ',
  position:
   { start: { line: 1, column: 5, offset: 4 },
     end: { line: 1, column: 6, offset: 5 } } }
{ type: 'WordNode',
  children: [ { type: 'TextNode', value: 'the', position: [Object] } ],
  position:
   { start: { line: 1, column: 6, offset: 5 },
     end: { line: 1, column: 9, offset: 8 } } }
{ type: 'TextNode',
  value: 'the',
  position:
   { start: { line: 1, column: 6, offset: 5 },
     end: { line: 1, column: 9, offset: 8 } } }
{ type: 'WhiteSpaceNode',
  value: ' ',
  position:
   { start: { line: 1, column: 9, offset: 8 },
     end: { line: 1, column: 10, offset: 9 } } }
{ type: 'WordNode',
  children:
   [ { type: 'TextNode', value: 'United', position: [Object] } ],
  position:
   { start: { line: 1, column: 10, offset: 9 },
     end: { line: 1, column: 16, offset: 15 } } }
{ type: 'TextNode',
  value: 'United',
  position:
   { start: { line: 1, column: 10, offset: 9 },
     end: { line: 1, column: 16, offset: 15 } } }
{ type: 'WhiteSpaceNode',
  value: ' ',
  position:
   { start: { line: 1, column: 16, offset: 15 },
     end: { line: 1, column: 17, offset: 16 } } }
{ type: 'WordNode',
  children:
   [ { type: 'TextNode', value: 'States', position: [Object] } ],
  position:
   { start: { line: 1, column: 17, offset: 16 },
     end: { line: 1, column: 23, offset: 22 } } }
{ type: 'TextNode',
  value: 'States',
  position:
   { start: { line: 1, column: 17, offset: 16 },
     end: { line: 1, column: 23, offset: 22 } } }
{ type: 'WhiteSpaceNode',
  value: '\n\n',
  position:
   { start: { line: 1, column: 24, offset: 23 },
     end: { line: 3, column: 1, offset: 25 } } }
{ type: 'ParagraphNode',
  children:
   [ { type: 'SentenceNode', children: [Array], position: [Object] } ],
  position:
   { start: { line: 3, column: 1, offset: 25 },
     end: { line: 3, column: 115, offset: 139 } } }
{ type: 'SentenceNode',
  children:
   [ { type: 'WordNode', children: [Array], position: [Object] },
     { type: 'WhiteSpaceNode', value: ' ', position: [Object] },
     { type: 'WordNode', children: [Array], position: [Object] },
     { type: 'WhiteSpaceNode', value: ' ', position: [Object] },
     { type: 'WordNode', children: [Array], position: [Object] },
     { type: 'WhiteSpaceNode', value: ' ', position: [Object] },
     { type: 'WordNode', children: [Array], position: [Object] },
     { type: 'WhiteSpaceNode', value: ' ', position: [Object] },
     { type: 'WordNode', children: [Array], position: [Object] },
     { type: 'WhiteSpaceNode', value: ' ', position: [Object] },
     { type: 'SymbolNode', value: '$', position: [Object] },
     { type: 'WordNode', children: [Array], position: [Object] },
     { type: 'PunctuationNode', value: ',', position: [Object] },
     { type: 'WordNode', children: [Array], position: [Object] },
     { type: 'WhiteSpaceNode', value: ' ', position: [Object] },
     { type: 'WordNode', children: [Array], position: [Object] },
     { type: 'WhiteSpaceNode', value: ' ', position: [Object] },
     { type: 'SymbolNode', value: '$', position: [Object] },
     { type: 'WordNode', children: [Array], position: [Object] },
     { type: 'PunctuationNode', value: ',', position: [Object] },
     { type: 'WordNode', children: [Array], position: [Object] },
     { type: 'WhiteSpaceNode', value: ' ', position: [Object] },
     { type: 'WordNode', children: [Array], position: [Object] },
     { type: 'PunctuationNode', value: ',', position: [Object] },
     { type: 'WhiteSpaceNode', value: ' ', position: [Object] },
     { type: 'WordNode', children: [Array], position: [Object] },
     { type: 'WhiteSpaceNode', value: ' ', position: [Object] },
     { type: 'WordNode', children: [Array], position: [Object] },
     { type: 'WhiteSpaceNode', value: ' ', position: [Object] },
     { type: 'WordNode', children: [Array], position: [Object] },
     { type: 'WhiteSpaceNode', value: ' ', position: [Object] },
     { type: 'WordNode', children: [Array], position: [Object] },
     { type: 'WhiteSpaceNode', value: ' ', position: [Object] },
     { type: 'WordNode', children: [Array], position: [Object] },
     { type: 'WhiteSpaceNode', value: ' ', position: [Object] },
     { type: 'WordNode', children: [Array], position: [Object] },
     { type: 'WhiteSpaceNode', value: ' ', position: [Object] },
     { type: 'WordNode', children: [Array], position: [Object] },
     { type: 'WhiteSpaceNode', value: ' ', position: [Object] },
     { type: 'WordNode', children: [Array], position: [Object] },
     { type: 'WhiteSpaceNode', value: ' ', position: [Object] },
     { type: 'WordNode', children: [Array], position: [Object] },
     { type: 'WhiteSpaceNode', value: ' ', position: [Object] },
     { type: 'WordNode', children: [Array], position: [Object] },
     { type: 'WhiteSpaceNode', value: ' ', position: [Object] },
     { type: 'WordNode', children: [Array], position: [Object] },
     { type: 'PunctuationNode', value: '.', position: [Object] } ],
  position:
   { start: { line: 3, column: 1, offset: 25 },
     end: { line: 3, column: 115, offset: 139 } } }
{ type: 'WordNode',
  children: [ { type: 'TextNode', value: 'The', position: [Object] } ],
  position:
   { start: { line: 3, column: 1, offset: 25 },
     end: { line: 3, column: 4, offset: 28 } } }
{ type: 'TextNode',
  value: 'The',
  position:
   { start: { line: 3, column: 1, offset: 25 },
     end: { line: 3, column: 4, offset: 28 } } }
{ type: 'WhiteSpaceNode',
  value: ' ',
  position:
   { start: { line: 3, column: 4, offset: 28 },
     end: { line: 3, column: 5, offset: 29 } } }
{ type: 'WordNode',
  children:
   [ { type: 'TextNode', value: 'project', position: [Object] } ],
  position:
   { start: { line: 3, column: 5, offset: 29 },
     end: { line: 3, column: 12, offset: 36 } } }
{ type: 'TextNode',
  value: 'project',
  position:
   { start: { line: 3, column: 5, offset: 29 },
     end: { line: 3, column: 12, offset: 36 } } }
{ type: 'WhiteSpaceNode',
  value: ' ',
  position:
   { start: { line: 3, column: 12, offset: 36 },
     end: { line: 3, column: 13, offset: 37 } } }
{ type: 'WordNode',
  children: [ { type: 'TextNode', value: 'will', position: [Object] } ],
  position:
   { start: { line: 3, column: 13, offset: 37 },
     end: { line: 3, column: 17, offset: 41 } } }
{ type: 'TextNode',
  value: 'will',
  position:
   { start: { line: 3, column: 13, offset: 37 },
     end: { line: 3, column: 17, offset: 41 } } }
{ type: 'WhiteSpaceNode',
  value: ' ',
  position:
   { start: { line: 3, column: 17, offset: 41 },
     end: { line: 3, column: 18, offset: 42 } } }
{ type: 'WordNode',
  children: [ { type: 'TextNode', value: 'cost', position: [Object] } ],
  position:
   { start: { line: 3, column: 18, offset: 42 },
     end: { line: 3, column: 22, offset: 46 } } }
{ type: 'TextNode',
  value: 'cost',
  position:
   { start: { line: 3, column: 18, offset: 42 },
     end: { line: 3, column: 22, offset: 46 } } }
{ type: 'WhiteSpaceNode',
  value: ' ',
  position:
   { start: { line: 3, column: 22, offset: 46 },
     end: { line: 3, column: 23, offset: 47 } } }
{ type: 'WordNode',
  children:
   [ { type: 'TextNode', value: 'between', position: [Object] } ],
  position:
   { start: { line: 3, column: 23, offset: 47 },
     end: { line: 3, column: 30, offset: 54 } } }
{ type: 'TextNode',
  value: 'between',
  position:
   { start: { line: 3, column: 23, offset: 47 },
     end: { line: 3, column: 30, offset: 54 } } }
{ type: 'WhiteSpaceNode',
  value: ' ',
  position:
   { start: { line: 3, column: 30, offset: 54 },
     end: { line: 3, column: 31, offset: 55 } } }
{ type: 'SymbolNode',
  value: '$',
  position:
   { start: { line: 3, column: 31, offset: 55 },
     end: { line: 3, column: 32, offset: 56 } } }
{ type: 'WordNode',
  children: [ { type: 'TextNode', value: '5', position: [Object] } ],
  position:
   { start: { line: 3, column: 32, offset: 56 },
     end: { line: 3, column: 33, offset: 57 } } }
{ type: 'TextNode',
  value: '5',
  position:
   { start: { line: 3, column: 32, offset: 56 },
     end: { line: 3, column: 33, offset: 57 } } }
{ type: 'PunctuationNode',
  value: ',',
  position:
   { start: { line: 3, column: 33, offset: 57 },
     end: { line: 3, column: 34, offset: 58 } } }
{ type: 'WordNode',
  children: [ { type: 'TextNode', value: '000', position: [Object] } ],
  position:
   { start: { line: 3, column: 34, offset: 58 },
     end: { line: 3, column: 37, offset: 61 } } }
{ type: 'TextNode',
  value: '000',
  position:
   { start: { line: 3, column: 34, offset: 58 },
     end: { line: 3, column: 37, offset: 61 } } }
{ type: 'WhiteSpaceNode',
  value: ' ',
  position:
   { start: { line: 3, column: 37, offset: 61 },
     end: { line: 3, column: 38, offset: 62 } } }
{ type: 'WordNode',
  children: [ { type: 'TextNode', value: 'and', position: [Object] } ],
  position:
   { start: { line: 3, column: 38, offset: 62 },
     end: { line: 3, column: 41, offset: 65 } } }
{ type: 'TextNode',
  value: 'and',
  position:
   { start: { line: 3, column: 38, offset: 62 },
     end: { line: 3, column: 41, offset: 65 } } }
{ type: 'WhiteSpaceNode',
  value: ' ',
  position:
   { start: { line: 3, column: 41, offset: 65 },
     end: { line: 3, column: 42, offset: 66 } } }
{ type: 'SymbolNode',
  value: '$',
  position:
   { start: { line: 3, column: 42, offset: 66 },
     end: { line: 3, column: 43, offset: 67 } } }
{ type: 'WordNode',
  children: [ { type: 'TextNode', value: '30', position: [Object] } ],
  position:
   { start: { line: 3, column: 43, offset: 67 },
     end: { line: 3, column: 45, offset: 69 } } }
{ type: 'TextNode',
  value: '30',
  position:
   { start: { line: 3, column: 43, offset: 67 },
     end: { line: 3, column: 45, offset: 69 } } }
{ type: 'PunctuationNode',
  value: ',',
  position:
   { start: { line: 3, column: 45, offset: 69 },
     end: { line: 3, column: 46, offset: 70 } } }
{ type: 'WordNode',
  children: [ { type: 'TextNode', value: '000', position: [Object] } ],
  position:
   { start: { line: 3, column: 46, offset: 70 },
     end: { line: 3, column: 49, offset: 73 } } }
{ type: 'TextNode',
  value: '000',
  position:
   { start: { line: 3, column: 46, offset: 70 },
     end: { line: 3, column: 49, offset: 73 } } }
{ type: 'WhiteSpaceNode',
  value: ' ',
  position:
   { start: { line: 3, column: 49, offset: 73 },
     end: { line: 3, column: 50, offset: 74 } } }
{ type: 'WordNode',
  children:
   [ { type: 'TextNode', value: 'dollars', position: [Object] } ],
  position:
   { start: { line: 3, column: 50, offset: 74 },
     end: { line: 3, column: 57, offset: 81 } } }
{ type: 'TextNode',
  value: 'dollars',
  position:
   { start: { line: 3, column: 50, offset: 74 },
     end: { line: 3, column: 57, offset: 81 } } }
{ type: 'PunctuationNode',
  value: ',',
  position:
   { start: { line: 3, column: 57, offset: 81 },
     end: { line: 3, column: 58, offset: 82 } } }
{ type: 'WhiteSpaceNode',
  value: ' ',
  position:
   { start: { line: 3, column: 58, offset: 82 },
     end: { line: 3, column: 59, offset: 83 } } }
{ type: 'WordNode',
  children:
   [ { type: 'TextNode', value: 'depending', position: [Object] } ],
  position:
   { start: { line: 3, column: 59, offset: 83 },
     end: { line: 3, column: 68, offset: 92 } } }
{ type: 'TextNode',
  value: 'depending',
  position:
   { start: { line: 3, column: 59, offset: 83 },
     end: { line: 3, column: 68, offset: 92 } } }
{ type: 'WhiteSpaceNode',
  value: ' ',
  position:
   { start: { line: 3, column: 68, offset: 92 },
     end: { line: 3, column: 69, offset: 93 } } }
{ type: 'WordNode',
  children: [ { type: 'TextNode', value: 'on', position: [Object] } ],
  position:
   { start: { line: 3, column: 69, offset: 93 },
     end: { line: 3, column: 71, offset: 95 } } }
{ type: 'TextNode',
  value: 'on',
  position:
   { start: { line: 3, column: 69, offset: 93 },
     end: { line: 3, column: 71, offset: 95 } } }
{ type: 'WhiteSpaceNode',
  value: ' ',
  position:
   { start: { line: 3, column: 71, offset: 95 },
     end: { line: 3, column: 72, offset: 96 } } }
{ type: 'WordNode',
  children: [ { type: 'TextNode', value: 'how', position: [Object] } ],
  position:
   { start: { line: 3, column: 72, offset: 96 },
     end: { line: 3, column: 75, offset: 99 } } }
{ type: 'TextNode',
  value: 'how',
  position:
   { start: { line: 3, column: 72, offset: 96 },
     end: { line: 3, column: 75, offset: 99 } } }
{ type: 'WhiteSpaceNode',
  value: ' ',
  position:
   { start: { line: 3, column: 75, offset: 99 },
     end: { line: 3, column: 76, offset: 100 } } }
{ type: 'WordNode',
  children: [ { type: 'TextNode', value: 'fancy', position: [Object] } ],
  position:
   { start: { line: 3, column: 76, offset: 100 },
     end: { line: 3, column: 81, offset: 105 } } }
{ type: 'TextNode',
  value: 'fancy',
  position:
   { start: { line: 3, column: 76, offset: 100 },
     end: { line: 3, column: 81, offset: 105 } } }
{ type: 'WhiteSpaceNode',
  value: ' ',
  position:
   { start: { line: 3, column: 81, offset: 105 },
     end: { line: 3, column: 82, offset: 106 } } }
{ type: 'WordNode',
  children: [ { type: 'TextNode', value: 'you', position: [Object] } ],
  position:
   { start: { line: 3, column: 82, offset: 106 },
     end: { line: 3, column: 85, offset: 109 } } }
{ type: 'TextNode',
  value: 'you',
  position:
   { start: { line: 3, column: 82, offset: 106 },
     end: { line: 3, column: 85, offset: 109 } } }
{ type: 'WhiteSpaceNode',
  value: ' ',
  position:
   { start: { line: 3, column: 85, offset: 109 },
     end: { line: 3, column: 86, offset: 110 } } }
{ type: 'WordNode',
  children: [ { type: 'TextNode', value: 'want', position: [Object] } ],
  position:
   { start: { line: 3, column: 86, offset: 110 },
     end: { line: 3, column: 90, offset: 114 } } }
{ type: 'TextNode',
  value: 'want',
  position:
   { start: { line: 3, column: 86, offset: 110 },
     end: { line: 3, column: 90, offset: 114 } } }
{ type: 'WhiteSpaceNode',
  value: ' ',
  position:
   { start: { line: 3, column: 90, offset: 114 },
     end: { line: 3, column: 91, offset: 115 } } }
{ type: 'WordNode',
  children: [ { type: 'TextNode', value: 'the', position: [Object] } ],
  position:
   { start: { line: 3, column: 91, offset: 115 },
     end: { line: 3, column: 94, offset: 118 } } }
{ type: 'TextNode',
  value: 'the',
  position:
   { start: { line: 3, column: 91, offset: 115 },
     end: { line: 3, column: 94, offset: 118 } } }
{ type: 'WhiteSpaceNode',
  value: ' ',
  position:
   { start: { line: 3, column: 94, offset: 118 },
     end: { line: 3, column: 95, offset: 119 } } }
{ type: 'WordNode',
  children: [ { type: 'TextNode', value: 'final', position: [Object] } ],
  position:
   { start: { line: 3, column: 95, offset: 119 },
     end: { line: 3, column: 100, offset: 124 } } }
{ type: 'TextNode',
  value: 'final',
  position:
   { start: { line: 3, column: 95, offset: 119 },
     end: { line: 3, column: 100, offset: 124 } } }
{ type: 'WhiteSpaceNode',
  value: ' ',
  position:
   { start: { line: 3, column: 100, offset: 124 },
     end: { line: 3, column: 101, offset: 125 } } }
{ type: 'WordNode',
  children:
   [ { type: 'TextNode', value: 'product', position: [Object] } ],
  position:
   { start: { line: 3, column: 101, offset: 125 },
     end: { line: 3, column: 108, offset: 132 } } }
{ type: 'TextNode',
  value: 'product',
  position:
   { start: { line: 3, column: 101, offset: 125 },
     end: { line: 3, column: 108, offset: 132 } } }
{ type: 'WhiteSpaceNode',
  value: ' ',
  position:
   { start: { line: 3, column: 108, offset: 132 },
     end: { line: 3, column: 109, offset: 133 } } }
{ type: 'WordNode',
  children: [ { type: 'TextNode', value: 'to', position: [Object] } ],
  position:
   { start: { line: 3, column: 109, offset: 133 },
     end: { line: 3, column: 111, offset: 135 } } }
{ type: 'TextNode',
  value: 'to',
  position:
   { start: { line: 3, column: 109, offset: 133 },
     end: { line: 3, column: 111, offset: 135 } } }
{ type: 'WhiteSpaceNode',
  value: ' ',
  position:
   { start: { line: 3, column: 111, offset: 135 },
     end: { line: 3, column: 112, offset: 136 } } }
{ type: 'WordNode',
  children: [ { type: 'TextNode', value: 'be', position: [Object] } ],
  position:
   { start: { line: 3, column: 112, offset: 136 },
     end: { line: 3, column: 114, offset: 138 } } }
{ type: 'TextNode',
  value: 'be',
  position:
   { start: { line: 3, column: 112, offset: 136 },
     end: { line: 3, column: 114, offset: 138 } } }
{ type: 'PunctuationNode',
  value: '.',
  position:
   { start: { line: 3, column: 114, offset: 138 },
     end: { line: 3, column: 115, offset: 139 } } }
$