/**
* --------------------------------------------------------------------
* 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 = ``;
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] ? `` : '';
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(``);
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 };