@helping-ai-workflow/md2doc 2.10.1 → 2.11.1
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/blockmap.js +126 -8
- package/lib/editor/client.js +2431 -652
- package/lib/editor/convert-md.js +200 -0
- package/lib/editor/indent-clamp.js +152 -0
- package/lib/editor/inline-md.js +25 -1
- package/lib/editor/list-md.js +567 -4
- package/lib/editor/server.js +56 -3
- package/lib/md2doc.js +430 -39
- package/package.json +2 -2
package/lib/editor/server.js
CHANGED
|
@@ -14,6 +14,15 @@ const TABLE_MD_SRC = fs.readFileSync(path.join(__dirname, 'table-md.js'), 'utf8'
|
|
|
14
14
|
// right after it to keep the two sibling serializers grouped together.
|
|
15
15
|
const LIST_MD_SRC = fs.readFileSync(path.join(__dirname, 'list-md.js'), 'utf8');
|
|
16
16
|
const HISTORY_SRC = fs.readFileSync(path.join(__dirname, 'history.js'), 'utf8');
|
|
17
|
+
// Task 6: spec §3.4's shift-then-clamp, a pure data transform with no
|
|
18
|
+
// dependency on any other editor module — order among these is irrelevant, it
|
|
19
|
+
// only has to land before client.js reads window.md2docIndentClamp.
|
|
20
|
+
const INDENT_CLAMP_SRC = fs.readFileSync(path.join(__dirname, 'indent-clamp.js'), 'utf8');
|
|
21
|
+
// S2 spec §3.2/§4.3: the pure marker stripper/emitter behind the 轉換成
|
|
22
|
+
// submenu. Same "no dependency on any other editor module" property as
|
|
23
|
+
// indent-clamp.js above — it only has to land before client.js reads
|
|
24
|
+
// window.md2docConvertMd.
|
|
25
|
+
const CONVERT_MD_SRC = fs.readFileSync(path.join(__dirname, 'convert-md.js'), 'utf8');
|
|
17
26
|
|
|
18
27
|
function readJson(req, limitBytes = 50 * 1024 * 1024) {
|
|
19
28
|
return new Promise((resolve, reject) => {
|
|
@@ -42,6 +51,25 @@ function send(res, status, obj) {
|
|
|
42
51
|
res.end(body);
|
|
43
52
|
}
|
|
44
53
|
|
|
54
|
+
// The one invariant that ties the two halves of the payload together: every
|
|
55
|
+
// block's line range must address a line that actually EXISTS in `lines`.
|
|
56
|
+
// blockmap.js derives ranges from marked's own tokenisation while `lines`
|
|
57
|
+
// comes from a regex split here, so the two can only agree while both use the
|
|
58
|
+
// SAME definition of a line terminator — and when they disagree the failure is
|
|
59
|
+
// silent and destructive (lineops.replaceLines() splices past the end of the
|
|
60
|
+
// array, deleting every line the block map thought was there). Throwing here
|
|
61
|
+
// turns that into the server route's 500 + the client's error banner, which is
|
|
62
|
+
// a document that will not open rather than a document that opens and then
|
|
63
|
+
// eats its own tail.
|
|
64
|
+
function assertBlockRangesFit(blocks, lines) {
|
|
65
|
+
let maxEnd = 0;
|
|
66
|
+
for (const b of blocks || []) if (b.endLine > maxEnd) maxEnd = b.endLine;
|
|
67
|
+
if (maxEnd > lines.length) {
|
|
68
|
+
throw new Error('block map is out of range: endLine ' + maxEnd +
|
|
69
|
+
' > ' + lines.length + ' lines (line-terminator handling disagrees with marked)');
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
45
73
|
async function createEditorServer({ files, idleTimeoutMs = 30000, clientJs = '', listenPort = 0 }) {
|
|
46
74
|
const absFiles = files.map((f) => path.resolve(f));
|
|
47
75
|
let idleTimer = null;
|
|
@@ -84,6 +112,14 @@ async function createEditorServer({ files, idleTimeoutMs = 30000, clientJs = '',
|
|
|
84
112
|
// EOL 偵測與拆行:lines 內部一律不含 \r(spec §3.11)。只有
|
|
85
113
|
// /api/save 會把它接回檔案原本的 EOL;/api/render 一律用 \n。
|
|
86
114
|
//
|
|
115
|
+
// 三種終止符,不是兩種(T7):marked 的 preprocess 把裸 \r 正規化成
|
|
116
|
+
// \n(實測 marked 14:'# H\rpara\r' → heading + paragraph 兩個
|
|
117
|
+
// token),所以 blockmap 會給出「第 2 行」這種行號;而 /\r\n|\n/
|
|
118
|
+
// 不拆裸 \r,`lines` 只有一個元素。行號與 lines 脫鉤之後,任何
|
|
119
|
+
// commit 的 replaceLines() 都會把 startLine 之後的內容整段吃掉——
|
|
120
|
+
// 實測 '# H\rpara\r' 編輯第一個 block 之後 'para' 直接消失。
|
|
121
|
+
// 拆行規則必須跟 marked 的換行定義一致。
|
|
122
|
+
//
|
|
87
123
|
// 多數決,不是「有 CRLF 就算 CRLF」(final review I3):save 會把
|
|
88
124
|
// `lines` 全部用同一個 eol 接回去,所以一萬行的 LF 檔裡混進一行
|
|
89
125
|
// CRLF,舊式偵測會在第一次存檔時把一萬行全部改寫成 CRLF ——
|
|
@@ -92,12 +128,27 @@ async function createEditorServer({ files, idleTimeoutMs = 30000, clientJs = '',
|
|
|
92
128
|
// 用「\n 總數 − CRLF 數」算裸 LF,而不是 /(^|[^\r])\n/g:後者是
|
|
93
129
|
// non-overlapping 比對,連續空行的第二個 \n 會被前一次比對吃掉的
|
|
94
130
|
// 字元擋掉而漏數。減法沒有這個誤差。
|
|
131
|
+
//
|
|
132
|
+
// 裸 \r 也進多數決,理由跟上一段同一條:既然現在會拆它,一個純
|
|
133
|
+
// CR 檔(classic Mac)就會在第一次存檔時被整份改寫成 LF——正是
|
|
134
|
+
// §3.11 第 4 點禁止的事。平手一律 LF。
|
|
95
135
|
const lfTotal = (mdText.match(/\n/g) || []).length;
|
|
136
|
+
const crTotal = (mdText.match(/\r/g) || []).length;
|
|
96
137
|
const crlfCount = (mdText.match(/\r\n/g) || []).length;
|
|
97
|
-
const
|
|
138
|
+
const bareLf = lfTotal - crlfCount;
|
|
139
|
+
const bareCr = crTotal - crlfCount;
|
|
140
|
+
// Strict > on every comparison, so ANY tie falls through to LF — which
|
|
141
|
+
// is what the paragraph above promises. `>=` against bareCr handed a
|
|
142
|
+
// CR/CRLF tie to CRLF and contradicted it. Nothing else moves: a
|
|
143
|
+
// pure-CRLF file has bareCr === 0.
|
|
144
|
+
const eol = (crlfCount > bareLf && crlfCount > bareCr) ? '\r\n'
|
|
145
|
+
: (bareCr > bareLf && bareCr > crlfCount) ? '\r'
|
|
146
|
+
: '\n';
|
|
98
147
|
const { html, blocks } = await renderMarkdown(mdText, file, { editMode: true });
|
|
148
|
+
const lines = mdText.split(/\r\n|\r|\n/);
|
|
149
|
+
assertBlockRangesFit(blocks, lines);
|
|
99
150
|
const payload = JSON.stringify({
|
|
100
|
-
fileId, mtimeMs, eol, lines
|
|
151
|
+
fileId, mtimeMs, eol, lines, blocks,
|
|
101
152
|
});
|
|
102
153
|
const inject =
|
|
103
154
|
`<script>window.__ED__ = ${payload.replace(/</g, '\\u003c')}</script>\n` +
|
|
@@ -106,6 +157,8 @@ async function createEditorServer({ files, idleTimeoutMs = 30000, clientJs = '',
|
|
|
106
157
|
`<script>${TABLE_MD_SRC}</script>\n` +
|
|
107
158
|
`<script>${LIST_MD_SRC}</script>\n` +
|
|
108
159
|
`<script>${HISTORY_SRC}</script>\n` +
|
|
160
|
+
`<script>${INDENT_CLAMP_SRC}</script>\n` +
|
|
161
|
+
`<script>${CONVERT_MD_SRC}</script>\n` +
|
|
109
162
|
`<script>${clientJs}</script>\n`;
|
|
110
163
|
// Splice at the LAST "</body>" — the document's real closing tag.
|
|
111
164
|
// The first occurrence can sit inside an inlined diagram bundle's JS
|
|
@@ -201,4 +254,4 @@ async function createEditorServer({ files, idleTimeoutMs = 30000, clientJs = '',
|
|
|
201
254
|
};
|
|
202
255
|
}
|
|
203
256
|
|
|
204
|
-
module.exports = { createEditorServer };
|
|
257
|
+
module.exports = { createEditorServer, assertBlockRangesFit };
|