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 `${ICON_SVG[hit.name]}`; } 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]) : `${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 += ``; } const check = item.checked == null ? '' : ` `; html += `
  • ${check}${renderInline(item.text, resolveMention)}`; prevDepth = item.depth; } html += '
  • '; while (stack.length > 1) html += ``; html += ``; return html; } export function renderMarkdown(markdown, { resolveMention = null, rawHtml = true } = {}) { const blocks = parseBlocks(markdown); let html = ''; for (const b of blocks) { switch (b.type) { case 'heading': html += `${renderInline(b.text, resolveMention)}\n`; break; case 'paragraph': 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 += '
    \n'; break; case 'list': html += renderList(b.items, resolveMention) + '\n'; break; case 'table': { const cells = (row, tag) => row.map((c) => `<${tag}>${renderInline(c, resolveMention)}`).join(''); html += `${cells(b.header, 'th')}`; for (const r of b.rows) html += `${cells(r, 'td')}`; html += '
    \n'; break; } } } return html; } export function isHtmlDocument(text) { return typeof text === 'string' && /^\s*(?:])/i.test(text); } export function renderDocumentPage({ title, subtitle = '', markdown, resolveMention = null }) { const body = renderMarkdown(markdown, { resolveMention }); return ` ${escapeHtml(title)}
    ${escapeHtml(subtitle)}
    ${body}
    ${body.includes('class="mermaid"') ? ` ` : ''} ${ body.includes('
    
    
    
    
    ` : ''}
    
    `;
    }