@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 +3 -0
- package/package.json +3 -3
- package/src/components/markdown.ts +39 -120
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.
|
|
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.
|
|
41
|
-
"@f5-sales-demo/pi-utils": "23.
|
|
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 & {
|
|
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
|
-
//
|
|
465
|
-
const normalizedText = normalizeOsc8Terminators(
|
|
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(
|
|
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
|
|
701
|
-
|
|
702
|
-
if (this.#theme.
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
const
|
|
710
|
-
|
|
711
|
-
|
|
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,
|
|
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
|
-
|
|
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
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
const
|
|
989
|
-
const
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
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
|
|