auto-git:
[add] src/markdown/markdown-render.css [add] src/markdown/markdown.js
This commit is contained in:
262
src/markdown/markdown-render.css
Normal file
262
src/markdown/markdown-render.css
Normal file
@@ -0,0 +1,262 @@
|
|||||||
|
:root {
|
||||||
|
/* Core tokens */
|
||||||
|
--md-border-color: #e0e0e0;
|
||||||
|
--md-surface: #f8f8f8;
|
||||||
|
--md-surface-alt: #fdfdfd;
|
||||||
|
--md-radius: 12px;
|
||||||
|
|
||||||
|
--md-text-color: #111;
|
||||||
|
--md-muted-color: #555;
|
||||||
|
--md-link-color: #0a66c2;
|
||||||
|
--md-link-hover-color: #004182;
|
||||||
|
|
||||||
|
--md-code-bg: #fdfdfd;
|
||||||
|
--md-code-inline-bg: #f3f3f3;
|
||||||
|
|
||||||
|
--md-code-font: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas,
|
||||||
|
"Liberation Mono", "Courier New", monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Optional wrapper if you choose to wrap markdown output:
|
||||||
|
<div class="md-root" v-html="markdownToHTML(text)"> */
|
||||||
|
.md-root {
|
||||||
|
color: var(--md-text-color);
|
||||||
|
font-size: 1rem;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Headings */
|
||||||
|
.md-heading {
|
||||||
|
margin: 1.5rem 0 0.75rem;
|
||||||
|
font-weight: 600;
|
||||||
|
line-height: 1.25;
|
||||||
|
color: var(--md-text-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.md-heading--1 {
|
||||||
|
font-size: 1.8rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.md-heading--2 {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.md-heading--3 {
|
||||||
|
font-size: 1.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.md-heading--4 {
|
||||||
|
font-size: 1.1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Horizontal rule */
|
||||||
|
.md-hr {
|
||||||
|
border: 0;
|
||||||
|
border-top: 1px solid var(--md-border-color);
|
||||||
|
margin: 1.5rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Lists */
|
||||||
|
.md-list {
|
||||||
|
padding-left: 1.4rem;
|
||||||
|
margin: 0.5rem 0 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.md-list--unordered {
|
||||||
|
list-style: disc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.md-list--ordered {
|
||||||
|
list-style: decimal;
|
||||||
|
}
|
||||||
|
|
||||||
|
.md-list__item {
|
||||||
|
margin: 0.2rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Tables */
|
||||||
|
.md-table {
|
||||||
|
border-collapse: separate;
|
||||||
|
border-spacing: 0;
|
||||||
|
width: 100%;
|
||||||
|
margin: 1rem 0;
|
||||||
|
border: 1px solid var(--md-border-color);
|
||||||
|
border-radius: var(--md-radius);
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.md-table thead {
|
||||||
|
background: var(--md-surface);
|
||||||
|
}
|
||||||
|
|
||||||
|
.md-table__row {
|
||||||
|
}
|
||||||
|
|
||||||
|
.md-table__row--head {
|
||||||
|
}
|
||||||
|
|
||||||
|
.md-table__head-cell,
|
||||||
|
.md-table__cell {
|
||||||
|
padding: 0.6rem 0.75rem;
|
||||||
|
vertical-align: top;
|
||||||
|
text-align: left;
|
||||||
|
border-bottom: 1px solid var(--md-border-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.md-table__row:last-child .md-table__cell {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.md-align-left {
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.md-align-center {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.md-align-right {
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Blockquotes */
|
||||||
|
.md-blockquote {
|
||||||
|
margin: 1rem 0;
|
||||||
|
padding: 0.75rem 1rem;
|
||||||
|
border-left: 3px solid var(--md-border-color);
|
||||||
|
background: var(--md-surface);
|
||||||
|
color: var(--md-muted-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Code blocks */
|
||||||
|
.md-codeblock {
|
||||||
|
margin: 1rem 0;
|
||||||
|
border: 1px solid var(--md-border-color);
|
||||||
|
border-radius: var(--md-radius);
|
||||||
|
overflow: hidden;
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.md-codeblock__header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 0.5rem;
|
||||||
|
padding: 0.5rem 0.75rem;
|
||||||
|
background: var(--md-surface);
|
||||||
|
border-bottom: 1px solid var(--md-border-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.md-codeblock__lang {
|
||||||
|
font-size: 0.875rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--md-text-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.md-codeblock__copy {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 0.25rem;
|
||||||
|
padding: 0.25rem 0.5rem;
|
||||||
|
border: 1px solid var(--md-border-color);
|
||||||
|
border-radius: 8px;
|
||||||
|
background: #fff;
|
||||||
|
color: var(--md-text-color);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.md-codeblock__copy:hover {
|
||||||
|
background: #f3f3f3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.md-codeblock__copy:active {
|
||||||
|
transform: translateY(1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.md-icon {
|
||||||
|
stroke: currentColor;
|
||||||
|
fill: none;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Could be specialized later if needed */
|
||||||
|
.md-icon-external {
|
||||||
|
}
|
||||||
|
|
||||||
|
.md-icon-copy {
|
||||||
|
}
|
||||||
|
|
||||||
|
.md-codeblock__pre {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0.75rem;
|
||||||
|
border: 0;
|
||||||
|
overflow: auto;
|
||||||
|
max-width: 100%;
|
||||||
|
background: var(--md-code-bg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.md-codeblock__code {
|
||||||
|
display: block;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
word-break: break-word;
|
||||||
|
overflow-wrap: anywhere;
|
||||||
|
max-width: 100%;
|
||||||
|
font-family: var(--md-code-font);
|
||||||
|
font-size: 0.95em;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Inline code (outside of fenced blocks) */
|
||||||
|
code:not(.md-codeblock__code) {
|
||||||
|
font-family: var(--md-code-font);
|
||||||
|
font-size: 0.95em;
|
||||||
|
padding: 0.1em 0.25em;
|
||||||
|
border-radius: 4px;
|
||||||
|
background: var(--md-code-inline-bg);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Links */
|
||||||
|
.md-link {
|
||||||
|
color: var(--md-link-color);
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.md-link:hover,
|
||||||
|
.md-link:focus-visible {
|
||||||
|
color: var(--md-link-hover-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.md-link--external {
|
||||||
|
position: relative;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.35rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.md-link__label {
|
||||||
|
/* can be customized if needed */
|
||||||
|
}
|
||||||
|
|
||||||
|
.md-link__tooltip {
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
top: 100%;
|
||||||
|
margin-top: 0.35rem;
|
||||||
|
padding: 0.35rem 0.5rem;
|
||||||
|
background: #111;
|
||||||
|
color: #fff;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
white-space: nowrap;
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(4px);
|
||||||
|
transition: opacity 120ms ease, transform 120ms ease;
|
||||||
|
pointer-events: none;
|
||||||
|
z-index: 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
.md-link--external:hover .md-link__tooltip,
|
||||||
|
.md-link--external:focus-visible .md-link__tooltip {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
275
src/markdown/markdown.js
Normal file
275
src/markdown/markdown.js
Normal file
@@ -0,0 +1,275 @@
|
|||||||
|
export function markdownToHTML(text) {
|
||||||
|
// 0) Remove <think>...</think>/<thinking>...</thinking> blocks
|
||||||
|
text = text.replace(/<think(?:ing)?>[\s\S]*?(?:<\/think(?:ing)?>|$)/gi, '');
|
||||||
|
|
||||||
|
// Normalize exotic spaces (narrow/non-breaking) to regular spaces
|
||||||
|
text = text.replace(/[\u00a0\u202f\u2007]/g, ' ');
|
||||||
|
|
||||||
|
text = balanceStreamingCodeFence(text);
|
||||||
|
|
||||||
|
const escapeHtml = (value = '') =>
|
||||||
|
value
|
||||||
|
.replace(/&/g, '&')
|
||||||
|
.replace(/</g, '<')
|
||||||
|
.replace(/>/g, '>');
|
||||||
|
|
||||||
|
const escapeAttr = (value = '') =>
|
||||||
|
escapeHtml(value)
|
||||||
|
.replace(/"/g, '"')
|
||||||
|
.replace(/'/g, ''');
|
||||||
|
|
||||||
|
const applyInline = (source) => {
|
||||||
|
const codeRuns = [];
|
||||||
|
let tmp = source.replace(/`([^`]+?)`/g, (_, code) => {
|
||||||
|
const idx = codeRuns.push(code) - 1;
|
||||||
|
return `@@CODEINLINE${idx}@@`;
|
||||||
|
});
|
||||||
|
|
||||||
|
const strongRuns = [];
|
||||||
|
tmp = tmp.replace(/\*\*([\s\S]+?)\*\*/g, (_, content) => {
|
||||||
|
const idx = strongRuns.push(content) - 1;
|
||||||
|
return `@@STRONG${idx}@@`;
|
||||||
|
});
|
||||||
|
|
||||||
|
const emphasisRuns = [];
|
||||||
|
tmp = tmp.replace(/(?<!\*)\*([\s\S]+?)\*(?!\*)/g, (_, content) => {
|
||||||
|
const idx = emphasisRuns.push(content) - 1;
|
||||||
|
return `@@EM${idx}@@`;
|
||||||
|
});
|
||||||
|
|
||||||
|
return tmp
|
||||||
|
.replace(/@@STRONG(\d+)@@/g, (_, idx) => `<b>${strongRuns[+idx]}</b>`)
|
||||||
|
.replace(/@@EM(\d+)@@/g, (_, idx) => `<i>${emphasisRuns[+idx]}</i>`)
|
||||||
|
.replace(/@@CODEINLINE(\d+)@@/g, (_, idx) => `<code>${codeRuns[+idx]}</code>`);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 1) Normalize line endings
|
||||||
|
let tmp = text.replace(/\r\n/g, '\n').replace(/\r/g, '\n');
|
||||||
|
|
||||||
|
// 2) Extract code blocks and replace with placeholders (protect from all formatting)
|
||||||
|
const codeblocks = [];
|
||||||
|
const placeholder = (idx) => `@@CODEBLOCK${idx}@@`;
|
||||||
|
|
||||||
|
tmp = tmp.replace(/```([^\n]*)\n([\s\S]*?)```/g, (_, lang, code) => {
|
||||||
|
let cleaned = (code || '').replace(/\r\n/g, '\n').replace(/\r/g, '\n');
|
||||||
|
const lines = cleaned.split('\n');
|
||||||
|
while (lines.length > 0 && /^\s*$/.test(lines[lines.length - 1])) lines.pop();
|
||||||
|
cleaned = lines.join('\n');
|
||||||
|
codeblocks.push({ lang: (lang || '').trim(), code: cleaned });
|
||||||
|
return placeholder(codeblocks.length - 1);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 3) HTML-escape special characters (outside of fenced code blocks)
|
||||||
|
let escaped = escapeHtml(tmp);
|
||||||
|
|
||||||
|
// 4) Headings (with consistent hooks)
|
||||||
|
escaped = escaped
|
||||||
|
.replace(/^#### (.+)$/gm, '<h4 class="md-heading md-heading--4">$1</h4>')
|
||||||
|
.replace(/^### (.+)$/gm, '<h3 class="md-heading md-heading--3">$1</h3>')
|
||||||
|
.replace(/^## (.+)$/gm, '<h2 class="md-heading md-heading--2">$1</h2>')
|
||||||
|
.replace(/^# (.+)$/gm, '<h1 class="md-heading md-heading--1">$1</h1>');
|
||||||
|
|
||||||
|
// 4.1) Horizontal rules: --- or *** or ___ on a line
|
||||||
|
escaped = escaped.replace(/^(?:-{3,}|\*{3,}|_{3,})\s*$/gm, '<hr class="md-hr">');
|
||||||
|
|
||||||
|
// 4.2) Ordered lists: lines starting with "1. ", "2. ", ...
|
||||||
|
escaped = escaped.replace(
|
||||||
|
/(^|\n)([ \t]*\d+\. .+(?:\n[ \t]*\d+\. .+)*)/g,
|
||||||
|
(_, lead, listBlock) => {
|
||||||
|
const items = listBlock
|
||||||
|
.split(/\n/)
|
||||||
|
.map((line) => line.replace(/^[ \t]*\d+\.\s+/, '').trim())
|
||||||
|
.filter((item) => item.length > 0)
|
||||||
|
.map((item) => `<li class="md-list__item">${item}</li>`)
|
||||||
|
.join('');
|
||||||
|
if (!items) return listBlock;
|
||||||
|
return `${lead}<ol class="md-list md-list--ordered">${items}</ol>`;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// 4.3) Blockquotes
|
||||||
|
escaped = escaped.replace(
|
||||||
|
/(^|\n)([ \t]*> .+(?:\n[ \t]*> .+)*)/g,
|
||||||
|
(_, lead, blockquoteBlock) => {
|
||||||
|
const lines = blockquoteBlock
|
||||||
|
.split(/\n/)
|
||||||
|
.map((line) => line.replace(/^[ \t]*>\s*/, '').trim())
|
||||||
|
.join('\n');
|
||||||
|
return `${lead}<blockquote class="md-blockquote">${lines}</blockquote>`;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// 4.5) Unordered lists
|
||||||
|
escaped = escaped.replace(
|
||||||
|
/(^|\n)([ \t]*[-*] .+(?:\n[ \t]*[-*] .+)*)/g,
|
||||||
|
(_, lead, listBlock) => {
|
||||||
|
const items = listBlock
|
||||||
|
.split(/\n/)
|
||||||
|
.map((line) => line.replace(/^[ \t]*[-*]\s+/, '').trim())
|
||||||
|
.filter((item) => item.length > 0)
|
||||||
|
.map((item) => `<li class="md-list__item">${item}</li>`)
|
||||||
|
.join('');
|
||||||
|
if (!items) return listBlock;
|
||||||
|
return `${lead}<ul class="md-list md-list--unordered">${items}</ul>`;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// 4.6) Markdown tables (GitHub-style). Strict: requires header, separator, ≥2 cols.
|
||||||
|
const mdTableBlockRe =
|
||||||
|
/(^\|[^\n]*\|?\s*\n\|\s*[:\-]+(?:\s*\|\s*[:\-]+)+\s*\|?\s*\n(?:\|[^\n]*\|?\s*(?:\n|$))*)/gm;
|
||||||
|
|
||||||
|
escaped = escaped.replace(mdTableBlockRe, (block) => {
|
||||||
|
const hadTrailingNewline = /\n$/.test(block);
|
||||||
|
const lines = block.replace(/\n$/, '').split('\n');
|
||||||
|
|
||||||
|
const split = (line) => line.replace(/^\||\|$/g, '').split('|').map((s) => s.trim());
|
||||||
|
|
||||||
|
const headers = split(lines[0]);
|
||||||
|
const seps = split(lines[1]);
|
||||||
|
if (headers.length < 2 || seps.length < 2) return block;
|
||||||
|
if (!seps.every((s) => /^[ :\-]+$/.test(s) && /-/.test(s))) return block;
|
||||||
|
|
||||||
|
const aligns = seps.map((seg) => {
|
||||||
|
const s = seg.replace(/\s+/g, '');
|
||||||
|
const left = s.startsWith(':');
|
||||||
|
const right = s.endsWith(':');
|
||||||
|
if (left && right) return 'center';
|
||||||
|
if (right) return 'right';
|
||||||
|
return 'left';
|
||||||
|
});
|
||||||
|
|
||||||
|
const bodyLines = lines.slice(2).filter((l) => /^\|/.test(l.trim()));
|
||||||
|
const alignClass = (i) => `md-align-${aligns[i] || 'left'}`;
|
||||||
|
|
||||||
|
const ths = headers
|
||||||
|
.map(
|
||||||
|
(h, i) =>
|
||||||
|
`<th class="md-table__head-cell ${alignClass(i)}">${h}</th>`
|
||||||
|
)
|
||||||
|
.join('');
|
||||||
|
|
||||||
|
const rows = bodyLines
|
||||||
|
.map((line) => {
|
||||||
|
const cells = split(line);
|
||||||
|
const tds = cells
|
||||||
|
.map(
|
||||||
|
(c, i) =>
|
||||||
|
`<td class="md-table__cell ${alignClass(i)}">${c}</td>`
|
||||||
|
)
|
||||||
|
.join('');
|
||||||
|
return `<tr class="md-table__row">${tds}</tr>`;
|
||||||
|
})
|
||||||
|
.join('');
|
||||||
|
|
||||||
|
const table = `<table class="md-table"><thead><tr class="md-table__row md-table__row--head">${ths}</tr></thead><tbody>${rows}</tbody></table>`;
|
||||||
|
|
||||||
|
return table + (hadTrailingNewline ? '\n' : '');
|
||||||
|
});
|
||||||
|
|
||||||
|
// 5) Bold, italic, inline code
|
||||||
|
let html = applyInline(escaped);
|
||||||
|
|
||||||
|
// 5.5) Links
|
||||||
|
const safeLink = (hrefRaw) => {
|
||||||
|
const href = (hrefRaw || '').trim();
|
||||||
|
if (!href) return '';
|
||||||
|
if (/^https?:\/\//i.test(href)) return href;
|
||||||
|
if (/^mailto:/i.test(href) || /^tel:/i.test(href)) return href;
|
||||||
|
if (href.startsWith('/') || href.startsWith('#')) return href;
|
||||||
|
return '';
|
||||||
|
};
|
||||||
|
|
||||||
|
html = html.replace(/$begin:math:display$\(\[\^$end:math:display$]+?)\]$begin:math:text$\(\[\^\)\]\+\?\)$end:math:text$/g, (_, label, href) => {
|
||||||
|
const url = safeLink(href);
|
||||||
|
const tooltip = escapeHtml(href || '');
|
||||||
|
if (!url) return label;
|
||||||
|
return `<a class="md-link md-link--external" href="${escapeAttr(
|
||||||
|
url
|
||||||
|
)}" target="_blank" rel="noreferrer noopener"><span class="md-link__label">${label}</span> <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="md-icon md-icon-external"><path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"></path><polyline points="15 3 21 3 21 9"></polyline><line x1="10" y1="14" x2="21" y2="3"></line></svg><span class="md-link__tooltip">${tooltip}</span></a>`;
|
||||||
|
});
|
||||||
|
|
||||||
|
// 6) Convert line-breaks to <br> for NON-code content
|
||||||
|
html = html.replace(/\n/g, '<br>');
|
||||||
|
|
||||||
|
// 6.1) Collapse 3+ consecutive <br> into a double-break
|
||||||
|
html = html.replace(/(?:<br>[\s]*){3,}/g, '<br><br>');
|
||||||
|
|
||||||
|
// 6.2) Normalize spacing around block elements
|
||||||
|
html = html
|
||||||
|
.replace(
|
||||||
|
/(<br>\s*)+(<(?:h[1-4]|hr|table|ul|ol|blockquote)\b[^>]*>)/g,
|
||||||
|
'<br>$2'
|
||||||
|
)
|
||||||
|
.replace(
|
||||||
|
/(<\/(?:h[1-4]|table|ul|ol|blockquote)>\s*)(<br>\s*)+/g,
|
||||||
|
'$1<br>'
|
||||||
|
);
|
||||||
|
|
||||||
|
// 6.3) Trim breaks after headings
|
||||||
|
html = html.replace(/(<\/h[1-4]>)(<br>\s*)+/g, '$1');
|
||||||
|
|
||||||
|
// 6.4) Trim trailing breaks after lists
|
||||||
|
html = html.replace(/(<\/(?:ul|ol)>)(<br>\s*)+/g, '$1');
|
||||||
|
|
||||||
|
// 7) Restore code blocks with header + copy button
|
||||||
|
html = html.replace(/@@CODEBLOCK(\d+)@@/g, (_, idx) => {
|
||||||
|
const { lang, code } = codeblocks[+idx];
|
||||||
|
const title = (lang && lang.trim()) ? lang.trim() : 'code';
|
||||||
|
const titleLabel = escapeHtml(title);
|
||||||
|
const languageClass = title.toLowerCase().replace(/[^a-z0-9_-]/g, '') || 'code';
|
||||||
|
|
||||||
|
const escapedCode = escapeHtml(code);
|
||||||
|
const encodedForCopy = encodeURIComponent(code);
|
||||||
|
|
||||||
|
const head = `<div class="md-codeblock__header"><div class="md-codeblock__lang">${titleLabel}</div><button type="button" class="md-codeblock__copy" aria-label="Copy code" title="Copy code" data-copy-code="${escapeAttr(
|
||||||
|
encodedForCopy
|
||||||
|
)}"><svg class="md-icon md-icon-copy" viewBox="0 0 24 24" width="16" height="16" aria-hidden="true"><path d="M16 1H4a2 2 0 0 0-2 2v12h2V3h12V1zm3 4H8a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h11a2 2 0 0 0 2-2V7a2 2 0 0 0-2-2zm0 16H8V7h11v14z"/></svg></button></div>`;
|
||||||
|
|
||||||
|
const body = `<pre class="md-codeblock__pre"><code class="md-codeblock__code language-${languageClass}">${escapedCode}</code></pre>`;
|
||||||
|
|
||||||
|
return `<div class="md-codeblock">${head}${body}</div>`;
|
||||||
|
});
|
||||||
|
|
||||||
|
// 8) Cleanup around codeblocks
|
||||||
|
html = html
|
||||||
|
.replace(/<br>\s*(?=<div class="md-codeblock"\b)/g, '')
|
||||||
|
.replace(
|
||||||
|
/(<div class="md-codeblock"[^>]*>[\s\S]*?<\/div>)\s*<br>/g,
|
||||||
|
'$1'
|
||||||
|
);
|
||||||
|
|
||||||
|
return html;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Virtually close an unfinished fenced code block so it renders during streaming.
|
||||||
|
function balanceStreamingCodeFence(md) {
|
||||||
|
const lines = md.split(/\r?\n/);
|
||||||
|
let open = null;
|
||||||
|
|
||||||
|
for (let i = 0; i < lines.length; i++) {
|
||||||
|
const line = lines[i];
|
||||||
|
|
||||||
|
if (!open) {
|
||||||
|
const m = line.match(/^\s*([`~]{3,})([^\s]*)?.*$/);
|
||||||
|
if (m) {
|
||||||
|
open = { fenceChar: m[1][0], fenceLen: m[1].length };
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const re = new RegExp(
|
||||||
|
`^\\s*(${open.fenceChar}{${open.fenceLen},})\\s*$`
|
||||||
|
);
|
||||||
|
if (re.test(line)) {
|
||||||
|
open = null;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (open) {
|
||||||
|
const virtual = `${open.fenceChar.repeat(open.fenceLen)}`;
|
||||||
|
return md.endsWith('\n') ? md + virtual : md + '\n' + virtual;
|
||||||
|
}
|
||||||
|
|
||||||
|
return md;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user