markdown-it-digit 1.1.0で文書全体の数値を自動変換する

markdown-it-digit 1.1.0では、専用記法で指定した数字だけでなく、Markdown文書に普通に書かれた数字をまとめて変換できるようにしました。

これまでの基本構文は、数字ごとにlocaleを明示する形式でした。

Markdown
$123456789${jp}

この書き方は、変換する数字とlocaleがその場で分かる点では明確です。しかし、1つの文書で同じlocaleを使い続ける場合、すべての数字へ専用記法を書くのは手間がかかります。

そこで1.1.0では、文書全体のデフォルトlocaleを設定できる仕組みを追加しました。

目次

実現したかったこと

今回の出発点は、MarkdownのYAML Front Matterで文書ごとのlocaleを指定する構想でした。

YAML
---
markdown:
  digit:
    locale: jp
    minDigits: 4
---

この設定があるとき、本文は専用記法を使わずに書けます。

Markdown
売上は123456789円でした。

日本語形式で変換すると、HTMLは次のようになります。

HTML
売上は1<sub></sub>2345<sub></sub>6789円でした。

ただし、markdown-it-digit 自体へYAML解析を組み込む設計にはしませんでした。YAMLを理解するのはVS Code拡張機能などの呼び出し側とし、プラグインは渡された設定だけを見る構成にしています。

新たに実装した機能

1. YAMLではなくoptionsとenvを受け取る

markdown-it-digit 1.1.0は、設定を2つの経路から受け取ります。

1つ目はplugin optionsです。markdown-it-digit を単体で使用し、同じmarkdown-itインスタンスへ共通設定を適用する場合に使います。

JavaScript
const MarkdownIt = require("markdown-it");
const markdownItDigit = require("markdown-it-digit");

const md = new MarkdownIt().use(markdownItDigit, {
    locale: "jp",
    minDigits: 4
});

2つ目はmarkdown-itの env です。レンダリングする文書ごとに設定を変えられます。

JavaScript
md.render(source, {
    markdownDigit: {
        locale: "en",
        minDigits: 5
    }
});

この分離により、プラグインは設定がYAML、VS Code設定、あるいは別の仕組みから来たものかを知る必要がありません。呼び出し側が最終的な文書設定を env.markdownDigit として渡せば利用できます。

設定の優先順位は次のようにしました。

Markdown
本文の明示locale
>
env.markdownDigit
>
plugin options
>
内部既定値

env にない項目はplugin optionsから補います。例えばlocaleだけを文書単位で変更し、minDigits はインスタンス共通の設定を使うこともできます。

2. minDigitsで自動変換の対象を決める

minDigits は、自動変換へ渡す最小桁数です。既定値は4で、正の整数を指定します。

JavaScript
const md = new MarkdownIt().use(markdownItDigit, {
    locale: "en",
    minDigits: 4
});

md.renderInline("999 1000 1234567");
// 999 1,000 1,234,567

日本語の万進法では、4桁の 1000 をformatterへ渡しても表示は変わりません。一方、5桁の 10000 からは万の単位が入ります。minDigits は「必ず見た目が変わる桁数」ではなく、「変換処理の対象にする桁数」を表します。

不正な minDigits が指定された場合は、既定値の4を使用します。対応していないlocaleやlocale未設定の場合は自動変換を有効にしません。そのため、従来どおりオプションなしで登録した利用者の動作は変わりません。

3. 特定の数字だけ変換しないraw指定

文書全体を自動変換すると、一部の番号だけ元の数字で表示したい場面が出てきます。そのため、明示記法専用の予約値として raw を追加しました。

Markdown
通常: 123456789
変換しない: $123456789${raw}
英語形式: $123456789${en}

文書のデフォルトlocaleが jp の場合、結果は次のようになります。

HTML
通常: 1<sub></sub>2345<sub></sub>6789
変換しない: 123456789
英語形式: 123,456,789

raw は記法部分を取り除き、先頭ゼロを含む元の数字をそのまま出力します。文書全体のデフォルトlocaleとして使う値ではないため、plugin optionsや env.markdownDigit.locale に指定しても自動変換は有効になりません。

実装の検討過程

単純な文字列置換にしなかった理由

「4桁以上の数字を探して変換する」だけなら、Markdown全体を正規表現で置換する方法も考えられます。しかし、それではURL、コード、HTML属性など、本来変更してはいけない数字まで書き換えてしまいます。

今回もmarkdown-itが解析したトークンを利用し、通常のインラインテキストだけを対象にしました。次の領域は自動変換しません。

自動変換されない数字
  • fenced code blockとインデントされたcode block
  • インラインコード
  • URLと明示的なURI scheme
  • Markdownリンクのリンク先と自動リンク
  • HTML属性とHTMLコメント
  • バックスラッシュでエスケープした専用記法

リンクでは、表示テキストの数字は対象になりますが、リンク先URLの数字は変更しません。この区別もMarkdownの構造を解析しているから可能になります。

inline ruleだけでは通常数字を拾えなかった

実装中に重要な問題が1つ発生しました。

既存の専用記法は $ から始まるため、独自のinline ruleで検出できます。同じ方法で普通の数字も拾おうとしましたが、markdown-itの通常テキスト規則が数字を含む文字列を先にまとめて消費し、自動変換用の規則が数字の位置で呼ばれませんでした。

最初のテストでは、次の入力がまったく変換されませんでした。

HTML
999 1000 1234567

そこで、自動変換はinline解析の後に実行するcore ruleへ切り替えました。core ruleで text トークンだけを調べ、対象となる数字の部分を既存の digit トークンへ分割します。

Markdown
inline解析

通常テキストトークンを確認

対象数字をdigitトークンへ分割

既存rendererとlocale formatterで描画

この構成なら、既存のlocale formatterとrendererをそのまま共用できます。また、コードやリンク先などは最初から別種類のトークンになっているため、自動変換の対象から外せます。

エスケープされた専用記法については、markdown-itが生成する text_special トークンとの並びも確認し、自動変換を有効にしていても記法内の数字だけが変換されないようにしました。

実文書を使った確認

単体・統合テストに加えて、見出し、複数の段落、明示locale、リンク、インラインコード、コードブロックを含むMarkdown文書をファイルから読み込む確認も行いました。

部分的な文字列一致だけでなく、レンダリングした文書全体を期待するHTMLと完全一致で比較しています。実際の変換結果をHTMLファイルへ出力し、目視でも確認できるようにしました。

最終的な自動テストは96件です。

  • npm test: 96件成功
  • npm run lint: 成功
  • npm pack --dry-run: 成功

npm pack --dry-run では、バージョン1.1.0、14ファイル、圧縮後約8.0 kBであることを確認しました。最初の実行ではローカルのnpmキャッシュへ書き込めず権限エラーになりましたが、必要な権限で再実行してパッケージ内容を確認できました。

まとめ

今回の変更で、markdown-it-digit は数字ごとの明示記法に加え、文書全体のデフォルトlocaleを扱えるようになりました。

v1.1.0 で実装した機能
  • plugin optionsでmarkdown-itインスタンス全体へ設定する
  • env.markdownDigit で文書ごとに設定する
  • minDigits で自動変換を開始する桁数を指定する
  • 明示localeで特定の数字だけ別形式にする
  • raw で特定の数字だけ変換しない
  • 従来の除外範囲とオプションなしの動作を維持する

YAML Front Matter自体を解析する責務は呼び出し側へ残したため、npmパッケージ単体でも、VS Code拡張機能からでも同じAPIを利用できます。文書設定の取得方法と数値変換を分離しつつ、用途を広げられたリリースになりました。

この記事が参考になりましたら、
上記の「いいね」を押していただけると嬉しいです😄
今後の記事作りの励みになります!
目次