@f5-sales-demo/pi-tui 23.5.2 → 23.6.8

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/CHANGELOG.md CHANGED
@@ -6,6 +6,9 @@
6
6
 
7
7
  ### Fixed
8
8
 
9
+ - Render Markdown headings and code examples with theme styling and language labels, without
10
+ displaying hash prefixes or fence delimiters. Literal backticks inside code remain unchanged
11
+ ([#4940](https://github.com/f5-sales-demo/xcsh/issues/4940)).
9
12
  - Added conservative runtime OSC 8 capability control, safe Markdown link extraction and targets,
10
13
  ST normalization, and physical-wrap link isolation. Semantically ported from
11
14
  [can1357/oh-my-pi#10196](https://github.com/can1357/oh-my-pi/pull/10196),
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "@f5-sales-demo/pi-tui",
4
- "version": "23.5.2",
4
+ "version": "23.6.8",
5
5
  "description": "Terminal User Interface library with differential rendering for efficient text-based applications",
6
6
  "homepage": "https://github.com/f5-sales-demo/xcsh",
7
7
  "author": "Can Boluk",
@@ -37,8 +37,8 @@
37
37
  "fmt": "biome format --write ."
38
38
  },
39
39
  "dependencies": {
40
- "@f5-sales-demo/pi-natives": "23.5.2",
41
- "@f5-sales-demo/pi-utils": "23.5.2",
40
+ "@f5-sales-demo/pi-natives": "23.6.8",
41
+ "@f5-sales-demo/pi-utils": "23.6.8",
42
42
  "marked": "18.0.11"
43
43
  },
44
44
  "devDependencies": {
@@ -10,6 +10,7 @@ import {
10
10
  Ellipsis,
11
11
  padding,
12
12
  replaceTabs,
13
+ sliceWithWidth,
13
14
  truncateToWidth,
14
15
  visibleWidth,
15
16
  wrapTextWithAnsi,
@@ -304,7 +305,11 @@ interface InlineStyleContext {
304
305
  stylePrefix: string;
305
306
  }
306
307
 
307
- type ListToken = Token & { items: Array<{ tokens?: Token[] }>; ordered: boolean; start?: number };
308
+ type ListToken = Token & {
309
+ items: Array<{ tokens?: Token[]; task?: boolean; checked?: boolean }>;
310
+ ordered: boolean;
311
+ start?: number;
312
+ };
308
313
  type TableCellToken = { tokens?: Token[] };
309
314
  type TableToken = Token & {
310
315
  header: TableCellToken[];
@@ -461,8 +466,8 @@ export class Markdown implements Component {
461
466
  return result;
462
467
  }
463
468
 
464
- // Replace tabs with 3 spaces for consistent rendering
465
- const normalizedText = normalizeOsc8Terminators(replaceTabs(this.#text));
469
+ // Parse source whitespace unchanged; expand tabs only for display.
470
+ const normalizedText = normalizeOsc8Terminators(this.#text);
466
471
 
467
472
  // Parse markdown to HTML-like tokens
468
473
  const tokens = markdownParser.lexer(normalizedText);
@@ -621,15 +626,12 @@ export class Markdown implements Component {
621
626
  switch (token.type) {
622
627
  case "heading": {
623
628
  const headingLevel = token.depth;
624
- const headingPrefix = `${"#".repeat(headingLevel)} `;
625
629
  const headingText = this.#renderInlineTokens(token.tokens || [], styleContext);
626
630
  let styledHeading: string;
627
631
  if (headingLevel === 1) {
628
632
  styledHeading = this.#theme.heading(this.#theme.bold(this.#theme.underline(headingText)));
629
- } else if (headingLevel === 2) {
630
- styledHeading = this.#theme.heading(this.#theme.bold(headingText));
631
633
  } else {
632
- styledHeading = this.#theme.heading(this.#theme.bold(headingPrefix + headingText));
634
+ styledHeading = this.#theme.heading(this.#theme.bold(headingText));
633
635
  }
634
636
  lines.push(styledHeading);
635
637
  if (nextTokenType && nextTokenType !== "space") {
@@ -638,6 +640,7 @@ export class Markdown implements Component {
638
640
  break;
639
641
  }
640
642
 
643
+ case "text":
641
644
  case "paragraph": {
642
645
  const paragraphTokens = token.tokens || [];
643
646
  if (this.#mediaOptions && paragraphTokens.some(child => child.type === "image")) {
@@ -697,21 +700,21 @@ export class Markdown implements Component {
697
700
  }
698
701
  }
699
702
 
700
- const codeIndent = padding(this.#codeBlockIndent);
701
- lines.push(this.#theme.codeBlockBorder(`\`\`\`${token.lang || ""}`));
702
- if (this.#theme.highlightCode) {
703
- const highlightedLines = this.#theme.highlightCode(token.text, token.lang);
704
- for (const hlLine of highlightedLines) {
705
- lines.push(`${codeIndent}${hlLine}`);
706
- }
707
- } else {
708
- // Split code by newlines and style each line
709
- const codeLines = token.text.split("\n");
710
- for (const codeLine of codeLines) {
711
- lines.push(`${codeIndent}${this.#theme.codeBlock(codeLine)}`);
703
+ const indentWidth = Math.min(this.#codeBlockIndent, Math.max(0, width - 1));
704
+ const codeIndent = padding(indentWidth);
705
+ if (token.lang) lines.push(...wrapTextWithAnsi(this.#theme.codeBlockBorder(token.lang), width));
706
+ const codeLines = this.#theme.highlightCode
707
+ ? this.#theme.highlightCode(token.text, token.lang)
708
+ : token.text.split("\n").map((line: string) => this.#theme.codeBlock(line));
709
+ for (const codeLine of codeLines) {
710
+ const displayed = replaceTabs(codeLine);
711
+ const leadingSpaces = /^ */.exec(Bun.stripANSI(displayed))?.[0].length ?? 0;
712
+ const sourceIndentWidth = Math.min(leadingSpaces, Math.max(0, width - indentWidth - 1));
713
+ const body = sliceWithWidth(displayed, sourceIndentWidth, visibleWidth(displayed), true).text;
714
+ for (const visualLine of wrapTextWithAnsi(body, width - indentWidth - sourceIndentWidth)) {
715
+ lines.push(codeIndent + padding(sourceIndentWidth) + visualLine);
712
716
  }
713
717
  }
714
- lines.push(this.#theme.codeBlockBorder("```"));
715
718
  if (nextTokenType && nextTokenType !== "space") {
716
719
  lines.push(""); // Add spacing after code blocks (unless space token follows)
717
720
  }
@@ -719,7 +722,7 @@ export class Markdown implements Component {
719
722
  }
720
723
 
721
724
  case "list": {
722
- const listLines = this.#renderList(token as ListToken, 0, styleContext);
725
+ const listLines = this.#renderList(token as ListToken, width, styleContext);
723
726
  lines.push(...listLines);
724
727
  // Don't add spacing after lists if a space token follows
725
728
  // (the space token will handle it)
@@ -805,7 +808,7 @@ export class Markdown implements Component {
805
808
  }
806
809
  }
807
810
 
808
- return lines;
811
+ return lines.flatMap(line => (TERMINAL.isImageLine(line) ? [line] : wrapTextWithAnsi(replaceTabs(line), width)));
809
812
  }
810
813
 
811
814
  #renderParagraphWithMedia(tokens: Token[], width: number, styleContext?: InlineStyleContext): string[] {
@@ -975,106 +978,22 @@ export class Markdown implements Component {
975
978
  return result;
976
979
  }
977
980
 
978
- /**
979
- * Render a list with proper nesting support
980
- */
981
- #renderList(token: ListToken, depth: number, styleContext?: InlineStyleContext): string[] {
981
+ /** Render children in their own width, then add the item's structural prefix. */
982
+ #renderList(token: ListToken, width: number, styleContext?: InlineStyleContext): string[] {
982
983
  const lines: string[] = [];
983
- const indent = " ".repeat(depth);
984
- // Use the list's start property (defaults to 1 for ordered lists)
985
- const startNumber = token.start ?? 1;
986
-
987
- for (let i = 0; i < token.items.length; i++) {
988
- const item = token.items[i];
989
- const bullet = token.ordered ? `${startNumber + i}. ` : "- ";
990
-
991
- // Process item tokens to handle nested lists
992
- const itemLines = this.#renderListItem(item.tokens || [], depth, styleContext);
993
-
994
- if (itemLines.length > 0) {
995
- // First line - check if it's a nested list
996
- // A nested list will start with indent (spaces) followed by cyan bullet
997
- const firstLine = itemLines[0];
998
- const isNestedList = /^\s+\x1b\[36m[-\d]/.test(firstLine); // starts with spaces + cyan + bullet char
999
-
1000
- if (isNestedList) {
1001
- // This is a nested list, just add it as-is (already has full indent)
1002
- lines.push(firstLine);
1003
- } else {
1004
- // Regular text content - add indent and bullet
1005
- lines.push(indent + this.#theme.listBullet(bullet) + firstLine);
1006
- }
1007
-
1008
- // Rest of the lines
1009
- for (let j = 1; j < itemLines.length; j++) {
1010
- const line = itemLines[j];
1011
- const isNestedListLine = /^\s+\x1b\[36m[-\d]/.test(line); // starts with spaces + cyan + bullet char
1012
-
1013
- if (isNestedListLine) {
1014
- // Nested list line - already has full indent
1015
- lines.push(line);
1016
- } else {
1017
- // Regular content - add parent indent + 2 spaces for continuation
1018
- lines.push(`${indent} ${line}`);
1019
- }
1020
- }
1021
- } else {
1022
- lines.push(indent + this.#theme.listBullet(bullet));
1023
- }
1024
- }
1025
-
1026
- return lines;
1027
- }
1028
-
1029
- /**
1030
- * Render list item tokens, handling nested lists
1031
- * Returns lines WITHOUT the parent indent (renderList will add it)
1032
- */
1033
- #renderListItem(tokens: Token[], parentDepth: number, styleContext?: InlineStyleContext): string[] {
1034
- const lines: string[] = [];
1035
-
1036
- for (const token of tokens) {
1037
- if (token.type === "list") {
1038
- // Nested list - render with one additional indent level
1039
- // These lines will have their own indent, so we just add them as-is
1040
- const nestedLines = this.#renderList(token as ListToken, parentDepth + 1, styleContext);
1041
- lines.push(...nestedLines);
1042
- } else if (token.type === "text") {
1043
- // Text content (may have inline tokens)
1044
- const text =
1045
- token.tokens && token.tokens.length > 0
1046
- ? this.#renderInlineTokens(token.tokens, styleContext)
1047
- : token.text || "";
1048
- lines.push(text);
1049
- } else if (token.type === "paragraph") {
1050
- // Paragraph in list item
1051
- const text = this.#renderInlineTokens(token.tokens || [], styleContext);
1052
- lines.push(text);
1053
- } else if (token.type === "code") {
1054
- // Code block in list item
1055
- const codeIndent = padding(this.#codeBlockIndent);
1056
- lines.push(this.#theme.codeBlockBorder(`\`\`\`${token.lang || ""}`));
1057
- if (this.#theme.highlightCode) {
1058
- const highlightedLines = this.#theme.highlightCode(token.text, token.lang);
1059
- for (const hlLine of highlightedLines) {
1060
- lines.push(`${codeIndent}${hlLine}`);
1061
- }
1062
- } else {
1063
- const codeLines = token.text.split("\n");
1064
- for (const codeLine of codeLines) {
1065
- lines.push(`${codeIndent}${this.#theme.codeBlock(codeLine)}`);
1066
- }
1067
- }
1068
- lines.push(this.#theme.codeBlockBorder("```"));
1069
- } else {
1070
- // Other token types - try to render as inline
1071
- const text = this.#renderInlineTokens([token], styleContext);
1072
- if (text) {
1073
- lines.push(text);
1074
- }
1075
- }
984
+ for (const [index, item] of token.items.entries()) {
985
+ const bullet = token.ordered ? `${(token.start ?? 1) + index}. ` : "- ";
986
+ const task = item.task ? (item.checked ? "[x] " : "[ ] ") : "";
987
+ const prefix = bullet + task;
988
+ const indentWidth = Math.min(visibleWidth(prefix), Math.max(0, width - 1));
989
+ const children = item.tokens ?? [];
990
+ const itemLines = children.flatMap((child, i) =>
991
+ this.#renderToken(child, Math.max(1, width - indentWidth), children[i + 1]?.type, styleContext),
992
+ );
993
+ if (itemLines.length === 0) itemLines.push("");
994
+ lines.push(this.#theme.listBullet(prefix) + itemLines[0]);
995
+ for (const line of itemLines.slice(1)) lines.push(padding(indentWidth) + line);
1076
996
  }
1077
-
1078
997
  return lines;
1079
998
  }
1080
999