@@ -15,11 +15,84 @@ 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+
1882/**
1983 * Renders markdown text into styled HTML
2084 */
2185export const renderMarkdown = ( markdown : string ) : string => {
22- let html = markdown ;
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 ;
2396
2497 // Create IDs for headers to enable anchor links
2598 const headerToId = ( text : string ) => {
@@ -29,60 +102,7 @@ export const renderMarkdown = (markdown: string): string => {
29102 . replace ( / ( ^ - | - $ ) / g, '' ) ;
30103 } ;
31104
32- // Process blockquotes
33- html = html . replace ( / ( ^ > .* ( \n > .* ) * ) / gm, function ( match ) {
34- const lines = match . split ( '\n' ) ;
35- let content = '' ;
36-
37- lines . forEach ( ( line ) => {
38- const match = line . match ( / ^ > ( > * ) \s ? ( .* ) $ / ) ;
39- if ( match ) {
40- const nestedChars = match [ 1 ] ;
41- const lineContent = match [ 2 ] ;
42- const nestLevel = nestedChars . length + 1 ;
43-
44- const borderColorClasses = [
45- 'border-blue-300 bg-blue-50' ,
46- 'border-purple-300 bg-purple-50' ,
47- 'border-green-300 bg-green-50' ,
48- 'border-orange-300 bg-orange-50' ,
49- ] ;
50-
51- const borderClass =
52- borderColorClasses [ ( nestLevel - 1 ) % borderColorClasses . length ] ;
53- const marginClass = nestLevel > 1 ? 'ml-4' : '' ;
54- const contentToAdd = lineContent . trim ( ) === '' ? '<br>' : lineContent ;
55- if ( content === '' ) {
56- content = `<blockquote class="border-l-4 ${ borderClass } ${ marginClass } pl-4 py-2 my-4 italic text-gray-700 rounded-r-md">${ contentToAdd } ` ;
57- } else {
58- content += `<br>${ contentToAdd } ` ;
59- }
60- }
61- } ) ;
62- if ( content !== '' ) {
63- content += '</blockquote>' ;
64- }
65-
66- return content ;
67- } ) ;
68-
69- html = html . replace (
70- / ! \[ ( .* ?) \] \( ( .* ?) (?: \s " ( .* ?) " ) ? \) / gim,
71- function ( _ , alt , src , title ) {
72- const caption = title
73- ? `<figcaption class="text-center text-sm text-gray-600 mt-2">${ title } </figcaption>`
74- : '' ;
75-
76- return `<figure class="flex flex-col items-center my-6">
77- <div class="overflow-hidden rounded-lg shadow-md hover:shadow-lg transition-shadow duration-300">
78- <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" />
79- </div>
80- ${ caption }
81- </figure>` ;
82- } ,
83- ) ;
84-
85- // Convert headers with anchor links
105+ // Process headers
86106 html = html . replace ( / ^ # # # ( .* $ ) / gim, ( _ , title ) => {
87107 const id = headerToId ( title ) ;
88108 return `<h3 id="${ id } " class="text-xl font-bold my-4 text-gray-800 group flex items-center">
@@ -116,7 +136,7 @@ export const renderMarkdown = (markdown: string): string => {
116136 <a aria-hidden="true" tabindex="-1" href="#${ id } " class="ml-2 text-blue-500 opacity-0 group-hover:opacity-100 transition-opacity">
117137 <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor" viewBox="0 0 16 16">
118138 <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"/>
119- <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"/>
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"/>
120140 </svg>
121141 </a>
122142 </h1>` ;
@@ -159,12 +179,6 @@ export const renderMarkdown = (markdown: string): string => {
159179 '<a href="$2" class="text-blue-600 hover:underline transition-colors duration-200">$1</a>' ,
160180 ) ;
161181
162- // Convert inline code
163- html = html . replace (
164- / ` ( .* ?) ` / gim,
165- '<code class="bg-gray-100 px-1 py-0.5 rounded text-sm font-mono">$1</code>' ,
166- ) ;
167-
168182 // Improved table handling
169183 const tableRegex = / ^ \| ( .+ ) \| ( \r ? \n \| [ - | \s ] + \| ) ( \r ? \n \| .+ \| ) + / gm;
170184 html = html . replace ( tableRegex , function ( match ) {
@@ -203,6 +217,7 @@ export const renderMarkdown = (markdown: string): string => {
203217 tableHtml += '</tbody></table></div>' ;
204218 return tableHtml ;
205219 } ) ;
220+
206221 html = html . replace (
207222 / ^ \s * - \[ ( [ x X ] ) \] ( .+ ) $ / gim,
208223 function ( _ , checked , content ) {
@@ -296,6 +311,15 @@ export const renderMarkdown = (markdown: string): string => {
296311 '<p class="my-4 text-gray-700 leading-relaxed">$1</p>' ,
297312 ) ;
298313
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+
299323 // Style first paragraph's first letter (if it exists)
300324 html = html . replace (
301325 / < 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 ) / ,
@@ -329,3 +353,50 @@ export const renderMarkdown = (markdown: string): string => {
329353
330354 return html ;
331355} ;
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