@promptctl/rich-js 0.18.1 → 0.19.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.
Files changed (150) hide show
  1. package/README.md +9 -7
  2. package/dist/core/box.d.ts +3 -1
  3. package/dist/core/box.js +6 -13
  4. package/dist/core/cells.d.ts +61 -28
  5. package/dist/core/cells.js +171 -124
  6. package/dist/core/clock.d.ts +48 -0
  7. package/dist/core/clock.js +42 -0
  8. package/dist/core/color.d.ts +98 -18
  9. package/dist/core/color.js +331 -144
  10. package/dist/core/console.d.ts +38 -7
  11. package/dist/core/console.js +159 -90
  12. package/dist/core/easing.d.ts +86 -0
  13. package/dist/core/easing.js +171 -0
  14. package/dist/core/env.d.ts +16 -0
  15. package/dist/core/env.js +16 -0
  16. package/dist/core/export-html.js +1 -4
  17. package/dist/core/export-lines.d.ts +22 -13
  18. package/dist/core/export-lines.js +37 -19
  19. package/dist/core/export-svg.d.ts +49 -0
  20. package/dist/core/export-svg.js +213 -0
  21. package/dist/core/fnv1a.d.ts +10 -0
  22. package/dist/core/fnv1a.js +17 -0
  23. package/dist/core/highlighter.d.ts +2 -2
  24. package/dist/core/highlighter.js +113 -47
  25. package/dist/core/json.js +1 -1
  26. package/dist/core/markup.d.ts +28 -7
  27. package/dist/core/markup.js +235 -312
  28. package/dist/core/measure.d.ts +3 -3
  29. package/dist/core/measure.js +12 -7
  30. package/dist/core/memo.d.ts +19 -0
  31. package/dist/core/memo.js +34 -0
  32. package/dist/core/osc8.d.ts +6 -6
  33. package/dist/core/osc8.js +9 -21
  34. package/dist/core/paint.d.ts +107 -0
  35. package/dist/core/paint.js +150 -0
  36. package/dist/core/place.d.ts +1 -1
  37. package/dist/core/place.js +4 -6
  38. package/dist/core/pretty.d.ts +105 -2
  39. package/dist/core/pretty.js +527 -97
  40. package/dist/core/protocol.d.ts +41 -3
  41. package/dist/core/protocol.js +20 -0
  42. package/dist/core/render.d.ts +33 -30
  43. package/dist/core/render.js +43 -83
  44. package/dist/core/segment.d.ts +6 -5
  45. package/dist/core/segment.js +11 -15
  46. package/dist/core/strip.d.ts +13 -2
  47. package/dist/core/strip.js +44 -23
  48. package/dist/core/style.d.ts +27 -7
  49. package/dist/core/style.js +97 -34
  50. package/dist/core/text.d.ts +43 -16
  51. package/dist/core/text.js +180 -59
  52. package/dist/core/wrap.d.ts +5 -0
  53. package/dist/core/wrap.js +16 -3
  54. package/dist/host/app.d.ts +31 -19
  55. package/dist/host/app.js +90 -60
  56. package/dist/host/index.d.ts +2 -1
  57. package/dist/host/terminal-host.d.ts +3 -2
  58. package/dist/index.d.ts +17 -10
  59. package/dist/index.js +11 -4
  60. package/dist/node/prompt.d.ts +13 -0
  61. package/dist/node/prompt.js +30 -9
  62. package/dist/node/save.d.ts +4 -3
  63. package/dist/node/save.js +6 -3
  64. package/dist/node/terminal-host.d.ts +3 -2
  65. package/dist/node/traceback.d.ts +2 -1
  66. package/dist/node/traceback.js +3 -39
  67. package/dist/renderables/align.js +2 -9
  68. package/dist/renderables/columns.d.ts +45 -29
  69. package/dist/renderables/columns.js +142 -103
  70. package/dist/renderables/constrain.js +6 -12
  71. package/dist/renderables/effect.d.ts +98 -0
  72. package/dist/renderables/effect.js +160 -0
  73. package/dist/renderables/embed.d.ts +87 -30
  74. package/dist/renderables/embed.js +134 -45
  75. package/dist/renderables/group.d.ts +6 -3
  76. package/dist/renderables/group.js +5 -0
  77. package/dist/renderables/layout.d.ts +6 -23
  78. package/dist/renderables/layout.js +24 -113
  79. package/dist/renderables/live.d.ts +40 -16
  80. package/dist/renderables/live.js +96 -83
  81. package/dist/renderables/markdown.d.ts +16 -2
  82. package/dist/renderables/markdown.js +521 -118
  83. package/dist/renderables/padding.d.ts +22 -1
  84. package/dist/renderables/padding.js +30 -24
  85. package/dist/renderables/panel.d.ts +31 -33
  86. package/dist/renderables/panel.js +137 -141
  87. package/dist/renderables/progress.d.ts +104 -16
  88. package/dist/renderables/progress.js +205 -81
  89. package/dist/renderables/progressBar.d.ts +2 -2
  90. package/dist/renderables/progressBar.js +30 -19
  91. package/dist/renderables/prompt.d.ts +37 -15
  92. package/dist/renderables/prompt.js +125 -85
  93. package/dist/renderables/ratio.d.ts +123 -0
  94. package/dist/renderables/ratio.js +213 -0
  95. package/dist/renderables/rule.d.ts +1 -0
  96. package/dist/renderables/rule.js +44 -48
  97. package/dist/renderables/spinner.d.ts +28 -2
  98. package/dist/renderables/spinner.js +52 -19
  99. package/dist/renderables/status.d.ts +13 -7
  100. package/dist/renderables/status.js +15 -29
  101. package/dist/renderables/syntax.d.ts +37 -7
  102. package/dist/renderables/syntax.js +134 -83
  103. package/dist/renderables/table.d.ts +58 -20
  104. package/dist/renderables/table.js +370 -192
  105. package/dist/renderables/traceback.d.ts +5 -4
  106. package/dist/renderables/traceback.js +239 -68
  107. package/dist/renderables/tree.d.ts +10 -2
  108. package/dist/renderables/tree.js +61 -34
  109. package/dist/renderables/viewport.js +2 -4
  110. package/dist/template-bindings/color-funcs.d.ts +31 -9
  111. package/dist/template-bindings/color-funcs.js +40 -14
  112. package/dist/template-bindings/helpers.d.ts +4 -2
  113. package/dist/template-bindings/helpers.js +5 -20
  114. package/dist/template-bindings/index.d.ts +29 -17
  115. package/dist/template-bindings/index.js +36 -20
  116. package/dist/template-bindings/palette-funcs.d.ts +3 -1
  117. package/dist/template-bindings/palette-funcs.js +11 -7
  118. package/dist/template-bindings/style-funcs.js +4 -2
  119. package/dist/themes/buildPalette.d.ts +24 -3
  120. package/dist/themes/buildPalette.js +52 -8
  121. package/dist/themes/colorMath.d.ts +29 -17
  122. package/dist/themes/colorMath.js +74 -48
  123. package/dist/themes/colorRef.d.ts +2 -17
  124. package/dist/themes/colorRef.js +13 -31
  125. package/dist/themes/palette.d.ts +24 -0
  126. package/dist/themes/palette.js +46 -3
  127. package/dist/themes/ramp.d.ts +21 -34
  128. package/dist/themes/ramp.js +23 -42
  129. package/dist/themes/registry.js +39 -28
  130. package/dist/widgets/button.d.ts +2 -6
  131. package/dist/widgets/button.js +9 -40
  132. package/dist/widgets/checkbox.d.ts +2 -5
  133. package/dist/widgets/checkbox.js +9 -31
  134. package/dist/widgets/dropdown.d.ts +2 -5
  135. package/dist/widgets/dropdown.js +14 -48
  136. package/dist/widgets/ink.d.ts +22 -0
  137. package/dist/widgets/ink.js +31 -0
  138. package/dist/widgets/slider.d.ts +2 -5
  139. package/dist/widgets/slider.js +11 -34
  140. package/dist/widgets/text-input.d.ts +19 -10
  141. package/dist/widgets/text-input.js +156 -147
  142. package/dist/widgets/themed-widget.d.ts +20 -0
  143. package/dist/widgets/themed-widget.js +89 -0
  144. package/dist/widgets/toggle.d.ts +2 -5
  145. package/dist/widgets/toggle.js +8 -31
  146. package/dist/widgets/widget-app.d.ts +2 -0
  147. package/dist/widgets/widget-app.js +13 -6
  148. package/dist/widgets/widget-base.d.ts +2 -0
  149. package/dist/widgets/widget-base.js +9 -2
  150. package/package.json +11 -1
@@ -5,8 +5,8 @@
5
5
  * Plugin tags ([LAW:locality-or-seam]): a `MarkupRegistry` lets consumers
6
6
  * register tag handlers without forking the parser. When the parser sees
7
7
  * `[name attrs...]inner[/name]` and `name` is registered, it parses attrs,
8
- * recursively renders the inner markup as a child Renderable, and calls the
9
- * handler — splicing the handler's returned Renderable into the output. The
8
+ * renders the inner markup as the child `RichText`, and calls the handler —
9
+ * splicing the `RichText` it returns into the output. The
10
10
  * built-in style dialect and the plugin dialect are routed by the registry,
11
11
  * which is the single trust boundary between them.
12
12
  *
@@ -14,11 +14,12 @@
14
14
  * (`[bold]a[italic]b[/bold]c[/italic]`) because a style is an annotation and
15
15
  * annotations may overlap freely; a plugin tag is a replacement whose handler
16
16
  * takes one contiguous `inner`, so an overlapping pair has no slice to hand it
17
- * and is rejected with a `MarkupSyntaxError`.
17
+ * and is rejected with a `MarkupSyntaxError`. For the same reason a style tag
18
+ * may not open on one side of a plugin tag and close on the other.
18
19
  */
19
20
  import { cellLen } from "./cells.js";
20
21
  import { Style, StyleSyntaxError } from "./style.js";
21
- import { RichText, Span, stripControlChars } from "./text.js";
22
+ import { RichText, stripControlChars } from "./text.js";
22
23
  import { emojiReplace } from "./emoji.js";
23
24
  // --- Tag ---
24
25
  export class Tag {
@@ -73,8 +74,7 @@ export class MarkupSyntaxError extends MarkupError {
73
74
  /** 1-based column of `offset` within its line, in UTF-16 code units. */
74
75
  column;
75
76
  /**
76
- * The opening tags still open at `offset`, outermost first, as written. For
77
- * overlapping plugin tags, only plugin tags are named.
77
+ * The opening tags still open at `offset`, outermost first, as written.
78
78
  */
79
79
  openTags;
80
80
  constructor(reason, markup, offset, openTags) {
@@ -249,193 +249,28 @@ function tokenize(markup) {
249
249
  function isTag(token) {
250
250
  return token.kind === "tag";
251
251
  }
252
- /**
253
- * The tokens that begin in `[from, to)` of the source. `tokenize` emits them in
254
- * source order, so this is two binary searches and a slice: a string of many
255
- * plugin pairs costs what it holds, not pairs × tokens.
256
- */
257
- function within(tokens, from, to) {
258
- return tokens.slice(firstAtOrAfter(tokens, from), firstAtOrAfter(tokens, to));
259
- }
260
- function firstAtOrAfter(tokens, offset) {
261
- let low = 0;
262
- let high = tokens.length;
263
- while (low < high) {
264
- const mid = (low + high) >>> 1;
265
- if (tokens[mid].start < offset)
266
- low = mid + 1;
267
- else
268
- high = mid;
269
- }
270
- return low;
271
- }
272
- /**
273
- * Parses the built-in style dialect — `[bold red]text[/bold red]` — into a
274
- * `RichText` with styled spans. Module-private on purpose: `renderMarkup` is
275
- * this module's one crossing, and it hands every slice here once its plugin
276
- * pairs are resolved.
277
- *
278
- * [LAW:single-enforcer] Exporting this is what let a caller bind to the inner
279
- * layer, and two of them did — `console.ts` and `prompt.ts` imported it as
280
- * `render as renderMarkup`, so the import line read identically to the
281
- * plugin-aware sites and the difference was invisible at every point of use. A
282
- * tag registered on `globalMarkupRegistry` resolved in a table cell and was
283
- * silently eaten by `console.print`, the path almost every consumer takes
284
- * (rich-markup-pcp). Unexported, that drift is a compile error rather than a
285
- * rule someone has to keep remembering.
286
- *
287
- * [LAW:dataflow-not-control-flow] Rendering *without* plugins stays reachable
288
- * as a value rather than a second name: `renderMarkup(s, { registry: new
289
- * MarkupRegistry() })`. Two exported functions put that variability in the
290
- * function names; an empty registry puts it in the data, where the one
291
- * boundary admits both.
292
- */
293
- function render(parts, origin, baseStyle, options) {
294
- const doEmoji = options?.emoji !== false;
295
- // Build plain text and track spans
296
- let plainText = "";
297
- const spans = [];
298
- const spliced = [];
299
- const openStack = [];
300
- const unparsable = (reason, tag) => new MarkupSyntaxError(reason, origin.source, tag.start, [
301
- ...origin.enclosing,
302
- ...openStack.map((opened) => opened.tag),
303
- ]);
304
- // [LAW:dataflow-not-control-flow] One stack across every part, so a style
305
- // tag opened before a plugin pair is still open after it and its span covers
306
- // the handler's output like any other run of text (rich-markup-cg6).
307
- for (const part of parts) {
308
- if (part.kind === "rendered") {
309
- spliced.push({ at: plainText.length, text: part.text });
310
- plainText += part.text.plain;
311
- continue;
312
- }
313
- for (const token of part.tokens) {
314
- if (!isTag(token)) {
315
- // A `\[` in front of a bracket no tag starts at is unescaped here, per
316
- // text token, as the reference does; a tag's own escape was settled by
317
- // `tokenize` against its backslash run.
318
- const text = token.text.replace(/\\\[/g, "[");
319
- // Text reaches `plainText` as `RichText` will hold it, so every offset
320
- // counted along the way — a span, a splice point — indexes the text it
321
- // lands on. Stripped only by the constructor, a control character
322
- // shifted every style after it one cell right.
323
- plainText += stripControlChars(doEmoji ? emojiReplace(text) : text);
324
- continue;
325
- }
326
- const tag = token;
327
- if (tag.isImplicitClose) {
328
- // [/] — close the most recent open tag
329
- if (openStack.length === 0) {
330
- throw unparsable(`Closing tag ${tag.fullMatch} has no open tag to close`, tag);
331
- }
332
- const opened = openStack.pop();
333
- spans.push(new Span(opened.textStart, plainText.length, openTagStyle(opened)));
334
- }
335
- else if (tag.isClosing) {
336
- // [/style] — find and close matching open tag
337
- const idx = findLastOpen(openStack, tag.styleName);
338
- if (idx === -1) {
339
- throw unparsable(`Closing tag ${tag.fullMatch} doesn't match any open tag`, tag);
340
- }
341
- const opened = openStack[idx];
342
- spans.push(new Span(opened.textStart, plainText.length, openTagStyle(opened)));
343
- openStack.splice(idx, 1);
344
- }
345
- else {
346
- // Opening tag
347
- openStack.push({
348
- tag: tag.fullMatch,
349
- styleName: tag.styleName,
350
- parameters: tag.parameters,
351
- textStart: plainText.length,
352
- });
353
- }
354
- }
355
- }
356
- // Auto-close what is still open, innermost first. The reference pops its
357
- // stack here (`while style_stack: start, tag = style_stack.pop()`), and
358
- // popping is what keeps every span in this list in the order its tag closed
359
- // — the one order the sort below is defined against. Walking the stack
360
- // forwards instead put the outermost unclosed tag in first, and that lone
361
- // disagreement was invisible while nothing sorted: it cancelled the missing
362
- // sort, so `[red][blue]x` was the one nesting this port already got right.
363
- while (openStack.length > 0) {
364
- const opened = openStack.pop();
365
- spans.push(new Span(opened.textStart, plainText.length, openTagStyle(opened)));
366
- }
367
- // [LAW:one-source-of-truth] The paint order is the reference's, expressed
368
- // rather than re-derived: `sorted(spans[::-1], key=attrgetter("start"))` in
369
- // `rich/markup.py`. `RichText` applies spans in list order and each one adds
370
- // over the last, so this list's order *is* which style wins where two
371
- // overlap; sorting by start lays the outer span down first and lets the
372
- // inner one repaint the run it sits in.
373
- //
374
- // The reversal is the whole of the tie-break, not decoration.
375
- // `[red][blue]x[/blue][/red]` yields two spans that both start at 0, and a
376
- // stable sort leaves those in the order they arrived — closing order,
377
- // innermost first — which is the outer colour winning. Reversed first, the
378
- // outer arrives first and the inner repaints it (rich-markup-krk).
379
- spans.reverse();
380
- spans.sort((a, b) => a.start - b.start);
381
- const result = new RichText(plainText);
382
- if (baseStyle)
383
- result.stylize(baseStyle);
384
- // Apply link parameters as link styles
385
- for (const span of spans) {
386
- const style = typeof span.style === "string" ? span.style : span.style.toString();
387
- // Check if this is a link tag (name=url pattern was parsed)
388
- const linkMatch = /^link\s+(.+)$/.exec(style);
389
- if (linkMatch) {
390
- result.stylize(new Style({ link: linkMatch[1] }), span.start, span.end);
391
- }
392
- else {
393
- result.stylize(style, span.start, span.end);
394
- }
395
- }
396
- // A handler's own styles go down last — its base style, then its spans.
397
- // Every style span that reaches into a handler's output encloses all of it —
398
- // no tag of this walk sits inside a plugin pair — so each of these is the
399
- // inner span, and inner repaints outer exactly as the sort above has it for
400
- // tags.
401
- for (const { at, text } of spliced) {
402
- result.stylize(text.style, at, at + text.length);
403
- for (const span of text.spans)
404
- result.stylize(span.style, at + span.start, at + span.end);
405
- }
406
- return result;
407
- }
408
252
  /**
409
253
  * The style string an open tag closes into — `name`, or `name parameters` when
410
254
  * the tag carried an `=`.
411
255
  *
412
- * [LAW:one-source-of-truth] One answer for the four sites that ask it: the three
413
- * ways a tag can close (`[/name]`, `[/]`, and end of input) and the search that
414
- * matches `[/red on blue]` back to `[red on blue]`. It was written out at each,
415
- * which is how the three closing paths could — and did — drift apart on
416
- * something none of the four copies was about.
256
+ * [LAW:one-source-of-truth] One answer for the two sites that ask it: the span
257
+ * a tag closes into, whichever of `[/name]`, `[/]`, a plugin pair's end or the
258
+ * end of input closed it, and the search that matches `[/red on blue]` back to
259
+ * `[red on blue]`. It was once written out at each closing path, which is how
260
+ * those paths could — and did — drift apart on something none of the copies
261
+ * was about.
417
262
  */
418
263
  function openTagStyle(opened) {
419
264
  return opened.parameters !== undefined
420
265
  ? `${opened.styleName} ${opened.parameters}`
421
266
  : opened.styleName;
422
267
  }
423
- /**
424
- * Whether `[/name]` closes `opened`.
425
- *
426
- * [LAW:one-source-of-truth] Two walks ask this: the built-in parser closing a
427
- * style, and the plugin pass keeping the same stack so it knows what `[/]`
428
- * refers to. If they disagreed on which open tag `[/red on blue]` takes off
429
- * the stack, they would disagree about what the next `[/]` closes.
430
- */
268
+ /** Whether `[/name]` names the style tag `opened`. */
431
269
  function closesByName(opened, name) {
432
270
  // Handle "on" in style names for closing: [/red on blue] should match [red on blue]
433
271
  const normalized = name.trim();
434
272
  return openTagStyle(opened) === normalized || opened.styleName === normalized;
435
273
  }
436
- function findLastOpen(stack, name) {
437
- return findLastIndex(stack, (entry) => closesByName(entry, name));
438
- }
439
274
  function findLastIndex(items, match) {
440
275
  for (let i = items.length - 1; i >= 0; i--) {
441
276
  if (match(items[i]))
@@ -575,157 +410,245 @@ function parsePluginAttrs(after) {
575
410
  * Plugin-aware markup render. Always returns a `RichText`. Built-in style
576
411
  * tags become spans; registered plugin tags are resolved by their handler,
577
412
  * whose returned `RichText` stands in the output where its pair stood, under
578
- * any style tag that encloses the pair. The recursion is into the inner slice
579
- * of each plugin pair, and every recursion node returns a `RichText`.
413
+ * any style tag that encloses the pair.
580
414
  */
581
415
  export function renderMarkup(markup, options) {
582
- return renderSlice(tokenize(markup), { source: markup, enclosing: [] }, options);
583
- }
584
- function renderSlice(tokens, origin, options) {
585
416
  const registry = options?.registry ?? globalMarkupRegistry;
586
- // Pair each opening plugin tag with its matching closer up-front, so the
587
- // splice walk can just iterate top-level pairs in source order with no
588
- // nested-state book-keeping.
589
- const { annotated, topLevel: tagPairs } = pairPluginTags(tokens.filter(isTag), registry, origin);
590
- // [LAW:one-type-per-behavior] Each top-level plugin pair is resolved by its
591
- // handler, and what it returns takes the pair's place in one built-in walk
592
- // over the whole slice. Rendering the text between pairs as separate parses
593
- // is what closed every style tag at a plugin boundary (rich-markup-cg6).
594
- const parts = [];
595
- let cursor = 0;
596
- for (const [openIdx, closeIdx] of tagPairs) {
597
- const open = annotated[openIdx];
598
- const close = annotated[closeIdx];
599
- parts.push({ kind: "markup", tokens: within(tokens, cursor, open.start) });
600
- const innerRaw = origin.source.slice(open.end, close.start);
601
- // The base style is painted once, under the whole output, by the top of
602
- // this recursion. Painted into `children` as well, it came back spliced in
603
- // over every tag enclosing the pair.
604
- const innerRichText = renderSlice(within(tokens, open.end, close.start), { ...origin, enclosing: [...origin.enclosing, open.fullMatch] }, { ...options, baseStyle: undefined });
605
- const handler = registry.get(open.pluginName);
606
- parts.push({ kind: "rendered", text: handler({ attrs: open.attrs, children: innerRichText, raw: innerRaw }) });
607
- cursor = close.end;
608
- }
609
- parts.push({ kind: "markup", tokens: within(tokens, cursor, Infinity) });
610
- return render(parts, origin, options?.baseStyle, { emoji: options?.emoji !== false });
417
+ return compose(walk(tokenize(markup), markup, registry, options?.emoji !== false), options?.baseStyle);
611
418
  }
612
- function pairPluginTags(tags, registry, origin) {
613
- // Annotate tags with plugin info, then pair openers with closers. Only
614
- // top-level pairs are returned; inner pairs will be re-discovered by the
615
- // recursive `renderSlice` call on the inner slice.
616
- const annotated = tags.map((t) => annotatePluginTag(t, registry));
617
- // [LAW:one-source-of-truth] `[/]` closes the most recent open tag of *any*
618
- // kind, so this walk keeps the one stack the built-in dialect keeps, style
619
- // tags on it too; an implicit close pairs a plugin tag exactly when it is on
620
- // top. A stack of plugin tags alone cannot say what `[/]` refers to, which is
621
- // why `[shout]one[/]` used to leave its handler unfired (rich-markup-gfr).
622
- //
623
- // A pair's end closes everything opened inside it, because that is where the
624
- // recursion's slice ends and the built-in walk closes what is left open. The
625
- // stack drops them there too, so a later `[/]` means here what it means to
626
- // the walk that renders it.
419
+ /**
420
+ * A string as the text it draws, before any highlighting: read as markup
421
+ * unless `markup` is false, its emoji codes replaced either way, as Rich's
422
+ * `render_str` reads one, its `end` the default line end Rich's leaves it.
423
+ * [LAW:single-enforcer]
424
+ */
425
+ export function readStr(source, markup) {
426
+ return markup ? renderMarkup(source) : new RichText(emojiReplace(source));
427
+ }
428
+ /**
429
+ * The highlighter a string is drawn with under `settings`: its `highlighter`
430
+ * while `highlight` is not false — Rich's `render_str`, which highlights with
431
+ * `console.highlighter` when `highlight` is true or left to the console.
432
+ * [LAW:single-enforcer]
433
+ */
434
+ export function activeHighlighter(settings) {
435
+ return settings.highlight === false ? undefined : settings.highlighter;
436
+ }
437
+ /**
438
+ * A string as the text it draws, the way Rich's `render_str` makes one: read
439
+ * by `readStr` under `markup`, then highlighted by `activeHighlighter`.
440
+ * `print` and every renderable that is handed a string both draw it this way,
441
+ * so a console's settings mean the same thing wherever the string lands.
442
+ */
443
+ export function renderStr(source, settings) {
444
+ const text = readStr(source, settings.markup !== false);
445
+ activeHighlighter(settings)?.highlight(text);
446
+ return text;
447
+ }
448
+ function markOf(out) {
449
+ return { at: out.length, chunks: out.chunks.length, spans: out.spans.length, spliced: out.spliced.length };
450
+ }
451
+ function write(out, text) {
452
+ out.chunks.push(text);
453
+ out.length += text.length;
454
+ }
455
+ /** Everything written since `mark`, taken off `out` and positioned from 0. */
456
+ function takeFrom(out, mark) {
457
+ out.length = mark.at;
458
+ return {
459
+ plain: out.chunks.splice(mark.chunks).join(""),
460
+ spans: out.spans.splice(mark.spans).map(({ start, end, style }) => ({ start: start - mark.at, end: end - mark.at, style })),
461
+ spliced: out.spliced.splice(mark.spliced).map(({ at, text }) => ({ at: at - mark.at, text })),
462
+ };
463
+ }
464
+ function isPlugin(opened) {
465
+ return opened.plugin !== undefined;
466
+ }
467
+ /**
468
+ * Reads a whole markup string's tokens into one frame: every style tag a
469
+ * span, every plugin pair resolved by its handler.
470
+ *
471
+ * [LAW:one-source-of-truth] One stack of open tags and one rule for what each
472
+ * closing tag closes — `namedBy` — for style tags and plugin tags alike. Plugin
473
+ * pairs used to be found by a walk of their own, ahead of the one that built
474
+ * spans, and each kept its own stack; the two had to agree on every closing
475
+ * rule, and review found three places they did not (rich-markup-pyq5). One
476
+ * walk also rejects in string order, since every tag is judged when it is
477
+ * reached: a crossing found by the pairing walk used to hide a stray closer
478
+ * before it.
479
+ *
480
+ * Module-private on purpose, as `compose` is: `renderMarkup` is this module's
481
+ * one crossing. [LAW:single-enforcer] The built-in parser was once exported,
482
+ * and two callers bound to it under the name `renderMarkup`, so a tag on
483
+ * `globalMarkupRegistry` resolved in a table cell and was silently eaten by
484
+ * `console.print` (rich-markup-pcp).
485
+ *
486
+ * A plugin tag's content is everything written after it, because the walk
487
+ * cannot know yet whether the tag will pair. When it pairs, that tail is taken
488
+ * off the output as the handler's `children`, and the handler's output is
489
+ * written in its place. When it never does — an outer pair's end or the end
490
+ * of the string closes it — nothing moves: the tag is a style span over its
491
+ * content, like any tag naming no style, and costs what one does.
492
+ */
493
+ function walk(tokens, source, registry, emoji) {
494
+ const out = { chunks: [], length: 0, spans: [], spliced: [] };
495
+ const start = markOf(out);
627
496
  const stack = [];
628
- const pairs = new Map();
629
- // Each opener's closing tag, whichever tag closed it — its own or a pair's end.
630
- const closedAt = new Map();
631
- // Each tag a pair's end closed, with that pair's opening tag.
632
- const closedByPair = new Map();
633
- // Style tags closed inside a plugin tag they opened outside of, keyed by that
634
- // plugin tag — a crossing only once the plugin tag turns out to pair.
635
- const crossedInto = new Map();
636
- const isOpener = (t) => !t.isClosing && !t.isImplicitClose;
637
- const isPlugin = (idx) => annotated[idx].pluginName !== undefined;
638
- const closes = (close) => (openIdx) => {
639
- const open = annotated[openIdx];
640
- if (close.isImplicitClose)
641
- return true;
642
- return close.pluginName !== undefined
643
- ? open.pluginName === close.pluginName
644
- : open.pluginName === undefined && closesByName(open, close.styleName);
497
+ // Every tag opened so far, in source order, for a rejection to look back on.
498
+ const opened = [];
499
+ const openAt = (offset) => opened.filter((o) => o.tag.start < offset && (o.closed?.at ?? Infinity) >= offset).map((o) => o.tag.fullMatch);
500
+ const unparsable = (reason, offset) => new MarkupSyntaxError(reason, source, offset, openAt(offset));
501
+ const crossing = (style, closer, pair) => unparsable(`Closing tag ${closer.fullMatch} closes ${style.tag.fullMatch} ` +
502
+ `across the boundary of plugin tag ${pair.tag.fullMatch}: ` +
503
+ `a style tag must open and close on the same side of a plugin tag, ` +
504
+ `because a handler replaces the text between a plugin tag and its closer.`, closer.start);
505
+ // [LAW:no-silent-failure] A closing tag that closes nothing reaches for the
506
+ // last tag before it that it names. If a pair's end closed that tag, the
507
+ // closer reaches across the pair, and saying so beats "doesn't match".
508
+ const stray = (closer, plugin) => {
509
+ const named = opened.filter(namedBy(closer, plugin)).at(-1);
510
+ const closed = named?.closed;
511
+ const pair = closed?.byPair;
512
+ if (named === undefined || closed === undefined || pair === undefined) {
513
+ return unparsable(closer.isImplicitClose
514
+ ? `Closing tag ${closer.fullMatch} has no open tag to close`
515
+ : `Closing tag ${closer.fullMatch} doesn't match any open tag`, closer.start);
516
+ }
517
+ if (!pairs(closer, plugin, named))
518
+ return crossing(named, closer, pair);
519
+ // Overlap is unrepresentable rather than unimplemented: a handler receives
520
+ // `children` as one contiguous slice, so a region straddling another pair's
521
+ // closing boundary has nothing to hand it. A style span may cross another
522
+ // style span because it annotates; a plugin pair may not because it
523
+ // replaces. The caret goes where nesting broke: the outer pair's end.
524
+ return unparsable(`Plugin tag [${named.plugin.name}] overlaps [${pair.plugin.name}]: plugin tags must nest, ` +
525
+ `because a handler receives one contiguous slice. ` +
526
+ `Close [/${named.plugin.name}] before [/${pair.plugin.name}].`, closed.at);
645
527
  };
646
- // The tags open at a tag's position, as the built-in walk would report them.
647
- const openAt = (caret) => annotated.flatMap((t, k) => (k < caret && isOpener(t) && (closedAt.get(k) ?? Infinity) >= caret ? [t.fullMatch] : []));
648
- const crossing = (styleOpen, close, pairOpen) => new MarkupSyntaxError(`Closing tag ${annotated[close].fullMatch} closes ${annotated[styleOpen].fullMatch} ` +
649
- `across the boundary of plugin tag ${annotated[pairOpen].fullMatch}: ` +
650
- `a style tag must open and close on the same side of a plugin pair, ` +
651
- `because the handler replaces the text inside it.`, origin.source, annotated[close].start, [...origin.enclosing, ...openAt(close)]);
652
- for (let i = 0; i < annotated.length; i++) {
653
- const t = annotated[i];
654
- if (isOpener(t)) {
655
- stack.push(i);
528
+ // Closes a tag as a style span over what was written since it opened. A
529
+ // plugin tag closed this way never paired.
530
+ const settle = (tag, at, byPair) => {
531
+ tag.closed = at === undefined ? undefined : { at, byPair };
532
+ out.spans.push({ start: tag.mark.at, end: out.length, style: openTagStyle(tag.tag) });
533
+ };
534
+ for (const token of tokens) {
535
+ if (!isTag(token)) {
536
+ // A `\[` in front of a bracket no tag starts at is unescaped here, per
537
+ // text token, as the reference does; a tag's own escape was settled by
538
+ // `tokenize` against its backslash run.
539
+ const text = token.text.replace(/\\\[/g, "[");
540
+ // Text reaches the output as `RichText` will hold it, so every offset
541
+ // counted along the way — a span, a splice point — indexes the text it
542
+ // lands on. Stripped only by the constructor, a control character
543
+ // shifted every style after it one cell right.
544
+ write(out, stripControlChars(emoji ? emojiReplace(text) : text));
656
545
  continue;
657
546
  }
658
- const j = findLastIndex(stack, closes(t));
659
- if (j === -1) {
660
- // [LAW:no-silent-failure] A closer that matches nothing reaches for the
661
- // last tag before it that it could name, `[/]` and `[/name]` alike. If a
662
- // pair's end closed that tag, the closer reaches across the pair, and
663
- // saying so beats the built-in parser's "doesn't match any open tag";
664
- // otherwise the built-in parser rejects it, with the location it reports.
665
- let target = i - 1;
666
- while (target >= 0 && !(isOpener(annotated[target]) && closes(t)(target)))
667
- target--;
668
- const pairOpen = closedByPair.get(target);
669
- if (pairOpen === undefined)
670
- continue;
671
- if (t.pluginName === undefined)
672
- throw crossing(target, i, pairOpen);
673
- // Overlap is unrepresentable rather than unimplemented: a handler receives
674
- // `children` as one contiguous slice, so a region straddling another
675
- // pair's closing boundary has nothing to hand it. A style span may cross
676
- // another style span because it annotates; a plugin pair may not because
677
- // it replaces. The caret goes where nesting broke: the outer pair's end.
678
- const inner = annotated[target];
679
- const outer = annotated[pairOpen];
680
- const caret = closedAt.get(pairOpen);
681
- throw new MarkupSyntaxError(`Plugin tag [${inner.pluginName}] overlaps [${outer.pluginName}]: plugin tags must nest, ` +
682
- `because a handler receives one contiguous slice. ` +
683
- `Close [/${inner.pluginName}] before [/${outer.pluginName}].`, origin.source, annotated[caret].start, [...origin.enclosing, ...openAt(caret)]);
547
+ const plugin = pluginOf(token, registry);
548
+ if (!token.isClosing && !token.isImplicitClose) {
549
+ const entry = { tag: token, mark: markOf(out), plugin, closed: undefined };
550
+ stack.push(entry);
551
+ opened.push(entry);
552
+ continue;
684
553
  }
685
- const openIdx = stack[j];
686
- if (!isPlugin(openIdx)) {
687
- for (const k of stack.slice(j + 1)) {
688
- if (isPlugin(k) && !crossedInto.has(k))
689
- crossedInto.set(k, { styleOpen: openIdx, close: i });
690
- }
554
+ const j = findLastIndex(stack, namedBy(token, plugin));
555
+ if (j === -1)
556
+ throw stray(token, plugin);
557
+ const target = stack[j];
558
+ if (!pairs(token, plugin, target)) {
559
+ // A closer that ends no pair may pass over style tags, which overlap
560
+ // freely, but not over a plugin tag: the handler replaces what is
561
+ // inside it.
562
+ const boundary = findLastIndex(stack, isPlugin);
563
+ if (boundary > j)
564
+ throw crossing(target, token, stack[boundary]);
691
565
  stack.splice(j, 1);
692
- closedAt.set(openIdx, i);
566
+ settle(target, token.start, undefined);
693
567
  continue;
694
568
  }
695
- const crossed = crossedInto.get(openIdx);
696
- if (crossed !== undefined)
697
- throw crossing(crossed.styleOpen, crossed.close, openIdx);
698
- for (const k of stack.splice(j)) {
699
- closedAt.set(k, i);
700
- if (k !== openIdx)
701
- closedByPair.set(k, openIdx);
569
+ // A pair's end closes everything opened inside it, innermost first, so the
570
+ // handler's `children` hold every span its content carries.
571
+ const inside = stack.splice(j).slice(1);
572
+ for (const tag of inside.reverse())
573
+ settle(tag, token.start, target);
574
+ target.closed = { at: token.start, byPair: undefined };
575
+ const { handler, attrs } = target.plugin;
576
+ const children = compose(takeFrom(out, target.mark));
577
+ const text = handler({ attrs, children, raw: source.slice(target.tag.end, token.start) });
578
+ out.spliced.push({ at: out.length, text });
579
+ write(out, text.plain);
580
+ }
581
+ // Close what is still open, innermost first. The reference pops its stack
582
+ // here (`while style_stack: start, tag = style_stack.pop()`), and popping is
583
+ // what keeps every span in the order its tag closed — the one order
584
+ // `compose`'s sort is defined against.
585
+ while (stack.length > 0)
586
+ settle(stack.pop(), undefined, undefined);
587
+ return takeFrom(out, start);
588
+ }
589
+ /**
590
+ * Which open tags a closing tag names: `[/]` every one, and `[/name]` a tag
591
+ * whose style it names or a plugin tag of the plugin it names.
592
+ */
593
+ function namedBy(closer, plugin) {
594
+ if (closer.isImplicitClose)
595
+ return () => true;
596
+ return (opened) => closesByName(opened.tag, closer.styleName) || pairs(closer, plugin, opened);
597
+ }
598
+ /** Whether `closer` ends a plugin pair at `opened`: `[/]`, or its plugin's own name. */
599
+ function pairs(closer, plugin, opened) {
600
+ return opened.plugin !== undefined && (closer.isImplicitClose || opened.plugin.name === plugin?.name);
601
+ }
602
+ /** A frame as the `RichText` it draws, `baseStyle` beneath all of it. */
603
+ function compose(frame, baseStyle) {
604
+ // [LAW:one-source-of-truth] The paint order is the reference's, expressed
605
+ // rather than re-derived: `sorted(spans[::-1], key=attrgetter("start"))` in
606
+ // `rich/markup.py`. `RichText` applies spans in list order and each one adds
607
+ // over the last, so this list's order *is* which style wins where two
608
+ // overlap; sorting by start lays the outer span down first and lets the
609
+ // inner one repaint the run it sits in.
610
+ //
611
+ // The reversal is the whole of the tie-break, not decoration.
612
+ // `[red][blue]x[/blue][/red]` yields two spans that both start at 0, and a
613
+ // stable sort leaves those in the order they arrived — closing order,
614
+ // innermost first — which is the outer colour winning. Reversed first, the
615
+ // outer arrives first and the inner repaints it (rich-markup-krk).
616
+ const spans = [...frame.spans].reverse().sort((a, b) => a.start - b.start);
617
+ const result = new RichText(frame.plain);
618
+ if (baseStyle)
619
+ result.stylize(baseStyle);
620
+ // Apply link parameters as link styles
621
+ for (const { start, end, style } of spans) {
622
+ // Check if this is a link tag (name=url pattern was parsed)
623
+ const linkMatch = /^link\s+(.+)$/.exec(style);
624
+ if (linkMatch) {
625
+ result.stylize(new Style({ link: linkMatch[1] }), start, end);
626
+ }
627
+ else {
628
+ result.stylize(style, start, end);
702
629
  }
703
- pairs.set(openIdx, i);
704
630
  }
705
- // Pairs nest, so one is top-level exactly when it opens after the last
706
- // top-level pair closed. Decided after the walk and not at each close: a
707
- // plugin tag still on the stack there may never pair, and then encloses
708
- // nothing.
709
- const topLevel = new Map();
710
- let outerClose = -1;
711
- for (const openIdx of [...pairs.keys()].sort((a, b) => a - b)) {
712
- if (openIdx < outerClose)
713
- continue;
714
- outerClose = pairs.get(openIdx);
715
- topLevel.set(openIdx, outerClose);
631
+ // A handler's own styles go down last — its base style, then its spans.
632
+ // Every style span that reaches into a handler's output encloses all of it —
633
+ // no style tag may cross a plugin pair's boundary — so each of these is the
634
+ // inner span, and inner repaints outer exactly as the sort above has it for
635
+ // tags.
636
+ for (const { at, text } of frame.spliced) {
637
+ result.stylize(text.style, at, at + text.length);
638
+ for (const span of text.spans)
639
+ result.stylize(span.style, at + span.start, at + span.end);
716
640
  }
717
- return { annotated, topLevel };
641
+ return result;
718
642
  }
719
- function annotatePluginTag(tag, registry) {
720
- // The legacy parser splits at the first `=`, so tag.styleName for
643
+ function pluginOf(tag, registry) {
644
+ // The tokenizer splits at the first `=`, so tag.styleName for
721
645
  // `[click verb=foo]` is "click verb". Re-read the tag text for a name that
722
646
  // actually addresses a plugin.
723
647
  const inner = tag.fullMatch.slice(1, -1).replace(/^\//, "");
724
- const head = PLUGIN_TAG_HEAD.exec(inner);
725
- const name = head?.[1];
726
- if (name === undefined || !registry.has(name))
727
- return tag;
728
- const after = inner.slice(name.length);
729
- const attrs = tag.isClosing ? {} : parsePluginAttrs(after);
730
- return { ...tag, pluginName: name, attrs };
648
+ const name = PLUGIN_TAG_HEAD.exec(inner)?.[1];
649
+ const handler = name === undefined ? undefined : registry.get(name);
650
+ if (name === undefined || handler === undefined)
651
+ return undefined;
652
+ const attrs = tag.isClosing ? {} : parsePluginAttrs(inner.slice(name.length));
653
+ return { name, handler, attrs };
731
654
  }
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Measurement — min/max cell width calculation for renderables.
3
3
  */
4
- import { type Measurable, type RenderOptions } from "./protocol.js";
4
+ import { type Measurable, type Renderable, type RenderOptions } from "./protocol.js";
5
5
  export declare class Measurement {
6
6
  readonly minimum: number;
7
7
  readonly maximum: number;
@@ -22,6 +22,6 @@ export declare class Measurement {
22
22
  withMaximum(width: number): Measurement;
23
23
  withMinimum(width: number): Measurement;
24
24
  clamp(minWidth: number, maxWidth: number): Measurement;
25
- static get(options: RenderOptions, measurable: Measurable): Measurement;
25
+ static get(options: RenderOptions, renderable: Renderable | Measurable): Measurement;
26
26
  }
27
- export declare function measureRenderables(options: RenderOptions, measurables: Measurable[]): Measurement;
27
+ export declare function measureRenderables(options: RenderOptions, renderables: readonly (Renderable | Measurable)[]): Measurement;