@@ -7,10 +7,55 @@ type SimpleSource = {
77 title ?: string ;
88 link : string ;
99} ;
10- import hljs from "highlight.js" ;
10+ import hljs from "highlight.js/lib/core" ;
11+ import type { LanguageFn } from "highlight.js" ;
12+ import javascript from "highlight.js/lib/languages/javascript" ;
13+ import typescript from "highlight.js/lib/languages/typescript" ;
14+ import json from "highlight.js/lib/languages/json" ;
15+ import bash from "highlight.js/lib/languages/bash" ;
16+ import shell from "highlight.js/lib/languages/shell" ;
17+ import python from "highlight.js/lib/languages/python" ;
18+ import go from "highlight.js/lib/languages/go" ;
19+ import rust from "highlight.js/lib/languages/rust" ;
20+ import java from "highlight.js/lib/languages/java" ;
21+ import csharp from "highlight.js/lib/languages/csharp" ;
22+ import cpp from "highlight.js/lib/languages/cpp" ;
23+ import cLang from "highlight.js/lib/languages/c" ;
24+ import xml from "highlight.js/lib/languages/xml" ;
25+ import css from "highlight.js/lib/languages/css" ;
26+ import scss from "highlight.js/lib/languages/scss" ;
27+ import markdownLang from "highlight.js/lib/languages/markdown" ;
28+ import yaml from "highlight.js/lib/languages/yaml" ;
29+ import sql from "highlight.js/lib/languages/sql" ;
30+ import plaintext from "highlight.js/lib/languages/plaintext" ;
1131import { parseIncompleteMarkdown } from "./parseIncompleteMarkdown" ;
1232import { parseMarkdownIntoBlocks } from "./parseBlocks" ;
1333
34+ const bundledLanguages : [ string , LanguageFn ] [ ] = [
35+ [ "javascript" , javascript ] ,
36+ [ "typescript" , typescript ] ,
37+ [ "json" , json ] ,
38+ [ "bash" , bash ] ,
39+ [ "shell" , shell ] ,
40+ [ "python" , python ] ,
41+ [ "go" , go ] ,
42+ [ "rust" , rust ] ,
43+ [ "java" , java ] ,
44+ [ "csharp" , csharp ] ,
45+ [ "cpp" , cpp ] ,
46+ [ "c" , cLang ] ,
47+ [ "xml" , xml ] ,
48+ [ "html" , xml ] ,
49+ [ "css" , css ] ,
50+ [ "scss" , scss ] ,
51+ [ "markdown" , markdownLang ] ,
52+ [ "yaml" , yaml ] ,
53+ [ "sql" , sql ] ,
54+ [ "plaintext" , plaintext ] ,
55+ ] ;
56+
57+ bundledLanguages . forEach ( ( [ name , language ] ) => hljs . registerLanguage ( name , language ) ) ;
58+
1459interface katexBlockToken extends Tokens . Generic {
1560 type : "katexBlock" ;
1661 raw : string ;
@@ -159,15 +204,40 @@ function addInlineCitations(md: string, webSearchSources: SimpleSource[] = []):
159204 } ) ;
160205}
161206
207+ function sanitizeHref ( href ?: string | null ) : string | undefined {
208+ if ( ! href ) return undefined ;
209+ const trimmed = href . trim ( ) ;
210+ const lower = trimmed . toLowerCase ( ) ;
211+ if ( lower . startsWith ( "javascript:" ) || lower . startsWith ( "data:text/html" ) ) {
212+ return undefined ;
213+ }
214+ return trimmed . replace ( / > $ / , "" ) ;
215+ }
216+
217+ function highlightCode ( text : string , lang ?: string ) : string {
218+ if ( lang && hljs . getLanguage ( lang ) ) {
219+ try {
220+ return hljs . highlight ( text , { language : lang , ignoreIllegals : true } ) . value ;
221+ } catch {
222+ // fall through to auto-detect
223+ }
224+ }
225+ return hljs . highlightAuto ( text ) . value ;
226+ }
227+
162228function createMarkedInstance ( sources : SimpleSource [ ] ) : Marked {
163229 return new Marked ( {
164230 hooks : {
165231 postprocess : ( html ) => addInlineCitations ( html , sources ) ,
166232 } ,
167233 extensions : [ katexBlockExtension , katexInlineExtension ] ,
168234 renderer : {
169- link : ( href , title , text ) =>
170- `<a href="${ href ?. replace ( / > $ / , "" ) } " target="_blank" rel="noreferrer">${ text } </a>` ,
235+ link : ( href , title , text ) => {
236+ const safeHref = sanitizeHref ( href ) ;
237+ return safeHref
238+ ? `<a href="${ safeHref } " target="_blank" rel="noreferrer">${ text } </a>`
239+ : `<span>${ escapeHTML ( text ?? "" ) } </span>` ;
240+ } ,
171241 html : ( html ) => escapeHTML ( html ) ,
172242 } ,
173243 gfm : true ,
@@ -200,6 +270,13 @@ type TextToken = {
200270 html : string | Promise < string > ;
201271} ;
202272
273+ const blockCache = new Map < string , BlockToken > ( ) ;
274+
275+ function cacheKey ( index : number , blockContent : string , sources : SimpleSource [ ] ) {
276+ const sourceKey = sources . map ( ( s ) => s . link ) . join ( "|" ) ;
277+ return `${ index } -${ hashString ( blockContent ) } |${ sourceKey } ` ;
278+ }
279+
203280export async function processTokens ( content : string , sources : SimpleSource [ ] ) : Promise < Token [ ] > {
204281 // Apply incomplete markdown preprocessing for smooth streaming
205282 const processedContent = parseIncompleteMarkdown ( content ) ;
@@ -213,7 +290,7 @@ export async function processTokens(content: string, sources: SimpleSource[]): P
213290 return {
214291 type : "code" as const ,
215292 lang : token . lang ,
216- code : hljs . highlightAuto ( token . text , hljs . getLanguage ( token . lang ) ?. aliases ) . value ,
293+ code : highlightCode ( token . text , token . lang ) ,
217294 rawCode : token . text ,
218295 isClosed : isFencedBlockClosed ( token . raw ?? "" ) ,
219296 } ;
@@ -240,7 +317,7 @@ export function processTokensSync(content: string, sources: SimpleSource[]): Tok
240317 return {
241318 type : "code" as const ,
242319 lang : token . lang ,
243- code : hljs . highlightAuto ( token . text , hljs . getLanguage ( token . lang ) ?. aliases ) . value ,
320+ code : highlightCode ( token . text , token . lang ) ,
244321 rawCode : token . text ,
245322 isClosed : isFencedBlockClosed ( token . raw ?? "" ) ,
246323 } ;
@@ -282,12 +359,18 @@ export async function processBlocks(
282359
283360 return await Promise . all (
284361 blocks . map ( async ( blockContent , index ) => {
362+ const key = cacheKey ( index , blockContent , sources ) ;
363+ const cached = blockCache . get ( key ) ;
364+ if ( cached ) return cached ;
365+
285366 const tokens = await processTokens ( blockContent , sources ) ;
286- return {
367+ const block : BlockToken = {
287368 id : `${ index } -${ hashString ( blockContent ) } ` ,
288369 content : blockContent ,
289370 tokens,
290371 } ;
372+ blockCache . set ( key , block ) ;
373+ return block ;
291374 } )
292375 ) ;
293376}
@@ -299,11 +382,17 @@ export function processBlocksSync(content: string, sources: SimpleSource[] = [])
299382 const blocks = parseMarkdownIntoBlocks ( content ) ;
300383
301384 return blocks . map ( ( blockContent , index ) => {
385+ const key = cacheKey ( index , blockContent , sources ) ;
386+ const cached = blockCache . get ( key ) ;
387+ if ( cached ) return cached ;
388+
302389 const tokens = processTokensSync ( blockContent , sources ) ;
303- return {
390+ const block : BlockToken = {
304391 id : `${ index } -${ hashString ( blockContent ) } ` ,
305392 content : blockContent ,
306393 tokens,
307394 } ;
395+ blockCache . set ( key , block ) ;
396+ return block ;
308397 } ) ;
309398}
0 commit comments