const ESCAPE_MAP = { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }; export function esc(value) { return String(value ?? '').replace(/[&<>"']/g, (c) => ESCAPE_MAP[c]); } export function renderDefinitions() { return ` `; } const SIGIL_TONE = { frontend: 'frontend', start: 'frontend', backend: 'backend', active: 'backend', database: 'database', success: 'database', cloud: 'cloud', waiting: 'cloud', security: 'security', failure: 'security', messagebus: 'messagebus', external: 'external', neutral: 'external', }; const SIGIL_SHAPE = { frontend: ` `, backend: ``, database: ` `, cloud: ``, security: ` `, messagebus: ` `, external: ` `, start: ` `, active: ``, waiting: ``, success: ` `, failure: ` `, neutral: ` `, }; // A quiet, renderer-owned role stamp. It is authored SVG content rather than a // viewer overlay, so it survives canonical export while adding no focus target, // accessible name, layout box, or interaction state of its own. export function renderSemanticSigil(kind, { x, y, size = 11 } = {}) { const normalized = Object.hasOwn(SIGIL_SHAPE, kind) ? kind : 'neutral'; const tone = SIGIL_TONE[normalized] || 'external'; const scale = size / 16; return ``; } export function renderCards(cards) { const list = Array.isArray(cards) ? cards : []; return `
${list.map((card) => `

${esc(card.title)}

    ${card.items.map((item) => `
  • • ${esc(item)}
  • `).join('\n')}
`).join('\n\n')}
`; } const SVG_SLOT_RE = / [\s\S]*? /; const CARDS_SLOT_RE = / [\s\S]*? /; const SUBTITLE_SLOT_RE = /^([ \t]*)

\[Subtitle description\]<\/p>[ \t]*(\r?\n)?/m; const GUIDED_VIEWS_PLACEHOLDER = ''; const SOURCE_EVIDENCE_PLACEHOLDER = ' '; const TEMPLATE_PLACEHOLDERS = [ '', '[PROJECT NAME] Architecture Diagram', '

[PROJECT NAME] Architecture

', GUIDED_VIEWS_PLACEHOLDER, ]; export function applyTemplate(template, { title, subtitle, svg, cards, visualPreset = 'classic', guidedViews = [], sourceEvidence = null }) { if (!SVG_SLOT_RE.test(template)) { throw new Error('applyTemplate: template missing ARCHIFY:SVG_SLOT sentinel'); } if (!CARDS_SLOT_RE.test(template)) { throw new Error('applyTemplate: template missing ARCHIFY:CARDS_SLOT sentinel'); } if (!SUBTITLE_SLOT_RE.test(template)) { throw new Error('applyTemplate: template missing subtitle placeholder'); } for (const ph of TEMPLATE_PLACEHOLDERS) { if (!template.includes(ph)) { throw new Error(`applyTemplate: template missing placeholder ${JSON.stringify(ph)}`); } } // Keep existing custom templates compatible when evidence is not requested. // Silently dropping verified evidence would be misleading, so the new slot // becomes mandatory only for the opt-in evidence path. if (sourceEvidence && !template.includes(SOURCE_EVIDENCE_PLACEHOLDER)) { throw new Error(`applyTemplate: repository evidence requires placeholder ${JSON.stringify(SOURCE_EVIDENCE_PLACEHOLDER)}`); } // Function replacers: a literal `$&`, `$'`, `$\`` or `$$` in titles, labels, // or rendered SVG must not be interpreted as a replacement pattern. const guidedViewsJson = JSON.stringify(guidedViews) .replaceAll('<', '\\u003c') .replaceAll('>', '\\u003e') .replaceAll('&', '\\u0026'); const sourceEvidenceJson = JSON.stringify(sourceEvidence) .replaceAll('<', '\\u003c') .replaceAll('>', '\\u003e') .replaceAll('&', '\\u0026'); const renderedSubtitle = typeof subtitle === 'string' && subtitle.trim() ? `

${esc(subtitle)}

` : ''; return template .replace(TEMPLATE_PLACEHOLDERS[0], () => ``) .replace(TEMPLATE_PLACEHOLDERS[1], () => `${esc(title)} Diagram`) .replace(TEMPLATE_PLACEHOLDERS[2], () => `

${esc(title)}

`) .replace(SUBTITLE_SLOT_RE, (_match, indent, newline = '') => renderedSubtitle ? `${indent}${renderedSubtitle}${newline}` : '') .replace(SVG_SLOT_RE, () => svg) .replace(CARDS_SLOT_RE, () => cards) .replace(GUIDED_VIEWS_PLACEHOLDER, () => ``) .replace(SOURCE_EVIDENCE_PLACEHOLDER, () => sourceEvidence ? ` ` : ''); } // CJK and other wide/fullwidth glyphs render at roughly twice the advance // width of ASCII in the monospace stacks the template uses. Keep halfwidth // forms (notably U+FF61–U+FF9F Katakana) out of this set. The explicit ranges // also cover vertical punctuation and supplementary East Asian scripts that // literal glyph ranges made difficult to audit. const FULLWIDTH_RE = /[\u1100-\u115F\u2329-\u232A\u2E80-\uA4CF\uAC00-\uD7A3\uF900-\uFAFF\uFE10-\uFE19\uFE30-\uFE6F\uFF01-\uFF60\uFFE0-\uFFE6\u{16FE0}-\u{18DFF}\u{1AFF0}-\u{1AFFF}\u{1B000}-\u{1B2FF}\u{1F000}-\u{1FAFF}\u{20000}-\u{3FFFD}]/u; export function textUnits(text) { let units = 0; for (const ch of String(text ?? '')) units += FULLWIDTH_RE.test(ch) ? 2 : 1; return units; }