65 lines
1.8 KiB
JavaScript
65 lines
1.8 KiB
JavaScript
/*
|
|
* 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 };
|