/** * -------------------------------------------------------------------- * docmd : the zero-config documentation engine. * * @package @docmd/core (and ecosystem) * @website https://docmd.io * @repository https://github.com/docmd-io/docmd * @license MIT * @copyright Copyright (c) 2025-present docmd.io * * [docmd-source] - Please do not remove this header. * -------------------------------------------------------------------- */ function escapeHtml(str: string): string { return str .replace(/&/g, '&') .replace(//g, '>'); } function highlightContainerAttributes(attrStr: string): string { let commentHtml = ''; let mainAttrs = attrStr; // Extract trailing # comment const commentMatch = attrStr.match(/(\s+#.*)$/); if (commentMatch) { commentHtml = `${escapeHtml(commentMatch[1])}`; mainAttrs = attrStr.slice(0, attrStr.length - commentMatch[0].length); } const attrRegex = /([a-zA-Z0-9_-]+:)(?:"([^"]*)"|'([^']*)'|(\S+))?/g; let result = ''; let lastIndex = 0; for (const match of mainAttrs.matchAll(attrRegex)) { const matchIndex = match.index!; if (matchIndex > lastIndex) { result += matchUnattributedText(mainAttrs.slice(lastIndex, matchIndex)); } lastIndex = matchIndex + match[0].length; const key = escapeHtml(match[1]); const val = match[2] ?? match[3] ?? match[4] ?? ''; const quoteChar = match[2] !== undefined ? '"' : (match[3] !== undefined ? "'" : ''); result += `${key}`; if (quoteChar) { result += `${quoteChar}${escapeHtml(val)}${quoteChar}`; } else if (val) { result += `${escapeHtml(val)}`; } } if (lastIndex < mainAttrs.length) { result += matchUnattributedText(mainAttrs.slice(lastIndex)); } return result + commentHtml; } function matchUnattributedText(text: string): string { return text.replace(/("[^"]*"|'[^']*'|\b[a-zA-Z0-9_-]+\b)/g, (m) => { if (m.startsWith('"') || m.startsWith("'")) { return `${escapeHtml(m)}`; } return `${escapeHtml(m)}`; }); } function highlightInline(text: string): string { let escaped = escapeHtml(text); // Inline container: ::: tag "Text" style:success ::: /tag escaped = escaped.replace( /(:::)\s*([a-zA-Z0-9_-]+)(.*?)(:::)\s*\/([a-zA-Z0-9_-]+)/g, (_, openColons, openType, content, closeColons, closeType) => { const openHtml = `${openColons} ${openType}`; const contentHtml = highlightContainerAttributes(content); const closeHtml = `${closeColons} /${closeType}`; return `${openHtml}${contentHtml}${closeHtml}`; } ); // Math: $math$ or $$math$$ escaped = escaped.replace(/(\$\$[^\$]+\$\$|\$[^\$]+\$)/g, '$1'); // Inline code: `code` escaped = escaped.replace(/(`[^`]+`)/g, '$1'); // Bold: **text** escaped = escaped.replace(/(\*\*[^*]+\*\*)/g, '$1'); // Italics: *text* escaped = escaped.replace(/(?*$1*'); // Links: [text](url) escaped = escaped.replace(/(\[[^\]]+\])(\([^)]+\))/g, '$1$2'); return escaped; } export function highlightDocmd(code: string): { value: string; language: string } { const lines = code.split(/\r?\n/); let inCodeBlock = false; let inMathBlock = false; let inFrontmatter = false; let hasSeenContent = false; const highlightedLines: string[] = []; for (let i = 0; i < lines.length; i++) { const line = lines[i]; const trimmed = line.trim(); // 1. Frontmatter (--- or +++ at top of document) if (inFrontmatter) { if (trimmed === '---' || trimmed === '+++') { inFrontmatter = false; hasSeenContent = true; highlightedLines.push(`${escapeHtml(trimmed)}`); continue; } const fmMatch = line.match(/^([a-zA-Z0-9_-]+:)(.*)$/); if (fmMatch) { highlightedLines.push( `${escapeHtml(fmMatch[1])}${escapeHtml(fmMatch[2])}` ); } else { highlightedLines.push(`${escapeHtml(line)}`); } continue; } if (!hasSeenContent) { if (trimmed === '---' || trimmed === '+++') { inFrontmatter = true; highlightedLines.push(`${escapeHtml(trimmed)}`); continue; } if (trimmed !== '') { hasSeenContent = true; } } // 2. Math Blocks ($$) if (trimmed === '$$') { inMathBlock = !inMathBlock; highlightedLines.push(`$$`); continue; } if (inMathBlock) { highlightedLines.push(`${escapeHtml(line)}`); continue; } // 3. Fenced Code Blocks (```) if (trimmed.startsWith('```') || trimmed.startsWith('~~~')) { inCodeBlock = !inCodeBlock; highlightedLines.push(`${escapeHtml(line)}`); continue; } if (inCodeBlock) { highlightedLines.push(`${escapeHtml(line)}`); continue; } // 3. Docmd Container Opener/Closer (:::) if (trimmed.startsWith(':::')) { const containerCloseMatch = line.match(/^(\s*:::)\s*\/([a-zA-Z0-9_-]+)\s*(#.*)?$/); if (containerCloseMatch) { const colons = `${escapeHtml(containerCloseMatch[1])}`; const type = escapeHtml(containerCloseMatch[2]); const comment = containerCloseMatch[3] ? `${escapeHtml(containerCloseMatch[3])}` : ''; highlightedLines.push(`${colons} /${type}${comment}`); continue; } const containerOpenMatch = line.match(/^(\s*:::)\s*([a-zA-Z0-9_-]+)(.*)$/); if (containerOpenMatch) { const colons = `${escapeHtml(containerOpenMatch[1])}`; const type = escapeHtml(containerOpenMatch[2]); const rest = containerOpenMatch[3]; const restHtml = highlightContainerAttributes(rest); highlightedLines.push(`${colons} ${type}${restHtml}`); continue; } } // 4. Markdown Headers (# Title) const headerMatch = line.match(/^(\s*#{1,6})(\s+.*)$/); if (headerMatch) { const hashes = escapeHtml(headerMatch[1]); const title = highlightInline(headerMatch[2]); highlightedLines.push(`${hashes}${title}`); continue; } // 5. Blockquotes (> Quote) const quoteMatch = line.match(/^(\s*>)(.*)$/); if (quoteMatch) { const quoteChar = escapeHtml(quoteMatch[1]); const quoteText = highlightInline(quoteMatch[2]); highlightedLines.push(`${quoteChar}${quoteText}`); continue; } // 6. List items (- item, 1. item) const listMatch = line.match(/^(\s*(?:\d+\.|\-|\*|\+))\s+(.*)$/); if (listMatch) { const bullet = escapeHtml(listMatch[1]); const text = highlightInline(listMatch[2]); highlightedLines.push(`${bullet} ${text}`); continue; } // 7. General Markdown line highlightedLines.push(highlightInline(line)); } return { value: highlightedLines.join('\n'), language: 'docmd' }; } export default { highlight: highlightDocmd };