From 08e70b09958e948207b8bb3909cd6c45912764e1 Mon Sep 17 00:00:00 2001 From: Harshalvk Date: Mon, 10 Mar 2025 21:11:10 +0530 Subject: [PATCH 1/2] feat: add code-block for markdown --- src/utils/copy-code.ts | 51 ++++++++++ src/utils/mdparser-utils.ts | 195 ++++++++++++++++++++++++------------ 2 files changed, 184 insertions(+), 62 deletions(-) create mode 100644 src/utils/copy-code.ts diff --git a/src/utils/copy-code.ts b/src/utils/copy-code.ts new file mode 100644 index 00000000..b47f4dba --- /dev/null +++ b/src/utils/copy-code.ts @@ -0,0 +1,51 @@ +/** + * Initialize copy functionality for code blocks + */ +export function initializeCodeCopy() { + document.addEventListener('click', (e) => { + const target = e.target as HTMLElement; + const button = target.closest('.copy-code-button'); + + if (button) { + const preElement = button.parentElement?.querySelector('pre'); + if (preElement) { + const codeContent = preElement.textContent || ''; + navigator.clipboard.writeText(codeContent).then(() => { + // Change button text temporarily after copying + button.innerHTML = ` + + + + + Copied! + + `; + button.classList.add('text-green-400'); + + // Reset button after 2 seconds + setTimeout(() => { + button.innerHTML = 'Copy'; + button.classList.remove('text-green-400'); + }, 2000); + }).catch(() => { + // Show error state + button.innerHTML = ` + + + + + Error! + + `; + button.classList.add('text-red-400'); + + // Reset button after 2 seconds + setTimeout(() => { + button.innerHTML = 'Copy'; + button.classList.remove('text-red-400'); + }, 2000); + }); + } + } + }); +} \ No newline at end of file diff --git a/src/utils/mdparser-utils.ts b/src/utils/mdparser-utils.ts index f68866de..8e720678 100644 --- a/src/utils/mdparser-utils.ts +++ b/src/utils/mdparser-utils.ts @@ -15,11 +15,84 @@ export function escapeHtml(unsafe: string): string { .replace(/'/g, '''); } +/** + * Escape special characters in a string for use in a regular expression + */ +function escapeRegExp(string: string): string { + return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); +} + +/** + * Process code blocks in markdown text + */ +function processCodeBlocks(markdown: string): { html: string; codeBlocks: { [key: string]: string } } { + const codeBlocks: { [key: string]: string } = {}; + let codeBlockCounter = 0; + + // Replace code blocks with placeholders + const processedHtml = markdown.replace( + /```([\w-]*)\n([\s\S]*?)```/gm, + (match, language, code) => { + // Create a unique placeholder that won't appear in normal text + const placeholder = `__CODEBLOCK_${Math.random().toString(36).substring(2)}_${codeBlockCounter}__`; + const escapedCode = escapeHtml(code.trim()); + const langClass = language ? `language-${language}` : ''; + const codeId = `code-${Math.random().toString(36).substring(2, 9)}`; + + // Only add the language label if a language is specified + const languageLabel = language ? `${language}` : ''; + + // Log the code content being set in the data-code attribute + console.log('Setting data-code attribute with code:', code); + + codeBlocks[placeholder] = `
+ +
${escapedCode}
+
`; + codeBlockCounter++; + return placeholder; + } + ); + + return { html: processedHtml, codeBlocks }; +} + +/** + * Process inline code in markdown text + */ +function processInlineCode(markdown: string): { html: string; inlineCode: { [key: string]: string } } { + const inlineCode: { [key: string]: string } = {}; + let inlineCounter = 0; + + // Replace inline code with placeholders + const processedHtml = markdown.replace(/(? { + const placeholder = `__INLINE_${Math.random().toString(36).substring(2)}_${inlineCounter}__`; + inlineCode[placeholder] = `${escapeHtml(code)}`; + inlineCounter++; + return placeholder; + }); + + return { html: processedHtml, inlineCode }; +} + /** * Renders markdown text into styled HTML */ export const renderMarkdown = (markdown: string): string => { - let html = markdown; + // Normalize line endings to \n + markdown = markdown.replace(/\r\n/g, '\n'); + + // First, process code blocks and store them safely + const { html: htmlWithCodeBlockPlaceholders, codeBlocks } = processCodeBlocks(markdown); + + // Then, process inline code and store them safely + const { html: htmlWithAllPlaceholders, inlineCode } = processInlineCode(htmlWithCodeBlockPlaceholders); + + let html = htmlWithAllPlaceholders; // Create IDs for headers to enable anchor links const headerToId = (text: string) => { @@ -29,60 +102,7 @@ export const renderMarkdown = (markdown: string): string => { .replace(/(^-|-$)/g, ''); }; - // Process blockquotes - html = html.replace(/(^>.*(\n>.*)*)/gm, function (match) { - const lines = match.split('\n'); - let content = ''; - - lines.forEach((line) => { - const match = line.match(/^>(>*)\s?(.*)$/); - if (match) { - const nestedChars = match[1]; - const lineContent = match[2]; - const nestLevel = nestedChars.length + 1; - - const borderColorClasses = [ - 'border-blue-300 bg-blue-50', - 'border-purple-300 bg-purple-50', - 'border-green-300 bg-green-50', - 'border-orange-300 bg-orange-50', - ]; - - const borderClass = - borderColorClasses[(nestLevel - 1) % borderColorClasses.length]; - const marginClass = nestLevel > 1 ? 'ml-4' : ''; - const contentToAdd = lineContent.trim() === '' ? '
' : lineContent; - if (content === '') { - content = `
${contentToAdd}`; - } else { - content += `
${contentToAdd}`; - } - } - }); - if (content !== '') { - content += '
'; - } - - return content; - }); - - html = html.replace( - /!\[(.*?)\]\((.*?)(?:\s"(.*?)")?\)/gim, - function (_, alt, src, title) { - const caption = title - ? `
${title}
` - : ''; - - return `
-
- ${alt} -
- ${caption} -
`; - }, - ); - - // Convert headers with anchor links + // Process headers html = html.replace(/^### (.*$)/gim, (_, title) => { const id = headerToId(title); return `

@@ -116,7 +136,7 @@ export const renderMarkdown = (markdown: string): string => {

`; @@ -159,12 +179,6 @@ export const renderMarkdown = (markdown: string): string => { '$1', ); - // Convert inline code - html = html.replace( - /`(.*?)`/gim, - '$1', - ); - // Improved table handling const tableRegex = /^\|(.+)\|(\r?\n\|[-|\s]+\|)(\r?\n\|.+\|)+/gm; html = html.replace(tableRegex, function (match) { @@ -203,6 +217,7 @@ export const renderMarkdown = (markdown: string): string => { tableHtml += ''; return tableHtml; }); + html = html.replace( /^\s*- \[([ xX])\] (.+)$/gim, function (_, checked, content) { @@ -296,6 +311,15 @@ export const renderMarkdown = (markdown: string): string => { '

$1

', ); + // After processing all other elements, restore code blocks and inline code + Object.entries(codeBlocks).forEach(([placeholder, codeHtml]) => { + html = html.replace(new RegExp(escapeRegExp(placeholder), 'g'), codeHtml); + }); + + Object.entries(inlineCode).forEach(([placeholder, codeHtml]) => { + html = html.replace(new RegExp(escapeRegExp(placeholder), 'g'), codeHtml); + }); + // Style first paragraph's first letter (if it exists) html = html.replace( /

(\w)/, @@ -329,3 +353,50 @@ export const renderMarkdown = (markdown: string): string => { return html; }; + +/** + * Initialize syntax highlighting for code blocks if a syntax highlighting library is available + * This function should be called after the DOM is loaded + */ +export function initializeSyntaxHighlighting(): void { + // Check if Prism or Highlight.js is available + const hasPrism = typeof (window as any).Prism !== 'undefined'; + const hasHighlightJs = typeof (window as any).hljs !== 'undefined'; + + if (hasPrism) { + // If Prism is available, let it automatically highlight all code blocks + // Prism does this automatically for elements with language-* classes + try { + (window as any).Prism.highlightAll(); + } catch (error) { + console.error('Error initializing Prism syntax highlighting:', error); + } + } else if (hasHighlightJs) { + // If Highlight.js is available, manually highlight each code block + try { + document.querySelectorAll('pre code').forEach((block) => { + (window as any).hljs.highlightBlock(block); + }); + } catch (error) { + console.error('Error initializing Highlight.js syntax highlighting:', error); + } + } +} + +/** + * Initialize all markdown-related features + * This function should be called after the DOM is loaded + */ +export function initializeMarkdownFeatures(): void { + // Import and initialize code copy functionality + document.addEventListener('DOMContentLoaded', () => { + import('./copy-code').then(({ initializeCodeCopy }) => { + initializeCodeCopy(); + }).catch(error => { + console.error('Error initializing code copy functionality:', error); + }); + + // Initialize syntax highlighting + initializeSyntaxHighlighting(); + }); +} From db670b8518640f38c213329e42790bcd82e878a8 Mon Sep 17 00:00:00 2001 From: Harshalvk Date: Fri, 14 Mar 2025 15:06:25 +0530 Subject: [PATCH 2/2] Refactor markdown rendering: streamline code block processing and improve inline code handling --- src/utils/copy-code.ts | 51 --------- src/utils/mdparser-utils.ts | 213 ++++++++++++++---------------------- 2 files changed, 80 insertions(+), 184 deletions(-) delete mode 100644 src/utils/copy-code.ts diff --git a/src/utils/copy-code.ts b/src/utils/copy-code.ts deleted file mode 100644 index b47f4dba..00000000 --- a/src/utils/copy-code.ts +++ /dev/null @@ -1,51 +0,0 @@ -/** - * Initialize copy functionality for code blocks - */ -export function initializeCodeCopy() { - document.addEventListener('click', (e) => { - const target = e.target as HTMLElement; - const button = target.closest('.copy-code-button'); - - if (button) { - const preElement = button.parentElement?.querySelector('pre'); - if (preElement) { - const codeContent = preElement.textContent || ''; - navigator.clipboard.writeText(codeContent).then(() => { - // Change button text temporarily after copying - button.innerHTML = ` - - - - - Copied! - - `; - button.classList.add('text-green-400'); - - // Reset button after 2 seconds - setTimeout(() => { - button.innerHTML = 'Copy'; - button.classList.remove('text-green-400'); - }, 2000); - }).catch(() => { - // Show error state - button.innerHTML = ` - - - - - Error! - - `; - button.classList.add('text-red-400'); - - // Reset button after 2 seconds - setTimeout(() => { - button.innerHTML = 'Copy'; - button.classList.remove('text-red-400'); - }, 2000); - }); - } - } - }); -} \ No newline at end of file diff --git a/src/utils/mdparser-utils.ts b/src/utils/mdparser-utils.ts index 8e720678..b80d70cb 100644 --- a/src/utils/mdparser-utils.ts +++ b/src/utils/mdparser-utils.ts @@ -15,84 +15,11 @@ export function escapeHtml(unsafe: string): string { .replace(/'/g, '''); } -/** - * Escape special characters in a string for use in a regular expression - */ -function escapeRegExp(string: string): string { - return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); -} - -/** - * Process code blocks in markdown text - */ -function processCodeBlocks(markdown: string): { html: string; codeBlocks: { [key: string]: string } } { - const codeBlocks: { [key: string]: string } = {}; - let codeBlockCounter = 0; - - // Replace code blocks with placeholders - const processedHtml = markdown.replace( - /```([\w-]*)\n([\s\S]*?)```/gm, - (match, language, code) => { - // Create a unique placeholder that won't appear in normal text - const placeholder = `__CODEBLOCK_${Math.random().toString(36).substring(2)}_${codeBlockCounter}__`; - const escapedCode = escapeHtml(code.trim()); - const langClass = language ? `language-${language}` : ''; - const codeId = `code-${Math.random().toString(36).substring(2, 9)}`; - - // Only add the language label if a language is specified - const languageLabel = language ? `${language}` : ''; - - // Log the code content being set in the data-code attribute - console.log('Setting data-code attribute with code:', code); - - codeBlocks[placeholder] = `

- -
${escapedCode}
-
`; - codeBlockCounter++; - return placeholder; - } - ); - - return { html: processedHtml, codeBlocks }; -} - -/** - * Process inline code in markdown text - */ -function processInlineCode(markdown: string): { html: string; inlineCode: { [key: string]: string } } { - const inlineCode: { [key: string]: string } = {}; - let inlineCounter = 0; - - // Replace inline code with placeholders - const processedHtml = markdown.replace(/(? { - const placeholder = `__INLINE_${Math.random().toString(36).substring(2)}_${inlineCounter}__`; - inlineCode[placeholder] = `${escapeHtml(code)}`; - inlineCounter++; - return placeholder; - }); - - return { html: processedHtml, inlineCode }; -} - /** * Renders markdown text into styled HTML */ export const renderMarkdown = (markdown: string): string => { - // Normalize line endings to \n - markdown = markdown.replace(/\r\n/g, '\n'); - - // First, process code blocks and store them safely - const { html: htmlWithCodeBlockPlaceholders, codeBlocks } = processCodeBlocks(markdown); - - // Then, process inline code and store them safely - const { html: htmlWithAllPlaceholders, inlineCode } = processInlineCode(htmlWithCodeBlockPlaceholders); - - let html = htmlWithAllPlaceholders; + let html = markdown; // Create IDs for headers to enable anchor links const headerToId = (text: string) => { @@ -102,7 +29,78 @@ export const renderMarkdown = (markdown: string): string => { .replace(/(^-|-$)/g, ''); }; - // Process headers + // Process code blocks - Process this first to avoid conflicts with other markdown elements + html = html.replace( + /```([\w-]*)\s*([\s\S]*?)```/gm, + function (_match, _language, codeContent) { + // Prepare the code content for display + const escapedCode = escapeHtml(codeContent.trim()); + + // Build the HTML for the code block without copy button + return ` +
+
+
${escapedCode}
+
+
+ `; + }, + ); + + // Process blockquotes + html = html.replace(/(^>.*(\n>.*)*)/gm, function (match) { + const lines = match.split('\n'); + let content = ''; + + lines.forEach((line) => { + const match = line.match(/^>(>*)\s?(.*)$/); + if (match) { + const nestedChars = match[1]; + const lineContent = match[2]; + const nestLevel = nestedChars.length + 1; + + const borderColorClasses = [ + 'border-blue-300 bg-blue-50', + 'border-purple-300 bg-purple-50', + 'border-green-300 bg-green-50', + 'border-orange-300 bg-orange-50', + ]; + + const borderClass = + borderColorClasses[(nestLevel - 1) % borderColorClasses.length]; + const marginClass = nestLevel > 1 ? 'ml-4' : ''; + const contentToAdd = lineContent.trim() === '' ? '
' : lineContent; + if (content === '') { + content = `
${contentToAdd}`; + } else { + content += `
${contentToAdd}`; + } + } + }); + if (content !== '') { + content += '
'; + } + + return content; + }); + + html = html.replace( + /!\[(.*?)\]\((.*?)(?:\s"(.*?)")?\)/gim, + function (_, alt, src, title) { + const caption = title + ? `
${title}
` + : ''; + + return `
+
+ ${alt} +
+ ${caption} +
`; + }, + ); + + // Convert headers with anchor links html = html.replace(/^### (.*$)/gim, (_, title) => { const id = headerToId(title); return `

@@ -136,7 +134,7 @@ export const renderMarkdown = (markdown: string): string => {

`; @@ -179,6 +177,12 @@ export const renderMarkdown = (markdown: string): string => { '$1', ); + // Convert inline code - Make sure this comes after code blocks + html = html.replace( + /`([^`]+)`/gim, + '$1', + ); + // Improved table handling const tableRegex = /^\|(.+)\|(\r?\n\|[-|\s]+\|)(\r?\n\|.+\|)+/gm; html = html.replace(tableRegex, function (match) { @@ -217,7 +221,6 @@ export const renderMarkdown = (markdown: string): string => { tableHtml += ''; return tableHtml; }); - html = html.replace( /^\s*- \[([ xX])\] (.+)$/gim, function (_, checked, content) { @@ -311,15 +314,6 @@ export const renderMarkdown = (markdown: string): string => { '

$1

', ); - // After processing all other elements, restore code blocks and inline code - Object.entries(codeBlocks).forEach(([placeholder, codeHtml]) => { - html = html.replace(new RegExp(escapeRegExp(placeholder), 'g'), codeHtml); - }); - - Object.entries(inlineCode).forEach(([placeholder, codeHtml]) => { - html = html.replace(new RegExp(escapeRegExp(placeholder), 'g'), codeHtml); - }); - // Style first paragraph's first letter (if it exists) html = html.replace( /

(\w)/, @@ -353,50 +347,3 @@ export const renderMarkdown = (markdown: string): string => { return html; }; - -/** - * Initialize syntax highlighting for code blocks if a syntax highlighting library is available - * This function should be called after the DOM is loaded - */ -export function initializeSyntaxHighlighting(): void { - // Check if Prism or Highlight.js is available - const hasPrism = typeof (window as any).Prism !== 'undefined'; - const hasHighlightJs = typeof (window as any).hljs !== 'undefined'; - - if (hasPrism) { - // If Prism is available, let it automatically highlight all code blocks - // Prism does this automatically for elements with language-* classes - try { - (window as any).Prism.highlightAll(); - } catch (error) { - console.error('Error initializing Prism syntax highlighting:', error); - } - } else if (hasHighlightJs) { - // If Highlight.js is available, manually highlight each code block - try { - document.querySelectorAll('pre code').forEach((block) => { - (window as any).hljs.highlightBlock(block); - }); - } catch (error) { - console.error('Error initializing Highlight.js syntax highlighting:', error); - } - } -} - -/** - * Initialize all markdown-related features - * This function should be called after the DOM is loaded - */ -export function initializeMarkdownFeatures(): void { - // Import and initialize code copy functionality - document.addEventListener('DOMContentLoaded', () => { - import('./copy-code').then(({ initializeCodeCopy }) => { - initializeCodeCopy(); - }).catch(error => { - console.error('Error initializing code copy functionality:', error); - }); - - // Initialize syntax highlighting - initializeSyntaxHighlighting(); - }); -}