@helping-ai-workflow/md2doc 3.14.1 → 3.16.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.
- package/lib/editor/block-catalog.js +131 -0
- package/lib/editor/blockmap.js +3 -0
- package/lib/editor/client.js +3185 -633
- package/lib/editor/inline-md.js +20 -1
- package/lib/editor/lineops.js +6 -0
- package/lib/editor/list-md.js +11 -5
- package/lib/editor/server.js +40 -1
- package/lib/editor/toolbar-model.js +5 -3
- package/lib/md2doc.js +231 -28
- package/lib/task-items.js +48 -0
- package/package.json +2 -2
|
@@ -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
|
+
});
|
package/lib/editor/blockmap.js
CHANGED
|
@@ -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.
|