Remark(2)プラグイン の作り方
藤田昭人
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 2 ⚠ 1 warning
このプログラムは「文と文の間の空白をチェックし、空白1文字でなければワーニングメッセージを表示」します。プラグインのデモンストレーション以上の意味は無さそうです。
プラグインのインターフェースについて
JavaScriptは引数の記述の自由度プラグインのインターフェース仕様がきになったので、調べてみました。Remark のプラグインのインターフェースはUnifiedのプラグインの項目で確認することができます。
プラグインのインターフェースに関連することだけを次に抜粋しておきます。
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
による 実行フェーズ は処理されます。 これらの関数の正確なセマンティクスについては、そのドキュメントを参照してください。パラメーター
リターン
function next(err[, tree[, file]])
transformer のシグネチャに
next
(第三引数) が含まれている場合、 transformer は非同期操作を行うことができます。 その場合はnext()
を呼び出す必要があります。パラメーター
どうやら構文木とファイルがセットで引き渡って来るようです。動的に確保され自由度の高い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 } } } $