@helping-ai-workflow/md2doc 3.14.1 → 3.15.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,131 @@
1
+ 'use strict';
2
+ /* UMD, same shape as convert-md.js: `require`-able in node for the unit tests
3
+ (test/block-catalog.test.js), and injected into the editor page as
4
+ `window.md2docBlockCatalog` (lib/editor/server.js). Interpolated raw into a
5
+ <script> tag, so it must never contain a closing script tag. */
6
+ (function (root, factory) {
7
+ if (typeof module === 'object' && module.exports) module.exports = factory();
8
+ else root.md2docBlockCatalog = factory();
9
+ })(typeof self !== 'undefined' ? self : this, function () {
10
+
11
+ // v3.15.0 T1: the block catalogue behind the "/" menu (spec 3.4, decision D5).
12
+ // Pure data plus the menu's filter: no DOM, no editor state.
13
+ //
14
+ // `lines` is the source an item writes when it lands in an empty paragraph,
15
+ // measured against the renderer (ledger 2026-10-18):
16
+ // * headings carry no trailing space (`##` lexes as an empty h2);
17
+ // * the empty to-do is `- [ ] ` WITH its trailing space — without it the
18
+ // item renders as a plain bullet;
19
+ // * math is `$$` / `x^2` / `$$` — `$$` / blank / `$$` renders as two
20
+ // paragraphs;
21
+ // * the table is 3 columns by 2 body rows, one skeleton for "/", + and the
22
+ // toolbar alike.
23
+ // `pristine` is true when the lines carry no sample (an empty skeleton the
24
+ // user has not filled in yet; abandoned untouched, it is removed again) and
25
+ // false for the four items that insert a working sample.
26
+
27
+ const SLASH_GROUPS = [
28
+ { id: 'basic', label: '基本' },
29
+ { id: 'list', label: '清單' },
30
+ { id: 'block', label: '區塊' },
31
+ { id: 'diagram', label: '圖表' },
32
+ ];
33
+
34
+ const FENCE = '\x60\x60\x60';
35
+ const TABLE_SKELETON = ['| A | B | C |', '|---|---|---|', '| | | |', '| | | |'];
36
+
37
+ function item(id, group, label, hint, icon, keywords, lines, kind) {
38
+ return { id, group, label, hint, icon, keywords, lines, kind, pristine: kind !== 'fence' };
39
+ }
40
+
41
+ const SLASH_ITEMS = [
42
+ item('text', 'basic', '文字', '', 'text', 'text paragraph p 段落', null, 'text'),
43
+ item('h1', 'basic', '標題 1', '#', 'h1', 'h1 heading title', ['#'], 'heading'),
44
+ item('h2', 'basic', '標題 2', '##', 'h2', 'h2 heading', ['##'], 'heading'),
45
+ item('h3', 'basic', '標題 3', '###', 'h3', 'h3 heading', ['###'], 'heading'),
46
+ item('ul', 'list', '項目符號列表', '-', 'ul', 'bullet ul list 清單 項目', ['-'], 'list'),
47
+ item('ol', 'list', '編號列表', '1.', 'ol', 'ordered ol number list 編號 清單', ['1.'], 'list'),
48
+ item('task', 'list', '待辦清單', '[]', 'task', 'todo task check checkbox 待辦', ['- [ ] '], 'list'),
49
+ item('quote', 'block', '引用', '>', 'quote', 'quote blockquote', ['>'], 'quote'),
50
+ item('code', 'block', '程式碼', FENCE, 'code', 'code codeblock 程式', [FENCE, '', FENCE], 'code'),
51
+ item('divider', 'block', '分隔線', '---', 'minus', 'divider hr line rule 分隔', ['---'], 'divider'),
52
+ item('table', 'block', '表格', '', 'table', 'table 表', TABLE_SKELETON.slice(), 'table'),
53
+ item('image', 'block', '圖片', '', 'image', 'image img picture 圖', null, 'image'),
54
+ item('mermaid', 'diagram', 'Mermaid 圖', '', 'mermaid', 'mermaid flowchart diagram 流程 圖',
55
+ [FENCE + 'mermaid', 'graph TD', ' A --> B', FENCE], 'fence'),
56
+ item('graphviz', 'diagram', 'Graphviz 圖', '', 'graphviz', 'graphviz dot graph diagram 圖',
57
+ [FENCE + 'dot', 'digraph { a -> b }', FENCE], 'fence'),
58
+ item('wavedrom', 'diagram', 'WaveDrom 波形', '', 'wavedrom', 'wavedrom wave timing 波形',
59
+ [FENCE + 'wavedrom', "{ signal: [{ name: 'clk', wave: 'p....' }] }", FENCE], 'fence'),
60
+ item('math', 'diagram', '數學公式', '$$', 'math', 'math equation latex tex 公式', ['$$', 'x^2', '$$'], 'fence'),
61
+ ];
62
+
63
+ // D7: a case-insensitive PREFIX match against every whitespace-split token of
64
+ // the label and the keywords. Prefix, not substring: 「表」 must find only
65
+ // 表格, and a substring match would also hit 項目符號列表 and 編號列表.
66
+ function tokensOf(it) {
67
+ return (it.label + ' ' + it.keywords).toLowerCase().split(/\s+/).filter(Boolean);
68
+ }
69
+ function filterSlashItems(query) {
70
+ const q = String(query || '').toLowerCase();
71
+ if (!q) return SLASH_ITEMS.slice();
72
+ return SLASH_ITEMS.filter((it) => tokensOf(it).some((t) => t.indexOf(q) === 0));
73
+ }
74
+
75
+ function itemById(id) {
76
+ for (let i = 0; i < SLASH_ITEMS.length; i++) if (SLASH_ITEMS[i].id === id) return SLASH_ITEMS[i];
77
+ return null;
78
+ }
79
+
80
+ // v3.15.0 T3: the Markdown shortcuts (spec 3.3, D3). `textBeforeCaret` is the
81
+ // paragraph's whole text in front of the caret, which must BE the marker —
82
+ // `a## ` or ` ## ` is ordinary text. A spaced marker converts on its space
83
+ // (U+0020, or the U+00A0 a contenteditable often types for a trailing one)
84
+ // whatever follows the caret; the two fence-like markers only when nothing
85
+ // does (`restIsEmpty`), since the text after them has nowhere to go.
86
+ const SPACE = '[ \u00a0]';
87
+ const SHORTCUT_RULES = [
88
+ { re: new RegExp('^(#{1,6})' + SPACE + '$'), make: (m) => ({ kind: 'heading', depth: m[1].length }) },
89
+ { re: new RegExp('^[-*]' + SPACE + '$'), make: () => ({ kind: 'ul' }) },
90
+ { re: new RegExp('^(\\d{1,9})\\.' + SPACE + '$'), make: (m) => ({ kind: 'ol', start: Number(m[1]) }) },
91
+ { re: new RegExp('^\\[ ?\\]' + SPACE + '$'), make: () => ({ kind: 'task' }) },
92
+ { re: new RegExp('^>' + SPACE + '$'), make: () => ({ kind: 'quote' }) },
93
+ ];
94
+ function matchShortcut(textBeforeCaret, restIsEmpty) {
95
+ const t = String(textBeforeCaret);
96
+ for (let i = 0; i < SHORTCUT_RULES.length; i++) {
97
+ const m = SHORTCUT_RULES[i].re.exec(t);
98
+ if (m) return SHORTCUT_RULES[i].make(m);
99
+ }
100
+ if (!restIsEmpty) return null;
101
+ if (t === FENCE) return { kind: 'code' };
102
+ if (t === '---') return { kind: 'divider' };
103
+ return null;
104
+ }
105
+
106
+ // The lines a match writes. With nothing after the caret (`restLines` empty,
107
+ // or one empty line) it is the D5 skeleton — the same lines the "/" menu's
108
+ // item writes. Otherwise the remainder becomes the block's content, the way
109
+ // convert-md.js's emitAs() writes a conversion: a heading joins its lines
110
+ // with a space (a heading is one line), a list item puts its continuation
111
+ // lines under its marker, a quote prefixes every line.
112
+ function shortcutLines(match, restLines) {
113
+ const rest = Array.isArray(restLines) ? restLines : [];
114
+ const empty = rest.length === 0 || (rest.length === 1 && rest[0] === '');
115
+ const k = match.kind;
116
+ if (k === 'heading') {
117
+ const hashes = '#'.repeat(match.depth);
118
+ return [empty ? hashes : hashes + ' ' + rest.map((l) => l.trim()).join(' ').trim()];
119
+ }
120
+ if (k === 'code') return empty ? [FENCE, '', FENCE] : [FENCE].concat(rest, [FENCE]);
121
+ if (k === 'divider') return ['---'];
122
+ if (k === 'quote') return empty ? ['>'] : rest.map((l) => '> ' + l);
123
+ const bullet = k === 'ol' ? String(match.start) + '.' : '-';
124
+ if (empty) return [k === 'task' ? '- [ ] ' : bullet];
125
+ const marker = (k === 'task' ? '- [ ]' : bullet) + ' ';
126
+ const cont = ' '.repeat(bullet.length + 1);
127
+ return [marker + rest[0]].concat(rest.slice(1).map((l) => cont + l));
128
+ }
129
+
130
+ return { SLASH_GROUPS, SLASH_ITEMS, filterSlashItems, itemById, TABLE_SKELETON, matchShortcut, shortcutLines };
131
+ });
@@ -1,5 +1,8 @@
1
1
  'use strict';
2
2
  const { marked } = require('marked');
3
+ // v3.15.0 B2 fix round 1 (R56): the empty to-do token rule, shared with the
4
+ // reader renderer (lib/task-items.js).
5
+ require('../task-items.js');
3
6
 
4
7
  // Top-level marked tokens → 1-indexed inclusive line ranges.
5
8
  // `space` tokens (blank-line runs) advance the cursor but are not blocks.