@zenera/cli 1.1.19 → 1.1.20
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 +6 -0
- package/dist/banner.d.ts +8 -0
- package/dist/banner.js +2 -0
- package/dist/commands/cache.js +1 -0
- package/dist/commands/check.js +34 -2
- package/dist/commands/index.js +1 -1
- package/dist/commands/init.js +1 -1
- package/dist/commands/meta.js +1 -1
- package/dist/commands/open.js +1 -1
- package/dist/main.js +8 -1
- package/dist/meta.js +19 -7
- package/dist/scaffold.d.ts +37 -0
- package/dist/scaffold.js +67 -3
- package/dist/term.d.ts +2 -0
- package/dist/term.js +25 -0
- package/dist/tui/app.js +237 -65
- package/dist/tui/theme.d.ts +10 -0
- package/dist/tui/theme.js +4 -0
- package/dist/tui/wrap.d.ts +100 -18
- package/dist/tui/wrap.js +500 -27
- package/dist/validate.js +115 -9
- package/package.json +2 -2
- package/templates/editor/.github/copilot-instructions.md +33 -8
- package/templates/editor/.github/skills/zen-cli/references/projects.md +19 -8
- package/templates/editor/.github/skills/zen-memory/SKILL.md +77 -33
- package/templates/editor/.github/skills/zen-memory/scripts/check-instructions.sh +2 -1
- package/templates/editor/.vscode/agents.schema.json +611 -0
- package/templates/editor/.vscode/extensions.json +3 -0
- package/templates/editor/.vscode/settings.json +8 -0
- package/templates/project/agents/fork-instructions.md +151 -0
- package/templates/project/agents/memory-instructions.md +120 -8
package/dist/tui/wrap.js
CHANGED
|
@@ -93,8 +93,8 @@ export const CHROME_ROWS = 7;
|
|
|
93
93
|
*/
|
|
94
94
|
export const THINKING_ROWS = 1;
|
|
95
95
|
/** How much of the frame the work in flight may take. A branch is a box now,
|
|
96
|
-
* not a row, so a fan-out of two costs
|
|
97
|
-
export const ACTIVITY_ROWS =
|
|
96
|
+
* not a row, so a fan-out of two costs twenty-two of these. */
|
|
97
|
+
export const ACTIVITY_ROWS = 24;
|
|
98
98
|
/** The width of the elapsed-time column every call row is drawn behind. */
|
|
99
99
|
export const TIME_COL = 5;
|
|
100
100
|
/**
|
|
@@ -114,8 +114,18 @@ export function budgetOf(rows, activity, thinking) {
|
|
|
114
114
|
const tail = Math.min(Math.max(0, thinking), THINKING_ROWS, Math.max(0, rest - 1));
|
|
115
115
|
return { activity: shown, thinking: tail, live: rest - tail, total };
|
|
116
116
|
}
|
|
117
|
-
/** The rows a branch box may spend on its own calls and
|
|
118
|
-
export const BRANCH_ROWS =
|
|
117
|
+
/** The rows a branch box may spend on its own calls, prose and reasoning. */
|
|
118
|
+
export const BRANCH_ROWS = 9;
|
|
119
|
+
/**
|
|
120
|
+
* The fewest a box is worth drawing at.
|
|
121
|
+
*
|
|
122
|
+
* One row per branch fit more of them on screen and said nothing about any of
|
|
123
|
+
* them: a box holding a single call is a label, and the reason it is a box at
|
|
124
|
+
* all is that what a branch just did and said is why it is doing this. Below
|
|
125
|
+
* six rows there is no *what it just did* — so a fork too wide to give every
|
|
126
|
+
* branch six is cut to the ones that fit and counted, which is `fitActivity`.
|
|
127
|
+
*/
|
|
128
|
+
export const BRANCH_MIN = 6;
|
|
119
129
|
/** Rows a branch box spends on chrome: the title rule and the closing one. */
|
|
120
130
|
export const BOX_CHROME = 2;
|
|
121
131
|
/**
|
|
@@ -123,26 +133,51 @@ export const BOX_CHROME = 2;
|
|
|
123
133
|
* activity region has to divide between them.
|
|
124
134
|
*
|
|
125
135
|
* Every branch gets the same number, because they are the same kind of thing
|
|
126
|
-
* and a fan-out is read across, not down.
|
|
127
|
-
* complete rather than on being detailed: eight branches showing one call each
|
|
128
|
-
* is a picture of the fork, eight rows of one branch is not.
|
|
136
|
+
* and a fan-out is read across, not down.
|
|
129
137
|
*/
|
|
130
138
|
export function branchRows(count, allowance) {
|
|
131
139
|
if (count <= 0) {
|
|
132
140
|
return 0;
|
|
133
141
|
}
|
|
134
142
|
const each = Math.floor(Math.max(0, allowance) / count) - BOX_CHROME;
|
|
135
|
-
return Math.max(
|
|
143
|
+
return Math.max(BRANCH_MIN, Math.min(BRANCH_ROWS, each));
|
|
136
144
|
}
|
|
137
145
|
/**
|
|
138
|
-
* How wide
|
|
139
|
-
*
|
|
146
|
+
* How wide *prose* is drawn. A line spanning a 200-column terminal is
|
|
147
|
+
* measurably harder to read than one that stops, which is why every demo in
|
|
140
148
|
* `examples/sdk/` puts its answer in a box of bounded width — the terminal is the
|
|
141
149
|
* page, not the paragraph.
|
|
150
|
+
*
|
|
151
|
+
* `boxWidth` is what a whole answer is drawn at; this is the floor it starts from.
|
|
142
152
|
*/
|
|
143
153
|
export function answerWidth(columns) {
|
|
144
154
|
return Math.max(24, Math.min(columns - 4, 96));
|
|
145
155
|
}
|
|
156
|
+
/**
|
|
157
|
+
* How wide the box holding `text` has to be.
|
|
158
|
+
*
|
|
159
|
+
* Prose stops at the comfort width, but a fence and a table cannot be
|
|
160
|
+
* reflowed — fold a table row and the columns stop lining up, which is the one
|
|
161
|
+
* thing the table was for — so the box grows to hold the widest of them, as far
|
|
162
|
+
* as the terminal allows and no further.
|
|
163
|
+
*/
|
|
164
|
+
export function boxWidth(text, columns) {
|
|
165
|
+
let rigid = 0;
|
|
166
|
+
for (const block of blocksOf(text)) {
|
|
167
|
+
// A fence is drawn behind a `│ ` gutter; a table row is not.
|
|
168
|
+
const lines = block.kind === 'code'
|
|
169
|
+
? (block.lines ?? []).map((l) => l.length + 6)
|
|
170
|
+
: block.kind === 'table'
|
|
171
|
+
? textOf(block)
|
|
172
|
+
.split('\n')
|
|
173
|
+
.map((l) => l.length + 4)
|
|
174
|
+
: [];
|
|
175
|
+
for (const width of lines) {
|
|
176
|
+
rigid = Math.max(rigid, width);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
return Math.max(answerWidth(columns), Math.min(Math.max(24, columns - 4), rigid));
|
|
180
|
+
}
|
|
146
181
|
// ---------------------------------------------------------------------------
|
|
147
182
|
// Reading a payload
|
|
148
183
|
// ---------------------------------------------------------------------------
|
|
@@ -349,32 +384,470 @@ export function gistOf(text, width) {
|
|
|
349
384
|
const last = heads.at(-1)?.[1];
|
|
350
385
|
return clip(last ?? tail, width);
|
|
351
386
|
}
|
|
387
|
+
// ---------------------------------------------------------------------------
|
|
388
|
+
// Inline
|
|
389
|
+
// ---------------------------------------------------------------------------
|
|
390
|
+
/** What a backslash may turn back into ordinary text. */
|
|
391
|
+
const ESCAPABLE = /[\\`*_~[\]()#+\-.!>|]/;
|
|
392
|
+
/** Emphasis has to hug its own text, or `2 * 3 * 4` reads as arithmetic in italics. */
|
|
393
|
+
const spacey = (ch) => ch === undefined || /\s/.test(ch);
|
|
394
|
+
/** An underscore between word characters is part of the word: `snake_case_name`. */
|
|
395
|
+
const wordy = (ch) => ch !== undefined && /[\p{L}\p{N}]/u.test(ch);
|
|
396
|
+
/** Far past anything a model writes, and the thing that stops a pathological nest. */
|
|
397
|
+
const NESTING = 4;
|
|
352
398
|
/**
|
|
353
|
-
*
|
|
354
|
-
*
|
|
355
|
-
*
|
|
356
|
-
*
|
|
399
|
+
* The inline markup of one block, resolved.
|
|
400
|
+
*
|
|
401
|
+
* Order is the correctness argument. Code is settled first and never entered
|
|
402
|
+
* again, which is why this is a scanner and not a chain of replaces: in
|
|
403
|
+
* "use `[a](b)` syntax" the brackets are prose, and no regex over the whole
|
|
404
|
+
* string can know that. Everything else recurses on the text between its own
|
|
405
|
+
* markers, so `**bold with `code` in it**` keeps both.
|
|
357
406
|
*/
|
|
358
|
-
export function
|
|
407
|
+
export function inlineOf(text, style = {}, depth = 0) {
|
|
359
408
|
const out = [];
|
|
360
|
-
let
|
|
361
|
-
const
|
|
362
|
-
if (
|
|
363
|
-
out.push(
|
|
409
|
+
let buffer = '';
|
|
410
|
+
const keep = () => {
|
|
411
|
+
if (buffer) {
|
|
412
|
+
out.push({ text: buffer, ...style });
|
|
413
|
+
buffer = '';
|
|
414
|
+
}
|
|
415
|
+
};
|
|
416
|
+
let i = 0;
|
|
417
|
+
while (i < text.length) {
|
|
418
|
+
const ch = text[i];
|
|
419
|
+
if (ch === '\\' && ESCAPABLE.test(text[i + 1] ?? '')) {
|
|
420
|
+
buffer += text[i + 1];
|
|
421
|
+
i += 2;
|
|
422
|
+
continue;
|
|
423
|
+
}
|
|
424
|
+
if (ch === '`') {
|
|
425
|
+
const span = codeAt(text, i);
|
|
426
|
+
if (span) {
|
|
427
|
+
keep();
|
|
428
|
+
out.push({ text: span.text, ...style, code: true });
|
|
429
|
+
i = span.end;
|
|
430
|
+
continue;
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
if (depth < NESTING && (ch === '[' || (ch === '!' && text[i + 1] === '['))) {
|
|
434
|
+
const link = linkAt(text, i);
|
|
435
|
+
if (link) {
|
|
436
|
+
keep();
|
|
437
|
+
const inner = { ...style, ...(link.href ? { href: link.href } : {}) };
|
|
438
|
+
out.push(...inlineOf(link.label, inner, depth + 1));
|
|
439
|
+
i = link.end;
|
|
440
|
+
continue;
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
if (depth < NESTING && (ch === '*' || ch === '_' || ch === '~')) {
|
|
444
|
+
const marks = ch === '~' ? 2 : Math.min(runOf(text, i, ch), 2);
|
|
445
|
+
const close = marks === 2 || ch !== '~' ? closingAt(text, i, ch, marks) : undefined;
|
|
446
|
+
if (close !== undefined) {
|
|
447
|
+
keep();
|
|
448
|
+
// Strikethrough has no weight of its own here, so it resolves
|
|
449
|
+
// to its text: the markers were never meant to be read.
|
|
450
|
+
const added = ch === '~' ? {} : marks === 2 ? { bold: true } : { italic: true };
|
|
451
|
+
out.push(...inlineOf(text.slice(i + marks, close), { ...style, ...added }, depth + 1));
|
|
452
|
+
i = close + marks;
|
|
453
|
+
continue;
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
buffer += ch;
|
|
457
|
+
i += 1;
|
|
458
|
+
}
|
|
459
|
+
keep();
|
|
460
|
+
return out;
|
|
461
|
+
}
|
|
462
|
+
/** How many of `ch` run on from `at`. */
|
|
463
|
+
function runOf(text, at, ch) {
|
|
464
|
+
let n = 0;
|
|
465
|
+
while (text[at + n] === ch) {
|
|
466
|
+
n += 1;
|
|
467
|
+
}
|
|
468
|
+
return n;
|
|
469
|
+
}
|
|
470
|
+
/**
|
|
471
|
+
* The code span opening at `at`, or nothing when its backticks never close.
|
|
472
|
+
*
|
|
473
|
+
* A run of n backticks closes on a run of exactly n, which is what lets a code
|
|
474
|
+
* span contain a backtick. One space either side of the content is padding for
|
|
475
|
+
* that case rather than text, so it comes off.
|
|
476
|
+
*/
|
|
477
|
+
function codeAt(text, at) {
|
|
478
|
+
const run = runOf(text, at, '`');
|
|
479
|
+
const fence = '`'.repeat(run);
|
|
480
|
+
let from = at + run;
|
|
481
|
+
for (;;) {
|
|
482
|
+
const found = text.indexOf(fence, from);
|
|
483
|
+
if (found < 0) {
|
|
484
|
+
return undefined;
|
|
364
485
|
}
|
|
486
|
+
if (text[found + run] === '`') {
|
|
487
|
+
from = found + runOf(text, found, '`');
|
|
488
|
+
continue;
|
|
489
|
+
}
|
|
490
|
+
const body = text.slice(at + run, found);
|
|
491
|
+
const padded = body.length > 2 && body.startsWith(' ') && body.endsWith(' ');
|
|
492
|
+
return { text: padded ? body.slice(1, -1) : body, end: found + run };
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
/**
|
|
496
|
+
* Where the emphasis opened at `at` closes, or nothing.
|
|
497
|
+
*
|
|
498
|
+
* Two of CommonMark's flanking rules earn their keep and the rest do not: a
|
|
499
|
+
* marker that opens is followed by something other than a space, one that
|
|
500
|
+
* closes is preceded by something other than a space. Without them `a * b * c`
|
|
501
|
+
* is italic and every second multiplication in an answer goes missing.
|
|
502
|
+
*/
|
|
503
|
+
function closingAt(text, at, ch, marks) {
|
|
504
|
+
if (spacey(text[at + marks]) || (ch === '_' && wordy(text[at - 1]))) {
|
|
505
|
+
return undefined;
|
|
506
|
+
}
|
|
507
|
+
const mark = ch.repeat(marks);
|
|
508
|
+
let from = at + marks + 1;
|
|
509
|
+
while (from < text.length) {
|
|
510
|
+
const found = text.indexOf(mark, from);
|
|
511
|
+
if (found < 0) {
|
|
512
|
+
return undefined;
|
|
513
|
+
}
|
|
514
|
+
if (!spacey(text[found - 1]) && !(ch === '_' && wordy(text[found + marks]))) {
|
|
515
|
+
return found;
|
|
516
|
+
}
|
|
517
|
+
from = found + 1;
|
|
518
|
+
}
|
|
519
|
+
return undefined;
|
|
520
|
+
}
|
|
521
|
+
/** `[label](href)` or `` at `at`, or nothing when it is only a bracket. */
|
|
522
|
+
function linkAt(text, at) {
|
|
523
|
+
const open = text[at] === '!' ? at + 1 : at;
|
|
524
|
+
let depth = 0;
|
|
525
|
+
let close = -1;
|
|
526
|
+
for (let i = open; i < text.length; i++) {
|
|
527
|
+
const ch = text[i];
|
|
528
|
+
if (ch === '\\') {
|
|
529
|
+
i += 1;
|
|
530
|
+
}
|
|
531
|
+
else if (ch === '[') {
|
|
532
|
+
depth += 1;
|
|
533
|
+
}
|
|
534
|
+
else if (ch === ']') {
|
|
535
|
+
depth -= 1;
|
|
536
|
+
if (depth === 0) {
|
|
537
|
+
close = i;
|
|
538
|
+
break;
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
if (close < 0 || text[close + 1] !== '(') {
|
|
543
|
+
return undefined;
|
|
544
|
+
}
|
|
545
|
+
const end = text.indexOf(')', close + 2);
|
|
546
|
+
if (end < 0) {
|
|
547
|
+
return undefined;
|
|
548
|
+
}
|
|
549
|
+
// `[label](url "title")` — the title is for a tooltip nothing here has.
|
|
550
|
+
const target = text
|
|
551
|
+
.slice(close + 2, end)
|
|
552
|
+
.trim()
|
|
553
|
+
.split(/\s+/)[0] ?? '';
|
|
554
|
+
return {
|
|
555
|
+
label: text.slice(open + 1, close),
|
|
556
|
+
href: target.replace(/^<|>$/g, ''),
|
|
557
|
+
end: end + 1,
|
|
558
|
+
};
|
|
559
|
+
}
|
|
560
|
+
// ---------------------------------------------------------------------------
|
|
561
|
+
// Blocks
|
|
562
|
+
// ---------------------------------------------------------------------------
|
|
563
|
+
const FENCE = /^(\s*)(```+|~~~+)\s*(\S*)/;
|
|
564
|
+
const HEADING = /^ {0,3}(#{1,6})\s+(.*?)\s*#*\s*$/;
|
|
565
|
+
const RULE = /^ {0,3}([-*_])(?:\s*\1){2,}\s*$/;
|
|
566
|
+
const QUOTE = /^ {0,3}>\s?(.*)$/;
|
|
567
|
+
const ITEM = /^(\s*)(?:([-*+])|(\d{1,9})[.)])\s+(.*)$/;
|
|
568
|
+
/** A markdown table row. Both pipes, so a sentence with one in it is still prose. */
|
|
569
|
+
const TABLE_ROW = /^\s*\|.*\|\s*$/;
|
|
570
|
+
/** A cell of the rule row under a header: dashes, and where the column is set. */
|
|
571
|
+
const RULE_CELL = /^:?-+:?$/;
|
|
572
|
+
/** What a bullet is drawn as, by how deep it is nested. */
|
|
573
|
+
const BULLETS = ['\u2022', '\u25e6', '\u25aa'];
|
|
574
|
+
/** Splits a row on the pipes that divide it, dropping the two that bound it. */
|
|
575
|
+
function cellsOf(row) {
|
|
576
|
+
const out = [];
|
|
577
|
+
let cell = '';
|
|
578
|
+
for (let i = 0; i < row.length; i += 1) {
|
|
579
|
+
const ch = row[i];
|
|
580
|
+
// An escaped pipe is text, and stays escaped for the inline scan.
|
|
581
|
+
if (ch === '\\' && row[i + 1] === '|') {
|
|
582
|
+
cell += '\\|';
|
|
583
|
+
i += 1;
|
|
584
|
+
}
|
|
585
|
+
else if (ch === '|') {
|
|
586
|
+
out.push(cell);
|
|
587
|
+
cell = '';
|
|
588
|
+
}
|
|
589
|
+
else {
|
|
590
|
+
cell += ch;
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
out.push(cell);
|
|
594
|
+
return out.slice(1, -1).map((c) => c.trim());
|
|
595
|
+
}
|
|
596
|
+
/**
|
|
597
|
+
* The rows a table was written as, read into a grid.
|
|
598
|
+
*
|
|
599
|
+
* The rule row is not kept: what it said about alignment is kept instead, and
|
|
600
|
+
* `gridOf` draws a rule of the width the columns turn out to be. A table
|
|
601
|
+
* without one is still a table — models write them — it just has no header.
|
|
602
|
+
*/
|
|
603
|
+
function gatherTable(rows) {
|
|
604
|
+
const grid = rows.map(cellsOf);
|
|
605
|
+
const rule = grid[1];
|
|
606
|
+
const ruled = rule !== undefined && rule.length > 0 && rule.every((c) => RULE_CELL.test(c));
|
|
607
|
+
const body = ruled ? [grid[0], ...grid.slice(2)] : grid;
|
|
608
|
+
const columns = Math.max(0, ...body.map((row) => row.length));
|
|
609
|
+
return {
|
|
610
|
+
kind: 'table',
|
|
611
|
+
cells: body.map((row) => Array.from({ length: columns }, (_, i) => inlineOf(row[i] ?? ''))),
|
|
612
|
+
...(ruled
|
|
613
|
+
? {
|
|
614
|
+
align: Array.from({ length: columns }, (_, i) => (rule[i] ?? '').endsWith(':')
|
|
615
|
+
? rule[i].startsWith(':')
|
|
616
|
+
? 'center'
|
|
617
|
+
: 'right'
|
|
618
|
+
: 'left'),
|
|
619
|
+
}
|
|
620
|
+
: {}),
|
|
621
|
+
};
|
|
622
|
+
}
|
|
623
|
+
/** Takes back up to `by` columns of indent, and no more than the line has. */
|
|
624
|
+
function dedent(line, by) {
|
|
625
|
+
let n = 0;
|
|
626
|
+
while (n < by && (line[n] === ' ' || line[n] === '\t')) {
|
|
627
|
+
n += 1;
|
|
628
|
+
}
|
|
629
|
+
return line.slice(n);
|
|
630
|
+
}
|
|
631
|
+
/**
|
|
632
|
+
* An answer, as the blocks it is made of.
|
|
633
|
+
*
|
|
634
|
+
* Read top down, one line at a time, because that is the order the text
|
|
635
|
+
* arrives in and a stream must be readable before it is finished. The two
|
|
636
|
+
* verbatim kinds win over everything: a pipe inside a fence is whatever the
|
|
637
|
+
* code says it is, and a `#` inside one is a comment, not a heading.
|
|
638
|
+
*/
|
|
639
|
+
export function blocksOf(text) {
|
|
640
|
+
const out = [];
|
|
641
|
+
let prose;
|
|
642
|
+
let fence;
|
|
643
|
+
let table;
|
|
644
|
+
const closeProse = () => {
|
|
645
|
+
if (prose) {
|
|
646
|
+
out.push({
|
|
647
|
+
kind: prose.kind,
|
|
648
|
+
...(prose.level === undefined ? {} : { level: prose.level }),
|
|
649
|
+
...(prose.marker === undefined ? {} : { marker: prose.marker }),
|
|
650
|
+
spans: inlineOf(prose.lines.join('\n').trim()),
|
|
651
|
+
});
|
|
652
|
+
prose = undefined;
|
|
653
|
+
}
|
|
654
|
+
};
|
|
655
|
+
const closeTable = () => {
|
|
656
|
+
if (table) {
|
|
657
|
+
out.push(gatherTable(table));
|
|
658
|
+
table = undefined;
|
|
659
|
+
}
|
|
660
|
+
};
|
|
661
|
+
const closeFence = () => {
|
|
662
|
+
if (fence) {
|
|
663
|
+
out.push({
|
|
664
|
+
kind: 'code',
|
|
665
|
+
...(fence.title ? { title: fence.title } : {}),
|
|
666
|
+
lines: fence.lines,
|
|
667
|
+
});
|
|
668
|
+
fence = undefined;
|
|
669
|
+
}
|
|
670
|
+
};
|
|
671
|
+
const settle = () => {
|
|
672
|
+
closeProse();
|
|
673
|
+
closeTable();
|
|
365
674
|
};
|
|
366
675
|
for (const raw of text.split('\n')) {
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
676
|
+
if (fence) {
|
|
677
|
+
if (raw.trimStart().startsWith(fence.marker)) {
|
|
678
|
+
closeFence();
|
|
679
|
+
}
|
|
680
|
+
else {
|
|
681
|
+
fence.lines.push(dedent(raw, fence.indent));
|
|
682
|
+
}
|
|
683
|
+
continue;
|
|
684
|
+
}
|
|
685
|
+
const opening = FENCE.exec(raw);
|
|
686
|
+
if (opening) {
|
|
687
|
+
settle();
|
|
688
|
+
fence = {
|
|
689
|
+
marker: opening[2].slice(0, 3),
|
|
690
|
+
indent: opening[1].length,
|
|
691
|
+
...(opening[3] ? { title: opening[3] } : {}),
|
|
692
|
+
lines: [],
|
|
693
|
+
};
|
|
370
694
|
continue;
|
|
371
695
|
}
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
696
|
+
// A blank line ends whatever was being gathered and is not itself a block.
|
|
697
|
+
if (!raw.trim()) {
|
|
698
|
+
settle();
|
|
699
|
+
continue;
|
|
700
|
+
}
|
|
701
|
+
if (TABLE_ROW.test(raw)) {
|
|
702
|
+
closeProse();
|
|
703
|
+
(table ??= []).push(raw.trim());
|
|
704
|
+
continue;
|
|
705
|
+
}
|
|
706
|
+
closeTable();
|
|
707
|
+
const heading = HEADING.exec(raw);
|
|
708
|
+
if (heading) {
|
|
709
|
+
closeProse();
|
|
710
|
+
out.push({
|
|
711
|
+
kind: 'heading',
|
|
712
|
+
level: heading[1].length,
|
|
713
|
+
spans: inlineOf(heading[2]),
|
|
714
|
+
});
|
|
715
|
+
continue;
|
|
716
|
+
}
|
|
717
|
+
// Before the item check, so `* * *` is a rule and not a list of one star.
|
|
718
|
+
if (RULE.test(raw)) {
|
|
719
|
+
closeProse();
|
|
720
|
+
out.push({ kind: 'rule' });
|
|
721
|
+
continue;
|
|
722
|
+
}
|
|
723
|
+
const quote = QUOTE.exec(raw);
|
|
724
|
+
if (quote) {
|
|
725
|
+
if (prose?.kind !== 'quote') {
|
|
726
|
+
closeProse();
|
|
727
|
+
prose = { kind: 'quote', lines: [] };
|
|
728
|
+
}
|
|
729
|
+
prose.lines.push(quote[1]);
|
|
730
|
+
continue;
|
|
731
|
+
}
|
|
732
|
+
const item = ITEM.exec(raw);
|
|
733
|
+
if (item) {
|
|
734
|
+
closeProse();
|
|
735
|
+
const level = Math.min(BULLETS.length - 1, Math.floor(item[1].length / 2));
|
|
736
|
+
const number = item[3];
|
|
737
|
+
prose = {
|
|
738
|
+
kind: 'item',
|
|
739
|
+
level,
|
|
740
|
+
marker: number ? `${number}.` : BULLETS[level],
|
|
741
|
+
lines: [item[4]],
|
|
742
|
+
};
|
|
743
|
+
continue;
|
|
744
|
+
}
|
|
745
|
+
// A line under an item belongs to it; a line under nothing starts a paragraph.
|
|
746
|
+
if (prose) {
|
|
747
|
+
prose.lines.push(raw.trim());
|
|
748
|
+
}
|
|
749
|
+
else {
|
|
750
|
+
prose = { kind: 'paragraph', lines: [raw] };
|
|
751
|
+
}
|
|
376
752
|
}
|
|
377
|
-
|
|
753
|
+
settle();
|
|
754
|
+
closeFence();
|
|
378
755
|
return out;
|
|
379
756
|
}
|
|
757
|
+
// ---------------------------------------------------------------------------
|
|
758
|
+
// Back to text
|
|
759
|
+
// ---------------------------------------------------------------------------
|
|
760
|
+
/** What a run of spans measures, which is the text and never the styling. */
|
|
761
|
+
const lengthOf = (spans) => spans.reduce((n, s) => n + s.text.length, 0);
|
|
762
|
+
/** A cell in its column, set where the rule row asked for it. */
|
|
763
|
+
function padOf(spans, width, align) {
|
|
764
|
+
const slack = Math.max(0, width - lengthOf(spans));
|
|
765
|
+
const lead = align === 'right' ? slack : align === 'center' ? Math.floor(slack / 2) : 0;
|
|
766
|
+
return [
|
|
767
|
+
...(lead > 0 ? [{ text: ' '.repeat(lead) }] : []),
|
|
768
|
+
...spans,
|
|
769
|
+
...(slack > lead ? [{ text: ' '.repeat(slack - lead) }] : []),
|
|
770
|
+
];
|
|
771
|
+
}
|
|
772
|
+
/**
|
|
773
|
+
* A table as its rows, each one a single run of spans.
|
|
774
|
+
*
|
|
775
|
+
* Columns are laid out here and nowhere else, so the drawn table and the plain
|
|
776
|
+
* one are the same table: a cell's width is the length of its text, which is
|
|
777
|
+
* what a `Span` measures whatever styling rides beside it. Nothing a model
|
|
778
|
+
* writes in a cell can move the cell after it.
|
|
779
|
+
*/
|
|
780
|
+
export function gridOf(block) {
|
|
781
|
+
const cells = block.cells ?? [];
|
|
782
|
+
const count = Math.max(0, ...cells.map((row) => row.length));
|
|
783
|
+
if (count === 0) {
|
|
784
|
+
return [];
|
|
785
|
+
}
|
|
786
|
+
const widths = Array.from({ length: count }, (_, i) => Math.max(3, ...cells.map((row) => lengthOf(row[i] ?? []))));
|
|
787
|
+
const rows = cells.map((row) => {
|
|
788
|
+
const out = [];
|
|
789
|
+
widths.forEach((width, i) => {
|
|
790
|
+
out.push({ text: i === 0 ? '| ' : ' | ' });
|
|
791
|
+
out.push(...padOf(row[i] ?? [], width, block.align?.[i] ?? 'left'));
|
|
792
|
+
});
|
|
793
|
+
out.push({ text: ' |' });
|
|
794
|
+
return out;
|
|
795
|
+
});
|
|
796
|
+
if (block.align && rows.length > 0) {
|
|
797
|
+
rows.splice(1, 0, [{ text: `|${widths.map((w) => '-'.repeat(w + 2)).join('|')}|` }]);
|
|
798
|
+
}
|
|
799
|
+
return rows;
|
|
800
|
+
}
|
|
801
|
+
/**
|
|
802
|
+
* A block as the row or rows it reads as, with its markers resolved rather
|
|
803
|
+
* than shown, and nothing styled.
|
|
804
|
+
*
|
|
805
|
+
* This is what a sink that cannot style gets — the pre-answer stream, and the
|
|
806
|
+
* plain box `zen meta` draws — so that the text does not visibly change shape
|
|
807
|
+
* at the moment it settles.
|
|
808
|
+
*/
|
|
809
|
+
export function textOf(block, width = 0) {
|
|
810
|
+
if (block.kind === 'code') {
|
|
811
|
+
// The same chrome the settled answer draws, so a fence that is still
|
|
812
|
+
// arriving is already the shape it will end up as.
|
|
813
|
+
return [
|
|
814
|
+
`\u250c\u2500${block.title ? ` ${block.title}` : ''}`,
|
|
815
|
+
...(block.lines ?? []).map((l) => `\u2502 ${l}`),
|
|
816
|
+
'\u2514\u2500',
|
|
817
|
+
].join('\n');
|
|
818
|
+
}
|
|
819
|
+
if (block.lines) {
|
|
820
|
+
return block.lines.join('\n');
|
|
821
|
+
}
|
|
822
|
+
if (block.kind === 'table') {
|
|
823
|
+
return gridOf(block)
|
|
824
|
+
.map((row) => row.map((s) => s.text).join(''))
|
|
825
|
+
.join('\n');
|
|
826
|
+
}
|
|
827
|
+
if (block.kind === 'rule') {
|
|
828
|
+
return '\u2500'.repeat(Math.max(3, width));
|
|
829
|
+
}
|
|
830
|
+
const body = (block.spans ?? []).map((s) => s.text).join('');
|
|
831
|
+
if (block.kind === 'item') {
|
|
832
|
+
return `${' '.repeat(block.level ?? 0)}${block.marker ?? BULLETS[0]} ${body}`;
|
|
833
|
+
}
|
|
834
|
+
if (block.kind === 'quote') {
|
|
835
|
+
return `\u2502 ${body}`;
|
|
836
|
+
}
|
|
837
|
+
return body;
|
|
838
|
+
}
|
|
839
|
+
/** A whole answer with its markup resolved instead of shown. Still plain text. */
|
|
840
|
+
export function unmarked(text, width = 0) {
|
|
841
|
+
const rows = [];
|
|
842
|
+
let previous;
|
|
843
|
+
for (const block of blocksOf(text)) {
|
|
844
|
+
// Items of one list are one thing; a blank row between them is two lists.
|
|
845
|
+
if (previous && !(block.kind === 'item' && previous.kind === 'item')) {
|
|
846
|
+
rows.push('');
|
|
847
|
+
}
|
|
848
|
+
rows.push(textOf(block, width));
|
|
849
|
+
previous = block;
|
|
850
|
+
}
|
|
851
|
+
return rows.join('\n');
|
|
852
|
+
}
|
|
380
853
|
//# sourceMappingURL=wrap.js.map
|