fix: spec TOC

This commit is contained in:
2026-01-10 21:31:06 +00:00
parent 4fadbd111a
commit f1fa264ed7
11 changed files with 960 additions and 846 deletions

View File

@@ -132,6 +132,11 @@ const { terminology, specification, tocItems } = Astro.props;
color: #737373;
}
/* Scroll margin for anchored spec sections */
.spec-content :global(ol > li[id]) {
scroll-margin-top: calc(var(--header-height) + 2rem);
}
.spec-content :global(li) {
margin-bottom: 0.5rem;
color: #525252;

View File

@@ -96,6 +96,20 @@ function escapeRegex(str: string): string {
return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
}
// Spec section titles in order (used for both ToC and anchor injection)
const SPEC_SECTION_TITLES = [
"TL;DR",
"The Master Branch",
"Change Branches",
"Pull Requests",
"Versioning",
"Releases",
"Short-Term Release Branches",
"Long-term Release Branches",
"Bug Fixes & Rollback",
"Git Best Practices",
];
/**
* Extract the numbered spec sections (1. TL;DR, 2. The Master Branch, etc.)
*/
@@ -107,24 +121,8 @@ function extractSpecSections(specContent: string): SpecSection[] {
const olMatch = specContent.match(/<ol[^>]*>([\s\S]*?)<\/ol>/i);
if (!olMatch) return sections;
// Split by top-level list items
// We need to handle nested lists carefully
const sectionTitles = [
"TL;DR",
"The Master Branch",
"Change Branches",
"Pull Requests",
"Versioning",
"Releases",
"Short-Term Release Branches",
"Long-term Release Branches",
"Bug Fixes & Rollback",
"Git Best Practices",
];
// Find each section by looking for the title pattern
for (let i = 0; i < sectionTitles.length; i++) {
const title = sectionTitles[i];
for (const title of SPEC_SECTION_TITLES) {
const id = slugify(title);
// For the content, we'll just use the title for navigation
@@ -139,6 +137,27 @@ function extractSpecSections(specContent: string): SpecSection[] {
return sections;
}
/**
* Add anchor IDs to spec section list items.
* Finds top-level <li> elements that start with section titles and adds IDs.
*/
function addSpecSectionAnchors(specContent: string): string {
let result = specContent;
for (const title of SPEC_SECTION_TITLES) {
const id = `spec-${slugify(title)}`;
// Match <li> followed by the section title (possibly with whitespace)
// The title appears right after <li> in the rendered HTML
const pattern = new RegExp(
`(<li>)(\\s*${escapeRegex(title)})`,
"i"
);
result = result.replace(pattern, `<li id="${id}">$2`);
}
return result;
}
/**
* Extract FAQ items from the FAQ section HTML
*/
@@ -187,18 +206,13 @@ function extractFAQItems(faqContent: string): FAQItem[] {
}
/**
* Build table of contents from parsed sections
* Build table of contents from parsed sections.
* Only includes sections rendered in SpecSection (Terminology + Specification).
* Introduction/Summary are in AboutSection and excluded from this ToC.
*/
function buildTocItems(parsed: Partial<ParsedSpec>): TocItem[] {
const items: TocItem[] = [];
// Main sections
if (parsed.introduction) {
items.push({ id: "introduction", title: "Introduction", level: 2 });
}
if (parsed.summary) {
items.push({ id: "summary", title: "Summary", level: 2 });
}
if (parsed.terminology) {
items.push({ id: "terminology", title: "Terminology", level: 2 });
}
@@ -256,12 +270,15 @@ export function parseSpecContent(html: string, version: string): ParsedSpec {
"License",
]);
const specification = extractSection(
const specificationRaw = extractSection(
content,
"Git Common-Flow Specification",
["FAQ", "About", "License"]
);
// Add anchor IDs to spec section list items for ToC navigation
const specification = addSpecSectionAnchors(specificationRaw);
const faqContent = extractSection(content, "FAQ", ["About", "License"]);
const about = extractSection(content, "About", ["License"]);
@@ -269,7 +286,7 @@ export function parseSpecContent(html: string, version: string): ParsedSpec {
const license = extractSection(content, "License", []);
// Parse subsections
const specSections = extractSpecSections(specification);
const specSections = extractSpecSections(specificationRaw);
const faq = extractFAQItems(faqContent);
const parsed: ParsedSpec = {