diff options
Diffstat (limited to 'scripts')
| -rw-r--r-- | scripts/check-headings.js | 177 |
1 files changed, 177 insertions, 0 deletions
diff --git a/scripts/check-headings.js b/scripts/check-headings.js new file mode 100644 index 0000000..355b5a2 --- /dev/null +++ b/scripts/check-headings.js @@ -0,0 +1,177 @@ +/** + * 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 <sub>) + // 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(); |
