@helping-ai-workflow/md2doc 2.8.0 → 2.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/README.md +226 -0
- package/bin/md2doc.js +46 -33
- package/lib/editor/blockmap.js +75 -0
- package/lib/editor/cli.js +23 -0
- package/lib/editor/client.js +4739 -0
- package/lib/editor/history.js +94 -0
- package/lib/editor/inline-md.js +220 -0
- package/lib/editor/lineops.js +71 -0
- package/lib/editor/list-md.js +258 -0
- package/lib/editor/open.js +59 -0
- package/lib/editor/server.js +190 -0
- package/lib/editor/table-md.js +169 -0
- package/lib/md2doc.js +1147 -608
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -127,6 +127,232 @@ the PDF output.
|
|
|
127
127
|
| `md2html foo.md --open` | `md2doc foo.md` (open is default) |
|
|
128
128
|
| `md2html *.md` (output next to source) | `md2doc *.md --out ./build/` (or accept temp output) |
|
|
129
129
|
|
|
130
|
+
## Editing (`--edit`)
|
|
131
|
+
|
|
132
|
+
```bash
|
|
133
|
+
md2doc --edit foo.md # serve foo.md in the browser editor, open a tab
|
|
134
|
+
md2doc --edit foo.md bar.md # one tab per file
|
|
135
|
+
md2doc --edit foo.md --port 4000 # pin the server to a specific port
|
|
136
|
+
md2doc --edit foo.md --no-open # start the server without launching a browser tab
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
`--edit` starts a local (`127.0.0.1`-only) server and opens the rendered document in
|
|
140
|
+
your browser. Click anywhere inside a paragraph, heading, list item, or table cell
|
|
141
|
+
to place your cursor and start typing directly — no separate "select then edit" step.
|
|
142
|
+
The block is live-marked as edited; focus leaving the block commits your changes
|
|
143
|
+
automatically. Press `Ctrl+S` to save to disk explicitly.
|
|
144
|
+
|
|
145
|
+
### Direct editing: click to type
|
|
146
|
+
|
|
147
|
+
Every paragraph, heading, list, and table cell is editable by clicking inside it.
|
|
148
|
+
The rendered formatting (bold, italic, code, links) displays as you type; no
|
|
149
|
+
Markdown syntax characters are shown. This applies to any block that doesn't
|
|
150
|
+
contain unsupported content (see "Degraded blocks" below).
|
|
151
|
+
|
|
152
|
+
| Block type | Interaction |
|
|
153
|
+
|---|---|
|
|
154
|
+
| Paragraph | Click to place caret, type; Enter commits, Esc reverts |
|
|
155
|
+
| Heading | Click to place caret, type; Enter commits, Esc reverts |
|
|
156
|
+
| List | Click to place caret, type; list-specific keys below |
|
|
157
|
+
| Table | Click any cell to edit; Tab/Shift+Tab navigate between cells |
|
|
158
|
+
|
|
159
|
+
### ⠿ Block menu
|
|
160
|
+
|
|
161
|
+
Every block has a ⠿ button on its left edge. Click it to open a menu with block-level
|
|
162
|
+
operations:
|
|
163
|
+
|
|
164
|
+
| Item | Applies to | Action |
|
|
165
|
+
|---|---|---|
|
|
166
|
+
| **−** (minus) | Heading only | Decrease heading level (# → ... → #####) |
|
|
167
|
+
| **+** (plus) | Heading only | Increase heading level |
|
|
168
|
+
| **MD 原始碼** | All blocks | Discard in-progress edits, switch to raw Markdown source editing |
|
|
169
|
+
| **刪除** | All blocks | Delete the block entirely (absorbs one adjacent blank line; one Ctrl+Z restores it exactly) |
|
|
170
|
+
| **✕** | All blocks | Close the menu |
|
|
171
|
+
|
|
172
|
+
The **MD 原始碼** button is the escape hatch: it reverts any unsaved typing in the
|
|
173
|
+
current block and opens the raw Markdown editor instead, letting you make changes
|
|
174
|
+
WYSIWYG cannot express.
|
|
175
|
+
|
|
176
|
+
### + Insert menu
|
|
177
|
+
|
|
178
|
+
Every block also has a + button, stacked just above the ⠿ handle in the same left
|
|
179
|
+
gutter. Click it to open a menu for inserting a new block directly below:
|
|
180
|
+
|
|
181
|
+
| Item | Inserts |
|
|
182
|
+
|---|---|
|
|
183
|
+
| **段落** | An empty paragraph |
|
|
184
|
+
| **標題** | An empty `##` heading |
|
|
185
|
+
| **清單** | An empty single-item list |
|
|
186
|
+
| **表格** | A minimal 2×2 table skeleton |
|
|
187
|
+
| **程式碼** | An empty fenced code block |
|
|
188
|
+
|
|
189
|
+
The new block is inserted below the block whose + you clicked, and the cursor lands
|
|
190
|
+
directly in it — the first (body) cell for a table, the raw source editor for a code
|
|
191
|
+
block, and the always-on WYSIWYG surface (pre-selected, so your first keystroke
|
|
192
|
+
replaces the placeholder) for everything else. The whole insert is a single Ctrl+Z
|
|
193
|
+
step — one undo removes the block entirely.
|
|
194
|
+
|
|
195
|
+
### Paragraph and heading editing
|
|
196
|
+
|
|
197
|
+
Click inside a paragraph or heading to place your cursor. Type and format text
|
|
198
|
+
normally; the rendered marks (bold, italic, code, links) show as you type.
|
|
199
|
+
|
|
200
|
+
**Selection toolbar**: when you select text inside a paragraph or heading, a
|
|
201
|
+
floating toolbar appears with formatting buttons:
|
|
202
|
+
|
|
203
|
+
| Button | Action |
|
|
204
|
+
|---|---|
|
|
205
|
+
| **B** | Toggle bold (`**text**`) |
|
|
206
|
+
| **I** | Toggle italic (`*text*`) |
|
|
207
|
+
| **`<>`** | Wrap selection in backticks (`` `code` ``) |
|
|
208
|
+
| **🔗** | Wrap selection as a link; click to edit the URL |
|
|
209
|
+
|
|
210
|
+
**Key shortcuts**:
|
|
211
|
+
|
|
212
|
+
| Key | Action |
|
|
213
|
+
|---|---|
|
|
214
|
+
| Enter | Commit and close the editor |
|
|
215
|
+
| Shift + Enter | Insert a line break within the paragraph |
|
|
216
|
+
| Esc | Revert all edits and close the editor |
|
|
217
|
+
| Ctrl + Z | Step backward through the paragraph's local edit history, then cascade to document-level undos once exhausted |
|
|
218
|
+
| Ctrl + Y (or Ctrl + Shift + Z) | Step forward through the paragraph's local edit history, then cascade to document-level redos once exhausted |
|
|
219
|
+
|
|
220
|
+
### List editing
|
|
221
|
+
|
|
222
|
+
Click inside a list item to place your cursor and type. Lists support structural editing:
|
|
223
|
+
|
|
224
|
+
| Key | Action |
|
|
225
|
+
|---|---|
|
|
226
|
+
| Enter | Split the current item into two siblings at the caret; empty item + Enter removes it and ends the burst |
|
|
227
|
+
| Shift + Enter | Insert a line break (`<br>`) within the item (does not split) |
|
|
228
|
+
| Tab | Indent the current item (becomes a child of the previous sibling; no-op if no previous sibling) |
|
|
229
|
+
| Shift + Tab | Outdent the current item (moves after its parent; no-op at top level) |
|
|
230
|
+
| Esc | Revert all edits and close the list |
|
|
231
|
+
| Ctrl + Z / Ctrl + Y | Step through the list's local edit history, then cascade to document-level history |
|
|
232
|
+
|
|
233
|
+
An empty list (all items removed) is cleaned up automatically — the block is deleted
|
|
234
|
+
entirely and the document structure stays consistent.
|
|
235
|
+
|
|
236
|
+
### Table editing
|
|
237
|
+
|
|
238
|
+
Click any table cell to edit it. The table is treated as a single editing unit —
|
|
239
|
+
focus remains inside the table until you press Esc, click outside, or navigate away.
|
|
240
|
+
|
|
241
|
+
**Cell navigation**:
|
|
242
|
+
|
|
243
|
+
| Key / Action | Effect |
|
|
244
|
+
|---|---|
|
|
245
|
+
| Click a cell | Move to that cell and edit |
|
|
246
|
+
| Tab | Move to the next cell (left-to-right, row by row; no-op — stays put — on the last cell) |
|
|
247
|
+
| Shift + Tab | Move to the previous cell |
|
|
248
|
+
| Enter | Insert a line break (`<br>`) within the cell (does NOT commit the table) |
|
|
249
|
+
| Esc | Revert the entire table session (all cells) and discard all changes |
|
|
250
|
+
| Ctrl + Z / Ctrl + Y | Step through the table's local edit history, then cascade to document-level history |
|
|
251
|
+
|
|
252
|
+
**Column grip**: hover any cell in a column — a small horizontal 6-dot grip handle
|
|
253
|
+
appears just above it (Notion-style affordance, replacing the old edge-hover-only
|
|
254
|
+
target). Click the grip to open the column menu:
|
|
255
|
+
|
|
256
|
+
| Option | Action |
|
|
257
|
+
|---|---|
|
|
258
|
+
| **刪除欄** | Delete the column (last column protected) |
|
|
259
|
+
| **對齊** | Cycle alignment: left → center → right → left (no unset state; use Ctrl+Z to revert a cycle) |
|
|
260
|
+
|
|
261
|
+
A **+** insert bubble separately appears when you hover the top boundary between
|
|
262
|
+
two columns — click it to insert an empty column there.
|
|
263
|
+
|
|
264
|
+
**Row grip**: hover any cell in a body row — a small vertical 6-dot grip handle
|
|
265
|
+
appears just left of it (the header row never gets one; it isn't deletable or
|
|
266
|
+
draggable). Click the grip to open the row menu:
|
|
267
|
+
|
|
268
|
+
| Option | Action |
|
|
269
|
+
|---|---|
|
|
270
|
+
| **刪除列** | Delete the row (header row and last body row protected) |
|
|
271
|
+
| *Drag* | Press and hold the row grip and drag up/down; drop to reorder (body rows only; header fixed) |
|
|
272
|
+
|
|
273
|
+
A **+** insert bubble separately appears when you hover the left boundary between
|
|
274
|
+
two rows — click it to insert an empty row there.
|
|
275
|
+
|
|
276
|
+
**Edited tables emit minimal form**: tables that you edit are saved with single-space
|
|
277
|
+
padding and minimal separators (`|---|`) to keep the Markdown readable and
|
|
278
|
+
version-control-friendly.
|
|
279
|
+
|
|
280
|
+
### Degraded blocks: code, diagrams, images, math
|
|
281
|
+
|
|
282
|
+
Blocks containing content WYSIWYG cannot represent (code fences, Mermaid/Graphviz/
|
|
283
|
+
WaveDrom diagrams, images, LaTeX math, or unstyled HTML) automatically degrade to
|
|
284
|
+
raw-edit mode: click the block to open the raw Markdown source in a textarea, make
|
|
285
|
+
your changes, then press `Ctrl+Enter` to commit or `Esc` to cancel.
|
|
286
|
+
|
|
287
|
+
If a WYSIWYG session encounters unsupported content mid-edit (e.g., via a rich
|
|
288
|
+
paste), it automatically falls back to raw-edit with the block's untouched
|
|
289
|
+
on-disk source, preserving your unsaved work context.
|
|
290
|
+
|
|
291
|
+
### Burst undo: local history + cascade
|
|
292
|
+
|
|
293
|
+
Ctrl+Z / Ctrl+Y step through a block's local edit history first (the changes you
|
|
294
|
+
made in the current editing session). Once that history is exhausted, the next
|
|
295
|
+
Ctrl+Z cascades out to the document-level undo stack, covering all committed edits.
|
|
296
|
+
This lets you undo/redo recent changes within a block without affecting work in
|
|
297
|
+
other blocks.
|
|
298
|
+
|
|
299
|
+
### Whole-document controls
|
|
300
|
+
|
|
301
|
+
| Key / control | Action |
|
|
302
|
+
|---|---|
|
|
303
|
+
| `Ctrl`/`⌘` + `S` | Save the document to disk (explicit; changes commit locally when focus leaves) |
|
|
304
|
+
| `Ctrl`/`⌘` + `Z` | Undo: first steps through the focused block's local history, then cascades to document level |
|
|
305
|
+
| `Ctrl`/`⌘` + `Y` (or `Ctrl`/`⌘` + `Shift` + `Z`) | Redo: mirrors undo's cascade behavior |
|
|
306
|
+
| Click outside any block | Commit any open block if changed, dismiss the ⠿/+ menu |
|
|
307
|
+
| `Esc` (at document level, not in an open block) | Close the ⠿/+ menu |
|
|
308
|
+
|
|
309
|
+
### Auto-commit on focus change
|
|
310
|
+
|
|
311
|
+
When focus leaves a block:
|
|
312
|
+
- If the block is **unchanged**, it closes silently.
|
|
313
|
+
- If the block is **changed**, it commits automatically to the undo stack (but not to disk — you must press Ctrl+S for that).
|
|
314
|
+
|
|
315
|
+
This means switching between blocks flows naturally — you never get stuck waiting to
|
|
316
|
+
confirm or cancel.
|
|
317
|
+
|
|
318
|
+
### Save and conflict handling
|
|
319
|
+
|
|
320
|
+
**Save is explicit, not autosave**. Your changes are committed to the undo stack
|
|
321
|
+
immediately when you press Enter or focus leaves a block, but they're not written
|
|
322
|
+
to disk until you press `Ctrl+S`.
|
|
323
|
+
|
|
324
|
+
**Conflict detection**: each save carries the file's last-known modification time.
|
|
325
|
+
If the file on disk has changed since the page loaded (edited elsewhere, or saved
|
|
326
|
+
from another tab), the save is rejected with a conflict banner instead of silently
|
|
327
|
+
overwriting — reload the page to pick up the newer content, then re-apply your edit.
|
|
328
|
+
|
|
329
|
+
### Fidelity guarantee
|
|
330
|
+
|
|
331
|
+
Only the lines inside the block(s) you actually commit are rewritten. Every other
|
|
332
|
+
line — including whitespace-sensitive formatting like padded table columns, trailing
|
|
333
|
+
spaces, and the file's original EOF-newline state — is left byte-for-byte untouched,
|
|
334
|
+
whether you save with zero edits or after several. This applies both to unchanged
|
|
335
|
+
blocks and to blocks you open and then revert.
|
|
336
|
+
|
|
337
|
+
### Known Phase-3 limitations
|
|
338
|
+
|
|
339
|
+
**First diagram/math type requires reload**: if a committed edit introduces the
|
|
340
|
+
*first* occurrence of a diagram type (Mermaid or WaveDrom) that the document didn't
|
|
341
|
+
already contain when the page was loaded, that diagram library was never embedded
|
|
342
|
+
into the page, so the new block renders as raw source until you reload the browser
|
|
343
|
+
tab (no need to restart `md2doc --edit`). The same applies to math: if a committed
|
|
344
|
+
edit introduces the document's *first* `math` fence or `$…$`/`$$…$$` expression,
|
|
345
|
+
the KaTeX stylesheet was never injected, so the equation renders unstyled until you
|
|
346
|
+
reload the tab.
|
|
347
|
+
|
|
348
|
+
**Alignment cycle has no unset state**: the **對齊** button cycles through left,
|
|
349
|
+
center, and right alignment. To revert an unwanted alignment, use Ctrl+Z.
|
|
350
|
+
|
|
351
|
+
---
|
|
352
|
+
|
|
353
|
+
`--edit` does not support directory inputs yet (planned for a later phase) and
|
|
354
|
+
cannot be combined with `--html` / `--pdf` / `--out` / `--bake-svg`.
|
|
355
|
+
|
|
130
356
|
## Supported diagram types
|
|
131
357
|
|
|
132
358
|
Embedded in fenced code blocks inside your Markdown:
|
package/bin/md2doc.js
CHANGED
|
@@ -33,6 +33,8 @@ function printHelp() {
|
|
|
33
33
|
' --no-open Skip the viewer launch.',
|
|
34
34
|
' --quiet Suppress per-file progress messages.',
|
|
35
35
|
' --bake-svg Pre-render mermaid/wavedrom to inert SVG (HTML output only; needs Chromium).',
|
|
36
|
+
' --edit Serve listed .md files in the browser editor (localhost).',
|
|
37
|
+
' --port <n> Pin the editor server port (only with --edit).',
|
|
36
38
|
' --version, -v Print version.',
|
|
37
39
|
' --help, -h Print this help.',
|
|
38
40
|
''
|
|
@@ -47,6 +49,8 @@ function parseArgs(argv) {
|
|
|
47
49
|
let openExplicit = null; // null = unset; true/false = user-specified
|
|
48
50
|
let quiet = false;
|
|
49
51
|
let bakeSvg = false;
|
|
52
|
+
let edit = false;
|
|
53
|
+
let port = null;
|
|
50
54
|
|
|
51
55
|
for (let i = 0; i < argv.length; i++) {
|
|
52
56
|
const a = argv[i];
|
|
@@ -64,6 +68,21 @@ function parseArgs(argv) {
|
|
|
64
68
|
if (a === '--no-open') { openExplicit = false; continue; }
|
|
65
69
|
if (a === '--quiet') { quiet = true; continue; }
|
|
66
70
|
if (a === '--bake-svg') { bakeSvg = true; continue; }
|
|
71
|
+
if (a === '--edit') { edit = true; continue; }
|
|
72
|
+
if (a === '--port') {
|
|
73
|
+
i++;
|
|
74
|
+
if (i >= argv.length) {
|
|
75
|
+
process.stderr.write('error: --port requires a value\n');
|
|
76
|
+
process.exit(2);
|
|
77
|
+
}
|
|
78
|
+
const n = Number(argv[i]);
|
|
79
|
+
if (!Number.isInteger(n) || n < 0) {
|
|
80
|
+
process.stderr.write('error: --port must be a non-negative integer\n');
|
|
81
|
+
process.exit(2);
|
|
82
|
+
}
|
|
83
|
+
port = n;
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
67
86
|
if (a === '--out') {
|
|
68
87
|
if (out !== null) {
|
|
69
88
|
process.stderr.write('error: --out specified more than once\n');
|
|
@@ -105,7 +124,7 @@ function parseArgs(argv) {
|
|
|
105
124
|
open = (out === null);
|
|
106
125
|
}
|
|
107
126
|
|
|
108
|
-
return { inputs, formats, formatsExplicit, out, open, quiet, bakeSvg };
|
|
127
|
+
return { inputs, formats, formatsExplicit, out, open, quiet, bakeSvg, edit, port };
|
|
109
128
|
}
|
|
110
129
|
|
|
111
130
|
function shortHash(absPath) {
|
|
@@ -200,38 +219,7 @@ function resolveOutputs(args) {
|
|
|
200
219
|
return pairs;
|
|
201
220
|
}
|
|
202
221
|
|
|
203
|
-
|
|
204
|
-
if (process.platform !== 'linux') return false;
|
|
205
|
-
if (process.env.WSL_DISTRO_NAME || process.env.WSL_INTEROP) return true;
|
|
206
|
-
try {
|
|
207
|
-
return fs.existsSync('/proc/sys/fs/binfmt_misc/WSLInterop');
|
|
208
|
-
} catch (_) {
|
|
209
|
-
return false;
|
|
210
|
-
}
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
function openViewer(filePath) {
|
|
214
|
-
const platform = process.platform;
|
|
215
|
-
let cmd, args;
|
|
216
|
-
if (platform === 'darwin') {
|
|
217
|
-
cmd = 'open'; args = [filePath];
|
|
218
|
-
} else if (platform === 'win32') {
|
|
219
|
-
cmd = 'cmd'; args = ['/c', 'start', '""', filePath];
|
|
220
|
-
} else if (isWSL()) {
|
|
221
|
-
const r = spawnSync('wslpath', ['-w', filePath], { encoding: 'utf8' });
|
|
222
|
-
if (r.status === 0 && r.stdout) {
|
|
223
|
-
cmd = 'explorer.exe'; args = [r.stdout.trim()];
|
|
224
|
-
} else {
|
|
225
|
-
cmd = 'xdg-open'; args = [filePath];
|
|
226
|
-
}
|
|
227
|
-
} else {
|
|
228
|
-
cmd = 'xdg-open'; args = [filePath];
|
|
229
|
-
}
|
|
230
|
-
const r = spawnSync(cmd, args, { stdio: 'ignore' });
|
|
231
|
-
if (r.error) {
|
|
232
|
-
process.stderr.write('warning: could not launch viewer for ' + filePath + ': ' + r.error.message + '\n');
|
|
233
|
-
}
|
|
234
|
-
}
|
|
222
|
+
const { openViewer } = require('../lib/editor/open.js');
|
|
235
223
|
|
|
236
224
|
function main() {
|
|
237
225
|
const args = parseArgs(process.argv.slice(2));
|
|
@@ -243,6 +231,31 @@ function main() {
|
|
|
243
231
|
}
|
|
244
232
|
}
|
|
245
233
|
|
|
234
|
+
if (args.edit) {
|
|
235
|
+
if (args.formatsExplicit || args.out !== null || args.bakeSvg) {
|
|
236
|
+
process.stderr.write('error: --edit cannot be combined with --html/--pdf/--out/--bake-svg\n');
|
|
237
|
+
process.exit(2);
|
|
238
|
+
}
|
|
239
|
+
for (const input of args.inputs) {
|
|
240
|
+
if (!fs.existsSync(input)) {
|
|
241
|
+
process.stderr.write('error: input not found: ' + input + '\n');
|
|
242
|
+
process.exit(1);
|
|
243
|
+
}
|
|
244
|
+
if (fs.statSync(input).isDirectory()) {
|
|
245
|
+
process.stderr.write('error: --edit directory mode is not implemented yet (Phase 3)\n');
|
|
246
|
+
process.exit(2);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
const { startEditSession } = require('../lib/editor/cli.js');
|
|
250
|
+
startEditSession({ files: args.inputs, port: args.port, open: args.open, quiet: args.quiet })
|
|
251
|
+
.catch((e) => { process.stderr.write('error: ' + e.message + '\n'); process.exit(1); });
|
|
252
|
+
return; // do not fall through to render pipeline
|
|
253
|
+
}
|
|
254
|
+
if (args.port !== null) {
|
|
255
|
+
process.stderr.write('error: --port is only valid with --edit\n');
|
|
256
|
+
process.exit(2);
|
|
257
|
+
}
|
|
258
|
+
|
|
246
259
|
const pairs = resolveOutputs(args);
|
|
247
260
|
const outputs = [];
|
|
248
261
|
for (const { input, output } of pairs) {
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
const { marked } = require('marked');
|
|
3
|
+
|
|
4
|
+
// Top-level marked tokens → 1-indexed inclusive line ranges.
|
|
5
|
+
// `space` tokens (blank-line runs) advance the cursor but are not blocks.
|
|
6
|
+
// A token's `raw` may include trailing newlines (blank lines marked folded
|
|
7
|
+
// in); the block's endLine excludes those so ranges never claim blank lines
|
|
8
|
+
// that separate blocks.
|
|
9
|
+
|
|
10
|
+
function trimmedLineCount(raw) {
|
|
11
|
+
const content = raw.replace(/\n+$/, '');
|
|
12
|
+
return content === '' ? 1 : content.split('\n').length;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// Recursively emit one `li` block per list item at every depth.
|
|
16
|
+
// Recursion order (mirrored by Task 4's render walk):
|
|
17
|
+
// 1. Push the item's own block.
|
|
18
|
+
// 2. Recurse into that item's nested child lists, left-to-right.
|
|
19
|
+
// 3. Move on to the next sibling item.
|
|
20
|
+
// `nextId` is a shared box { v: <int> } so ids stay 0..n-1 in document order.
|
|
21
|
+
function pushListItemBlocks(listToken, cursor, indent, blocks, nextId) {
|
|
22
|
+
for (const item of listToken.items) {
|
|
23
|
+
const childListTokens = item.tokens.filter((t) => t.type === 'list');
|
|
24
|
+
const totalSpan = trimmedLineCount(item.raw);
|
|
25
|
+
const childSpan = childListTokens.reduce((s, t) => s + trimmedLineCount(t.raw), 0);
|
|
26
|
+
const ownSpan = totalSpan - childSpan;
|
|
27
|
+
const block = {
|
|
28
|
+
id: nextId.v++,
|
|
29
|
+
type: 'li',
|
|
30
|
+
startLine: cursor,
|
|
31
|
+
endLine: cursor + ownSpan - 1,
|
|
32
|
+
listType: item.task ? 'task' : (listToken.ordered ? 'ol' : 'ul'),
|
|
33
|
+
indent,
|
|
34
|
+
};
|
|
35
|
+
if (item.task) block.checked = !!item.checked;
|
|
36
|
+
blocks.push(block);
|
|
37
|
+
let childCursor = cursor + ownSpan;
|
|
38
|
+
for (const ct of childListTokens) {
|
|
39
|
+
pushListItemBlocks(ct, childCursor, indent + 1, blocks, nextId);
|
|
40
|
+
childCursor += (ct.raw.match(/\n/g) || []).length;
|
|
41
|
+
}
|
|
42
|
+
// advance cursor by full item raw newlines; fall back to totalSpan if raw
|
|
43
|
+
// has no trailing newline (EOF item).
|
|
44
|
+
cursor += (item.raw.match(/\n/g) || []).length || totalSpan;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function buildBlockMap(mdText) {
|
|
49
|
+
const tokens = marked.lexer(mdText);
|
|
50
|
+
const blocks = [];
|
|
51
|
+
let cursor = 1; // current line number of the token's first character
|
|
52
|
+
const nextId = { v: 0 };
|
|
53
|
+
for (const t of tokens) {
|
|
54
|
+
const rawNewlines = (t.raw.match(/\n/g) || []).length;
|
|
55
|
+
if (t.type === 'list') {
|
|
56
|
+
pushListItemBlocks(t, cursor, 0, blocks, nextId);
|
|
57
|
+
} else if (t.type !== 'space') {
|
|
58
|
+
const content = t.raw.replace(/\n+$/, '');
|
|
59
|
+
const span = content === '' ? 1 : content.split('\n').length;
|
|
60
|
+
blocks.push({
|
|
61
|
+
id: nextId.v++,
|
|
62
|
+
type: t.type,
|
|
63
|
+
startLine: cursor,
|
|
64
|
+
endLine: cursor + span - 1,
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
// advance by full raw (including folded trailing blank lines).
|
|
68
|
+
// If raw has no trailing newline (EOF), the next token doesn't exist,
|
|
69
|
+
// so the off-by-one is unobservable.
|
|
70
|
+
cursor += rawNewlines;
|
|
71
|
+
}
|
|
72
|
+
return { blocks, lineCount: mdText.split('\n').length };
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
module.exports = { buildBlockMap };
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
const fs = require('fs');
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const { createEditorServer } = require('./server.js');
|
|
5
|
+
|
|
6
|
+
const CLIENT_SRC = fs.readFileSync(path.join(__dirname, 'client.js'), 'utf8');
|
|
7
|
+
|
|
8
|
+
async function startEditSession({ files, port = null, open = true, quiet = false }) {
|
|
9
|
+
const srv = await createEditorServer({
|
|
10
|
+
files,
|
|
11
|
+
clientJs: CLIENT_SRC,
|
|
12
|
+
listenPort: port === null ? 0 : port,
|
|
13
|
+
});
|
|
14
|
+
const urls = files.map((f) => srv.urlFor(f));
|
|
15
|
+
for (const u of urls) process.stdout.write(u + '\n');
|
|
16
|
+
if (open) {
|
|
17
|
+
const { openViewer } = require('./open.js');
|
|
18
|
+
for (const u of urls) openViewer(u);
|
|
19
|
+
}
|
|
20
|
+
srv.server.on('close', () => process.exit(0));
|
|
21
|
+
return srv;
|
|
22
|
+
}
|
|
23
|
+
module.exports = { startEditSession };
|