@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.
- package/lib/editor/client.js +260 -262
- package/lib/editor/icons.js +48 -0
- 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 +267 -154
- 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/theme/runtime.js
CHANGED
|
@@ -40,7 +40,10 @@
|
|
|
40
40
|
var bar = document.getElementById('mobile-bar');
|
|
41
41
|
var phone = window.matchMedia('(max-width: 1080px)');
|
|
42
42
|
function place() {
|
|
43
|
-
|
|
43
|
+
// Edit mode hides #mobile-bar; a button moved into it would vanish, so the
|
|
44
|
+
// bar has to be on screen, not merely in the DOM.
|
|
45
|
+
var barShown = bar && getComputedStyle(bar).display !== 'none';
|
|
46
|
+
if (barShown && phone.matches) { if (btn.parentNode !== bar) bar.appendChild(btn); }
|
|
44
47
|
else if (btn.parentNode !== document.body) document.body.appendChild(btn);
|
|
45
48
|
}
|
|
46
49
|
if (phone.addEventListener) phone.addEventListener('change', place); else if (phone.addListener) phone.addListener(place);
|
|
@@ -135,8 +138,83 @@
|
|
|
135
138
|
showMermaid(false);
|
|
136
139
|
if (isDark()) showMermaid(true);
|
|
137
140
|
});
|
|
138
|
-
onTheme(showMermaid);
|
|
139
141
|
}
|
|
142
|
+
// Registered even with no mermaid at load: an edit can add the first one
|
|
143
|
+
// (showMermaid() itself returns early while there is none).
|
|
144
|
+
onTheme(showMermaid);
|
|
145
|
+
|
|
146
|
+
// v3.10.0: the editor rebuilds the blocks an edit touched and hands the new
|
|
147
|
+
// .mermaid hosts to __md2docInitDiagrams (lib/md2doc.js). Those hosts are not
|
|
148
|
+
// in `mermaids` (captured at load), so they used to be drawn by mermaid's own
|
|
149
|
+
// init with whatever config was set last — light — and then picked up the
|
|
150
|
+
// generic recolour of a light-drawn svg: transparent node fills in dark.
|
|
151
|
+
// __md2docInitDiagrams now calls this instead. It re-reads the hosts in the
|
|
152
|
+
// document, carries every unchanged host's cached light/dark markup over,
|
|
153
|
+
// and draws only the new ones, in the theme on screen, on the same chain a
|
|
154
|
+
// toggle uses. The other theme is drawn for them the next time it is asked
|
|
155
|
+
// for (cached() is false while any entry is missing).
|
|
156
|
+
var freshSeq = 0;
|
|
157
|
+
function adoptMermaids() {
|
|
158
|
+
var now = [].slice.call(document.querySelectorAll('.content .mermaid'));
|
|
159
|
+
var at = new Map();
|
|
160
|
+
mermaids.forEach(function (el, i) { at.set(el, i); });
|
|
161
|
+
function carry(r) { return r ? now.map(function (el) { return at.has(el) ? r[at.get(el)] : undefined; }) : null; }
|
|
162
|
+
rendered = { light: carry(rendered.light), dark: carry(rendered.dark) };
|
|
163
|
+
var fresh = now.filter(function (el) { return !at.has(el); });
|
|
164
|
+
fresh.forEach(function (el) { if (!el.hasAttribute('data-md2doc-src')) el.setAttribute('data-md2doc-src', el.textContent); });
|
|
165
|
+
mermaids = now;
|
|
166
|
+
hasMermaid = now.length > 0;
|
|
167
|
+
return fresh;
|
|
168
|
+
}
|
|
169
|
+
function drawFresh(els, theme) {
|
|
170
|
+
window.mermaid.initialize(mermaidConfig(theme));
|
|
171
|
+
var p = Promise.resolve();
|
|
172
|
+
els.forEach(function (el) {
|
|
173
|
+
p = p.then(function () {
|
|
174
|
+
var i = mermaids.indexOf(el);
|
|
175
|
+
if (i < 0) return;
|
|
176
|
+
var id = 'md2doc-mermaid-' + theme + '-new-' + (++freshSeq);
|
|
177
|
+
var src = el.getAttribute('data-md2doc-src');
|
|
178
|
+
return window.mermaid.render(id, src).then(function (res) {
|
|
179
|
+
el.innerHTML = res.svg;
|
|
180
|
+
if (res.bindFunctions) res.bindFunctions(el);
|
|
181
|
+
el.setAttribute('data-processed', 'true');
|
|
182
|
+
var svg = el.querySelector('svg');
|
|
183
|
+
if (svg && theme === 'dark') { svg.setAttribute('data-md2doc-recoloured', ''); svg.setAttribute('data-md2doc-live', ''); }
|
|
184
|
+
if (rendered[theme]) rendered[theme][i] = el.innerHTML;
|
|
185
|
+
}, function () {
|
|
186
|
+
var stray = document.getElementById('d' + id);
|
|
187
|
+
if (stray && stray.parentNode) stray.parentNode.removeChild(stray);
|
|
188
|
+
// A broken diagram shows mermaid's own error box, drawn light (the
|
|
189
|
+
// observer gives it the full recolour in dark), like drawLight().
|
|
190
|
+
window.mermaid.initialize(mermaidConfig('light'));
|
|
191
|
+
el.removeAttribute('data-processed');
|
|
192
|
+
el.textContent = src;
|
|
193
|
+
return window.mermaid.run({ nodes: [el] }).catch(function () {});
|
|
194
|
+
});
|
|
195
|
+
});
|
|
196
|
+
});
|
|
197
|
+
return p.then(function () { window.mermaid.initialize(mermaidConfig('light')); });
|
|
198
|
+
}
|
|
199
|
+
// The re-read itself waits its turn on the chain: a full drawLight() /
|
|
200
|
+
// drawDark() already running indexes `mermaids` and `rendered` by the
|
|
201
|
+
// order it started with, so swapping either under it would file one host's
|
|
202
|
+
// markup under another (and store a new host's source text as its light
|
|
203
|
+
// render). Until its turn, a new host simply shows its source text.
|
|
204
|
+
window.__md2docThemeMermaid = function () {
|
|
205
|
+
if (typeof window.mermaid === 'undefined') return false;
|
|
206
|
+
inFlight++;
|
|
207
|
+
chain = chain.then(function () {
|
|
208
|
+
var fresh = adoptMermaids();
|
|
209
|
+
if (!fresh.length) return;
|
|
210
|
+
var theme = isDark() ? 'dark' : 'light';
|
|
211
|
+
mermaidShown = theme;
|
|
212
|
+
return drawFresh(fresh, theme);
|
|
213
|
+
})
|
|
214
|
+
.catch(function () { /* each host already fell back to mermaid's own error box */ })
|
|
215
|
+
.then(function () { inFlight--; });
|
|
216
|
+
return true;
|
|
217
|
+
};
|
|
140
218
|
|
|
141
219
|
// ── graphviz / WaveDrom / baked SVG: recolour by computed colour ──
|
|
142
220
|
// Rules: spec "Diagram recolour rules (D10)". Reading COMPUTED fill/stroke
|
package/lib/theme/tokens.js
CHANGED
|
@@ -37,6 +37,69 @@ const THEME_TOKENS = [
|
|
|
37
37
|
{ name: 'code', light: ['#b93a0c'], dark: '#ff9f70' },
|
|
38
38
|
{ name: 'accent-soft', light: ['#93c5fd'], dark: '#6ea8f5' },
|
|
39
39
|
{ name: 'error', light: ['#b91c1c'], dark: '#ff8a8a' },
|
|
40
|
+
// v3.10.0 editor chrome. `direct`: referenced only as var(--md-<name>) in
|
|
41
|
+
// the editor CSS, never by literal, so a light value that happens to equal a
|
|
42
|
+
// reader literal ('#ffffff') cannot steal it — see LITERAL_TO_TOKEN below.
|
|
43
|
+
{ name: 'ed-chrome', light: ['#ffffff'], dark: '#1b1b1d', direct: true },
|
|
44
|
+
{ name: 'ed-surface', light: ['#ffffff'], dark: '#2a2a2d', direct: true },
|
|
45
|
+
{ name: 'ed-ring', light: ['rgba(31, 35, 40, 0.12)'], dark: 'rgba(255, 255, 255, 0.10)', direct: true },
|
|
46
|
+
{ name: 'ed-shadow', light: ['rgba(31, 35, 40, 0.10)'], dark: 'rgba(0, 0, 0, 0.55)', direct: true },
|
|
47
|
+
{ name: 'ed-hover', light: ['#f3f5f7'], dark: '#303134', direct: true },
|
|
48
|
+
{ name: 'ed-glyph', light: ['#8c959f'], dark: '#7d8086', direct: true },
|
|
49
|
+
{ name: 'ed-field', light: ['#f6f8fa'], dark: '#232325', direct: true },
|
|
50
|
+
{ name: 'ed-sel', light: ['rgba(9, 105, 218, 0.10)'], dark: 'rgba(110, 168, 245, 0.16)', direct: true },
|
|
51
|
+
{ name: 'ed-range', light: ['rgba(9, 105, 218, 0.14)'], dark: 'rgba(110, 168, 245, 0.20)', direct: true },
|
|
52
|
+
{ name: 'ed-err', light: ['#cf222e'], dark: '#ff8a8a', direct: true },
|
|
53
|
+
{ name: 'ed-err-bg', light: ['#ffebe9'], dark: '#3a2224', direct: true },
|
|
54
|
+
{ name: 'ed-err-ink', light: ['#82071e'], dark: '#ffc9c9', direct: true },
|
|
55
|
+
{ name: 'ed-ok', light: ['#1a7f37'], dark: '#6fdd8b', direct: true },
|
|
56
|
+
{ name: 'ed-dirty', light: ['#9a6700'], dark: '#e3b341', direct: true },
|
|
57
|
+
// v3.10.0 fenced-code syntax colour (lib/highlight.js; .hljs-* rules), the
|
|
58
|
+
// GitHub palette. Direct: written as var(--md-syn-*) in the CSS.
|
|
59
|
+
{ name: 'syn-keyword', light: ['#cf222e'], dark: '#ff7b72', direct: true },
|
|
60
|
+
{ name: 'syn-number', light: ['#0550ae'], dark: '#79c0ff', direct: true },
|
|
61
|
+
{ name: 'syn-comment', light: ['#656d76'], dark: '#8b949e', direct: true },
|
|
62
|
+
{ name: 'syn-string', light: ['#0a3069'], dark: '#a5d6ff', direct: true },
|
|
63
|
+
{ name: 'syn-type', light: ['#953800'], dark: '#ffa657', direct: true },
|
|
64
|
+
{ name: 'syn-title', light: ['#8250df'], dark: '#d2a8ff', direct: true },
|
|
65
|
+
// v3.10.0 waveform editor (editModeLayoutCss .ed-wave-*). Literal roles: the
|
|
66
|
+
// wave CSS keeps its literals and applyThemeTokens rewrites them. Every one
|
|
67
|
+
// of these literals is used by no reader rule (measured 2026-10-04).
|
|
68
|
+
{ name: 'wave-accent', light: ['#1a73e8'], dark: '#6ea8f5' },
|
|
69
|
+
{ name: 'wave-rule', light: ['#e2e2e2'], dark: '#38393c' },
|
|
70
|
+
{ name: 'wave-ink', light: ['#111'], dark: '#e3e3e3' },
|
|
71
|
+
{ name: 'wave-mut', light: ['#555'], dark: '#a3a6ab' },
|
|
72
|
+
{ name: 'wave-edge', light: ['#0041c4'], dark: '#79b0f6' },
|
|
73
|
+
{ name: 'wave-strong', light: ['#222'], dark: '#f5f6f7' },
|
|
74
|
+
{ name: 'wave-grid', light: ['#ececec'], dark: '#303134' },
|
|
75
|
+
{ name: 'wave-faint', light: ['#aaa'], dark: '#7d8086' },
|
|
76
|
+
{ name: 'wave-text', light: ['#333'], dark: '#e3e3e3' },
|
|
77
|
+
{ name: 'wave-border', light: ['#bbb'], dark: '#505257' },
|
|
78
|
+
{ name: 'wave-border-2', light: ['#888'], dark: '#7d8086' },
|
|
79
|
+
{ name: 'wave-field', light: ['#f6f6f6'], dark: '#232325' },
|
|
80
|
+
{ name: 'wave-warn-ink', light: ['#7a4b00'], dark: '#e3b341' },
|
|
81
|
+
{ name: 'wave-panel', light: ['#fafafa'], dark: '#232325' },
|
|
82
|
+
{ name: 'wave-control', light: ['#c4c4c4'], dark: '#46484c' },
|
|
83
|
+
{ name: 'wave-mut-2', light: ['#666'], dark: '#a3a6ab' },
|
|
84
|
+
{ name: 'wave-warn-bg', light: ['#fff6e0'], dark: '#3a3020' },
|
|
85
|
+
{ name: 'wave-warn-line', light: ['#e8c987'], dark: '#6e5e14' },
|
|
86
|
+
{ name: 'wave-err-ink', light: ['#7a1f00'], dark: '#ffb4a0' },
|
|
87
|
+
{ name: 'wave-err-bg', light: ['#ffe9e0'], dark: '#3a2420' },
|
|
88
|
+
{ name: 'wave-err-line', light: ['#e8a987'], dark: '#7a3b2a' },
|
|
89
|
+
{ name: 'wave-sel-bg', light: ['#eaf2fe'], dark: '#24364f' },
|
|
90
|
+
{ name: 'wave-group-line', light: ['#d8d8d8'], dark: '#46484c' },
|
|
91
|
+
{ name: 'wave-group-bg', light: ['#f2f2f2'], dark: '#2a2a2d' },
|
|
92
|
+
{ name: 'wave-x', light: ['#d9d9d9'], dark: '#505257' },
|
|
93
|
+
{ name: 'wave-bus', light: ['#f5e9c8'], dark: '#4a4128' },
|
|
94
|
+
{ name: 'wave-bus-3', light: ['#d8ead3'], dark: '#2f4a2b' },
|
|
95
|
+
{ name: 'wave-bus-4', light: ['#d6e4f7'], dark: '#26384f' },
|
|
96
|
+
{ name: 'wave-bus-5', light: ['#f7d6e4'], dark: '#4a2c3a' },
|
|
97
|
+
{ name: 'wave-cursor', light: ['#d93025'], dark: '#ff8a8a' },
|
|
98
|
+
{ name: 'wave-status', light: ['#444'], dark: '#a3a6ab' },
|
|
99
|
+
{ name: 'wave-caption', light: ['#8a8a8a'], dark: '#a3a6ab' },
|
|
100
|
+
// .ed-wave-parse-message's red (the .ed-conflict banner used it too until
|
|
101
|
+
// v3.10.0 moved the banner onto the direct ed-err-* roles).
|
|
102
|
+
{ name: 'wave-err', light: ['#b00020'], dark: '#ff8a8a' },
|
|
40
103
|
];
|
|
41
104
|
|
|
42
105
|
// Literals in reader rules that deliberately stay literal. Measured on real
|
|
@@ -48,9 +111,15 @@ const KEEP_LITERALS = ['#000', '#dbeafe'];
|
|
|
48
111
|
// Content links: today they use the browser default, which stays in light.
|
|
49
112
|
const LINK_DARK = '#79b0f6';
|
|
50
113
|
|
|
51
|
-
// Rules whose literals stay literal: the
|
|
52
|
-
//
|
|
53
|
-
const SKIP_SELECTOR = /(^|[\s,>+~(])
|
|
114
|
+
// Rules whose literals stay literal: the lightbox, which is a dark overlay
|
|
115
|
+
// already. v3.10.0: the editor's own UI is themed (it used to be skipped).
|
|
116
|
+
const SKIP_SELECTOR = /(^|[\s,>+~(])\.lightbox/;
|
|
117
|
+
|
|
118
|
+
// rgba() literals the editor CSS keeps on purpose (normalised, no spaces).
|
|
119
|
+
// Scrims and shadows are translucent black and work on both grounds; the
|
|
120
|
+
// wave selection is a translucent blue. Anything else in an .ed- rule must be
|
|
121
|
+
// a var() role — test/reader-design.test.js enforces it.
|
|
122
|
+
const KEEP_RGBA = ['rgba(0,0,0,.35)', 'rgba(0,0,0,.45)', 'rgba(26,115,232,.18)'];
|
|
54
123
|
|
|
55
124
|
const MERMAID_LIGHT_VARS = {
|
|
56
125
|
fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Arial, "Microsoft JhengHei", sans-serif',
|
|
@@ -63,7 +132,7 @@ const MERMAID_DARK_VARS = Object.assign({}, MERMAID_LIGHT_VARS, {
|
|
|
63
132
|
});
|
|
64
133
|
|
|
65
134
|
const LITERAL_TO_TOKEN = new Map();
|
|
66
|
-
for (const t of THEME_TOKENS) for (const l of t.light) LITERAL_TO_TOKEN.set(l, t.name);
|
|
135
|
+
for (const t of THEME_TOKENS) if (!t.direct) for (const l of t.light) LITERAL_TO_TOKEN.set(l, t.name);
|
|
67
136
|
|
|
68
137
|
// Line-based on purpose: the reader CSS is one declaration per line or one
|
|
69
138
|
// rule per line, and a line-based pass leaves comment prose (which names
|
|
@@ -135,6 +204,6 @@ function applyReaderTheme(html) {
|
|
|
135
204
|
}
|
|
136
205
|
|
|
137
206
|
module.exports = {
|
|
138
|
-
THEME_TOKENS, KEEP_LITERALS, LINK_DARK, MERMAID_LIGHT_VARS, MERMAID_DARK_VARS,
|
|
207
|
+
THEME_TOKENS, KEEP_LITERALS, KEEP_RGBA, LINK_DARK, MERMAID_LIGHT_VARS, MERMAID_DARK_VARS,
|
|
139
208
|
applyThemeTokens, themeCss, applyReaderTheme,
|
|
140
209
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@helping-ai-workflow/md2doc",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.10.0",
|
|
4
4
|
"description": "Markdown → HTML / PDF renderer with WaveDrom, Mermaid, Graphviz, and draw.io support",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"markdown",
|
|
@@ -35,6 +35,7 @@
|
|
|
35
35
|
},
|
|
36
36
|
"dependencies": {
|
|
37
37
|
"@hpcc-js/wasm-graphviz": "1.22.0",
|
|
38
|
+
"highlight.js": "11.11.1",
|
|
38
39
|
"katex": "^0.16.47",
|
|
39
40
|
"marked": "^14.1.0",
|
|
40
41
|
"marked-katex-extension": "^5.1.10",
|
|
@@ -45,8 +46,8 @@
|
|
|
45
46
|
},
|
|
46
47
|
"scripts": {
|
|
47
48
|
"preinstall": "node scripts/preinstall.js",
|
|
48
|
-
"test": "node test/md2doc.test.js && node test/blockmap.test.js && node test/drawio.test.js && node test/images.test.js && node test/scroll-anchor.test.js && node test/lightbox.test.js && node test/lightbox-anno.test.js && node test/lightbox-anno-style.test.js && node test/reader-panels.test.js && node test/cli.test.js && node test/code-operator.test.js && node test/convert-md.test.js && node test/render-api.test.js && node test/lineops.test.js && node test/indent-clamp.test.js && node test/selection.test.js && node test/cell-range.test.js && node test/editmode-render.test.js && node test/editmode-wavedrom-reinit.test.js && node test/editor-server.test.js && node test/cli-edit.test.js && node test/open-viewer.test.js && node test/editor-client.test.js && node test/editor-client-runtime.test.js && node test/roundtrip.test.js && node test/byte-stability.test.js && node test/editor-server-throw.test.js && node test/editor-reader-rebind.test.js && node test/inline-md.test.js && node test/table-md.test.js && node test/table-anchor-recovery.test.js && node test/gate-compat.test.js && node test/history.test.js && node test/list-md.test.js && node test/table-width.test.js && node test/toolbar-model.test.js && node test/
|
|
49
|
-
"test:browser": "node test/reader-click.test.js"
|
|
49
|
+
"test": "node test/md2doc.test.js && node test/highlight.test.js && node test/blockmap.test.js && node test/drawio.test.js && node test/images.test.js && node test/scroll-anchor.test.js && node test/lightbox.test.js && node test/lightbox-anno.test.js && node test/lightbox-anno-style.test.js && node test/reader-panels.test.js && node test/cli.test.js && node test/code-operator.test.js && node test/convert-md.test.js && node test/render-api.test.js && node test/lineops.test.js && node test/indent-clamp.test.js && node test/selection.test.js && node test/cell-range.test.js && node test/editmode-render.test.js && node test/editmode-wavedrom-reinit.test.js && node test/editor-server.test.js && node test/cli-edit.test.js && node test/open-viewer.test.js && node test/editor-client.test.js && node test/editor-client-runtime.test.js && node test/roundtrip.test.js && node test/byte-stability.test.js && node test/editor-server-throw.test.js && node test/editor-reader-rebind.test.js && node test/inline-md.test.js && node test/table-md.test.js && node test/table-anchor-recovery.test.js && node test/gate-compat.test.js && node test/history.test.js && node test/list-md.test.js && node test/table-width.test.js && node test/toolbar-model.test.js && node test/asset.test.js && node test/paste-md.test.js && node test/patchmap.test.js && node test/wave-codec.test.js && node test/wave-draw.test.js && node test/wave-geometry.test.js && node test/wave-store.test.js && node test/wave-rail-clearance.test.js && node test/editor-journey.test.js && node test/detach-census.test.js && node test/editor-arrow-nav.test.js && node test/editor-cell-range.test.js && node test/reader-design.test.js && node test/theme.test.js",
|
|
50
|
+
"test:browser": "node test/reader-click.test.js && node test/editor-click.test.js"
|
|
50
51
|
},
|
|
51
52
|
"repository": {
|
|
52
53
|
"type": "git",
|
package/lib/editor/docsource.js
DELETED
|
@@ -1,115 +0,0 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
/* UMD, same shape as convert-md.js / lineops.js: `require`-able in node for
|
|
3
|
-
the unit tests, and injected into the editor page as
|
|
4
|
-
`window.md2docDocSource` (lib/editor/server.js). client.js is inlined
|
|
5
|
-
into the page as a plain <script>, not bundled, so a bare top-level
|
|
6
|
-
require('./docsource.js') there would be an undefined identifier in the
|
|
7
|
-
browser and would kill the whole injected script. */
|
|
8
|
-
(function (root, factory) {
|
|
9
|
-
if (typeof module === 'object' && module.exports) module.exports = factory();
|
|
10
|
-
else root.md2docDocSource = factory();
|
|
11
|
-
})(typeof self !== 'undefined' ? self : this, function () {
|
|
12
|
-
|
|
13
|
-
// Pure state machine + lines[]<->string conversion for the "追加 4 全文原始碼
|
|
14
|
-
// 逃生口" (whole-document source escape hatch): 編輯 ⇄ 原始碼.
|
|
15
|
-
// v3.2.1: 預覽(preview)已移除,見下方 MODES 上的說明。
|
|
16
|
-
//
|
|
17
|
-
// EOL representation: `eol` is always a single literal separator string --
|
|
18
|
-
// '\n', '\r\n' or '\r' -- the same convention server.js already establishes
|
|
19
|
-
// at load time (majority-vote across the file, ties go to '\n'; see
|
|
20
|
-
// server.js's `eol` derivation next to its own `mdText.split(/\r\n|\r|\n/)`).
|
|
21
|
-
// This module does not re-derive eol and does not guess per-line; it is
|
|
22
|
-
// handed the file's one canonical eol and uses it for every join.
|
|
23
|
-
//
|
|
24
|
-
// Splitting, though, is always done with the UNIVERSAL three-way regex
|
|
25
|
-
// `/\r\n|\r|\n/`, never with a literal `eol`-only split. Two reasons:
|
|
26
|
-
// 1. Symmetry with how `lines[]` was produced in the first place
|
|
27
|
-
// (server.js:154) -- if load used the universal regex, from-source
|
|
28
|
-
// must use the same one, or a file whose canonical eol is '\r\n' but
|
|
29
|
-
// whose textarea content picked up a stray '\n' (browsers normalize
|
|
30
|
-
// textarea .value to '\n' on some paste paths) would silently lose a
|
|
31
|
-
// line instead of being read back correctly.
|
|
32
|
-
// 2. `\r\n` is listed before the two single-character alternatives, so a
|
|
33
|
-
// CRLF pair is always consumed as ONE separator, never split into a
|
|
34
|
-
// spurious blank line between a lone \r and a lone \n.
|
|
35
|
-
const MODES = ['edit', 'source'];
|
|
36
|
-
|
|
37
|
-
// v3.2.1: 兩態循環 edit <-> source。preview 移除 —— 實測它相對 edit 只做
|
|
38
|
-
// 兩件事(applyPreviewEditability(false) 與兩行隱藏 ⠿/+ 的 CSS),卻擋在
|
|
39
|
-
// source 回 edit 的路上,而且 22 顆工具列按鈕中 13 顆在它底下仍然寫得進磁碟。
|
|
40
|
-
//
|
|
41
|
-
// 為什麼是移除而不是加閘門:一個「列舉可及面」推導出來的唯讀閘門必然漏掉
|
|
42
|
-
// 一整族 —— 切換前就已經開著的 ⠿ 選單在切換後仍是 140x118、display:flex,
|
|
43
|
-
// 其 16 個項目每一個都寫得進磁碟。閘門要約 8 個守衛點跨 2 個檔案才擋得住,
|
|
44
|
-
// 而 preview 本身沒有任何 edit 給不了的能力。
|
|
45
|
-
//
|
|
46
|
-
// An unrecognized mode (defensive: a caller passing something stale) falls
|
|
47
|
-
// back to the first state rather than throwing, matching how the rest of
|
|
48
|
-
// this module prefers a safe default over a thrown exception for malformed
|
|
49
|
-
// input.
|
|
50
|
-
function next(mode) {
|
|
51
|
-
const idx = MODES.indexOf(mode);
|
|
52
|
-
return MODES[(idx + 1) % MODES.length];
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
// lines[] -> the whole document as one string, for seeding the source
|
|
56
|
-
// textarea. `lines` mirrors the app's canonical model (server.js's
|
|
57
|
-
// `mdText.split(/\r\n|\r|\n/)`): a trailing '' entry represents a final
|
|
58
|
-
// trailing newline in the source file, no trailing '' entry represents a
|
|
59
|
-
// file with no trailing newline. Plain join reproduces exactly that.
|
|
60
|
-
function toSource(lines, eol) {
|
|
61
|
-
const arr = Array.isArray(lines) ? lines : [];
|
|
62
|
-
const sep = typeof eol === 'string' && eol !== '' ? eol : '\n';
|
|
63
|
-
return arr.join(sep);
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
function linesEqual(a, b) {
|
|
67
|
-
if (a.length !== b.length) return false;
|
|
68
|
-
for (let i = 0; i < a.length; i += 1) {
|
|
69
|
-
if (a[i] !== b[i]) return false;
|
|
70
|
-
}
|
|
71
|
-
return true;
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
// string -> {lines, changed}, the inverse of toSource. `lines` is always a
|
|
75
|
-
// faithful parse of `text` (universal-regex split, see the module header),
|
|
76
|
-
// independent of `prevLines` -- what `prevLines` controls is only the
|
|
77
|
-
// `changed` flag's meaning, per the plan-owner ruling below.
|
|
78
|
-
//
|
|
79
|
-
// CONTRACT (plan-owner ruling, fix round 1 -- do not "optimize" this back):
|
|
80
|
-
// `prevLines` is the lines[] the source textarea was seeded from -- the
|
|
81
|
-
// pre-edit state a real caller (client.js) always has on hand when it
|
|
82
|
-
// calls this on the way OUT of source mode. `prevLines` is optional in
|
|
83
|
-
// ARITY ONLY, so the name `fromSource(text, eol)` and every two-arg call
|
|
84
|
-
// site keep parsing correctly; it is NOT optional in the sense of "safe to
|
|
85
|
-
// omit and get a sensible answer for `changed`":
|
|
86
|
-
//
|
|
87
|
-
// - `prevLines` GIVEN (array): `changed` is a real content diff against
|
|
88
|
-
// it (`!linesEqual(lines, prevLines)`). This is the only form that can
|
|
89
|
-
// tell "the user edited a line" apart from "untouched," and it is the
|
|
90
|
-
// form the integration task (client.js) must call.
|
|
91
|
-
// - `prevLines` OMITTED: `changed` is unconditionally `true`.
|
|
92
|
-
//
|
|
93
|
-
// Why `true` and not a "did this round-trip cleanly" fidelity check (the
|
|
94
|
-
// round 1 defect): a pure function of `(text, eol)` alone has nothing to
|
|
95
|
-
// diff an edit against, and a same-eol in-place character edit -- the
|
|
96
|
-
// overwhelming majority of real edits -- round-trips through split+join
|
|
97
|
-
// exactly as cleanly as untouched text does. A fidelity-based `changed`
|
|
98
|
-
// therefore reports `false` (unchanged) for most real edits too, and the
|
|
99
|
-
// caller's contract is "changed: false means do not write the file" --
|
|
100
|
-
// which silently discards the user's edit. That is a data-loss default.
|
|
101
|
-
// `changed: true` is the fail-safe direction: the unknown case reads as
|
|
102
|
-
// "assume it was edited," so the caller writes; the worst case is one
|
|
103
|
-
// needless write of byte-identical content, never a dropped edit.
|
|
104
|
-
function fromSource(text, eol, prevLines) {
|
|
105
|
-
// `eol` is accepted for API symmetry with `toSource(lines, eol)` and is
|
|
106
|
-
// NOT used here: parsing is always the universal three-way split (module
|
|
107
|
-
// header), never a literal-`eol` split, so there is nothing for it to do.
|
|
108
|
-
const s = typeof text === 'string' ? text : String(text == null ? '' : text);
|
|
109
|
-
const lines = s.split(/\r\n|\r|\n/);
|
|
110
|
-
const changed = Array.isArray(prevLines) ? !linesEqual(lines, prevLines) : true;
|
|
111
|
-
return { lines, changed };
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
return { MODES, next, toSource, fromSource };
|
|
115
|
-
});
|