/** * Checks all general question MDX files under ippan-situmon to find * `####` sub-headings that are NOT in the QuestionSummary component. */ const fs = require("fs"); const path = require("path"); const BASE = path.resolve(__dirname, "..", "src/content/docs/ippan-situmon"); function findMdxFiles(dir) { const results = []; const entries = fs.readdirSync(dir, { withFileTypes: true }); for (const entry of entries) { const fullPath = path.join(dir, entry.name); if (entry.isDirectory()) { results.push(...findMdxFiles(fullPath)); } else if ( entry.name.endsWith(".mdx") && entry.name !== "index.mdx" && entry.name !== "_partial.mdx" ) { results.push(fullPath); } } return results; } // Parse anchor values from a QuestionSummary qa array function parseAnchors(content) { const anchorRegex = /anchor:\s*"([^"]+)"/g; const anchors = []; let match; while ((match = anchorRegex.exec(content)) !== null) { anchors.push(match[1]); } return anchors; } // Parse `####` headings from MDX content function parseH4Headings(content) { const headingRegex = /^####\s+(.+?)(?:\s*\{[^}]*\})?\s*$/gm; const headings = []; let match; while ((match = headingRegex.exec(content)) !== null) { const raw = match[1].trim(); headings.push(raw); } return headings; } // Generate Starlight-style anchor ID from heading text function toAnchorId(text) { return text .toLowerCase() .replace(/<[^>]+>/g, "") // remove HTML tags .replace( /[^\w\s\-\u3040-\u309f\u30a0-\u30ff\u4e00-\u9fffぁ-んァ-ン一-龯ー・〜?!。、()「」【】]/g, "", ) .replace(/\s+/g, "-") .replace(/-+/g, "-") .replace(/^-|-$/g, ""); } // Check if a heading matches any anchor function headingMatchesAnchor(heading, anchors) { const headingId = toAnchorId(heading); for (const anchor of anchors) { // Exact match if (anchor === heading) return true; // Anchor ID match if (anchor === headingId) return true; // Anchor prefixed with "-" if (anchor === `-${heading}`) return true; if (anchor === `-${headingId}`) return true; // Anchor that starts with "-" - check if headingId matches after "-" if (anchor.startsWith("-")) { const afterDash = anchor.substring(1); if (afterDash === headingId || afterDash === heading) return true; } // Headings sometimes have HTML in them (like ) // Strip HTML from heading and compare to anchor const headingPlain = heading.replace(/<[^>]+>/g, ""); const headingPlainId = toAnchorId(headingPlain); if (anchor === headingPlain) return true; if (anchor === headingPlainId) return true; if (anchor === `-${headingPlain}`) return true; if (anchor === `-${headingPlainId}`) return true; if (anchor.startsWith("-") && anchor.substring(1) === headingPlainId) return true; } return false; } function main() { const files = findMdxFiles(BASE).sort(); const results = []; for (const file of files) { const content = fs.readFileSync(file, "utf-8"); const h4Headings = parseH4Headings(content); const anchors = parseAnchors(content); if (h4Headings.length === 0) continue; const relPath = path.relative(process.cwd(), file); const missing = []; for (const heading of h4Headings) { if (!headingMatchesAnchor(heading, anchors)) { missing.push(heading); } } results.push({ file: relPath, totalH4: h4Headings.length, totalAnchors: anchors.length, missing, allPresent: missing.length === 0, }); } // Output results console.log("=".repeat(80)); console.log("ANALYSIS: #### headings NOT in QuestionSummary"); console.log("=".repeat(80)); console.log(); let filesWithMissing = 0; let totalMissing = 0; for (const r of results) { if (r.missing.length === 0) continue; filesWithMissing++; totalMissing += r.missing.length; if (r.totalAnchors === 0) { console.log(`❌ ${r.file}`); console.log( ` Has ${r.totalH4} '####' headings but NO QuestionSummary component.`, ); console.log(); continue; } console.log( `⚠️ ${r.file} [${r.totalAnchors} anchors, ${r.missing.length}/${r.totalH4} headings missing]`, ); for (const m of r.missing) { console.log(` #### ${m}`); } console.log(); } console.log("=".repeat(80)); console.log(`SUMMARY:`); console.log(` Files with missing headings: ${filesWithMissing}`); console.log( ` Files entirely missing QuestionSummary: ${results.filter((r) => r.totalAnchors === 0 && r.totalH4 > 0).length}`, ); console.log( ` Files fully in sync: ${results.filter((r) => r.allPresent && r.totalAnchors > 0).length}`, ); console.log(` Total missing headings: ${totalMissing}`); console.log(` Total files analyzed: ${results.length}`); console.log("=".repeat(80)); } main();