@helping-ai-workflow/md2doc 3.16.0 → 3.17.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 +2459 -106
- package/lib/editor/code-edit.js +184 -0
- package/lib/editor/icons.js +3 -0
- package/lib/editor/inline-md.js +105 -1
- package/lib/editor/server.js +30 -2
- package/lib/highlight.js +46 -5
- package/lib/md2doc.js +257 -39
- package/lib/theme/runtime.js +33 -2
- package/package.json +2 -2
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
/* UMD, same shape as convert-md.js: `require`-able in node for the unit tests
|
|
3
|
+
(test/code-edit.test.js), and injected into the editor page as
|
|
4
|
+
`window.md2docCodeEdit` (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.md2docCodeEdit = factory();
|
|
9
|
+
})(typeof self !== 'undefined' ? self : this, function () {
|
|
10
|
+
|
|
11
|
+
// v3.17.0 T1: the pure core of the in-place code editor (decisions D3, D7,
|
|
12
|
+
// D10). No DOM, no editor state.
|
|
13
|
+
|
|
14
|
+
// D7: the languages the highlighter registers (lib/highlight.js LANGS, same
|
|
15
|
+
// order), each with its display name and every alias that resolves to it —
|
|
16
|
+
// highlight.js's own built-in aliases plus the two custom lists. A unit row
|
|
17
|
+
// keeps this table equal to the server's.
|
|
18
|
+
const CODE_LANGS = [
|
|
19
|
+
{ id: 'verilog', label: 'Verilog', aliases: ['v', 'sv', 'svh', 'systemverilog'] },
|
|
20
|
+
{ id: 'python', label: 'Python', aliases: ['py', 'gyp', 'ipython'] },
|
|
21
|
+
{ id: 'bash', label: 'Bash', aliases: ['sh', 'zsh', 'shell'] },
|
|
22
|
+
{ id: 'c', label: 'C', aliases: ['h'] },
|
|
23
|
+
{ id: 'cpp', label: 'C++', aliases: ['cc', 'c++', 'h++', 'hpp', 'hh', 'hxx', 'cxx'] },
|
|
24
|
+
{ id: 'json', label: 'JSON', aliases: ['jsonc'] },
|
|
25
|
+
{ id: 'yaml', label: 'YAML', aliases: ['yml'] },
|
|
26
|
+
{ id: 'tcl', label: 'Tcl', aliases: ['tk'] },
|
|
27
|
+
{ id: 'makefile', label: 'Makefile', aliases: ['mk', 'mak', 'make'] },
|
|
28
|
+
{ id: 'javascript', label: 'JavaScript', aliases: ['js', 'jsx', 'mjs', 'cjs'] },
|
|
29
|
+
{ id: 'diff', label: 'Diff', aliases: ['patch'] },
|
|
30
|
+
{ id: '', label: '純文字', aliases: ['plaintext', 'text', 'txt'] },
|
|
31
|
+
];
|
|
32
|
+
|
|
33
|
+
function firstWord(info) {
|
|
34
|
+
return String(info || '').trim().split(/\s+/)[0] || '';
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// The chip's text for a fence info string: the first word's language name,
|
|
38
|
+
// aliases resolved; empty is 純文字; an unknown word is shown as written.
|
|
39
|
+
function langLabel(info) {
|
|
40
|
+
const word = firstWord(info);
|
|
41
|
+
if (!word) return '純文字';
|
|
42
|
+
const w = word.toLowerCase();
|
|
43
|
+
for (const l of CODE_LANGS) {
|
|
44
|
+
if ((l.id && l.id === w) || l.aliases.indexOf(w) !== -1) return l.label;
|
|
45
|
+
}
|
|
46
|
+
return word;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// The language menu's filter: case-insensitive prefix on id, label or alias.
|
|
50
|
+
function filterLangs(query) {
|
|
51
|
+
const q = String(query || '').trim().toLowerCase();
|
|
52
|
+
if (!q) return CODE_LANGS.slice();
|
|
53
|
+
return CODE_LANGS.filter((l) =>
|
|
54
|
+
(l.id && l.id.indexOf(q) === 0) || l.label.toLowerCase().indexOf(q) === 0 ||
|
|
55
|
+
l.aliases.some((a) => a.indexOf(q) === 0));
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Replace the info string's first word; '' removes it. The other words are
|
|
59
|
+
// kept, single-spaced.
|
|
60
|
+
function setFenceLang(info, lang) {
|
|
61
|
+
const words = String(info || '').trim().split(/\s+/).filter(Boolean);
|
|
62
|
+
const rest = words.slice(1);
|
|
63
|
+
return (lang ? [lang].concat(rest) : rest).join(' ');
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// blockLines[0] must open a fence (up to 3 spaces, then 3+ backticks or
|
|
67
|
+
// tildes). CommonMark: a backtick fence's info string may not hold a backtick
|
|
68
|
+
// (that line is inline code). Body lines are returned raw (indentation
|
|
69
|
+
// included); the closer is the first line of 0-3 spaces + the same char at
|
|
70
|
+
// least `open` times + nothing else. No closer: the block runs to the end.
|
|
71
|
+
function parseFence(blockLines) {
|
|
72
|
+
if (!blockLines || !blockLines.length) return null;
|
|
73
|
+
const m = /^( {0,3})(`{3,}|~{3,})(.*)$/.exec(blockLines[0]);
|
|
74
|
+
if (!m) return null;
|
|
75
|
+
const char = m[2][0];
|
|
76
|
+
const info = m[3].trim();
|
|
77
|
+
if (char === '`' && info.indexOf('`') !== -1) return null;
|
|
78
|
+
const open = m[2].length;
|
|
79
|
+
const closer = new RegExp('^ {0,3}' + (char === '`' ? '`' : '~') + '{' + open + ',}[ \\t]*$');
|
|
80
|
+
const body = [];
|
|
81
|
+
let closed = false;
|
|
82
|
+
for (let i = 1; i < blockLines.length; i++) {
|
|
83
|
+
if (closer.test(blockLines[i])) { closed = true; break; }
|
|
84
|
+
body.push(blockLines[i]);
|
|
85
|
+
}
|
|
86
|
+
return { indent: m[1], char, open, info, body, closed };
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// D10: n = max(3, the opening length, the longest run of the fence char
|
|
90
|
+
// anywhere in the body + 1). The body is written as given.
|
|
91
|
+
function buildFence(f) {
|
|
92
|
+
let longest = 0;
|
|
93
|
+
const re = f.char === '~' ? /~+/g : /`+/g;
|
|
94
|
+
for (const line of f.body) {
|
|
95
|
+
let r;
|
|
96
|
+
re.lastIndex = 0;
|
|
97
|
+
while ((r = re.exec(line))) if (r[0].length > longest) longest = r[0].length;
|
|
98
|
+
}
|
|
99
|
+
const n = Math.max(3, f.open || 3, longest + 1);
|
|
100
|
+
const bar = f.indent + f.char.repeat(n);
|
|
101
|
+
return [bar + f.info].concat(f.body, [bar]);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// D3 Enter: a newline plus the current line's leading whitespace (only as far
|
|
105
|
+
// as the caret when the caret sits inside the indentation).
|
|
106
|
+
function newlineWithIndent(text, pos) {
|
|
107
|
+
const lineStart = text.lastIndexOf('\n', pos - 1) + 1;
|
|
108
|
+
const ws = /^[ \t]*/.exec(text.slice(lineStart))[0];
|
|
109
|
+
const indent = ws.slice(0, Math.max(0, Math.min(ws.length, pos - lineStart)));
|
|
110
|
+
return { text: text.slice(0, pos) + '\n' + indent + text.slice(pos), caret: pos + 1 + indent.length };
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// D3 Tab / Shift+Tab over a selection (R42). Touched lines are those that
|
|
114
|
+
// intersect [selStart, selEnd]; when the selection is not collapsed and its
|
|
115
|
+
// end sits at column 0 of a line, that line is not touched. Indent adds two
|
|
116
|
+
// spaces; dedent removes up to two leading spaces. A position on a touched
|
|
117
|
+
// line keeps its column relative to the line's text (indent: a column-0
|
|
118
|
+
// position moves with the text, unlike VS Code; dedent: clamps at the line
|
|
119
|
+
// start); positions on other lines just follow the text.
|
|
120
|
+
function indentLines(text, selStart, selEnd, dedent) {
|
|
121
|
+
const lines = text.split('\n');
|
|
122
|
+
const starts = [];
|
|
123
|
+
let off = 0;
|
|
124
|
+
for (const l of lines) { starts.push(off); off += l.length + 1; }
|
|
125
|
+
const touched = lines.map((l, i) => {
|
|
126
|
+
const s = starts[i], e = s + l.length;
|
|
127
|
+
if (!(s <= selEnd && e >= selStart)) return false;
|
|
128
|
+
if (selEnd > selStart && s === selEnd) return false;
|
|
129
|
+
return true;
|
|
130
|
+
});
|
|
131
|
+
const delta = lines.map((l, i) => {
|
|
132
|
+
if (!touched[i]) return 0;
|
|
133
|
+
if (!dedent) return 2;
|
|
134
|
+
const lead = /^ */.exec(l)[0].length;
|
|
135
|
+
return -Math.min(2, lead);
|
|
136
|
+
});
|
|
137
|
+
const newStarts = [];
|
|
138
|
+
let n = 0;
|
|
139
|
+
for (let i = 0; i < lines.length; i++) { newStarts.push(n); n += lines[i].length + delta[i] + 1; }
|
|
140
|
+
const out = lines.map((l, i) => (delta[i] > 0 ? ' ' + l : delta[i] < 0 ? l.slice(-delta[i]) : l));
|
|
141
|
+
function map(pos) {
|
|
142
|
+
let i = lines.length - 1;
|
|
143
|
+
while (i > 0 && starts[i] > pos) i--;
|
|
144
|
+
const col = pos - starts[i];
|
|
145
|
+
return newStarts[i] + (delta[i] < 0 ? Math.max(0, col + delta[i]) : col + delta[i]);
|
|
146
|
+
}
|
|
147
|
+
return { text: out.join('\n'), selStart: map(selStart), selEnd: map(selEnd) };
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// The edit's own undo stack (native undo dies when re-colouring replaces the
|
|
151
|
+
// innerHTML). Snap = { text, selStart, selEnd }. A push whose text equals the
|
|
152
|
+
// current one is not a step; a push drops the redo tail; the stack keeps the
|
|
153
|
+
// last `cap` snapshots.
|
|
154
|
+
class TextHistory {
|
|
155
|
+
constructor(initial, cap) {
|
|
156
|
+
this.cap = cap || 200;
|
|
157
|
+
this.stack = [initial];
|
|
158
|
+
this.i = 0;
|
|
159
|
+
}
|
|
160
|
+
push(snap) {
|
|
161
|
+
if (snap.text === this.stack[this.i].text) {
|
|
162
|
+
this.stack[this.i] = snap;
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
this.stack.length = this.i + 1;
|
|
166
|
+
this.stack.push(snap);
|
|
167
|
+
if (this.stack.length > this.cap) this.stack.shift();
|
|
168
|
+
this.i = this.stack.length - 1;
|
|
169
|
+
}
|
|
170
|
+
undo() {
|
|
171
|
+
if (this.i <= 0) return null;
|
|
172
|
+
this.i--;
|
|
173
|
+
return this.stack[this.i];
|
|
174
|
+
}
|
|
175
|
+
redo() {
|
|
176
|
+
if (this.i >= this.stack.length - 1) return null;
|
|
177
|
+
this.i++;
|
|
178
|
+
return this.stack[this.i];
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
return { CODE_LANGS, langLabel, filterLangs, parseFence, buildFence, setFenceLang,
|
|
183
|
+
newlineWithIndent, indentLines, TextHistory };
|
|
184
|
+
});
|
package/lib/editor/icons.js
CHANGED
|
@@ -11,8 +11,10 @@ const BODY = {
|
|
|
11
11
|
"arrow-up-to-line": "<path d=\"M5 3h14\" /><path d=\"m18 13-6-6-6 6\" /><path d=\"M12 7v14\" />",
|
|
12
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
13
|
"check": "<path d=\"M20 6 9 17l-5-5\" />",
|
|
14
|
+
"chevron-down": "<path d=\"m6 9 6 6 6-6\" />",
|
|
14
15
|
"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
16
|
"code": "<polyline points=\"16 18 22 12 16 6\" /><polyline points=\"8 6 2 12 8 18\" />",
|
|
17
|
+
"copy": "<rect width=\"14\" height=\"14\" x=\"8\" y=\"8\" rx=\"2\" ry=\"2\" /><path d=\"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2\" />",
|
|
16
18
|
"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
19
|
"heading": "<path d=\"M6 12h12\" /><path d=\"M6 20V4\" /><path d=\"M18 20V4\" />",
|
|
18
20
|
"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\" />",
|
|
@@ -26,6 +28,7 @@ const BODY = {
|
|
|
26
28
|
"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
29
|
"minus": "<path d=\"M5 12h14\" />",
|
|
28
30
|
"panel-left": "<rect width=\"18\" height=\"18\" x=\"3\" y=\"3\" rx=\"2\" /><path d=\"M9 3v18\" />",
|
|
31
|
+
"pencil": "<path d=\"M21.174 6.812a1 1 0 0 0-3.986-3.987L3.842 16.174a2 2 0 0 0-.5.83l-1.321 4.352a.5.5 0 0 0 .623.622l4.353-1.32a2 2 0 0 0 .83-.497z\" /><path d=\"m15 5 4 4\" />",
|
|
29
32
|
"plus": "<path d=\"M5 12h14\" /><path d=\"M12 5v14\" />",
|
|
30
33
|
"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
34
|
"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\" />",
|
package/lib/editor/inline-md.js
CHANGED
|
@@ -442,5 +442,109 @@
|
|
|
442
442
|
return /^https?:\/\/\S+$/.test(String(text == null ? '' : text).trim());
|
|
443
443
|
}
|
|
444
444
|
|
|
445
|
-
|
|
445
|
+
// ── v3.17.0 T7 (D17): the image paragraph ───────────────────────────────
|
|
446
|
+
// A paragraph that is one `` and nothing else. scanImage
|
|
447
|
+
// finds the three spans a rewrite may touch (the alt between the brackets,
|
|
448
|
+
// the destination, everything else is left byte-for-byte): the title with
|
|
449
|
+
// its quote style and the whitespace around it, the leading and trailing
|
|
450
|
+
// whitespace of the line. A linked image, a reference image (`![a][r]`),
|
|
451
|
+
// text around the image or a second image is not one.
|
|
452
|
+
function scanImage(text) {
|
|
453
|
+
const s = String(text == null ? '' : text);
|
|
454
|
+
const m = /^[ \t]*!\[/.exec(s);
|
|
455
|
+
if (!m) return null;
|
|
456
|
+
let i = m[0].length;
|
|
457
|
+
const altStart = i;
|
|
458
|
+
let depth = 0;
|
|
459
|
+
for (; i < s.length; i++) {
|
|
460
|
+
const c = s[i];
|
|
461
|
+
if (c === '\\') { i++; continue; }
|
|
462
|
+
if (c === '\n') return null;
|
|
463
|
+
if (c === '[') depth++;
|
|
464
|
+
else if (c === ']') { if (depth === 0) break; depth--; }
|
|
465
|
+
}
|
|
466
|
+
if (i >= s.length || s[i] !== ']') return null;
|
|
467
|
+
const altEnd = i;
|
|
468
|
+
i++;
|
|
469
|
+
if (s[i] !== '(') return null;
|
|
470
|
+
i++;
|
|
471
|
+
while (i < s.length && (s[i] === ' ' || s[i] === '\t')) i++;
|
|
472
|
+
const destStart = i;
|
|
473
|
+
let angle = false;
|
|
474
|
+
let destEnd;
|
|
475
|
+
if (s[i] === '<') {
|
|
476
|
+
angle = true;
|
|
477
|
+
i++;
|
|
478
|
+
for (; i < s.length; i++) {
|
|
479
|
+
const c = s[i];
|
|
480
|
+
if (c === '\\') { i++; continue; }
|
|
481
|
+
if (c === '\n' || c === '<') return null;
|
|
482
|
+
if (c === '>') break;
|
|
483
|
+
}
|
|
484
|
+
if (i >= s.length) return null;
|
|
485
|
+
destEnd = i + 1;
|
|
486
|
+
i = destEnd;
|
|
487
|
+
} else {
|
|
488
|
+
let parens = 0;
|
|
489
|
+
for (; i < s.length; i++) {
|
|
490
|
+
const c = s[i];
|
|
491
|
+
if (c === '\\') { i++; continue; }
|
|
492
|
+
if (c === ' ' || c === '\t' || c === '\n') break;
|
|
493
|
+
if (c === '(') parens++;
|
|
494
|
+
else if (c === ')') { if (parens === 0) break; parens--; }
|
|
495
|
+
}
|
|
496
|
+
if (parens !== 0) return null;
|
|
497
|
+
destEnd = i;
|
|
498
|
+
}
|
|
499
|
+
let j = i;
|
|
500
|
+
while (j < s.length && (s[j] === ' ' || s[j] === '\t')) j++;
|
|
501
|
+
let title = null;
|
|
502
|
+
if (j > i && j < s.length && (s[j] === '"' || s[j] === "'" || s[j] === '(')) {
|
|
503
|
+
const close = s[j] === '(' ? ')' : s[j];
|
|
504
|
+
let k = j + 1;
|
|
505
|
+
for (; k < s.length; k++) {
|
|
506
|
+
if (s[k] === '\\') { k++; continue; }
|
|
507
|
+
if (s[k] === close) break;
|
|
508
|
+
}
|
|
509
|
+
if (k >= s.length) return null;
|
|
510
|
+
title = s.slice(j, k + 1);
|
|
511
|
+
j = k + 1;
|
|
512
|
+
while (j < s.length && (s[j] === ' ' || s[j] === '\t')) j++;
|
|
513
|
+
}
|
|
514
|
+
if (s[j] !== ')') return null;
|
|
515
|
+
const tail = s.slice(j + 1);
|
|
516
|
+
if (!/^[ \t]*$/.test(tail)) return null;
|
|
517
|
+
return { s, altStart, altEnd, destStart, destEnd, angle, title };
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
function parseImageParagraph(text) {
|
|
521
|
+
const f = scanImage(text);
|
|
522
|
+
if (!f) return null;
|
|
523
|
+
const rawAlt = f.s.slice(f.altStart, f.altEnd);
|
|
524
|
+
const rawDest = f.s.slice(f.destStart, f.destEnd);
|
|
525
|
+
return {
|
|
526
|
+
alt: rawAlt.replace(/\\([!-\/:-@\[-`{-~])/g, '$1'),
|
|
527
|
+
dest: f.angle ? rawDest.slice(1, -1) : rawDest,
|
|
528
|
+
title: f.title,
|
|
529
|
+
angle: f.angle,
|
|
530
|
+
};
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
// Alt and/or destination replaced, everything else kept. `alt` is plain text
|
|
534
|
+
// (the three characters that would end or bend the brackets gain a
|
|
535
|
+
// backslash); `dest` is written as given, so the caller makes it
|
|
536
|
+
// markdown-ready — an angle-bracketed original loses its brackets.
|
|
537
|
+
function rewriteImage(text, patch) {
|
|
538
|
+
const f = scanImage(text);
|
|
539
|
+
if (!f) return String(text == null ? '' : text);
|
|
540
|
+
const p = patch || {};
|
|
541
|
+
let out = f.s.slice(0, f.altStart);
|
|
542
|
+
out += typeof p.alt === 'string' ? p.alt.replace(/[\\\[\]]/g, '\\$&') : f.s.slice(f.altStart, f.altEnd);
|
|
543
|
+
out += f.s.slice(f.altEnd, f.destStart);
|
|
544
|
+
out += typeof p.dest === 'string' ? p.dest : f.s.slice(f.destStart, f.destEnd);
|
|
545
|
+
out += f.s.slice(f.destEnd);
|
|
546
|
+
return out;
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
return { serializeInline, canWysiwyg, escapeText, escapeBlockStarts, normalizeHref, isPastedUrl, parseImageParagraph, rewriteImage };
|
|
446
550
|
});
|
package/lib/editor/server.js
CHANGED
|
@@ -4,7 +4,7 @@ const path = require('path');
|
|
|
4
4
|
const http = require('http');
|
|
5
5
|
const crypto = require('crypto');
|
|
6
6
|
const drawioLib = require('../drawio.js');
|
|
7
|
-
const { renderMarkdown, buildKatexStyleTag, diagramEnginePaths } = require('../md2doc.js');
|
|
7
|
+
const { renderMarkdown, buildKatexStyleTag, diagramEnginePaths, renderDotSvg } = require('../md2doc.js');
|
|
8
8
|
|
|
9
9
|
const LINEOPS_SRC = fs.readFileSync(path.join(__dirname, 'lineops.js'), 'utf8');
|
|
10
10
|
const INLINE_MD_SRC = fs.readFileSync(path.join(__dirname, 'inline-md.js'), 'utf8');
|
|
@@ -29,6 +29,10 @@ const CONVERT_MD_SRC = fs.readFileSync(path.join(__dirname, 'convert-md.js'), 'u
|
|
|
29
29
|
// skeletons). Pure, no dependency on any other editor module; it only has to
|
|
30
30
|
// land before client.js reads window.md2docBlockCatalog.
|
|
31
31
|
const BLOCK_CATALOG_SRC = fs.readFileSync(path.join(__dirname, 'block-catalog.js'), 'utf8');
|
|
32
|
+
// v3.17.0 T1: the pure core of the in-place code editor (fence parse/build,
|
|
33
|
+
// language list, Enter/Tab/Shift+Tab text edits, the edit's own undo history).
|
|
34
|
+
// No dependency on any other editor module; lands right after block-catalog.
|
|
35
|
+
const CODE_EDIT_SRC = fs.readFileSync(path.join(__dirname, 'code-edit.js'), 'utf8');
|
|
32
36
|
// v3.2.0: the pure string-diff module behind incremental DOM patching —
|
|
33
37
|
// stripBlockId()/patchmap() compare oldParts[i] against newParts[j] with only
|
|
34
38
|
// data-block-id normalized (see the global constraint on that). Same "no
|
|
@@ -218,6 +222,9 @@ function diagramLibBody(name) {
|
|
|
218
222
|
const close = tag.lastIndexOf('</style>');
|
|
219
223
|
return { type: 'text/css; charset=utf-8', body: tag.slice(open, close) };
|
|
220
224
|
}
|
|
225
|
+
if (name === 'highlight.js') {
|
|
226
|
+
return { type: 'application/javascript; charset=utf-8', body: require('../highlight.js').browserBundle() };
|
|
227
|
+
}
|
|
221
228
|
return null;
|
|
222
229
|
}
|
|
223
230
|
|
|
@@ -699,7 +706,7 @@ async function createEditorServer({
|
|
|
699
706
|
// all: any page the user happens to be browsing while `md2doc --edit`
|
|
700
707
|
// is running could fire a cross-site "simple" POST at every candidate
|
|
701
708
|
// localhost port and write to disk.
|
|
702
|
-
const STATE_CHANGING_POST_PATHS = new Set(['/api/render', '/api/save', '/api/ping', '/api/asset']);
|
|
709
|
+
const STATE_CHANGING_POST_PATHS = new Set(['/api/render', '/api/save', '/api/ping', '/api/asset', '/api/preview']);
|
|
703
710
|
if (req.method === 'POST' && STATE_CHANGING_POST_PATHS.has(url.pathname)) {
|
|
704
711
|
const contentType = String(req.headers['content-type'] || '');
|
|
705
712
|
if (!/^application\/json\b/i.test(contentType)) {
|
|
@@ -768,6 +775,7 @@ async function createEditorServer({
|
|
|
768
775
|
`<script>${INDENT_CLAMP_SRC}</script>\n` +
|
|
769
776
|
`<script>${CONVERT_MD_SRC}</script>\n` +
|
|
770
777
|
`<script>${BLOCK_CATALOG_SRC}</script>\n` +
|
|
778
|
+
`<script>${CODE_EDIT_SRC}</script>\n` +
|
|
771
779
|
`<script>${PATCHMAP_SRC}</script>\n` +
|
|
772
780
|
`<script>${SELECTION_SRC}</script>\n` +
|
|
773
781
|
`<script>${CELL_RANGE_SRC}</script>\n` +
|
|
@@ -826,6 +834,26 @@ async function createEditorServer({
|
|
|
826
834
|
return send(res, 200, { parts, blocks });
|
|
827
835
|
}
|
|
828
836
|
|
|
837
|
+
// v3.17.0 (D11, R31): the stateless Graphviz preview for the diagram
|
|
838
|
+
// panel. Deliberately NOT /api/render: that route re-reads the document
|
|
839
|
+
// and records its draw.io references for the staleness heartbeat; a
|
|
840
|
+
// preview of an unsaved dot source must touch neither.
|
|
841
|
+
if (req.method === 'POST' && url.pathname === '/api/preview') {
|
|
842
|
+
const body = await readJson(req, 1024 * 1024);
|
|
843
|
+
const fileId = body ? body.fileId : undefined;
|
|
844
|
+
if (!Number.isInteger(fileId) || fileId < 0 || fileId >= absFiles.length) {
|
|
845
|
+
return send(res, 400, { error: 'fileId must be an index into the open file list' });
|
|
846
|
+
}
|
|
847
|
+
if (body.lang !== 'dot') return send(res, 400, { error: 'lang must be dot' });
|
|
848
|
+
if (typeof body.source !== 'string') return send(res, 400, { error: 'source must be a string' });
|
|
849
|
+
try {
|
|
850
|
+
return send(res, 200, { svg: await renderDotSvg(body.source) });
|
|
851
|
+
} catch (e) {
|
|
852
|
+
const first = String((e && e.message) || e).split('\n')[0];
|
|
853
|
+
return send(res, 422, { error: first });
|
|
854
|
+
}
|
|
855
|
+
}
|
|
856
|
+
|
|
829
857
|
if (req.method === 'POST' && url.pathname === '/api/save') {
|
|
830
858
|
const { fileId, content, baseMtimeMs } = await readJson(req);
|
|
831
859
|
const file = absFiles[fileId];
|
package/lib/highlight.js
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
// text rather than guessing.
|
|
6
6
|
const hljs = require('highlight.js/lib/core');
|
|
7
7
|
|
|
8
|
-
const
|
|
8
|
+
const LANG_DEFS = {
|
|
9
9
|
verilog: require('highlight.js/lib/languages/verilog'),
|
|
10
10
|
python: require('highlight.js/lib/languages/python'),
|
|
11
11
|
bash: require('highlight.js/lib/languages/bash'),
|
|
@@ -19,9 +19,16 @@ const LANGS = {
|
|
|
19
19
|
diff: require('highlight.js/lib/languages/diff'),
|
|
20
20
|
plaintext: require('highlight.js/lib/languages/plaintext'),
|
|
21
21
|
};
|
|
22
|
-
for (const [name, def] of Object.entries(
|
|
23
|
-
|
|
24
|
-
|
|
22
|
+
for (const [name, def] of Object.entries(LANG_DEFS)) hljs.registerLanguage(name, def);
|
|
23
|
+
// The two custom alias lists, flat: alias -> language. Registered below and
|
|
24
|
+
// replayed verbatim by browserBundle().
|
|
25
|
+
const ALIASES = {};
|
|
26
|
+
for (const [target, list] of [['verilog', ['systemverilog', 'sv', 'v']], ['bash', ['sh', 'shell', 'zsh']]]) {
|
|
27
|
+
hljs.registerAliases(list, { languageName: target });
|
|
28
|
+
for (const a of list) ALIASES[a] = target;
|
|
29
|
+
}
|
|
30
|
+
// The registered ids, in registration order (the editor's language menu).
|
|
31
|
+
const LANGS = Object.keys(LANG_DEFS);
|
|
25
32
|
|
|
26
33
|
// `lang` is the fence's info string; only its first word names the language,
|
|
27
34
|
// case-insensitively. Returns escaped, coloured HTML, or null when the
|
|
@@ -32,4 +39,38 @@ function highlightCode(code, lang) {
|
|
|
32
39
|
return hljs.highlight(code, { language: name, ignoreIllegals: true }).value;
|
|
33
40
|
}
|
|
34
41
|
|
|
35
|
-
|
|
42
|
+
// v3.17.0 D6: the same highlighter as one script for the edit page, served on
|
|
43
|
+
// demand at /api/lib/highlight.js. highlight.js core and each registered
|
|
44
|
+
// language wrapped in a CommonJS shim, then the same registration and the same
|
|
45
|
+
// highlightCode semantics. No closing script tag may appear in it (the page
|
|
46
|
+
// loads it as a script; a test guards that). Built once.
|
|
47
|
+
let bundle = null;
|
|
48
|
+
function browserBundle() {
|
|
49
|
+
if (bundle !== null) return bundle;
|
|
50
|
+
const fs = require('fs');
|
|
51
|
+
const wrap = (file) => '(function () { var module = { exports: {} }; var exports = module.exports;\n' +
|
|
52
|
+
fs.readFileSync(require.resolve(file), 'utf8') + '\nreturn module.exports; })()';
|
|
53
|
+
const parts = [
|
|
54
|
+
'(function () {',
|
|
55
|
+
'var hljs = ' + wrap('highlight.js/lib/core') + ';',
|
|
56
|
+
'var defs = {',
|
|
57
|
+
LANGS.map((n) => JSON.stringify(n) + ': ' + wrap('highlight.js/lib/languages/' + n)).join(',\n'),
|
|
58
|
+
'};',
|
|
59
|
+
'var LANGS = ' + JSON.stringify(LANGS) + ';',
|
|
60
|
+
'var ALIASES = ' + JSON.stringify(ALIASES) + ';',
|
|
61
|
+
'LANGS.forEach(function (n) { hljs.registerLanguage(n, defs[n]); });',
|
|
62
|
+
'Object.keys(ALIASES).forEach(function (a) { hljs.registerAliases([a], { languageName: ALIASES[a] }); });',
|
|
63
|
+
'function highlightCode(code, lang) {',
|
|
64
|
+
" var name = String(lang || '').trim().split(/\\s+/)[0].toLowerCase();",
|
|
65
|
+
' if (!name || !hljs.getLanguage(name)) return null;',
|
|
66
|
+
" return hljs.highlight(code, { language: name, ignoreIllegals: true }).value;",
|
|
67
|
+
'}',
|
|
68
|
+
"var root = typeof window !== 'undefined' ? window : self;",
|
|
69
|
+
'root.md2docHljs = { highlightCode: highlightCode, LANGS: LANGS };',
|
|
70
|
+
'})();',
|
|
71
|
+
];
|
|
72
|
+
bundle = parts.join('\n');
|
|
73
|
+
return bundle;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
module.exports = { highlightCode, LANGS, ALIASES, browserBundle };
|