markdown-it-digitを作ってnpmへ公開するまで

Markdownに大きな数値を書くと、桁数をひと目で把握しにくいことがあります。たとえば 100000000 が1億だと分かっていても、文章の中でゼロを数えるのは少し面倒です。

そこで、数値に明示的な記法を加えると、英語式のカンマ区切りや日本語の万進法へ変換できるmarkdown-itプラグインを作りました。それが markdown-it-digit です。

この記事では使い方だけでなく、記法とPublic APIを決めた過程、Markdown内で変換してはいけない領域への対応、テスト、sandbox検証、そしてGitHub Actionsからnpmへ公開するまでを開発ログとしてまとめます。

目次

Markdownの数値を読みやすくしたかった

最初に考えたのは、Markdown内の数値を読みやすく表示することでした。ただし、文書に含まれる数字をすべて自動変換すると、年、型番、コード、URLなど、書き換えてほしくないものまで変わる可能性があります。

そこで、通常の数値には触れず、変換したい場所だけを専用記法で指定する方針にしました。

Markdown
$<number>${<locale>}

たとえば、次の記法は英語式の3桁区切りになります。

Markdown
$1234567${en}

出力は次のとおりです。

HTML
1,234,567

この形式なら、元のMarkdownを見ても「この数値は変換対象で、どの表記を使うのか」が分かります。通常の 1234567 はそのまま残るため、プラグインを導入しただけで既存文書の数字が一斉に変わることもありません。

インストールと基本的な使い方

パッケージはnpmからインストールできます。

Bash
npm install markdown-it-digit

v1.0.0のPublic APIは、CommonJSのmarkdown-itプラグイン関数だけです。

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

const md = new MarkdownIt().use(markdownItDigit);

console.log(md.render("$1234567${en}"));

結果は次のHTMLになります。

HTML
<p>1,234,567</p>

ESM用エントリポイント、plugin options、外部からlocaleを登録するAPIは用意していません。最初の公開版では機能を広げすぎず、プラグイン関数1つを安定した入口として扱うことにしました。

6種類の数値表記を実装する

v1.0.0で対応したlocale識別子は、eninjpcnkrtw の6種類です。

英語式とインド式の桁区切り

en は、右から3桁ごとにカンマを入れます。

Markdown
$123456789${en}
HTML
123,456,789

in は、右端だけを3桁にし、その左側を2桁ずつ区切ります。

Markdown
$123456789${in}
HTML
12,34,56,789

同じカンマ区切りでも、グループ化の規則は異なります。そのため、parserで数値とlocaleを取り出し、localeごとのformatterへ渡す構成にしました。

東アジアの万進法

jpcnkrtw は、右から4桁ずつ分割し、境界に単位を挿入します。

Markdown
$123456789${jp}
HTML
1<sub></sub>2345<sub></sub>67

ほかの識別子では単位表記が変わります。

識別子対象の表記変換例実際の見え方
jp日本語1<sub>億</sub>2345<sub>万</sub>6789123456789
cn簡体字中国語1<sub>亿</sub>2345<sub>万</sub>67891亿23456789
kr韓国語1<sub>억</sub>2345<sub>만</sub>6789123456789
tw繁体字中国語1<sub>億</sub>2345<sub>萬</sub>6789123456789

これらはISO言語コードやBCP 47言語タグではなく、このパッケージ独自のフォーマット識別子です。

実装では4桁分割の処理を共通化し、localeごとの差を単位テーブルとして持たせました。単位は万に相当する10の4乗から、無量大数に相当する10の68乗まで定義しています。

ここで重要なのは、数値を短縮しないことです。

Markdown
$100000000${jp}

これを単に「1億」とせず、次のように出力します。

HTML
1<sub></sub>0000<sub></sub>0000

ゼロだけのグループも残し、単位は桁位置を確認するためのガイドとして挿入します。元の数字を欠落させないことを優先した設計です。

開発ポイント

1. 数値を文字列のまま処理する

parserが受け付ける数値は、1文字以上のASCII数字です。負数と小数はv1.0.0の対象にしていません。

解析後もJavaScriptの Number には変換せず、文字列のままformatterへ渡します。これには2つの理由があります。

1つ目は、最大安全整数を超える大きな数値で桁落ちを起こさないためです。2つ目は、先頭ゼロを入力の一部として保持するためです。

Markdown
$0001000${en}
HTML
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コメント内
  • バックスラッシュでエスケープした記法

たとえば、リンクの表示テキストは変換しても、リンク先は変換しません。

Markdown
[$1000${en}](https://example.com/$1000${en})
HTML
<a href="https://example.com/$1000${en}">1,000</a>

URLらしい文字列を一律に処理するだけでなく、mailto:tel:、独自の scheme: も保護します。一方、URI schemeを持たない docs/$1000${en} のようなパス風テキストは通常の文章として扱い、数値部分を変換します。

この境界は実装だけを見て決めず、統合テストでMarkdownとしての実際の出力を確認しながら固めました。

3. 不正入力でもレンダリング全体を止めない

不完全な記法や、数字以外を含む入力は変換しません。

Markdown
$abc${en}
$1000${
$1000${}
${en}

locale識別子は大文字と小文字を区別します。構文として読めても、formatterが登録されていない unknownENen-US などは元の記法を維持します。

さらに、formatter内部で予期しない例外が起きた場合も、Markdown全体のレンダリングを失敗させないようにしました。その場合は元の記法をHTMLエスケープして返します。

東アジア形式で出力する <sub> 要素と単位文字列は、プラグイン内の固定データから生成しています。入力値をそのままHTMLとして差し込まないことも、rendererのテストで確認しました。

4. Public APIを1つに絞る

パッケージの exports では、ルートのエントリポイントだけを公開しています。

JSON
{
  "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などを含めました。

リリース前には、次の確認を行いました。

Bash
npm run lint
npm test
npm pack --dry-run

npm 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ブランチを maindevelop へ反映し、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 が返り、ジョブが終了しました。

Bash
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もログへ表示するようにしました。

YAML
- 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へ公開されました。

Markdown
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なし
  • jpcnkrtw は独自のフォーマット識別子

今後の候補としては、負数や小数、ほかの数値表記、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リポジトリで公開しています。

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