毎週、毎月ある程度テンプレート化された記事を公開する場合
毎週、毎月の変動するデータをもとに自動でテンプレートに当て込みMarkdownを生成したい
この処理をある程度自動で行えたら良いなと思い調べてやってみた
完結にするためテンプレートは下記のようなMarkdownとする
- sample1.md
## サブタイトル1 ## サブタイトル2 ## サブタイトル3
この手のことをやろうとした場合、自分の中で真っ先に選択肢として挙がったのがremarkだった
remarkを使っても良かったが
- MarkdownをparseしてASTに変換する
- ASTからMarkdownを生成する
の2パターンだけが必要だったのでremarkの内部で使っているmdast-util-from-markdown
,mdast-util-to-markdown
を使うことにした
- syntax-tree/mdast-util-to-markdown: mdast utility to serialize markdown
- syntax-tree/mdast-util-from-markdown: mdast utility to parse markdown
テーブル表示に関しては別途モジュールを追加する必要があった(mdast-util-gfm-table
)
install
yarn add mdast-util-from-markdown mdast-util-gfm-table micromark-extension-gfm-table
- package.json
"main": "sample.js",
+ "type": "module",
"scripts": {
サンプルのコードには載せていないが最終的にdynamic importを使ったためサンプルでも"type": "module"
を追加している
parse
MarkdownをASTに変換する処理
import * as fs from 'fs' import {fromMarkdown} from 'mdast-util-from-markdown' import {gfmTable} from 'micromark-extension-gfm-table' import {gfmTableFromMarkdown} from 'mdast-util-gfm-table' const markdown = fs.readFileSync('sample1.md'); const ast = fromMarkdown(markdown,{ extensions: [gfmTable], mdastExtensions: [gfmTableFromMarkdown] }); console.dir(ast, {depth: null})
ASTの内容
{ type: 'root', children: [ { type: 'heading', depth: 2, children: [ { type: 'text', value: 'サブタイトル1', position: { start: { line: 1, column: 4, offset: 3 }, end: { line: 1, column: 11, offset: 10 } } } ], position: { start: { line: 1, column: 1, offset: 0 }, end: { line: 1, column: 11, offset: 10 } } }, { type: 'heading', depth: 2, children: [ { type: 'text', value: 'サブタイトル2', position: { start: { line: 3, column: 4, offset: 15 }, end: { line: 3, column: 11, offset: 22 } } } ], position: { start: { line: 3, column: 1, offset: 12 }, end: { line: 3, column: 11, offset: 22 } } }, { type: 'heading', depth: 2, children: [ { type: 'text', value: 'サブタイトル3', position: { start: { line: 5, column: 4, offset: 27 }, end: { line: 5, column: 11, offset: 34 } } } ], position: { start: { line: 5, column: 1, offset: 24 }, end: { line: 5, column: 11, offset: 34 } } } ], position: { start: { line: 1, column: 1, offset: 0 }, end: { line: 7, column: 1, offset: 36 } } }
行、列、位置の情報までのっている
compile
ASTからMarkdownを生成する処理
サンプルでは上記で取得したASTからposition
の情報だけ削除して定義した
import * as fs from 'fs' import {toMarkdown} from 'mdast-util-to-markdown' import {gfmTableToMarkdown} from 'mdast-util-gfm-table' const ast = { type: 'root', children: [ { type: 'heading', depth: 2, children: [ { type: 'text', value: 'サブタイトル1', } ], }, { type: 'heading', depth: 2, children: [ { type: 'text', value: 'サブタイトル2', } ], }, { type: 'heading', depth: 2, children: [ { type: 'text', value: 'サブタイトル3', } ], } ], } fs.writeFileSync('sample2.md', toMarkdown(ast, {bullet: '-', extensions: [gfmTableToMarkdown()]}));
$ cat sample2.md ## サブタイトル1 ## サブタイトル2 ## サブタイトル3
bullet: '-'
をオプションで指定しているのは自分がいつもMarkdownでリストを書くときは*
ではなく-
を用いているため
他にもいじれるオプションがあるので下記を参考にすれば良い
syntax-tree/mdast-util-to-markdown: mdast utility to serialize markdown
ASTをいじる
import * as fs from 'fs' import {fromMarkdown} from 'mdast-util-from-markdown' import {toMarkdown} from 'mdast-util-to-markdown' import {gfmTable} from 'micromark-extension-gfm-table' import {gfmTableFromMarkdown, gfmTableToMarkdown} from 'mdast-util-gfm-table' const markdown = fs.readFileSync('sample1.md'); const ast = fromMarkdown(markdown,{ extensions: [gfmTable], mdastExtensions: [gfmTableFromMarkdown] }); const itemAst = [{ type: 'list', ordered: false, start: null, spread: false, children: [ { type: 'listItem', spread: false, checked: null, children: [ { type: 'paragraph', children: [ { type: 'text', value: 'list item1', } ], } ], }, { type: 'listItem', spread: false, checked: null, children: [ { type: 'paragraph', children: [ { type: 'text', value: 'list item2', } ], } ], }, { type: 'listItem', spread: false, checked: null, children: [ { type: 'paragraph', children: [ { type: 'text', value: 'list item3', } ], } ], } ], }]; const heading = 'サブタイトル2'; const children = ast.children.reduce((acc, item) => { if(item.type === 'heading' && item.children[0].value === heading) { return [...acc, item, ...itemAst]; } return [...acc, item]; }, []) const afterAst = {...ast, ...{children}} fs.writeFileSync('sample3.md', toMarkdown(afterAst, {bullet: '-', extensions: [gfmTableToMarkdown()]}));
特定Header(今回はサブタイトル2
)の直下に事前生成したASTを入れ込むことでコンテンツ追加を可能にした
- 実行結果
$ cat sample3.md ## サブタイトル1 ## サブタイトル2 - list item1 - list item2 - list item3 ## サブタイトル3
事前生成するASTをどうするかというのは今のところparseの項でASTを出力したように、こんな感じのMarkdownを生成したいというのをMarkdownで書いてからParseしてconsole.dirした結果をみてこういうASTにすればよいのねって感じで組んでいくという愚直な感じでやってみた
まとめ
- テンプレートをもとに、特定ヘッダ以下にあらかじめ生成したコンテンツを追加できた
- Markdownをmdastでparseした
- ASTからMarkdownの生成を行った
- ASTの中身を書き換える処理を書いた
コンテンツの生成はBigQueryなりJSONなりにデータをあらかじめ入れておいて時期によって値が変わるようにすればある程度定形な記事は自動化できる
自分は毎月やっているじぶんリリースノートの定形内容を出力できるようにした
自動で生成できる部分が増えれば今まで色々なレポートやサイトから引っ張ってきていた手作業をなくせるので効率が上がるはず、しばらく運用してみる
余談
自分のケースではMarkdown記事にfrontmatterを使用していないので使わなかったが、Markdownで記事書くのであればだいたいfrontmatter使っていると思われるので下記も使うことになりそう
また、今回調べていく中で
textlint -> remark -> unified -> mdast
といった感じで次々しらない単語が出てきて勉強にはなった(正直まだそんなに理解できてない)