@@ -31,6 +31,14 @@ import sql from "highlight.js/lib/languages/sql";
3131import plaintext from "highlight.js/lib/languages/plaintext" ;
3232import { parseIncompleteMarkdown } from "./parseIncompleteMarkdown" ;
3333import { parseMarkdownIntoBlocks } from "./parseBlocks" ;
34+ import { escapeHTML , type BlockToken , type Token } from "./markedLight" ;
35+
36+ // Re-export the light pieces so existing consumers of this module (the worker,
37+ // tests) keep a single import site. Main-thread code must import them from
38+ // ./markedLight directly — importing this module eagerly pulls KaTeX +
39+ // highlight.js into the entry bundle.
40+ export { escapeHTML , fallbackBlocks } from "./markedLight" ;
41+ export type { BlockToken , CodeToken , TextToken , Token } from "./markedLight" ;
3442
3543const bundledLanguages : [ string , LanguageFn ] [ ] = [
3644 [ "javascript" , javascript ] ,
@@ -227,20 +235,6 @@ const katexInlineExtension: TokenizerExtension & RendererExtension = {
227235 } ,
228236} ;
229237
230- function escapeHTML ( content : string ) {
231- return content . replace (
232- / [ < > & " ' ] / g,
233- ( x ) =>
234- ( {
235- "<" : "<" ,
236- ">" : ">" ,
237- "&" : "&" ,
238- "'" : "'" ,
239- '"' : """ ,
240- } ) [ x ] || x
241- ) ;
242- }
243-
244238function addInlineCitations ( md : string , webSearchSources : SimpleSource [ ] = [ ] ) : string {
245239 const linkStyle =
246240 "color: rgb(59, 130, 246); text-decoration: none; hover:text-decoration: underline;" ;
@@ -421,19 +415,6 @@ function isFencedBlockClosed(raw?: string): boolean {
421415 return closingFencePattern . test ( trimmed ) ;
422416}
423417
424- type CodeToken = {
425- type : "code" ;
426- lang : string ;
427- code : string ;
428- rawCode : string ;
429- isClosed : boolean ;
430- } ;
431-
432- type TextToken = {
433- type : "text" ;
434- html : string | Promise < string > ;
435- } ;
436-
437418const blockCache = new Map < string , BlockToken > ( ) ;
438419
439420// The markdown worker pool keeps a small number of long-lived workers alive for the whole
@@ -499,14 +480,6 @@ export function processTokensSync(content: string, sources: SimpleSource[]): Tok
499480 } ) ;
500481}
501482
502- export type Token = CodeToken | TextToken ;
503-
504- export type BlockToken = {
505- id : string ;
506- content : string ;
507- tokens : Token [ ] ;
508- } ;
509-
510483/**
511484 * Simple hash function for generating stable block IDs
512485 */
@@ -555,37 +528,6 @@ export async function processBlocks(
555528 ) ;
556529}
557530
558- /**
559- * Cheap, allocation-light blocks used for SSR and the initial client render.
560- *
561- * Rich markdown rendering (marked + highlight.js + KaTeX + DOMPurify/jsdom) is
562- * intentionally NOT run here: it executes synchronously on the single Node event loop
563- * during SSR and, summed across every message of a conversation, can block the loop
564- * long enough to fail liveness/readiness health checks (observed event-loop stalls up
565- * to ~1.5s). The browser upgrades each message to fully rendered markdown via the
566- * markdown worker (or async processBlocks fallback) on mount.
567- *
568- * The output is deterministic and identical on server and client, so hydration is not
569- * affected; only the first paint shows lightly-formatted text before the worker result
570- * arrives.
571- */
572- export function fallbackBlocks ( content : string ) : BlockToken [ ] {
573- // Static id: it is only used as the {#each} key for this single throwaway block and
574- // has no semantic meaning, so there is no need to hash the (potentially large) content.
575- return [
576- {
577- id : "fallback" ,
578- content,
579- tokens : [
580- {
581- type : "text" ,
582- html : `<div style="white-space:pre-wrap;overflow-wrap:anywhere">${ escapeHTML ( content ) } </div>` ,
583- } ,
584- ] ,
585- } ,
586- ] ;
587- }
588-
589531/**
590532 * Synchronous version of processBlocks. Kept for non-SSR callers and tests; it is no
591533 * longer used on the SSR render path (see fallbackBlocks).
0 commit comments