Skip to content

Commit db670b8

Browse files
committed
Refactor markdown rendering: streamline code block processing and improve inline code handling
1 parent 070b8a9 commit db670b8

File tree

2 files changed

+80
-184
lines changed

2 files changed

+80
-184
lines changed

src/utils/copy-code.ts

Lines changed: 0 additions & 51 deletions
This file was deleted.

src/utils/mdparser-utils.ts

Lines changed: 80 additions & 133 deletions
Original file line numberDiff line numberDiff line change
@@ -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, '&quot;')}" 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
*/
8521
export 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*- \[([ xX])\] (.+)$/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 class="my-4 text-gray-700 leading-relaxed">(\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

Comments
 (0)