@helping-ai-workflow/md2doc 3.9.0 → 3.10.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.
@@ -0,0 +1,48 @@
1
+ 'use strict';
2
+ /* Lucide icons v0.469.0 (ISC License, https://lucide.dev/license), only the ones
3
+ the editor draws. UMD like toolbar-model.js: require()-able in node for the
4
+ tests, injected into the edit page as window.md2docIcons (lib/editor/server.js). */
5
+ (function (root, factory) {
6
+ if (typeof module === 'object' && module.exports) module.exports = factory();
7
+ else root.md2docIcons = factory();
8
+ })(typeof self !== 'undefined' ? self : this, function () {
9
+ const BODY = {
10
+ "arrow-down-to-line": "<path d=\"M12 17V3\" /><path d=\"m6 11 6 6 6-6\" /><path d=\"M19 21H5\" />",
11
+ "arrow-up-to-line": "<path d=\"M5 3h14\" /><path d=\"m18 13-6-6-6 6\" /><path d=\"M12 7v14\" />",
12
+ "bold": "<path d=\"M6 12h9a4 4 0 0 1 0 8H7a1 1 0 0 1-1-1V5a1 1 0 0 1 1-1h7a4 4 0 0 1 0 8\" />",
13
+ "check": "<path d=\"M20 6 9 17l-5-5\" />",
14
+ "circle-alert": "<circle cx=\"12\" cy=\"12\" r=\"10\" /><line x1=\"12\" x2=\"12\" y1=\"8\" y2=\"12\" /><line x1=\"12\" x2=\"12.01\" y1=\"16\" y2=\"16\" />",
15
+ "code": "<polyline points=\"16 18 22 12 16 6\" /><polyline points=\"8 6 2 12 8 18\" />",
16
+ "grip-vertical": "<circle cx=\"9\" cy=\"12\" r=\"1\" /><circle cx=\"9\" cy=\"5\" r=\"1\" /><circle cx=\"9\" cy=\"19\" r=\"1\" /><circle cx=\"15\" cy=\"12\" r=\"1\" /><circle cx=\"15\" cy=\"5\" r=\"1\" /><circle cx=\"15\" cy=\"19\" r=\"1\" />",
17
+ "heading": "<path d=\"M6 12h12\" /><path d=\"M6 20V4\" /><path d=\"M18 20V4\" />",
18
+ "image": "<rect width=\"18\" height=\"18\" x=\"3\" y=\"3\" rx=\"2\" ry=\"2\" /><circle cx=\"9\" cy=\"9\" r=\"2\" /><path d=\"m21 15-3.086-3.086a2 2 0 0 0-2.828 0L6 21\" />",
19
+ "indent-decrease": "<path d=\"M21 12H11\" /><path d=\"M21 18H11\" /><path d=\"M21 6H11\" /><path d=\"m7 8-4 4 4 4\" />",
20
+ "indent-increase": "<path d=\"M21 12H11\" /><path d=\"M21 18H11\" /><path d=\"M21 6H11\" /><path d=\"m3 8 4 4-4 4\" />",
21
+ "info": "<circle cx=\"12\" cy=\"12\" r=\"10\" /><path d=\"M12 16v-4\" /><path d=\"M12 8h.01\" />",
22
+ "italic": "<line x1=\"19\" x2=\"10\" y1=\"4\" y2=\"4\" /><line x1=\"14\" x2=\"5\" y1=\"20\" y2=\"20\" /><line x1=\"15\" x2=\"9\" y1=\"4\" y2=\"20\" />",
23
+ "link": "<path d=\"M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71\" /><path d=\"M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71\" />",
24
+ "list": "<path d=\"M3 12h.01\" /><path d=\"M3 18h.01\" /><path d=\"M3 6h.01\" /><path d=\"M8 12h13\" /><path d=\"M8 18h13\" /><path d=\"M8 6h13\" />",
25
+ "list-checks": "<path d=\"m3 17 2 2 4-4\" /><path d=\"m3 7 2 2 4-4\" /><path d=\"M13 6h8\" /><path d=\"M13 12h8\" /><path d=\"M13 18h8\" />",
26
+ "list-ordered": "<path d=\"M10 12h11\" /><path d=\"M10 18h11\" /><path d=\"M10 6h11\" /><path d=\"M4 10h2\" /><path d=\"M4 6h1v4\" /><path d=\"M6 18H4c0-1 2-2 2-3s-1-1.5-2-1\" />",
27
+ "minus": "<path d=\"M5 12h14\" />",
28
+ "panel-left": "<rect width=\"18\" height=\"18\" x=\"3\" y=\"3\" rx=\"2\" /><path d=\"M9 3v18\" />",
29
+ "plus": "<path d=\"M5 12h14\" /><path d=\"M12 5v14\" />",
30
+ "quote": "<path d=\"M16 3a2 2 0 0 0-2 2v6a2 2 0 0 0 2 2 1 1 0 0 1 1 1v1a2 2 0 0 1-2 2 1 1 0 0 0-1 1v2a1 1 0 0 0 1 1 6 6 0 0 0 6-6V5a2 2 0 0 0-2-2z\" /><path d=\"M5 3a2 2 0 0 0-2 2v6a2 2 0 0 0 2 2 1 1 0 0 1 1 1v1a2 2 0 0 1-2 2 1 1 0 0 0-1 1v2a1 1 0 0 0 1 1 6 6 0 0 0 6-6V5a2 2 0 0 0-2-2z\" />",
31
+ "redo-2": "<path d=\"m15 14 5-5-5-5\" /><path d=\"M20 9H9.5A5.5 5.5 0 0 0 4 14.5A5.5 5.5 0 0 0 9.5 20H13\" />",
32
+ "save": "<path d=\"M15.2 3a2 2 0 0 1 1.4.6l3.8 3.8a2 2 0 0 1 .6 1.4V19a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2z\" /><path d=\"M17 21v-7a1 1 0 0 0-1-1H8a1 1 0 0 0-1 1v7\" /><path d=\"M7 3v4a1 1 0 0 0 1 1h7\" />",
33
+ "square-code": "<path d=\"M10 9.5 8 12l2 2.5\" /><path d=\"m14 9.5 2 2.5-2 2.5\" /><rect width=\"18\" height=\"18\" x=\"3\" y=\"3\" rx=\"2\" />",
34
+ "strikethrough": "<path d=\"M16 4H9a3 3 0 0 0-2.83 4\" /><path d=\"M14 12a4 4 0 0 1 0 8H6\" /><line x1=\"4\" x2=\"20\" y1=\"12\" y2=\"12\" />",
35
+ "table": "<path d=\"M12 3v18\" /><rect width=\"18\" height=\"18\" x=\"3\" y=\"3\" rx=\"2\" /><path d=\"M3 9h18\" /><path d=\"M3 15h18\" />",
36
+ "underline": "<path d=\"M6 4v6a6 6 0 0 0 12 0V4\" /><line x1=\"4\" x2=\"20\" y1=\"20\" y2=\"20\" />",
37
+ "undo-2": "<path d=\"M9 14 4 9l5-5\" /><path d=\"M4 9h10.5a5.5 5.5 0 0 1 5.5 5.5a5.5 5.5 0 0 1-5.5 5.5H11\" />",
38
+ "x": "<path d=\"M18 6 6 18\" /><path d=\"m6 6 12 12\" />"
39
+ };
40
+ const NAMES = Object.freeze(Object.keys(BODY));
41
+ function svg(name, size) {
42
+ const s = size || 16;
43
+ return '<svg class="ed-ico" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="' + s + '" height="' + s + '" ' +
44
+ 'fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" ' +
45
+ 'aria-hidden="true" focusable="false">' + BODY[name] + '</svg>';
46
+ }
47
+ return { NAMES, svg };
48
+ });
@@ -43,12 +43,12 @@ const SELECTION_SRC = fs.readFileSync(path.join(__dirname, 'selection.js'), 'utf
43
43
  // client.js reads window.md2docCellRange.
44
44
  const CELL_RANGE_SRC = fs.readFileSync(path.join(__dirname, 'cell-range.js'), 'utf8');
45
45
  // v3.1.0 Task E: the four Phase-1 modules behind the visible toolbar, the
46
- // paste pipeline, the image-drop asset endpoint and the whole-document
47
- // source escape hatch. Same "pure module, no dependency on any other editor
46
+ // paste pipeline and the image-drop asset endpoint (the whole-document
47
+ // source escape hatch went with v3.10.0). Same "pure module, no dependency on any other editor
48
48
  // module" property as indent-clamp.js / convert-md.js / selection.js above —
49
49
  // each only has to land before client.js reads its window global.
50
50
  const TOOLBAR_MODEL_SRC = fs.readFileSync(path.join(__dirname, 'toolbar-model.js'), 'utf8');
51
- const DOCSOURCE_SRC = fs.readFileSync(path.join(__dirname, 'docsource.js'), 'utf8');
51
+ const ICONS_SRC = fs.readFileSync(path.join(__dirname, 'icons.js'), 'utf8');
52
52
  const ASSET_SRC = fs.readFileSync(path.join(__dirname, 'asset.js'), 'utf8');
53
53
  // turndown's BROWSER build. node's `require('turndown')` resolves to
54
54
  // lib/turndown.cjs.js, which is not loadable as a plain <script> tag — the
@@ -736,8 +736,8 @@ async function createEditorServer({
736
736
  `<script>${PATCHMAP_SRC}</script>\n` +
737
737
  `<script>${SELECTION_SRC}</script>\n` +
738
738
  `<script>${CELL_RANGE_SRC}</script>\n` +
739
+ `<script>${ICONS_SRC}</script>\n` +
739
740
  `<script>${TOOLBAR_MODEL_SRC}</script>\n` +
740
- `<script>${DOCSOURCE_SRC}</script>\n` +
741
741
  `<script>${ASSET_SRC}</script>\n` +
742
742
  `<script>${TURNDOWN_SRC}</script>\n` +
743
743
  `<script>${PASTE_MD_SRC}</script>\n` +
@@ -982,6 +982,16 @@ async function createEditorServer({
982
982
  } catch (e) {
983
983
  fileId = undefined; drawioClientId = undefined; drawioAck = undefined;
984
984
  }
985
+ // v3.10.0: the client compares this with its own baseline to notice an
986
+ // edit made outside the browser. A header, not a body: the bare 204
987
+ // ("nothing stale") is part of the drawio contract and stays bodiless.
988
+ const pingFile = (typeof fileId === 'number') ? absFiles[fileId] : undefined;
989
+ // Async on purpose: this route fires every 10s from every open tab, and
990
+ // the drawio fast path below is pinned to ZERO fs.statSync calls for a
991
+ // document with no drawio references (test/editor-server.test.js (a)).
992
+ if (pingFile) {
993
+ try { res.setHeader('X-Md2doc-Mtime', String((await fs.promises.stat(pingFile)).mtimeMs)); } catch (e) { /* file gone: no header */ }
994
+ }
985
995
  // Ack FIRST, then re-check: a client that has just confirmed it
986
996
  // applied version N must get a 204 on this same beat unless the file
987
997
  // has moved on past N since.
@@ -11,53 +11,47 @@
11
11
  else root.md2docToolbarModel = factory();
12
12
  })(typeof self !== 'undefined' ? self : this, function () {
13
13
 
14
- // The 23-button roster, spec §4 table (`export▾` moved out of scope — no
15
- // browser-reachable export path) plus the v3.4.0 §3 `save` button. Seven
14
+ // The 22-button roster, spec §4 table (`export▾` moved out of scope — no
15
+ // browser-reachable export path) plus the v3.4.0 §3 `save` button.
16
+ // v3.10.0 removed the edit/source mode button with source mode itself. Seven
16
17
  // groups, in render order; group order here doubles as separator placement
17
18
  // for the caller (a separator sits between consecutive groups).
18
19
  const BUTTON_DEFS = [
19
20
  // file
20
- { id: 'save', group: 'file', label: '💾', icon: '💾', title: '儲存 (Ctrl+S)', toggle: false },
21
+ { id: 'save', group: 'file', label: '💾', icon: 'save', title: '儲存 (Ctrl+S)', toggle: false },
21
22
 
22
23
  // history
23
- { id: 'undo', group: 'history', label: 'Undo', icon: '↶', title: '復原 (Ctrl+Z)', toggle: false },
24
- { id: 'redo', group: 'history', label: 'Redo', icon: '↷', title: '重做 (Ctrl+Y)', toggle: false },
24
+ { id: 'undo', group: 'history', label: 'Undo', icon: 'undo-2', title: '復原 (Ctrl+Z)', toggle: false },
25
+ { id: 'redo', group: 'history', label: 'Redo', icon: 'redo-2', title: '重做 (Ctrl+Y)', toggle: false },
25
26
 
26
27
  // block
27
- { id: 'headings', group: 'block', label: 'H', icon: 'H', title: '標題', toggle: true },
28
- { id: 'quote', group: 'block', label: '❝', icon: '❝', title: '引用', toggle: true },
29
- { id: 'code', group: 'block', label: '</>', icon: '</>', title: '程式碼區塊', toggle: true },
30
- { id: 'list', group: 'block', label: '•', icon: '•', title: '項目符號列表', toggle: true },
31
- { id: 'ordered-list', group: 'block', label: '1.', icon: '1.', title: '編號列表', toggle: true },
32
- { id: 'check', group: 'block', label: '☑', icon: '☑', title: '待辦清單', toggle: false },
28
+ { id: 'headings', group: 'block', label: 'H', icon: 'heading', title: '標題', toggle: true },
29
+ { id: 'quote', group: 'block', label: '❝', icon: 'quote', title: '引用', toggle: true },
30
+ { id: 'code', group: 'block', label: '</>', icon: 'square-code', title: '程式碼區塊', toggle: true },
31
+ { id: 'list', group: 'block', label: '•', icon: 'list', title: '項目符號列表', toggle: true },
32
+ { id: 'ordered-list', group: 'block', label: '1.', icon: 'list-ordered', title: '編號列表', toggle: true },
33
+ { id: 'check', group: 'block', label: '☑', icon: 'list-checks', title: '待辦清單', toggle: false },
33
34
 
34
35
  // inline
35
- { id: 'bold', group: 'inline', label: 'B', icon: 'B', title: '粗體', toggle: true },
36
- { id: 'italic', group: 'inline', label: 'I', icon: 'I', title: '斜體', toggle: true },
37
- { id: 'strike', group: 'inline', label: 'S', icon: 'S', title: '刪除線', toggle: true },
38
- { id: 'inline-code', group: 'inline', label: '`', icon: '`', title: '行內程式碼', toggle: true },
39
- { id: 'link', group: 'inline', label: '🔗', icon: '🔗', title: '連結', toggle: true },
36
+ { id: 'bold', group: 'inline', label: 'B', icon: 'bold', title: '粗體', toggle: true },
37
+ { id: 'italic', group: 'inline', label: 'I', icon: 'italic', title: '斜體', toggle: true },
38
+ { id: 'strike', group: 'inline', label: 'S', icon: 'strikethrough', title: '刪除線', toggle: true },
39
+ { id: 'inline-code', group: 'inline', label: '`', icon: 'code', title: '行內程式碼', toggle: true },
40
+ { id: 'link', group: 'inline', label: '🔗', icon: 'link', title: '連結', toggle: true },
40
41
 
41
42
  // indent
42
- { id: 'outdent', group: 'indent', label: '⇤', icon: '⇤', title: '減少縮排', toggle: false },
43
- { id: 'indent', group: 'indent', label: '⇥', icon: '⇥', title: '增加縮排', toggle: false },
43
+ { id: 'outdent', group: 'indent', label: '⇤', icon: 'indent-decrease', title: '減少縮排', toggle: false },
44
+ { id: 'indent', group: 'indent', label: '⇥', icon: 'indent-increase', title: '增加縮排', toggle: false },
44
45
 
45
46
  // insert
46
- { id: 'table', group: 'insert', label: '⊞', icon: '⊞', title: '插入表格', toggle: false },
47
- { id: 'insert-before', group: 'insert', label: '⬆', icon: '⬆', title: '在上方插入區塊', toggle: false },
48
- { id: 'insert-after', group: 'insert', label: '⬇', icon: '⬇', title: '在下方插入區塊', toggle: false },
49
- { id: 'line', group: 'insert', label: '―', icon: '―', title: '插入分隔線', toggle: false },
50
- { id: 'image', group: 'insert', label: '🖼', icon: '🖼', title: '插入圖片', toggle: false },
47
+ { id: 'table', group: 'insert', label: '⊞', icon: 'table', title: '插入表格', toggle: false },
48
+ { id: 'insert-before', group: 'insert', label: '⬆', icon: 'arrow-up-to-line', title: '在上方插入區塊', toggle: false },
49
+ { id: 'insert-after', group: 'insert', label: '⬇', icon: 'arrow-down-to-line', title: '在下方插入區塊', toggle: false },
50
+ { id: 'line', group: 'insert', label: '―', icon: 'minus', title: '插入分隔線', toggle: false },
51
+ { id: 'image', group: 'insert', label: '🖼', icon: 'image', title: '插入圖片', toggle: false },
51
52
 
52
53
  // view
53
- { id: 'outline', group: 'view', label: '☰', icon: '☰', title: '大綱', toggle: false },
54
- // v3.2.1: 這顆是 edit <-> source 的兩態切換(preview 模式已移除)。id 保留
55
- // 為 'preview' 是刻意的 —— 每一支選擇器、journey 案例與 runtime 測試都指名
56
- // 它,改名只換得一個更好聽的字串。label / title 不是烘死值:deriveState()
57
- // 每次都依當前 mode 覆寫成【下一個】狀態的名字,這裡的值只是 edit 模式的
58
- // 起始值。toggle: false —— 兩態下 aria-pressed 講不出真話(按鈕講的是去處,
59
- // 不是目前所在),真話由會變的 label / title 承擔。
60
- { id: 'preview', group: 'view', label: 'M↓', icon: 'M↓', title: '切換到原始碼模式', toggle: false },
54
+ { id: 'outline', group: 'view', label: '☰', icon: 'panel-left', title: '大綱', toggle: false },
61
55
  ];
62
56
 
63
57
  const BUTTONS = Object.freeze(BUTTON_DEFS.map((b) => Object.freeze(Object.assign({}, b))));
@@ -77,7 +71,7 @@ const GROUPS = Object.freeze(['file', 'history', 'block', 'inline', 'indent', 'i
77
71
  // `!dirty` line happens to sit; if that line is ever moved above the
78
72
  // no-block loop, this set becomes the only thing still enforcing it. Do not
79
73
  // delete it as "dead code" without re-checking that ordering.
80
- const NO_BLOCK_ALLOWED = new Set(['undo', 'redo', 'outline', 'preview', 'save']);
74
+ const NO_BLOCK_ALLOWED = new Set(['undo', 'redo', 'outline', 'save']);
81
75
 
82
76
  // The mark element each inline button toggles. This module cannot ask the
83
77
  // DOM which marks a selection sits in, so the client measures that and hands
@@ -107,7 +101,7 @@ const MARK_IDS = Object.freeze(Object.keys(MARK_TAG_BY_ID));
107
101
  // 'inert' pressing the button would do nothing at all
108
102
 
109
103
  // ctx: {blockType, indent, headingDepth, inList, listOrdered, hasSelection,
110
- // mode, marks}
104
+ // marks, dirty}
111
105
  // -> { [id]: {active: bool, mixed: bool, disabled: bool, label?: string} }
112
106
  function deriveState(ctx) {
113
107
  const c = ctx || {};
@@ -116,7 +110,6 @@ function deriveState(ctx) {
116
110
  const headingDepth = c.headingDepth;
117
111
  const inList = !!c.inList;
118
112
  const listOrdered = !!c.listOrdered;
119
- const mode = c.mode;
120
113
  const dirty = !!c.dirty;
121
114
  const hasBlock = blockType !== null && blockType !== undefined;
122
115
 
@@ -125,19 +118,6 @@ function deriveState(ctx) {
125
118
  state[b.id] = { active: false, mixed: false, disabled: false };
126
119
  }
127
120
 
128
- // v3.2.1: the mode button names the NEXT state, never the current one.
129
- // Under the old tri-state cycle it was permanently labelled 「預覽」 and
130
- // reported aria-pressed=true in BOTH non-edit modes, so pressing it twice
131
- // landed in source with no grips and an interface that said nothing about
132
- // where you were — the reported "stuck" defect. It carries no `active`
133
- // now: with two states the honest signal is the label, and the current
134
- // mode is spelled out in .ed-toolbar-status by the client.
135
- // It stays enabled in source mode via the mode==='source' override below
136
- // (do not gate this on inList/hasBlock — it is the escape hatch OUT of
137
- // those states).
138
- state.preview.label = mode === 'source' ? '✎' : 'M↓';
139
- state.preview.title = mode === 'source' ? '切換到編輯模式' : '切換到原始碼模式';
140
-
141
121
  // headings: dropdown whose label tracks the current heading depth.
142
122
  state.headings.label = blockType === 'heading' ? 'H' + headingDepth : 'H';
143
123
  state.headings.active = blockType === 'heading';
@@ -231,41 +211,8 @@ function deriveState(ctx) {
231
211
  // single-sourced: client.js's `documentIsDirty()` — since v3.4.0 batch3
232
212
  // Task 7 that is `stack.isDirty() || burstHasUncommittedEdit() ||
233
213
  // waveUnwrittenEdit` — arrives as ctx.dirty and is not recomputed here.
234
- // This runs before the source-mode override below so that override can
235
- // still give save its own exemption.
236
214
  state.save.disabled = !dirty;
237
215
 
238
- // mode 'source' is the full-document raw-source escape hatch: everything
239
- // is disabled except the mode button that got you there (id 'preview',
240
- // which in v3.2.1 is the edit <-> source toggle), since T11-2 'outline'
241
- // (☰), and since v3.4.0 §3 'save'. The mode button is still the ONLY way
242
- // back to 'edit' — that has not changed. 'outline' stays reachable because
243
- // it is what opens the sidebar drawer (toggleOutlineSidebar() in
244
- // lib/editor/client.js), and the fixed `.sidebar-toggle` button that used
245
- // to reach the drawer another way is hidden in edit mode entirely (see the
246
- // T11-2 comment in lib/md2doc.js) — a disabled ☰ here would leave source
247
- // mode with no way to open that drawer at all. 'save' stays reachable
248
- // because source mode can still hold unsaved changes (the user can type
249
- // into the source textarea) — see the per-button comment below for the
250
- // exact shape of the exemption. This override still runs last and still
251
- // overrides every disabled flag computed above for every other button; do
252
- // not narrow it for anything but these three ids.
253
- if (mode === 'source') {
254
- for (const b of BUTTONS) {
255
- if (b.id === 'preview' || b.id === 'outline') { state[b.id].disabled = false; continue; }
256
- // v3.4.0 §3: save is a third exemption, for the same shape of reason
257
- // outline already had one — source mode can still hold unsaved
258
- // changes (the user can type into the source textarea), so forcing
259
- // the only save button grey here would make a mode that holds
260
- // unsaved work with no way out of it. The exemption is from the MODE
261
- // forcing it grey, not from being grey — a clean document still shows
262
- // it disabled in source mode (see the `state.save.disabled = !dirty`
263
- // line above, which already ran and is left untouched here).
264
- if (b.id === 'save') continue;
265
- state[b.id].disabled = true;
266
- }
267
- }
268
-
269
216
  return state;
270
217
  }
271
218
 
@@ -0,0 +1,35 @@
1
+ 'use strict';
2
+ // v3.10.0: syntax colour for fenced code, done once when the HTML is written —
3
+ // the reading page needs no script for it, and PDF gets it for free. Only the
4
+ // languages below are registered; anything else (or no language) stays plain
5
+ // text rather than guessing.
6
+ const hljs = require('highlight.js/lib/core');
7
+
8
+ const LANGS = {
9
+ verilog: require('highlight.js/lib/languages/verilog'),
10
+ python: require('highlight.js/lib/languages/python'),
11
+ bash: require('highlight.js/lib/languages/bash'),
12
+ c: require('highlight.js/lib/languages/c'),
13
+ cpp: require('highlight.js/lib/languages/cpp'),
14
+ json: require('highlight.js/lib/languages/json'),
15
+ yaml: require('highlight.js/lib/languages/yaml'),
16
+ tcl: require('highlight.js/lib/languages/tcl'),
17
+ makefile: require('highlight.js/lib/languages/makefile'),
18
+ javascript: require('highlight.js/lib/languages/javascript'),
19
+ diff: require('highlight.js/lib/languages/diff'),
20
+ plaintext: require('highlight.js/lib/languages/plaintext'),
21
+ };
22
+ for (const [name, def] of Object.entries(LANGS)) hljs.registerLanguage(name, def);
23
+ hljs.registerAliases(['systemverilog', 'sv', 'v'], { languageName: 'verilog' });
24
+ hljs.registerAliases(['sh', 'shell', 'zsh'], { languageName: 'bash' });
25
+
26
+ // `lang` is the fence's info string; only its first word names the language,
27
+ // case-insensitively. Returns escaped, coloured HTML, or null when the
28
+ // language is missing or not registered.
29
+ function highlightCode(code, lang) {
30
+ const name = String(lang || '').trim().split(/\s+/)[0].toLowerCase();
31
+ if (!name || !hljs.getLanguage(name)) return null;
32
+ return hljs.highlight(code, { language: name, ignoreIllegals: true }).value;
33
+ }
34
+
35
+ module.exports = { highlightCode };