@helping-ai-workflow/md2doc 3.9.1 → 3.11.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/client.js +1026 -437
- package/lib/editor/convert-md.js +18 -11
- package/lib/editor/icons.js +48 -0
- package/lib/editor/inline-md.js +8 -0
- package/lib/editor/list-md.js +26 -1
- package/lib/editor/server.js +14 -4
- package/lib/editor/toolbar-model.js +27 -80
- package/lib/highlight.js +35 -0
- package/lib/md2doc.js +234 -147
- package/lib/theme/runtime.js +80 -2
- package/lib/theme/tokens.js +74 -5
- package/package.json +4 -3
- package/lib/editor/docsource.js +0 -115
package/lib/editor/convert-md.js
CHANGED
|
@@ -91,19 +91,24 @@ function stripMarker(sourceLines, blockKind) {
|
|
|
91
91
|
if (blockKind === 'li') {
|
|
92
92
|
const m = src[0].match(LI_MARKER);
|
|
93
93
|
if (!m) return { content: [], ok: false };
|
|
94
|
-
// A MULTI-LINE li refuses. The previous code flattened continuations with
|
|
95
|
-
// replace(/^\s+/, ''), which is exactly the guess the code branch below
|
|
96
|
-
// refuses to make for an indented code block -- and it silently destroys
|
|
97
|
-
// whatever structure the continuation carried (a nested fence, an indented
|
|
98
|
-
// sub-block). Today the \u00a74.1 gate refuses a multi-line li as an operation
|
|
99
|
-
// target before this is ever reached, so this is defence in depth; dead
|
|
100
|
-
// code in this repo has a history of later gaining a caller, and it must
|
|
101
|
-
// not be a mine when it does.
|
|
102
|
-
if (src.length > 1) return { content: [], ok: false };
|
|
103
94
|
let first = src[0].slice(m[0].length);
|
|
104
95
|
const box = first.match(TASK_BOX);
|
|
105
96
|
if (box) first = first.slice(box[0].length);
|
|
106
|
-
|
|
97
|
+
// v3.11: a MULTI-LINE li (a hard-wrapped item, or a hard break) converts
|
|
98
|
+
// too. Its continuation lines belong to the item's own paragraph — any
|
|
99
|
+
// nested block would have made the run unsupported and refused before
|
|
100
|
+
// this — so each loses exactly the item's CONTENT column (the marker's
|
|
101
|
+
// width: anything deeper keeps its relative indent) or, on a lazy line
|
|
102
|
+
// indented less than that, all of its leading whitespace. That is not the
|
|
103
|
+
// old blanket replace(/^\s+/, '') the code branch below refuses to guess
|
|
104
|
+
// with: an over-indented line keeps its extra columns.
|
|
105
|
+
const col = m[0].length;
|
|
106
|
+
const rest = src.slice(1).map((l) => {
|
|
107
|
+
let k = 0;
|
|
108
|
+
while (k < col && l.charAt(k) === ' ') k++;
|
|
109
|
+
return k === col ? l.slice(col) : l.replace(/^\s+/, '');
|
|
110
|
+
});
|
|
111
|
+
return { content: [first].concat(rest), ok: true };
|
|
107
112
|
}
|
|
108
113
|
|
|
109
114
|
if (blockKind === 'heading') {
|
|
@@ -160,7 +165,9 @@ function emitAs(content, target, opts) {
|
|
|
160
165
|
// A heading is one line by definition. Joining is the only lossless move
|
|
161
166
|
// available; splitting would make one gesture produce two blocks.
|
|
162
167
|
const hashes = '#'.repeat(Number(target.slice(1)));
|
|
163
|
-
|
|
168
|
+
// A multi-line body (a converted hard-wrapped item) becomes one heading
|
|
169
|
+
// line; trailing hard-break spaces would otherwise land mid-heading.
|
|
170
|
+
const text = body.length > 1 ? body.map((l) => l.trim()).join(' ') : body.join(' ');
|
|
164
171
|
// An EMPTY heading emits no trailing space, or converting '##' to a
|
|
165
172
|
// heading of the same level would rewrite the line to '## '.
|
|
166
173
|
return [text === '' ? prefix + hashes : prefix + hashes + ' ' + text];
|
|
@@ -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
|
+
});
|
package/lib/editor/inline-md.js
CHANGED
|
@@ -106,6 +106,14 @@
|
|
|
106
106
|
|
|
107
107
|
function serializeAnchor(node, unsupported) {
|
|
108
108
|
const href = node.getAttribute('href');
|
|
109
|
+
// A raw HTML anchor (`<a id="ref-x"></a>`, NO href attribute) is not a
|
|
110
|
+
// markdown link: emitting '[label]()' for it erased the anchor on the
|
|
111
|
+
// first edit of its block. Unsupported, so the block keeps its source.
|
|
112
|
+
// An empty href ('' — from '[x]()') is still a link.
|
|
113
|
+
if (href === null) {
|
|
114
|
+
unsupported.push('A');
|
|
115
|
+
return '';
|
|
116
|
+
}
|
|
109
117
|
const text = node.textContent;
|
|
110
118
|
// citation: <a href="#slug">[body]</a> -> [[body]]
|
|
111
119
|
if (href && href.charAt(0) === '#' && /^\[.*\]$/.test(text)) {
|
package/lib/editor/list-md.js
CHANGED
|
@@ -450,6 +450,15 @@
|
|
|
450
450
|
// replace from rewriting lines the user never looked at.
|
|
451
451
|
function serializeBlocks(blockEls, opts) {
|
|
452
452
|
const carryOver = (opts && opts.carryOver) || null;
|
|
453
|
+
// v3.11 (batch-2 §6): `opts.blankBefore(blockEl, prevItemEl)` — true when a
|
|
454
|
+
// blank line separates this item from the item emitted before it. The
|
|
455
|
+
// CALLER decides (client.js reads it off the file: a loose list keeps the
|
|
456
|
+
// blank lines it had, and a new neighbour takes the spacing of the list it
|
|
457
|
+
// joins). The blank line gets a lineMeta entry with blockId null and
|
|
458
|
+
// `blank: true`, so
|
|
459
|
+
// `lineMeta.length === md.split('\n').length` still holds.
|
|
460
|
+
const blankBefore = (opts && typeof opts.blankBefore === 'function') ? opts.blankBefore : null;
|
|
461
|
+
let prevItemEl = null;
|
|
453
462
|
const unsupported = [];
|
|
454
463
|
const unsupportedByLi = [];
|
|
455
464
|
const multiLineBlockIds = [];
|
|
@@ -586,8 +595,16 @@
|
|
|
586
595
|
// keep. (isBlankBlockText() stays as-is for the block's own direct
|
|
587
596
|
// children, where the template's inter-element whitespace can legally
|
|
588
597
|
// be a single space with no newline.)
|
|
589
|
-
|
|
598
|
+
let inlineKids = [];
|
|
590
599
|
allChildNodes(textEl).forEach((c) => { if (!isBlankText(c)) inlineKids.push(c); });
|
|
600
|
+
// v3.11 (batch-2 §6): a LOOSE item's surface is one <p> — its content
|
|
601
|
+
// is that paragraph's, and the blank lines that made the list loose
|
|
602
|
+
// are restated between items (opts.blankBefore below), not carried by
|
|
603
|
+
// the <p>. Two or more <p> (a multi-paragraph item) still reach the
|
|
604
|
+
// inline serializer whole and stay unsupported.
|
|
605
|
+
if (inlineKids.length === 1 && inlineKids[0].nodeType === 1 && inlineKids[0].nodeName === 'P') {
|
|
606
|
+
inlineKids = allChildNodes(inlineKids[0]);
|
|
607
|
+
}
|
|
591
608
|
const res = inlineMd.serializeInline({ childNodes: inlineKids });
|
|
592
609
|
itemMd = res.md;
|
|
593
610
|
res.unsupported.forEach((u) => innerUnsupported.push(u));
|
|
@@ -709,6 +726,14 @@
|
|
|
709
726
|
// Round 6: join an open task prefix (above) to this line, dropping the
|
|
710
727
|
// columns it already wrote; then, if THIS item is itself a content-free
|
|
711
728
|
// task item, become the prefix instead of emitting.
|
|
729
|
+
// A line joined onto an open task prefix is the same physical line, so
|
|
730
|
+
// it never takes a blank in front of it.
|
|
731
|
+
const blankHere = !pending && lines.length > 0 && !!blankBefore && !!blankBefore(blockEl, prevItemEl);
|
|
732
|
+
prevItemEl = blockEl;
|
|
733
|
+
if (blankHere) {
|
|
734
|
+
lines.push('');
|
|
735
|
+
lineMeta.push({ blockId: null, indentPrefix: '', marker: '', blank: true });
|
|
736
|
+
}
|
|
712
737
|
let head = indentPrefix + marker;
|
|
713
738
|
// `indentPrefix` in lineMeta is the text PHYSICALLY standing before this
|
|
714
739
|
// line's own marker. Normally that is the accumulated width; on a line a
|
package/lib/editor/server.js
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
15
|
-
// browser-reachable export path) plus the v3.4.0 §3 `save` button.
|
|
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: '
|
|
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: '
|
|
24
|
-
{ id: 'redo', group: 'history', label: 'Redo', icon: '
|
|
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: '
|
|
28
|
-
{ id: 'quote', group: 'block', label: '❝', icon: '
|
|
29
|
-
{ id: 'code', group: 'block', label: '</>', icon: '
|
|
30
|
-
{ id: 'list', group: 'block', label: '•', icon: '
|
|
31
|
-
{ id: 'ordered-list', group: 'block', label: '1.', icon: '
|
|
32
|
-
{ id: 'check', group: 'block', label: '☑', icon: '
|
|
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: '
|
|
36
|
-
{ id: 'italic', group: 'inline', label: 'I', icon: '
|
|
37
|
-
{ id: 'strike', group: 'inline', label: 'S', icon: '
|
|
38
|
-
{ id: 'inline-code', group: 'inline', label: '`', icon: '
|
|
39
|
-
{ id: 'link', group: 'inline', label: '🔗', icon: '
|
|
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: '
|
|
43
|
-
{ id: 'indent', group: 'indent', label: '⇥', icon: '
|
|
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: '
|
|
47
|
-
{ id: 'insert-before', group: 'insert', label: '⬆', icon: '
|
|
48
|
-
{ id: 'insert-after', group: 'insert', label: '⬇', icon: '
|
|
49
|
-
{ id: 'line', group: 'insert', label: '―', icon: '
|
|
50
|
-
{ id: 'image', group: 'insert', label: '🖼', icon: '
|
|
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: '
|
|
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', '
|
|
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
|
-
//
|
|
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
|
|
package/lib/highlight.js
ADDED
|
@@ -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 };
|