@tooee/renderers 0.1.5 → 0.1.7

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.
@@ -0,0 +1,7 @@
1
+ import { RowDocumentRenderable } from "./RowDocumentRenderable.js";
2
+ declare module "@opentui/react" {
3
+ interface OpenTUIComponents {
4
+ "row-document": typeof RowDocumentRenderable;
5
+ }
6
+ }
7
+ //# sourceMappingURL=row-document.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"row-document.d.ts","sourceRoot":"","sources":["../src/row-document.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,qBAAqB,EAAE,MAAM,4BAA4B,CAAA;AAIlE,OAAO,QAAQ,gBAAgB,CAAC;IAC9B,UAAU,iBAAiB;QACzB,cAAc,EAAE,OAAO,qBAAqB,CAAA;KAC7C;CACF"}
@@ -0,0 +1,4 @@
1
+ import { extend } from "@opentui/react";
2
+ import { RowDocumentRenderable } from "./RowDocumentRenderable.js";
3
+ extend({ "row-document": RowDocumentRenderable });
4
+ //# sourceMappingURL=row-document.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"row-document.js","sourceRoot":"","sources":["../src/row-document.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,gBAAgB,CAAA;AACvC,OAAO,EAAE,qBAAqB,EAAE,MAAM,4BAA4B,CAAA;AAElE,MAAM,CAAC,EAAE,cAAc,EAAE,qBAAqB,EAAE,CAAC,CAAA"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tooee/renderers",
3
- "version": "0.1.5",
3
+ "version": "0.1.7",
4
4
  "description": "Content renderers (markdown, code, image, table) for Tooee",
5
5
  "license": "MIT",
6
6
  "author": "Gareth Andrew",
@@ -37,7 +37,7 @@
37
37
  "typecheck": "tsc --noEmit"
38
38
  },
39
39
  "dependencies": {
40
- "@tooee/themes": "0.1.5",
40
+ "@tooee/themes": "0.1.7",
41
41
  "marked": "^16.3.0"
42
42
  },
43
43
  "devDependencies": {
package/src/CodeView.tsx CHANGED
@@ -1,6 +1,7 @@
1
- import { useEffect, useRef } from "react"
2
- import type { LineNumberRenderable } from "@opentui/core"
1
+ import { useEffect, useRef, type RefObject } from "react"
3
2
  import { useTheme } from "@tooee/themes"
3
+ import type { RowDocumentRenderable, RowDocumentPalette, RowDocumentDecorations } from "./RowDocumentRenderable.js"
4
+ import "./row-document.js"
4
5
 
5
6
  interface CodeViewProps {
6
7
  content: string
@@ -12,6 +13,7 @@ interface CodeViewProps {
12
13
  matchingLines?: Set<number>
13
14
  currentMatchLine?: number
14
15
  toggledLines?: Set<number>
16
+ docRef?: RefObject<RowDocumentRenderable | null>
15
17
  }
16
18
 
17
19
  export function CodeView({
@@ -24,94 +26,50 @@ export function CodeView({
24
26
  matchingLines,
25
27
  currentMatchLine,
26
28
  toggledLines,
29
+ docRef,
27
30
  }: CodeViewProps) {
28
31
  const { syntax, theme } = useTheme()
29
- const lineNumRef = useRef<LineNumberRenderable>(null)
32
+ const internalRef = useRef<RowDocumentRenderable>(null)
33
+ const effectiveRef = docRef ?? internalRef
30
34
 
31
- useEffect(() => {
32
- const ref = lineNumRef.current
33
- if (!ref) return
34
-
35
- ref.clearAllLineColors()
36
- ref.clearAllLineSigns()
35
+ const palette: RowDocumentPalette = {
36
+ gutterFg: theme.textMuted,
37
+ gutterBg: theme.backgroundElement,
38
+ cursorBg: theme.cursorLine,
39
+ selectionBg: theme.selection,
40
+ matchBg: theme.warning,
41
+ currentMatchBg: theme.primary,
42
+ toggledBg: theme.backgroundPanel,
43
+ cursorSignFg: theme.primary,
44
+ matchSignFg: theme.warning,
45
+ currentMatchSignFg: theme.primary,
46
+ }
37
47
 
38
- // Search matches
39
- if (matchingLines) {
40
- for (const line of matchingLines) {
41
- ref.setLineSign(line, {
42
- after: "●",
43
- afterColor: line === currentMatchLine ? theme.primary : theme.warning,
44
- })
45
- }
46
- }
47
-
48
- // Selection range
49
- if (selectionStart != null && selectionEnd != null) {
50
- for (let i = selectionStart; i <= selectionEnd; i++) {
51
- ref.setLineColor(i, { content: theme.selection, gutter: theme.selection })
52
- }
53
- }
54
-
55
- if (toggledLines) {
56
- for (const line of toggledLines) {
57
- const isSelected =
58
- selectionStart != null && selectionEnd != null && line >= selectionStart && line <= selectionEnd
59
- if (line === cursor || isSelected) continue
60
- ref.setLineColor(line, {
61
- content: theme.backgroundPanel,
62
- gutter: theme.backgroundPanel,
63
- })
64
- }
65
- }
66
-
67
- // Cursor line (overwrites selection color on cursor line)
68
- if (cursor != null) {
69
- ref.setLineColor(cursor, { content: theme.cursorLine, gutter: theme.cursorLine })
70
- ref.setLineSign(cursor, {
71
- before: "▸",
72
- beforeColor: theme.primary,
73
- // Preserve search match sign if present
74
- ...(matchingLines?.has(cursor)
75
- ? {
76
- after: "●",
77
- afterColor: cursor === currentMatchLine ? theme.primary : theme.warning,
78
- }
79
- : {}),
80
- })
48
+ useEffect(() => {
49
+ const decorations: RowDocumentDecorations = {
50
+ cursorRow: cursor,
51
+ selection: selectionStart != null && selectionEnd != null
52
+ ? { start: selectionStart, end: selectionEnd }
53
+ : null,
54
+ matchingRows: matchingLines,
55
+ currentMatchRow: currentMatchLine,
56
+ toggledRows: toggledLines,
81
57
  }
82
- }, [
83
- content,
84
- cursor,
85
- selectionStart,
86
- selectionEnd,
87
- matchingLines,
88
- currentMatchLine,
89
- toggledLines,
90
- theme,
91
- ])
58
+ effectiveRef.current?.setDecorations(decorations)
59
+ }, [cursor, selectionStart, selectionEnd, matchingLines, currentMatchLine, toggledLines])
92
60
 
93
61
  const codeElement = <code content={content} filetype={language} syntaxStyle={syntax} />
94
62
 
95
63
  return (
96
- <box
97
- style={{
98
- flexDirection: "column",
99
- }}
64
+ <row-document
65
+ ref={effectiveRef}
66
+ key={theme.textMuted + theme.backgroundElement}
67
+ showLineNumbers={showLineNumbers}
68
+ palette={palette}
69
+ signColumnWidth={1}
70
+ style={{ flexGrow: 1 }}
100
71
  >
101
- {showLineNumbers ? (
102
- <line-number
103
- ref={lineNumRef}
104
- key={theme.textMuted + theme.backgroundElement}
105
- fg={theme.textMuted}
106
- bg={theme.backgroundElement}
107
- paddingRight={1}
108
- showLineNumbers
109
- >
110
- {codeElement}
111
- </line-number>
112
- ) : (
113
- codeElement
114
- )}
115
- </box>
72
+ {codeElement}
73
+ </row-document>
116
74
  )
117
75
  }
@@ -1,83 +1,79 @@
1
1
  import { marked, type Token, type Tokens } from "marked"
2
- import type { ReactNode } from "react"
2
+ import { useEffect, useRef, type ReactNode, type RefObject } from "react"
3
+ import { useTerminalDimensions } from "@opentui/react"
3
4
  import { useTheme, type ResolvedTheme } from "@tooee/themes"
4
5
  import type { SyntaxStyle } from "@opentui/core"
5
- import { Table } from "./Table.js"
6
+ import { computeColumnWidths, buildBorderLine, buildDataLine, isNumeric } from "./Table.js"
7
+ import type { RowDocumentRenderable, RowDocumentPalette, RowDocumentDecorations } from "./RowDocumentRenderable.js"
8
+ import "./row-document.js"
6
9
 
7
10
  interface MarkdownViewProps {
8
11
  content: string
12
+ showLineNumbers?: boolean
9
13
  activeBlock?: number
10
14
  selectedBlocks?: { start: number; end: number }
11
15
  matchingBlocks?: Set<number>
12
16
  currentMatchBlock?: number
13
17
  toggledBlocks?: Set<number>
18
+ docRef?: RefObject<RowDocumentRenderable | null>
14
19
  }
15
20
 
16
21
  export function MarkdownView({
17
22
  content,
23
+ showLineNumbers = true,
18
24
  activeBlock,
19
25
  selectedBlocks,
20
26
  matchingBlocks,
21
27
  currentMatchBlock,
22
28
  toggledBlocks,
29
+ docRef,
23
30
  }: MarkdownViewProps) {
24
31
  const { theme, syntax } = useTheme()
32
+ const internalRef = useRef<RowDocumentRenderable>(null)
33
+ const effectiveRef = docRef ?? internalRef
25
34
  const tokens = marked.lexer(content)
26
35
  const blocks = tokens.filter((t) => t.type !== "space")
27
36
 
28
- return (
29
- <box style={{ flexDirection: "column" }}>
30
- {blocks.map((token, index) => {
31
- const { accent: accentColor, background: bgColor } = getBlockStyle(
32
- index,
33
- theme,
34
- activeBlock,
35
- selectedBlocks,
36
- matchingBlocks,
37
- currentMatchBlock,
38
- toggledBlocks,
39
- )
40
- const blockContent = (
41
- <TokenRenderer key={index} token={token} theme={theme} syntax={syntax} />
42
- )
37
+ const palette: RowDocumentPalette = {
38
+ gutterFg: theme.textMuted,
39
+ gutterBg: theme.backgroundElement,
40
+ cursorBg: theme.cursorLine,
41
+ selectionBg: theme.selection,
42
+ matchBg: theme.warning,
43
+ currentMatchBg: theme.primary,
44
+ toggledBg: theme.backgroundPanel,
45
+ cursorSignFg: theme.primary,
46
+ matchSignFg: theme.warning,
47
+ currentMatchSignFg: theme.primary,
48
+ }
43
49
 
44
- if (accentColor) {
45
- return (
46
- <box
47
- key={index}
48
- style={{ flexDirection: "row" }}
49
- backgroundColor={bgColor ?? undefined}
50
- >
51
- <text content="▎" fg={accentColor} />
52
- <box style={{ flexGrow: 1, flexDirection: "column" }}>{blockContent}</box>
53
- </box>
54
- )
55
- }
50
+ useEffect(() => {
51
+ const decorations: RowDocumentDecorations = {
52
+ cursorRow: activeBlock,
53
+ selection: selectedBlocks ? { start: selectedBlocks.start, end: selectedBlocks.end } : null,
54
+ matchingRows: matchingBlocks,
55
+ currentMatchRow: currentMatchBlock,
56
+ toggledRows: toggledBlocks,
57
+ }
58
+ effectiveRef.current?.setDecorations(decorations)
59
+ }, [activeBlock, selectedBlocks, matchingBlocks, currentMatchBlock, toggledBlocks])
56
60
 
57
- return <box key={index}>{blockContent}</box>
58
- })}
59
- </box>
60
- )
61
- }
61
+ const blockElements = blocks.map((token, index) => (
62
+ <TokenRenderer key={index} token={token} theme={theme} syntax={syntax} />
63
+ ))
62
64
 
63
- function getBlockStyle(
64
- index: number,
65
- theme: ResolvedTheme,
66
- activeBlock?: number,
67
- selectedBlocks?: { start: number; end: number },
68
- matchingBlocks?: Set<number>,
69
- currentMatchBlock?: number,
70
- toggledBlocks?: Set<number>,
71
- ): { accent: string | null; background: string | null } {
72
- // Priority: cursor > toggled > current match > match > selection
73
- if (activeBlock === index) return { accent: theme.primary, background: theme.backgroundElement }
74
- if (toggledBlocks?.has(index)) return { accent: theme.secondary, background: theme.backgroundPanel }
75
- if (currentMatchBlock === index) return { accent: theme.accent, background: null }
76
- if (matchingBlocks?.has(index)) return { accent: theme.warning, background: null }
77
- if (selectedBlocks && index >= selectedBlocks.start && index <= selectedBlocks.end) {
78
- return { accent: theme.secondary, background: theme.backgroundPanel }
79
- }
80
- return { accent: null, background: null }
65
+ return (
66
+ <row-document
67
+ ref={effectiveRef}
68
+ key={theme.textMuted + theme.backgroundElement}
69
+ showLineNumbers={showLineNumbers}
70
+ palette={palette}
71
+ signColumnWidth={1}
72
+ style={{ flexGrow: 1 }}
73
+ >
74
+ {blockElements}
75
+ </row-document>
76
+ )
81
77
  }
82
78
 
83
79
  function TokenRenderer({
@@ -177,6 +173,7 @@ function CodeBlockRenderer({
177
173
  theme: ResolvedTheme
178
174
  syntax: SyntaxStyle
179
175
  }) {
176
+ const lineCount = token.text.split("\n").length
180
177
  return (
181
178
  <box
182
179
  style={{
@@ -190,7 +187,12 @@ function CodeBlockRenderer({
190
187
  flexDirection: "column",
191
188
  }}
192
189
  >
193
- <code content={token.text} filetype={token.lang} syntaxStyle={syntax} />
190
+ <code
191
+ content={token.text}
192
+ filetype={token.lang}
193
+ syntaxStyle={syntax}
194
+ style={{ height: lineCount }}
195
+ />
194
196
  </box>
195
197
  )
196
198
  }
@@ -274,30 +276,46 @@ function ListItemRenderer({
274
276
  }
275
277
 
276
278
  function MarkdownTableRenderer({ token }: { token: Tokens.Table }) {
277
- const seen = new Map<string, number>()
278
- const columns = token.header.map((cell, index) => {
279
- const header = getPlainText(cell.tokens)
280
- const trimmed = header.trim()
281
- const base = trimmed || `column_${index + 1}`
282
- const count = seen.get(base) ?? 0
283
- seen.set(base, count + 1)
284
- const key = count === 0 ? base : `${base}_${count + 1}`
285
- return {
286
- key,
287
- header: trimmed || undefined,
288
- }
279
+ const { theme } = useTheme()
280
+ const { width: terminalWidth } = useTerminalDimensions()
281
+
282
+ const headers = token.header.map((cell) => getPlainText(cell.tokens).trim())
283
+ const rowData = token.rows.map((row) =>
284
+ row.map((cell) => getPlainText(cell.tokens)),
285
+ )
286
+
287
+ // Account for gutter + margins when computing available width
288
+ const effectiveMaxWidth = terminalWidth - 4
289
+
290
+ const colWidths = computeColumnWidths(headers, rowData, effectiveMaxWidth, {
291
+ minColumnWidth: 4,
292
+ maxColumnWidth: 50,
293
+ sampleSize: 100,
289
294
  })
290
- const rows = token.rows.map((row) => {
291
- const record: Record<string, string> = {}
292
- row.forEach((cell, index) => {
293
- const column = columns[index]
294
- if (column) {
295
- record[column.key] = getPlainText(cell.tokens)
296
- }
297
- })
298
- return record
295
+
296
+ const alignments = headers.map((_header, colIdx) => {
297
+ const sampleValues = rowData.slice(0, 10).map((row) => row[colIdx] ?? "")
298
+ const numericCount = sampleValues.filter(isNumeric).length
299
+ return numericCount > sampleValues.length / 2
299
300
  })
300
- return <Table columns={columns} rows={rows} />
301
+
302
+ const topBorder = buildBorderLine(colWidths, "\u250c", "\u252c", "\u2510", "\u2500")
303
+ const headerSep = buildBorderLine(colWidths, "\u251c", "\u253c", "\u2524", "\u2500")
304
+ const bottomBorder = buildBorderLine(colWidths, "\u2514", "\u2534", "\u2518", "\u2500")
305
+ const headerLine = buildDataLine(headers, colWidths, alignments)
306
+ const dataLines = rowData.map((row) => buildDataLine(row, colWidths, alignments))
307
+
308
+ return (
309
+ <box style={{ marginLeft: 1, marginRight: 1, marginBottom: 1, flexDirection: "column" }}>
310
+ <text content={topBorder} style={{ fg: theme.border }} />
311
+ <text content={headerLine} style={{ fg: theme.primary }} />
312
+ <text content={headerSep} style={{ fg: theme.border }} />
313
+ {dataLines.map((line, i) => (
314
+ <text key={i} content={line} style={{ fg: theme.text }} />
315
+ ))}
316
+ <text content={bottomBorder} style={{ fg: theme.border }} />
317
+ </box>
318
+ )
301
319
  }
302
320
 
303
321
  function HorizontalRule({ theme }: { theme: ResolvedTheme }) {