@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.
@@ -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
- if (bar && phone.matches) { if (btn.parentNode !== bar) bar.appendChild(btn); }
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
@@ -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 editor's own UI (edit mode is light
52
- // only) and the lightbox, which is a dark overlay already.
53
- const SKIP_SELECTOR = /(^|[\s,>+~(])(html\.ed-|\.ed-|\.lightbox)/;
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.9.0",
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/docsource.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",
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",
@@ -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
- });