Markdownに大きな数値を書くと、桁数をひと目で把握しにくいことがあります。たとえば 100000000 が1億だと分かっていても、文章の中でゼロを数えるのは少し面倒です。
そこで、数値に明示的な記法を加えると、英語式のカンマ区切りや日本語の万進法へ変換できるmarkdown-itプラグインを作りました。それが markdown-it-digit です。
この記事では使い方だけでなく、記法とPublic APIを決めた過程、Markdown内で変換してはいけない領域への対応、テスト、sandbox検証、そしてGitHub Actionsからnpmへ公開するまでを開発ログとしてまとめます。
Markdownの数値を読みやすくしたかった
最初に考えたのは、Markdown内の数値を読みやすく表示することでした。ただし、文書に含まれる数字をすべて自動変換すると、年、型番、コード、URLなど、書き換えてほしくないものまで変わる可能性があります。
そこで、通常の数値には触れず、変換したい場所だけを専用記法で指定する方針にしました。
$<number>${<locale>}たとえば、次の記法は英語式の3桁区切りになります。
$1234567${en}出力は次のとおりです。
1,234,567この形式なら、元のMarkdownを見ても「この数値は変換対象で、どの表記を使うのか」が分かります。通常の 1234567 はそのまま残るため、プラグインを導入しただけで既存文書の数字が一斉に変わることもありません。
インストールと基本的な使い方
パッケージはnpmからインストールできます。
npm install markdown-it-digitv1.0.0のPublic APIは、CommonJSのmarkdown-itプラグイン関数だけです。
const MarkdownIt = require("markdown-it");
const markdownItDigit = require("markdown-it-digit");
const md = new MarkdownIt().use(markdownItDigit);
console.log(md.render("$1234567${en}"));結果は次のHTMLになります。
<p>1,234,567</p>ESM用エントリポイント、plugin options、外部からlocaleを登録するAPIは用意していません。最初の公開版では機能を広げすぎず、プラグイン関数1つを安定した入口として扱うことにしました。
6種類の数値表記を実装する
v1.0.0で対応したlocale識別子は、en、in、jp、cn、kr、tw の6種類です。
英語式とインド式の桁区切り
en は、右から3桁ごとにカンマを入れます。
$123456789${en}123,456,789in は、右端だけを3桁にし、その左側を2桁ずつ区切ります。
$123456789${in}12,34,56,789同じカンマ区切りでも、グループ化の規則は異なります。そのため、parserで数値とlocaleを取り出し、localeごとのformatterへ渡す構成にしました。
東アジアの万進法
jp、cn、kr、tw は、右から4桁ずつ分割し、境界に単位を挿入します。
$123456789${jp}1<sub>億</sub>2345<sub>万</sub>67ほかの識別子では単位表記が変わります。
| 識別子 | 対象の表記 | 変換例 | 実際の見え方 |
|---|---|---|---|
jp | 日本語 | 1<sub>億</sub>2345<sub>万</sub>6789 | 1億2345万6789 |
cn | 簡体字中国語 | 1<sub>亿</sub>2345<sub>万</sub>6789 | 1亿2345万6789 |
kr | 韓国語 | 1<sub>억</sub>2345<sub>만</sub>6789 | 1억2345만6789 |
tw | 繁体字中国語 | 1<sub>億</sub>2345<sub>萬</sub>6789 | 1億2345萬6789 |
これらはISO言語コードやBCP 47言語タグではなく、このパッケージ独自のフォーマット識別子です。
実装では4桁分割の処理を共通化し、localeごとの差を単位テーブルとして持たせました。単位は万に相当する10の4乗から、無量大数に相当する10の68乗まで定義しています。
ここで重要なのは、数値を短縮しないことです。
$100000000${jp}これを単に「1億」とせず、次のように出力します。
1<sub>億</sub>0000<sub>万</sub>0000ゼロだけのグループも残し、単位は桁位置を確認するためのガイドとして挿入します。元の数字を欠落させないことを優先した設計です。
開発ポイント
1. 数値を文字列のまま処理する
parserが受け付ける数値は、1文字以上のASCII数字です。負数と小数はv1.0.0の対象にしていません。
解析後もJavaScriptの Number には変換せず、文字列のままformatterへ渡します。これには2つの理由があります。
1つ目は、最大安全整数を超える大きな数値で桁落ちを起こさないためです。2つ目は、先頭ゼロを入力の一部として保持するためです。
$0001000${en}0,001,000数値として計算するプラグインではなく、入力された桁を表記上グループ化するプラグインとして境界を決めました。
2. Markdownを壊さないための境界判定
開発で特にテストが増えたのは、専用記法を「どこで変換しないか」という部分でした。
Markdown全体に対して正規表現置換をかけるだけでは、コード例やURLまで変わってしまいます。そのため、markdown-itのinline ruleとして専用記法を認識し、独自tokenをrendererへ渡しています。
通常のインラインテキスト、強調内、Markdownリンクの表示テキスト、HTMLタグ間の通常テキストは変換対象です。一方、次の領域は変換しません。
- fenced code blockとインデントされたcode block
- インラインコード
- URLと明示的なURI scheme内
www.で始まるURL内- Markdownリンクのリンク先
- Markdownの自動リンク内
- HTML属性内
- HTMLを有効にした場合のHTMLコメント内
- バックスラッシュでエスケープした記法
たとえば、リンクの表示テキストは変換しても、リンク先は変換しません。
[$1000${en}](https://example.com/$1000${en})<a href="https://example.com/$1000${en}">1,000</a>URLらしい文字列を一律に処理するだけでなく、mailto:、tel:、独自の scheme: も保護します。一方、URI schemeを持たない docs/$1000${en} のようなパス風テキストは通常の文章として扱い、数値部分を変換します。
この境界は実装だけを見て決めず、統合テストでMarkdownとしての実際の出力を確認しながら固めました。
3. 不正入力でもレンダリング全体を止めない
不完全な記法や、数字以外を含む入力は変換しません。
$abc${en}
$1000${
$1000${}
${en}locale識別子は大文字と小文字を区別します。構文として読めても、formatterが登録されていない unknown、EN、en-US などは元の記法を維持します。
さらに、formatter内部で予期しない例外が起きた場合も、Markdown全体のレンダリングを失敗させないようにしました。その場合は元の記法をHTMLエスケープして返します。
東アジア形式で出力する <sub> 要素と単位文字列は、プラグイン内の固定データから生成しています。入力値をそのままHTMLとして差し込まないことも、rendererのテストで確認しました。
4. Public APIを1つに絞る
パッケージの exports では、ルートのエントリポイントだけを公開しています。
{
"exports": {
".": "./src/index.js"
}
}parser、renderer、各localeのformatter、内部tokenのmetadataはInternal APIです。利用例でもdeep importは使わず、require("markdown-it-digit") だけを案内しています。
内部モジュールを自由にimportできる状態にすると、実装を整理しただけで利用者のコードを壊す可能性があります。v1.0.0では公開範囲を小さくし、deep importが拒否されること自体もテストしました。
5. 83件のテストとsandbox検証
テストはNode.jsのtest runnerを使い、最終的に83件になりました。大きく分けると、次の観点を確認しています。
- 専用記法を解析するparser
- localeごとのformatter
- tokenから安全にHTMLを返すrenderer
- markdown-itへ登録したときの統合動作
- パッケージのPublic APIとdeep import制限
正常な変換例だけでなく、巨大整数、先頭ゼロ、4桁以下の値、ゼロだけの中間グループ、無量大数を超える上位桁、不完全な記法、未対応locale、URL、コード、HTMLなどを含めました。
リリース前には、次の確認を行いました。
npm run lint
npm test
npm pack --dry-runnpm pack では、実際に公開されるファイルも確認しました。v1.0.0のtarballは14ファイルで、LICENSE、英語版と日本語版のREADME、package.json、src 以下だけが含まれています。開発用テストや作業ファイルが混入していないことを確認できました。
さらに、生成したtarballを開発リポジトリとは別のCommonJS sandboxへインストールしました。そこでは6つのlocale、先頭ゼロ、未対応locale、不正構文、複数記法、Markdown装飾、リンク、URI、エスケープ、deep import制限を確認しました。
npm公開後には、sandboxの依存先を実際の markdown-it-digit@1.0.0 にして再確認しています。ローカルのソースコードが動くだけでなく、レジストリから利用者と同じ形でインストールできることまで確かめました。
公開
1. releaseブランチでv1.0.0を仕上げる
実装が揃った段階で、develop から release-1.0.0 ブランチを作りました。このブランチでは新機能を増やさず、公開に必要な仕上げへ範囲を限定しました。
主な作業は次のとおりです。
- 英語版
README.mdの作成 - 日本語版
README.jp.mdの作成 - 実装済みの内容に合わせた仕様書の整理
- CHANGELOGへのv1.0.0の記録
- package versionの
1.0.0への更新 - npm用descriptionの追加
- lint、test、pack、sandboxの最終検証
確認後、releaseブランチを main と develop へ反映し、main のリリース対象へ v1.0.0 タグを付けました。タグのpushを契機にGitHub Actionsからnpmへ公開する構成です。
2. GitHub Actionsでの初回publishが失敗した
公開workflowでは、タグをcheckoutし、依存関係を npm ci でインストールしてから83件のテストを実行し、最後に npm publish を行います。npm Trusted Publishingを使うため、workflowにはOIDC用の id-token: write も設定しました。
ところが、最初の v1.0.0 実行ではpublishに失敗しました。
テストは83件すべて成功し、tarballも意図した14ファイルで作られていました。しかし、最後の npm publish でnpmレジストリから E404 が返り、ジョブが終了しました。
npm error code E404
npm error 404 Not Found - PUT https://registry.npmjs.org/markdown-it-digit - Not foundここで、workflowのNode.jsを22から24へ変更し、実際に使われるNode.jsとnpmのversionもログへ表示するようにしました。
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 24
registry-url: "https://registry.npmjs.org"
cache: npm
- name: Show versions
run: |
node --version
npm --version修正後の実行環境はNode.js 24.19.0、npm 11.17.0 でした。再実行ではテストに成功したあと、Trusted Publishingによってprovenanceが生成され、markdown-it-digit@1.0.0 がnpmへ公開されました。
npm notice publish Signed provenance statement with source and build information from GitHub Actions
+ markdown-it-digit@1.0.0ローカルのlint、test、pack、sandboxがすべて成功していても、公開経路そのものは実際に通さなければ分かりませんでした。今回の失敗を通じて、実行環境のversionをログに残すことと、公開後にレジストリから再インストールする確認もリリース作業の一部だと実感しました。
3. v1.0.0で意図的に残した制約
v1.0.0は、できることを増やしすぎず、確認できた範囲を仕様として公開しました。現時点では次の制約があります。
- 変換対象はASCII数字で書いた整数のみ
- 負数と小数は未対応
- CommonJSのみ
- plugin optionsなし
- 外部locale登録APIなし
jp、cn、kr、twは独自のフォーマット識別子
今後の候補としては、負数や小数、ほかの数値表記、ESM対応、localeを拡張する仕組みなどが考えられます。ただし、Public APIや既存動作との互換性に影響するため、必要性と仕様を確認してから追加する予定です。
まとめ
markdown-it-digit は、小さな専用記法をlocaleごとの数値表記へ変換するmarkdown-itプラグインです。実装自体はparser、renderer、formatterへ分けられますが、パッケージとして公開するまでには、それ以上の作業がありました。
Markdownのどこを変換しないかを決め、Public APIを絞り、83件のテストを用意し、tarballの内容を調べ、別のsandboxでインストールして確認しました。それでも最初のnpm publishは失敗し、GitHub Actionsの実行環境を修正してようやくTrusted Publishingによる公開まで完了しました。
現在、markdown-it-digit@1.0.0 はnpmからインストールできます。ソースコードと詳しい仕様はGitHubリポジトリで公開しています。
