import { readFile, access, readdir } from "node:fs/promises";
import { dirname, resolve } from "node:path";
import { fileURLToPath } from "node:url";
const here = dirname(fileURLToPath(import.meta.url));
const htmlPath = resolve(here, "chapter-7.6.1.html");
const markdownPath = resolve(here, "chapter-7.6.1.md");
const html = await readFile(htmlPath, "utf8");
const markdown = await readFile(markdownPath, "utf8");
const readme = await readFile(resolve(here, "README.md"), "utf8");
const references = await readFile(resolve(here, "references.md"), "utf8");
const figureNames = (await readdir(resolve(here, "figures")))
.filter((name) => name.endsWith(".svg"))
.sort();
const figures = await Promise.all(
figureNames.map(async (name) => [
`SVG ${name}`,
await readFile(resolve(here, "figures", name), "utf8"),
]),
);
const failures = [];
for (const [name, document] of [
["HTML", html],
["Markdown", markdown],
["README", readme],
["References", references],
...figures,
]) {
if (document.includes("\u2014")) {
failures.push(`${name} contains a forbidden em dash`);
}
for (const phrase of [
["면접", "질문"].join(" "),
["이", "문서에서", "새로", "제작한", "설명용", "SVG"].join(" "),
"설명은 틀리다",
"라고 말할 수는 없다",
"LTO 전체를 하나의 시간 복잡도로 단정하지 않는다",
"LTO는 캐시할 수 없다",
"GCC에 링커가 내장된 것은 아니다",
"ld가 부족한 링커라서 실패한 것이 아니다",
"GCC는 완전한 C 표준 라이브러리 구현을 제공하지 않는다",
"실패가 아니라 성공이다",
"STB_WEAK가 아니다",
"COMMON은 .bss와 같은 입력 섹션이 아니다",
"아카이브 전체를 실행 파일에 복사하지 않고",
"이해하면 안 된다",
"관측을 계약으로 승격하지 말 것",
"PIE 자체가 무작위화를 수행하지는 않는다",
"한 단어 “weak”로 뭉개지 않는다",
"가장 중요한 구분",
]) {
if (document.includes(phrase)) {
failures.push(`${name} contains removed wording: ${phrase}`);
}
}
}
if (/https?:\/\/[^"' )]+(?:\.css|\.js)/i.test(html)) {
failures.push("external CSS/JS dependency found");
}
for (const required of [
"ELI10",
"REMIND",
"WARNING",
"GOTCHA",
"COMMON MISTAKE",
"QUIZ",
"EXERCISE",
]) {
if (!html.includes(required)) failures.push(`missing label: ${required}`);
if (!markdown.includes(required)) {
failures.push(`missing Markdown label: ${required}`);
}
}
for (const requiredId of [
"driver-libc",
"lto-thinlto",
"loader-aslr",
"static-libraries",
"archive-search",
"relocation-preview",
"modern-linkers",
]) {
if (!html.includes(`id="${requiredId}"`)) {
failures.push(`missing HTML section: ${requiredId}`);
}
if (!markdown.includes(``)) {
failures.push(`missing Markdown section: ${requiredId}`);
}
}
const expectedSectionOrder = [
"reading",
"mapping",
"eli10",
"context",
"rules",
"book-cases",
"elf-truth",
"storage",
"gcc10",
"lab",
"linkers",
"beyond-book",
"static-libraries",
"driver-libc",
"archive-search",
"practice",
"glossary",
"relocation-preview",
"modern-linkers",
"lto-thinlto",
"loader-aslr",
"beyond",
];
const htmlSectionOrder = [...html.matchAll(//g)].map((match) => match[1]);
const markdownSectionOrder = [...markdown.matchAll(/<\/a>/g)].map((match) => match[1]);
if (htmlSectionOrder.join("\n") !== expectedSectionOrder.join("\n")) {
failures.push(`unexpected HTML section order: ${htmlSectionOrder.join(", ")}`);
}
if (markdownSectionOrder.join("\n") !== expectedSectionOrder.join("\n")) {
failures.push(`unexpected Markdown section order: ${markdownSectionOrder.join(", ")}`);
}
for (const requiredText of [
"relocation entry",
"libvector.a",
"--warn-backrefs",
"not a dynamic executable",
"PT_INTERP",
"DT_NEEDED",
"randomize_va_space",
"Position Independent Executable",
"-fuse-ld=mold",
"GNU gold",
"증분 컴파일",
"ThinLTO",
"CSAPP 링크 모델과 현대 LTO",
"CPU는 기계 명령을 실행한다",
"weak + weak의 계약 범위",
"archive 입력과 오브젝트 입력",
"PIE는 주 실행 파일의 재배치를 가능하게",
"--gc-sections",
"thin local LTO",
'lto = "thin"',
"2020-05-07",
"Linux 2.6.12",
"STB_GLOBAL + SHN_COMMON",
"optionalDependencies",
"ClassNotFoundException",
]) {
if (!html.includes(requiredText)) failures.push(`missing HTML content: ${requiredText}`);
if (!markdown.includes(requiredText)) failures.push(`missing Markdown content: ${requiredText}`);
}
const ids = new Set([...html.matchAll(/\sid="([^"]+)"/g)].map((match) => match[1]));
for (const match of html.matchAll(/href="#([^"]+)"/g)) {
if (!ids.has(match[1])) failures.push(`broken fragment: #${match[1]}`);
}
for (const match of html.matchAll(/(?:src|href)="([^"#][^"]*)"/g)) {
const target = match[1];
if (/^(?:https?:|mailto:)/.test(target)) continue;
try {
await access(resolve(here, target));
} catch {
failures.push(`missing local target: ${target}`);
}
}
for (const match of html.matchAll(/
]*)>/g)) {
if (!/\balt="[^"]*"/.test(match[1])) failures.push("image without alt text");
}
const markdownIds = new Set(
[...markdown.matchAll(/<\/a>/g)].map((match) => match[1]),
);
for (const match of markdown.matchAll(/\[[^\]]+\]\(([^)]+)\)/g)) {
const target = match[1];
if (/^(?:https?:|mailto:)/.test(target)) continue;
if (target.startsWith("#")) {
if (!markdownIds.has(target.slice(1))) {
failures.push(`broken Markdown fragment: ${target}`);
}
continue;
}
const [localTarget] = target.split("#");
try {
await access(resolve(here, localTarget));
} catch {
failures.push(`missing Markdown local target: ${localTarget}`);
}
}
const markdownFigures = [...markdown.matchAll(/!\[[^\]]+\]\(([^)]+)\)/g)];
if (markdownFigures.length !== 12) {
failures.push(`expected 12 Markdown figures, found ${markdownFigures.length}`);
}
const codeFences = [...markdown.matchAll(/^```/gm)].length;
if (codeFences % 2 !== 0) failures.push("unbalanced Markdown code fences");
if (failures.length) {
console.error(failures.join("\n"));
process.exit(1);
}
console.log(
`Document checks passed: HTML ${ids.size} ids, Markdown ${markdownIds.size} ids, local links resolved.`,
);