notebook

都内でWEB系エンジニアやってます。

mdastとjs-yamlを使ってすでに存在するMarkdownファイルのfrontmatterに項目の追加や修正をする

前回の続き

前回はMarkdownの内容をASTに変換してAST内のコンテンツを追加編集するものだったが、今回はMarkdown内のFrontmatterに対して追加や修正を行う

ケースとしてはぼちぼちあると思うので残しておく

frontmatterの判定

micromark-extension-frontmatter,mdast-util-frontmatterを読み込んでfrontmatterをパースできるようにする

syntax-tree/mdast-util-frontmatter: mdast extensions to parse and serialize frontmatter (YAML, TOML, etc)

ほぼREADME通り

自分のケースではtomlは不要だったので除いた

この拡張を含めてパースするとNodeタイプがtype: yamlで判断されるようになる

あと書き戻した場合にもしっかりfrontmatterの体をなした状態で書き出せる

この状態でNodeがfrontmatterか?というのは判断できるようになったが、valueはただの文字列のまま

Markdown内での記述とfrontmatterNodeの中身

  • Markdown内での記述
hoge: 1
fuga: 2
  • frontmatter Nodeの中身
    {
      type: 'yaml',
      value: 'hoge: 1\nfuga: 2',
      position: {
        start: { line: 1, column: 1, offset: 0 },
        end: { line: 4, column: 4, offset: 23 }
      }
    },

形式はyamlになっているので今度はjs-yamlで文字列をパースしてデータの中身をいじれるようにする

frontmatterNodeの読み込みと値の書き換え

  • 実行前
---
hoge: 1
fuga: 2
---

次のようなfrontmatterのMarkdownファイルに対して何かしら追加してみる

一部抜粋

import yaml from 'js-yaml';

const frontmatterIndex = ast.children.findIndex(node => node.type === "yaml");
const frontmatterNode = ast.children[frontmatterIndex];
const metadata = yaml.load(frontmatterNode.value);

const updatedMetadata = {...metadata, ...{added: 'AddedValue!!'}};
const frontmatterAst = {...frontMatterNode, ...{value: yaml.dump(newFrontmatter)}};

const children = [
  ...ast.children.slice(0,frontmatterIndex),
  frontmatterAst,
  ...ast.children.slice(frontmatterIndex + 1)
];

const afterAst = { ...ast, ...{children}};

frontmatterのNodeを特定し、valueの中身をyaml.loadでパースして中身を編集してyaml.dumpで文字列に戻してあげる

このサンプルコードはfrontmatterが既存のMarkdownに存在する前提

  • 実行後
---
hoge: 1
fuga: 2
added: AddedValue!!
---

しっかり追加される

編集は既存にあるキーを指定してデータをセットしてあげるだけ

フォーマットのコントロール

前提として、Markdownの修正というからには既存のフォーマットにできるだけ合わせたい

自分の使っている書き方だとデフォルトの挙動からいくつか修正が必要だったので調べた

nodeca/js-yaml: JavaScript YAML parser and dumper. Very fast.

js-yamlのREADMEに色々と使い方やオプションの説明が載っているので基本的にはそこ読みながらで進められる

frontmatter修正の流れ

おさらいとしてざっくり処理の流れ

  • mdastでMarkdownファイルのASTを生成
  • AST内のfrontmatterのNodeの値をjs-yamlでパースしオブジェクトに変換
  • オブジェクトの中身を修正もしくは追加
  • 修正したオブジェクトをyaml.dumpし、値をfrontmatterのNodeの値(node.value)に設定
  • 修正したNodeをASTに含める
  • mdastでMarkdownファイルへ書き出し

以降は主に2,4番目の話

日付にクオートをつけない

デフォルトの挙動で素直に書き戻すと次のような差分が出てしまう

- date: 2023-08-01
+ date: '2023-08-01'

optionのschemaでいくつか指定できるがJSON_SCHEMAを指定することで回避した

const metadata = yaml.load(frontmatterNode.value, { schema: yaml.JSON_SCHEMA });

これはYAMLフォーマットから変換するときに特定のフォーマットの文字列は内部的にdateや他のデータ型で扱うというルールがあるよう

今回はdateのフォーマットだった、なので出力時にクオートをつけるという仕様になっているみたい

JSON_SCHEMAはJSONのstringify,parseなどと同じ仕様で読み書きするというschemaのよう

日付のフォーマットだったとしても文字列としてそのまま読み書きするためクオートがつかなくなる

変換時の規則を指定するためのものなので今回のようにパースして書き戻すような場合は読み(load)書き(dump)両方で指定する必要がある

試しにload時指定しなかった場合、次のようなエラーになってしまった

file:///home/user/gh/self/markdown-importer/node_modules/js-yaml/dist/js-yaml.mjs:3689
      throw new exception('unacceptable kind of an object to dump ' + type);
            ^
YAMLException: unacceptable kind of an object to dump [object Date]
    at writeNode (file:///home/user/gh/self/markdown-importer/node_modules/js-yaml/dist/js-yaml.mjs:3689:13)
    at writeBlockMapping (file:///home/user/gh/self/markdown-importer/node_modules/js-yaml/dist/js-yaml.mjs:3552:10)
    at writeNode (file:///home/user/gh/self/markdown-importer/node_modules/js-yaml/dist/js-yaml.mjs:3655:9)
    at Object.dump$1 [as dump] (file:///home/user/gh/self/markdown-importer/node_modules/js-yaml/dist/js-yaml.mjs:3781:7)
    at file:///home/user/gh/self/markdown-importer/sample_yaml.js:43:38
    at Array.map (<anonymous>)
    at file:///home/user/gh/self/markdown-importer/sample_yaml.js:33:31
    at ModuleJob.run (node:internal/modules/esm/module_job:194:25) {
  reason: 'unacceptable kind of an object to dump [object Date]',
  mark: undefined
}

load時はDateオブジェクトだねって解釈したがdump時はそのまま出力っていわれて「えええっ!!!」っていわれている

よく考えたら逆のパターンはもしかしたらエラーにならないかもしれない、試してないけど

空文字にクオートをつけない

Markdownファイル上では特に何も記入しなかった場合

デフォルトの挙動で素直に書き戻すと次のような差分が出てしまう

- hoge:
+ hoge: ''

こちらはdump時のstylesオプションで対応する

nodeca/js-yaml: JavaScript YAML parser and dumper. Very fast.

書き出し(dump)時にstylesオプションで特定のケースに対してどのような処理を行うかを選択する

  • frontmatterのNodeAST生成時の処理(抜粋)
const frontmatterAst = {
  type: "yaml",
  value: yaml.dump(updatedMetadata, {
    schema: yaml.JSON_SCHEMA,
    styles: {
      '!!null': 'empty'
    },
  }),
}

現状の設定だと空文字は内部的にnullと判断されているためnullの場合はemptyで出力するという指定をする

emptyはクオートつけないというスタイルのよう

  • 結果
hoge:

となり、勝手にクオートがつかなくなった

READMEにnullの他のパターン、null以外のint,bool,floatのパターンも載っている

nodeca/js-yaml: JavaScript YAML parser and dumper. Very fast.

まとめ

これで今回のユースケースでは満たせそう

  • js-yamlでfrontmatterの中身をパースしてキーの追加や上書きをできるようにした
  • フォーマットをコントロールしたい場合はjs-yamlのオプションでコントロールする
    • stylesで指定
    • schemaで指定

ちょうど下記でMarkdown内のfrontmatter部分だけ修正したいケースが発生し、スクリプトを書いたので参考程度にはなるはず

markdown-importer/modify_daily_note_frontmatter.js at main · swfz/markdown-importer