UNPKG

@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
// 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