zenn-markdown-html 0.2.11 → 0.2.12-alpha.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,8 +1,96 @@
1
+ /**
2
+ * コードブロックのレンダリングとシンタックスハイライト
3
+ *
4
+ * ## 非同期処理アーキテクチャの概要
5
+ *
6
+ * Shiki(シンタックスハイライター)は非同期APIを持つが、
7
+ * markdown-it のレンダラーは同期的に文字列を返す必要がある。
8
+ * この制約を解決するため、プレースホルダー方式を採用している。
9
+ *
10
+ * ### 処理フロー(3フェーズ)
11
+ *
12
+ * ```
13
+ * [Phase 1: 収集] markdown-it レンダリング(同期)
14
+ * ↓
15
+ * コードブロックを検出するたびに:
16
+ * 1. コードブロック情報を配列に保存
17
+ * 2. プレースホルダー(HTMLコメント)を返す
18
+ * ↓
19
+ * 出力: プレースホルダー付きHTML + コードブロック情報配列
20
+ *
21
+ * [Phase 2: ハイライト] Shiki によるハイライト(非同期・並列)
22
+ * ↓
23
+ * Promise.all で全コードブロックを並列処理
24
+ * ↓
25
+ * 出力: ハイライト済みHTML配列
26
+ *
27
+ * [Phase 3: 置換] プレースホルダーを置換(同期)
28
+ * ↓
29
+ * プレースホルダーをハイライト済みHTMLに置換
30
+ * ↓
31
+ * 出力: 最終HTML
32
+ * ```
33
+ *
34
+ * ### この方式のメリット
35
+ *
36
+ * 1. 同期/非同期の不一致を解決: markdown-it の同期的なプラグインシステムを維持
37
+ * 2. 並列処理: 複数のコードブロックを Promise.all で同時にハイライト
38
+ * 3. 遅延ロード: Shiki の言語定義を必要に応じてロード(メモリ効率)
39
+ *
40
+ * ### 関連ファイル
41
+ *
42
+ * - `index.ts`: markdownToHtml() - 3フェーズを統合
43
+ * - `highlight.ts`: highlight() - Shiki によるハイライト処理
44
+ * - `md-renderer-fence.ts`: このファイル - Phase 1 と 3 を担当
45
+ */
1
46
  import MarkdownIt from 'markdown-it';
2
47
  import { MarkdownOptions } from '../types';
48
+ /**
49
+ * コードブロック情報を保存するインターフェース
50
+ * Phase 1 で収集し、Phase 2 でハイライト処理に使用
51
+ */
52
+ export interface CodeBlockInfo {
53
+ content: string;
54
+ langName: string;
55
+ hasDiff: boolean;
56
+ fileName?: string;
57
+ line?: number;
58
+ placeholder: string;
59
+ }
3
60
  export declare function parseInfo(str: string): {
4
61
  hasDiff: boolean;
5
62
  langName: string;
6
63
  fileName?: string;
7
64
  };
8
- export declare function mdRendererFence(md: MarkdownIt, options?: MarkdownOptions): void;
65
+ /**
66
+ * [Phase 1] markdown-it にコードブロックのレンダラーを登録する
67
+ *
68
+ * markdown-it がコードブロック(```)を検出するたびに呼ばれ、
69
+ * 以下の処理を行う:
70
+ * 1. コードブロックの情報(内容、言語、diff有無など)を codeBlocks 配列に追加
71
+ * 2. プレースホルダー(例: <!--SHIKI_CODE_BLOCK_xxxxxxxx-->)を返す
72
+ *
73
+ * このレンダラーは同期的に動作し、実際のハイライト処理は
74
+ * Phase 2(applyHighlighting)で非同期に行われる。
75
+ *
76
+ * @param md - markdown-it インスタンス
77
+ * @param options - Markdown 変換オプション
78
+ * @param codeBlocks - コードブロック情報を格納する配列(副作用で変更される)
79
+ */
80
+ export declare function mdRendererFence(md: MarkdownIt, options: MarkdownOptions, codeBlocks: CodeBlockInfo[]): void;
81
+ /**
82
+ * [Phase 2 & 3] プレースホルダーをハイライトされたコードに置換する
83
+ *
84
+ * Phase 2: 全コードブロックを Shiki で並列ハイライト
85
+ * - Promise.all により、複数のコードブロックを同時に処理
86
+ * - 各コードブロックに対して highlight() を呼び出し
87
+ * - 言語が未ロードの場合は自動的にロード(遅延ロード)
88
+ *
89
+ * Phase 3: プレースホルダーを置換
90
+ * - <!--SHIKI_CODE_BLOCK_xxxxxxxx--> を実際のハイライト済み HTML に置換
91
+ *
92
+ * @param html - プレースホルダーを含む HTML 文字列
93
+ * @param codeBlocks - Phase 1 で収集したコードブロック情報の配列
94
+ * @returns ハイライト済みの完全な HTML 文字列
95
+ */
96
+ export declare function applyHighlighting(html: string, codeBlocks: CodeBlockInfo[]): Promise<string>;
@@ -3,18 +3,111 @@
3
3
  Object.defineProperty(exports, "__esModule", {
4
4
  value: true
5
5
  });
6
+ exports.applyHighlighting = applyHighlighting;
6
7
  exports.mdRendererFence = mdRendererFence;
7
8
  exports.parseInfo = parseInfo;
8
9
  var _markdownIt = require("./markdown-it");
9
10
  var _highlight = require("./highlight");
10
- function getHtml({
11
+ /**
12
+ * コードブロックのレンダリングとシンタックスハイライト
13
+ *
14
+ * ## 非同期処理アーキテクチャの概要
15
+ *
16
+ * Shiki(シンタックスハイライター)は非同期APIを持つが、
17
+ * markdown-it のレンダラーは同期的に文字列を返す必要がある。
18
+ * この制約を解決するため、プレースホルダー方式を採用している。
19
+ *
20
+ * ### 処理フロー(3フェーズ)
21
+ *
22
+ * ```
23
+ * [Phase 1: 収集] markdown-it レンダリング(同期)
24
+ * ↓
25
+ * コードブロックを検出するたびに:
26
+ * 1. コードブロック情報を配列に保存
27
+ * 2. プレースホルダー(HTMLコメント)を返す
28
+ * ↓
29
+ * 出力: プレースホルダー付きHTML + コードブロック情報配列
30
+ *
31
+ * [Phase 2: ハイライト] Shiki によるハイライト(非同期・並列)
32
+ * ↓
33
+ * Promise.all で全コードブロックを並列処理
34
+ * ↓
35
+ * 出力: ハイライト済みHTML配列
36
+ *
37
+ * [Phase 3: 置換] プレースホルダーを置換(同期)
38
+ * ↓
39
+ * プレースホルダーをハイライト済みHTMLに置換
40
+ * ↓
41
+ * 出力: 最終HTML
42
+ * ```
43
+ *
44
+ * ### この方式のメリット
45
+ *
46
+ * 1. 同期/非同期の不一致を解決: markdown-it の同期的なプラグインシステムを維持
47
+ * 2. 並列処理: 複数のコードブロックを Promise.all で同時にハイライト
48
+ * 3. 遅延ロード: Shiki の言語定義を必要に応じてロード(メモリ効率)
49
+ *
50
+ * ### 関連ファイル
51
+ *
52
+ * - `index.ts`: markdownToHtml() - 3フェーズを統合
53
+ * - `highlight.ts`: highlight() - Shiki によるハイライト処理
54
+ * - `md-renderer-fence.ts`: このファイル - Phase 1 と 3 を担当
55
+ */
56
+
57
+ /**
58
+ * コードブロック情報を保存するインターフェース
59
+ * Phase 1 で収集し、Phase 2 でハイライト処理に使用
60
+ */
61
+
62
+ // プレースホルダーのプレフィックス
63
+ const PLACEHOLDER_PREFIX = '<!--SHIKI_CODE_BLOCK_';
64
+ const PLACEHOLDER_SUFFIX = '-->';
65
+
66
+ /**
67
+ * ランダムな8文字の文字列を生成する
68
+ */
69
+ function generateRandomId() {
70
+ return Math.random().toString(36).slice(2, 10);
71
+ }
72
+
73
+ /**
74
+ * プレースホルダーを生成する
75
+ * ユーザーが本文中に同じ文字列を書いても衝突しないようランダムIDを使用
76
+ */
77
+ function createPlaceholder() {
78
+ return `${PLACEHOLDER_PREFIX}${generateRandomId()}${PLACEHOLDER_SUFFIX}`;
79
+ }
80
+
81
+ /**
82
+ * コードブロックの HTML を生成する
83
+ * Shiki の出力(<pre><code>...</code></pre>)を外側のコンテナでラップする
84
+ * 注: <pre> や <code> へのクラス・属性追加は highlight() の transformers で行う
85
+ */
86
+ function wrapHighlightedCode({
87
+ highlightedHtml,
88
+ fileName
89
+ }) {
90
+ // ファイル名コンテナを追加
91
+ const fileNameHtml = fileName ? `<div class="code-block-filename-container"><span class="code-block-filename">${_markdownIt.md.utils.escapeHtml(fileName)}</span></div>` : '';
92
+ return `<div class="code-block-container">${fileNameHtml}${highlightedHtml}</div>`;
93
+ }
94
+
95
+ /**
96
+ * エラー時のフォールバック HTML を生成
97
+ * Shiki でのハイライトに失敗した場合に使用
98
+ */
99
+ function getPlainHtml({
11
100
  content,
12
- className,
13
101
  fileName,
14
102
  line
15
103
  }) {
16
- const escapedClass = _markdownIt.md.utils.escapeHtml(className);
17
- return `<div class="code-block-container">${fileName ? `<div class="code-block-filename-container"><span class="code-block-filename">${_markdownIt.md.utils.escapeHtml(fileName)}</span></div>` : ''}<pre class="${escapedClass}"><code class="${escapedClass !== '' ? `${escapedClass} code-line` : 'code-line'}" ${line !== undefined ? `data-line="${line}"` : ''}>${content}</code></pre></div>`;
104
+ const escapedContent = _markdownIt.md.utils.escapeHtml(content);
105
+ const lineAttr = line !== undefined ? ` data-line="${line}"` : '';
106
+ const preHtml = `<pre><code class="code-line"${lineAttr}>${escapedContent}</code></pre>`;
107
+ return wrapHighlightedCode({
108
+ highlightedHtml: preHtml,
109
+ fileName
110
+ });
18
111
  }
19
112
  function getClassName({
20
113
  langName = '',
@@ -27,13 +120,11 @@ function getClassName({
27
120
  }
28
121
  return langName ? `language-${langName}` : '';
29
122
  }
123
+
124
+ // Shiki がネイティブサポートしていない言語のフォールバック
30
125
  const fallbackLanguages = {
31
- vue: 'html',
32
126
  react: 'jsx',
33
- fish: 'shell',
34
- sh: 'shell',
35
- cwl: 'yaml',
36
- tf: 'hcl' // ref: https://github.com/PrismJS/prism/issues/1252
127
+ cwl: 'yaml'
37
128
  };
38
129
  function normalizeLangName(str) {
39
130
  if (!(str !== null && str !== void 0 && str.length)) return '';
@@ -64,7 +155,23 @@ function parseInfo(str) {
64
155
  hasDiff
65
156
  };
66
157
  }
67
- function mdRendererFence(md, options) {
158
+
159
+ /**
160
+ * [Phase 1] markdown-it にコードブロックのレンダラーを登録する
161
+ *
162
+ * markdown-it がコードブロック(```)を検出するたびに呼ばれ、
163
+ * 以下の処理を行う:
164
+ * 1. コードブロックの情報(内容、言語、diff有無など)を codeBlocks 配列に追加
165
+ * 2. プレースホルダー(例: <!--SHIKI_CODE_BLOCK_xxxxxxxx-->)を返す
166
+ *
167
+ * このレンダラーは同期的に動作し、実際のハイライト処理は
168
+ * Phase 2(applyHighlighting)で非同期に行われる。
169
+ *
170
+ * @param md - markdown-it インスタンス
171
+ * @param options - Markdown 変換オプション
172
+ * @param codeBlocks - コードブロック情報を格納する配列(副作用で変更される)
173
+ */
174
+ function mdRendererFence(md, options, codeBlocks) {
68
175
  // override fence
69
176
  md.renderer.rules.fence = function (...args) {
70
177
  var _tokens$idx$map;
@@ -80,21 +187,70 @@ function mdRendererFence(md, options) {
80
187
  } = parseInfo(info);
81
188
  if (langName === 'mermaid') {
82
189
  var _options$customEmbed;
83
- const generator = options === null || options === void 0 || (_options$customEmbed = options.customEmbed) === null || _options$customEmbed === void 0 ? void 0 : _options$customEmbed.mermaid;
190
+ const generator = (_options$customEmbed = options.customEmbed) === null || _options$customEmbed === void 0 ? void 0 : _options$customEmbed.mermaid;
84
191
  // generator が(上書きされて)定義されてない場合はそのまま出力する
85
192
  return generator ? generator(content.trim(), options) : content;
86
193
  }
87
- const className = getClassName({
88
- langName,
89
- hasDiff
90
- });
91
- const highlightedContent = (0, _highlight.highlight)(content, langName, hasDiff);
92
194
  const fenceStart = (_tokens$idx$map = tokens[idx].map) === null || _tokens$idx$map === void 0 ? void 0 : _tokens$idx$map[0];
93
- return getHtml({
94
- content: highlightedContent,
95
- className,
195
+ const placeholder = createPlaceholder();
196
+ codeBlocks.push({
197
+ content,
198
+ langName,
199
+ hasDiff,
96
200
  fileName,
97
- line: fenceStart
201
+ line: fenceStart,
202
+ placeholder
98
203
  });
204
+ return placeholder;
99
205
  };
206
+ }
207
+
208
+ /**
209
+ * [Phase 2 & 3] プレースホルダーをハイライトされたコードに置換する
210
+ *
211
+ * Phase 2: 全コードブロックを Shiki で並列ハイライト
212
+ * - Promise.all により、複数のコードブロックを同時に処理
213
+ * - 各コードブロックに対して highlight() を呼び出し
214
+ * - 言語が未ロードの場合は自動的にロード(遅延ロード)
215
+ *
216
+ * Phase 3: プレースホルダーを置換
217
+ * - <!--SHIKI_CODE_BLOCK_xxxxxxxx--> を実際のハイライト済み HTML に置換
218
+ *
219
+ * @param html - プレースホルダーを含む HTML 文字列
220
+ * @param codeBlocks - Phase 1 で収集したコードブロック情報の配列
221
+ * @returns ハイライト済みの完全な HTML 文字列
222
+ */
223
+ async function applyHighlighting(html, codeBlocks) {
224
+ // すべてのコードブロックを並列でハイライト
225
+ const highlightedBlocks = await Promise.all(codeBlocks.map(async block => {
226
+ const className = getClassName({
227
+ langName: block.langName,
228
+ hasDiff: block.hasDiff
229
+ });
230
+ try {
231
+ const highlightedHtml = await (0, _highlight.highlight)(block.content, block.langName, {
232
+ hasDiff: block.hasDiff,
233
+ className,
234
+ line: block.line
235
+ });
236
+ return wrapHighlightedCode({
237
+ highlightedHtml,
238
+ fileName: block.fileName
239
+ });
240
+ } catch {
241
+ // エラー時はプレーンテキストとして出力
242
+ return getPlainHtml({
243
+ content: block.content,
244
+ fileName: block.fileName,
245
+ line: block.line
246
+ });
247
+ }
248
+ }));
249
+
250
+ // プレースホルダーを置換
251
+ let result = html;
252
+ for (let i = 0; i < highlightedBlocks.length; i++) {
253
+ result = result.replace(codeBlocks[i].placeholder, highlightedBlocks[i]);
254
+ }
255
+ return result;
100
256
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zenn-markdown-html",
3
- "version": "0.2.11",
3
+ "version": "0.2.12-alpha.0",
4
4
  "license": "MIT",
5
5
  "description": "Convert markdown to zenn flavor html.",
6
6
  "main": "lib/index.js",
@@ -40,9 +40,7 @@
40
40
  "@eslint/js": "^9.38.0",
41
41
  "@types/markdown-it": "^14.1.2",
42
42
  "@types/node": "^24.9.1",
43
- "@types/prismjs": "^1.26.5",
44
43
  "@types/sanitize-html": "^2.16.0",
45
- "babel-plugin-prismjs": "^2.1.0",
46
44
  "eslint": "^9.38.0",
47
45
  "eslint-config-prettier": "^10.1.8",
48
46
  "node-html-parser": "^7.0.1",
@@ -61,10 +59,10 @@
61
59
  "markdown-it-inline-comments": "^1.0.1",
62
60
  "markdown-it-link-attributes": "^4.0.1",
63
61
  "markdown-it-task-lists": "^2.1.1",
64
- "prismjs": "^1.30.0",
65
- "sanitize-html": "^2.17.0"
62
+ "sanitize-html": "^2.17.0",
63
+ "shiki": "^1.24.0"
66
64
  },
67
- "gitHead": "48d124814a4e625ae047c4ae01b3084ffae0aa0d",
65
+ "gitHead": "3d5f1d52c73b986b5a0ec911c7078d7a4822566a",
68
66
  "publishConfig": {
69
67
  "access": "public"
70
68
  }
@@ -1,7 +0,0 @@
1
- /**
2
- * PrismJSのDiff構文を使用できるようにするためのプラグイン
3
- * ソースコードの大部分は、以下のファイルより抜き出したもの
4
- * @reference https://github.com/PrismJS/prism/blob/master/plugins/diff-highlight/prism-diff-highlight.js
5
- * @note `babel-plugin-prismjs`によって全ての言語プラグインを読み込んでいるため`locaLanguages()`の実行はしていない
6
- */
7
- export declare function enableDiffHighlight(): void;