1
1
import { marked } from "marked" ;
2
- import hljs from "highlight.js" ;
3
2
import { markedHighlight } from "marked-highlight" ;
4
- import type { Link } from "marked" ;
5
-
6
- marked . use ( markedHighlight ( {
7
- highlight : ( code , lang ) => {
8
- if ( lang && hljs . getLanguage ( lang ) ) {
9
- try {
10
- return hljs . highlight ( code , { language : lang } ) . value ;
11
- } catch ( err ) {
12
- console . error ( "Error highlighting code:" , err ) ;
13
- }
14
- }
15
- return code ; // Use the original code if language isn't found
16
- }
17
- } ) ) ;
18
-
19
- const renderer = new marked . Renderer ( ) ;
20
-
21
- // Store the original link renderer
22
- const originalLinkRenderer = renderer . link . bind ( renderer ) ;
23
-
24
- // Customize the link renderer to add icons
25
- renderer . link = ( { href, title, text } : Link ) => {
26
- if ( ! href ) return text ;
27
-
28
- const linkHtml = originalLinkRenderer ( { href, title, text } ) ;
29
-
30
- let iconSvg = '' ;
31
-
32
- if ( href . includes ( 'github.com' ) ) {
33
- // GitHub icon
34
- iconSvg = '<svg class="inline-block ml-1 w-4 h-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M9 19c-5 1.5-5-2.5-7-3m14 6v-3.87a3.37 3.37 0 0 0-.94-2.61c3.14-.35 6.44-1.54 6.44-7A5.44 5.44 0 0 0 20 4.77 5.07 5.07 0 0 0 19.91 1S18.73.65 16 2.48a13.38 13.38 0 0 0-7 0C6.27.65 5.09 1 5.09 1A5.07 5.07 0 0 0 5 4.77a5.44 5.44 0 0 0-1.5 3.78c0 5.42 3.3 6.61 6.44 7A3.37 3.37 0 0 0 9 18.13V22"></path></svg>' ;
35
- } else if ( href . includes ( 'wikipedia.org' ) ) {
36
- // Simple "W" icon for Wikipedia
37
- iconSvg = '<span class="inline-flex items-center justify-center ml-1 w-4 h-4 text-xs font-bold bg-gray-200 dark:bg-gray-700 rounded-full">W</span>' ;
38
- } else {
39
- // Generic external link icon for other links
40
- iconSvg = '<svg class="inline-block ml-1 w-4 h-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"></path><polyline points="15 3 21 3 21 9"></polyline><line x1="10" y1="14" x2="21" y2="3"></line></svg>' ;
3
+ import hljs from "highlight.js" ;
4
+
5
+ marked . setOptions ( {
6
+ gfm : true ,
7
+ breaks : true ,
8
+ silent : true ,
9
+ } ) ;
10
+
11
+ marked . use (
12
+ markedHighlight ( {
13
+ langPrefix : "hljs language-" ,
14
+ highlight ( code , lang , info ) {
15
+ lang = info . split ( "." ) . pop ( ) || lang ;
16
+ if ( lang == "shell" ) lang = "bash" ;
17
+ if ( lang == "result" ) lang = "markdown" ;
18
+ const language = hljs . getLanguage ( lang ) ? lang : "plaintext" ;
19
+ return hljs . highlight ( code , { language } ) . value ;
20
+ } ,
21
+ } )
22
+ ) ;
23
+
24
+ export function processNestedCodeBlocks ( content : string ) {
25
+ // If no code blocks or only one code block, return as-is
26
+ if ( content . split ( '```' ) . length < 3 ) {
27
+ const match = content . match ( / ` ` ` ( \S * ) / ) ;
28
+ return {
29
+ processedContent : content ,
30
+ langtags : match ? [ match [ 1 ] ] : [ ]
31
+ } ;
41
32
}
42
-
43
- // Insert the icon after the link
44
- return linkHtml . slice ( 0 , - 4 ) + iconSvg + '</a>' ;
45
- } ;
46
33
47
- export function processNestedCodeBlocks ( content : string ) : { processedContent : string ; langtags : string [ ] } {
34
+ const lines = content . split ( '\n' ) ;
48
35
const langtags : string [ ] = [ ] ;
49
- const codeBlockRegex = / ` ` ` ( \w + ) ? \n ( [ \s \S ] * ?) ` ` ` / g;
50
- let match ;
51
- let lastIndex = 0 ;
52
- let processedContent = '' ;
53
-
54
- while ( ( match = codeBlockRegex . exec ( content ) ) !== null ) {
55
- const [ fullMatch , lang ] = match ;
56
- if ( lang ) langtags . push ( lang ) ;
57
- processedContent += content . slice ( lastIndex , match . index ) + fullMatch ;
58
- lastIndex = match . index + fullMatch . length ;
36
+ const result : string [ ] = [ ] ;
37
+
38
+ for ( let i = 0 ; i < lines . length ; i ++ ) {
39
+ const line = lines [ i ] ;
40
+ const strippedLine = line . trim ( ) ;
41
+
42
+ if ( strippedLine . startsWith ( '```' ) ) {
43
+ if ( strippedLine !== '```' ) {
44
+ // Start of a code block with a language
45
+ const lang = strippedLine . slice ( 3 ) ;
46
+ langtags . push ( lang ) ;
47
+ }
48
+ result . push ( line ) ;
49
+ } else {
50
+ result . push ( line ) ;
51
+ }
59
52
}
60
53
61
- processedContent += content . slice ( lastIndex ) ;
62
- return { processedContent, langtags } ;
54
+ return {
55
+ processedContent : result . join ( '\n' ) ,
56
+ langtags : langtags . filter ( Boolean )
57
+ } ;
63
58
}
64
59
65
- export function transformThinkingTags ( content : string ) : string {
60
+ export function transformThinkingTags ( content : string ) {
61
+ if ( content . startsWith ( '`' ) && content . endsWith ( '`' ) ) {
62
+ return content ;
63
+ }
64
+
66
65
return content . replace (
67
66
/ < t h i n k i n g > ( [ \s \S ] * ?) < \/ t h i n k i n g > / g,
68
- '<details><summary>💭 Thinking</summary>\n\n$1\n\n</details>'
67
+ ( _match : string , thinkingContent : string ) =>
68
+ `<details><summary>💭 Thinking</summary>\n\n${ thinkingContent } \n\n</details>`
69
+ ) ;
70
+ }
71
+
72
+ export function parseMarkdownContent ( content : string ) {
73
+ const processedContent = transformThinkingTags ( content ) ;
74
+ const { processedContent : transformedContent , langtags } = processNestedCodeBlocks ( processedContent ) ;
75
+
76
+ let parsedResult = marked . parse ( transformedContent , {
77
+ async : false ,
78
+ } ) ;
79
+
80
+ parsedResult = parsedResult . replace (
81
+ / < p r e > < c o d e (?: \s + c l a s s = " ( [ ^ " ] + ) " ) ? > ( [ ^ ] * ?) < \/ c o d e > < \/ p r e > / g,
82
+ ( _ , classes = "" , code ) => {
83
+ const langtag_fallback = ( ( classes || "" ) . split ( " " ) [ 1 ] || "Code" ) . replace ( "language-" , "" ) ;
84
+ const langtag = langtags ?. shift ( ) || langtag_fallback ;
85
+ const emoji = getCodeBlockEmoji ( langtag ) ;
86
+ return `
87
+ <details>
88
+ <summary>${ emoji } ${ langtag } </summary>
89
+ <pre><code class="${ classes } ">${ code } </code></pre>
90
+ </details>
91
+ ` ;
92
+ }
69
93
) ;
94
+
95
+ return parsedResult ;
96
+ }
97
+
98
+ function getCodeBlockEmoji ( langtag : string ) : string {
99
+ if ( isPath ( langtag ) ) return "📄" ;
100
+ if ( isTool ( langtag ) ) return "🛠️" ;
101
+ if ( isOutput ( langtag ) ) return "📤" ;
102
+ if ( isWrite ( langtag ) ) return "📝" ;
103
+ return "💻" ;
70
104
}
71
105
72
- export function parseMarkdownContent ( content : string ) : string {
73
- const transformedContent = transformThinkingTags ( content ) ;
74
- return marked ( transformedContent , { renderer } ) ;
75
- }
106
+ function isPath ( langtag : string ) : boolean {
107
+ return ( langtag . includes ( "/" ) || langtag . includes ( "\\" ) || langtag . includes ( "." ) ) && langtag . split ( " " ) . length === 1 ;
108
+ }
109
+
110
+ function isTool ( langtag : string ) : boolean {
111
+ return [ "ipython" , "shell" , "tmux" ] . includes ( langtag . split ( " " ) [ 0 ] . toLowerCase ( ) ) ;
112
+ }
113
+
114
+ function isOutput ( langtag : string ) : boolean {
115
+ return [ "stdout" , "stderr" , "result" ] . includes ( langtag . toLowerCase ( ) ) ;
116
+ }
117
+
118
+ function isWrite ( langtag : string ) : boolean {
119
+ return [ "save" , "patch" , "append" ] . includes ( langtag . split ( " " ) [ 0 ] . toLowerCase ( ) ) ;
120
+ }
0 commit comments