// 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 (
{b.v.map((li, j) => - {legalInline(li, `${i}-${j}`)}
)}
);
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 });