{"version":3,"file":"content-validator-CEcRdx4H.mjs","names":["getTextContent","parseYaml"],"sources":["../src/validation/schema-validator.ts","../src/validation/code-block-validator.ts","../src/validation/heading-validator.ts","../src/validation/image-structure-validator.ts","../src/validation/link-validator.ts","../src/validation/runner.ts","../src/validation/content-validator.ts"],"sourcesContent":["/**\n * Schema validation with rich error reporting.\n *\n * Wraps Zod's safeParse to produce human-readable validation issues.\n */\n\nimport type { ZodError, ZodType } from \"zod\";\n\nexport type ValidationIssue = {\n  /** Field path (e.g. 'tags[0]') */\n  field?: string;\n  /** Human-readable error message */\n  message: string;\n  /** Error severity */\n  severity: \"error\" | \"warn\";\n  /** Origin of this issue — helps consumers distinguish validation phases. */\n  source?: \"schema\" | \"content\" | \"plugin\" | \"custom\";\n};\n\nexport type ValidationEntryResult = {\n  slug: string;\n  filePath: string;\n  issues: ValidationIssue[];\n};\n\nexport type ValidationResult = {\n  collection: string;\n  entries: ValidationEntryResult[];\n  errors: number;\n  warnings: number;\n};\n\n/** Format a Zod error path into a human-readable field path. */\nexport function formatPath(path: PropertyKey[]): string {\n  return path\n    .map((segment, i) => {\n      if (typeof segment === \"number\") return `[${segment}]`;\n      if (typeof segment === \"symbol\") return `[${String(segment)}]`;\n      if (i === 0) return segment;\n      return `.${segment}`;\n    })\n    .join(\"\");\n}\n\n/** Validate data against a Zod schema and return structured issues. */\nexport function validateSchema(\n  data: Record<string, any>,\n  schema: ZodType,\n): {\n  issues: ValidationIssue[];\n  validatedData: any;\n} {\n  const result = schema.safeParse(data);\n  if (result.success) {\n    return {\n      issues: [],\n      validatedData: result.data,\n    };\n  }\n\n  const issues = (result.error as ZodError).issues.map((issue) => ({\n    field: issue.path.length > 0 ? formatPath(issue.path) : undefined,\n    message: issue.message,\n    severity: \"error\" as const,\n    source: \"schema\" as const,\n  }));\n\n  // On failure, return raw data so the pipeline can continue in non-strict mode.\n  // The issues array signals that validation did not pass.\n  return {\n    issues,\n    validatedData: data,\n  };\n}\n","/**\n * Code block validator — checks fenced code block meta syntax.\n *\n * Walks the shared MDAST for `code` nodes. Validates known meta properties\n * and language identifiers. Meta syntax follows Pagesmith's built-in code\n * renderer conventions.\n */\n\nimport type { ValidationIssue } from \"./schema-validator\";\nimport type { ContentValidator, MdastNode, ResolvedValidatorContext } from \"./types\";\n\n/** Known meta properties accepted by the built-in Pagesmith code renderer. */\nconst KNOWN_META_PROPS = new Set([\n  \"title\",\n  \"showLineNumbers\",\n  \"startLineNumber\",\n  \"wrap\",\n  \"frame\",\n  \"collapse\",\n  \"mark\",\n  \"ins\",\n  \"del\",\n]);\n\n/** Meta properties that accept line range values like {1-5,8,10-12}. */\nconst LINE_RANGE_PROPS = new Set([\"mark\", \"ins\", \"del\", \"collapse\"]);\n\n/** Extract meta tokens as key-value pairs from a meta string. */\nfunction extractMetaTokens(meta: string): Array<{ key: string; value?: string }> {\n  const tokens: Array<{ key: string; value?: string }> = [];\n\n  const tokenRegex = /(\\w+)(?:=(\\{[^}]*\\}|\"[^\"]*\"|'[^']*'|\\S+))?/g;\n  let match: RegExpExecArray | null;\n  while ((match = tokenRegex.exec(meta)) !== null) {\n    const raw = match[2];\n    let value: string | undefined;\n    if (raw !== undefined) {\n      // Strip surrounding braces, quotes\n      if (\n        (raw.startsWith(\"{\") && raw.endsWith(\"}\")) ||\n        (raw.startsWith('\"') && raw.endsWith('\"')) ||\n        (raw.startsWith(\"'\") && raw.endsWith(\"'\"))\n      ) {\n        value = raw.slice(1, -1);\n      } else {\n        value = raw;\n      }\n    }\n    tokens.push({ key: match[1]!, value });\n  }\n\n  return tokens;\n}\n\n/** Validate that a line range value contains only valid tokens. */\nfunction findMalformedRangeTokens(value: string): string[] {\n  const bad: string[] = [];\n  for (const part of value.split(\",\")) {\n    const token = part.trim();\n    if (!token) continue;\n    if (/^\\d+$/.test(token)) continue;\n    if (/^\\d+\\s*-\\s*\\d+$/.test(token)) continue;\n    bad.push(token);\n  }\n  return bad;\n}\n\n/** Collect all `code` nodes from MDAST. */\nfunction collectCodeBlocks(node: MdastNode): MdastNode[] {\n  const blocks: MdastNode[] = [];\n\n  if (node.type === \"code\") {\n    blocks.push(node);\n  }\n\n  if (node.children) {\n    for (const child of node.children) {\n      blocks.push(...collectCodeBlocks(child));\n    }\n  }\n\n  return blocks;\n}\n\nexport const codeBlockValidator: ContentValidator = {\n  name: \"code-blocks\",\n\n  validate(ctx: ResolvedValidatorContext): ValidationIssue[] {\n    if (!ctx.rawContent) return [];\n\n    const issues: ValidationIssue[] = [];\n    const tree = ctx.mdast as MdastNode;\n\n    const codeBlocks = collectCodeBlocks(tree);\n\n    for (const block of codeBlocks) {\n      const line = block.position?.start.line;\n      const lineInfo = line ? ` (line ${line})` : \"\";\n      const meta = block.meta ?? \"\";\n      const hasMeta = meta.trim().length > 0;\n\n      // Language required when using syntax features\n      if (hasMeta && !block.lang) {\n        issues.push({\n          field: `code-block${lineInfo}`,\n          message: \"Code block has meta properties but no language identifier\",\n          severity: \"warn\",\n        });\n      }\n\n      if (!hasMeta) continue;\n\n      // Check for unknown meta properties and malformed line ranges\n      const tokens = extractMetaTokens(meta);\n      for (const { key, value } of tokens) {\n        if (!KNOWN_META_PROPS.has(key)) {\n          issues.push({\n            field: `code-block${lineInfo}`,\n            message: `Unknown code block meta property: \"${key}\"`,\n            severity: \"warn\",\n          });\n        }\n\n        if (LINE_RANGE_PROPS.has(key) && typeof value === \"string\" && value.length > 0) {\n          const bad = findMalformedRangeTokens(value);\n          for (const token of bad) {\n            issues.push({\n              field: `code-block${lineInfo}`,\n              message: `Malformed line range \"${token}\" in ${key}={...} — expected a number or range like 1-5`,\n              severity: \"warn\",\n            });\n          }\n        }\n      }\n    }\n\n    return issues;\n  },\n};\n","/**\n * Heading validator — checks heading structure in markdown.\n *\n * Walks the shared MDAST for heading nodes. Validates level ordering and h1 uniqueness.\n */\n\nimport type { ValidationIssue } from \"./schema-validator\";\nimport type { ContentValidator, MdastNode, ResolvedValidatorContext } from \"./types\";\n\n/** Extract plain text from a heading node's children. */\nfunction getTextContent(node: MdastNode): string {\n  // Text-bearing leaf nodes whose `value` carries user-visible characters.\n  // `inlineCode` in particular is common in heading text like `\\`npm run\\``\n  // and should not be treated as empty content.\n  if (node.type === \"text\" || node.type === \"inlineCode\") return node.value ?? \"\";\n  if (node.children) return node.children.map(getTextContent).join(\"\");\n  return \"\";\n}\n\n/** Collect all heading nodes from MDAST. */\nfunction collectHeadings(node: MdastNode): Array<{ depth: number; text: string; line?: number }> {\n  const headings: Array<{ depth: number; text: string; line?: number }> = [];\n\n  if (node.type === \"heading\" && node.depth) {\n    headings.push({\n      depth: node.depth,\n      text: getTextContent(node),\n      line: node.position?.start.line,\n    });\n  }\n\n  if (node.children) {\n    for (const child of node.children) {\n      headings.push(...collectHeadings(child));\n    }\n  }\n\n  return headings;\n}\n\nexport const headingValidator: ContentValidator = {\n  name: \"headings\",\n\n  validate(ctx: ResolvedValidatorContext): ValidationIssue[] {\n    if (!ctx.rawContent) return [];\n\n    const issues: ValidationIssue[] = [];\n    const tree = ctx.mdast as MdastNode;\n\n    const headings = collectHeadings(tree);\n\n    // No headings in a document with content is worth noting\n    if (headings.length === 0) {\n      const hasContent = ctx.rawContent.trim().length > 0;\n      if (hasContent) {\n        issues.push({\n          message: \"Document has content but no headings\",\n          severity: \"warn\",\n        });\n      }\n      return issues;\n    }\n\n    // Empty heading text\n    for (const h of headings) {\n      if (!h.text.trim()) {\n        const lineInfo = h.line ? ` (line ${h.line})` : \"\";\n        issues.push({\n          field: `headings${lineInfo}`,\n          message: `Empty heading at level h${h.depth}`,\n          severity: \"warn\",\n        });\n      }\n    }\n\n    // At most one h1\n    const h1s = headings.filter((h) => h.depth === 1);\n    if (h1s.length > 1) {\n      for (const h of h1s.slice(1)) {\n        const lineInfo = h.line ? ` (line ${h.line})` : \"\";\n        issues.push({\n          field: `headings${lineInfo}`,\n          message: `Multiple h1 headings found: \"${h.text}\"`,\n          severity: \"warn\",\n        });\n      }\n    }\n\n    // No skipped levels (only flag when going deeper)\n    for (let i = 1; i < headings.length; i++) {\n      const prev = headings[i - 1]!;\n      const curr = headings[i]!;\n      if (curr.depth > prev.depth + 1) {\n        const lineInfo = curr.line ? ` (line ${curr.line})` : \"\";\n        issues.push({\n          field: `headings${lineInfo}`,\n          message: `Heading level skip: h${prev.depth} -> h${curr.depth} (\"${curr.text}\")`,\n          severity: \"warn\",\n        });\n      }\n    }\n\n    return issues;\n  },\n};\n","/**\n * Image structure validator — enforces the canonical HTML shape Pagesmith\n * emits (and accepts) for author-written image markup.\n *\n * The shape is:\n *\n *     <figure>\n *       <picture>\n *         <source ... />*       (zero or more <source> entries)\n *         <img ... />            (exactly one <img>)\n *       </picture>\n *       <figcaption> ... </figcaption>?   (optional)\n *     </figure>\n *\n * Or, for single-format assets that don't need responsive format fallbacks\n * (e.g. SVG, GIF), the `<picture>` layer may be omitted:\n *\n *     <figure>\n *       <img ... />\n *       <figcaption> ... </figcaption>?\n *     </figure>\n *\n * Validator checks against invalid markup authors have produced in\n * downstream repos (notably a `<figure>` nested *inside* `<picture>`, which\n * collapses theme-source fallbacks in every browser):\n *\n *   - No `<figure>` inside `<picture>`\n *   - No wrapping elements other than `<source>` / `<img>` (or comments)\n *     directly inside `<picture>`\n *   - Every `<picture>` contains at least one `<img>`\n *   - Every `<picture>` has at most one `<img>`\n *   - No unclosed `<picture>` (open-tag / close-tag count must match)\n *\n * Validation walks `html` MDAST nodes so fenced code blocks and inline code\n * (which legitimately *show* picture markup as documentation) are ignored.\n */\n\nimport type { ValidationIssue } from \"./schema-validator\";\nimport type { ContentValidator, MdastNode, ResolvedValidatorContext } from \"./types\";\n\nconst PICTURE_BLOCK_RE = /<picture\\b[^>]*>([\\s\\S]*?)<\\/picture\\s*>/gi;\nconst PICTURE_OPEN_RE = /<picture\\b/gi;\nconst PICTURE_CLOSE_RE = /<\\/picture\\b/gi;\nconst FIGURE_IN_PICTURE_RE = /<figure\\b/i;\nconst IMG_IN_PICTURE_RE = /<img\\b/gi;\nconst ALLOWED_IN_PICTURE_RE = /<(?!\\/?(?:source|img)\\b)\\/?([a-z][a-z0-9-]*)\\b/gi;\nconst COMMENT_RE = /<!--[\\s\\S]*?-->/g;\n// Fenced code blocks and inline code legitimately *show* `<picture>` markup\n// as documentation. Strip them from the raw-content sweep so author examples\n// are not reported as structural violations.\nconst FENCED_CODE_RE = /```[\\s\\S]*?```|~~~[\\s\\S]*?~~~/g;\nconst INLINE_CODE_RE = /`[^`\\n]*`/g;\n\n/**\n * Count newlines up to a given offset so the reported line number matches\n * the source markdown rather than the start of the raw HTML block.\n */\nfunction offsetToLine(text: string, offset: number, base: number): number {\n  let line = base;\n  for (let i = 0; i < offset; i += 1) if (text.charCodeAt(i) === 0x0a) line += 1;\n  return line;\n}\n\n/**\n * Inspect every `<picture>...</picture>` block inside a raw HTML chunk and\n * push an issue for any structural violation.\n */\nfunction checkHtmlChunk(html: string, baseLine: number, issues: ValidationIssue[]): void {\n  // Comments may legitimately contain anything; strip them first so we don't\n  // match `<figure>` / `<img>` text that happens to sit in a comment.\n  const stripped = html.replace(COMMENT_RE, (m) => \" \".repeat(m.length));\n\n  // Balance check: unclosed <picture> usually indicates a typo that would\n  // cascade into misleading \"figure inside picture\" reports, so surface it\n  // explicitly first.\n  const openCount = (stripped.match(PICTURE_OPEN_RE) ?? []).length;\n  const closeCount = (stripped.match(PICTURE_CLOSE_RE) ?? []).length;\n  if (openCount !== closeCount) {\n    const firstOpen = stripped.search(PICTURE_OPEN_RE);\n    const line = firstOpen >= 0 ? offsetToLine(stripped, firstOpen, baseLine) : baseLine;\n    issues.push({\n      field: `images (line ${line})`,\n      message: `Unbalanced <picture> tags (${openCount} opens vs ${closeCount} closes).`,\n      severity: \"error\",\n    });\n  }\n\n  PICTURE_BLOCK_RE.lastIndex = 0;\n  let m: RegExpExecArray | null;\n  while ((m = PICTURE_BLOCK_RE.exec(stripped)) !== null) {\n    const inner = m[1] ?? \"\";\n    const line = offsetToLine(stripped, m.index, baseLine);\n    const field = `images (line ${line})`;\n\n    if (FIGURE_IN_PICTURE_RE.test(inner)) {\n      issues.push({\n        field,\n        message:\n          \"<figure> is not allowed inside <picture>. Wrap <picture> with <figure>, not the other way around: `<figure><picture>…<img></picture><figcaption>…</figcaption></figure>`.\",\n        severity: \"error\",\n      });\n    }\n\n    IMG_IN_PICTURE_RE.lastIndex = 0;\n    const imgMatches = inner.match(IMG_IN_PICTURE_RE) ?? [];\n    if (imgMatches.length === 0) {\n      issues.push({\n        field,\n        message:\n          \"<picture> must contain a fallback <img>. Browsers use <img> as the default when no <source> matches.\",\n        severity: \"error\",\n      });\n    } else if (imgMatches.length > 1) {\n      issues.push({\n        field,\n        message: `<picture> must contain exactly one <img> (found ${imgMatches.length}).`,\n        severity: \"error\",\n      });\n    }\n\n    ALLOWED_IN_PICTURE_RE.lastIndex = 0;\n    const seenDisallowed = new Set<string>();\n    let em: RegExpExecArray | null;\n    while ((em = ALLOWED_IN_PICTURE_RE.exec(inner)) !== null) {\n      const tag = (em[1] ?? \"\").toLowerCase();\n      if (!tag || seenDisallowed.has(tag)) continue;\n      // Already reported via the dedicated figure check.\n      if (tag === \"figure\") continue;\n      seenDisallowed.add(tag);\n      issues.push({\n        field,\n        message: `<picture> should only contain <source> and <img> (found <${tag}>).`,\n        severity: \"error\",\n      });\n    }\n  }\n}\n\n/**\n * Walk MDAST for every raw-HTML-bearing node so we can inspect author\n * markup without re-parsing the source.\n */\nfunction walk(node: MdastNode, issues: ValidationIssue[]): void {\n  if (\n    (node.type === \"html\" || node.type === \"mdxJsxFlowElement\") &&\n    typeof node.value === \"string\"\n  ) {\n    const line = node.position?.start.line ?? 1;\n    checkHtmlChunk(node.value, line, issues);\n  }\n  if (node.children) {\n    for (const child of node.children) walk(child, issues);\n  }\n}\n\n/**\n * Replace a matched substring with an equivalent-length whitespace run so\n * downstream line-number reporting stays accurate after stripping.\n */\nfunction blankOut(text: string, re: RegExp): string {\n  return text.replace(re, (match) => match.replace(/[^\\n]/g, \" \"));\n}\n\n/**\n * Also sweep the raw markdown source to catch raw `<picture>` blocks that\n * a permissive MDAST parser may split across multiple nodes or strip from\n * the tree (rare, but defensive). Fenced code and inline code are blanked\n * out first so author examples like `` `<picture>` `` are not falsely\n * reported as unbalanced tags.\n */\nfunction checkRawContent(rawContent: string, issues: ValidationIssue[]): void {\n  const stripped = blankOut(blankOut(rawContent, FENCED_CODE_RE), INLINE_CODE_RE);\n  checkHtmlChunk(stripped, 1, issues);\n}\n\n/**\n * Validate image HTML structure in a single markdown entry. Exported so\n * callers that already parsed HTML themselves can reuse the rules.\n */\nexport function validateImageHtml(html: string, baseLine = 1): ValidationIssue[] {\n  const issues: ValidationIssue[] = [];\n  checkHtmlChunk(html, baseLine, issues);\n  return issues;\n}\n\nexport const imageStructureValidator: ContentValidator = {\n  name: \"image-structure\",\n\n  validate(ctx: ResolvedValidatorContext): ValidationIssue[] {\n    const issues: ValidationIssue[] = [];\n\n    const tree = ctx.mdast as MdastNode;\n    walk(tree, issues);\n\n    // Belt-and-suspenders: scan the raw markdown too, but de-duplicate so\n    // the same violation is not reported twice.\n    if (ctx.rawContent) {\n      const rawIssues: ValidationIssue[] = [];\n      checkRawContent(ctx.rawContent, rawIssues);\n      const seen = new Set(issues.map((i) => `${i.field}::${i.message}`));\n      for (const issue of rawIssues) {\n        const key = `${issue.field}::${issue.message}`;\n        if (seen.has(key)) continue;\n        seen.add(key);\n        issues.push(issue);\n      }\n    }\n\n    return issues;\n  },\n};\n","/**\n * Link validator — checks links in markdown content.\n *\n * Walks the shared MDAST for link/image nodes and:\n *  - Validates well-formed-ness of external URLs.\n *  - Resolves internal links against the containing file, or the configured\n *    `rootDir`/`basePath` for absolute site paths (e.g. `/guide/foo`).\n *  - Verifies that internal images exist on disk.\n *  - Optionally enforces a whitelist of internal link targets\n *    (e.g. \"internal links must point to other README.md files\").\n *  - Optionally fetches external URLs (links and images) to confirm a `2xx`\n *    response. This check is opt-in and bounded by concurrency + timeout.\n */\n\nimport { existsSync } from \"fs\";\nimport { dirname, extname, resolve } from \"path\";\nimport type { ValidationIssue } from \"./schema-validator\";\nimport type { ContentValidator, MdastNode, ResolvedValidatorContext } from \"./types\";\n\n/** Extract plain text from a node's children. */\nfunction getTextContent(node: MdastNode): string {\n  // `text` and `inlineCode` nodes are the main carriers of user-visible\n  // characters inside a link. Without inlineCode support, valid links like\n  // `` [`npm install`](https://npmjs.com) `` would be flagged as empty.\n  if (node.type === \"text\" || node.type === \"inlineCode\") return node.value ?? \"\";\n  // Linked images (e.g. `[![alt](img.svg)](https://example.com)`) are a\n  // common, accessible pattern. Fold the image's alt text into the link's\n  // visible-text surface so the link is not reported as empty.\n  if (node.type === \"image\") return node.alt ?? \"\";\n  if (node.children) return node.children.map(getTextContent).join(\"\");\n  return \"\";\n}\n\ntype CollectedLink = {\n  kind: \"link\" | \"image\";\n  url: string;\n  text: string;\n  alt?: string;\n  line?: number;\n};\n\n/** Walk MDAST tree, collecting link and image nodes. */\nfunction collectLinks(node: MdastNode): CollectedLink[] {\n  const links: CollectedLink[] = [];\n\n  if ((node.type === \"link\" || node.type === \"image\") && node.url) {\n    links.push({\n      kind: node.type,\n      url: node.url,\n      text: node.type === \"link\" ? getTextContent(node) : \"\",\n      alt: node.type === \"image\" ? node.alt : undefined,\n      line: node.position?.start.line,\n    });\n  }\n\n  if (node.children) {\n    for (const child of node.children) {\n      links.push(...collectLinks(child));\n    }\n  }\n\n  return links;\n}\n\nfunction isInternalLink(url: string): boolean {\n  if (url.startsWith(\"#\")) return false;\n  if (url.startsWith(\"http://\") || url.startsWith(\"https://\")) return false;\n  if (url.startsWith(\"//\")) return false;\n  if (url.startsWith(\"mailto:\")) return false;\n  if (url.startsWith(\"tel:\")) return false;\n  if (url.startsWith(\"data:\")) return false;\n  return true;\n}\n\nfunction isWellFormedUrl(url: string): boolean {\n  try {\n    new URL(url);\n    return true;\n  } catch {\n    return false;\n  }\n}\n\nfunction stripFragmentAndQuery(url: string): string {\n  return url.split(\"#\")[0]!.split(\"?\")[0]!;\n}\n\nexport type LinkValidatorOptions = {\n  /**\n   * URL prefixes or glob-like patterns to skip during internal/external link\n   * validation. Useful for known-dynamic routes or generated links.\n   */\n  skipPatterns?: string[];\n\n  /**\n   * Absolute root directory used to resolve site-absolute URLs (paths that\n   * start with `/`). When set, `/guide/foo` resolves to\n   * `<rootDir>/guide/foo`. When unset, absolute paths cannot be verified and\n   * are skipped.\n   */\n  rootDir?: string;\n\n  /**\n   * Site base path prefix (e.g. `/pagesmith`). When provided, it is stripped\n   * from site-absolute URLs before resolving against `rootDir`.\n   */\n  basePath?: string;\n\n  /**\n   * Extra filesystem roots used to resolve site-absolute URLs. Each entry\n   * maps a URL prefix to a directory. Any URL starting with `prefix` is\n   * checked for existence under `dir` before the link is flagged as broken.\n   *\n   * Typical use: register `publicDir` at prefix `/`, and each docs\n   * `assets[<prefix>]` entry at its configured prefix.\n   */\n  additionalRoots?: Array<{ prefix: string; dir: string }>;\n\n  /**\n   * Custom predicate for internal link targets. When provided and it returns\n   * false, an error is reported. Invoked with the cleaned URL (no query /\n   * fragment) and the resolved filesystem path.\n   */\n  allowInternalTarget?: (href: string, resolvedPath: string) => boolean;\n\n  /**\n   * When `true`, non-image internal links that do not resolve to a markdown\n   * file (`.md`, `.mdx`, or any `README.md` / `index.md` in a directory) are\n   * flagged as errors. Enforces the \"internal links must be to other local\n   * markdown\" rule while still allowing image links and links to static\n   * assets (which are reported through the image validator's channels).\n   */\n  internalLinksMustBeMarkdown?: boolean;\n\n  /**\n   * When `true`, every internal page link must be authored as a *relative*\n   * path ending in `.md` or `.mdx` (for example `./relative/README.md`,\n   * `./foo.md`, `../sibling/index.md`). Bare forms (`./foo`, `./foo/`) and\n   * site-absolute URLs (`/guide/foo`) are rejected so the source form is\n   * always an unambiguous, grep-able path to a real file on disk.\n   *\n   * Images, fragment-only hrefs, `mailto:`/`tel:`, external URLs, and any\n   * URL matching `skipPatterns` / resolving under an `additionalRoots`\n   * asset tree are exempt. The docs preset turns this on by default; the\n   * core default stays `false` so third-party consumers are not broken.\n   */\n  requireCanonicalInternalLinks?: boolean;\n\n  /**\n   * When `true`, every image must carry non-empty alt text. Default: `true`.\n   * Set to `false` to treat missing alt only as a warning (via the existing\n   * accessibility channel).\n   */\n  requireAltText?: boolean;\n\n  /**\n   * When `true`, fail if the markdown contains a raw `<img>` HTML tag.\n   * Authors should use the `![]()` Markdown syntax (or a MDX component) so\n   * the image pipeline can rewrite URLs, hash filenames, and emit\n   * theme-aware variants. Default: `true`.\n   */\n  forbidHtmlImgTag?: boolean;\n\n  /**\n   * When `true`, adjacent image references with `-light` / `-dark` filename\n   * suffixes must appear as a matched pair. Missing or mis-ordered pairs\n   * become errors so authors always ship both theme variants. Images with a\n   * `.invert` class-style suffix are exempt and accepted as single entries.\n   * Default: `true`.\n   */\n  requireThemeVariantPairs?: boolean;\n\n  /**\n   * When `true`, external (http/https) URLs are fetched and a non-2xx or\n   * failing response becomes a warning/error. Off by default so local\n   * validation stays fast and offline-friendly.\n   */\n  checkExternalReachability?: boolean;\n\n  /**\n   * Severity used when an external URL is unreachable (fetch fails or\n   * response status is not `2xx`). Default: `'warn'`.\n   */\n  unreachableSeverity?: \"warn\" | \"error\";\n\n  /** Request timeout (ms) when fetching external URLs. Default: 10000. */\n  fetchTimeoutMs?: number;\n\n  /**\n   * Upper bound on the number of concurrent external URL requests from a\n   * single validator invocation. Default: 8.\n   */\n  fetchConcurrency?: number;\n\n  /**\n   * Optional fetch implementation. Defaults to the global `fetch`. Accepts\n   * `HEAD` requests and falls back to `GET` automatically when `HEAD` fails.\n   */\n  fetchImpl?: typeof fetch;\n};\n\nfunction shouldSkip(skipPatterns: string[], url: string): boolean {\n  return skipPatterns.some((pattern) => {\n    if (pattern.includes(\"*\")) {\n      const regex = new RegExp(\"^\" + pattern.replace(/\\*/g, \".*\") + \"$\");\n      return regex.test(url);\n    }\n    return url.startsWith(pattern);\n  });\n}\n\n/**\n * Suffixes tried against each resolved base path. Supports link targets\n * authored without their extension (typical for docs sites where\n * `guide/foo` maps to either `guide/foo.md` or `guide/foo/README.md`).\n *\n * The markdown variants appear first so that resolution prefers a concrete\n * markdown file over a matching directory (directories satisfy `existsSync`\n * but should not pass the `internalLinksMustBeMarkdown` check).\n */\nconst MARKDOWN_CANDIDATE_SUFFIXES = [\n  \".md\",\n  \".mdx\",\n  \"/README.md\",\n  \"/README.mdx\",\n  \"/index.md\",\n  \"/index.mdx\",\n  \"\",\n];\n\nfunction expandCandidate(base: string): string[] {\n  const candidates: string[] = [];\n  for (const suffix of MARKDOWN_CANDIDATE_SUFFIXES) {\n    candidates.push(`${base}${suffix}`);\n  }\n  return candidates;\n}\n\n/**\n * Resolve a non-external href to a list of candidate filesystem paths.\n * Returns an empty array when the href cannot be resolved locally (e.g. a\n * site-absolute URL with no `rootDir` and no matching additional root).\n *\n * The link validator treats the link as valid if *any* candidate exists on\n * disk so that sites with multiple passthrough roots (content + public +\n * asset mappings) validate cleanly.\n */\nfunction resolveInternalCandidates(\n  url: string,\n  fileDir: string,\n  rootDir: string | undefined,\n  basePath: string | undefined,\n  additionalRoots: Array<{ prefix: string; dir: string }>,\n): string[] {\n  const cleanPath = stripFragmentAndQuery(url);\n  if (!cleanPath) return [];\n\n  if (cleanPath.startsWith(\"/\")) {\n    let localPath = cleanPath;\n    if (basePath) {\n      const trimmedBase = basePath.replace(/\\/+$/, \"\");\n      if (trimmedBase && localPath.startsWith(`${trimmedBase}/`)) {\n        localPath = localPath.slice(trimmedBase.length);\n      } else if (trimmedBase && localPath === trimmedBase) {\n        localPath = \"/\";\n      }\n    }\n    const bases: string[] = [];\n    if (rootDir) {\n      bases.push(resolve(rootDir, `.${localPath}`));\n    }\n    for (const root of additionalRoots) {\n      const prefix = root.prefix.endsWith(\"/\") ? root.prefix : `${root.prefix}/`;\n      const prefixedPath =\n        localPath === root.prefix || localPath === `${root.prefix}/`\n          ? \"/\"\n          : localPath.startsWith(prefix)\n            ? localPath.slice(prefix.length - 1)\n            : localPath === \"/\" && root.prefix === \"/\"\n              ? \"/\"\n              : null;\n      if (prefixedPath !== null) {\n        bases.push(resolve(root.dir, `.${prefixedPath}`));\n      }\n    }\n    const candidates: string[] = [];\n    for (const base of bases) candidates.push(...expandCandidate(base));\n    return candidates;\n  }\n\n  return expandCandidate(resolve(fileDir, cleanPath));\n}\n\n/**\n * Fetch a URL with a timeout. Tries HEAD first and falls back to GET when\n * HEAD returns a non-2xx status (some hosts reject HEAD requests).\n */\nasync function fetchStatus(\n  url: string,\n  opts: { timeoutMs: number; fetchImpl: typeof fetch },\n): Promise<{ ok: boolean; status: number | null; error?: string }> {\n  const controller = new AbortController();\n  const timer = setTimeout(() => controller.abort(), opts.timeoutMs);\n  try {\n    const resp = await opts.fetchImpl(url, {\n      method: \"HEAD\",\n      signal: controller.signal,\n      redirect: \"follow\",\n    });\n    if (resp.ok) return { ok: true, status: resp.status };\n\n    // Some servers don't support HEAD; retry with GET.\n    if ([400, 403, 404, 405, 501].includes(resp.status)) {\n      const getResp = await opts.fetchImpl(url, {\n        method: \"GET\",\n        signal: controller.signal,\n        redirect: \"follow\",\n      });\n      return { ok: getResp.ok, status: getResp.status };\n    }\n\n    return { ok: false, status: resp.status };\n  } catch (err) {\n    const message = err instanceof Error ? err.message : String(err);\n    return { ok: false, status: null, error: message };\n  } finally {\n    clearTimeout(timer);\n  }\n}\n\n/**\n * Map over a list with bounded concurrency so fetch-heavy validators do not\n * open hundreds of sockets at once.\n */\nasync function mapConcurrent<T, U>(\n  items: readonly T[],\n  concurrency: number,\n  fn: (item: T, index: number) => Promise<U>,\n): Promise<U[]> {\n  const results: U[] = Array.from({ length: items.length });\n  let cursor = 0;\n  const workers = Array.from({ length: Math.max(1, concurrency) }, async () => {\n    while (true) {\n      const index = cursor;\n      cursor += 1;\n      if (index >= items.length) return;\n      results[index] = await fn(items[index]!, index);\n    }\n  });\n  await Promise.all(workers);\n  return results;\n}\n\nconst MARKDOWN_EXTS = new Set([\".md\", \".mdx\"]);\n\nfunction isMarkdownFile(filePath: string): boolean {\n  return MARKDOWN_EXTS.has(extname(filePath).toLowerCase());\n}\n\n/**\n * Parse a filename to classify theme-variant behavior. Returns:\n *   - `variant: 'light' | 'dark'` and the shared base when the filename\n *     matches `<base>-light.<ext>` / `<base>-dark.<ext>`.\n *   - `variant: 'invert'` for `<base>.invert.<ext>` assets that render in\n *     either theme via CSS filters.\n *   - `null` when the filename does not participate in theming.\n */\nfunction classifyThemeVariant(\n  url: string,\n): { variant: \"light\" | \"dark\"; base: string; ext: string } | { variant: \"invert\" } | null {\n  const clean = stripFragmentAndQuery(url);\n  const ext = extname(clean);\n  if (!ext) return null;\n  const withoutExt = clean.slice(0, -ext.length);\n  const segments = withoutExt.split(\".\");\n  if (segments.length >= 2 && segments[segments.length - 1] === \"invert\") {\n    return { variant: \"invert\" };\n  }\n  const lightMatch = /^(.+)-light$/.exec(withoutExt);\n  if (lightMatch) return { variant: \"light\", base: lightMatch[1]!, ext };\n  const darkMatch = /^(.+)-dark$/.exec(withoutExt);\n  if (darkMatch) return { variant: \"dark\", base: darkMatch[1]!, ext };\n  return null;\n}\n\nexport function createLinkValidator(options?: LinkValidatorOptions): ContentValidator {\n  const skipPatterns = options?.skipPatterns ?? [];\n  const rootDir = options?.rootDir;\n  const basePath = options?.basePath;\n  const additionalRoots = options?.additionalRoots ?? [];\n  const allowInternalTarget = options?.allowInternalTarget;\n  const internalLinksMustBeMarkdown = options?.internalLinksMustBeMarkdown ?? false;\n  const requireCanonicalInternalLinks = options?.requireCanonicalInternalLinks ?? false;\n  const requireAltText = options?.requireAltText ?? true;\n  const forbidHtmlImgTag = options?.forbidHtmlImgTag ?? true;\n  const requireThemeVariantPairs = options?.requireThemeVariantPairs ?? true;\n  const checkExternalReachability = options?.checkExternalReachability ?? false;\n  const unreachableSeverity = options?.unreachableSeverity ?? \"warn\";\n  const fetchTimeoutMs = options?.fetchTimeoutMs ?? 10_000;\n  const fetchConcurrency = options?.fetchConcurrency ?? 8;\n  const fetchImpl = options?.fetchImpl ?? (globalThis.fetch as typeof fetch | undefined);\n\n  return {\n    name: \"links\",\n\n    async validate(ctx: ResolvedValidatorContext): Promise<ValidationIssue[]> {\n      if (!ctx.rawContent) return [];\n\n      const issues: ValidationIssue[] = [];\n      const tree = ctx.mdast as MdastNode;\n      const links = collectLinks(tree);\n      const fileDir = dirname(ctx.filePath);\n\n      // ── Block-level structural checks on the raw source ──\n      // Walk the MDAST for `html` nodes (raw HTML blocks/inline) that\n      // introduce `<img>` tags. MDAST already excludes fenced code blocks\n      // and inline code, so documentation examples that *show* an `<img>`\n      // inside a fence are not falsely flagged. `<img>` wrapped in a\n      // `<picture>` element is allowed because it is the canonical pattern\n      // for theme-aware or format-aware responsive images.\n      if (forbidHtmlImgTag) {\n        const imgTagRe = /<img\\b/gi;\n        const checkHtmlChunk = (value: string, lineOffset: number): void => {\n          // Remove every <picture>...</picture> block so inner <img> tags\n          // are treated as part of the accepted theme-variant pattern.\n          const withoutPictures = value.replace(/<picture\\b[\\s\\S]*?<\\/picture>/gi, (match) =>\n            // Preserve newline count so reported line numbers stay accurate.\n            \"\\n\".repeat((match.match(/\\n/g) ?? []).length),\n          );\n          imgTagRe.lastIndex = 0;\n          let m: RegExpExecArray | null;\n          while ((m = imgTagRe.exec(withoutPictures)) !== null) {\n            const prefix = withoutPictures.slice(0, m.index);\n            const line = lineOffset + prefix.split(\"\\n\").length - 1;\n            issues.push({\n              field: `images (line ${line})`,\n              message:\n                \"Raw <img> HTML tag found outside a <picture> wrapper. Use Markdown image syntax `![alt](url)` (or wrap with <picture> for theme variants) so the image pipeline can rewrite URLs and emit theme variants.\",\n              severity: \"error\",\n            });\n          }\n        };\n        const walk = (node: MdastNode): void => {\n          if ((node.type === \"html\" || node.type === \"mdxJsxFlowElement\") && node.value) {\n            checkHtmlChunk(node.value, node.position?.start.line ?? 1);\n          }\n          if (node.children) {\n            for (const child of node.children) walk(child);\n          }\n        };\n        walk(tree);\n      }\n\n      // ── Theme-variant pair check (adjacent -light / -dark images) ──\n      if (requireThemeVariantPairs) {\n        const imageNodes = links.filter((l) => l.kind === \"image\");\n        for (let i = 0; i < imageNodes.length; i += 1) {\n          const current = imageNodes[i]!;\n          const next = imageNodes[i + 1];\n          const classification = classifyThemeVariant(current.url);\n          if (!classification || classification.variant === \"invert\") continue;\n          const partnerVariant = classification.variant === \"light\" ? \"dark\" : \"light\";\n          const partnerBase = classification.base;\n          const partnerExt = classification.ext;\n          const lineInfo = current.line ? ` (line ${current.line})` : \"\";\n\n          if (!next) {\n            issues.push({\n              field: `images${lineInfo}`,\n              message: `Missing ${partnerVariant} partner for ${current.url} — expected an image with base \"${partnerBase}${partnerExt === \"\" ? \"\" : partnerExt}\" alongside it.`,\n              severity: \"error\",\n            });\n            continue;\n          }\n          const nextClass = classifyThemeVariant(next.url);\n          const partnerOk =\n            nextClass &&\n            \"base\" in nextClass &&\n            nextClass.variant === partnerVariant &&\n            nextClass.base === partnerBase &&\n            nextClass.ext === partnerExt;\n          if (!partnerOk) {\n            issues.push({\n              field: `images${lineInfo}`,\n              message: `${classification.variant} image ${current.url} must be immediately followed by its ${partnerVariant} counterpart.`,\n              severity: \"error\",\n            });\n          } else {\n            // Skip the partner so we do not re-flag it as orphaned.\n            i += 1;\n          }\n        }\n      }\n\n      // Collect external URLs for concurrent reachability checks later.\n      const externalChecks: Array<{ link: CollectedLink; url: string }> = [];\n\n      for (const link of links) {\n        const lineInfo = link.line ? ` (line ${link.line})` : \"\";\n\n        if (link.kind === \"image\" && requireAltText) {\n          const alt = link.alt ?? \"\";\n          if (!alt.trim()) {\n            issues.push({\n              field: `images${lineInfo}`,\n              message: `Image is missing alt text: ${link.url}`,\n              severity: \"error\",\n            });\n          }\n        }\n\n        if (link.kind === \"link\" && link.text !== undefined && !link.text.trim()) {\n          issues.push({\n            field: `links${lineInfo}`,\n            message: `Link has no visible text: ${link.url}`,\n            severity: \"warn\",\n          });\n        }\n\n        if (shouldSkip(skipPatterns, link.url)) continue;\n\n        // Canonical-form check runs before the resolver so bare `./foo` or\n        // `/guide/foo` links are flagged even when they happen to resolve\n        // (e.g. via `README.md` suffix expansion). Images are exempt\n        // because they legitimately point at static assets that do not\n        // have markdown extensions.\n        if (\n          link.kind === \"link\" &&\n          requireCanonicalInternalLinks &&\n          isInternalLink(link.url) &&\n          !link.url.startsWith(\"#\")\n        ) {\n          const cleanPath = stripFragmentAndQuery(link.url);\n          const exemptByAssetRoot = additionalRoots.some((root) => {\n            const prefix = root.prefix.endsWith(\"/\") ? root.prefix : `${root.prefix}/`;\n            return cleanPath === root.prefix || cleanPath.startsWith(prefix);\n          });\n          if (!exemptByAssetRoot) {\n            const isRelative = cleanPath.startsWith(\"./\") || cleanPath.startsWith(\"../\");\n            const endsWithMarkdown = /\\.(?:md|mdx)$/i.test(cleanPath);\n            if (!isRelative || !endsWithMarkdown) {\n              issues.push({\n                field: `links${lineInfo}`,\n                message: `Non-canonical internal link: ${link.url}. Internal page links must be authored as a relative path ending in \\`.md\\` (for example \\`./relative/README.md\\`) so the target is a real, grep-able file on disk.`,\n                severity: \"error\",\n              });\n              continue;\n            }\n          }\n        }\n\n        if (link.url.startsWith(\"http://\") || link.url.startsWith(\"https://\")) {\n          if (!isWellFormedUrl(link.url)) {\n            issues.push({\n              field: `links${lineInfo}`,\n              message: `Malformed external URL: ${link.url}`,\n              severity: \"warn\",\n            });\n            continue;\n          }\n          if (checkExternalReachability && fetchImpl) {\n            externalChecks.push({ link, url: link.url });\n          }\n          continue;\n        }\n\n        if (!isInternalLink(link.url)) continue;\n\n        const candidates = resolveInternalCandidates(\n          link.url,\n          fileDir,\n          rootDir,\n          basePath,\n          additionalRoots,\n        );\n        if (candidates.length === 0) continue;\n\n        const resolved = candidates.find((candidate) => existsSync(candidate));\n        if (!resolved) {\n          issues.push({\n            field: `${link.kind === \"image\" ? \"images\" : \"links\"}${lineInfo}`,\n            message:\n              link.kind === \"image\"\n                ? `Missing local image: ${link.url}`\n                : `Broken internal link: ${link.url}`,\n            severity: \"error\",\n          });\n          continue;\n        }\n\n        if (link.kind === \"link\" && allowInternalTarget) {\n          const cleanUrl = stripFragmentAndQuery(link.url);\n          if (cleanUrl && !allowInternalTarget(cleanUrl, resolved)) {\n            issues.push({\n              field: `links${lineInfo}`,\n              message: `Disallowed internal link target: ${link.url}`,\n              severity: \"error\",\n            });\n          }\n        }\n\n        if (link.kind === \"link\" && internalLinksMustBeMarkdown) {\n          // Links that resolve through a registered asset root (passthrough\n          // URLs like `/llms.txt`, `/prompts/setup-core.md`, `/schemas/*`)\n          // are intentionally non-page asset targets and must not trip\n          // the \"internal links must be markdown\" rule.\n          const cleanUrl = stripFragmentAndQuery(link.url);\n          const isAssetRootLink =\n            cleanUrl.startsWith(\"/\") &&\n            additionalRoots.some((root) => {\n              const prefix = root.prefix.endsWith(\"/\") ? root.prefix : `${root.prefix}/`;\n              return cleanUrl === root.prefix || cleanUrl.startsWith(prefix);\n            });\n          if (!isAssetRootLink && !isMarkdownFile(resolved)) {\n            issues.push({\n              field: `links${lineInfo}`,\n              message: `Internal link must point to a markdown file (got ${extname(resolved) || \"no-extension\"}): ${link.url}`,\n              severity: \"error\",\n            });\n          }\n        }\n      }\n\n      if (externalChecks.length > 0 && fetchImpl) {\n        const statuses = await mapConcurrent(externalChecks, fetchConcurrency, async (entry) => {\n          return fetchStatus(entry.url, { timeoutMs: fetchTimeoutMs, fetchImpl });\n        });\n        for (let i = 0; i < externalChecks.length; i += 1) {\n          const entry = externalChecks[i]!;\n          const status = statuses[i]!;\n          if (status.ok) continue;\n          const lineInfo = entry.link.line ? ` (line ${entry.link.line})` : \"\";\n          const detail = status.status\n            ? `status ${status.status}`\n            : `error ${status.error ?? \"fetch failed\"}`;\n          issues.push({\n            field: `${entry.link.kind === \"image\" ? \"images\" : \"links\"}${lineInfo}`,\n            message: `${entry.link.kind === \"image\" ? \"Hosted image\" : \"External URL\"} unreachable (${detail}): ${entry.url}`,\n            severity: unreachableSeverity,\n          });\n        }\n      }\n\n      return issues;\n    },\n  };\n}\n\nexport const linkValidator: ContentValidator = createLinkValidator();\n","/**\n * Validation runner — executes a list of content validators on an entry.\n *\n * Validators that throw are caught and converted to error-severity issues\n * so one failing validator does not block the rest.\n */\n\nimport type { Root } from \"mdast\";\nimport remarkParse from \"remark-parse\";\nimport { unified } from \"unified\";\nimport type { ValidationIssue } from \"./schema-validator\";\nimport type { ContentValidator, ResolvedValidatorContext, ValidatorContext } from \"./types\";\n\nimport { codeBlockValidator } from \"./code-block-validator\";\nimport { headingValidator } from \"./heading-validator\";\nimport { imageStructureValidator } from \"./image-structure-validator\";\nimport { linkValidator } from \"./link-validator\";\n\n/** The built-in validators for markdown content. */\nexport const builtinMarkdownValidators: ContentValidator[] = [\n  linkValidator,\n  codeBlockValidator,\n  headingValidator,\n  imageStructureValidator,\n];\n\n/** Parse raw markdown to MDAST once, reusing any pre-parsed tree. */\nfunction resolveContext(ctx: ValidatorContext): ResolvedValidatorContext {\n  const mdast: Root =\n    ctx.mdast ??\n    unified()\n      .use(remarkParse)\n      .parse(ctx.rawContent ?? \"\");\n  return { ...ctx, mdast };\n}\n\n/** Run all validators on a single content entry. */\nexport async function runValidators(\n  ctx: ValidatorContext,\n  validators: ContentValidator[],\n): Promise<ValidationIssue[]> {\n  // Parse the MDAST once before the loop so all validators share the same tree.\n  const resolved = resolveContext(ctx);\n\n  const issues: ValidationIssue[] = [];\n\n  for (const validator of validators) {\n    try {\n      const result = await validator.validate(resolved);\n      issues.push(...result.map((issue) => ({ ...issue, source: \"content\" as const })));\n    } catch (err) {\n      // Convert thrown errors into issues so one bad validator doesn't abort all\n      const message = err instanceof Error ? err.message : String(err);\n      issues.push({\n        message: `Validator \"${validator.name}\" threw: ${message}`,\n        severity: \"error\",\n        source: \"content\",\n      });\n    }\n  }\n\n  return issues;\n}\n","/**\n * High-level content validator — walk a content directory, parse every\n * markdown file, validate its frontmatter against an optional schema, and run\n * link / image / heading / code-block validators over the body.\n *\n * This API is intentionally dependency-light and synchronous-friendly so it\n * can be called from both the `pagesmith-site validate` and\n * `pagesmith-docs validate` CLIs. Network-based checks (external URL\n * reachability) are opt-in.\n *\n * The returned summary is structured so callers can render custom reports or\n * gate CI pipelines on the `errors` count.\n */\n\nimport { readFile } from \"fs/promises\";\nimport { basename, extname, relative } from \"path\";\nimport matter from \"gray-matter\";\nimport { parse as parseYaml } from \"yaml\";\nimport fg from \"fast-glob\";\nimport remarkParse from \"remark-parse\";\nimport { unified } from \"unified\";\nimport type { ZodType } from \"zod\";\nimport { codeBlockValidator } from \"./code-block-validator\";\nimport { headingValidator } from \"./heading-validator\";\nimport { imageStructureValidator } from \"./image-structure-validator\";\nimport { createLinkValidator, type LinkValidatorOptions } from \"./link-validator\";\nimport { runValidators } from \"./runner\";\nimport { validateSchema, type ValidationIssue } from \"./schema-validator\";\nimport type { ContentValidator } from \"./types\";\n\nexport type ValidateContentOptions = {\n  /**\n   * Absolute path to the content directory. All markdown files below this\n   * directory will be walked.\n   */\n  contentDir: string;\n\n  /**\n   * Glob patterns (relative to `contentDir`) used to locate markdown files.\n   * Default: `['**\\/*.md', '**\\/*.mdx']`.\n   */\n  include?: string[];\n\n  /** Glob patterns to exclude. Default: `['**\\/node_modules/**']`. */\n  exclude?: string[];\n\n  /**\n   * Optional Zod schema to validate the frontmatter of every file. Applies\n   * uniformly to every entry. Use {@link resolveFrontmatterSchema} when the\n   * schema differs by collection / file location.\n   *\n   * When omitted, only structural validators (links, images, headings, code\n   * blocks) run and frontmatter is accepted as-is.\n   */\n  frontmatterSchema?: ZodType;\n\n  /**\n   * Per-file frontmatter schema lookup. Invoked once per markdown file with\n   * the absolute path; return `undefined` to skip schema validation for that\n   * file (structural validators still run). Takes precedence over\n   * {@link frontmatterSchema}.\n   *\n   * Typical use: pair with `loadContentSchemaMap` to validate each file\n   * against the schema declared in the project's `content.config.ts`.\n   */\n  resolveFrontmatterSchema?: (filePath: string) => ZodType | undefined;\n\n  /**\n   * Forwarded to {@link createLinkValidator}. Useful for site-absolute links\n   * (set `rootDir` + `basePath`), restricting allowed internal targets, and\n   * opting into external URL reachability checks.\n   */\n  linkValidator?: LinkValidatorOptions;\n\n  /**\n   * Additional custom validators to run on every entry. They receive the\n   * parsed MDAST alongside the raw content and frontmatter.\n   */\n  extraValidators?: ContentValidator[];\n\n  /**\n   * Whether to include the built-in heading / code-block validators.\n   * Default: `true`.\n   */\n  includeStructuralValidators?: boolean;\n\n  /**\n   * Collection name recorded on results. Default: `'content'`.\n   */\n  collectionName?: string;\n};\n\nexport type ContentFileResult = {\n  /** Absolute source file path. */\n  filePath: string;\n  /** Path relative to `contentDir`. */\n  relativePath: string;\n  /** Human-readable slug derived from the file path. */\n  slug: string;\n  /** Issues produced by schema + structural validators. */\n  issues: ValidationIssue[];\n};\n\nexport type ValidateContentSummary = {\n  collection: string;\n  contentDir: string;\n  fileCount: number;\n  errors: number;\n  warnings: number;\n  results: ContentFileResult[];\n};\n\n/** Convert a relative markdown path into a slug. */\nfunction toSlug(relativePath: string): string {\n  const ext = extname(relativePath);\n  const withoutExt = ext ? relativePath.slice(0, -ext.length) : relativePath;\n  const normalized = withoutExt.replace(/\\\\/g, \"/\");\n  if (basename(normalized) === \"README\") {\n    const dir = normalized.replace(/\\/README$/, \"\");\n    return dir === \"\" ? \"/\" : `/${dir}`;\n  }\n  return `/${normalized}`;\n}\n\n/**\n * Validate every markdown file in a content directory.\n *\n * The returned summary aggregates schema + content validator issues and\n * includes totals so CLIs can trivially produce pass/fail exit codes.\n */\nexport async function validateContent(\n  options: ValidateContentOptions,\n): Promise<ValidateContentSummary> {\n  const {\n    contentDir,\n    include = [\"**/*.md\", \"**/*.mdx\"],\n    exclude = [\"**/node_modules/**\"],\n    frontmatterSchema,\n    resolveFrontmatterSchema,\n    linkValidator,\n    extraValidators = [],\n    includeStructuralValidators = true,\n    collectionName = \"content\",\n  } = options;\n\n  const files = await fg(include, {\n    cwd: contentDir,\n    absolute: true,\n    ignore: exclude,\n    dot: false,\n    onlyFiles: true,\n  });\n  files.sort();\n\n  const validators: ContentValidator[] = [];\n  validators.push(createLinkValidator(linkValidator));\n  if (includeStructuralValidators) {\n    validators.push(codeBlockValidator);\n    validators.push(headingValidator);\n    validators.push(imageStructureValidator);\n  }\n  validators.push(...extraValidators);\n\n  const results: ContentFileResult[] = [];\n  let errors = 0;\n  let warnings = 0;\n\n  for (const filePath of files) {\n    const rel = relative(contentDir, filePath);\n    const slug = toSlug(rel);\n    const issues: ValidationIssue[] = [];\n\n    let data: Record<string, unknown> = {};\n    let body = \"\";\n    try {\n      const raw = await readFile(filePath, \"utf-8\");\n      const parsed = matter(raw, { engines: { yaml: parseYaml } });\n      data = (parsed.data as Record<string, unknown>) ?? {};\n      body = parsed.content;\n    } catch (err) {\n      const message = err instanceof Error ? err.message : String(err);\n      issues.push({\n        message: `Failed to parse markdown: ${message}`,\n        severity: \"error\",\n        source: \"content\",\n      });\n      results.push({ filePath, relativePath: rel, slug, issues });\n      errors += 1;\n      continue;\n    }\n\n    const schema = resolveFrontmatterSchema?.(filePath) ?? frontmatterSchema;\n    if (schema) {\n      const { issues: schemaIssues, validatedData } = validateSchema(data, schema);\n      issues.push(...schemaIssues);\n      data = validatedData as Record<string, unknown>;\n    }\n\n    try {\n      const mdast = unified().use(remarkParse).parse(body);\n      const contentIssues = await runValidators(\n        {\n          filePath,\n          slug,\n          collection: collectionName,\n          rawContent: body,\n          data,\n          mdast,\n        },\n        validators,\n      );\n      issues.push(...contentIssues);\n    } catch (err) {\n      const message = err instanceof Error ? err.message : String(err);\n      issues.push({\n        message: `Validator pipeline threw: ${message}`,\n        severity: \"error\",\n        source: \"content\",\n      });\n    }\n\n    for (const issue of issues) {\n      if (issue.severity === \"error\") errors += 1;\n      else warnings += 1;\n    }\n    results.push({ filePath, relativePath: rel, slug, issues });\n  }\n\n  return {\n    collection: collectionName,\n    contentDir,\n    fileCount: files.length,\n    errors,\n    warnings,\n    results,\n  };\n}\n\n/**\n * Render a human-readable report from a {@link ValidateContentSummary}.\n *\n * Designed for CLIs: the returned string is suitable for `console.log`\n * directly. Empty when there are no issues.\n */\nexport function formatContentValidationReport(\n  summary: ValidateContentSummary,\n  options?: { showClean?: boolean },\n): string {\n  const showClean = options?.showClean ?? false;\n  const lines: string[] = [];\n  for (const result of summary.results) {\n    if (result.issues.length === 0) {\n      if (showClean) lines.push(`OK ${result.relativePath}`);\n      continue;\n    }\n    lines.push(`${result.relativePath}`);\n    for (const issue of result.issues) {\n      const prefix = issue.severity === \"error\" ? \"  \\u2717\" : \"  \\u26A0\";\n      const field = issue.field ? ` [${issue.field}]` : \"\";\n      lines.push(`${prefix}${field} ${issue.message}`);\n    }\n  }\n  lines.push(\"\");\n  lines.push(\n    `${summary.fileCount} files validated — ${summary.errors} error(s), ${summary.warnings} warning(s)`,\n  );\n  return lines.join(\"\\n\");\n}\n"],"mappings":"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"}