pi-odd-worktree 0.0.0-stage → 1.0.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 +234 -2
- package/extensions/odd-worktree-tools.ts +7593 -0
- package/lib/checkout-facts.js +598 -0
- package/lib/clipboard.js +226 -0
- package/lib/gh-issues.js +1555 -0
- package/lib/issue-preview.js +273 -0
- package/lib/issues-view.js +2274 -0
- package/lib/odd-docs.js +505 -0
- package/lib/odd-markdown.js +413 -0
- package/lib/odd-view.js +990 -0
- package/lib/open-external.js +106 -0
- package/lib/preview-scroll-keys.js +192 -0
- package/lib/session-auto-name.js +292 -0
- package/lib/session-worktree-register.js +62 -0
- package/lib/tui-paint.js +273 -0
- package/lib/view-layout.js +368 -0
- package/lib/warp-create-panel.js +722 -0
- package/lib/warp-create-picker.js +906 -0
- package/lib/warp-delete-confirm.js +465 -0
- package/lib/warp-selector-view.js +1265 -0
- package/lib/worktree-card.js +1117 -0
- package/lib/worktree-changes.js +1395 -0
- package/lib/worktree-close-herdr.js +73 -0
- package/lib/worktree-git.js +1048 -0
- package/lib/worktree-hooks.js +738 -0
- package/lib/worktree-model-result.js +291 -0
- package/lib/worktree-registry.js +886 -0
- package/lib/worktree-session-guard.js +595 -0
- package/lib/worktree-symlinks.js +336 -0
- package/lib/worktree-warp-sessions.js +566 -0
- package/package.json +36 -4
- package/tools/commands.test.js +176 -0
- package/tools/fixtures/gh-api-user.json +1 -0
- package/tools/fixtures/gh-issue-list.json +1 -0
- package/tools/fixtures/gh-issue-view.json +1 -0
- package/tools/fixtures/gh-label-list.json +1 -0
- package/tools/fixtures/gh-repo-view.json +1 -0
- package/tools/fixtures/load-extension.js +66 -0
- package/tools/gh-issues-feedback.test.js +359 -0
- package/tools/gh-issues.test.js +1502 -0
- package/tools/issue-preview.test.js +216 -0
- package/tools/issues-command.test.js +1217 -0
- package/tools/issues-view.test.js +4401 -0
- package/tools/odd-docs.test.js +803 -0
- package/tools/odd-markdown.test.js +236 -0
- package/tools/odd-view.test.js +2328 -0
- package/tools/package-surface.test.js +138 -0
- package/tools/t5-session-identity.test.js +306 -0
- package/tools/warp-create-panel.test.js +185 -0
- package/tools/warp-create-picker.test.js +1357 -0
- package/tools/warp-delete-confirm.test.js +616 -0
- package/tools/warp-selector-view.test.js +1463 -0
- package/tools/worktree-changes.test.js +951 -0
- package/tools/worktree-close-herdr-async.test.js +204 -0
- package/tools/worktree-odd-migration.test.js +224 -0
- package/tools/worktree-registry-cache.test.js +163 -0
- package/tools/worktree-registry-odd.test.js +231 -0
- package/tools/worktree-registry.test.js +244 -0
- package/tools/worktree-session-guard.test.js +507 -0
- package/tools/worktree-tools.test.js +434 -0
|
@@ -0,0 +1,413 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
// Odd document markdown renderer (odd-viewer S5).
|
|
3
|
+
//
|
|
4
|
+
// Pure text in, styled lines out: no host API, no theme implementation and
|
|
5
|
+
// no filesystem access live here, so the renderer runs headless in tests.
|
|
6
|
+
// The caller injects the paint seam (`paint(role, text)`, plus optional
|
|
7
|
+
// `emphasis` for bold and `measure` for width), which keeps every role name
|
|
8
|
+
// out of this module and lets a theme-less host render plain text.
|
|
9
|
+
//
|
|
10
|
+
// Scope is deliberately the shape this repository's odd documents actually
|
|
11
|
+
// use (inventoried: file title, `## Specs`, `## Tasks`, `## Log`, occasional
|
|
12
|
+
// `## Design`/`## Reconciliation`/`## Known environmental failures` with
|
|
13
|
+
// `###` sub-headings, task tables, bullets, GitHub task rows, fenced code
|
|
14
|
+
// and inline code). Consecutive blank lines collapse to one separator and
|
|
15
|
+
// trailing blanks are trimmed: every source line with content reaches the
|
|
16
|
+
// output, and nothing inside a line is lost — a long line wraps instead.
|
|
17
|
+
|
|
18
|
+
const BLOCK_ROLES = {
|
|
19
|
+
heading1: 'mdHeading',
|
|
20
|
+
heading2: 'mdHeading',
|
|
21
|
+
heading3: 'accent',
|
|
22
|
+
heading4: 'muted',
|
|
23
|
+
bullet: 'text',
|
|
24
|
+
code: 'dim',
|
|
25
|
+
quote: 'muted',
|
|
26
|
+
rule: 'border',
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
const LIST_ITEM = /^(\s*)([-*+])\s+(.*)$/;
|
|
30
|
+
const ORDERED_ITEM = /^(\s*)(\d+)[.)]\s+(.*)$/;
|
|
31
|
+
const CHECKBOX = /^\[( |x|X)\]\s+(.*)$/;
|
|
32
|
+
const HEADING = /^(#{1,6})\s+(.*?)\s*$/;
|
|
33
|
+
const RULE = /^\s*(-{3,}|\*{3,}|_{3,})\s*$/;
|
|
34
|
+
const FENCE = /^\s*(?:```|~~~)/;
|
|
35
|
+
const TABLE_ROW = /^\s*\|(.*)\|\s*$/;
|
|
36
|
+
const TABLE_RULE = /^[\s|:-]+$/;
|
|
37
|
+
const QUOTE = /^\s*>\s?(.*)$/;
|
|
38
|
+
|
|
39
|
+
// A rendered markdown line can come from someone else's tracker (the Issues
|
|
40
|
+
// preview), so a control sequence in the source is display data and never an
|
|
41
|
+
// instruction: ANSI/OSC sequences and the remaining C0 controls are dropped
|
|
42
|
+
// before anything is parsed, and a tab becomes spaces so the width budget below
|
|
43
|
+
// matches what the terminal draws.
|
|
44
|
+
const OSC_SEQUENCE = /\x1b\][^\x07\x1b]*(?:\x07|\x1b\\)/g;
|
|
45
|
+
const CSI_SEQUENCE = /\x1b\[[0-9;?]*[ -/]*[@-~]/g;
|
|
46
|
+
const ESC_SEQUENCE = /\x1b[@-Z\\-_]?/g;
|
|
47
|
+
const C0_CONTROL = /[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/g;
|
|
48
|
+
|
|
49
|
+
function sanitizeSource(value) {
|
|
50
|
+
return String(value ?? '')
|
|
51
|
+
.replace(OSC_SEQUENCE, '')
|
|
52
|
+
.replace(CSI_SEQUENCE, '')
|
|
53
|
+
.replace(ESC_SEQUENCE, '')
|
|
54
|
+
.replace(C0_CONTROL, '')
|
|
55
|
+
.replace(/\t/g, ' ');
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function stripAnsi(value) {
|
|
59
|
+
return String(value).replace(/\x1b\[[0-9;]*m/g, '').replace(/\x1b\]8;;.*?\x07/g, '');
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function defaultMeasure(text) {
|
|
63
|
+
return [...stripAnsi(text)].length;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function makeContext(options) {
|
|
67
|
+
const opts = options && typeof options === 'object' ? options : {};
|
|
68
|
+
const rawWidth = Number(opts.width);
|
|
69
|
+
const width = Number.isFinite(rawWidth) && rawWidth > 0 ? Math.floor(rawWidth) : 80;
|
|
70
|
+
// Off by default: an odd document keeps the line-per-line shape its authors
|
|
71
|
+
// wrote. The Issues preview turns it on, because there a single newline
|
|
72
|
+
// inside a paragraph is a space (github-flavored markdown).
|
|
73
|
+
const flowParagraphs = opts.flowParagraphs === true;
|
|
74
|
+
const paint = typeof opts.paint === 'function' ? opts.paint : (_role, text) => String(text);
|
|
75
|
+
const emphasis = typeof opts.emphasis === 'function' ? opts.emphasis : (text) => String(text);
|
|
76
|
+
const measure = typeof opts.measure === 'function' ? opts.measure : defaultMeasure;
|
|
77
|
+
const safePaint = (role, text) => {
|
|
78
|
+
try {
|
|
79
|
+
const painted = paint(role, String(text));
|
|
80
|
+
return typeof painted === 'string' ? painted : String(text);
|
|
81
|
+
} catch {
|
|
82
|
+
// A hostile theme never breaks the renderer.
|
|
83
|
+
return String(text);
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
const safeEmphasis = (text) => {
|
|
87
|
+
try {
|
|
88
|
+
const bold = emphasis(String(text));
|
|
89
|
+
return typeof bold === 'string' ? bold : String(text);
|
|
90
|
+
} catch {
|
|
91
|
+
return String(text);
|
|
92
|
+
}
|
|
93
|
+
};
|
|
94
|
+
const safeMeasure = (text) => {
|
|
95
|
+
try {
|
|
96
|
+
const size = Number(measure(text));
|
|
97
|
+
if (Number.isFinite(size)) return size;
|
|
98
|
+
} catch {
|
|
99
|
+
// Fall through to the plain codepoint count.
|
|
100
|
+
}
|
|
101
|
+
return defaultMeasure(text);
|
|
102
|
+
};
|
|
103
|
+
return { width, flowParagraphs, paint: safePaint, emphasis: safeEmphasis, measure: safeMeasure };
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Inline spans -> styled words. Emphasis and code are applied per WORD so
|
|
107
|
+
// every emitted word carries a balanced escape sequence: the wrapper below
|
|
108
|
+
// joins words with plain spaces, which is what keeps a wrapped line
|
|
109
|
+
// measurable without ever cutting through an escape.
|
|
110
|
+
function inlineTokens(text) {
|
|
111
|
+
const tokens = [];
|
|
112
|
+
const source = String(text);
|
|
113
|
+
const pattern = /`([^`]+)`|\*\*([^*]+)\*\*|\[([^\]]+)\]\(([^)]+)\)|__([^_]+)__/g;
|
|
114
|
+
let cursor = 0;
|
|
115
|
+
let match;
|
|
116
|
+
while ((match = pattern.exec(source)) !== null) {
|
|
117
|
+
if (match.index > cursor) tokens.push({ text: source.slice(cursor, match.index), style: 'text' });
|
|
118
|
+
if (match[1] !== undefined) tokens.push({ text: match[1], style: 'code' });
|
|
119
|
+
else if (match[2] !== undefined) tokens.push({ text: match[2], style: 'bold' });
|
|
120
|
+
else if (match[3] !== undefined) tokens.push({ text: match[3], style: 'text' });
|
|
121
|
+
else if (match[5] !== undefined) tokens.push({ text: match[5], style: 'bold' });
|
|
122
|
+
cursor = pattern.lastIndex;
|
|
123
|
+
}
|
|
124
|
+
if (cursor < source.length) tokens.push({ text: source.slice(cursor), style: 'text' });
|
|
125
|
+
return tokens;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function styleOf(ctx, style, word) {
|
|
129
|
+
if (style === 'code') return ctx.paint('accent', ctx.paint('dim', word));
|
|
130
|
+
if (style === 'dim') return ctx.paint('dim', word);
|
|
131
|
+
if (style === 'bold') return ctx.emphasis(ctx.paint('text', word));
|
|
132
|
+
return ctx.paint('text', word);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function wordsOf(ctx, text) {
|
|
136
|
+
const words = [];
|
|
137
|
+
let previousText = null;
|
|
138
|
+
for (const token of inlineTokens(text)) {
|
|
139
|
+
// A word stays glued when no source whitespace separates it from the
|
|
140
|
+
// previous word: the previous token ends with a non-space and this one
|
|
141
|
+
// starts with one (tokens partition the source, so adjacency is exact).
|
|
142
|
+
// Later words inside one token always follow whitespace, never glued.
|
|
143
|
+
const gluedToken = previousText !== null && !/\s$/.test(previousText) && !/^\s/.test(token.text);
|
|
144
|
+
let first = true;
|
|
145
|
+
for (const part of String(token.text).split(/\s+/)) {
|
|
146
|
+
if (part.length > 0) {
|
|
147
|
+
words.push({ painted: styleOf(ctx, token.style, part), style: token.style, word: part, glued: first && gluedToken && words.length > 0 });
|
|
148
|
+
first = false;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
previousText = token.text;
|
|
152
|
+
}
|
|
153
|
+
return words;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Hard split for a single word wider than the line: it is cut, never
|
|
157
|
+
// dropped, so no content disappears silently.
|
|
158
|
+
function splitWord(ctx, word, width) {
|
|
159
|
+
const pieces = [];
|
|
160
|
+
let current = '';
|
|
161
|
+
for (const char of Array.from(word.word)) {
|
|
162
|
+
const next = current + char;
|
|
163
|
+
if (ctx.measure(next) > width && current.length > 0) {
|
|
164
|
+
pieces.push(current);
|
|
165
|
+
current = char;
|
|
166
|
+
} else {
|
|
167
|
+
current = next;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
if (current.length > 0) pieces.push(current);
|
|
171
|
+
return pieces.map((piece) => styleOf(ctx, word.style, piece));
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function wrapWords(ctx, words, width) {
|
|
175
|
+
// Glued words form one atom joined without a space; line breaks happen
|
|
176
|
+
// only between atoms (where the source had whitespace), except for the
|
|
177
|
+
// hard split of an atom wider than the line below.
|
|
178
|
+
const atoms = [];
|
|
179
|
+
for (const word of words) {
|
|
180
|
+
if (word.glued && atoms.length > 0) {
|
|
181
|
+
const last = atoms[atoms.length - 1];
|
|
182
|
+
last.words.push(word);
|
|
183
|
+
last.painted += word.painted;
|
|
184
|
+
} else {
|
|
185
|
+
atoms.push({ words: [word], painted: word.painted });
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
for (const atom of atoms) atom.size = ctx.measure(atom.painted);
|
|
189
|
+
const lines = [];
|
|
190
|
+
let current = '';
|
|
191
|
+
let currentWidth = 0;
|
|
192
|
+
const flush = () => {
|
|
193
|
+
if (currentWidth > 0) lines.push(current);
|
|
194
|
+
current = '';
|
|
195
|
+
currentWidth = 0;
|
|
196
|
+
};
|
|
197
|
+
for (const atom of atoms) {
|
|
198
|
+
if (atom.size > width) {
|
|
199
|
+
if (atom.words.length === 1) {
|
|
200
|
+
flush();
|
|
201
|
+
for (const piece of splitWord(ctx, atom.words[0], width)) {
|
|
202
|
+
if (ctx.measure(piece) >= width) lines.push(piece);
|
|
203
|
+
else {
|
|
204
|
+
current = piece;
|
|
205
|
+
currentWidth = ctx.measure(piece);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
flush();
|
|
209
|
+
continue;
|
|
210
|
+
}
|
|
211
|
+
flush();
|
|
212
|
+
for (const word of atom.words) {
|
|
213
|
+
const size = ctx.measure(word.painted);
|
|
214
|
+
if (size > width) {
|
|
215
|
+
flush();
|
|
216
|
+
for (const piece of splitWord(ctx, word, width)) {
|
|
217
|
+
if (ctx.measure(piece) >= width) lines.push(piece);
|
|
218
|
+
else {
|
|
219
|
+
current = piece;
|
|
220
|
+
currentWidth = ctx.measure(piece);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
flush();
|
|
224
|
+
continue;
|
|
225
|
+
}
|
|
226
|
+
if (currentWidth === 0) {
|
|
227
|
+
current = word.painted;
|
|
228
|
+
currentWidth = size;
|
|
229
|
+
} else if (currentWidth + size <= width) {
|
|
230
|
+
current += word.painted;
|
|
231
|
+
currentWidth += size;
|
|
232
|
+
} else {
|
|
233
|
+
flush();
|
|
234
|
+
current = word.painted;
|
|
235
|
+
currentWidth = size;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
continue;
|
|
239
|
+
}
|
|
240
|
+
if (currentWidth === 0) {
|
|
241
|
+
current = atom.painted;
|
|
242
|
+
currentWidth = atom.size;
|
|
243
|
+
continue;
|
|
244
|
+
}
|
|
245
|
+
if (currentWidth + 1 + atom.size <= width) {
|
|
246
|
+
current = `${current} ${atom.painted}`;
|
|
247
|
+
currentWidth += 1 + atom.size;
|
|
248
|
+
continue;
|
|
249
|
+
}
|
|
250
|
+
flush();
|
|
251
|
+
current = atom.painted;
|
|
252
|
+
currentWidth = atom.size;
|
|
253
|
+
}
|
|
254
|
+
flush();
|
|
255
|
+
return lines;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function hardWrap(ctx, text, width) {
|
|
259
|
+
const words = [];
|
|
260
|
+
for (const part of String(text).split(/\s+/)) {
|
|
261
|
+
if (part.length > 0) words.push({ painted: ctx.paint('dim', part), style: 'dim', word: part });
|
|
262
|
+
}
|
|
263
|
+
return wrapWords(ctx, words, width);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// A line that opens its own block: blank, fence, heading, rule, quote, list or
|
|
267
|
+
// ordered item, or a table header (a `|…|` row followed by its `|---|` rule).
|
|
268
|
+
function startsBlock(raw, index) {
|
|
269
|
+
const line = raw[index];
|
|
270
|
+
if (line === undefined || String(line).trim().length === 0) return true;
|
|
271
|
+
if (FENCE.test(line) || HEADING.test(line) || RULE.test(line) || QUOTE.test(line)) return true;
|
|
272
|
+
if (LIST_ITEM.test(line) || ORDERED_ITEM.test(line)) return true;
|
|
273
|
+
return isTableStart(raw, index);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
function isTableStart(raw, index) {
|
|
277
|
+
const header = raw[index];
|
|
278
|
+
if (header === undefined || !TABLE_ROW.test(header)) return false;
|
|
279
|
+
const rule = raw[index + 1];
|
|
280
|
+
if (rule === undefined || !TABLE_ROW.test(rule)) return false;
|
|
281
|
+
return TABLE_RULE.test(TABLE_ROW.exec(rule)[1]);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
function renderTableRow(ctx, line, width) {
|
|
285
|
+
const inner = String(line).trim().replace(/^\|/, '').replace(/\|$/, '');
|
|
286
|
+
const cells = inner.split('|').map((cell) => cell.trim());
|
|
287
|
+
const text = cells.join(' | ');
|
|
288
|
+
return wrapWords(ctx, wordsOf(ctx, text), width);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// Blank-line policy: one separator between blocks, none at the start or
|
|
292
|
+
// end. Returns true when a separator was emitted.
|
|
293
|
+
function pushSeparator(lines) {
|
|
294
|
+
if (lines.length > 0 && lines[lines.length - 1] !== '') lines.push('');
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
function renderOddMarkdown(text, options) {
|
|
298
|
+
const ctx = makeContext(options);
|
|
299
|
+
const width = ctx.width;
|
|
300
|
+
const source = sanitizeSource(text).replace(/\r\n?/g, '\n');
|
|
301
|
+
if (source.trim().length === 0) return [];
|
|
302
|
+
const raw = source.split('\n');
|
|
303
|
+
const lines = [];
|
|
304
|
+
let index = 0;
|
|
305
|
+
while (index < raw.length) {
|
|
306
|
+
const line = raw[index];
|
|
307
|
+
if (line.trim().length === 0) {
|
|
308
|
+
pushSeparator(lines);
|
|
309
|
+
index += 1;
|
|
310
|
+
continue;
|
|
311
|
+
}
|
|
312
|
+
if (FENCE.test(line)) {
|
|
313
|
+
pushSeparator(lines);
|
|
314
|
+
index += 1;
|
|
315
|
+
const body = [];
|
|
316
|
+
while (index < raw.length && !FENCE.test(raw[index])) {
|
|
317
|
+
body.push(raw[index]);
|
|
318
|
+
index += 1;
|
|
319
|
+
}
|
|
320
|
+
if (index < raw.length) index += 1;
|
|
321
|
+
for (const codeLine of body) {
|
|
322
|
+
const wrapped = hardWrap(ctx, codeLine.length > 0 ? codeLine : ' ', Math.max(1, width - 2));
|
|
323
|
+
for (const piece of wrapped) lines.push(` ${piece}`);
|
|
324
|
+
}
|
|
325
|
+
continue;
|
|
326
|
+
}
|
|
327
|
+
const heading = HEADING.exec(line);
|
|
328
|
+
if (heading) {
|
|
329
|
+
const level = heading[1].length;
|
|
330
|
+
const label = heading[2];
|
|
331
|
+
const mark = level === 1 ? '▌ ' : level === 2 ? '▌ ' : level === 3 ? ' ▪ ' : ' · ';
|
|
332
|
+
const role = level === 1 ? BLOCK_ROLES.heading1 : level === 2 ? BLOCK_ROLES.heading2 : level === 3 ? BLOCK_ROLES.heading3 : BLOCK_ROLES.heading4;
|
|
333
|
+
pushSeparator(lines);
|
|
334
|
+
const painted = level <= 2
|
|
335
|
+
? ctx.paint(role, ctx.emphasis(`${mark}${label}`))
|
|
336
|
+
: ctx.paint(role, `${mark}${label}`);
|
|
337
|
+
lines.push(...wrapWords(ctx, [{ painted, style: 'text', word: `${mark}${label}` }], width));
|
|
338
|
+
index += 1;
|
|
339
|
+
continue;
|
|
340
|
+
}
|
|
341
|
+
if (RULE.test(line)) {
|
|
342
|
+
pushSeparator(lines);
|
|
343
|
+
lines.push(ctx.paint(BLOCK_ROLES.rule, '─'.repeat(width)));
|
|
344
|
+
index += 1;
|
|
345
|
+
continue;
|
|
346
|
+
}
|
|
347
|
+
const quote = QUOTE.exec(line);
|
|
348
|
+
if (quote) {
|
|
349
|
+
const wrapped = wrapWords(ctx, wordsOf(ctx, quote[1]), Math.max(1, width - 2));
|
|
350
|
+
for (const piece of wrapped) lines.push(`${ctx.paint(BLOCK_ROLES.quote, '│')} ${piece}`);
|
|
351
|
+
index += 1;
|
|
352
|
+
continue;
|
|
353
|
+
}
|
|
354
|
+
if (isTableStart(raw, index)) {
|
|
355
|
+
const rows = [];
|
|
356
|
+
let cursor = index;
|
|
357
|
+
while (cursor < raw.length && TABLE_ROW.test(raw[cursor])) {
|
|
358
|
+
rows.push(raw[cursor]);
|
|
359
|
+
cursor += 1;
|
|
360
|
+
}
|
|
361
|
+
for (const row of rows) {
|
|
362
|
+
const separator = TABLE_RULE.test(TABLE_ROW.exec(row)[1]);
|
|
363
|
+
if (separator) continue;
|
|
364
|
+
lines.push(...renderTableRow(ctx, row, width));
|
|
365
|
+
}
|
|
366
|
+
index = cursor;
|
|
367
|
+
continue;
|
|
368
|
+
}
|
|
369
|
+
const listItem = LIST_ITEM.exec(line);
|
|
370
|
+
if (listItem) {
|
|
371
|
+
const indent = listItem[1].replace(/\t/g, ' ');
|
|
372
|
+
const rest = listItem[3];
|
|
373
|
+
const checkbox = CHECKBOX.exec(rest);
|
|
374
|
+
const mark = checkbox ? (checkbox[1].trim().length > 0 ? '☑ ' : '☐ ') : '• ';
|
|
375
|
+
const content = checkbox ? checkbox[2] : rest;
|
|
376
|
+
const prefix = `${indent}${mark}`;
|
|
377
|
+
const prefixWidth = ctx.measure(prefix);
|
|
378
|
+
const wrapped = wrapWords(ctx, wordsOf(ctx, content), Math.max(1, width - prefixWidth));
|
|
379
|
+
wrapped.forEach((piece, at) => {
|
|
380
|
+
lines.push(at === 0 ? `${ctx.paint(BLOCK_ROLES.bullet, prefix)}${piece}` : ' '.repeat(prefixWidth) + piece);
|
|
381
|
+
});
|
|
382
|
+
index += 1;
|
|
383
|
+
continue;
|
|
384
|
+
}
|
|
385
|
+
const ordered = ORDERED_ITEM.exec(line);
|
|
386
|
+
if (ordered) {
|
|
387
|
+
const indent = ordered[1].replace(/\t/g, ' ');
|
|
388
|
+
const prefix = `${indent}${ordered[2]}. `;
|
|
389
|
+
const prefixWidth = ctx.measure(prefix);
|
|
390
|
+
const wrapped = wrapWords(ctx, wordsOf(ctx, ordered[3]), Math.max(1, width - prefixWidth));
|
|
391
|
+
wrapped.forEach((piece, at) => {
|
|
392
|
+
lines.push(at === 0 ? `${ctx.paint(BLOCK_ROLES.bullet, prefix)}${piece}` : ' '.repeat(prefixWidth) + piece);
|
|
393
|
+
});
|
|
394
|
+
index += 1;
|
|
395
|
+
continue;
|
|
396
|
+
}
|
|
397
|
+
pushSeparator(lines);
|
|
398
|
+
// Flow mode joins the plain lines up to the next block into ONE paragraph,
|
|
399
|
+
// so a hard-wrapped body reads the way its author wrote it instead of one
|
|
400
|
+
// line plus a separator per source line.
|
|
401
|
+
const paragraph = [line.trim()];
|
|
402
|
+
index += 1;
|
|
403
|
+
while (ctx.flowParagraphs && !startsBlock(raw, index)) {
|
|
404
|
+
paragraph.push(String(raw[index]).trim());
|
|
405
|
+
index += 1;
|
|
406
|
+
}
|
|
407
|
+
lines.push(...wrapWords(ctx, wordsOf(ctx, paragraph.join(' ')), width));
|
|
408
|
+
}
|
|
409
|
+
while (lines.length > 0 && lines[lines.length - 1] === '') lines.pop();
|
|
410
|
+
return lines;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
module.exports = { renderOddMarkdown, stripAnsi, defaultMeasure };
|