/* * Markdown page-outline calculations. * * This module recognizes source headings and assigns stable HTML identifiers. * It has no application state and does not inspect or modify the DOM. */ /** * goal : Produce a unique HTML identifier for a rendered heading. * pre : usedIds contains identifiers already assigned within the document. * post : The returned identifier is added to usedIds. * result : A readable normalized identifier, with a numeric suffix if needed. */ function headingId(text, usedIds) { const base = String(text || "") .trim() .toLocaleLowerCase() .normalize("NFKD") .replace(/[\u0300-\u036f]/g, "") .replace(/[^\p{L}\p{N}]+/gu, "-") .replace(/^-+|-+$/g, "") || "section"; let id = base; let number = 2; while (usedIds.has(id)) { id = `${base}-${number}`; number += 1; } usedIds.add(id); return id; } /** * goal : Extract ATX headings and their source locations from Markdown. * pre : markdown is source text and may contain fenced code blocks. * post : The source is not changed; headings inside fences are ignored. * result : Ordered level/text/line records for the document outline. */ function markdownHeadings(markdown) { const headings = []; const lines = String(markdown || "").replace(/\r\n/g, "\n").split("\n"); const fencePattern = /^\s*(```|~~~)/; let inFence = false; for (let lineNumber = 0; lineNumber < lines.length; lineNumber += 1) { const line = lines[lineNumber]; if (fencePattern.test(line)) { inFence = !inFence; continue; } if (inFence) continue; const match = line.match(/^\s{0,3}(#{1,6})\s+(.+?)\s*#*\s*$/); if (!match) continue; headings.push({ level: match[1].length, text: match[2].trim(), line: lineNumber }); } return headings; } export { headingId, markdownHeadings };