zenn-markdown-html 0.2.11 → 0.2.12-alpha.1
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.
- package/lib/index.d.ts +12 -1
- package/lib/index.js +37 -5
- package/lib/sanitizer.js +2 -2
- package/lib/utils/highlight.d.ts +48 -1
- package/lib/utils/highlight.js +254 -321
- package/lib/utils/md-renderer-fence.d.ts +89 -1
- package/lib/utils/md-renderer-fence.js +176 -20
- package/package.json +4 -6
- package/lib/prism-plugins/prism-diff-highlight.d.ts +0 -7
- package/lib/prism-plugins/prism-diff-highlight.js +0 -403
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
|
17
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
94
|
-
|
|
95
|
-
|
|
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.
|
|
3
|
+
"version": "0.2.12-alpha.1",
|
|
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
|
-
"
|
|
65
|
-
"
|
|
62
|
+
"sanitize-html": "^2.17.0",
|
|
63
|
+
"shiki": "^1.24.0"
|
|
66
64
|
},
|
|
67
|
-
"gitHead": "
|
|
65
|
+
"gitHead": "1fb8da6f1e1ecb8c5dbef801efc152d7b94ae69f",
|
|
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;
|