@tooee/renderers 0.1.11 → 0.1.14
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/dist/CodeView.d.ts +3 -7
- package/dist/CodeView.d.ts.map +1 -1
- package/dist/CodeView.js +5 -31
- package/dist/CodeView.js.map +1 -1
- package/dist/CommandPalette.d.ts.map +1 -1
- package/dist/CommandPalette.js +1 -23
- package/dist/CommandPalette.js.map +1 -1
- package/dist/DecorationLayer.d.ts +14 -0
- package/dist/DecorationLayer.d.ts.map +1 -0
- package/dist/DecorationLayer.js +2 -0
- package/dist/DecorationLayer.js.map +1 -0
- package/dist/MarkdownView.d.ts +16 -9
- package/dist/MarkdownView.d.ts.map +1 -1
- package/dist/MarkdownView.js +256 -107
- package/dist/MarkdownView.js.map +1 -1
- package/dist/RowDocumentRenderable.d.ts +14 -26
- package/dist/RowDocumentRenderable.d.ts.map +1 -1
- package/dist/RowDocumentRenderable.js +74 -78
- package/dist/RowDocumentRenderable.js.map +1 -1
- package/dist/Table.d.ts +5 -9
- package/dist/Table.d.ts.map +1 -1
- package/dist/Table.js +17 -46
- package/dist/Table.js.map +1 -1
- package/dist/index.d.ts +7 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -2
- package/dist/index.js.map +1 -1
- package/dist/parsers.d.ts.map +1 -1
- package/dist/parsers.js.map +1 -1
- package/dist/useGutterPalette.d.ts +3 -0
- package/dist/useGutterPalette.d.ts.map +1 -0
- package/dist/useGutterPalette.js +10 -0
- package/dist/useGutterPalette.js.map +1 -0
- package/package.json +18 -16
- package/src/CodeView.tsx +11 -54
- package/src/CommandPalette.tsx +1 -25
- package/src/DecorationLayer.ts +11 -0
- package/src/MarkdownView.tsx +382 -164
- package/src/RowDocumentRenderable.ts +91 -135
- package/src/Table.tsx +35 -71
- package/src/index.ts +6 -8
- package/src/parsers.ts +4 -1
- package/src/useGutterPalette.ts +15 -0
package/src/MarkdownView.tsx
CHANGED
|
@@ -1,73 +1,161 @@
|
|
|
1
1
|
import { marked, type Token, type Tokens } from "marked"
|
|
2
|
-
import {
|
|
2
|
+
import { useMemo, type ReactNode, type RefObject } from "react"
|
|
3
3
|
import { useTheme, type ResolvedTheme } from "@tooee/themes"
|
|
4
|
-
import {
|
|
5
|
-
|
|
6
|
-
|
|
4
|
+
import {
|
|
5
|
+
bold as boldChunk,
|
|
6
|
+
italic as italicChunk,
|
|
7
|
+
underline as underlineChunk,
|
|
8
|
+
parseColor,
|
|
9
|
+
} from "@opentui/core"
|
|
10
|
+
import type {
|
|
11
|
+
SyntaxStyle,
|
|
12
|
+
TextTableContent,
|
|
13
|
+
TextTableCellContent,
|
|
14
|
+
TextChunk,
|
|
15
|
+
} from "@opentui/core"
|
|
16
|
+
import type { MarkState } from "@tooee/marks"
|
|
17
|
+
import type { RowDocumentRenderable } from "./RowDocumentRenderable.js"
|
|
18
|
+
import { useGutterPalette } from "./useGutterPalette.js"
|
|
7
19
|
import "./row-document.js"
|
|
8
20
|
import "./text-table.js"
|
|
9
21
|
|
|
22
|
+
// ---------------------------------------------------------------------------
|
|
23
|
+
// Types
|
|
24
|
+
// ---------------------------------------------------------------------------
|
|
25
|
+
|
|
10
26
|
interface MarkdownViewProps {
|
|
11
27
|
content: string
|
|
12
28
|
showLineNumbers?: boolean
|
|
13
|
-
|
|
14
|
-
selectedBlocks?: { start: number; end: number }
|
|
15
|
-
matchingBlocks?: Set<number>
|
|
16
|
-
currentMatchBlock?: number
|
|
17
|
-
toggledBlocks?: Set<number>
|
|
29
|
+
marks?: MarkState
|
|
18
30
|
docRef?: RefObject<RowDocumentRenderable | null>
|
|
19
31
|
}
|
|
20
32
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
33
|
+
/**
|
|
34
|
+
* A flattened block — all blocks exist at the top level with an indent.
|
|
35
|
+
* Nested structures (lists containing code blocks, etc.) are flattened
|
|
36
|
+
* into sibling blocks with appropriate indentation.
|
|
37
|
+
*/
|
|
38
|
+
export interface FlatBlock {
|
|
39
|
+
token: Token
|
|
40
|
+
indent: number
|
|
41
|
+
bullet?: string // "- " or "1. " for list item lines
|
|
42
|
+
checked?: boolean // undefined = not a checkbox, true/false = checkbox state
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// ---------------------------------------------------------------------------
|
|
46
|
+
// Token flattening
|
|
47
|
+
// ---------------------------------------------------------------------------
|
|
48
|
+
|
|
49
|
+
export function flattenTokens(tokens: Token[]): FlatBlock[] {
|
|
50
|
+
const result: FlatBlock[] = []
|
|
51
|
+
flattenTokenList(tokens, 0, result)
|
|
52
|
+
return result
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function flattenTokenList(tokens: Token[], indent: number, result: FlatBlock[]): void {
|
|
56
|
+
for (const token of tokens) {
|
|
57
|
+
if (token.type === "space") continue
|
|
58
|
+
|
|
59
|
+
if (token.type === "list") {
|
|
60
|
+
const list = token as Tokens.List
|
|
61
|
+
for (let i = 0; i < list.items.length; i++) {
|
|
62
|
+
const item = list.items[i]
|
|
63
|
+
const bullet = list.ordered ? `${i + (list.start || 1)}. ` : "- "
|
|
64
|
+
flattenListItem(item, indent, bullet, result)
|
|
65
|
+
}
|
|
66
|
+
} else {
|
|
67
|
+
result.push({ token, indent })
|
|
68
|
+
}
|
|
48
69
|
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function flattenListItem(
|
|
73
|
+
item: Tokens.ListItem,
|
|
74
|
+
indent: number,
|
|
75
|
+
bullet: string,
|
|
76
|
+
result: FlatBlock[],
|
|
77
|
+
): void {
|
|
78
|
+
const checked = item.checked != null ? item.checked : undefined
|
|
79
|
+
const childTokens = item.tokens || []
|
|
80
|
+
let bulletUsed = false
|
|
81
|
+
|
|
82
|
+
for (const token of childTokens) {
|
|
83
|
+
if (token.type === "space" || token.type === "checkbox") continue
|
|
49
84
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
85
|
+
if (token.type === "text" || token.type === "paragraph") {
|
|
86
|
+
// Inline content — attach the bullet to the first one
|
|
87
|
+
result.push({
|
|
88
|
+
token,
|
|
89
|
+
indent,
|
|
90
|
+
bullet: bulletUsed ? undefined : bullet,
|
|
91
|
+
checked: bulletUsed ? undefined : checked,
|
|
92
|
+
})
|
|
93
|
+
bulletUsed = true
|
|
94
|
+
} else if (token.type === "list") {
|
|
95
|
+
// Emit bullet line if nothing preceded this nested list
|
|
96
|
+
if (!bulletUsed) {
|
|
97
|
+
result.push({
|
|
98
|
+
token: { type: "text", raw: "", text: "", tokens: [] } as unknown as Token,
|
|
99
|
+
indent,
|
|
100
|
+
bullet,
|
|
101
|
+
checked,
|
|
102
|
+
})
|
|
103
|
+
bulletUsed = true
|
|
104
|
+
}
|
|
105
|
+
// Nested list — increase indent to align with content after bullet
|
|
106
|
+
const list = token as Tokens.List
|
|
107
|
+
for (let i = 0; i < list.items.length; i++) {
|
|
108
|
+
const subItem = list.items[i]
|
|
109
|
+
const subBullet = list.ordered ? `${i + (list.start || 1)}. ` : "- "
|
|
110
|
+
flattenListItem(subItem, indent + bullet.length, subBullet, result)
|
|
111
|
+
}
|
|
112
|
+
} else {
|
|
113
|
+
// Block content (code, table, blockquote, hr, etc.)
|
|
114
|
+
if (!bulletUsed) {
|
|
115
|
+
result.push({
|
|
116
|
+
token: { type: "text", raw: "", text: "", tokens: [] } as unknown as Token,
|
|
117
|
+
indent,
|
|
118
|
+
bullet,
|
|
119
|
+
checked,
|
|
120
|
+
})
|
|
121
|
+
bulletUsed = true
|
|
122
|
+
}
|
|
123
|
+
// Emit the block indented to align with content after the bullet
|
|
124
|
+
result.push({ token, indent: indent + bullet.length })
|
|
57
125
|
}
|
|
58
|
-
|
|
59
|
-
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// List item had no content tokens — still emit the bullet
|
|
129
|
+
if (!bulletUsed) {
|
|
130
|
+
result.push({
|
|
131
|
+
token: { type: "text", raw: "", text: "", tokens: [] } as unknown as Token,
|
|
132
|
+
indent,
|
|
133
|
+
bullet,
|
|
134
|
+
checked,
|
|
135
|
+
})
|
|
136
|
+
}
|
|
137
|
+
}
|
|
60
138
|
|
|
61
|
-
|
|
62
|
-
|
|
139
|
+
// ---------------------------------------------------------------------------
|
|
140
|
+
// Component
|
|
141
|
+
// ---------------------------------------------------------------------------
|
|
142
|
+
|
|
143
|
+
export function MarkdownView({ content, showLineNumbers = true, marks, docRef }: MarkdownViewProps) {
|
|
144
|
+
const { theme, syntax } = useTheme()
|
|
145
|
+
const palette = useGutterPalette()
|
|
146
|
+
const tokens = marked.lexer(content)
|
|
147
|
+
const blocks = flattenTokens(tokens)
|
|
148
|
+
|
|
149
|
+
const blockElements = blocks.map((block, index) => (
|
|
150
|
+
<FlatBlockRenderer key={index} block={block} theme={theme} syntax={syntax} />
|
|
63
151
|
))
|
|
64
152
|
|
|
65
153
|
return (
|
|
66
154
|
<row-document
|
|
67
|
-
ref={
|
|
68
|
-
key={theme.textMuted + theme.backgroundElement}
|
|
155
|
+
ref={docRef}
|
|
69
156
|
showLineNumbers={showLineNumbers}
|
|
70
157
|
palette={palette}
|
|
158
|
+
decorations={marks?.sets}
|
|
71
159
|
signColumnWidth={1}
|
|
72
160
|
style={{ flexGrow: 1 }}
|
|
73
161
|
>
|
|
@@ -76,32 +164,50 @@ export function MarkdownView({
|
|
|
76
164
|
)
|
|
77
165
|
}
|
|
78
166
|
|
|
79
|
-
|
|
80
|
-
|
|
167
|
+
// ---------------------------------------------------------------------------
|
|
168
|
+
// Block renderer (flat)
|
|
169
|
+
// ---------------------------------------------------------------------------
|
|
170
|
+
|
|
171
|
+
function FlatBlockRenderer({
|
|
172
|
+
block,
|
|
81
173
|
theme,
|
|
82
174
|
syntax,
|
|
83
175
|
}: {
|
|
84
|
-
|
|
176
|
+
block: FlatBlock
|
|
85
177
|
theme: ResolvedTheme
|
|
86
178
|
syntax: SyntaxStyle
|
|
87
179
|
}): ReactNode {
|
|
180
|
+
const { token, indent, bullet } = block
|
|
181
|
+
|
|
182
|
+
// List item line (has bullet)
|
|
183
|
+
if (bullet !== undefined) {
|
|
184
|
+
return <ListLineRenderer block={block} theme={theme} />
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// Regular block token
|
|
88
188
|
switch (token.type) {
|
|
89
189
|
case "heading":
|
|
90
|
-
return <HeadingRenderer token={token as Tokens.Heading} theme={theme} />
|
|
190
|
+
return <HeadingRenderer token={token as Tokens.Heading} theme={theme} indent={indent} />
|
|
91
191
|
case "paragraph":
|
|
92
|
-
return <ParagraphRenderer token={token as Tokens.Paragraph} theme={theme} />
|
|
192
|
+
return <ParagraphRenderer token={token as Tokens.Paragraph} theme={theme} indent={indent} />
|
|
93
193
|
case "code":
|
|
94
|
-
return
|
|
194
|
+
return (
|
|
195
|
+
<CodeBlockRenderer
|
|
196
|
+
token={token as Tokens.Code}
|
|
197
|
+
theme={theme}
|
|
198
|
+
syntax={syntax}
|
|
199
|
+
indent={indent}
|
|
200
|
+
/>
|
|
201
|
+
)
|
|
95
202
|
case "blockquote":
|
|
96
|
-
return
|
|
97
|
-
|
|
98
|
-
|
|
203
|
+
return (
|
|
204
|
+
<BlockquoteRenderer token={token as Tokens.Blockquote} theme={theme} indent={indent} />
|
|
205
|
+
)
|
|
99
206
|
case "table":
|
|
100
|
-
return <MarkdownTableRenderer token={token as Tokens.Table} />
|
|
207
|
+
return <MarkdownTableRenderer token={token as Tokens.Table} indent={indent} />
|
|
101
208
|
case "hr":
|
|
102
|
-
return <HorizontalRule theme={theme} />
|
|
209
|
+
return <HorizontalRule theme={theme} indent={indent} />
|
|
103
210
|
case "space":
|
|
104
|
-
return null
|
|
105
211
|
case "html":
|
|
106
212
|
return null
|
|
107
213
|
default:
|
|
@@ -113,7 +219,7 @@ function TokenRenderer({
|
|
|
113
219
|
fg: theme.markdownText,
|
|
114
220
|
marginBottom: 1,
|
|
115
221
|
marginTop: 0,
|
|
116
|
-
marginLeft: 1,
|
|
222
|
+
marginLeft: 1 + indent,
|
|
117
223
|
marginRight: 1,
|
|
118
224
|
}}
|
|
119
225
|
/>
|
|
@@ -123,7 +229,52 @@ function TokenRenderer({
|
|
|
123
229
|
}
|
|
124
230
|
}
|
|
125
231
|
|
|
126
|
-
|
|
232
|
+
// ---------------------------------------------------------------------------
|
|
233
|
+
// List line renderer
|
|
234
|
+
// ---------------------------------------------------------------------------
|
|
235
|
+
|
|
236
|
+
function ListLineRenderer({ block, theme }: { block: FlatBlock; theme: ResolvedTheme }) {
|
|
237
|
+
const { token, indent, bullet, checked } = block
|
|
238
|
+
const checkboxPrefix = checked !== undefined ? (checked ? "[x] " : "[ ] ") : ""
|
|
239
|
+
|
|
240
|
+
// Get inline tokens from the text/paragraph token
|
|
241
|
+
const inlineTokens: Token[] =
|
|
242
|
+
"tokens" in token && Array.isArray(token.tokens) ? token.tokens : []
|
|
243
|
+
|
|
244
|
+
const hasText = "text" in token && typeof token.text === "string" && token.text.length > 0
|
|
245
|
+
const hasContent = inlineTokens.length > 0 || hasText
|
|
246
|
+
|
|
247
|
+
return (
|
|
248
|
+
<box style={{ marginLeft: 1 + indent, marginRight: 1 }}>
|
|
249
|
+
<text style={{ fg: theme.markdownText }}>
|
|
250
|
+
<span fg={theme.markdownListItem}>{bullet}</span>
|
|
251
|
+
{checkboxPrefix !== "" && (
|
|
252
|
+
<span fg={checked ? theme.accent : theme.textMuted}>{checkboxPrefix}</span>
|
|
253
|
+
)}
|
|
254
|
+
{hasContent &&
|
|
255
|
+
(inlineTokens.length > 0 ? (
|
|
256
|
+
<InlineTokens tokens={inlineTokens} theme={theme} />
|
|
257
|
+
) : hasText ? (
|
|
258
|
+
("text" in token ? (token as { text: string }).text : "")
|
|
259
|
+
) : null)}
|
|
260
|
+
</text>
|
|
261
|
+
</box>
|
|
262
|
+
)
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// ---------------------------------------------------------------------------
|
|
266
|
+
// Block renderers
|
|
267
|
+
// ---------------------------------------------------------------------------
|
|
268
|
+
|
|
269
|
+
function HeadingRenderer({
|
|
270
|
+
token,
|
|
271
|
+
theme,
|
|
272
|
+
indent,
|
|
273
|
+
}: {
|
|
274
|
+
token: Tokens.Heading
|
|
275
|
+
theme: ResolvedTheme
|
|
276
|
+
indent: number
|
|
277
|
+
}) {
|
|
127
278
|
const headingColors: Record<number, string> = {
|
|
128
279
|
1: theme.markdownHeading,
|
|
129
280
|
2: theme.secondary,
|
|
@@ -142,21 +293,29 @@ function HeadingRenderer({ token, theme }: { token: Tokens.Heading; theme: Resol
|
|
|
142
293
|
6: "###### ",
|
|
143
294
|
}
|
|
144
295
|
|
|
145
|
-
const headingText = getPlainText(token.tokens || [])
|
|
146
|
-
|
|
147
296
|
return (
|
|
148
|
-
<box style={{ marginTop: 1, marginBottom: 1 }}>
|
|
297
|
+
<box style={{ marginTop: 1, marginBottom: 1, marginLeft: indent }}>
|
|
149
298
|
<text style={{ fg: headingColors[token.depth] || theme.text }}>
|
|
150
299
|
<span fg={theme.textMuted}>{prefixes[token.depth]}</span>
|
|
151
|
-
<strong>
|
|
300
|
+
<strong>
|
|
301
|
+
<InlineTokens tokens={token.tokens || []} theme={theme} />
|
|
302
|
+
</strong>
|
|
152
303
|
</text>
|
|
153
304
|
</box>
|
|
154
305
|
)
|
|
155
306
|
}
|
|
156
307
|
|
|
157
|
-
function ParagraphRenderer({
|
|
308
|
+
function ParagraphRenderer({
|
|
309
|
+
token,
|
|
310
|
+
theme,
|
|
311
|
+
indent,
|
|
312
|
+
}: {
|
|
313
|
+
token: Tokens.Paragraph
|
|
314
|
+
theme: ResolvedTheme
|
|
315
|
+
indent: number
|
|
316
|
+
}) {
|
|
158
317
|
return (
|
|
159
|
-
<box style={{ marginBottom: 1, marginLeft: 1, marginRight: 1 }}>
|
|
318
|
+
<box style={{ marginBottom: 1, marginLeft: 1 + indent, marginRight: 1 }}>
|
|
160
319
|
<text style={{ fg: theme.markdownText }}>
|
|
161
320
|
<InlineTokens tokens={token.tokens || []} theme={theme} />
|
|
162
321
|
</text>
|
|
@@ -168,10 +327,12 @@ function CodeBlockRenderer({
|
|
|
168
327
|
token,
|
|
169
328
|
theme,
|
|
170
329
|
syntax,
|
|
330
|
+
indent,
|
|
171
331
|
}: {
|
|
172
332
|
token: Tokens.Code
|
|
173
333
|
theme: ResolvedTheme
|
|
174
334
|
syntax: SyntaxStyle
|
|
335
|
+
indent: number
|
|
175
336
|
}) {
|
|
176
337
|
const lineCount = token.text.split("\n").length
|
|
177
338
|
return (
|
|
@@ -179,7 +340,7 @@ function CodeBlockRenderer({
|
|
|
179
340
|
style={{
|
|
180
341
|
marginTop: 0,
|
|
181
342
|
marginBottom: 1,
|
|
182
|
-
marginLeft: 1,
|
|
343
|
+
marginLeft: 1 + indent,
|
|
183
344
|
marginRight: 1,
|
|
184
345
|
border: true,
|
|
185
346
|
borderColor: theme.border,
|
|
@@ -197,101 +358,72 @@ function CodeBlockRenderer({
|
|
|
197
358
|
)
|
|
198
359
|
}
|
|
199
360
|
|
|
200
|
-
function BlockquoteRenderer({
|
|
201
|
-
|
|
202
|
-
? token.tokens
|
|
203
|
-
.map((t) => {
|
|
204
|
-
const innerTokens = "tokens" in t ? (t as { tokens?: Token[] }).tokens : undefined
|
|
205
|
-
const textContent = "text" in t ? (t as { text?: string }).text : ""
|
|
206
|
-
return getPlainText(innerTokens || []) || textContent || ""
|
|
207
|
-
})
|
|
208
|
-
.join("\n")
|
|
209
|
-
: ""
|
|
210
|
-
|
|
211
|
-
return (
|
|
212
|
-
<box style={{ marginTop: 0, marginBottom: 1, marginLeft: 1, marginRight: 1, paddingLeft: 2 }}>
|
|
213
|
-
<text style={{ fg: theme.markdownBlockQuote }} content="│ " />
|
|
214
|
-
<text style={{ fg: theme.textMuted }} content={quoteText} />
|
|
215
|
-
</box>
|
|
216
|
-
)
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
function ListRenderer({ token, theme }: { token: Tokens.List; theme: ResolvedTheme }) {
|
|
220
|
-
return (
|
|
221
|
-
<box style={{ marginBottom: 1, marginLeft: 3, marginRight: 1, flexDirection: "column" }}>
|
|
222
|
-
{token.items.map((item, index) => (
|
|
223
|
-
<ListItemRenderer
|
|
224
|
-
key={index}
|
|
225
|
-
item={item}
|
|
226
|
-
ordered={token.ordered}
|
|
227
|
-
index={index + (token.start || 1)}
|
|
228
|
-
theme={theme}
|
|
229
|
-
/>
|
|
230
|
-
))}
|
|
231
|
-
</box>
|
|
232
|
-
)
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
function ListItemRenderer({
|
|
236
|
-
item,
|
|
237
|
-
ordered,
|
|
238
|
-
index,
|
|
361
|
+
function BlockquoteRenderer({
|
|
362
|
+
token,
|
|
239
363
|
theme,
|
|
364
|
+
indent,
|
|
240
365
|
}: {
|
|
241
|
-
|
|
242
|
-
ordered: boolean
|
|
243
|
-
index: number
|
|
366
|
+
token: Tokens.Blockquote
|
|
244
367
|
theme: ResolvedTheme
|
|
368
|
+
indent: number
|
|
245
369
|
}) {
|
|
246
|
-
|
|
247
|
-
const
|
|
370
|
+
// Collect inline tokens from blockquote's child paragraphs/text
|
|
371
|
+
const inlineTokens: Token[] = []
|
|
372
|
+
if (token.tokens) {
|
|
373
|
+
for (const child of token.tokens) {
|
|
374
|
+
if ("tokens" in child && Array.isArray(child.tokens)) {
|
|
375
|
+
if (inlineTokens.length > 0) {
|
|
376
|
+
inlineTokens.push({ type: "text", raw: "\n", text: "\n" } as Token)
|
|
377
|
+
}
|
|
378
|
+
inlineTokens.push(...(child.tokens as Token[]))
|
|
379
|
+
} else if ("text" in child && typeof child.text === "string") {
|
|
380
|
+
inlineTokens.push(child)
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
}
|
|
248
384
|
|
|
249
385
|
return (
|
|
250
|
-
<box
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
<text key={idx} style={{ fg: theme.markdownText }}>
|
|
264
|
-
<InlineTokens tokens={token.tokens} theme={theme} />
|
|
265
|
-
</text>
|
|
266
|
-
)
|
|
267
|
-
}
|
|
268
|
-
if ("text" in token && typeof token.text === "string") {
|
|
269
|
-
return <text key={idx} style={{ fg: theme.markdownText }} content={token.text} />
|
|
270
|
-
}
|
|
271
|
-
return null
|
|
272
|
-
})}
|
|
273
|
-
</box>
|
|
386
|
+
<box
|
|
387
|
+
style={{
|
|
388
|
+
marginTop: 0,
|
|
389
|
+
marginBottom: 1,
|
|
390
|
+
marginLeft: 1 + indent,
|
|
391
|
+
marginRight: 1,
|
|
392
|
+
paddingLeft: 2,
|
|
393
|
+
}}
|
|
394
|
+
>
|
|
395
|
+
<text style={{ fg: theme.markdownBlockQuote }} content="│ " />
|
|
396
|
+
<text style={{ fg: theme.textMuted }}>
|
|
397
|
+
<InlineTokens tokens={inlineTokens} theme={theme} />
|
|
398
|
+
</text>
|
|
274
399
|
</box>
|
|
275
400
|
)
|
|
276
401
|
}
|
|
277
402
|
|
|
278
|
-
function MarkdownTableRenderer({ token }: { token: Tokens.Table }) {
|
|
403
|
+
function MarkdownTableRenderer({ token, indent }: { token: Tokens.Table; indent: number }) {
|
|
279
404
|
const { theme } = useTheme()
|
|
280
405
|
|
|
281
406
|
const content: TextTableContent = useMemo(() => {
|
|
282
|
-
const headerRow: TextTableCellContent[] = token.header.map(cell =>
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
407
|
+
const headerRow: TextTableCellContent[] = token.header.map((cell) => {
|
|
408
|
+
const chunks = inlineTokensToChunks(cell.tokens, theme)
|
|
409
|
+
// Wrap header chunks in bold
|
|
410
|
+
return chunks.length > 0
|
|
411
|
+
? chunks.map((c) => boldChunk(c))
|
|
412
|
+
: [boldChunk(getPlainText(cell.tokens).trim())]
|
|
413
|
+
})
|
|
414
|
+
const dataRows = token.rows.map((row) =>
|
|
415
|
+
row.map((cell) => {
|
|
416
|
+
const chunks = inlineTokensToChunks(cell.tokens, theme)
|
|
417
|
+
return chunks.length > 0
|
|
418
|
+
? chunks
|
|
419
|
+
: ([{ __isChunk: true as const, text: getPlainText(cell.tokens) }] as TextTableCellContent)
|
|
420
|
+
}),
|
|
289
421
|
)
|
|
290
422
|
return [headerRow, ...dataRows]
|
|
291
|
-
}, [token])
|
|
423
|
+
}, [token, theme])
|
|
292
424
|
|
|
293
425
|
return (
|
|
294
|
-
<box style={{ marginLeft: 1, marginRight: 1, marginBottom: 1 }}>
|
|
426
|
+
<box style={{ marginLeft: 1 + indent, marginRight: 1, marginBottom: 1 }}>
|
|
295
427
|
<text-table
|
|
296
428
|
content={content}
|
|
297
429
|
wrapMode="word"
|
|
@@ -306,25 +438,17 @@ function MarkdownTableRenderer({ token }: { token: Tokens.Table }) {
|
|
|
306
438
|
)
|
|
307
439
|
}
|
|
308
440
|
|
|
309
|
-
function HorizontalRule({ theme }: { theme: ResolvedTheme }) {
|
|
441
|
+
function HorizontalRule({ theme, indent }: { theme: ResolvedTheme; indent: number }) {
|
|
310
442
|
return (
|
|
311
|
-
<box style={{ marginTop: 0, marginBottom: 1, marginLeft: 1, marginRight: 1 }}>
|
|
443
|
+
<box style={{ marginTop: 0, marginBottom: 1, marginLeft: 1 + indent, marginRight: 1 }}>
|
|
312
444
|
<text style={{ fg: theme.markdownHorizontalRule }} content={"─".repeat(40)} />
|
|
313
445
|
</box>
|
|
314
446
|
)
|
|
315
447
|
}
|
|
316
448
|
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
if (token.type === "text") return token.text
|
|
321
|
-
if (token.type === "codespan") return (token as Tokens.Codespan).text
|
|
322
|
-
if ("tokens" in token && token.tokens) return getPlainText(token.tokens as Token[])
|
|
323
|
-
if ("text" in token) return (token as { text: string }).text
|
|
324
|
-
return ""
|
|
325
|
-
})
|
|
326
|
-
.join("")
|
|
327
|
-
}
|
|
449
|
+
// ---------------------------------------------------------------------------
|
|
450
|
+
// Inline token rendering (React elements)
|
|
451
|
+
// ---------------------------------------------------------------------------
|
|
328
452
|
|
|
329
453
|
function InlineTokens({ tokens, theme }: { tokens: Token[]; theme: ResolvedTheme }): ReactNode {
|
|
330
454
|
const result: ReactNode[] = []
|
|
@@ -340,11 +464,17 @@ function InlineTokens({ tokens, theme }: { tokens: Token[]; theme: ResolvedTheme
|
|
|
340
464
|
break
|
|
341
465
|
case "strong":
|
|
342
466
|
result.push(
|
|
343
|
-
<strong key={key}>
|
|
467
|
+
<strong key={key}>
|
|
468
|
+
<InlineTokens tokens={(token as Tokens.Strong).tokens || []} theme={theme} />
|
|
469
|
+
</strong>,
|
|
344
470
|
)
|
|
345
471
|
break
|
|
346
472
|
case "em":
|
|
347
|
-
result.push(
|
|
473
|
+
result.push(
|
|
474
|
+
<em key={key}>
|
|
475
|
+
<InlineTokens tokens={(token as Tokens.Em).tokens || []} theme={theme} />
|
|
476
|
+
</em>,
|
|
477
|
+
)
|
|
348
478
|
break
|
|
349
479
|
case "codespan":
|
|
350
480
|
result.push(
|
|
@@ -358,12 +488,30 @@ function InlineTokens({ tokens, theme }: { tokens: Token[]; theme: ResolvedTheme
|
|
|
358
488
|
result.push(
|
|
359
489
|
<u key={key}>
|
|
360
490
|
<a href={linkToken.href} fg={theme.markdownLink}>
|
|
361
|
-
{
|
|
491
|
+
<InlineTokens tokens={linkToken.tokens || []} theme={theme} />
|
|
362
492
|
</a>
|
|
363
493
|
</u>,
|
|
364
494
|
)
|
|
365
495
|
break
|
|
366
496
|
}
|
|
497
|
+
case "del":
|
|
498
|
+
result.push(
|
|
499
|
+
<span key={key} fg={theme.textMuted}>
|
|
500
|
+
{"~"}
|
|
501
|
+
<InlineTokens tokens={(token as Tokens.Del).tokens || []} theme={theme} />
|
|
502
|
+
{"~"}
|
|
503
|
+
</span>,
|
|
504
|
+
)
|
|
505
|
+
break
|
|
506
|
+
case "image": {
|
|
507
|
+
const imgToken = token as Tokens.Image
|
|
508
|
+
result.push(
|
|
509
|
+
<span key={key} fg={theme.textMuted}>
|
|
510
|
+
{imgToken.text || imgToken.href}
|
|
511
|
+
</span>,
|
|
512
|
+
)
|
|
513
|
+
break
|
|
514
|
+
}
|
|
367
515
|
case "br":
|
|
368
516
|
result.push("\n")
|
|
369
517
|
break
|
|
@@ -383,3 +531,73 @@ function InlineTokens({ tokens, theme }: { tokens: Token[]; theme: ResolvedTheme
|
|
|
383
531
|
|
|
384
532
|
return <>{result}</>
|
|
385
533
|
}
|
|
534
|
+
|
|
535
|
+
// ---------------------------------------------------------------------------
|
|
536
|
+
// Inline token rendering (TextChunks — for text-table cells)
|
|
537
|
+
// ---------------------------------------------------------------------------
|
|
538
|
+
|
|
539
|
+
function inlineTokensToChunks(tokens: Token[], theme: ResolvedTheme): TextChunk[] {
|
|
540
|
+
const chunks: TextChunk[] = []
|
|
541
|
+
|
|
542
|
+
for (const token of tokens) {
|
|
543
|
+
switch (token.type) {
|
|
544
|
+
case "text":
|
|
545
|
+
chunks.push({ __isChunk: true as const, text: (token as Tokens.Text).text })
|
|
546
|
+
break
|
|
547
|
+
case "strong":
|
|
548
|
+
for (const sub of inlineTokensToChunks(
|
|
549
|
+
(token as Tokens.Strong).tokens || [],
|
|
550
|
+
theme,
|
|
551
|
+
)) {
|
|
552
|
+
chunks.push(boldChunk(sub))
|
|
553
|
+
}
|
|
554
|
+
break
|
|
555
|
+
case "em":
|
|
556
|
+
for (const sub of inlineTokensToChunks((token as Tokens.Em).tokens || [], theme)) {
|
|
557
|
+
chunks.push(italicChunk(sub))
|
|
558
|
+
}
|
|
559
|
+
break
|
|
560
|
+
case "codespan":
|
|
561
|
+
chunks.push({
|
|
562
|
+
__isChunk: true as const,
|
|
563
|
+
text: ` ${(token as Tokens.Codespan).text} `,
|
|
564
|
+
fg: parseColor(theme.markdownCode),
|
|
565
|
+
bg: parseColor(theme.backgroundPanel),
|
|
566
|
+
})
|
|
567
|
+
break
|
|
568
|
+
case "link": {
|
|
569
|
+
const linkToken = token as Tokens.Link
|
|
570
|
+
for (const sub of inlineTokensToChunks(linkToken.tokens || [], theme)) {
|
|
571
|
+
chunks.push(underlineChunk({ ...sub, fg: parseColor(theme.markdownLink) }))
|
|
572
|
+
}
|
|
573
|
+
break
|
|
574
|
+
}
|
|
575
|
+
case "escape":
|
|
576
|
+
chunks.push({ __isChunk: true as const, text: (token as Tokens.Escape).text })
|
|
577
|
+
break
|
|
578
|
+
default:
|
|
579
|
+
if ("text" in token && typeof (token as { text?: string }).text === "string") {
|
|
580
|
+
chunks.push({ __isChunk: true as const, text: (token as { text: string }).text })
|
|
581
|
+
}
|
|
582
|
+
break
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
return chunks
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
// ---------------------------------------------------------------------------
|
|
590
|
+
// Plain text extraction (only for width computation, not rendering)
|
|
591
|
+
// ---------------------------------------------------------------------------
|
|
592
|
+
|
|
593
|
+
function getPlainText(tokens: Token[]): string {
|
|
594
|
+
return tokens
|
|
595
|
+
.map((token) => {
|
|
596
|
+
if (token.type === "text") return token.text
|
|
597
|
+
if (token.type === "codespan") return (token as Tokens.Codespan).text
|
|
598
|
+
if ("tokens" in token && token.tokens) return getPlainText(token.tokens as Token[])
|
|
599
|
+
if ("text" in token) return (token as { text: string }).text
|
|
600
|
+
return ""
|
|
601
|
+
})
|
|
602
|
+
.join("")
|
|
603
|
+
}
|