@@ -15,84 +15,11 @@ export function escapeHtml(unsafe: string): string {
1515 . replace ( / ' / g, ''' ) ;
1616}
1717
18- /**
19- * Escape special characters in a string for use in a regular expression
20- */
21- function escapeRegExp ( string : string ) : string {
22- return string . replace ( / [ . * + ? ^ $ { } ( ) | [ \] \\ ] / g, '\\$&' ) ;
23- }
24-
25- /**
26- * Process code blocks in markdown text
27- */
28- function processCodeBlocks ( markdown : string ) : { html : string ; codeBlocks : { [ key : string ] : string } } {
29- const codeBlocks : { [ key : string ] : string } = { } ;
30- let codeBlockCounter = 0 ;
31-
32- // Replace code blocks with placeholders
33- const processedHtml = markdown . replace (
34- / ` ` ` ( [ \w - ] * ) \n ( [ \s \S ] * ?) ` ` ` / gm,
35- ( match , language , code ) => {
36- // Create a unique placeholder that won't appear in normal text
37- const placeholder = `__CODEBLOCK_${ Math . random ( ) . toString ( 36 ) . substring ( 2 ) } _${ codeBlockCounter } __` ;
38- const escapedCode = escapeHtml ( code . trim ( ) ) ;
39- const langClass = language ? `language-${ language } ` : '' ;
40- const codeId = `code-${ Math . random ( ) . toString ( 36 ) . substring ( 2 , 9 ) } ` ;
41-
42- // Only add the language label if a language is specified
43- const languageLabel = language ? `<span class="text-xs font-mono">${ language } </span>` : '' ;
44-
45- // Log the code content being set in the data-code attribute
46- console . log ( 'Setting data-code attribute with code:' , code ) ;
47-
48- codeBlocks [ placeholder ] = `<div class="relative my-6 rounded-lg overflow-hidden bg-gray-800 shadow-lg">
49- <button class="copy-code-button absolute top-2 right-2 text-gray-400 hover:text-white transition-colors px-2 py-1 rounded text-sm flex items-center" data-code="${ code . replace ( / " / g, '"' ) } " aria-label="Copy code to clipboard">
50- <svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 mr-1" fill="none" viewBox="0 0 24 24" stroke="currentColor">
51- <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z" />
52- </svg>
53- </button>
54- <pre id="${ codeId } " class="p-4 overflow-x-auto text-gray-300 text-sm"><code class="${ langClass } ">${ escapedCode } </code></pre>
55- </div>` ;
56- codeBlockCounter ++ ;
57- return placeholder ;
58- }
59- ) ;
60-
61- return { html : processedHtml , codeBlocks } ;
62- }
63-
64- /**
65- * Process inline code in markdown text
66- */
67- function processInlineCode ( markdown : string ) : { html : string ; inlineCode : { [ key : string ] : string } } {
68- const inlineCode : { [ key : string ] : string } = { } ;
69- let inlineCounter = 0 ;
70-
71- // Replace inline code with placeholders
72- const processedHtml = markdown . replace ( / (?< ! ` ) ` ( [ ^ ` \n ] + ?) ` (? ! ` ) / g, ( match , code ) => {
73- const placeholder = `__INLINE_${ Math . random ( ) . toString ( 36 ) . substring ( 2 ) } _${ inlineCounter } __` ;
74- inlineCode [ placeholder ] = `<code class="bg-gray-100 px-1 py-0.5 rounded text-sm font-mono">${ escapeHtml ( code ) } </code>` ;
75- inlineCounter ++ ;
76- return placeholder ;
77- } ) ;
78-
79- return { html : processedHtml , inlineCode } ;
80- }
81-
8218/**
8319 * Renders markdown text into styled HTML
8420 */
8521export const renderMarkdown = ( markdown : string ) : string => {
86- // Normalize line endings to \n
87- markdown = markdown . replace ( / \r \n / g, '\n' ) ;
88-
89- // First, process code blocks and store them safely
90- const { html : htmlWithCodeBlockPlaceholders , codeBlocks } = processCodeBlocks ( markdown ) ;
91-
92- // Then, process inline code and store them safely
93- const { html : htmlWithAllPlaceholders , inlineCode } = processInlineCode ( htmlWithCodeBlockPlaceholders ) ;
94-
95- let html = htmlWithAllPlaceholders ;
22+ let html = markdown ;
9623
9724 // Create IDs for headers to enable anchor links
9825 const headerToId = ( text : string ) => {
@@ -102,7 +29,78 @@ export const renderMarkdown = (markdown: string): string => {
10229 . replace ( / ( ^ - | - $ ) / g, '' ) ;
10330 } ;
10431
105- // Process headers
32+ // Process code blocks - Process this first to avoid conflicts with other markdown elements
33+ html = html . replace (
34+ / ` ` ` ( [ \w - ] * ) \s * ( [ \s \S ] * ?) ` ` ` / gm,
35+ function ( _match , _language , codeContent ) {
36+ // Prepare the code content for display
37+ const escapedCode = escapeHtml ( codeContent . trim ( ) ) ;
38+
39+ // Build the HTML for the code block without copy button
40+ return `
41+ <div class="relative rounded-lg overflow-hidden shadow-lg bg-gray-900">
42+ <div class="px-2 overflow-x-auto">
43+ <pre class="m-0 p-0"><code class="font-mono text-sm text-gray-200 block whitespace-pre overflow-visible">${ escapedCode } </code></pre>
44+ </div>
45+ </div>
46+ ` ;
47+ } ,
48+ ) ;
49+
50+ // Process blockquotes
51+ html = html . replace ( / ( ^ > .* ( \n > .* ) * ) / gm, function ( match ) {
52+ const lines = match . split ( '\n' ) ;
53+ let content = '' ;
54+
55+ lines . forEach ( ( line ) => {
56+ const match = line . match ( / ^ > ( > * ) \s ? ( .* ) $ / ) ;
57+ if ( match ) {
58+ const nestedChars = match [ 1 ] ;
59+ const lineContent = match [ 2 ] ;
60+ const nestLevel = nestedChars . length + 1 ;
61+
62+ const borderColorClasses = [
63+ 'border-blue-300 bg-blue-50' ,
64+ 'border-purple-300 bg-purple-50' ,
65+ 'border-green-300 bg-green-50' ,
66+ 'border-orange-300 bg-orange-50' ,
67+ ] ;
68+
69+ const borderClass =
70+ borderColorClasses [ ( nestLevel - 1 ) % borderColorClasses . length ] ;
71+ const marginClass = nestLevel > 1 ? 'ml-4' : '' ;
72+ const contentToAdd = lineContent . trim ( ) === '' ? '<br>' : lineContent ;
73+ if ( content === '' ) {
74+ content = `<blockquote class="border-l-4 ${ borderClass } ${ marginClass } pl-4 py-2 my-4 italic text-gray-700 rounded-r-md">${ contentToAdd } ` ;
75+ } else {
76+ content += `<br>${ contentToAdd } ` ;
77+ }
78+ }
79+ } ) ;
80+ if ( content !== '' ) {
81+ content += '</blockquote>' ;
82+ }
83+
84+ return content ;
85+ } ) ;
86+
87+ html = html . replace (
88+ / ! \[ ( .* ?) \] \( ( .* ?) (?: \s " ( .* ?) " ) ? \) / gim,
89+ function ( _ , alt , src , title ) {
90+ const caption = title
91+ ? `<figcaption class="text-center text-sm text-gray-600 mt-2">${ title } </figcaption>`
92+ : '' ;
93+
94+ return `<figure class="flex flex-col items-center my-6">
95+ <div class="overflow-hidden rounded-lg shadow-md hover:shadow-lg transition-shadow duration-300">
96+ <img src="${ src } " alt="${ alt } " class="max-w-full sm:max-w-md md:max-w-lg rounded-lg object-contain max-h-80 hover:scale-105 transition-transform duration-300" loading="lazy" data-zoomable="true" />
97+ </div>
98+ ${ caption }
99+ </figure>` ;
100+ } ,
101+ ) ;
102+
103+ // Convert headers with anchor links
106104 html = html . replace ( / ^ # # # ( .* $ ) / gim, ( _ , title ) => {
107105 const id = headerToId ( title ) ;
108106 return `<h3 id="${ id } " class="text-xl font-bold my-4 text-gray-800 group flex items-center">
@@ -136,7 +134,7 @@ export const renderMarkdown = (markdown: string): string => {
136134 <a aria-hidden="true" tabindex="-1" href="#${ id } " class="ml-2 text-blue-500 opacity-0 group-hover:opacity-100 transition-opacity">
137135 <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor" viewBox="0 0 16 16">
138136 <path d="M4.715 6.542 3.343 7.914a3 3 0 1 0 4.243 4.243l1.828-1.829A3 3 0 0 0 8.586 5.5L8 6.086a1.002 1.002 0 0 0-.154.199 2 2 0 0 1 .861 3.337L6.88 11.45a2 2 0 1 1-2.83-2.83l.793-.792a4.018 4.018 0 0 1-.128-1.287z"/>
139- <path d="M6.586 4.672A3 3 0 0 0 7.414 9.5l.775-.776a2 2 0 0 1-.896-3.346L9.12 3.55a2 2 0 1 1 2.83- 2.83l-.793.792c.112.42.155.855.128 1.287l1.372-1.372a3 3 0 1 0-4.243-4.243L6.586 4.672z"/>
137+ <path d="M6.586 4.672A3 3 0 0 0 7.414 9.5l.775-.776a2 2 0 0 1-.896-3.346L9.12 3.55a2 2 0 1 1 2.83 2.83l-.793.792c.112.42.155.855.128 1.287l1.372-1.372a3 3 0 1 0-4.243-4.243L6.586 4.672z"/>
140138 </svg>
141139 </a>
142140 </h1>` ;
@@ -179,6 +177,12 @@ export const renderMarkdown = (markdown: string): string => {
179177 '<a href="$2" class="text-blue-600 hover:underline transition-colors duration-200">$1</a>' ,
180178 ) ;
181179
180+ // Convert inline code - Make sure this comes after code blocks
181+ html = html . replace (
182+ / ` ( [ ^ ` ] + ) ` / gim,
183+ '<code class="bg-gray-100 px-1 py-0.5 rounded text-sm font-mono">$1</code>' ,
184+ ) ;
185+
182186 // Improved table handling
183187 const tableRegex = / ^ \| ( .+ ) \| ( \r ? \n \| [ - | \s ] + \| ) ( \r ? \n \| .+ \| ) + / gm;
184188 html = html . replace ( tableRegex , function ( match ) {
@@ -217,7 +221,6 @@ export const renderMarkdown = (markdown: string): string => {
217221 tableHtml += '</tbody></table></div>' ;
218222 return tableHtml ;
219223 } ) ;
220-
221224 html = html . replace (
222225 / ^ \s * - \[ ( [ x X ] ) \] ( .+ ) $ / gim,
223226 function ( _ , checked , content ) {
@@ -311,15 +314,6 @@ export const renderMarkdown = (markdown: string): string => {
311314 '<p class="my-4 text-gray-700 leading-relaxed">$1</p>' ,
312315 ) ;
313316
314- // After processing all other elements, restore code blocks and inline code
315- Object . entries ( codeBlocks ) . forEach ( ( [ placeholder , codeHtml ] ) => {
316- html = html . replace ( new RegExp ( escapeRegExp ( placeholder ) , 'g' ) , codeHtml ) ;
317- } ) ;
318-
319- Object . entries ( inlineCode ) . forEach ( ( [ placeholder , codeHtml ] ) => {
320- html = html . replace ( new RegExp ( escapeRegExp ( placeholder ) , 'g' ) , codeHtml ) ;
321- } ) ;
322-
323317 // Style first paragraph's first letter (if it exists)
324318 html = html . replace (
325319 / < p c l a s s = " m y - 4 t e x t - g r a y - 7 0 0 l e a d i n g - r e l a x e d " > ( \w ) / ,
@@ -353,50 +347,3 @@ export const renderMarkdown = (markdown: string): string => {
353347
354348 return html ;
355349} ;
356-
357- /**
358- * Initialize syntax highlighting for code blocks if a syntax highlighting library is available
359- * This function should be called after the DOM is loaded
360- */
361- export function initializeSyntaxHighlighting ( ) : void {
362- // Check if Prism or Highlight.js is available
363- const hasPrism = typeof ( window as any ) . Prism !== 'undefined' ;
364- const hasHighlightJs = typeof ( window as any ) . hljs !== 'undefined' ;
365-
366- if ( hasPrism ) {
367- // If Prism is available, let it automatically highlight all code blocks
368- // Prism does this automatically for elements with language-* classes
369- try {
370- ( window as any ) . Prism . highlightAll ( ) ;
371- } catch ( error ) {
372- console . error ( 'Error initializing Prism syntax highlighting:' , error ) ;
373- }
374- } else if ( hasHighlightJs ) {
375- // If Highlight.js is available, manually highlight each code block
376- try {
377- document . querySelectorAll ( 'pre code' ) . forEach ( ( block ) => {
378- ( window as any ) . hljs . highlightBlock ( block ) ;
379- } ) ;
380- } catch ( error ) {
381- console . error ( 'Error initializing Highlight.js syntax highlighting:' , error ) ;
382- }
383- }
384- }
385-
386- /**
387- * Initialize all markdown-related features
388- * This function should be called after the DOM is loaded
389- */
390- export function initializeMarkdownFeatures ( ) : void {
391- // Import and initialize code copy functionality
392- document . addEventListener ( 'DOMContentLoaded' , ( ) => {
393- import ( './copy-code' ) . then ( ( { initializeCodeCopy } ) => {
394- initializeCodeCopy ( ) ;
395- } ) . catch ( error => {
396- console . error ( 'Error initializing code copy functionality:' , error ) ;
397- } ) ;
398-
399- // Initialize syntax highlighting
400- initializeSyntaxHighlighting ( ) ;
401- } ) ;
402- }
0 commit comments