docs: overhaul public product guides (#1749)

* docs: overhaul public product guides

* fix(docs): address review findings

* docs: clarify validation contracts

* docs: complete validator API contracts

* docs: clarify validator existence check
This commit is contained in:
José Almeida
2026-07-17 11:30:37 +01:00
committed by GitHub
parent 934b713ee9
commit c633b8faa1
66 changed files with 7322 additions and 3096 deletions
+806 -20
View File
@@ -1,3 +1,4 @@
import { createHash } from "node:crypto";
import fs from "node:fs/promises";
import path from "node:path";
import { fileURLToPath, pathToFileURL } from "node:url";
@@ -6,6 +7,7 @@ const repoRoot = path.resolve(
path.dirname(fileURLToPath(import.meta.url)),
"..",
);
const defaultDocsDir = path.join(repoRoot, "docs/public");
/**
* Validate that every published Markdown page has frontmatter and appears
@@ -14,9 +16,7 @@ const repoRoot = path.resolve(
* @param {string} [docsDir] Directory containing published docs and meta.json.
* @returns {Promise<{pageCount: number}>} Number of validated published pages.
*/
export async function validatePublicDocs(
docsDir = path.join(repoRoot, "docs/public"),
) {
export async function validatePublicDocs(docsDir = defaultDocsDir) {
const meta = await readMeta(docsDir);
const files = await collectMarkdownFiles(docsDir);
const pagesBySlug = new Map();
@@ -28,6 +28,7 @@ export async function validatePublicDocs(
if (path.posix.basename(file).toLowerCase() === "readme.md") continue;
assertFrontmatter(file, markdown);
assertDocumentStructure(file, markdown);
const slug = file.replace(/\.mdx?$/, "").replace(/\/index$/, "");
const existing = pagesBySlug.get(slug);
@@ -58,9 +59,685 @@ export async function validatePublicDocs(
}
}
const featureMediaDir = path.join(docsDir, "media/feature-guides");
if (await pathExists(featureMediaDir)) {
await validateFeatureMedia({ docsDir });
}
if (path.resolve(docsDir) === defaultDocsDir) {
await validateCoverageInventory({ repoRoot, docsDir });
}
return { pageCount: pagesBySlug.size };
}
/**
* Validate reviewed, focused feature-guide media and its publication ownership.
*
* The checked-in manifest is the durable boundary between disposable capture
* tooling and published documentation. Every clip must retain accepted QA,
* exact file hashes, a real page/section owner, and a complete DocsVideo embed.
*
* @param {{docsDir: string}} paths Published docs root.
* @returns {Promise<{clipCount: number}>} Number of validated clips.
*/
export async function validateFeatureMedia({ docsDir }) {
const mediaDir = path.join(docsDir, "media/feature-guides");
const manifestPath = path.join(mediaDir, "manifest.json");
const notesPath = path.join(mediaDir, "NOTES.txt");
const manifest = JSON.parse(await fs.readFile(manifestPath, "utf8"));
if (manifest?.schema_version !== 1 || !Array.isArray(manifest.clips)) {
throw new Error(
"feature media manifest must use schema version 1 and contain clips",
);
}
if (manifest.qa_status !== "accepted") {
throw new Error("feature media manifest must have accepted QA status");
}
if (
typeof manifest.generated_at !== "string" ||
!Number.isFinite(Date.parse(manifest.generated_at))
) {
throw new Error(
"feature media manifest must record a valid generation time",
);
}
if (manifest.clips.length < 1 || manifest.clips.length > 10) {
throw new Error(
"feature media manifest must contain between 1 and 10 clips",
);
}
assertFeatureMediaContract(manifest.delivery_contract);
const notes = await fs.readFile(notesPath, "utf8");
if (!notes.trim()) {
throw new Error(
"feature media NOTES.txt must describe capture provenance and QA",
);
}
const expectedFiles = new Set(["manifest.json", "NOTES.txt"]);
const slugs = new Set();
for (const clip of manifest.clips) {
assertFeatureClipShape(clip);
if (slugs.has(clip.slug)) {
throw new Error(`feature media manifest repeats clip slug: ${clip.slug}`);
}
slugs.add(clip.slug);
const pagePath = resolveInside(
docsDir,
clip.intended_docs.page,
`${clip.slug} documentation page`,
);
if (!/\.mdx?$/i.test(pagePath)) {
throw new Error(
`${clip.slug} documentation owner must be a Markdown page`,
);
}
const page = await fs.readFile(pagePath, "utf8");
if (!collectHeadingTitles(page).has(clip.intended_docs.section)) {
throw new Error(
`${clip.slug} targets missing section in ${clip.intended_docs.page}: ${clip.intended_docs.section}`,
);
}
if (!hasCompleteFeatureMediaEmbed(page, clip.filenames)) {
throw new Error(
`${clip.slug} is not embedded with its complete media triplet in ${clip.intended_docs.page}`,
);
}
const expectedCodecs = { webm: "vp9", mp4: "h264", poster: "webp" };
for (const [kind, extension] of Object.entries({
webm: "webm",
mp4: "mp4",
poster: "webp",
})) {
const filename = clip.filenames[kind];
if (filename !== `${clip.slug}.${extension}`) {
throw new Error(
`${clip.slug} has unexpected ${kind} filename: ${filename}`,
);
}
const record = clip.files[kind];
if (
!record ||
!Number.isInteger(record.bytes) ||
record.bytes <= 0 ||
record.codec !== expectedCodecs[kind] ||
typeof record.sha256 !== "string" ||
!/^[a-f0-9]{64}$/.test(record.sha256)
) {
throw new Error(`${clip.slug} has invalid ${kind} file metadata`);
}
const filePath = resolveInside(
mediaDir,
filename,
`${clip.slug} ${kind}`,
);
const contents = await fs.readFile(filePath);
if (contents.byteLength !== record.bytes) {
throw new Error(`${filename} byte count does not match manifest`);
}
const digest = createHash("sha256").update(contents).digest("hex");
if (digest !== record.sha256) {
throw new Error(`${filename} SHA-256 does not match manifest`);
}
expectedFiles.add(filename);
}
}
const entries = await fs.readdir(mediaDir, { withFileTypes: true });
for (const entry of entries) {
if (!entry.isFile() || !expectedFiles.has(entry.name)) {
throw new Error(
`feature media directory contains untracked file: ${entry.name}`,
);
}
}
for (const filename of expectedFiles) {
if (!entries.some((entry) => entry.isFile() && entry.name === filename)) {
throw new Error(
`feature media directory is missing tracked file: ${filename}`,
);
}
}
return { clipCount: slugs.size };
}
/**
* Require the publication dimensions, cadence, codecs, and no-audio policy.
*
* @param {object} contract Media contract from the feature-guide manifest.
* @returns {void}
*/
function assertFeatureMediaContract(contract) {
if (
contract?.dimensions?.width !== 960 ||
contract?.dimensions?.height !== 600 ||
contract?.frame_rate !== 25 ||
contract?.audio !== false ||
JSON.stringify(contract?.video_formats) !==
JSON.stringify(["vp9-webm", "h264-mp4"]) ||
contract?.poster_format !== "webp"
) {
throw new Error(
"feature media manifest has an unsupported delivery contract",
);
}
}
/**
* Validate one clip's ownership, provenance, delivery metadata, and file maps.
*
* @param {object} clip Feature-guide manifest clip entry.
* @returns {void}
*/
function assertFeatureClipShape(clip) {
if (!clip || typeof clip !== "object" || Array.isArray(clip)) {
throw new Error("feature media manifest contains an invalid clip entry");
}
const fields = [
[
"slug",
typeof clip.slug === "string" &&
/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(clip.slug),
],
["title", typeof clip.title === "string" && clip.title.trim().length > 0],
[
"accessible_caption",
typeof clip.accessible_caption === "string" &&
clip.accessible_caption.trim().length > 0,
],
[
"source_scenario",
typeof clip.source_scenario === "string" &&
clip.source_scenario.trim().length >= 20,
],
[
"data_isolation",
typeof clip.data_isolation === "string" &&
clip.data_isolation.trim().length >= 20,
],
[
"duration_seconds",
typeof clip.duration_seconds === "number" &&
clip.duration_seconds >= 6 &&
clip.duration_seconds <= 15,
],
[
"dimensions",
clip.dimensions?.width === 960 && clip.dimensions?.height === 600,
],
[
"intended_docs",
typeof clip.intended_docs?.page === "string" &&
typeof clip.intended_docs?.section === "string" &&
clip.intended_docs.section.trim().length > 0,
],
[
"filenames",
Boolean(
clip.filenames &&
typeof clip.filenames === "object" &&
!Array.isArray(clip.filenames),
),
],
[
"files",
Boolean(
clip.files && typeof clip.files === "object" && !Array.isArray(clip.files),
),
],
];
const invalidField = fields.find(([, valid]) => !valid)?.[0];
if (invalidField) {
const owner =
typeof clip.slug === "string" && clip.slug.length > 0
? clip.slug
: "<unknown>";
throw new Error(`feature media clip ${owner} has invalid ${invalidField}`);
}
}
/**
* Match each manifest filename to its corresponding DocsVideo attribute.
*
* @param {string} markdown Published page source.
* @param {Record<string, string>} filenames Expected media filenames by format.
* @returns {boolean} Whether one DocsVideo embeds the complete media triplet.
*/
function hasCompleteFeatureMediaEmbed(markdown, filenames) {
return [
...stripMarkdownCode(markdown).matchAll(/<DocsVideo\b[\s\S]*?\/>/g),
].some((match) => {
const attributes = Object.fromEntries(
[
...match[0].matchAll(
/\b(webm|mp4|poster)\s*=\s*(?:"([^"]*)"|'([^']*)')/g,
),
].map((attribute) => [
attribute[1],
attribute[2] ?? attribute[3],
]),
);
return Object.entries(filenames).every(
([kind, filename]) =>
attributes[kind]?.replace(/^\.\//, "") ===
`media/feature-guides/${filename}`,
);
});
}
/**
* Collect visible heading labels for media section-ownership checks.
*
* @param {string} markdown Published page source.
* @returns {Set<string>} Normalized visible heading labels.
*/
function collectHeadingTitles(markdown) {
const titles = new Set();
for (const match of stripMarkdownCode(markdown, {
keepInlineCode: true,
}).matchAll(/^ {0,3}#{1,6}[ \t]+(.+?)\s*#*\s*$/gm)) {
titles.add(stripHeadingMarkup(match[1]));
}
return titles;
}
/**
* Remove supported inline heading markup and reject unterminated HTML tags.
*
* @param {string} value Raw Markdown heading label.
* @returns {string} Visible heading text.
*/
function stripHeadingMarkup(value) {
const linkedText = value
.replace(/!\[([^\]]*)\]\([^)]*\)/g, "$1")
.replace(/\[([^\]]+)\]\([^)]*\)/g, "$1");
let text = "";
let insideTag = false;
for (const character of linkedText) {
if (character === "<") {
insideTag = true;
} else if (character === ">" && insideTag) {
insideTag = false;
} else if (!insideTag) {
text += character;
}
}
if (insideTag) {
throw new Error("heading contains unterminated inline HTML");
}
return text.replace(/[`*_~]/g, "").trim();
}
/**
* Resolve a relative path while requiring it to remain below the supplied root.
*
* @param {string} root Allowed filesystem root.
* @param {string} relativePath Untrusted repository-relative path.
* @param {string} label Human-readable field name for validation errors.
* @returns {string} Absolute path inside the root.
*/
function resolveInside(root, relativePath, label) {
const target = path.resolve(root, relativePath);
const relative = path.relative(root, target);
if (!relative || relative.startsWith("..") || path.isAbsolute(relative)) {
throw new Error(`${label} must stay inside ${root}`);
}
return target;
}
/**
* Check whether a filesystem target exists.
*
* @param {string} target Absolute filesystem path.
* @returns {Promise<boolean>} Whether the target exists.
*/
async function pathExists(target) {
try {
await fs.access(target);
return true;
} catch {
return false;
}
}
/**
* Validate the source-backed feature coverage inventory.
*
* The inventory assigns every published product area to documentation pages
* and concrete implementation/test evidence. It also accounts for every
* statically registered Settings route and Kandev MCP tool, so newly shipped
* surfaces cannot silently bypass the public docs.
*
* @param {{repoRoot: string, docsDir: string}} paths Repository and docs roots.
* @returns {Promise<{areaCount: number, settingsRouteCount: number, mcpToolCount: number}>}
*/
export async function validateCoverageInventory({ repoRoot, docsDir }) {
const coveragePath = path.join(docsDir, "coverage.json");
const coverage = JSON.parse(await fs.readFile(coveragePath, "utf8"));
if (coverage?.version !== 1 || !Array.isArray(coverage.areas)) {
throw new Error(
"coverage.json must use version 1 and contain an areas array",
);
}
if (coverage.areas.length === 0) {
throw new Error("coverage.json must contain at least one coverage area");
}
const pageSlugs = new Set(
(await collectMarkdownFiles(docsDir))
.filter((file) => path.posix.basename(file).toLowerCase() !== "readme.md")
.map((file) => file.replace(/\.mdx?$/, "").replace(/\/index$/, "")),
);
const areaIds = new Set();
const coveredDocsPages = new Set();
const coveredSettingsRoutes = new Set();
const coveredMcpTools = new Set();
for (const area of coverage.areas) {
assertCoverageAreaShape(area);
if (areaIds.has(area.id)) {
throw new Error(`coverage.json repeats area id: ${area.id}`);
}
areaIds.add(area.id);
for (const slug of area.docs) {
if (!pageSlugs.has(slug)) {
throw new Error(`${area.id} cites unknown docs page: ${slug}`);
}
coveredDocsPages.add(slug);
}
for (const source of area.sources) {
await assertCoverageEvidence(repoRoot, area.id, "source", source);
}
for (const testFile of area.tests) {
await assertCoverageEvidence(repoRoot, area.id, "test", testFile);
}
addUniqueCoverageValues(
coveredSettingsRoutes,
area.settingsRoutes ?? [],
"settings route",
);
addUniqueCoverageValues(coveredMcpTools, area.mcpTools ?? [], "MCP tool");
}
for (const slug of pageSlugs) {
if (!coveredDocsPages.has(slug)) {
throw new Error(`coverage.json does not account for docs page: ${slug}`);
}
}
const exclusions = coverage.exclusions ?? {};
const excludedSettingsRoutes = validateCoverageExclusions(
exclusions.settingsRoutes ?? [],
"route",
"settings route",
);
const excludedMcpTools = validateCoverageExclusions(
exclusions.mcpTools ?? [],
"tool",
"MCP tool",
);
const shippedSettingsRoutes = await collectSettingsRoutes(repoRoot);
assertCompleteSurfaceCoverage(
shippedSettingsRoutes,
coveredSettingsRoutes,
excludedSettingsRoutes,
"settings route",
);
const registeredMcpTools = await collectMcpTools(repoRoot);
assertCompleteSurfaceCoverage(
registeredMcpTools,
coveredMcpTools,
excludedMcpTools,
"MCP tool",
);
return {
areaCount: areaIds.size,
settingsRouteCount: shippedSettingsRoutes.size,
mcpToolCount: registeredMcpTools.size,
};
}
/**
* Require one coverage area to declare audiences, docs, and concrete evidence.
*
* @param {object} area Coverage inventory area.
* @returns {void}
*/
function assertCoverageAreaShape(area) {
if (!area || typeof area !== "object" || Array.isArray(area)) {
throw new Error("coverage.json areas must contain objects");
}
if (
typeof area.id !== "string" ||
!/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(area.id)
) {
throw new Error("coverage.json area ids must use lowercase kebab-case");
}
assertStringArray(area, "audiences", area.id);
assertStringArray(area, "docs", area.id);
assertStringArray(area, "sources", area.id);
assertStringArray(area, "tests", area.id);
if (!new Set(["stable", "beta", "experimental"]).has(area.stability)) {
throw new Error(`${area.id} has unsupported stability: ${area.stability}`);
}
for (const optionalField of ["settingsRoutes", "mcpTools"]) {
if (area[optionalField] !== undefined) {
assertStringArray(area, optionalField, area.id, { allowEmpty: true });
}
}
}
/**
* Validate a named inventory field as a string list.
*
* @param {object} object Inventory object containing the field.
* @param {string} field Field name to validate.
* @param {string} owner Area identifier used in validation errors.
* @param {{allowEmpty?: boolean}} options Empty-list policy.
* @returns {void}
*/
function assertStringArray(object, field, owner, { allowEmpty = false } = {}) {
const value = object[field];
if (
!Array.isArray(value) ||
(!allowEmpty && value.length === 0) ||
!value.every((entry) => typeof entry === "string" && entry.length > 0)
) {
throw new Error(
`${owner} ${field} must be ${allowEmpty ? "an" : "a non-empty"} array of strings`,
);
}
}
/**
* Require cited implementation or test evidence to be a repository file.
*
* @param {string} root Repository root.
* @param {string} areaId Coverage area identifier.
* @param {string} kind Evidence kind shown in validation errors.
* @param {string} relativePath Repository-relative evidence path.
* @returns {Promise<void>}
*/
async function assertCoverageEvidence(root, areaId, kind, relativePath) {
const target = path.resolve(root, relativePath);
const relative = path.relative(root, target);
if (relative.startsWith("..") || path.isAbsolute(relative)) {
throw new Error(
`${areaId} cites ${kind} outside the repository: ${relativePath}`,
);
}
let stats;
try {
stats = await fs.stat(target);
} catch {
throw new Error(`${areaId} cites missing ${kind}: ${relativePath}`);
}
if (!stats.isFile()) {
throw new Error(`${areaId} cites non-file ${kind}: ${relativePath}`);
}
}
/**
* Add coverage assignments while rejecting duplicate owners.
*
* @param {Set<string>} target Accumulated assignments.
* @param {string[]} values New settings routes or MCP tools.
* @param {string} label Surface name shown in validation errors.
* @returns {void}
*/
function addUniqueCoverageValues(target, values, label) {
for (const value of values) {
if (target.has(value)) {
throw new Error(
`coverage.json assigns ${label} more than once: ${value}`,
);
}
target.add(value);
}
}
/**
* Parse intentionally excluded surfaces and require a durable reason for each.
*
* @param {object[]} entries Coverage exclusion records.
* @param {string} key Property containing the excluded surface identifier.
* @param {string} label Surface name shown in validation errors.
* @returns {Set<string>} Excluded surface identifiers.
*/
function validateCoverageExclusions(entries, key, label) {
if (!Array.isArray(entries)) {
throw new Error(`coverage.json excluded ${label}s must be an array`);
}
const values = new Set();
for (const entry of entries) {
if (
!entry ||
typeof entry !== "object" ||
typeof entry[key] !== "string" ||
typeof entry.reason !== "string" ||
entry.reason.trim().length < 8
) {
throw new Error(
`coverage.json excluded ${label}s require ${key} and a reason`,
);
}
if (values.has(entry[key])) {
throw new Error(
`coverage.json excludes ${label} more than once: ${entry[key]}`,
);
}
values.add(entry[key]);
}
return values;
}
/**
* Extract the shipped Settings route registry from its typed frontend table.
*
* @param {string} root Repository root.
* @returns {Promise<Set<string>>} Registered Settings routes.
*/
async function collectSettingsRoutes(root) {
const source = await fs.readFile(
path.join(root, "apps/web/src/settings-routes.tsx"),
"utf8",
);
const routeTable = source.match(
/const SETTINGS_ROUTES[\s\S]*?\n};\s*\n\s*export function SettingsRoutes/,
)?.[0];
if (!routeTable) {
throw new Error("could not locate SETTINGS_ROUTES in settings-routes.tsx");
}
return new Set(
[...routeTable.matchAll(/^\s*"(\/settings(?:\/[^"]*)?)":/gm)].map(
(match) => match[1],
),
);
}
/**
* Extract every registered Kandev MCP tool name from backend server sources.
*
* @param {string} root Repository root.
* @returns {Promise<Set<string>>} Registered MCP tool names.
*/
async function collectMcpTools(root) {
const serverDir = path.join(root, "apps/backend/internal/mcp/server");
const files = await collectFilesWithExtension(serverDir, ".go");
const tools = new Set();
for (const file of files) {
const source = await fs.readFile(file, "utf8");
for (const match of source.matchAll(
/mcp\.NewTool(?:WithRawSchema)?\(\s*"([^"]+)"/g,
)) {
tools.add(match[1]);
}
}
return tools;
}
/**
* Recursively collect regular files with a requested extension.
*
* @param {string} dir Directory to traverse.
* @param {string} extension Filename extension including the leading dot.
* @returns {Promise<string[]>} Absolute matching file paths.
*/
async function collectFilesWithExtension(dir, extension) {
const entries = await fs.readdir(dir, { withFileTypes: true });
const files = await Promise.all(
entries.map(async (entry) => {
const target = path.join(dir, entry.name);
if (entry.isDirectory())
return collectFilesWithExtension(target, extension);
return entry.isFile() && entry.name.endsWith(extension) ? [target] : [];
}),
);
return files.flat();
}
/**
* Require shipped surfaces to form a disjoint covered-or-excluded partition.
*
* @param {Set<string>} shipped Surfaces registered by the application.
* @param {Set<string>} covered Surfaces assigned to documentation areas.
* @param {Set<string>} excluded Surfaces intentionally excluded with reasons.
* @param {string} label Surface name shown in validation errors.
* @returns {void}
*/
function assertCompleteSurfaceCoverage(shipped, covered, excluded, label) {
for (const value of covered) {
if (excluded.has(value)) {
throw new Error(`coverage.json both covers and excludes ${label}: ${value}`);
}
}
for (const value of shipped) {
if (!covered.has(value) && !excluded.has(value)) {
throw new Error(`coverage.json does not account for ${label}: ${value}`);
}
}
for (const value of [...covered, ...excluded]) {
if (!shipped.has(value)) {
throw new Error(`coverage.json references unknown ${label}: ${value}`);
}
}
}
/**
* Read and validate the shape of public navigation metadata.
*
@@ -128,6 +805,28 @@ function assertFrontmatter(file, markdown) {
}
}
/**
* Require one real page title immediately after frontmatter.
*
* @param {string} file Relative page path used in validation errors.
* @param {string} markdown Page source.
* @returns {void}
*/
function assertDocumentStructure(file, markdown) {
const frontmatter = markdown.match(
/^---\r?\n[\s\S]*?\r?\n---(?:\r?\n|$)/,
)?.[0];
const body = stripMarkdownCode(
markdown.slice(frontmatter?.length ?? 0),
).trimStart();
const headings = [...body.matchAll(/^# [^\n]+$/gm)];
if (!body.startsWith("# ") || headings.length !== 1) {
throw new Error(
`${file} must begin with exactly one level-one heading after frontmatter`,
);
}
}
/**
* Require every relative Markdown link or image to resolve on disk.
*
@@ -142,6 +841,7 @@ async function assertLocalLinks(docsDir, file, markdown) {
const referencePattern = /!?\[([^\]\n]+)\]\[([^\]\n]*)\]/g;
const shortcutReferencePattern = /(?<![!\\\[\]])\[([^\]\n]+)\](?![\[(:])/g;
const destinations = collectInlineLinkDestinations(source);
destinations.push(...collectDocsVideoDestinations(source));
const definitions = new Map();
for (const match of source.matchAll(definitionPattern)) {
@@ -176,7 +876,7 @@ async function assertLocalLinks(docsDir, file, markdown) {
for (const destination of destinations) {
const href = parseMarkdownDestination(destination);
if (!href || href.startsWith("#") || isExternalDestination(href)) {
if (!href || isExternalDestination(href)) {
continue;
}
if (href.startsWith("/")) {
@@ -185,28 +885,107 @@ async function assertLocalLinks(docsDir, file, markdown) {
);
}
const pathOnly = href.split(/[?#]/, 1)[0];
if (!pathOnly) continue;
const hashIndex = href.indexOf("#");
const pathAndQuery = hashIndex === -1 ? href : href.slice(0, hashIndex);
const rawFragment = hashIndex === -1 ? "" : href.slice(hashIndex + 1);
const pathOnly = pathAndQuery.split("?", 1)[0];
let decoded;
try {
decoded = decodeURIComponent(pathOnly);
} catch {
throw new Error(`${file} contains an invalid encoded local link: ${href}`);
throw new Error(
`${file} contains an invalid encoded local link: ${href}`,
);
}
const target = path.resolve(
path.dirname(path.join(docsDir, file)),
decoded.replace(/\\([\\() ])/g, "$1"),
);
const target = pathOnly
? path.resolve(
path.dirname(path.join(docsDir, file)),
decoded.replace(/\\([\\() ])/g, "$1"),
)
: path.join(docsDir, file);
try {
await fs.access(target);
} catch {
throw new Error(`${file} links to missing local target: ${href}`);
}
if (rawFragment && /\.mdx?$/i.test(target)) {
await assertHeadingFragment(file, href, target, rawFragment);
}
}
}
/**
* Require a local Markdown fragment to match the published heading identifier.
*
* @param {string} file Relative source path used in validation errors.
* @param {string} href Original link destination.
* @param {string} target Absolute target Markdown path.
* @param {string} rawFragment URL-encoded fragment without the hash marker.
* @returns {Promise<void>}
*/
async function assertHeadingFragment(file, href, target, rawFragment) {
let fragment;
try {
fragment = decodeURIComponent(rawFragment);
} catch {
throw new Error(`${file} contains an invalid encoded local link: ${href}`);
}
const markdown = await fs.readFile(target, "utf8");
if (!collectHeadingAnchors(markdown).has(fragment)) {
throw new Error(`${file} links to missing heading: ${href}`);
}
}
/**
* Collect the GitHub-style identifiers emitted for Markdown headings.
*
* Duplicate headings receive the same numeric suffix used by rehype-slug.
*
* @param {string} markdown Markdown page source.
* @returns {Set<string>} Published heading identifiers.
*/
function collectHeadingAnchors(markdown) {
const source = stripMarkdownCode(markdown, { keepInlineCode: true });
const anchors = new Set();
const counts = new Map();
for (const match of source.matchAll(/^ {0,3}#{1,6}[ \t]+(.+?)\s*#*\s*$/gm)) {
const base = stripHeadingMarkup(match[1])
.toLowerCase()
.replace(/[^\p{L}\p{N}\s_-]/gu, "")
.replace(/\s+/g, "-");
if (!base) continue;
const duplicateIndex = counts.get(base) ?? 0;
counts.set(base, duplicateIndex + 1);
anchors.add(duplicateIndex === 0 ? base : `${base}-${duplicateIndex}`);
}
return anchors;
}
/**
* Collect source and poster paths from the supported DocsVideo MDX component.
*
* @param {string} markdown Markdown with code regions removed.
* @returns {string[]} Local or external media destinations.
*/
function collectDocsVideoDestinations(markdown) {
const destinations = [];
for (const tag of markdown.matchAll(/<DocsVideo\b[\s\S]*?\/>/g)) {
for (const attribute of tag[0].matchAll(
/\b(?:webm|mp4|poster)=(?:"([^"]+)"|'([^']+)')/g,
)) {
destinations.push(attribute[1] ?? attribute[2]);
}
}
return destinations;
}
/**
* Collect inline Markdown destinations while preserving balanced parentheses.
*
@@ -269,7 +1048,11 @@ function collectInlineLinkDestinations(markdown) {
*/
function isEscaped(value, index) {
let backslashes = 0;
for (let cursor = index - 1; cursor >= 0 && value[cursor] === "\\"; cursor -= 1) {
for (
let cursor = index - 1;
cursor >= 0 && value[cursor] === "\\";
cursor -= 1
) {
backslashes += 1;
}
return backslashes % 2 === 1;
@@ -280,9 +1063,10 @@ function isEscaped(value, index) {
* live links.
*
* @param {string} markdown Page source.
* @param {{keepInlineCode?: boolean}} [options] Inline-code handling.
* @returns {string} Markdown with code regions removed.
*/
function stripMarkdownCode(markdown) {
function stripMarkdownCode(markdown, { keepInlineCode = false } = {}) {
let fence = null;
let indentedCodeIndent = null;
let canStartIndentedCode = true;
@@ -329,20 +1113,22 @@ function stripMarkdownCode(markdown) {
return "";
}
const listMarker = line.match(
/^([ \t]*)(?:[-+*]|\d{1,9}[.)])([ \t]+)/,
);
const listMarker = line.match(/^([ \t]*)(?:[-+*]|\d{1,9}[.)])([ \t]+)/);
if (listMarker) {
listContentIndents.push(columnWidth(listMarker[0]));
}
canStartIndentedCode = /^(?: {0,3}#{1,6}(?:[ \t]+|$)| {0,3}(?:=+|-+)[ \t]*$| {0,3}\[[^\]\n]+\]:)/.test(
line,
);
canStartIndentedCode =
/^(?: {0,3}#{1,6}(?:[ \t]+|$)| {0,3}(?:=+|-+)[ \t]*$| {0,3}\[[^\]\n]+\]:)/.test(
line,
);
return line;
});
return lines.join("\n").replace(/`+[^`\n]*`+/g, "");
const withoutBlocks = lines.join("\n");
return keepInlineCode
? withoutBlocks
: withoutBlocks.replace(/`+[^`\n]*`+/g, "");
}
/**