// Build-time markup helpers for note bodies: and/but/or/then connectors, heading ids, footnote sidenotes, TK marks and id scoping. import { slugify } from './slug.mjs'; const CONNECTOR = /^(and|but|or|nor|yet|so|for|then|also|plus|because)$/i; const FENCE = /^\s{0,3}(```|~~~)/; /** Turn lone conjunction lines ("and", "but", "or", "then"…) outside fenced code into separator HTML blocks. Other lines are untouched. */ export function markConnectors(markdown) { let fence = null; return markdown .split('\n') .map((line) => { const f = line.match(FENCE); if (f) { if (!fence) fence = f[1]; else if (f[1] === fence) fence = null; return line; } if (fence) return line; const t = line.trim(); if (!CONNECTOR.test(t)) return line; const w = t.toLowerCase(); return `\n
${w}
\n`; }) .join('\n'); } /** * End a blockquote at its last `>` line. NotePlan writes the author's own words on the line right * after a quote, which Markdown would otherwise fold into the quote by lazy continuation. */ export function endQuotes(markdown) { let fence = null; const lines = markdown.split('\n'); return lines .map((line, i) => { const f = line.match(FENCE); if (f) { if (!fence) fence = f[1]; else if (f[1] === fence) fence = null; return line; } const next = lines[i + 1]; if (fence || !/^\s{0,3}>/.test(line) || next === undefined || !next.trim() || /^\s{0,3}>/.test(next)) return line; return `${line}\n`; }) .join('\n'); } /** * Give each bare heading the id Eleventy's id plugin would: its slugified text, with -2, -3… for repeats. * Headings that already carry attributes are left as they are. */ export function addHeadingIds(html) { const seen = new Map(); return html.replace(/<(h[1-6])>([\s\S]*?)<\/\1>/g, (all, tag, inner) => { const base = slugify(inner.replace(/<[^>]*>/g, '')) || 'section'; const n = (seen.get(base) ?? 0) + 1; seen.set(base, n); return `<${tag} id="${n > 1 ? `${base}-${n}` : base}">${inner}${tag}>`; }); } const VOID = new Set(['hr', 'br', 'img', 'input', 'meta', 'link', 'wbr', 'source', 'area', 'col']); const TAG = /<(\/?)([a-zA-Z][a-zA-Z0-9]*)\b[^>]*?(\/?)>/g; /** End offsets (exclusive) of each top-level element in `html`. */ function topLevelEnds(html) { const ends = []; let depth = 0; TAG.lastIndex = 0; for (let m; (m = TAG.exec(html)); ) { const name = m[2].toLowerCase(); if (VOID.has(name) || m[3] === '/') { if (depth === 0) ends.push(TAG.lastIndex); } else if (m[1]) { depth = Math.max(0, depth - 1); if (depth === 0) ends.push(TAG.lastIndex); } else depth++; } return ends; } /** Move markdown-it-footnote output into inline `