import { createHash } from "node:crypto"; import fs from "node:fs/promises"; import path from "node:path"; import { fileURLToPath, pathToFileURL } from "node:url"; import { scanPublicDocsEmDashViolations } from "../apps/web/scripts/check-no-em-dash-ui.mjs"; 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 * exactly once in the public navigation metadata. * * @param {string} [docsDir] Directory containing published docs and meta.json. * @returns {Promise<{pageCount: number}>} Number of validated published pages. */ export async function validatePublicDocs(docsDir = defaultDocsDir) { const emDashViolations = scanPublicDocsEmDashViolations({ docsRoot: docsDir, root: docsDir, }); if (emDashViolations.length > 0) { const locations = emDashViolations.map( ({ file, line }) => `${file}:${line}`, ); throw new Error( `public docs contain em dash (U+2014): ${locations.join(", ")}`, ); } const meta = await readMeta(docsDir); const files = await collectMarkdownFiles(docsDir); const pagesBySlug = new Map(); for (const file of files) { const markdown = await fs.readFile(path.join(docsDir, file), "utf8"); await assertLocalLinks(docsDir, file, markdown); if (path.posix.basename(file).toLowerCase() === "readme.md") continue; assertFrontmatter(file, markdown); assertDocumentStructure(file, markdown); assertExperimentalCalloutPlacement(file, markdown); const slug = file.replace(/\.mdx?$/, "").replace(/\/index$/, ""); const existing = pagesBySlug.get(slug); if (existing) { throw new Error( `multiple published files resolve to slug ${slug}: ${existing}, ${file}`, ); } pagesBySlug.set(slug, file); } const listed = new Set(); for (const entry of meta.pages) { if (isNavigationDecoration(entry)) continue; if (listed.has(entry)) { throw new Error(`meta.json lists page more than once: ${entry}`); } if (!pagesBySlug.has(entry)) { throw new Error(`meta.json references unknown page: ${entry}`); } listed.add(entry); } for (const slug of pagesBySlug.keys()) { if (!listed.has(slug)) { throw new Error(`meta.json is missing published page: ${slug}`); } } 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 : ""; 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} filenames Expected media filenames by format. * @returns {boolean} Whether one DocsVideo embeds the complete media triplet. */ function hasCompleteFeatureMediaEmbed(markdown, filenames) { return [ ...stripMarkdownCode(markdown).matchAll(//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} 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} 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} */ 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} 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} 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>} 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\S]*?\nexport (?:const SETTINGS_ROUTE_PATHS|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>} 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} 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} shipped Surfaces registered by the application. * @param {Set} covered Surfaces assigned to documentation areas. * @param {Set} 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. * * @param {string} docsDir Directory containing meta.json. * @returns {Promise<{pages: string[]}>} Parsed navigation metadata. */ async function readMeta(docsDir) { const raw = await fs.readFile(path.join(docsDir, "meta.json"), "utf8"); const meta = JSON.parse(raw); if (!meta || typeof meta !== "object" || Array.isArray(meta)) { throw new Error("meta.json must contain a JSON object"); } if ( !Array.isArray(meta.pages) || !meta.pages.every((entry) => typeof entry === "string") ) { throw new Error("meta.json pages must be an array of strings"); } return meta; } /** * Recursively collect Markdown paths relative to the published docs root. * * @param {string} dir Published docs root. * @param {string} [relativeDir] Directory relative to the docs root. * @returns {Promise} Sorted relative Markdown paths. */ async function collectMarkdownFiles(dir, relativeDir = "") { const entries = await fs.readdir(path.join(dir, relativeDir), { withFileTypes: true, }); const files = await Promise.all( entries.map(async (entry) => { const relativePath = path.posix.join(relativeDir, entry.name); if (entry.isDirectory()) { return collectMarkdownFiles(dir, relativePath); } return /\.mdx?$/.test(entry.name) ? [relativePath] : []; }), ); return files.flat().sort(); } /** * Require non-empty title and description fields in leading frontmatter. * * @param {string} file Relative page path used in validation errors. * @param {string} markdown Page source. * @returns {void} */ function assertFrontmatter(file, markdown) { const block = markdown.match(/^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/)?.[1]; if ( !block || !/^title:\s*\S.*$/m.test(block) || !/^description:\s*\S.*$/m.test(block) ) { throw new Error( `${file} must start with YAML frontmatter containing title and description`, ); } const statusMatch = block.match(/^status:\s*(.*?)\s*$/m); if (statusMatch) { const status = statusMatch[1].replace(/^(["'])(.*)\1$/, "$2"); if (status !== "experimental") { throw new Error(`${file} has unsupported page status: ${status}`); } } } /** * 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`, ); } } /** * Keep experimental callouts attached to an explicit feature section. * A section-level indicator is the first content under its heading, followed * by the documentation it qualifies. * * @param {string} file Relative page path used in validation errors. * @param {string} markdown Page source. * @returns {void} */ function assertExperimentalCalloutPlacement(file, markdown) { const source = stripMarkdownCode(markdown); const headings = [...source.matchAll(/^(#{2,6})\s+([^\n]+?)\s*$/gm)]; const callouts = [...source.matchAll(/^>\s*\[!EXPERIMENTAL\]\s*$/gim)]; for (const callout of callouts) { const headingIndex = headings.findLastIndex( (candidate) => candidate.index < callout.index, ); const heading = headings[headingIndex]; if ( !heading || source.slice(heading.index + heading[0].length, callout.index).trim() ) { throw new Error( `${file} experimental callouts must immediately follow a descriptive heading`, ); } const title = heading[2].trim(); const level = heading[1].length; const nextHeading = headings .slice(headingIndex + 1) .find((candidate) => candidate[1].length <= level); const sectionEnd = nextHeading?.index ?? source.length; const calloutBlock = source .slice(callout.index) .match(/^>\s*\[!EXPERIMENTAL\]\s*\r?\n(?:^>.*(?:\r?\n|$))*/im)?.[0]; const sectionContent = source .slice( callout.index + (calloutBlock?.length ?? callout[0].length), sectionEnd, ) .trim(); if (!sectionContent) { throw new Error( `${file} has an experimental callout without substantive section content: ${title}`, ); } } } /** * Require every relative Markdown link or image to resolve on disk. * * @param {string} docsDir Published docs root. * @param {string} file Relative source path used in validation errors. * @param {string} markdown Page source. * @returns {Promise} */ async function assertLocalLinks(docsDir, file, markdown) { const source = stripMarkdownCode(markdown); const definitionPattern = /^\s{0,3}\[([^\]\n]+)\]:\s*(\S.*)$/gm; const referencePattern = /!?\[([^\]\n]+)\]\[([^\]\n]*)\]/g; const shortcutReferencePattern = /(?} */ 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} 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(//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. * * @param {string} markdown Markdown with code regions removed. * @returns {string[]} Raw content inside each link's parentheses. */ function collectInlineLinkDestinations(markdown) { const destinations = []; for (let start = 0; start < markdown.length; start += 1) { if (markdown[start] !== "[" || isEscaped(markdown, start)) continue; let bracketDepth = 1; let labelEnd = -1; for (let cursor = start + 1; cursor < markdown.length; cursor += 1) { const character = markdown[cursor]; if (character === "\n" || character === "\r") break; if (character === "\\") { cursor += 1; } else if (character === "[") { bracketDepth += 1; } else if (character === "]") { bracketDepth -= 1; if (bracketDepth === 0) { labelEnd = cursor; break; } } } if (labelEnd === -1 || markdown[labelEnd + 1] !== "(") continue; let parenthesisDepth = 1; for (let cursor = labelEnd + 2; cursor < markdown.length; cursor += 1) { const character = markdown[cursor]; if (character === "\n" || character === "\r") break; if (character === "\\") { cursor += 1; } else if (character === "(") { parenthesisDepth += 1; } else if (character === ")") { parenthesisDepth -= 1; if (parenthesisDepth === 0) { destinations.push(markdown.slice(labelEnd + 2, cursor)); break; } } } } return destinations; } /** * Return whether punctuation is preceded by an odd number of backslashes. * * @param {string} value Source text. * @param {number} index Character index. * @returns {boolean} Whether the character is escaped. */ function isEscaped(value, index) { let backslashes = 0; for ( let cursor = index - 1; cursor >= 0 && value[cursor] === "\\"; cursor -= 1 ) { backslashes += 1; } return backslashes % 2 === 1; } /** * Remove fenced, indented, and inline code so examples are not treated as * live links. * * @param {string} markdown Page source. * @param {{keepInlineCode?: boolean}} [options] Inline-code handling. * @returns {string} Markdown with code regions removed. */ function stripMarkdownCode(markdown, { keepInlineCode = false } = {}) { let fence = null; let indentedCodeIndent = null; let canStartIndentedCode = true; const listContentIndents = []; const lines = markdown.split(/\r?\n/).map((line) => { const marker = line.match(/^\s*(`{3,}|~{3,})/)?.[1]; if (marker) { if (!fence) { fence = marker; } else if (marker[0] === fence[0] && marker.length >= fence.length) { fence = null; } canStartIndentedCode = true; return ""; } if (fence) return ""; const blank = /^\s*$/.test(line); const indent = leadingIndentWidth(line); if (indentedCodeIndent !== null) { if (blank || indent >= indentedCodeIndent) { canStartIndentedCode = blank; return ""; } indentedCodeIndent = null; } if (blank) { canStartIndentedCode = true; return line; } while ( listContentIndents.length > 0 && indent < listContentIndents.at(-1) ) { listContentIndents.pop(); } const requiredCodeIndent = (listContentIndents.at(-1) ?? 0) + 4; if (canStartIndentedCode && indent >= requiredCodeIndent) { indentedCodeIndent = requiredCodeIndent; canStartIndentedCode = false; return ""; } 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, ); return line; }); const withoutBlocks = lines.join("\n"); return keepInlineCode ? withoutBlocks : withoutBlocks.replace(/`+[^`\n]*`+/g, ""); } /** * Count indentation columns, expanding tabs to four-column stops. * * @param {string} value Source line or prefix. * @returns {number} Leading indentation width in columns. */ function leadingIndentWidth(value) { return columnWidth(value.match(/^[ \t]*/)[0]); } /** * Count source columns, expanding tabs to four-column stops. * * @param {string} value Source text. * @returns {number} Width in columns. */ function columnWidth(value) { let width = 0; for (const character of value) { if (character === "\t") { width += 4 - (width % 4); } else { width += 1; } } return width; } /** * Apply CommonMark's case-insensitive, whitespace-collapsing reference label rules. * * @param {string} label Raw reference label. * @returns {string} Normalized reference label. */ function normalizeReferenceLabel(label) { return label.trim().replace(/\s+/g, " ").toLowerCase(); } /** * Read the destination portion before an optional Markdown link title. * * @param {string} raw Raw content inside link parentheses. * @returns {string} Link destination. */ function parseMarkdownDestination(raw) { const value = raw.trim(); if (value.startsWith("<")) { const end = value.indexOf(">"); return end === -1 ? value : value.slice(1, end); } return value.split(/\s+/, 1)[0]; } /** * Return whether a destination has a URL scheme or protocol-relative host. * * @param {string} href Link destination. * @returns {boolean} Whether the destination is external. */ function isExternalDestination(href) { return href.startsWith("//") || /^[a-z][a-z\d+.-]*:/i.test(href); } /** * Return whether a metadata entry is a navigation heading. * * @param {string} entry Navigation metadata entry. * @returns {boolean} Whether the entry is a heading decoration. */ function isNavigationDecoration(entry) { return /^---.*---$/.test(entry); } if (import.meta.url === pathToFileURL(process.argv[1]).href) { validatePublicDocs() .then(({ pageCount }) => console.log(`Validated ${pageCount} published docs pages.`), ) .catch((error) => { console.error(error.message); process.exitCode = 1; }); }