@helping-ai-workflow/md2doc 3.3.0 → 3.4.2
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/README.md +33 -8
- package/THIRD-PARTY-NOTICES.md +44 -0
- package/lib/drawio.js +178 -0
- package/lib/editor/blockmap.js +79 -6
- package/lib/editor/client.js +3353 -157
- package/lib/editor/inline-md.js +122 -12
- package/lib/editor/lineops.js +190 -3
- package/lib/editor/paste-md.js +5 -1
- package/lib/editor/server.js +359 -5
- package/lib/editor/toolbar-model.js +51 -14
- package/lib/editor/wave-codec.js +2631 -0
- package/lib/editor/wave-geometry.js +320 -0
- package/lib/editor/wave-store.js +950 -0
- package/lib/editor/wave-ui.js +2124 -0
- package/lib/md2doc.js +677 -14
- package/package.json +14 -6
- package/vendor/drawio/LICENSE +201 -0
- package/vendor/drawio/NOTICE +109 -0
- package/vendor/drawio/README.md +25 -0
- package/vendor/drawio/viewer-static.min.js +8596 -0
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @helping-ai-workflow/md2doc
|
|
2
2
|
|
|
3
|
-
> Markdown → HTML / PDF renderer with WaveDrom, Mermaid, and
|
|
3
|
+
> Markdown → HTML / PDF renderer with WaveDrom, Mermaid, Graphviz, and draw.io support.
|
|
4
4
|
|
|
5
5
|
A single global CLI (`md2doc`) you can call from any directory.
|
|
6
6
|
|
|
@@ -8,7 +8,9 @@ A single global CLI (`md2doc`) you can call from any directory.
|
|
|
8
8
|
|
|
9
9
|
Requires Node.js 18 or higher. The first install pulls puppeteer (≈ 170 MB Chromium download); subsequent installs reuse it.
|
|
10
10
|
|
|
11
|
-
Chromium is
|
|
11
|
+
Chromium is needed for **PDF export**, for the optional `--bake-svg` flag, and for **`.drawio` diagrams**, which are always rendered to SVG at generation time. Everything else needs nothing extra — Graphviz runs in-process via WebAssembly, and Mermaid / WaveDrom are bundled and inlined.
|
|
12
|
+
|
|
13
|
+
Nothing is fetched at render time or at read time: every engine, including the vendored draw.io viewer, runs locally, so documents render and read **offline with no system Graphviz, no draw.io install and no CDN**.
|
|
12
14
|
|
|
13
15
|
### Recommended: install via nvm
|
|
14
16
|
|
|
@@ -100,8 +102,8 @@ in several places grows the HTML accordingly. PDF output is unaffected.
|
|
|
100
102
|
|
|
101
103
|
### Viewing diagrams
|
|
102
104
|
|
|
103
|
-
Click any image, Mermaid, Graphviz or WaveDrom graphic in the rendered HTML to
|
|
104
|
-
full-screen.
|
|
105
|
+
Click any image, Mermaid, Graphviz, draw.io or WaveDrom graphic in the rendered HTML to
|
|
106
|
+
open it full-screen.
|
|
105
107
|
|
|
106
108
|
| Input | Action |
|
|
107
109
|
|---|---|
|
|
@@ -280,7 +282,7 @@ version-control-friendly.
|
|
|
280
282
|
### Degraded blocks: code, diagrams, images, math
|
|
281
283
|
|
|
282
284
|
Blocks containing content WYSIWYG cannot represent (code fences, Mermaid/Graphviz/
|
|
283
|
-
WaveDrom diagrams, images, LaTeX math, or unstyled HTML) automatically degrade to
|
|
285
|
+
WaveDrom/draw.io diagrams, images, LaTeX math, or unstyled HTML) automatically degrade to
|
|
284
286
|
raw-edit mode: click the block to open the raw Markdown source in a textarea, make
|
|
285
287
|
your changes, then press `Ctrl+Enter` to commit or `Esc` to cancel.
|
|
286
288
|
|
|
@@ -372,9 +374,19 @@ digraph G { A -> B }
|
|
|
372
374
|
```
|
|
373
375
|
````
|
|
374
376
|
|
|
375
|
-
|
|
377
|
+
A fourth engine, **draw.io**, is referenced with image syntax rather than a code fence, because a `.drawio` file is an external artifact rather than inline source:
|
|
378
|
+
|
|
379
|
+
```markdown
|
|
380
|
+

|
|
381
|
+

|
|
382
|
+

|
|
383
|
+
```
|
|
384
|
+
|
|
385
|
+
A `.xml` file is treated as a diagram only when its root element really is `<mxfile>` or `<mxGraphModel>`; anything else falls through to the ordinary image path unchanged. `#Name` selects a sheet by name, `#N` by 1-based position (a name wins over a number). The alt text becomes the rendered box's accessible name. Multi-sheet files get a small sheet bar on hover — every sheet is baked into the output, so switching sheets involves no engine.
|
|
386
|
+
|
|
387
|
+
All four render directly in the output (HTML or PDF), **offline and with no system dependencies**: Graphviz `dot` runs in-process via WebAssembly (no system `dot` binary required), Mermaid / WaveDrom are bundled and inlined (no CDN), and `.drawio` files are rendered by a vendored draw.io viewer running in a build-time headless Chromium. Each engine's runtime is embedded only when the document actually uses that diagram type — and the draw.io viewer never is, at any size, because `.drawio` diagrams are fully pre-rendered (a multi-sheet document carries only a few lines of sheet-switching script, not an engine).
|
|
376
388
|
|
|
377
|
-
By default, Mermaid and WaveDrom render in the browser when the HTML is opened; pass `--bake-svg` to pre-render them to inert SVG at generation time instead (Graphviz
|
|
389
|
+
By default, Mermaid and WaveDrom render in the browser when the HTML is opened; pass `--bake-svg` to pre-render them to inert SVG at generation time instead (Graphviz and draw.io are always pre-rendered to SVG).
|
|
378
390
|
|
|
379
391
|
## Why a global CLI
|
|
380
392
|
|
|
@@ -382,4 +394,17 @@ Multiple repos used to ship copies of this script. They drifted. This package ce
|
|
|
382
394
|
|
|
383
395
|
## Licence
|
|
384
396
|
|
|
385
|
-
MIT.
|
|
397
|
+
md2doc's own source is **MIT** — see [`LICENSE`](LICENSE).
|
|
398
|
+
|
|
399
|
+
The published package is not MIT-only, because it ships one vendored
|
|
400
|
+
third-party file: `vendor/drawio/viewer-static.min.js`, the draw.io /
|
|
401
|
+
diagrams.net viewer (v31.3.2, ~4 MB), under the **Apache License 2.0**.
|
|
402
|
+
Its licence text and a per-component NOTICE travel with it, in
|
|
403
|
+
[`vendor/drawio/LICENSE`](vendor/drawio/LICENSE) and
|
|
404
|
+
[`vendor/drawio/NOTICE`](vendor/drawio/NOTICE).
|
|
405
|
+
|
|
406
|
+
**Running a licence scan? Start at
|
|
407
|
+
[`THIRD-PARTY-NOTICES.md`](THIRD-PARTY-NOTICES.md).** It lists everything
|
|
408
|
+
redistributed inside the tarball, including the components bundled inside
|
|
409
|
+
the draw.io viewer itself (DOMPurify, pako, spin.js, Rough.js), and says
|
|
410
|
+
how each finding was established.
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# Third-party notices
|
|
2
|
+
|
|
3
|
+
md2doc's own source code is licensed under the MIT License — see
|
|
4
|
+
[`LICENSE`](LICENSE). `"license": "MIT"` in `package.json` refers to that
|
|
5
|
+
code and only to that code.
|
|
6
|
+
|
|
7
|
+
**The published npm package also contains code that is not md2doc's and
|
|
8
|
+
is not MIT.** If you are running a licence scan, this is the file you
|
|
9
|
+
want; the per-component detail is one level down, next to the artifact it
|
|
10
|
+
describes.
|
|
11
|
+
|
|
12
|
+
## Vendored at rest (inside the published tarball)
|
|
13
|
+
|
|
14
|
+
| Path | What | Licence | Detail |
|
|
15
|
+
|---|---|---|---|
|
|
16
|
+
| `vendor/drawio/viewer-static.min.js` | draw.io / diagrams.net viewer, v31.3.2, 4,151,717 bytes | **Apache License 2.0** | [`vendor/drawio/LICENSE`](vendor/drawio/LICENSE), [`vendor/drawio/NOTICE`](vendor/drawio/NOTICE) |
|
|
17
|
+
|
|
18
|
+
That single 4 MB file is a verbatim copy of
|
|
19
|
+
`src/main/webapp/js/viewer-static.min.js` from
|
|
20
|
+
[jgraph/drawio](https://github.com/jgraph/drawio) at tag `v31.3.2`
|
|
21
|
+
(sha256 `2fabaaa3e28d5f80f943285a2ce19c22cf870857203255f1e0347ef93693a297`).
|
|
22
|
+
md2doc executes it only inside a build-time headless Chromium, to turn
|
|
23
|
+
`.drawio` references into inline SVG; it is never emitted into output
|
|
24
|
+
HTML and never fetched over the network.
|
|
25
|
+
|
|
26
|
+
The Apache-2.0 text ships **inside `vendor/drawio/`**, next to the file it
|
|
27
|
+
covers, rather than only at the repository root — so it survives being
|
|
28
|
+
copied, vendored or extracted along with the artifact.
|
|
29
|
+
|
|
30
|
+
That bundle is itself not licence-homogeneous. It contains DOMPurify
|
|
31
|
+
3.4.13 (Apache-2.0 OR MPL-2.0), pako 2.2.0 (MIT AND Zlib), spin.js 2.0.0
|
|
32
|
+
(MIT) and Rough.js 4.6.6 (MIT).
|
|
33
|
+
[`vendor/drawio/NOTICE`](vendor/drawio/NOTICE) records each one's version,
|
|
34
|
+
licence, where inside the bundle it sits, **how that was established**,
|
|
35
|
+
and which of those findings rest on provenance rather than on a
|
|
36
|
+
declaration in the artifact itself.
|
|
37
|
+
|
|
38
|
+
## Runtime dependencies (installed by npm, not vendored)
|
|
39
|
+
|
|
40
|
+
md2doc's `dependencies` (marked, mermaid, wavedrom, katex, puppeteer,
|
|
41
|
+
turndown, `@hpcc-js/wasm-graphviz`) are ordinary npm packages: they are
|
|
42
|
+
resolved into your `node_modules` by your own installer, carry their own
|
|
43
|
+
licence files there, and are not redistributed inside md2doc's tarball.
|
|
44
|
+
They are out of scope for this file.
|
package/lib/drawio.js
ADDED
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const fs = require('fs');
|
|
5
|
+
|
|
6
|
+
// drawio (.drawio / .xml) 來源的判別與頁面解析。
|
|
7
|
+
//
|
|
8
|
+
// 這一支刻意不碰 marked、不碰瀏覽器 —— 判別與頁名是純字串處理,可以用純函式
|
|
9
|
+
// 測;真的需要 headless Chromium 的只有 bakeDrawioSvg()(見同檔下方)。
|
|
10
|
+
//
|
|
11
|
+
// 為什麼不用 XML parser:md2doc 不引進新的 runtime 依賴,而這裡要回答的問題
|
|
12
|
+
// (根元素是什麼、有哪些 <diagram name>)用受控的 regex 就夠。輸入是本機檔案
|
|
13
|
+
// 而不是網路內容,且判別失敗的後果是「當成普通圖片、原樣留著」——不是安全邊界。
|
|
14
|
+
|
|
15
|
+
// 開頭的 XML 宣告、DOCTYPE、註解、空白都要跳過,才看得到真正的根元素。
|
|
16
|
+
const LEADING_NOISE = /^(?:\s|<\?[\s\S]*?\?>|<!--[\s\S]*?-->|<!DOCTYPE[^>]*>)*/;
|
|
17
|
+
|
|
18
|
+
// 根元素判別:兩處(isDrawioXml / pageNamesOf)共用同一顆函式,避免各自維護
|
|
19
|
+
// 一份「跳過前導雜訊」的 regex 而在 DOCTYPE / 自我封閉標籤上悄悄長歪。
|
|
20
|
+
//
|
|
21
|
+
// 標籤後面接的字元用 `[\s\/>]` 而不是 `[\s>]`:自我封閉的 `<mxfile/>` 或
|
|
22
|
+
// `<mxGraphModel/>`(沒有任何屬性、也沒有子節點)在真實檔案裡少見,但一旦
|
|
23
|
+
// 出現,`[\s>]` 會因為下一個字元是 `/` 而漏判成「不是 drawio」——用
|
|
24
|
+
// `foo(root, 'not a diagram')` 這種輸入實測會踩到。同時要擋掉
|
|
25
|
+
// `<mxfilex>` / `<mxGraphModelFoo>` 這種前綴相同、但其實是別的標籤的輸入,
|
|
26
|
+
// 所以字元類仍然是白名單而不是「以 mxfile 開頭就算」。
|
|
27
|
+
function rootKindOf(text) {
|
|
28
|
+
const rest = String(text || '').replace(LEADING_NOISE, '');
|
|
29
|
+
if (/^<mxfile[\s\/>]/.test(rest)) return 'mxfile';
|
|
30
|
+
if (/^<mxGraphModel[\s\/>]/.test(rest)) return 'mxGraphModel';
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function isDrawioXml(text) {
|
|
35
|
+
return rootKindOf(text) !== null;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function pageNamesOf(text) {
|
|
39
|
+
const s = String(text || '');
|
|
40
|
+
if (rootKindOf(s) !== 'mxfile') {
|
|
41
|
+
// <mxGraphModel> 直接當一頁,沒有名字。
|
|
42
|
+
return [''];
|
|
43
|
+
}
|
|
44
|
+
const names = [];
|
|
45
|
+
const re = /<diagram\b([^>]*)>/g;
|
|
46
|
+
let m;
|
|
47
|
+
while ((m = re.exec(s)) !== null) {
|
|
48
|
+
const nameAttr = /\bname\s*=\s*"([^"]*)"/.exec(m[1]);
|
|
49
|
+
names.push(nameAttr ? nameAttr[1] : '');
|
|
50
|
+
}
|
|
51
|
+
return names.length ? names : [''];
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function resolvePageIndex(names, fragment) {
|
|
55
|
+
const frag = String(fragment == null ? '' : fragment);
|
|
56
|
+
if (!frag) return 0;
|
|
57
|
+
const byName = names.indexOf(frag);
|
|
58
|
+
if (byName >= 0) return byName;
|
|
59
|
+
if (/^\d+$/.test(frag)) {
|
|
60
|
+
const n = Number(frag);
|
|
61
|
+
if (n >= 1 && n <= names.length) return n - 1;
|
|
62
|
+
}
|
|
63
|
+
return 0;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// v3.4.0 batch2 Task 6 fix round 1 (review F7) — the identity of a referenced
|
|
67
|
+
// `.drawio`/`.xml` file's bytes AS THIS PROCESS LAST SAW THEM.
|
|
68
|
+
//
|
|
69
|
+
// (mtimeMs, size) rather than mtimeMs alone: this project's own environment
|
|
70
|
+
// notes document a filesystem (WSL DrvFs over /mnt/c) that degrades and
|
|
71
|
+
// behaves oddly around timestamps, and coarse-granularity mtimes are also the
|
|
72
|
+
// norm on SMB shares — a rewrite landing inside the same tick as the recorded
|
|
73
|
+
// one compares equal and is never reported. Size costs nothing extra (the
|
|
74
|
+
// Stats object is already in hand) and closes that hole for every edit that
|
|
75
|
+
// changes the file's length.
|
|
76
|
+
//
|
|
77
|
+
// Returns null when the file cannot be stat'd at all. That deliberately
|
|
78
|
+
// collapses "deleted" and "unreadable" (EACCES, an editor's atomic-replace
|
|
79
|
+
// window) into one value: for staleness they are the same answer — "the bytes
|
|
80
|
+
// you baked are no longer the bytes on disk" — and the caller has no repair
|
|
81
|
+
// action that would differ between them. The cost is diagnostic only.
|
|
82
|
+
function drawioStampOf(absPath) {
|
|
83
|
+
try {
|
|
84
|
+
const st = fs.statSync(absPath);
|
|
85
|
+
return String(st.mtimeMs) + '/' + String(st.size);
|
|
86
|
+
} catch (e) {
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const VIEWER_PATH = path.join(__dirname, '..', 'vendor', 'drawio', 'viewer-static.min.js');
|
|
92
|
+
|
|
93
|
+
// 每一頁烤成一份 SVG。
|
|
94
|
+
//
|
|
95
|
+
// 為什麼是 headless Chromium 而不是 Node 裡的某個函式庫:drawio 的版面
|
|
96
|
+
// 計算(自動換行、文字度量、stencil 形狀)依賴真正的瀏覽器排版引擎,
|
|
97
|
+
// 沒有等價的 Node 實作。md2doc 已經有 puppeteer(PDF 匯出在用),所以這
|
|
98
|
+
// 不是新依賴。
|
|
99
|
+
//
|
|
100
|
+
// 遠端路徑預設值必須在 viewer 載入之前就改寫掉。viewer 自己的寫法是
|
|
101
|
+
// `window.PROXY_URL = window.PROXY_URL || "https://viewer.diagrams.net/proxy"`
|
|
102
|
+
// 這種形式 —— `||` 代表先設好的值會勝出,所以 addScriptTag 之前先 evaluate。
|
|
103
|
+
//
|
|
104
|
+
// 數字要講清楚,因為下面那份清單看起來很像「全部」:viewer 裡這種形狀的
|
|
105
|
+
// 遠端預設值共有 14 個(11 個指向 diagrams.net、3 個指向 github/gitlab),
|
|
106
|
+
// 下面只中和了靜態渲染路徑真的會讀到的 8 個,外加 2 個載入旗標。沒中和的
|
|
107
|
+
// 6 個(EXPORT_URL、VSS_CONVERT_URL、DRAWIO_LIGHTBOX_URL、DRAWIO_GITHUB_URL、
|
|
108
|
+
// DRAWIO_GITHUB_API_URL、DRAWIO_GITLAB_URL)屬於匯出/格式轉換/git 整合功能,
|
|
109
|
+
// GraphViewer.createViewerForElement() 這條路不會叫到它們。
|
|
110
|
+
//
|
|
111
|
+
// 所以「輸出離線可用」這個保證**不是**靠這份清單完整,而是靠 test/drawio.test.js
|
|
112
|
+
// 對產出斷言沒有任何遠端 URL。要加功能而動到 viewer 的其他能力時,先重新量,
|
|
113
|
+
// 不要假設這 8 個就是全部。
|
|
114
|
+
async function bakeDrawioSvg(xmlText, opts) {
|
|
115
|
+
const o = opts || {};
|
|
116
|
+
const names = pageNamesOf(xmlText);
|
|
117
|
+
let browser = o.browser;
|
|
118
|
+
let ownBrowser = false;
|
|
119
|
+
if (!browser) {
|
|
120
|
+
const puppeteer = require('puppeteer');
|
|
121
|
+
browser = await puppeteer.launch({
|
|
122
|
+
headless: 'new',
|
|
123
|
+
args: ['--no-sandbox', '--disable-setuid-sandbox',
|
|
124
|
+
'--disable-crash-reporter', '--disable-dev-shm-usage'],
|
|
125
|
+
});
|
|
126
|
+
ownBrowser = true;
|
|
127
|
+
}
|
|
128
|
+
const page = await browser.newPage();
|
|
129
|
+
try {
|
|
130
|
+
await page.setContent('<!doctype html><html><body></body></html>');
|
|
131
|
+
await page.evaluate(() => {
|
|
132
|
+
// 全部指向 about:blank 之下的相對路徑:viewer 取不到就用內建的
|
|
133
|
+
// 基本形狀,而不是去打網路。md2doc 的輸出必須離線可用。
|
|
134
|
+
window.PROXY_URL = '';
|
|
135
|
+
window.STYLE_PATH = '';
|
|
136
|
+
window.SHAPES_PATH = '';
|
|
137
|
+
window.STENCIL_PATH = '';
|
|
138
|
+
window.DRAW_MATH_URL = '';
|
|
139
|
+
window.GRAPH_IMAGE_PATH = '';
|
|
140
|
+
window.mxImageBasePath = '';
|
|
141
|
+
window.mxBasePath = '';
|
|
142
|
+
window.mxLoadStylesheets = false;
|
|
143
|
+
window.mxLoadResources = false;
|
|
144
|
+
});
|
|
145
|
+
await page.addScriptTag({ path: VIEWER_PATH });
|
|
146
|
+
|
|
147
|
+
const out = [];
|
|
148
|
+
for (let i = 0; i < names.length; i++) {
|
|
149
|
+
const svg = await page.evaluate((xml, pageIndex) => {
|
|
150
|
+
const el = document.createElement('div');
|
|
151
|
+
el.className = 'mxgraph';
|
|
152
|
+
// width 不設會讓 GraphViewer 把圖縮到容器寬度。
|
|
153
|
+
el.setAttribute('data-mxgraph', JSON.stringify({
|
|
154
|
+
xml: xml, page: pageIndex, highlight: '#0000ff',
|
|
155
|
+
toolbar: null, resize: true, nav: false,
|
|
156
|
+
}));
|
|
157
|
+
document.body.appendChild(el);
|
|
158
|
+
window.GraphViewer.createViewerForElement(el);
|
|
159
|
+
const svgEl = el.querySelector('svg');
|
|
160
|
+
const html = svgEl ? svgEl.outerHTML : null;
|
|
161
|
+
el.remove();
|
|
162
|
+
return html;
|
|
163
|
+
}, xmlText, i);
|
|
164
|
+
|
|
165
|
+
if (!svg) {
|
|
166
|
+
throw new Error('drawio viewer produced no SVG for page ' + (i + 1) +
|
|
167
|
+
' (' + (names[i] || 'unnamed') + ')');
|
|
168
|
+
}
|
|
169
|
+
out.push(svg);
|
|
170
|
+
}
|
|
171
|
+
return out;
|
|
172
|
+
} finally {
|
|
173
|
+
await page.close();
|
|
174
|
+
if (ownBrowser) await browser.close();
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
module.exports = { isDrawioXml, pageNamesOf, resolvePageIndex, bakeDrawioSvg, drawioStampOf };
|
package/lib/editor/blockmap.js
CHANGED
|
@@ -12,6 +12,79 @@ function trimmedLineCount(raw) {
|
|
|
12
12
|
return content === '' ? 1 : content.split('\n').length;
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
+
// v3.4.0 (backlog #13, closing the "改動前後相同" observation for real):
|
|
16
|
+
// MEASURED on marked 14.1.4, a top-level 'paragraph' token's `raw` can carry
|
|
17
|
+
// an internal newline that names NO real blank line in the source. Minimal
|
|
18
|
+
// case: 'a\n\tb\n---\n- x\n- y\n\ntail\n' — the paragraph's raw comes back as
|
|
19
|
+
// 'a\n\n\tb\n' (3 newlines) for what is 2 real source lines ('a', a TAB- or
|
|
20
|
+
// >=4-space-over-indented lazy continuation 'b'); every block from the '---'
|
|
21
|
+
// onward then names the line ABOVE its real one. This is not "only literal
|
|
22
|
+
// '---'" — a table SEPARATOR row immediately after the same over-indented
|
|
23
|
+
// continuation ('|---|', ':---') reproduces it too, and the trigger tracks
|
|
24
|
+
// the count of over-indented continuation lines, not any single terminator
|
|
25
|
+
// (see runs/task11-item13-scope-hunt*.js: 195+ cases across continuation
|
|
26
|
+
// width x repeat-count x fifteen terminators — corruption only where the
|
|
27
|
+
// line right after the continuation is dash/equals-shaped, matching
|
|
28
|
+
// whatever internal check marked runs to decide a following line MIGHT be a
|
|
29
|
+
// setext-heading underline; every other terminator tried — the other hr
|
|
30
|
+
// markers, headings, blockquotes, fences, header-led tables, lists, a
|
|
31
|
+
// genuine setext '===' — left raw uncorrupted in every case). A 'heading'
|
|
32
|
+
// token was TRIED as a second covered type (it goes through the same marked
|
|
33
|
+
// code path — setext heading construction IS this ambiguity's positive
|
|
34
|
+
// case) and REJECTED: test/blockmap.test.js's own first fixture ('# Title\n\n
|
|
35
|
+
// Para one.\n\n…') broke immediately, because ATX headings legitimately FOLD
|
|
36
|
+
// a trailing blank-line run into their own raw when nothing else does (raw
|
|
37
|
+
// '# Title\n\n' for a heading followed by one blank line, with no separate
|
|
38
|
+
// 'space' token) — exactly the behaviour this file's own header comment
|
|
39
|
+
// documents ("A token's raw may include trailing newlines … folded in").
|
|
40
|
+
// `t.text` for a heading is just its own single line of prose ("Title", 0
|
|
41
|
+
// newlines) and has no way to reconstruct how many blank lines got folded
|
|
42
|
+
// after it, so text-based counting silently DROPS that fold and lands the
|
|
43
|
+
// next block one line early. Paragraphs do not fold trailing blanks this way
|
|
44
|
+
// (MEASURED: a blank-line run after a paragraph always comes back as its own
|
|
45
|
+
// separate 'space' token, at EOF and mid-document alike — see
|
|
46
|
+
// runs/task11-item13-parafold.js), which is exactly why paragraphs are safe
|
|
47
|
+
// to cover here and headings are not.
|
|
48
|
+
//
|
|
49
|
+
// This file's own item-level code already named the general family: a lazy
|
|
50
|
+
// continuation's `text`-token raw is SYNTHESISED ("a lazy continuation comes
|
|
51
|
+
// back as \"x\n\ncont\n\" (three newlines) for two source lines" — see
|
|
52
|
+
// childListStartOffsets()'s header comment), which is why that code never
|
|
53
|
+
// trusts a `text` token's raw for cursor advancement. This is the same
|
|
54
|
+
// defence at buildBlockMap()'s TOP level, where a top-level 'paragraph' is
|
|
55
|
+
// marked's equivalent of an in-item lazy continuation — the top level had no
|
|
56
|
+
// corresponding defence because the header comment's own stated basis for
|
|
57
|
+
// trusting top-level raws ("at top level the raws are faithful source
|
|
58
|
+
// slices") is exactly the assumption this case disproves.
|
|
59
|
+
//
|
|
60
|
+
// The fix does not detect corruption and fall back conditionally; it simply
|
|
61
|
+
// never counts newlines out of a 'paragraph' token's `raw` at all. `t.text`
|
|
62
|
+
// — marked's own dedented join of the token's real content lines, e.g.
|
|
63
|
+
// "a\nb" for the fixture above — was MEASURED to stay correct in every
|
|
64
|
+
// corrupted case found in the sweep: its newline count always equals (real
|
|
65
|
+
// source lines consumed - 1), carrying none of the synthetic blank lines
|
|
66
|
+
// `raw` picks up. And on every UNCORRUPTED case, `text`-based and
|
|
67
|
+
// `raw`-based counting agree exactly — MEASURED via a direct before/after
|
|
68
|
+
// diff of buildBlockMap()'s own OUTPUT (not just the raw newline count) over
|
|
69
|
+
// every fixture already in test/blockmap.test.js plus a 60000-document
|
|
70
|
+
// random line-soup fuzz corpus (see runs/task11-item13-fuzz-oracle.js and
|
|
71
|
+
// the report) — so there is no accuracy cost paid on the common path.
|
|
72
|
+
//
|
|
73
|
+
// Every OTHER top-level token type is left on the original raw-based
|
|
74
|
+
// computation, unconditionally: the sweep found no corruption outside
|
|
75
|
+
// 'paragraph', and 'space' tokens in particular NEED their raw's FULL,
|
|
76
|
+
// untrimmed newline count (to skip a multi-blank-line run) — trimmedLineCount()
|
|
77
|
+
// or text-based counting would get that case wrong on purpose, not by
|
|
78
|
+
// oversight.
|
|
79
|
+
function topLevelLineInfo(t) {
|
|
80
|
+
if (t.type === 'paragraph' && typeof t.text === 'string') {
|
|
81
|
+
const contentNewlines = (t.text.match(/\n/g) || []).length;
|
|
82
|
+
const endsWithNewline = t.raw.charAt(t.raw.length - 1) === '\n';
|
|
83
|
+
return { span: contentNewlines + 1, rawNewlines: contentNewlines + (endsWithNewline ? 1 : 0) };
|
|
84
|
+
}
|
|
85
|
+
return { span: trimmedLineCount(t.raw), rawNewlines: (t.raw.match(/\n/g) || []).length };
|
|
86
|
+
}
|
|
87
|
+
|
|
15
88
|
// Recursively emit one `li` block per list item at every depth.
|
|
16
89
|
// Recursion order (mirrored by Task 4's render walk):
|
|
17
90
|
// 1. Push the item's own block.
|
|
@@ -304,23 +377,23 @@ function buildBlockMap(mdText) {
|
|
|
304
377
|
let cursor = 1; // current line number of the token's first character
|
|
305
378
|
const nextId = { v: 0 };
|
|
306
379
|
for (const t of tokens) {
|
|
307
|
-
const
|
|
380
|
+
const info = topLevelLineInfo(t);
|
|
308
381
|
if (t.type === 'list') {
|
|
309
382
|
pushListItemBlocks(t, cursor, 0, blocks, nextId);
|
|
310
383
|
} else if (t.type !== 'space') {
|
|
311
|
-
const content = t.raw.replace(/\n+$/, '');
|
|
312
|
-
const span = content === '' ? 1 : content.split('\n').length;
|
|
313
384
|
blocks.push({
|
|
314
385
|
id: nextId.v++,
|
|
315
386
|
type: t.type,
|
|
316
387
|
startLine: cursor,
|
|
317
|
-
endLine: cursor + span - 1,
|
|
388
|
+
endLine: cursor + info.span - 1,
|
|
318
389
|
});
|
|
319
390
|
}
|
|
320
|
-
// advance by full raw (including folded trailing blank lines)
|
|
391
|
+
// advance by full raw (including folded trailing blank lines) for every
|
|
392
|
+
// type except 'paragraph'/'heading', which use topLevelLineInfo()'s
|
|
393
|
+
// text-based count instead — see its own comment for why.
|
|
321
394
|
// If raw has no trailing newline (EOF), the next token doesn't exist,
|
|
322
395
|
// so the off-by-one is unobservable.
|
|
323
|
-
cursor += rawNewlines;
|
|
396
|
+
cursor += info.rawNewlines;
|
|
324
397
|
}
|
|
325
398
|
return { blocks, lineCount: mdText.split('\n').length };
|
|
326
399
|
}
|