@entro314labs/starlight-document-converter
Version:
A comprehensive document converter for Astro Starlight that transforms various document formats into Starlight-compatible Markdown with proper frontmatter
282 lines (280 loc) • 8.37 kB
JavaScript
// src/plugins/built-in/toc-generator.ts
var TocGenerator = class {
maxDepth;
minEntries;
constructor(maxDepth = 4, minEntries = 2) {
this.maxDepth = maxDepth;
this.minEntries = minEntries;
}
/**
* Generate table of contents from markdown content
*/
generateToc(content) {
const headings = this.extractHeadings(content);
if (headings.length < this.minEntries) {
return [];
}
return this.buildTocTree(headings);
}
/**
* Generate table of contents with custom anchor generation
*/
generateTocWithCustomAnchors(content, anchorGenerator) {
const headings = this.extractHeadings(content, anchorGenerator);
if (headings.length < this.minEntries) {
return [];
}
return this.buildTocTree(headings);
}
/**
* Insert table of contents into content
*/
insertTocIntoContent(content, tocPosition = "after-title", customMarker) {
const toc = this.generateToc(content);
if (toc.length === 0) {
return content;
}
const tocMarkdown = this.renderTocAsMarkdown(toc);
switch (tocPosition) {
case "top":
return this.insertAtTop(content, tocMarkdown);
case "after-title":
return this.insertAfterTitle(content, tocMarkdown);
case "custom":
if (customMarker) {
return content.replace(customMarker, tocMarkdown);
}
return this.insertAfterTitle(content, tocMarkdown);
default:
return this.insertAfterTitle(content, tocMarkdown);
}
}
/**
* Extract headings from content
*/
extractHeadings(content, anchorGenerator) {
const headingRegex = /^(#{1,6})\s+(.+)$/gm;
const headings = [];
let match;
while ((match = headingRegex.exec(content)) !== null) {
const level = match[1].length;
const title = this.cleanHeadingText(match[2]);
if (level <= this.maxDepth) {
const anchor = anchorGenerator ? anchorGenerator(title) : this.generateAnchor(title);
headings.push({ level, title, anchor });
}
}
return headings;
}
/**
* Build hierarchical TOC tree
*/
buildTocTree(headings) {
const root = [];
const stack = [];
for (const heading of headings) {
const entry = {
level: heading.level,
title: heading.title,
anchor: heading.anchor,
children: []
};
while (stack.length > 0 && stack.at(-1).level >= heading.level) {
stack.pop();
}
if (stack.length === 0) {
root.push(entry);
} else {
const parent = stack.at(-1);
if (!parent.children) {
parent.children = [];
}
parent.children.push(entry);
}
stack.push(entry);
}
return root;
}
/**
* Clean heading text (remove markdown formatting)
*/
cleanHeadingText(text) {
return text.replace(/\*\*(.*?)\*\*/g, "$1").replace(/\*(.*?)\*/g, "$1").replace(/`(.*?)`/g, "$1").replace(/\[([^\]]+)\]\([^)]+\)/g, "$1").trim();
}
/**
* Generate URL-friendly anchor from title
*/
generateAnchor(title) {
return title.toLowerCase().replace(/[^a-z0-9\s-]/g, "").replace(/\s+/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "");
}
/**
* Render TOC as markdown
*/
renderTocAsMarkdown(toc) {
const lines = ["## Table of Contents", ""];
this.renderTocLevel(toc, lines, 0);
return `${lines.join("\n")}
`;
}
/**
* Render TOC as HTML
*/
renderTocAsHtml(toc) {
const lines = ['<nav class="table-of-contents">', "<h2>Table of Contents</h2>"];
lines.push("<ul>");
this.renderTocLevelHtml(toc, lines, 1);
lines.push("</ul>");
lines.push("</nav>");
return lines.join("\n");
}
/**
* Render TOC as JSON for Starlight sidebar
*/
renderTocForStarlightSidebar(toc) {
return toc.map((entry) => this.tocEntryToStarlightFormat(entry));
}
/**
* Render a level of TOC in markdown format
*/
renderTocLevel(entries, lines, depth) {
for (const entry of entries) {
const indent = " ".repeat(depth);
lines.push(`${indent}- [${entry.title}](#${entry.anchor})`);
if (entry.children && entry.children.length > 0) {
this.renderTocLevel(entry.children, lines, depth + 1);
}
}
}
/**
* Render a level of TOC in HTML format
*/
renderTocLevelHtml(entries, lines, depth) {
for (const entry of entries) {
const indent = " ".repeat(depth);
lines.push(`${indent}<li><a href="#${entry.anchor}">${entry.title}</a>`);
if (entry.children && entry.children.length > 0) {
lines.push(`${indent} <ul>`);
this.renderTocLevelHtml(entry.children, lines, depth + 1);
lines.push(`${indent} </ul>`);
}
lines.push(`${indent}</li>`);
}
}
/**
* Convert TOC entry to Starlight sidebar format
*/
tocEntryToStarlightFormat(entry) {
const result = {
label: entry.title,
link: `#${entry.anchor}`
};
if (entry.children && entry.children.length > 0) {
result.items = entry.children.map((child) => this.tocEntryToStarlightFormat(child));
}
return result;
}
/**
* Insert TOC at the top of content
*/
insertAtTop(content, tocMarkdown) {
if (content.startsWith("---\n")) {
const frontmatterEnd = content.indexOf("\n---\n", 4);
if (frontmatterEnd !== -1) {
const frontmatter = content.substring(0, frontmatterEnd + 5);
const body = content.substring(frontmatterEnd + 5);
return `${frontmatter}
${tocMarkdown}
${body.trim()}`;
}
}
return `${tocMarkdown}
${content}`;
}
/**
* Insert TOC after the first heading
*/
insertAfterTitle(content, tocMarkdown) {
let workingContent = content;
let frontmatter = "";
if (content.startsWith("---\n")) {
const frontmatterEnd = content.indexOf("\n---\n", 4);
if (frontmatterEnd !== -1) {
frontmatter = content.substring(0, frontmatterEnd + 5);
workingContent = content.substring(frontmatterEnd + 5);
}
}
const headingMatch = workingContent.match(/^(#+\s+.+)$/m);
if (headingMatch) {
const headingEnd = headingMatch.index + headingMatch[0].length;
const beforeHeading = workingContent.substring(0, headingEnd);
const afterHeading = workingContent.substring(headingEnd);
return `${frontmatter + beforeHeading}
${tocMarkdown}
${afterHeading.trim()}`;
}
return `${frontmatter}
${tocMarkdown}
${workingContent.trim()}`;
}
/**
* Check if content already has a table of contents
*/
hasExistingToc(content) {
const tocPatterns = [
/^##?\s+table\s+of\s+contents/im,
/^##?\s+contents/im,
/^##?\s+toc/im,
/<nav[^>]*table-of-contents/i
];
return tocPatterns.some((pattern) => pattern.test(content));
}
/**
* Remove existing table of contents from content
*/
removeExistingToc(content) {
let processedContent = content.replace(
/^##?\s+(?:table\s+of\s+contents|contents|toc)\s*\n(?:\s*-\s+\[.*?\]\(.*?\)\s*\n)*/gim,
""
);
processedContent = processedContent.replace(
/<nav[^>]*table-of-contents[^>]*>[\s\S]*?<\/nav>/gi,
""
);
return processedContent;
}
/**
* Generate navigation structure for Starlight
*/
generateStarlightNavigation(tocEntries, baseUrl = "") {
return tocEntries.map((entry) => ({
label: entry.title,
link: `${baseUrl}#${entry.anchor}`,
...entry.children && entry.children.length > 0 && {
items: this.generateStarlightNavigation(entry.children, baseUrl)
}
}));
}
/**
* Extract headings for automated sidebar generation
*/
extractHeadingsForSidebar(content, filePath) {
const headings = this.extractHeadings(content);
const title = headings.length > 0 ? headings[0].title : this.generateTitleFromPath(filePath);
const tocHeadings = headings.filter((h) => h.level > 1 || headings[0].level > 1);
return {
title,
headings: tocHeadings
};
}
/**
* Generate title from file path
*/
generateTitleFromPath(filePath) {
const fileName = filePath.split("/").pop()?.replace(/\.[^.]+$/, "") || "Untitled";
return fileName.replace(/[-_]/g, " ").replace(/([a-z])([A-Z])/g, "$1 $2").replace(/\b\w/g, (l) => l.toUpperCase());
}
};
export {
TocGenerator
};
//# sourceMappingURL=chunk-IDQGXGRI.js.map