Skip to content

Commit f98caa3

Browse files
committed
feat(markdown): enhance markdown processing with placeholder handling and unified dependency
- Added a placeholder handler in the markdown processor to manage custom nodes. - Updated the `createRemarkProcessor` function to include the new placeholder handler. - Refactored `customAstToString` to streamline AST stringification. - Introduced a new `unified` dependency for improved markdown processing. - Added comprehensive tests for new functionality and existing markdown utilities.
1 parent 89d1414 commit f98caa3

File tree

4 files changed

+394
-357
lines changed

4 files changed

+394
-357
lines changed

ee/tabby-ui/lib/utils/markdown.ts

+39-73
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,18 @@ const REMARK_STRINGIFY_OPTIONS: Options = {
88
emphasis: '*',
99
fences: true,
1010
listItemIndent: 'one',
11-
tightDefinitions: true
11+
tightDefinitions: true,
12+
handlers: {
13+
placeholder: (node: PlaceholderNode) => {
14+
console.log('placeholder node', node)
15+
return node.value
16+
}
17+
} as any
1218
}
1319

14-
1520
function createRemarkProcessor() {
16-
return remark().use(remarkStringify, REMARK_STRINGIFY_OPTIONS)
21+
return remark()
22+
.use(remarkStringify, REMARK_STRINGIFY_OPTIONS)
1723
}
1824

1925
/**
@@ -22,50 +28,14 @@ function createRemarkProcessor() {
2228
* @returns Plain string representation
2329
*/
2430
export function customAstToString(ast: Root): string {
25-
let result = ''
26-
for (const node of ast.children) {
27-
result += nodeToString(node) + '\n'
28-
}
29-
return result.trim()
30-
}
31-
32-
/**
33-
* Convert a single node to string
34-
* @param node AST node
35-
* @returns String representation
36-
*/
37-
function nodeToString(node: any): string {
38-
switch (node.type) {
39-
case 'paragraph':
40-
return paragraphToString(node)
41-
case 'text':
42-
return node.value
43-
default:
44-
return createRemarkProcessor().stringify({ type: 'root', children: [node] }).trim()
45-
}
31+
const processor = createRemarkProcessor()
32+
return processor.stringify(ast).trim()
4633
}
4734

48-
/**
49-
* Convert paragraph node to string
50-
* @param node Paragraph node
51-
* @returns String representation
52-
*/
53-
function paragraphToString(node: any): string {
54-
return childrenToString(node)
55-
}
5635

5736
/**
58-
* Process children of a node and join them
59-
* @param node Parent node
60-
* @returns Combined string of all children
37+
* Process code blocks with labels and convert them to placeholders
6138
*/
62-
function childrenToString(node: any): string {
63-
if (!node.children || node.children.length === 0) {
64-
return ''
65-
}
66-
return node.children.map((child: any) => nodeToString(child)).join('')
67-
}
68-
6939
export function processCodeBlocksWithLabel(ast: Root): RootContent[] {
7040
const newChildren: RootContent[] = []
7141
for (let i = 0; i < ast.children.length; i++) {
@@ -96,18 +66,19 @@ export function processCodeBlocksWithLabel(ast: Root): RootContent[] {
9666
nextNode.position.start.line - node.position.end.line === 1
9767

9868
let finalCommandText = ''
69+
let placeholderNode: RootContent | null = null
9970
let shouldProcessNode = true
10071

101-
// processing differet type of context
10272
switch (metas['label']) {
10373
case 'changes':
104-
finalCommandText = '[[contextCommand:"changes"]]'
74+
finalCommandText = 'contextCommand:changes'
75+
placeholderNode = createPlaceholderNode(`[[contextCommand:${finalCommandText}]]`) as unknown as RootContent
10576
break
10677
case 'file':
10778
if (metas['object']) {
10879
try {
109-
finalCommandText = formatPlaceholder('file', metas['object'])
110-
if (!finalCommandText) {
80+
placeholderNode = createPlaceholderNode(`[[file:${metas['object']}]]`) as unknown as RootContent
81+
if (!placeholderNode) {
11182
shouldProcessNode = false
11283
newChildren.push(node)
11384
}
@@ -120,8 +91,8 @@ export function processCodeBlocksWithLabel(ast: Root): RootContent[] {
12091
case 'symbol':
12192
if (metas['object']) {
12293
try {
123-
finalCommandText = formatPlaceholder('symbol', metas['object'])
124-
if (!finalCommandText) {
94+
placeholderNode = createPlaceholderNode(`[[symbol:${metas['object']}]]`) as unknown as RootContent
95+
if (!placeholderNode) {
12596
shouldProcessNode = false
12697
newChildren.push(node)
12798
}
@@ -154,7 +125,7 @@ export function processCodeBlocksWithLabel(ast: Root): RootContent[] {
154125
type: 'paragraph',
155126
children: [
156127
...(prevNode.children || []),
157-
{ type: 'text', value: ` ${finalCommandText} ` },
128+
placeholderNode || { type: 'text', value: ` ${finalCommandText} ` },
158129
...(nextNode.children || [])
159130
]
160131
} as RootContent)
@@ -167,7 +138,7 @@ export function processCodeBlocksWithLabel(ast: Root): RootContent[] {
167138
newChildren.push({
168139
type: 'paragraph',
169140
children: [
170-
{ type: 'text', value: `${finalCommandText} ` },
141+
placeholderNode || { type: 'text', value: `${finalCommandText} ` },
171142
...(nextNode.children || [])
172143
]
173144
} as RootContent)
@@ -176,14 +147,15 @@ export function processCodeBlocksWithLabel(ast: Root): RootContent[] {
176147
prevNode.type === 'paragraph' &&
177148
isPrevNodeSameLine
178149
) {
179-
;(prevNode.children || []).push({
180-
type: 'text',
181-
value: ` ${finalCommandText}`
182-
})
150+
;(prevNode.children || []).push(
151+
placeholderNode || { type: 'text', value: ` ${finalCommandText}` }
152+
)
183153
} else {
184154
newChildren.push({
185155
type: 'paragraph',
186-
children: [{ type: 'text', value: finalCommandText }]
156+
children: [
157+
placeholderNode || { type: 'text', value: finalCommandText }
158+
]
187159
} as RootContent)
188160
}
189161
} else {
@@ -217,7 +189,6 @@ export function formatObjectToMarkdownBlock(
217189
content: string
218190
): string {
219191
try {
220-
// Convert the object to a JSON string
221192
const objJSON = JSON.stringify(obj)
222193

223194
const codeNode: Root = {
@@ -235,29 +206,24 @@ export function formatObjectToMarkdownBlock(
235206
const processor = createRemarkProcessor()
236207

237208
const res = '\n' + processor.stringify(codeNode).trim() + '\n'
238-
console.log('res', res)
239209
return res;
240210
} catch (error) {
241-
console.error(`Error formatting ${label} to markdown block:`, error)
242211
return `\n*Error formatting ${label}*\n`
243212
}
244213
}
245214

246215

247216

248-
/**
249-
* Format a placeholder with proper backslash escaping
250-
* @param type The type of placeholder (e.g., 'file', 'symbol')
251-
* @param objStr The string representation of the object
252-
* @returns The formatted placeholder text
253-
*/
254-
export function formatPlaceholder(type: string, objStr: string): string {
255-
if (!objStr) return ''
256-
257-
try {
258-
return `[[${type}:${objStr}]]`
259-
} catch (error) {
260-
console.error(`Error formatting ${type} placeholder:`, error)
261-
return ''
262-
}
217+
218+
219+
export interface PlaceholderNode extends Node {
220+
type: 'placeholder'
221+
value: string
222+
}
223+
224+
export function createPlaceholderNode(value: string): PlaceholderNode {
225+
return {
226+
type: 'placeholder',
227+
value: value,
228+
} as PlaceholderNode
263229
}

ee/tabby-ui/package.json

+1
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,7 @@
130130
"swr": "^2.2.4",
131131
"tabby-chat-panel": "workspace:*",
132132
"tippy.js": "^6.3.7",
133+
"unified": "^11.0.5",
133134
"urql": "^4.0.6",
134135
"use-local-storage": "^3.0.0",
135136
"wonka": "^6.3.4",

0 commit comments

Comments
 (0)