diff --git a/packages/core/src/lib/index.ts b/packages/core/src/lib/index.ts index 71496bbcb..00e64f7e9 100644 --- a/packages/core/src/lib/index.ts +++ b/packages/core/src/lib/index.ts @@ -16,3 +16,4 @@ export * from "./tree-sitter" export * from "./data-paths" export * from "./extmarks" export * from "./terminal-palette" +export * from "./table-renderer" diff --git a/packages/core/src/lib/table-renderer.test.ts b/packages/core/src/lib/table-renderer.test.ts new file mode 100644 index 000000000..33fda678d --- /dev/null +++ b/packages/core/src/lib/table-renderer.test.ts @@ -0,0 +1,159 @@ +import { describe, it, expect } from "bun:test" +import { getDisplayWidth, padToWidth, truncateToWidth, detectTableRanges, parseTable } from "./table-renderer" + +describe("getDisplayWidth", () => { + it("returns correct width for ASCII characters", () => { + expect(getDisplayWidth("hello")).toBe(5) + expect(getDisplayWidth("")).toBe(0) + expect(getDisplayWidth(" ")).toBe(1) + }) + + it("returns correct width for CJK characters", () => { + expect(getDisplayWidth("中文")).toBe(4) + expect(getDisplayWidth("日本語")).toBe(6) + expect(getDisplayWidth("한글")).toBe(4) + }) + + it("returns correct width for mixed content", () => { + expect(getDisplayWidth("Hello中文")).toBe(9) + expect(getDisplayWidth("abc日本語xyz")).toBe(12) + }) + + it("returns correct width for hiragana and katakana", () => { + expect(getDisplayWidth("あいう")).toBe(6) + expect(getDisplayWidth("アイウ")).toBe(6) + }) +}) + +describe("padToWidth", () => { + it("pads left-aligned content", () => { + expect(padToWidth("hi", 5, "left")).toBe("hi ") + }) + + it("pads right-aligned content", () => { + expect(padToWidth("hi", 5, "right")).toBe(" hi") + }) + + it("pads center-aligned content", () => { + expect(padToWidth("hi", 6, "center")).toBe(" hi ") + expect(padToWidth("hi", 5, "center")).toBe(" hi ") + }) + + it("returns original string when already at target width", () => { + expect(padToWidth("hello", 5, "left")).toBe("hello") + }) + + it("returns original string when exceeds target width", () => { + expect(padToWidth("hello", 3, "left")).toBe("hello") + }) +}) + +describe("truncateToWidth", () => { + it("returns original string when within limit", () => { + expect(truncateToWidth("hello", 10)).toBe("hello") + }) + + it("truncates and adds ellipsis", () => { + expect(truncateToWidth("hello world", 8)).toBe("hello w…") + }) + + it("handles very short max width", () => { + expect(truncateToWidth("hello", 2)).toBe("he") + }) + + it("handles CJK characters correctly", () => { + const result = truncateToWidth("中文テスト", 6) + expect(getDisplayWidth(result)).toBeLessThanOrEqual(6) + }) +}) + +describe("detectTableRanges", () => { + it("detects a simple table", () => { + const content = `Some text + +| A | B | +|---|---| +| 1 | 2 | + +More text` + const ranges = detectTableRanges(content, []) + expect(ranges.length).toBe(1) + }) + + it("detects multiple tables", () => { + const content = `| A | B | +|---|---| +| 1 | 2 | + +text + +| X | Y | +|---|---| +| 3 | 4 |` + const ranges = detectTableRanges(content, []) + expect(ranges.length).toBe(2) + }) + + it("ignores pipes in code blocks", () => { + const content = `\`\`\` +| not | a | table | +\`\`\`` + const ranges = detectTableRanges(content, []) + expect(ranges.length).toBe(0) + }) + + it("requires delimiter row", () => { + const content = `| A | B | +| 1 | 2 |` + const ranges = detectTableRanges(content, []) + expect(ranges.length).toBe(0) + }) +}) + +describe("parseTable", () => { + it("parses a simple table", () => { + const content = `| Name | Age | +|------|-----| +| Alice | 30 | +| Bob | 25 |` + const table = parseTable(content) + expect(table).not.toBeNull() + expect(table!.rows.length).toBe(4) + expect(table!.columnWidths.length).toBe(2) + expect(table!.columnAligns).toEqual(["left", "left"]) + }) + + it("parses alignment markers", () => { + const content = `| Left | Center | Right | +|:-----|:------:|------:| +| a | b | c |` + const table = parseTable(content) + expect(table).not.toBeNull() + expect(table!.columnAligns).toEqual(["left", "center", "right"]) + }) + + it("calculates column widths correctly", () => { + const content = `| Short | LongerColumn | +|-------|--------------| +| a | b |` + const table = parseTable(content) + expect(table).not.toBeNull() + expect(table!.columnWidths[0]).toBe(5) + expect(table!.columnWidths[1]).toBe(12) + }) + + it("handles CJK content width correctly", () => { + const content = `| Name | City | +|------|------| +| 田中 | 東京 |` + const table = parseTable(content) + expect(table).not.toBeNull() + expect(table!.columnWidths[0]).toBe(4) + expect(table!.columnWidths[1]).toBe(4) + }) + + it("returns null for invalid table", () => { + expect(parseTable("not a table")).toBeNull() + expect(parseTable("|---|")).toBeNull() + }) +}) diff --git a/packages/core/src/lib/table-renderer.ts b/packages/core/src/lib/table-renderer.ts new file mode 100644 index 000000000..379f90c5c --- /dev/null +++ b/packages/core/src/lib/table-renderer.ts @@ -0,0 +1,502 @@ +import type { TextChunk } from "../text-buffer" +import type { SyntaxStyle, StyleDefinition } from "../syntax-style" +import { createTextAttributes } from "../utils" +import type { SimpleHighlight } from "./tree-sitter/types" + +const BOX_CHARS = { + unicode: { + topLeft: "┌", + topRight: "┐", + bottomLeft: "└", + bottomRight: "┘", + horizontal: "─", + vertical: "│", + leftT: "├", + rightT: "┤", + topT: "┬", + bottomT: "┴", + cross: "┼", + headerHorizontal: "─", + headerLeftT: "├", + headerRightT: "┤", + headerCross: "┼", + }, + ascii: { + topLeft: "+", + topRight: "+", + bottomLeft: "+", + bottomRight: "+", + horizontal: "-", + vertical: "|", + leftT: "+", + rightT: "+", + topT: "+", + bottomT: "+", + cross: "+", + headerHorizontal: "-", + headerLeftT: "+", + headerRightT: "+", + headerCross: "+", + }, + compact: { + topLeft: "", + topRight: "", + bottomLeft: "", + bottomRight: "", + horizontal: "", + vertical: " ", + leftT: "", + rightT: "", + topT: "", + bottomT: "", + cross: "", + headerHorizontal: "─", + headerLeftT: "", + headerRightT: "", + headerCross: "─", + }, +} as const + +export type TableStyle = keyof typeof BOX_CHARS +export type TextAlign = "left" | "center" | "right" + +export interface TableRenderOptions { + style?: TableStyle + maxColumnWidth?: number + minColumnWidth?: number + cellPadding?: number +} + +export interface TableCell { + content: string + displayWidth: number + align: TextAlign +} + +export interface TableRow { + cells: TableCell[] + isHeader: boolean + isDelimiter: boolean +} + +export interface ParsedTable { + rows: TableRow[] + columnWidths: number[] + columnAligns: TextAlign[] + startOffset: number + endOffset: number +} + +export interface TableRange { + start: number + end: number +} + +export function getDisplayWidth(str: string): number { + if (typeof Bun !== "undefined" && typeof Bun.stringWidth === "function") { + return Bun.stringWidth(str) + } + + let width = 0 + for (const char of str) { + const code = char.codePointAt(0) || 0 + + // CJK Unified Ideographs: U+4E00–U+9FFF + if (code >= 0x4e00 && code <= 0x9fff) { + width += 2 + continue + } + // CJK Extension A: U+3400–U+4DBF + if (code >= 0x3400 && code <= 0x4dbf) { + width += 2 + continue + } + // CJK Compatibility Ideographs: U+F900–U+FAFF + if (code >= 0xf900 && code <= 0xfaff) { + width += 2 + continue + } + // Fullwidth Forms: U+FF00–U+FF60 + if (code >= 0xff00 && code <= 0xff60) { + width += 2 + continue + } + // CJK Symbols and Punctuation: U+3000–U+303F + if (code >= 0x3000 && code <= 0x303f) { + width += 2 + continue + } + // Hiragana: U+3040–U+309F + if (code >= 0x3040 && code <= 0x309f) { + width += 2 + continue + } + // Katakana: U+30A0–U+30FF + if (code >= 0x30a0 && code <= 0x30ff) { + width += 2 + continue + } + // Hangul Syllables: U+AC00–U+D7AF + if (code >= 0xac00 && code <= 0xd7af) { + width += 2 + continue + } + // Emoji: U+1F300–U+1F9FF + if (code >= 0x1f300 && code <= 0x1f9ff) { + width += 2 + continue + } + // Emoji Emoticons: U+1F600–U+1F64F + if (code >= 0x1f600 && code <= 0x1f64f) { + width += 2 + continue + } + + width += 1 + } + + return width +} + +export function padToWidth(str: string, targetWidth: number, align: TextAlign): string { + const currentWidth = getDisplayWidth(str) + const padding = targetWidth - currentWidth + + if (padding <= 0) return str + + switch (align) { + case "right": + return " ".repeat(padding) + str + case "center": { + const left = Math.floor(padding / 2) + const right = padding - left + return " ".repeat(left) + str + " ".repeat(right) + } + default: + return str + " ".repeat(padding) + } +} + +export function truncateToWidth(str: string, maxWidth: number): string { + if (maxWidth < 3) return str.slice(0, maxWidth) + + const width = getDisplayWidth(str) + if (width <= maxWidth) return str + + let result = "" + let currentWidth = 0 + const ellipsis = "…" + const targetWidth = maxWidth - 1 + + for (const char of str) { + const charWidth = getDisplayWidth(char) + if (currentWidth + charWidth > targetWidth) break + result += char + currentWidth += charWidth + } + + return result + ellipsis +} + +function parseAlignment(delimiterCell: string): TextAlign { + const trimmed = delimiterCell.trim().replace(/\|/g, "") + const hasLeftColon = trimmed.startsWith(":") + const hasRightColon = trimmed.endsWith(":") + + if (hasLeftColon && hasRightColon) return "center" + if (hasRightColon) return "right" + return "left" +} + +export function detectTableRanges(content: string, highlights: SimpleHighlight[]): TableRange[] { + const ranges: TableRange[] = [] + const lines = content.split("\n") + let tableStart: number | null = null + let currentOffset = 0 + + for (let i = 0; i < lines.length; i++) { + const line = lines[i] + const lineStart = currentOffset + const isTableLine = line.includes("|") && !line.trim().startsWith("```") + + if (isTableLine) { + if (tableStart === null) { + tableStart = lineStart + } + } else { + if (tableStart !== null) { + ranges.push({ start: tableStart, end: currentOffset - 1 }) + tableStart = null + } + } + + currentOffset = lineStart + line.length + 1 + } + + if (tableStart !== null) { + ranges.push({ start: tableStart, end: content.length }) + } + + return ranges.filter((range) => { + const tableContent = content.slice(range.start, range.end) + const tableLines = tableContent.split("\n").filter((l) => l.trim()) + + if (tableLines.length < 2) return false + + const hasDelimiter = tableLines.some((line) => /\|[\s-:]+\|/.test(line) || /^[\s-:|]+$/.test(line.trim())) + + return hasDelimiter + }) +} + +export function parseTable(content: string, startOffset: number = 0): ParsedTable | null { + const lines = content.split("\n").filter((l) => l.trim()) + + if (lines.length < 2) return null + + let delimiterIdx = -1 + for (let i = 0; i < lines.length; i++) { + const line = lines[i].trim() + if (/^[\s|:-]+$/.test(line) && line.includes("-")) { + delimiterIdx = i + break + } + } + + if (delimiterIdx === -1 || delimiterIdx === 0) { + return null + } + + const delimiterCells = parseRowCells(lines[delimiterIdx]) + const columnAligns: TextAlign[] = delimiterCells.map(parseAlignment) + + const rows: TableRow[] = [] + const columnWidths: number[] = new Array(columnAligns.length).fill(0) + + for (let i = 0; i < lines.length; i++) { + if (i === delimiterIdx) { + rows.push({ + cells: delimiterCells.map((c, idx) => ({ + content: c.trim(), + displayWidth: getDisplayWidth(c.trim()), + align: columnAligns[idx] || "left", + })), + isHeader: false, + isDelimiter: true, + }) + continue + } + + const cells = parseRowCells(lines[i]) + const isHeader = i < delimiterIdx + + const rowCells: TableCell[] = [] + for (let j = 0; j < columnAligns.length; j++) { + const cellContent = (cells[j] || "").trim() + const displayWidth = getDisplayWidth(cellContent) + + rowCells.push({ + content: cellContent, + displayWidth, + align: columnAligns[j] || "left", + }) + + if (!lines[i].includes("---")) { + columnWidths[j] = Math.max(columnWidths[j], displayWidth) + } + } + + rows.push({ + cells: rowCells, + isHeader, + isDelimiter: false, + }) + } + + return { + rows, + columnWidths, + columnAligns, + startOffset, + endOffset: startOffset + content.length, + } +} + +function parseRowCells(line: string): string[] { + let trimmed = line.trim() + if (trimmed.startsWith("|")) trimmed = trimmed.slice(1) + if (trimmed.endsWith("|")) trimmed = trimmed.slice(0, -1) + + return trimmed.split("|").map((c) => c.trim()) +} + +function createChunk(text: string, style?: StyleDefinition): TextChunk { + return { + __isChunk: true, + text, + fg: style?.fg, + bg: style?.bg, + attributes: style + ? createTextAttributes({ + bold: style.bold, + italic: style.italic, + underline: style.underline, + dim: style.dim, + }) + : 0, + } +} + +export function renderTable( + table: ParsedTable, + syntaxStyle: SyntaxStyle, + options: TableRenderOptions = {}, +): TextChunk[] { + const { style = "unicode", maxColumnWidth = 50, minColumnWidth = 3, cellPadding = 1 } = options + + const chars = BOX_CHARS[style] + const chunks: TextChunk[] = [] + + const borderStyle = syntaxStyle.getStyle("punctuation.special") || syntaxStyle.getStyle("default") + const headerStyle = syntaxStyle.getStyle("markup.heading") || syntaxStyle.getStyle("default") + const cellStyle = syntaxStyle.getStyle("default") + + const columnWidths = table.columnWidths.map( + (w) => Math.min(maxColumnWidth, Math.max(minColumnWidth, w)) + cellPadding * 2, + ) + + const addBorder = (text: string) => { + chunks.push(createChunk(text, borderStyle)) + } + + const addCell = (content: string, width: number, align: TextAlign, isHeader: boolean) => { + const truncated = truncateToWidth(content, width - cellPadding * 2) + const padded = padToWidth(truncated, width - cellPadding * 2, align) + const paddedContent = " ".repeat(cellPadding) + padded + " ".repeat(cellPadding) + chunks.push(createChunk(paddedContent, isHeader ? headerStyle : cellStyle)) + } + + if (style !== "compact") { + addBorder(chars.topLeft) + for (let i = 0; i < columnWidths.length; i++) { + addBorder(chars.horizontal.repeat(columnWidths[i])) + if (i < columnWidths.length - 1) { + addBorder(chars.topT) + } + } + addBorder(chars.topRight) + chunks.push(createChunk("\n")) + } + + for (let rowIdx = 0; rowIdx < table.rows.length; rowIdx++) { + const row = table.rows[rowIdx] + + if (row.isDelimiter) { + if (style !== "compact") { + addBorder(chars.headerLeftT) + for (let i = 0; i < columnWidths.length; i++) { + addBorder(chars.headerHorizontal.repeat(columnWidths[i])) + if (i < columnWidths.length - 1) { + addBorder(chars.headerCross) + } + } + addBorder(chars.headerRightT) + chunks.push(createChunk("\n")) + } else { + for (let i = 0; i < columnWidths.length; i++) { + addBorder(chars.headerHorizontal.repeat(columnWidths[i])) + if (i < columnWidths.length - 1) { + addBorder(" ") + } + } + chunks.push(createChunk("\n")) + } + continue + } + + if (style !== "compact") { + addBorder(chars.vertical) + } + + for (let i = 0; i < columnWidths.length; i++) { + const cell = row.cells[i] || { content: "", displayWidth: 0, align: "left" as TextAlign } + addCell(cell.content, columnWidths[i], cell.align, row.isHeader) + + if (i < columnWidths.length - 1) { + addBorder(chars.vertical) + } + } + + if (style !== "compact") { + addBorder(chars.vertical) + } + chunks.push(createChunk("\n")) + } + + if (style !== "compact") { + addBorder(chars.bottomLeft) + for (let i = 0; i < columnWidths.length; i++) { + addBorder(chars.horizontal.repeat(columnWidths[i])) + if (i < columnWidths.length - 1) { + addBorder(chars.bottomT) + } + } + addBorder(chars.bottomRight) + } + + return chunks +} + +export function processContentWithTables( + content: string, + highlights: SimpleHighlight[], + syntaxStyle: SyntaxStyle, + options: TableRenderOptions = {}, + processNonTable: (content: string, highlights: SimpleHighlight[], syntaxStyle: SyntaxStyle) => TextChunk[], +): TextChunk[] { + const tableRanges = detectTableRanges(content, highlights) + + if (tableRanges.length === 0) { + return processNonTable(content, highlights, syntaxStyle) + } + + const chunks: TextChunk[] = [] + let lastEnd = 0 + + for (const range of tableRanges) { + if (range.start > lastEnd) { + const nonTableContent = content.slice(lastEnd, range.start) + const adjustedHighlights = highlights + .filter((h) => h[0] >= lastEnd && h[1] <= range.start) + .map((h): SimpleHighlight => [h[0] - lastEnd, h[1] - lastEnd, h[2], h[3]]) + + chunks.push(...processNonTable(nonTableContent, adjustedHighlights, syntaxStyle)) + } + + const tableContent = content.slice(range.start, range.end) + const parsedTable = parseTable(tableContent, range.start) + + if (parsedTable) { + chunks.push(...renderTable(parsedTable, syntaxStyle, options)) + } else { + const tableHighlights = highlights + .filter((h) => h[0] >= range.start && h[1] <= range.end) + .map((h): SimpleHighlight => [h[0] - range.start, h[1] - range.start, h[2], h[3]]) + + chunks.push(...processNonTable(tableContent, tableHighlights, syntaxStyle)) + } + + lastEnd = range.end + } + + if (lastEnd < content.length) { + const remainingContent = content.slice(lastEnd) + const adjustedHighlights = highlights + .filter((h) => h[0] >= lastEnd) + .map((h): SimpleHighlight => [h[0] - lastEnd, h[1] - lastEnd, h[2], h[3]]) + + chunks.push(...processNonTable(remainingContent, adjustedHighlights, syntaxStyle)) + } + + return chunks +} diff --git a/packages/core/src/lib/tree-sitter-styled-text.ts b/packages/core/src/lib/tree-sitter-styled-text.ts index 74f798fb6..288f55a26 100644 --- a/packages/core/src/lib/tree-sitter-styled-text.ts +++ b/packages/core/src/lib/tree-sitter-styled-text.ts @@ -5,6 +5,7 @@ import { TreeSitterClient } from "./tree-sitter/client" import type { SimpleHighlight } from "./tree-sitter/types" import { createTextAttributes } from "../utils" import { registerEnvVar, env } from "./env" +import { processContentWithTables, type TableRenderOptions } from "./table-renderer" registerEnvVar({ name: "OTUI_TS_STYLE_WARN", default: false, description: "Enable warnings for missing syntax styles" }) @@ -12,6 +13,10 @@ interface ConcealOptions { enabled: boolean } +export interface TableOptions extends TableRenderOptions { + enabled: boolean +} + interface Boundary { offset: number type: "start" | "end" @@ -35,15 +40,42 @@ function shouldSuppressInInjection(group: string, meta: any): boolean { return group === "markup.raw.block" } +export interface TreeSitterToTextChunksOptions { + conceal?: ConcealOptions + table?: TableOptions +} + export function treeSitterToTextChunks( content: string, highlights: SimpleHighlight[], syntaxStyle: SyntaxStyle, - options?: ConcealOptions, + options?: TreeSitterToTextChunksOptions, +): TextChunk[] { + const tableEnabled = options?.table?.enabled ?? false + + if (tableEnabled) { + return processContentWithTables( + content, + highlights, + syntaxStyle, + options?.table, + (nonTableContent, nonTableHighlights, style) => + treeSitterToTextChunksCore(nonTableContent, nonTableHighlights, style, options?.conceal), + ) + } + + return treeSitterToTextChunksCore(content, highlights, syntaxStyle, options?.conceal) +} + +function treeSitterToTextChunksCore( + content: string, + highlights: SimpleHighlight[], + syntaxStyle: SyntaxStyle, + concealOptions?: ConcealOptions, ): TextChunk[] { const chunks: TextChunk[] = [] const defaultStyle = syntaxStyle.getStyle("default") - const concealEnabled = options?.enabled ?? true + const concealEnabled = concealOptions?.enabled ?? true const injectionContainerRanges: Array<{ start: number; end: number }> = [] const boundaries: Boundary[] = [] @@ -277,6 +309,7 @@ export function treeSitterToTextChunks( export interface TreeSitterToStyledTextOptions { conceal?: ConcealOptions + table?: TableOptions } export async function treeSitterToStyledText( @@ -288,7 +321,10 @@ export async function treeSitterToStyledText( ): Promise { const result = await client.highlightOnce(content, filetype) if (result.highlights && result.highlights.length > 0) { - const chunks = treeSitterToTextChunks(content, result.highlights, syntaxStyle, options?.conceal) + const chunks = treeSitterToTextChunks(content, result.highlights, syntaxStyle, { + conceal: options?.conceal, + table: options?.table, + }) return new StyledText(chunks) } else { const defaultStyle = syntaxStyle.mergeStyles("default") diff --git a/packages/core/src/renderables/Code.ts b/packages/core/src/renderables/Code.ts index bd74d450b..52d0e5354 100644 --- a/packages/core/src/renderables/Code.ts +++ b/packages/core/src/renderables/Code.ts @@ -5,7 +5,7 @@ import { getTreeSitterClient, treeSitterToStyledText, TreeSitterClient } from ". import { TextBufferRenderable, type TextBufferOptions } from "./TextBufferRenderable" import type { OptimizedBuffer } from "../buffer" import type { SimpleHighlight } from "../lib/tree-sitter/types" -import { treeSitterToTextChunks } from "../lib/tree-sitter-styled-text" +import { treeSitterToTextChunks, type TableOptions } from "../lib/tree-sitter-styled-text" export interface CodeOptions extends TextBufferOptions { content?: string @@ -15,6 +15,7 @@ export interface CodeOptions extends TextBufferOptions { conceal?: boolean drawUnstyledText?: boolean streaming?: boolean + table?: TableOptions } export class CodeRenderable extends TextBufferRenderable { @@ -31,6 +32,7 @@ export class CodeRenderable extends TextBufferRenderable { private _streaming: boolean private _hadInitialContent: boolean = false private _lastHighlights: SimpleHighlight[] = [] + private _table?: TableOptions protected _contentDefaultOptions = { content: "", @@ -49,6 +51,7 @@ export class CodeRenderable extends TextBufferRenderable { this._conceal = options.conceal ?? this._contentDefaultOptions.conceal this._drawUnstyledText = options.drawUnstyledText ?? this._contentDefaultOptions.drawUnstyledText this._streaming = options.streaming ?? this._contentDefaultOptions.streaming + this._table = options.table if (this._content.length > 0) { this.textBuffer.setText(this._content) @@ -146,6 +149,17 @@ export class CodeRenderable extends TextBufferRenderable { } } + get table(): TableOptions | undefined { + return this._table + } + + set table(value: TableOptions | undefined) { + if (this._table !== value) { + this._table = value + this._highlightsDirty = true + } + } + get isHighlighting(): boolean { return this._isHighlighting } @@ -202,7 +216,8 @@ export class CodeRenderable extends TextBufferRenderable { } const chunks = treeSitterToTextChunks(content, result.highlights, this._syntaxStyle, { - enabled: this._conceal, + conceal: { enabled: this._conceal }, + table: this._table, }) const styledText = new StyledText(chunks) this.textBuffer.setStyledText(styledText) diff --git a/packages/solid/examples/components/ExampleSelector.tsx b/packages/solid/examples/components/ExampleSelector.tsx index bbaa002ab..f2a37032f 100644 --- a/packages/solid/examples/components/ExampleSelector.tsx +++ b/packages/solid/examples/components/ExampleSelector.tsx @@ -16,8 +16,14 @@ import TabSelectDemo from "./tab-select-demo.tsx" import TextSelectionDemo from "./text-selection-demo.tsx" import TextStyleScene from "./text-style-demo.tsx" import { TextareaDemo } from "./textarea-demo.tsx" +import { TableDemo } from "./table-demo.tsx" const EXAMPLES = [ + { + name: "Table Rendering Demo", + description: "Markdown table rendering with Unicode/ASCII styles and CJK support", + scene: "table-demo", + }, { name: "Diff Viewer Demo", description: "Unified and split diff view with syntax highlighting", @@ -150,6 +156,9 @@ const ExampleSelector = () => { return ( + + + diff --git a/packages/solid/examples/components/table-demo.tsx b/packages/solid/examples/components/table-demo.tsx new file mode 100644 index 000000000..da8c36525 --- /dev/null +++ b/packages/solid/examples/components/table-demo.tsx @@ -0,0 +1,47 @@ +import { SyntaxStyle, RGBA } from "@opentui/core" + +export function TableDemo() { + const syntaxStyle = SyntaxStyle.fromStyles({ + "markup.heading": { fg: RGBA.fromHex("#61afef"), bold: true }, + "punctuation.special": { fg: RGBA.fromHex("#5c6370"), dim: true }, + default: { fg: RGBA.fromHex("#abb2bf") }, + }) + + const markdownWithTable = `# Table Rendering Demo + +Here's a simple table: + +| Name | Age | City | +|:-----|:---:|-----:| +| Alice | 30 | NYC | +| Bob | 25 | LA | +| 田中太郎 | 28 | 東京 | + +And another with more columns: + +| Feature | Status | Notes | +|---------|--------|-------| +| Unicode borders | ✅ | Box-drawing chars | +| CJK support | ✅ | Proper width calc | +| Alignment | ✅ | Left/Center/Right | +` + + return ( + + Table Rendering Test (table.enabled = true) + + + + + Without Table Rendering (table.enabled = false) + + + + + ) +}