let ICONS = null, ICON_SVG = null, MARKS = null, CHIP = null;
try {
const here = new URL('.', import.meta.url);
for (const f of ['icon-registry.js', 'vendor/lucide-moving.js', 'mark-icons.js', 'chip-core.js', 'view-core.js', 'cell-graphics.js', 'chip-view.js']) {
await import(new URL(`../public/${f}`, here).href);
}
ICONS = globalThis.weaveIconRegistry ?? null;
ICON_SVG = globalThis.LUCIDE_MOVING ?? null;
MARKS = globalThis.weaveMarkIcons ?? null;
CHIP = globalThis.weaveChipView ?? null;
} catch {}
const ICON_TOKEN = /^:([a-z0-9][a-z0-9-]*):/;
export function inlineIconHtml(token) {
if (!ICONS || !ICON_SVG || !MARKS) return null;
const hit = ICONS.inline(token);
if (!hit) return null;
if (hit.name) {
return ``;
}
return ``;
}
export function escapeHtml(s) {
return String(s ?? '').replace(/[&<>"]/g, (c) => ({ '&': '&', '<': '<', '>': '>', '"': '"' }[c]));
}
const EMPTY_EMBED = /(? (i % 2 ? part : part.replace(EMPTY_EMBED, ''))).join('');
}
export function stripEmptyEmbeds(markdown) {
const text = String(markdown ?? '');
if (!text.includes(']()') && !/\]\(\s+\)/.test(text)) return text;
const lines = text.split('\n');
let fenced = false;
for (let i = 0; i < lines.length; i += 1) {
if (/^\s*```/.test(lines[i])) { fenced = !fenced; continue; }
if (!fenced) lines[i] = outsideCode(lines[i]);
}
return lines.join('\n');
}
const ENTITY = { colon: ':', tab: '\t', newline: '\n', amp: '&', sol: '/', period: '.', lpar: '(', rpar: ')' };
function decodeEntities(s) {
for (let n = 0; n < 4; n++) {
const next = s.replace(/&(#x[0-9a-f]+|#\d+|[a-z]+);?/gi, (m, e) => {
if (e[0] !== '#') return ENTITY[e.toLowerCase()] ?? m;
const code = e[1] === 'x' || e[1] === 'X' ? parseInt(e.slice(2), 16) : parseInt(e.slice(1), 10);
return code > 0 && code < 0x110000 ? String.fromCodePoint(code) : '';
});
if (next === s) return s;
s = next;
}
return s;
}
function safeUrl(url, { image = false } = {}) {
const plain = decodeEntities(String(url)).replace(/[\u0000-\u0020\u007f-\u009f]/g, '').toLowerCase();
const scheme = plain.match(/^([^/?#]*?):/);
if (!scheme) return url;
if (['http', 'https', 'mailto'].includes(scheme[1])) return url;
if (image && /^data:image\/(png|jpeg|gif|webp)[;,]/.test(plain)) return url;
return null;
}
function linkTarget(href) {
return /^https?:\/\//i.test(String(href)) ? ' target="_blank" rel="noopener"' : '';
}
function renderInline(text, resolveMention) {
let out = '';
let i = 0;
const src = String(text).replace(/[ \t]*$/, '').replace(/(?: {2,}|\\)\n/g, '\u0000\n');
while (i < src.length) {
if (src.startsWith('[[', i)) {
const end = src.indexOf(']]', i);
if (end > 0) {
const inner = src.slice(i + 2, end);
const pipe = inner.indexOf('|');
const ref = (pipe < 0 ? inner : inner.slice(0, pipe)).trim();
const label = pipe < 0 ? null : inner.slice(pipe + 1).trim();
const typed = ref.match(/^(table|space|workspace)(?::(.*))?$/);
const kind = typed ? typed[1] : (/^.+#\d+$/.test(ref) || /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/.test(ref)) ? 'entity' : null;
const target = typed ? (typed[2] ?? '').trim() : ref;
if (kind && resolveMention && (kind === 'workspace' || target)) {
let resolved = null;
try {
resolved = resolveMention(kind, target);
} catch {}
if (resolved && safeUrl(resolved.href) != null) {
const v = resolved.view ?? {
name: resolved.label, link: false, state: null,
fields: (resolved.fields ?? []).filter((f) => f && f.value != null && f.value !== '').slice(0, 3),
};
const title = label ?? (resolved.view ? resolved.name : resolved.label);
out += CHIP
? CHIP.chipHtml(v, { kind, href: resolved.href, home: resolved.home, label: title })
: `${escapeHtml(title)}`;
i = end + 2;
continue;
}
}
out += `${escapeHtml(inner)}`;
i = end + 2;
continue;
}
}
if (src[i] === ':') {
const m = src.slice(i).match(ICON_TOKEN);
const html = m && inlineIconHtml(m[1]);
if (html) { out += html; i += m[0].length; continue; }
}
if (src[i] === '`') {
const end = src.indexOf('`', i + 1);
if (end > 0) {
out += `${escapeHtml(src.slice(i + 1, end))}`;
i = end + 1;
continue;
}
}
if (src.startsWith('![', i)) {
const m = src.slice(i).match(/^!\[([^\]]*)\]\(([^)\s]+)\)/);
if (m) {
const url = safeUrl(m[2], { image: true });
out += url == null ? escapeHtml(m[1]) : ``;
i += m[0].length;
continue;
}
}
if (src[i] === '[') {
const m = src.slice(i).match(/^\[([^\]]+)\]\(([^)\s]+)\)/);
if (m) {
const url = safeUrl(m[2]);
out += url == null ? escapeHtml(m[1]) : `${renderInline(m[1], resolveMention)}`;
i += m[0].length;
continue;
}
}
if (src.startsWith('**', i)) {
const end = src.indexOf('**', i + 2);
if (end > 0) {
out += `${renderInline(src.slice(i + 2, end), resolveMention)}`;
i = end + 2;
continue;
}
}
if (src.startsWith('~~', i)) {
const end = src.indexOf('~~', i + 2);
if (end > 0) {
out += `
${renderInline(src.slice(i + 2, end), resolveMention)}`;
i = end + 2;
continue;
}
}
if (src[i] === '*' || (src[i] === '_' && /\s|^/.test(src[i - 1] ?? ' '))) {
const ch = src[i];
const end = src.indexOf(ch, i + 1);
if (end > 0 && end > i + 1) {
out += `${renderInline(src.slice(i + 1, end), resolveMention)}`;
i = end + 1;
continue;
}
}
out += escapeHtml(src[i]);
i++;
}
return out.replace(/\u0000/g, '
');
}
export function parseBlocks(markdown) {
const lines = String(markdown ?? '').replace(/\r\n/g, '\n').split('\n');
const blocks = [];
let i = 0;
while (i < lines.length) {
const line = lines[i];
if (/^\s*$/.test(line)) { i++; continue; }
const fence = line.match(/^```(\w*)\s*$/);
if (fence) {
const code = [];
i++;
while (i < lines.length && !/^```\s*$/.test(lines[i])) code.push(lines[i++]);
i++;
const lang = fence[1] || '';
blocks.push({
type: ['mermaid', 'mmd'].includes(lang.toLowerCase()) ? 'mermaid' : 'code',
lang,
text: code.join('\n'),
});
continue;
}
if (/^<[a-zA-Z!/]/.test(line)) {
const html = [line];
i++;
while (i < lines.length && !/^\s*$/.test(lines[i])) html.push(lines[i++]);
blocks.push({ type: 'html', text: html.join('\n') });
continue;
}
const h = line.match(/^(#{1,6})\s+(.*)$/);
if (h) {
blocks.push({ type: 'heading', level: h[1].length, text: h[2].trim() });
i++;
continue;
}
if (/^(-{3,}|\*{3,}|_{3,})\s*$/.test(line)) {
blocks.push({ type: 'hr' });
i++;
continue;
}
if (/^>\s?/.test(line)) {
const quote = [];
while (i < lines.length && /^>\s?/.test(lines[i])) quote.push(lines[i++].replace(/^>\s?/, ''));
blocks.push({ type: 'quote', text: quote.join('\n') });
continue;
}
if (line.includes('|') && i + 1 < lines.length && /^\s*\|?[\s:|-]+\|?\s*$/.test(lines[i + 1]) && lines[i + 1].includes('-')) {
const parseRow = (l) => l.replace(/^\s*\|/, '').replace(/\|\s*$/, '').split('|').map((c) => c.trim());
const header = parseRow(line);
i += 2;
const rows = [];
while (i < lines.length && lines[i].includes('|') && !/^\s*$/.test(lines[i])) rows.push(parseRow(lines[i++]));
blocks.push({ type: 'table', header, rows });
continue;
}
const listMatch = line.match(/^(\s*)([-*+]|\d+\.)\s+(.*)$/);
if (listMatch) {
const items = [];
while (i < lines.length) {
const m = lines[i].match(/^(\s*)([-*+]|\d+\.)\s+(.*)$/);
if (!m) break;
const task = m[3].match(/^\[([ xX])\]\s+(.*)$/);
items.push({
depth: Math.floor(m[1].length / 2),
ordered: /\d/.test(m[2]),
text: task ? task[2] : m[3],
checked: task ? task[1].toLowerCase() === 'x' : null,
});
i++;
}
blocks.push({ type: 'list', items });
continue;
}
const para = [line];
i++;
while (
i < lines.length &&
!/^\s*$/.test(lines[i]) &&
!/^(#{1,6})\s/.test(lines[i]) &&
!/^```/.test(lines[i]) &&
!/^>\s?/.test(lines[i]) &&
!/^(\s*)([-*+]|\d+\.)\s+/.test(lines[i]) &&
!/^(-{3,}|\*{3,}|_{3,})\s*$/.test(lines[i])
) {
para.push(lines[i++]);
}
blocks.push({ type: 'paragraph', text: para.join('\n') });
}
return blocks;
}
function renderList(items, resolveMention) {
let html = '';
const stack = [];
let prevDepth = null;
for (const item of items) {
const tag = item.ordered ? 'ol' : 'ul';
if (prevDepth === null) {
html += `<${tag}>`;
stack.push(tag);
} else if (item.depth > prevDepth) {
for (let d = prevDepth; d < item.depth; d++) {
html += `<${tag}>`;
stack.push(tag);
}
} else {
html += '';
for (let d = item.depth; d < prevDepth; d++) html += `${stack.pop()}>`;
}
const check = item.checked == null ? ''
: ` `;
html += `
${renderInline(b.text, resolveMention)}
\n`; break; case 'code': html += `${escapeHtml(b.text)}\n`;
break;
case 'mermaid':
html += `${escapeHtml(b.text)}\n`;
break;
case 'html':
html += rawHtml ? b.text + '\n' : `${escapeHtml(b.text)}
\n`; break; case 'quote': html += `${renderMarkdown(b.text, { resolveMention, rawHtml })}\n`; break; case 'hr': html += '
` : ''}
`;
}