@vibe-agent-toolkit/resources 0.1.38 → 0.1.39-rc.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (75) hide show
  1. package/README.md +33 -24
  2. package/dist/frontmatter-link-validator.d.ts +9 -9
  3. package/dist/frontmatter-link-validator.d.ts.map +1 -1
  4. package/dist/frontmatter-link-validator.js +28 -27
  5. package/dist/frontmatter-link-validator.js.map +1 -1
  6. package/dist/frontmatter-validator.d.ts +3 -2
  7. package/dist/frontmatter-validator.d.ts.map +1 -1
  8. package/dist/frontmatter-validator.js +8 -14
  9. package/dist/frontmatter-validator.js.map +1 -1
  10. package/dist/html-link-parser.d.ts +47 -0
  11. package/dist/html-link-parser.d.ts.map +1 -0
  12. package/dist/html-link-parser.js +111 -0
  13. package/dist/html-link-parser.js.map +1 -0
  14. package/dist/html-transform.d.ts +45 -0
  15. package/dist/html-transform.d.ts.map +1 -0
  16. package/dist/html-transform.js +116 -0
  17. package/dist/html-transform.js.map +1 -0
  18. package/dist/index.d.ts +5 -2
  19. package/dist/index.d.ts.map +1 -1
  20. package/dist/index.js +4 -2
  21. package/dist/index.js.map +1 -1
  22. package/dist/link-parser.d.ts +26 -2
  23. package/dist/link-parser.d.ts.map +1 -1
  24. package/dist/link-parser.js +40 -6
  25. package/dist/link-parser.js.map +1 -1
  26. package/dist/link-validator.d.ts +53 -6
  27. package/dist/link-validator.d.ts.map +1 -1
  28. package/dist/link-validator.js +100 -131
  29. package/dist/link-validator.js.map +1 -1
  30. package/dist/multi-schema-validator.d.ts.map +1 -1
  31. package/dist/multi-schema-validator.js +6 -8
  32. package/dist/multi-schema-validator.js.map +1 -1
  33. package/dist/resource-registry.d.ts +56 -13
  34. package/dist/resource-registry.d.ts.map +1 -1
  35. package/dist/resource-registry.js +165 -66
  36. package/dist/resource-registry.js.map +1 -1
  37. package/dist/schemas/link-auth.d.ts +382 -0
  38. package/dist/schemas/link-auth.d.ts.map +1 -0
  39. package/dist/schemas/link-auth.js +124 -0
  40. package/dist/schemas/link-auth.js.map +1 -0
  41. package/dist/schemas/project-config.d.ts +3277 -171
  42. package/dist/schemas/project-config.d.ts.map +1 -1
  43. package/dist/schemas/project-config.js +68 -0
  44. package/dist/schemas/project-config.js.map +1 -1
  45. package/dist/schemas/resource-metadata.d.ts +46 -10
  46. package/dist/schemas/resource-metadata.d.ts.map +1 -1
  47. package/dist/schemas/resource-metadata.js +14 -1
  48. package/dist/schemas/resource-metadata.js.map +1 -1
  49. package/dist/schemas/validation-result.d.ts +36 -57
  50. package/dist/schemas/validation-result.d.ts.map +1 -1
  51. package/dist/schemas/validation-result.js +5 -27
  52. package/dist/schemas/validation-result.js.map +1 -1
  53. package/dist/types/resources.d.ts +1 -1
  54. package/dist/types/resources.d.ts.map +1 -1
  55. package/dist/types/resources.js.map +1 -1
  56. package/dist/utils.d.ts +10 -0
  57. package/dist/utils.d.ts.map +1 -1
  58. package/dist/utils.js +14 -0
  59. package/dist/utils.js.map +1 -1
  60. package/package.json +4 -3
  61. package/src/frontmatter-link-validator.ts +28 -30
  62. package/src/frontmatter-validator.ts +19 -16
  63. package/src/html-link-parser.ts +140 -0
  64. package/src/html-transform.ts +157 -0
  65. package/src/index.ts +10 -1
  66. package/src/link-parser.ts +60 -11
  67. package/src/link-validator.ts +175 -145
  68. package/src/multi-schema-validator.ts +10 -8
  69. package/src/resource-registry.ts +205 -72
  70. package/src/schemas/link-auth.ts +146 -0
  71. package/src/schemas/project-config.ts +76 -0
  72. package/src/schemas/resource-metadata.ts +17 -1
  73. package/src/schemas/validation-result.ts +5 -29
  74. package/src/types/resources.ts +2 -1
  75. package/src/utils.ts +15 -0
package/dist/utils.d.ts CHANGED
@@ -2,6 +2,16 @@
2
2
  * Internal utility functions for the resources package.
3
3
  * These are not exported from the public API.
4
4
  */
5
+ /**
6
+ * Compute a `ValidationIssue.location`: the (absolute) source file path made
7
+ * relative to the project root. When no project root is known, fall back to the
8
+ * source path forward-slashed so the location is still useful and stable.
9
+ *
10
+ * @param sourceFilePath - Absolute path to the file the issue was found in.
11
+ * @param projectRoot - Project root, or undefined when none could be determined.
12
+ * @returns Forward-slashed relative location (or the forward-slashed absolute path).
13
+ */
14
+ export declare function issueLocation(sourceFilePath: string, projectRoot: string | undefined): string;
5
15
  /**
6
16
  * Check if a file path matches a glob pattern.
7
17
  *
@@ -1 +1 @@
1
- {"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../src/utils.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAQH;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,kBAAkB,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAoB7E;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,GAAG,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,CAS1E;AAED;;;;;;;;;;GAUG;AACH,MAAM,MAAM,sBAAsB,GAC9B;IAAE,IAAI,EAAE,aAAa,CAAA;CAAE,GACvB;IAAE,IAAI,EAAE,UAAU,CAAC;IAAC,YAAY,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAA;CAAE,GACtE;IAAE,IAAI,EAAE,kBAAkB,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAA;CAAE,GACtE;IAAE,IAAI,EAAE,uBAAuB,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAA;CAAE,CAAC;AAEhF;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AACH,wBAAgB,gBAAgB,CAC9B,IAAI,EAAE,MAAM,EACZ,cAAc,EAAE,MAAM,EACtB,WAAW,CAAC,EAAE,MAAM,GACnB,sBAAsB,CA6BxB;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,eAAe,CAAC,QAAQ,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,GAAG,OAAO,CA6B9E;AAED;;;GAGG;AACH,wBAAgB,wBAAwB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAE7D;AAED;;;GAGG;AACH,wBAAgB,wBAAwB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAEhE;AAED;;;;;;;;;GASG;AACH,wBAAgB,yBAAyB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAcjE;AAED,wBAAgB,qBAAqB,CAAC,CAAC,EAAE,MAAM,GAAG,OAAO,CAUxD"}
1
+ {"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../src/utils.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAQH;;;;;;;;GAQG;AACH,wBAAgB,aAAa,CAAC,cAAc,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,CAI7F;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,kBAAkB,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAoB7E;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,GAAG,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,CAS1E;AAED;;;;;;;;;;GAUG;AACH,MAAM,MAAM,sBAAsB,GAC9B;IAAE,IAAI,EAAE,aAAa,CAAA;CAAE,GACvB;IAAE,IAAI,EAAE,UAAU,CAAC;IAAC,YAAY,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAA;CAAE,GACtE;IAAE,IAAI,EAAE,kBAAkB,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAA;CAAE,GACtE;IAAE,IAAI,EAAE,uBAAuB,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAA;CAAE,CAAC;AAEhF;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AACH,wBAAgB,gBAAgB,CAC9B,IAAI,EAAE,MAAM,EACZ,cAAc,EAAE,MAAM,EACtB,WAAW,CAAC,EAAE,MAAM,GACnB,sBAAsB,CA6BxB;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,eAAe,CAAC,QAAQ,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,GAAG,OAAO,CA6B9E;AAED;;;GAGG;AACH,wBAAgB,wBAAwB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAE7D;AAED;;;GAGG;AACH,wBAAgB,wBAAwB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAEhE;AAED;;;;;;;;;GASG;AACH,wBAAgB,yBAAyB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAcjE;AAED,wBAAgB,qBAAqB,CAAC,CAAC,EAAE,MAAM,GAAG,OAAO,CAUxD"}
package/dist/utils.js CHANGED
@@ -6,6 +6,20 @@ import fs from 'node:fs';
6
6
  import path from 'node:path';
7
7
  import { toForwardSlash, safePath } from '@vibe-agent-toolkit/utils';
8
8
  import picomatch from 'picomatch';
9
+ /**
10
+ * Compute a `ValidationIssue.location`: the (absolute) source file path made
11
+ * relative to the project root. When no project root is known, fall back to the
12
+ * source path forward-slashed so the location is still useful and stable.
13
+ *
14
+ * @param sourceFilePath - Absolute path to the file the issue was found in.
15
+ * @param projectRoot - Project root, or undefined when none could be determined.
16
+ * @returns Forward-slashed relative location (or the forward-slashed absolute path).
17
+ */
18
+ export function issueLocation(sourceFilePath, projectRoot) {
19
+ return projectRoot === undefined
20
+ ? toForwardSlash(sourceFilePath)
21
+ : safePath.relative(projectRoot, sourceFilePath);
22
+ }
9
23
  /**
10
24
  * Check if a file path matches a glob pattern.
11
25
  *
package/dist/utils.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"utils.js","sourceRoot":"","sources":["../src/utils.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"utils.js","sourceRoot":"","sources":["../src/utils.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vibe-agent-toolkit/resources",
3
- "version": "0.1.38",
3
+ "version": "0.1.39-rc.10",
4
4
  "description": "Markdown resource parsing, validation, and link integrity checking",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -33,12 +33,13 @@
33
33
  "author": "Jeff Dutton",
34
34
  "license": "MIT",
35
35
  "dependencies": {
36
- "@vibe-agent-toolkit/agent-schema": "0.1.38",
37
- "@vibe-agent-toolkit/utils": "0.1.38",
36
+ "@vibe-agent-toolkit/agent-schema": "0.1.39-rc.10",
37
+ "@vibe-agent-toolkit/utils": "0.1.39-rc.10",
38
38
  "ajv": "^8.17.1",
39
39
  "ajv-formats": "^3.0.1",
40
40
  "github-slugger": "^2.0.0",
41
41
  "markdown-link-check": "^3.14.2",
42
+ "parse5": "^7.2.1",
42
43
  "picomatch": "^4.0.3",
43
44
  "remark-frontmatter": "^5.0.0",
44
45
  "remark-gfm": "^4.0.0",
@@ -7,21 +7,31 @@
7
7
  * frontmatter-specific type codes plus a list of external URLs the registry
8
8
  * can fold into its existing external URL collection.
9
9
  *
10
- * Type mapping:
11
- * broken_file -> frontmatter_link_broken
12
- * broken_anchor -> frontmatter_anchor_missing
13
- * link_to_gitignored -> frontmatter_link_to_gitignored
14
- * unknown_link -> frontmatter_unknown_link
10
+ * Code mapping:
11
+ * LINK_BROKEN_FILE -> FRONTMATTER_LINK_BROKEN
12
+ * LINK_BROKEN_ANCHOR -> FRONTMATTER_ANCHOR_MISSING
13
+ * LINK_TO_GITIGNORED -> FRONTMATTER_LINK_TO_GITIGNORED
14
+ * LINK_UNKNOWN -> FRONTMATTER_UNKNOWN_LINK
15
15
  *
16
16
  * Skipped (no issue, no external):
17
17
  * email (mailto:)
18
18
  * anchor-only (validated as anchor in current file via validateLink)
19
19
  */
20
20
 
21
+ import { createRegistryIssue, type IssueCode } from '@vibe-agent-toolkit/agent-schema';
22
+
21
23
  import { classifyLink } from './link-parser.js';
22
- import { validateLink, type ValidateLinkOptions } from './link-validator.js';
24
+ import { validateLink, type FragmentIndex, type ValidateLinkOptions } from './link-validator.js';
23
25
  import { walkFrontmatterUriReferences } from './schema-uri-walker.js';
24
- import type { HeadingNode, ResourceLink, ValidationIssue } from './types.js';
26
+ import type { ResourceLink, ValidationIssue } from './types.js';
27
+
28
+ /** Map the link-level code emitted by validateLink to its frontmatter-scoped code. */
29
+ const LINK_CODE_TO_FRONTMATTER_CODE: Partial<Record<IssueCode, IssueCode>> = {
30
+ LINK_BROKEN_FILE: 'FRONTMATTER_LINK_BROKEN',
31
+ LINK_BROKEN_ANCHOR: 'FRONTMATTER_ANCHOR_MISSING',
32
+ LINK_TO_GITIGNORED: 'FRONTMATTER_LINK_TO_GITIGNORED',
33
+ LINK_UNKNOWN: 'FRONTMATTER_UNKNOWN_LINK',
34
+ };
25
35
 
26
36
  /** A frontmatter-sourced external URL captured for downstream health checking. */
27
37
  export interface FrontmatterExternalUrl {
@@ -41,14 +51,14 @@ export interface FrontmatterLinkValidationResult {
41
51
  * @param frontmatter - Parsed frontmatter (or undefined)
42
52
  * @param schema - JSON Schema for the collection
43
53
  * @param sourceFilePath - Absolute path to the source file
44
- * @param headingsByFile - Heading trees (for anchor validation)
54
+ * @param fragmentsByFile - Fragment index (file path → set of valid fragments) for anchor validation
45
55
  * @param options - Same shape as validateLink (projectRoot, gitTracker, ...)
46
56
  */
47
57
  export async function validateFrontmatterLinks(
48
58
  frontmatter: Record<string, unknown> | undefined,
49
59
  schema: object,
50
60
  sourceFilePath: string,
51
- headingsByFile: Map<string, HeadingNode[]>,
61
+ fragmentsByFile: FragmentIndex,
52
62
  options?: ValidateLinkOptions,
53
63
  ): Promise<FrontmatterLinkValidationResult> {
54
64
  if (!frontmatter) return { issues: [], externalUrls: [] };
@@ -79,7 +89,7 @@ export async function validateFrontmatterLinks(
79
89
  line: 1, // Frontmatter per-field line numbers are post-v1.
80
90
  };
81
91
 
82
- const issue = await validateLink(syntheticLink, sourceFilePath, headingsByFile, options);
92
+ const issue = await validateLink(syntheticLink, sourceFilePath, fragmentsByFile, options);
83
93
  if (!issue) continue;
84
94
 
85
95
  issues.push(rewriteIssue(issue, capture.dottedPath));
@@ -89,24 +99,12 @@ export async function validateFrontmatterLinks(
89
99
  }
90
100
 
91
101
  function rewriteIssue(issue: ValidationIssue, dottedPath: string): ValidationIssue {
92
- return {
93
- ...issue,
94
- type: mapType(issue.type),
95
- message: `field \`${dottedPath}\`: ${issue.message}`,
96
- };
97
- }
98
-
99
- function mapType(originalType: string): string {
100
- switch (originalType) {
101
- case 'broken_file':
102
- return 'frontmatter_link_broken';
103
- case 'broken_anchor':
104
- return 'frontmatter_anchor_missing';
105
- case 'link_to_gitignored':
106
- return 'frontmatter_link_to_gitignored';
107
- case 'unknown_link':
108
- return 'frontmatter_unknown_link';
109
- default:
110
- return originalType;
111
- }
102
+ const mappedCode = LINK_CODE_TO_FRONTMATTER_CODE[issue.code as IssueCode] ?? (issue.code as IssueCode);
103
+ const message = `field \`${dottedPath}\`: ${issue.message}`;
104
+ const extras: Partial<Pick<ValidationIssue, 'location' | 'line' | 'link' | 'suggestion'>> = {};
105
+ if (issue.location !== undefined) extras.location = issue.location;
106
+ if (issue.line !== undefined) extras.line = issue.line;
107
+ if (issue.link !== undefined) extras.link = issue.link;
108
+ if (issue.suggestion !== undefined) extras.suggestion = issue.suggestion;
109
+ return createRegistryIssue(mappedCode, message, extras);
112
110
  }
@@ -13,9 +13,11 @@
13
13
  * This is the ONLY place in the codebase that should use AJV.
14
14
  */
15
15
 
16
+ import { createRegistryIssue, type ValidationIssue } from '@vibe-agent-toolkit/agent-schema';
17
+
16
18
  import { createAjvWithUriFormats } from './ajv-factory.js';
17
19
  import type { ValidationMode } from './schemas/project-config.js';
18
- import type { ValidationIssue } from './schemas/validation-result.js';
20
+ import { issueLocation } from './utils.js';
19
21
 
20
22
  /**
21
23
  * Validate frontmatter against a JSON Schema.
@@ -31,6 +33,7 @@ import type { ValidationIssue } from './schemas/validation-result.js';
31
33
  * @param resourcePath - File path for error reporting
32
34
  * @param mode - Validation mode: 'strict' (default) or 'permissive'
33
35
  * @param schemaPath - Path to schema file (for error context)
36
+ * @param projectRoot - Project root for computing relative issue locations
34
37
  * @returns Array of validation issues (empty if valid)
35
38
  *
36
39
  * @example
@@ -54,7 +57,8 @@ export function validateFrontmatter(
54
57
  schema: object,
55
58
  resourcePath: string,
56
59
  mode: ValidationMode = 'strict',
57
- schemaPath?: string
60
+ schemaPath?: string,
61
+ projectRoot?: string,
58
62
  ): ValidationIssue[] {
59
63
  const issues: ValidationIssue[] = [];
60
64
 
@@ -87,13 +91,13 @@ export function validateFrontmatter(
87
91
  const schemaContext = schemaPath ? ` (schema: ${schemaPath}, mode: ${mode})` : '';
88
92
  const requiredFields = schemaRequires.join(', ');
89
93
 
90
- issues.push({
91
- resourcePath,
92
- line: 1,
93
- type: 'frontmatter_missing',
94
- link: '',
95
- message: `No frontmatter found in file. Schema requires: ${requiredFields}${schemaContext}`,
96
- });
94
+ issues.push(
95
+ createRegistryIssue(
96
+ 'FRONTMATTER_MISSING',
97
+ `No frontmatter found in file. Schema requires: ${requiredFields}${schemaContext}`,
98
+ { location: issueLocation(resourcePath, projectRoot), line: 1 },
99
+ ),
100
+ );
97
101
  }
98
102
  return issues;
99
103
  }
@@ -108,13 +112,12 @@ export function validateFrontmatter(
108
112
  // Format validation errors with helpful messages
109
113
  for (const error of validate.errors) {
110
114
  const message = formatValidationError(error, frontmatter, mode, schemaPath);
111
- issues.push({
112
- resourcePath,
113
- line: 1,
114
- type: 'frontmatter_schema_error',
115
- link: '',
116
- message,
117
- });
115
+ issues.push(
116
+ createRegistryIssue('FRONTMATTER_SCHEMA_ERROR', message, {
117
+ location: issueLocation(resourcePath, projectRoot),
118
+ line: 1,
119
+ }),
120
+ );
118
121
  }
119
122
 
120
123
  return issues;
@@ -0,0 +1,140 @@
1
+ /**
2
+ * HTML resource parser.
3
+ *
4
+ * Parses local HTML files into the shared `ParseResult` shape:
5
+ * - `<a href>` and `<img src>` links (classified by `classifyLink`)
6
+ * - `id` / `name` attributes as fragment anchors
7
+ * - well-formedness diagnostics from parse5's `onParseError`
8
+ *
9
+ * Uses parse5 (WHATWG-conformant). The parse5 document + element walker are
10
+ * exported so the link rewriter (html-transform.ts) shares one parser path.
11
+ *
12
+ * Non-goal: `<base href>` is not honored — relative links are resolved against
13
+ * the file's own directory, not a document base.
14
+ */
15
+
16
+ import { readFile, stat } from 'node:fs/promises';
17
+
18
+ import { parse, type DefaultTreeAdapterMap } from 'parse5';
19
+
20
+ import { classifyLink, type ParseResult } from './link-parser.js';
21
+ import type { HtmlParseError } from './schemas/resource-metadata.js';
22
+ import type { ResourceLink } from './types.js';
23
+
24
+ type P5Node = DefaultTreeAdapterMap['node'];
25
+ type P5Element = DefaultTreeAdapterMap['element'];
26
+ type P5ChildNode = DefaultTreeAdapterMap['childNode'];
27
+
28
+ /** A parsed parse5 document together with any well-formedness diagnostics. */
29
+ export interface HtmlDocument {
30
+ document: DefaultTreeAdapterMap['document'];
31
+ parseErrors: HtmlParseError[];
32
+ }
33
+
34
+ /**
35
+ * Parse an HTML source string with source-location info and collect parser
36
+ * errors. Shared by `parseHtml` (link/anchor extraction) and `rewriteHtmlLinks`
37
+ * (attribute offset splicing).
38
+ */
39
+ export function parseHtmlDocument(source: string): HtmlDocument {
40
+ const parseErrors: HtmlParseError[] = [];
41
+ const document = parse(source, {
42
+ sourceCodeLocationInfo: true,
43
+ onParseError: (err) => {
44
+ // parse5's ParserError extends Location which always has startLine: number.
45
+ // We always include line since it is always present.
46
+ parseErrors.push({ message: err.code, line: err.startLine });
47
+ },
48
+ });
49
+ return { document, parseErrors };
50
+ }
51
+
52
+ /**
53
+ * Depth-first walk yielding every element node in the tree.
54
+ *
55
+ * Deliberate gap: a `<template>` element's content lives in its separate
56
+ * `content` fragment (not `childNodes`), so links inside `<template>` are not
57
+ * walked, and foreign-content (SVG/MathML) subtrees are not special-cased.
58
+ * Both are rare in the content link graph we rewrite.
59
+ */
60
+ export function* walkElements(node: P5Node): Generator<P5Element> {
61
+ if ('tagName' in node) {
62
+ yield node;
63
+ }
64
+ if ('childNodes' in node) {
65
+ for (const child of node.childNodes as P5ChildNode[]) {
66
+ yield* walkElements(child);
67
+ }
68
+ }
69
+ }
70
+
71
+ function getAttr(element: P5Element, name: string): string | undefined {
72
+ return element.attrs.find((a) => a.name === name)?.value;
73
+ }
74
+
75
+ function makeLink(href: string, line: number | undefined): ResourceLink {
76
+ return { text: '', href, type: classifyLink(href), ...(line !== undefined && { line }) };
77
+ }
78
+
79
+ function visitElement(
80
+ element: P5Element,
81
+ links: ResourceLink[],
82
+ anchors: Set<string>,
83
+ ): void {
84
+ const line = element.sourceCodeLocation?.startLine;
85
+
86
+ if (element.tagName === 'a') {
87
+ const href = getAttr(element, 'href');
88
+ if (href !== undefined) {
89
+ links.push(makeLink(href, line));
90
+ }
91
+ const name = getAttr(element, 'name');
92
+ if (name !== undefined && name !== '') {
93
+ anchors.add(name);
94
+ }
95
+ } else if (element.tagName === 'img') {
96
+ const src = getAttr(element, 'src');
97
+ if (src !== undefined) {
98
+ links.push(makeLink(src, line));
99
+ }
100
+ }
101
+
102
+ const id = getAttr(element, 'id');
103
+ if (id !== undefined && id !== '') {
104
+ anchors.add(id);
105
+ }
106
+ }
107
+
108
+ /**
109
+ * Parse an HTML file into a `ParseResult`.
110
+ *
111
+ * @param filePath - Absolute path to the HTML file.
112
+ */
113
+ export async function parseHtml(filePath: string): Promise<ParseResult> {
114
+ const [content, stats] = await Promise.all([
115
+ // eslint-disable-next-line security/detect-non-literal-fs-filename -- filePath is a user-provided path parameter
116
+ readFile(filePath, 'utf-8'),
117
+ // eslint-disable-next-line security/detect-non-literal-fs-filename -- filePath is a user-provided path parameter
118
+ stat(filePath),
119
+ ]);
120
+
121
+ const { document, parseErrors } = parseHtmlDocument(content);
122
+
123
+ const links: ResourceLink[] = [];
124
+ const anchors = new Set<string>();
125
+
126
+ for (const element of walkElements(document)) {
127
+ visitElement(element, links, anchors);
128
+ }
129
+
130
+ const anchorList = [...anchors];
131
+ return {
132
+ links,
133
+ headings: [],
134
+ content,
135
+ sizeBytes: stats.size,
136
+ estimatedTokenCount: Math.ceil(content.length / 4),
137
+ ...(anchorList.length > 0 && { anchors: anchorList }),
138
+ ...(parseErrors.length > 0 && { parseErrors }),
139
+ };
140
+ }
@@ -0,0 +1,157 @@
1
+ /**
2
+ * Structure-preserving HTML link rewriter.
3
+ *
4
+ * Rewrites `<a href>` and `<img src>` attribute VALUES in place by splicing the
5
+ * original source string at parse5-reported offsets. The document is never
6
+ * re-serialized (parse5's serializer normalizes whitespace, quotes, void
7
+ * elements, and the doctype), so unchanged input round-trips byte-for-byte.
8
+ *
9
+ * Uses the same `RewriteHref` callback model as `rewriteBodyLinks` — callers
10
+ * supply per-href target resolution; this module owns only the splice mechanics.
11
+ *
12
+ * Non-goal: `<base href>` is not honored. Relative hrefs are resolved by the
13
+ * caller against the file's own directory; a `<base>` element that would
14
+ * override that in a browser is ignored.
15
+ */
16
+
17
+ import { parseHtmlDocument, walkElements } from './html-link-parser.js';
18
+ import type { RewriteHref } from './rewriter-helpers.js';
19
+
20
+ /** Tag name → the single link-bearing attribute we rewrite. */
21
+ const LINK_ATTR_BY_TAG: Record<string, string | undefined> = { a: 'href', img: 'src' };
22
+
23
+ interface ValueSpan {
24
+ valueStart: number;
25
+ valueEnd: number;
26
+ /** `"` or `'` for quoted attributes; `''` for unquoted. */
27
+ quote: string;
28
+ }
29
+
30
+ interface Edit extends ValueSpan {
31
+ newValue: string;
32
+ }
33
+
34
+ /**
35
+ * Chars that force an unquoted HTML attribute value to be quoted. Follows the
36
+ * WHATWG unquoted-attribute-value rules: whitespace, quotes, `<`, `>`, plus
37
+ * backtick and `=` (a superset of paths we expect, kept strict for safety).
38
+ */
39
+ const UNQUOTED_UNSAFE = /[\s"'`<>=]/;
40
+
41
+ /**
42
+ * Locate the value sub-range within an attribute's full source span.
43
+ *
44
+ * parse5 reports the whole-attribute span (`href="value"`); this finds the
45
+ * value's absolute offsets and the quote char. Returns undefined for boolean
46
+ * attributes (no `=`).
47
+ */
48
+ function valueSpan(attrSource: string, base: number): ValueSpan | undefined {
49
+ const eq = attrSource.indexOf('=');
50
+ if (eq === -1) {
51
+ return undefined;
52
+ }
53
+ let i = eq + 1;
54
+ while (i < attrSource.length && /\s/.test(attrSource.charAt(i))) {
55
+ i += 1;
56
+ }
57
+ if (i >= attrSource.length) {
58
+ return undefined;
59
+ }
60
+ const ch = attrSource.charAt(i);
61
+ if (ch === '"' || ch === "'") {
62
+ const close = attrSource.indexOf(ch, i + 1);
63
+ const end = close === -1 ? attrSource.length : close;
64
+ return { valueStart: base + i + 1, valueEnd: base + end, quote: ch };
65
+ }
66
+ return { valueStart: base + i, valueEnd: base + attrSource.length, quote: '' };
67
+ }
68
+
69
+ /**
70
+ * Encode a new value for writing. For quoted attributes the surrounding quotes
71
+ * stay in the source (we only replace the inner value), so we escape `&` and the
72
+ * active quote. For originally-unquoted values we keep them bare when safe, else
73
+ * wrap in double quotes.
74
+ */
75
+ function encodeValue(newValue: string, quote: string): string {
76
+ if (quote === '"' || quote === "'") {
77
+ const amp = newValue.replaceAll('&', '&amp;');
78
+ return quote === '"' ? amp.replaceAll('"', '&quot;') : amp.replaceAll("'", '&#39;');
79
+ }
80
+ if (newValue.length > 0 && !UNQUOTED_UNSAFE.test(newValue)) {
81
+ return newValue.replaceAll('&', '&amp;');
82
+ }
83
+ const escaped = newValue.replaceAll('&', '&amp;').replaceAll('"', '&quot;');
84
+ return `"${escaped}"`;
85
+ }
86
+
87
+ /** A wanted link rewrite that could not be spliced back into the source. */
88
+ export interface UnappliedRewrite {
89
+ tagName: string;
90
+ attr: string;
91
+ /** The original attribute value the rewrite targeted. */
92
+ from: string;
93
+ /** The value the rewrite wanted to write. */
94
+ to: string;
95
+ /**
96
+ * Why the splice could not be located:
97
+ * - `no-source-location` — parse5 omitted the attribute's offsets (synthesized
98
+ * during error recovery on malformed input).
99
+ * - `unparseable-attribute` — the attribute's source span had no locatable
100
+ * value (e.g. an unterminated quote).
101
+ */
102
+ reason: 'no-source-location' | 'unparseable-attribute';
103
+ }
104
+
105
+ /**
106
+ * Rewrite `<a href>` / `<img src>` values in `source` using `rewriteHref`.
107
+ * Returns `source` unchanged (byte-for-byte) when no value changes.
108
+ *
109
+ * A rewrite that is wanted (the href resolves to a new value) but cannot be
110
+ * spliced back — because parse5 omitted the source location or the attribute
111
+ * span is unparseable — is reported via `onUnapplied` rather than dropped
112
+ * silently. Malformed pages are exactly the ones that hit this path, so the
113
+ * caller can surface it instead of shipping a stale link.
114
+ */
115
+ export function rewriteHtmlLinks(
116
+ source: string,
117
+ rewriteHref: RewriteHref,
118
+ onUnapplied?: (info: UnappliedRewrite) => void,
119
+ ): string {
120
+ const { document } = parseHtmlDocument(source);
121
+ const edits: Edit[] = [];
122
+
123
+ for (const element of walkElements(document)) {
124
+ const attrName = LINK_ATTR_BY_TAG[element.tagName];
125
+ if (attrName === undefined) {
126
+ continue;
127
+ }
128
+ const attr = element.attrs.find((a) => a.name === attrName);
129
+ if (attr === undefined) {
130
+ continue;
131
+ }
132
+ // Resolve first so we only record drops that actually lose a wanted edit.
133
+ const newValue = rewriteHref(attr.value);
134
+ if (newValue === attr.value) {
135
+ continue;
136
+ }
137
+ const location = element.sourceCodeLocation?.attrs?.[attrName];
138
+ if (location === undefined) {
139
+ onUnapplied?.({ tagName: element.tagName, attr: attrName, from: attr.value, to: newValue, reason: 'no-source-location' });
140
+ continue;
141
+ }
142
+ const span = valueSpan(source.slice(location.startOffset, location.endOffset), location.startOffset);
143
+ if (span === undefined) {
144
+ onUnapplied?.({ tagName: element.tagName, attr: attrName, from: attr.value, to: newValue, reason: 'unparseable-attribute' });
145
+ continue;
146
+ }
147
+ edits.push({ ...span, newValue });
148
+ }
149
+
150
+ // Apply descending by start offset so earlier edits don't shift later ones.
151
+ edits.sort((a, b) => b.valueStart - a.valueStart);
152
+ let result = source;
153
+ for (const edit of edits) {
154
+ result = result.slice(0, edit.valueStart) + encodeValue(edit.newValue, edit.quote) + result.slice(edit.valueEnd);
155
+ }
156
+ return result;
157
+ }
package/src/index.ts CHANGED
@@ -83,7 +83,12 @@ export {
83
83
  } from './schemas/validation-result.js';
84
84
 
85
85
  // Export parser interface for advanced use cases
86
- export { parseMarkdown, type ParseResult } from './link-parser.js';
86
+ export { parseMarkdown, classifyLink, isLocalFileLink, type ParseResult } from './link-parser.js';
87
+
88
+ export { parseHtml } from './html-link-parser.js';
89
+ // HtmlParseError is Zod-sourced (single source of truth) — see schemas/resource-metadata.ts.
90
+ export type { HtmlParseError } from './schemas/resource-metadata.js';
91
+ export { rewriteHtmlLinks, type UnappliedRewrite } from './html-transform.js';
87
92
 
88
93
  // Export frontmatter validation
89
94
  export { validateFrontmatter } from './frontmatter-validator.js';
@@ -134,4 +139,8 @@ export {
134
139
  SkillFileEntrySchema,
135
140
  SkillsConfigSchema,
136
141
  SkillPackagingConfigSchema,
142
+ SkillSourceDescriptorSchema,
143
+ TestConfigSchema,
144
+ type SkillSourceDescriptor,
145
+ type TestConfig,
137
146
  } from './schemas/project-config.js';