// Legal page shell — shared by privacy.html and terms.html const legalInline = (text, key) => { // **bold**, [text](url) links, and email/url autolink, verbatim otherwise. // Bold content is re-run through the same span parser (not pushed raw) so // a bolded link — **[Templates Library](templates.html)** — still // resolves to a real , not literal bracket/paren text. const nodes = []; let i = 0, n = 0; // Splits a plain (non-bold) span into autolinked email / [INSERT] / // [text](url) / plain-text nodes. Returns an array — never pushes to the // outer `nodes` directly — so callers (top-level text or bold content) // can both use it identically. const parseSpan = (s) => { const spanNodes = []; const parts = s.split(/(\[INSERT[^\]]*\]|help@fireyouragency\.net|\[[^\]]+\]\([^)]+\))/g); parts.forEach(p => { if (!p) return; const linkMatch = /^\[([^\]]+)\]\(([^)]+)\)$/.exec(p); if (p === 'help@fireyouragency.net') { spanNodes.push({p}); } else if (p.startsWith('[INSERT')) { spanNodes.push({p}); } else if (linkMatch) { spanNodes.push({linkMatch[1]}); } else { spanNodes.push({p}); } }); return spanNodes; }; const push = (s) => nodes.push(...parseSpan(s)); const re = /\*\*([^*]+)\*\*/g; let m; while ((m = re.exec(text)) !== null) { if (m.index > i) push(text.slice(i, m.index)); nodes.push({parseSpan(m[1])}); i = m.index + m[0].length; } if (i < text.length) push(text.slice(i)); return nodes; }; const slugify = (s) => s.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, ''); // Parse the markdown source into blocks; also returns the h2 list for the TOC const parseLegal = (src) => { const lines = src.replace(/^\n+/, '').split('\n'); const blocks = [], toc = []; let para = [], list = []; const flushPara = () => { if (para.length) { blocks.push({ t: 'p', v: para.join('\n') }); para = []; } }; const flushList = () => { if (list.length) { blocks.push({ t: 'ul', v: list.slice() }); list = []; } }; const flush = () => { flushPara(); flushList(); }; lines.forEach(raw => { const line = raw.trim(); if (!line) { flush(); return; } if (line.startsWith('### ')) { flush(); blocks.push({ t: 'h3', v: line.slice(4) }); return; } if (line.startsWith('## ')) { flush(); const v = line.slice(3), id = slugify(v); toc.push({ v, id }); blocks.push({ t: 'h2', v, id }); return; } if (line.startsWith('# ')) { flush(); return; } // page title comes from props if (line.startsWith('- ')) { flushPara(); list.push(line.slice(2)); return; } flushList(); para.push(line); }); flush(); return { blocks, toc }; }; const LegalBody = ({ blocks }) => (
{blocks.map((b, i) => { if (b.t === 'h2') return (

{legalInline(b.v, i)}

); if (b.t === 'h3') return (

{legalInline(b.v, i)}

); if (b.t === 'ul') return ( ); const allCaps = b.v === b.v.toUpperCase() && b.v.length > 60; return (

{legalInline(b.v, i)}

); })}
); const LegalPage = ({ title, kicker, source, active, sibling }) => { const { blocks, toc } = parseLegal(source); return (
{kicker}

{title}

{[['Effective', '[INSERT DATE]'], ['Last updated', '[INSERT DATE]']].map(([l, v]) => (
{l} {v}
))}
Famcom Labs, Inc. · Delaware
Questions about this {kicker.toLowerCase()}?
Email us →
); }; Object.assign(window, { LegalPage, LegalBody, parseLegal, legalInline });