1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
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();
|