@vellora/lint 0.1.0-alpha.0
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.
- package/LICENSE +21 -0
- package/README.md +26 -0
- package/dist/compat.d.ts +8 -0
- package/dist/compat.js +13 -0
- package/dist/compat.js.map +1 -0
- package/dist/diagnose.d.ts +2 -0
- package/dist/diagnose.js +20 -0
- package/dist/diagnose.js.map +1 -0
- package/dist/dom.d.ts +61 -0
- package/dist/dom.js +130 -0
- package/dist/dom.js.map +1 -0
- package/dist/engine.d.ts +34 -0
- package/dist/engine.js +60 -0
- package/dist/engine.js.map +1 -0
- package/dist/fix.d.ts +5 -0
- package/dist/fix.js +49 -0
- package/dist/fix.js.map +1 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.js +16 -0
- package/dist/index.js.map +1 -0
- package/dist/rules/css-animation.d.ts +2 -0
- package/dist/rules/css-animation.js +44 -0
- package/dist/rules/css-animation.js.map +1 -0
- package/dist/rules/flex-grid-in-td.d.ts +2 -0
- package/dist/rules/flex-grid-in-td.js +92 -0
- package/dist/rules/flex-grid-in-td.js.map +1 -0
- package/dist/rules/img-dimension-attrs.d.ts +2 -0
- package/dist/rules/img-dimension-attrs.js +49 -0
- package/dist/rules/img-dimension-attrs.js.map +1 -0
- package/dist/rules/index.d.ts +7 -0
- package/dist/rules/index.js +15 -0
- package/dist/rules/index.js.map +1 -0
- package/dist/rules/inline-svg.d.ts +2 -0
- package/dist/rules/inline-svg.js +92 -0
- package/dist/rules/inline-svg.js.map +1 -0
- package/dist/rules/invalid-markup.d.ts +2 -0
- package/dist/rules/invalid-markup.js +89 -0
- package/dist/rules/invalid-markup.js.map +1 -0
- package/dist/rules/script-element.d.ts +2 -0
- package/dist/rules/script-element.js +22 -0
- package/dist/rules/script-element.js.map +1 -0
- package/dist/style.d.ts +22 -0
- package/dist/style.js +60 -0
- package/dist/style.js.map +1 -0
- package/dist/types.d.ts +41 -0
- package/dist/types.js +7 -0
- package/dist/types.js.map +1 -0
- package/package.json +59 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Diego Malta
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# @vellora/lint
|
|
2
|
+
|
|
3
|
+
Dev-time HTML diagnose + fix for the [vellora](https://github.com/diomalta/vellora)
|
|
4
|
+
HTML/CSS subset (parse5 + resvg). Keeps your templates inside the supported subset
|
|
5
|
+
at authoring/CI time — like a linter, never silently at render time.
|
|
6
|
+
|
|
7
|
+
> **Pre-release (alpha)** — the `diagnose` / `fix` API is under active
|
|
8
|
+
> development.
|
|
9
|
+
|
|
10
|
+
## Install
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
npm install @vellora/lint
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
## Usage
|
|
17
|
+
|
|
18
|
+
```ts
|
|
19
|
+
import { diagnose, fix } from "@vellora/lint";
|
|
20
|
+
|
|
21
|
+
const report = diagnose(html);
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## License
|
|
25
|
+
|
|
26
|
+
MIT — see [LICENSE](./LICENSE).
|
package/dist/compat.d.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Stable rule → compatibility-table link map. `@vellora/lint` does not own the compatibility table
|
|
3
|
+
* (that lives elsewhere, per the design's non-goals); it only links into it. Every rule has exactly
|
|
4
|
+
* one non-empty, stable anchor so two findings with the same rule always carry the same `compatLink`.
|
|
5
|
+
*/
|
|
6
|
+
import type { RuleId } from "./types.js";
|
|
7
|
+
export declare const COMPAT_LINKS: Record<RuleId, string>;
|
|
8
|
+
export declare function compatLink(rule: RuleId): string;
|
package/dist/compat.js
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
const COMPAT_BASE = "https://vellora.dev/compat";
|
|
2
|
+
export const COMPAT_LINKS = {
|
|
3
|
+
"inline-svg": `${COMPAT_BASE}#inline-svg`,
|
|
4
|
+
"flex-grid-in-td": `${COMPAT_BASE}#flex-grid-in-td`,
|
|
5
|
+
"img-dimension-attrs": `${COMPAT_BASE}#img-dimension-attrs`,
|
|
6
|
+
"invalid-markup": `${COMPAT_BASE}#invalid-markup`,
|
|
7
|
+
"script-element": `${COMPAT_BASE}#script-element`,
|
|
8
|
+
"css-animation": `${COMPAT_BASE}#css-animation`,
|
|
9
|
+
};
|
|
10
|
+
export function compatLink(rule) {
|
|
11
|
+
return COMPAT_LINKS[rule];
|
|
12
|
+
}
|
|
13
|
+
//# sourceMappingURL=compat.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"compat.js","sourceRoot":"","sources":["../src/compat.ts"],"names":[],"mappings":"AAOA,MAAM,WAAW,GAAG,4BAA4B,CAAC;AAEjD,MAAM,CAAC,MAAM,YAAY,GAA2B;IAClD,YAAY,EAAE,GAAG,WAAW,aAAa;IACzC,iBAAiB,EAAE,GAAG,WAAW,kBAAkB;IACnD,qBAAqB,EAAE,GAAG,WAAW,sBAAsB;IAC3D,gBAAgB,EAAE,GAAG,WAAW,iBAAiB;IACjD,gBAAgB,EAAE,GAAG,WAAW,iBAAiB;IACjD,eAAe,EAAE,GAAG,WAAW,gBAAgB;CAChD,CAAC;AAEF,MAAM,UAAU,UAAU,CAAC,IAAY;IACrC,OAAO,YAAY,CAAC,IAAI,CAAC,CAAC;AAC5B,CAAC"}
|
package/dist/diagnose.js
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `diagnose(html)` — parse with parse5, run every rule's read-only `detect`, and return a structured
|
|
3
|
+
* report ordered by `(line, col, rule)`. Read-only by contract: it never mutates or re-serializes the
|
|
4
|
+
* input, never touches the network or filesystem, and must not run on the render hot path.
|
|
5
|
+
*/
|
|
6
|
+
import { exceedsMaxDepth, parseHtml } from "./dom.js";
|
|
7
|
+
import { orderFindings, toFinding, tooDeeplyNestedFinding } from "./engine.js";
|
|
8
|
+
import { RULES } from "./rules/index.js";
|
|
9
|
+
export function diagnose(html) {
|
|
10
|
+
const doc = parseHtml(html);
|
|
11
|
+
// A pathologically deep document surfaces a structured finding rather than risking a stack
|
|
12
|
+
// overflow in any recursive downstream step.
|
|
13
|
+
if (exceedsMaxDepth(doc.document)) {
|
|
14
|
+
return { conformant: false, findings: [tooDeeplyNestedFinding()] };
|
|
15
|
+
}
|
|
16
|
+
const findings = RULES.flatMap((rule) => rule.detect(doc).map((detection) => toFinding(rule, detection, doc.source)));
|
|
17
|
+
const ordered = orderFindings(findings);
|
|
18
|
+
return { conformant: ordered.length === 0, findings: ordered };
|
|
19
|
+
}
|
|
20
|
+
//# sourceMappingURL=diagnose.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"diagnose.js","sourceRoot":"","sources":["../src/diagnose.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,OAAO,EAAE,eAAe,EAAE,SAAS,EAAE,MAAM,UAAU,CAAC;AACtD,OAAO,EAAE,aAAa,EAAE,SAAS,EAAE,sBAAsB,EAAE,MAAM,aAAa,CAAC;AAC/E,OAAO,EAAE,KAAK,EAAE,MAAM,kBAAkB,CAAC;AAGzC,MAAM,UAAU,QAAQ,CAAC,IAAY;IACnC,MAAM,GAAG,GAAG,SAAS,CAAC,IAAI,CAAC,CAAC;IAC5B,2FAA2F;IAC3F,6CAA6C;IAC7C,IAAI,eAAe,CAAC,GAAG,CAAC,QAAQ,CAAC,EAAE,CAAC;QAClC,OAAO,EAAE,UAAU,EAAE,KAAK,EAAE,QAAQ,EAAE,CAAC,sBAAsB,EAAE,CAAC,EAAE,CAAC;IACrE,CAAC;IACD,MAAM,QAAQ,GAAG,KAAK,CAAC,OAAO,CAAC,CAAC,IAAI,EAAE,EAAE,CACtC,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC,SAAS,EAAE,EAAE,CAAC,SAAS,CAAC,IAAI,EAAE,SAAS,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC,CAC5E,CAAC;IACF,MAAM,OAAO,GAAG,aAAa,CAAC,QAAQ,CAAC,CAAC;IACxC,OAAO,EAAE,UAAU,EAAE,OAAO,CAAC,MAAM,KAAK,CAAC,EAAE,QAAQ,EAAE,OAAO,EAAE,CAAC;AACjE,CAAC"}
|
package/dist/dom.d.ts
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* parse5 helpers shared by every rule: parsing with source-location info, a depth-first element
|
|
3
|
+
* walk, attribute and inline-`style` accessors, and source-snippet extraction. Kept dependency-light
|
|
4
|
+
* and side-effect-free so importing `@vellora/lint` never touches the network or filesystem.
|
|
5
|
+
*/
|
|
6
|
+
import { type DefaultTreeAdapterTypes, type ParserError } from "parse5";
|
|
7
|
+
export type Element = DefaultTreeAdapterTypes.Element;
|
|
8
|
+
export type Node = DefaultTreeAdapterTypes.Node;
|
|
9
|
+
export type ChildNode = DefaultTreeAdapterTypes.ChildNode;
|
|
10
|
+
export type Document = DefaultTreeAdapterTypes.Document;
|
|
11
|
+
export interface ParsedDocument {
|
|
12
|
+
document: Document;
|
|
13
|
+
/** Parse errors reported by parse5 (`onParseError`), in source order. */
|
|
14
|
+
parseErrors: ParserError[];
|
|
15
|
+
/** The original input, used for source-snippet extraction. */
|
|
16
|
+
source: string;
|
|
17
|
+
}
|
|
18
|
+
/** Parse HTML with source-location info and a collected parse-error list. Never mutates input. */
|
|
19
|
+
export declare function parseHtml(html: string): ParsedDocument;
|
|
20
|
+
/** Serialize a document back to HTML (includes the DOCTYPE). This is a parse→serialize fixed point. */
|
|
21
|
+
export declare function serializeDocument(document: Document): string;
|
|
22
|
+
/** Serialize a single element (including its own tag) — used to feed an `<svg>` subtree to resvg. */
|
|
23
|
+
export declare function serializeElement(element: Element): string;
|
|
24
|
+
/** Is this a parse5 text node (`#text`)? */
|
|
25
|
+
export declare function isTextNode(node: ChildNode): boolean;
|
|
26
|
+
/** Is this a parse5 comment node (`#comment`)? */
|
|
27
|
+
export declare function isCommentNode(node: ChildNode): boolean;
|
|
28
|
+
/** Raw character data of a text node (empty string for non-text nodes). */
|
|
29
|
+
export declare function textValue(node: ChildNode): string;
|
|
30
|
+
/**
|
|
31
|
+
* Depth-first walk over every element, calling `visit(element, parent)` in document order.
|
|
32
|
+
*
|
|
33
|
+
* Iterative (explicit worklist) rather than recursive so a pathologically deep document — parse5
|
|
34
|
+
* imposes no nesting cap — cannot overflow the JS call stack and surface an uncaught `RangeError`.
|
|
35
|
+
* Children are pushed in reverse so they pop in document order; each frame carries its effective
|
|
36
|
+
* parent (the nearest ancestor element).
|
|
37
|
+
*/
|
|
38
|
+
export declare function walkElements(document: Document, visit: (element: Element, parent: Element | null) => void): void;
|
|
39
|
+
/**
|
|
40
|
+
* Maximum element-nesting depth `diagnose`/`fix` will process. Beyond this, parse5's own recursive
|
|
41
|
+
* serializer (and any future deep recursion) would overflow the JS call stack; the entry points cap
|
|
42
|
+
* here and surface a structured finding instead of letting a raw `RangeError` escape. Untrusted HTML
|
|
43
|
+
* this deep is pathological — well past anything a real document needs.
|
|
44
|
+
*/
|
|
45
|
+
export declare const MAX_NESTING_DEPTH = 5000;
|
|
46
|
+
/**
|
|
47
|
+
* Does the document nest elements deeper than `MAX_NESTING_DEPTH`? Iterative (explicit worklist) so
|
|
48
|
+
* the depth check itself cannot overflow the stack on the very input it guards against.
|
|
49
|
+
*/
|
|
50
|
+
export declare function exceedsMaxDepth(document: Document, max?: number): boolean;
|
|
51
|
+
export declare function tagName(element: Element): string;
|
|
52
|
+
export declare function getAttr(element: Element, name: string): string | null;
|
|
53
|
+
export declare function setAttr(element: Element, name: string, value: string): void;
|
|
54
|
+
export declare function removeAttr(element: Element, name: string): void;
|
|
55
|
+
/** 1-based start `{ line, col }` of an element from its source-location info (falls back to 1:1). */
|
|
56
|
+
export declare function startLocation(element: Element): {
|
|
57
|
+
line: number;
|
|
58
|
+
col: number;
|
|
59
|
+
};
|
|
60
|
+
/** Extract the offending source fragment for an element from the original source. */
|
|
61
|
+
export declare function snippetFor(element: Element, source: string): string;
|
package/dist/dom.js
ADDED
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* parse5 helpers shared by every rule: parsing with source-location info, a depth-first element
|
|
3
|
+
* walk, attribute and inline-`style` accessors, and source-snippet extraction. Kept dependency-light
|
|
4
|
+
* and side-effect-free so importing `@vellora/lint` never touches the network or filesystem.
|
|
5
|
+
*/
|
|
6
|
+
import { parse, serialize, serializeOuter, } from "parse5";
|
|
7
|
+
/** Parse HTML with source-location info and a collected parse-error list. Never mutates input. */
|
|
8
|
+
export function parseHtml(html) {
|
|
9
|
+
const parseErrors = [];
|
|
10
|
+
const document = parse(html, {
|
|
11
|
+
sourceCodeLocationInfo: true,
|
|
12
|
+
onParseError: (error) => parseErrors.push(error),
|
|
13
|
+
});
|
|
14
|
+
return { document, parseErrors, source: html };
|
|
15
|
+
}
|
|
16
|
+
/** Serialize a document back to HTML (includes the DOCTYPE). This is a parse→serialize fixed point. */
|
|
17
|
+
export function serializeDocument(document) {
|
|
18
|
+
return serialize(document);
|
|
19
|
+
}
|
|
20
|
+
/** Serialize a single element (including its own tag) — used to feed an `<svg>` subtree to resvg. */
|
|
21
|
+
export function serializeElement(element) {
|
|
22
|
+
return serializeOuter(element);
|
|
23
|
+
}
|
|
24
|
+
function isElement(node) {
|
|
25
|
+
return typeof node.tagName === "string";
|
|
26
|
+
}
|
|
27
|
+
/** Is this a parse5 text node (`#text`)? */
|
|
28
|
+
export function isTextNode(node) {
|
|
29
|
+
return node.nodeName === "#text";
|
|
30
|
+
}
|
|
31
|
+
/** Is this a parse5 comment node (`#comment`)? */
|
|
32
|
+
export function isCommentNode(node) {
|
|
33
|
+
return node.nodeName === "#comment";
|
|
34
|
+
}
|
|
35
|
+
/** Raw character data of a text node (empty string for non-text nodes). */
|
|
36
|
+
export function textValue(node) {
|
|
37
|
+
return isTextNode(node) ? node.value : "";
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Depth-first walk over every element, calling `visit(element, parent)` in document order.
|
|
41
|
+
*
|
|
42
|
+
* Iterative (explicit worklist) rather than recursive so a pathologically deep document — parse5
|
|
43
|
+
* imposes no nesting cap — cannot overflow the JS call stack and surface an uncaught `RangeError`.
|
|
44
|
+
* Children are pushed in reverse so they pop in document order; each frame carries its effective
|
|
45
|
+
* parent (the nearest ancestor element).
|
|
46
|
+
*/
|
|
47
|
+
export function walkElements(document, visit) {
|
|
48
|
+
const stack = [{ node: document, parent: null }];
|
|
49
|
+
for (let frame = stack.pop(); frame !== undefined; frame = stack.pop()) {
|
|
50
|
+
const { node, parent } = frame;
|
|
51
|
+
const element = isElement(node) ? node : null;
|
|
52
|
+
if (element) {
|
|
53
|
+
visit(element, parent);
|
|
54
|
+
}
|
|
55
|
+
const children = node.childNodes ?? [];
|
|
56
|
+
const effectiveParent = element ?? parent;
|
|
57
|
+
for (let i = children.length - 1; i >= 0; i--) {
|
|
58
|
+
const child = children[i];
|
|
59
|
+
if (child !== undefined) {
|
|
60
|
+
stack.push({ node: child, parent: effectiveParent });
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Maximum element-nesting depth `diagnose`/`fix` will process. Beyond this, parse5's own recursive
|
|
67
|
+
* serializer (and any future deep recursion) would overflow the JS call stack; the entry points cap
|
|
68
|
+
* here and surface a structured finding instead of letting a raw `RangeError` escape. Untrusted HTML
|
|
69
|
+
* this deep is pathological — well past anything a real document needs.
|
|
70
|
+
*/
|
|
71
|
+
export const MAX_NESTING_DEPTH = 5000;
|
|
72
|
+
/**
|
|
73
|
+
* Does the document nest elements deeper than `MAX_NESTING_DEPTH`? Iterative (explicit worklist) so
|
|
74
|
+
* the depth check itself cannot overflow the stack on the very input it guards against.
|
|
75
|
+
*/
|
|
76
|
+
export function exceedsMaxDepth(document, max = MAX_NESTING_DEPTH) {
|
|
77
|
+
const stack = [{ node: document, depth: 0 }];
|
|
78
|
+
for (let frame = stack.pop(); frame !== undefined; frame = stack.pop()) {
|
|
79
|
+
const { node, depth } = frame;
|
|
80
|
+
if (depth > max) {
|
|
81
|
+
return true;
|
|
82
|
+
}
|
|
83
|
+
const children = node.childNodes ?? [];
|
|
84
|
+
const childDepth = isElement(node) ? depth + 1 : depth;
|
|
85
|
+
for (const child of children) {
|
|
86
|
+
stack.push({ node: child, depth: childDepth });
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
return false;
|
|
90
|
+
}
|
|
91
|
+
export function tagName(element) {
|
|
92
|
+
return element.tagName.toLowerCase();
|
|
93
|
+
}
|
|
94
|
+
export function getAttr(element, name) {
|
|
95
|
+
const attr = element.attrs.find((a) => a.name === name);
|
|
96
|
+
return attr ? attr.value : null;
|
|
97
|
+
}
|
|
98
|
+
export function setAttr(element, name, value) {
|
|
99
|
+
const attr = element.attrs.find((a) => a.name === name);
|
|
100
|
+
if (attr) {
|
|
101
|
+
attr.value = value;
|
|
102
|
+
}
|
|
103
|
+
else {
|
|
104
|
+
element.attrs.push({ name, value });
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
export function removeAttr(element, name) {
|
|
108
|
+
const index = element.attrs.findIndex((a) => a.name === name);
|
|
109
|
+
if (index !== -1) {
|
|
110
|
+
element.attrs.splice(index, 1);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
/** 1-based start `{ line, col }` of an element from its source-location info (falls back to 1:1). */
|
|
114
|
+
export function startLocation(element) {
|
|
115
|
+
const loc = element.sourceCodeLocation;
|
|
116
|
+
if (loc) {
|
|
117
|
+
return { line: loc.startLine, col: loc.startCol };
|
|
118
|
+
}
|
|
119
|
+
return { line: 1, col: 1 };
|
|
120
|
+
}
|
|
121
|
+
/** Extract the offending source fragment for an element from the original source. */
|
|
122
|
+
export function snippetFor(element, source) {
|
|
123
|
+
const loc = element.sourceCodeLocation;
|
|
124
|
+
if (!loc) {
|
|
125
|
+
return `<${tagName(element)}>`;
|
|
126
|
+
}
|
|
127
|
+
const fragment = source.slice(loc.startOffset, loc.endOffset);
|
|
128
|
+
return fragment.length > 0 ? fragment : `<${tagName(element)}>`;
|
|
129
|
+
}
|
|
130
|
+
//# sourceMappingURL=dom.js.map
|
package/dist/dom.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"dom.js","sourceRoot":"","sources":["../src/dom.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,OAAO,EAGL,KAAK,EACL,SAAS,EACT,cAAc,GACf,MAAM,QAAQ,CAAC;AAehB,kGAAkG;AAClG,MAAM,UAAU,SAAS,CAAC,IAAY;IACpC,MAAM,WAAW,GAAkB,EAAE,CAAC;IACtC,MAAM,QAAQ,GAAG,KAAK,CAAC,IAAI,EAAE;QAC3B,sBAAsB,EAAE,IAAI;QAC5B,YAAY,EAAE,CAAC,KAAK,EAAE,EAAE,CAAC,WAAW,CAAC,IAAI,CAAC,KAAK,CAAC;KACjD,CAAC,CAAC;IACH,OAAO,EAAE,QAAQ,EAAE,WAAW,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC;AACjD,CAAC;AAED,uGAAuG;AACvG,MAAM,UAAU,iBAAiB,CAAC,QAAkB;IAClD,OAAO,SAAS,CAAC,QAAQ,CAAC,CAAC;AAC7B,CAAC;AAED,qGAAqG;AACrG,MAAM,UAAU,gBAAgB,CAAC,OAAgB;IAC/C,OAAO,cAAc,CAAC,OAAO,CAAC,CAAC;AACjC,CAAC;AAED,SAAS,SAAS,CAAC,IAAsB;IACvC,OAAO,OAAQ,IAAgB,CAAC,OAAO,KAAK,QAAQ,CAAC;AACvD,CAAC;AAED,4CAA4C;AAC5C,MAAM,UAAU,UAAU,CAAC,IAAe;IACxC,OAAO,IAAI,CAAC,QAAQ,KAAK,OAAO,CAAC;AACnC,CAAC;AAED,kDAAkD;AAClD,MAAM,UAAU,aAAa,CAAC,IAAe;IAC3C,OAAO,IAAI,CAAC,QAAQ,KAAK,UAAU,CAAC;AACtC,CAAC;AAED,2EAA2E;AAC3E,MAAM,UAAU,SAAS,CAAC,IAAe;IACvC,OAAO,UAAU,CAAC,IAAI,CAAC,CAAC,CAAC,CAAE,IAA0B,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,CAAC;AACnE,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,YAAY,CAC1B,QAAkB,EAClB,KAAyD;IAEzD,MAAM,KAAK,GAA6C,CAAC,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC,CAAC;IAC3F,KAAK,IAAI,KAAK,GAAG,KAAK,CAAC,GAAG,EAAE,EAAE,KAAK,KAAK,SAAS,EAAE,KAAK,GAAG,KAAK,CAAC,GAAG,EAAE,EAAE,CAAC;QACvE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,GAAG,KAAK,CAAC;QAC/B,MAAM,OAAO,GAAG,SAAS,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC;QAC9C,IAAI,OAAO,EAAE,CAAC;YACZ,KAAK,CAAC,OAAO,EAAE,MAAM,CAAC,CAAC;QACzB,CAAC;QACD,MAAM,QAAQ,GAAI,IAAqC,CAAC,UAAU,IAAI,EAAE,CAAC;QACzE,MAAM,eAAe,GAAG,OAAO,IAAI,MAAM,CAAC;QAC1C,KAAK,IAAI,CAAC,GAAG,QAAQ,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC,IAAI,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC;YAC9C,MAAM,KAAK,GAAG,QAAQ,CAAC,CAAC,CAAC,CAAC;YAC1B,IAAI,KAAK,KAAK,SAAS,EAAE,CAAC;gBACxB,KAAK,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,KAAK,EAAE,MAAM,EAAE,eAAe,EAAE,CAAC,CAAC;YACvD,CAAC;QACH,CAAC;IACH,CAAC;AACH,CAAC;AAED;;;;;GAKG;AACH,MAAM,CAAC,MAAM,iBAAiB,GAAG,IAAI,CAAC;AAEtC;;;GAGG;AACH,MAAM,UAAU,eAAe,CAAC,QAAkB,EAAE,GAAG,GAAG,iBAAiB;IACzE,MAAM,KAAK,GAAoC,CAAC,EAAE,IAAI,EAAE,QAAQ,EAAE,KAAK,EAAE,CAAC,EAAE,CAAC,CAAC;IAC9E,KAAK,IAAI,KAAK,GAAG,KAAK,CAAC,GAAG,EAAE,EAAE,KAAK,KAAK,SAAS,EAAE,KAAK,GAAG,KAAK,CAAC,GAAG,EAAE,EAAE,CAAC;QACvE,MAAM,EAAE,IAAI,EAAE,KAAK,EAAE,GAAG,KAAK,CAAC;QAC9B,IAAI,KAAK,GAAG,GAAG,EAAE,CAAC;YAChB,OAAO,IAAI,CAAC;QACd,CAAC;QACD,MAAM,QAAQ,GAAI,IAAqC,CAAC,UAAU,IAAI,EAAE,CAAC;QACzE,MAAM,UAAU,GAAG,SAAS,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,KAAK,GAAG,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC;QACvD,KAAK,MAAM,KAAK,IAAI,QAAQ,EAAE,CAAC;YAC7B,KAAK,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,KAAK,EAAE,KAAK,EAAE,UAAU,EAAE,CAAC,CAAC;QACjD,CAAC;IACH,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC;AAED,MAAM,UAAU,OAAO,CAAC,OAAgB;IACtC,OAAO,OAAO,CAAC,OAAO,CAAC,WAAW,EAAE,CAAC;AACvC,CAAC;AAED,MAAM,UAAU,OAAO,CAAC,OAAgB,EAAE,IAAY;IACpD,MAAM,IAAI,GAAG,OAAO,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,IAAI,CAAC,CAAC;IACxD,OAAO,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,CAAC;AAClC,CAAC;AAED,MAAM,UAAU,OAAO,CAAC,OAAgB,EAAE,IAAY,EAAE,KAAa;IACnE,MAAM,IAAI,GAAG,OAAO,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,IAAI,CAAC,CAAC;IACxD,IAAI,IAAI,EAAE,CAAC;QACT,IAAI,CAAC,KAAK,GAAG,KAAK,CAAC;IACrB,CAAC;SAAM,CAAC;QACN,OAAO,CAAC,KAAK,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC;IACtC,CAAC;AACH,CAAC;AAED,MAAM,UAAU,UAAU,CAAC,OAAgB,EAAE,IAAY;IACvD,MAAM,KAAK,GAAG,OAAO,CAAC,KAAK,CAAC,SAAS,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,IAAI,CAAC,CAAC;IAC9D,IAAI,KAAK,KAAK,CAAC,CAAC,EAAE,CAAC;QACjB,OAAO,CAAC,KAAK,CAAC,MAAM,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC;IACjC,CAAC;AACH,CAAC;AAED,qGAAqG;AACrG,MAAM,UAAU,aAAa,CAAC,OAAgB;IAC5C,MAAM,GAAG,GAAG,OAAO,CAAC,kBAAkB,CAAC;IACvC,IAAI,GAAG,EAAE,CAAC;QACR,OAAO,EAAE,IAAI,EAAE,GAAG,CAAC,SAAS,EAAE,GAAG,EAAE,GAAG,CAAC,QAAQ,EAAE,CAAC;IACpD,CAAC;IACD,OAAO,EAAE,IAAI,EAAE,CAAC,EAAE,GAAG,EAAE,CAAC,EAAE,CAAC;AAC7B,CAAC;AAED,qFAAqF;AACrF,MAAM,UAAU,UAAU,CAAC,OAAgB,EAAE,MAAc;IACzD,MAAM,GAAG,GAAG,OAAO,CAAC,kBAAkB,CAAC;IACvC,IAAI,CAAC,GAAG,EAAE,CAAC;QACT,OAAO,IAAI,OAAO,CAAC,OAAO,CAAC,GAAG,CAAC;IACjC,CAAC;IACD,MAAM,QAAQ,GAAG,MAAM,CAAC,KAAK,CAAC,GAAG,CAAC,WAAW,EAAE,GAAG,CAAC,SAAS,CAAC,CAAC;IAC9D,OAAO,QAAQ,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,IAAI,OAAO,CAAC,OAAO,CAAC,GAAG,CAAC;AAClE,CAAC"}
|
package/dist/engine.d.ts
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { Element, ParsedDocument } from "./dom.js";
|
|
2
|
+
import type { Finding, RuleId, Severity } from "./types.js";
|
|
3
|
+
/** A detection hit: the offending element plus the human/agent-facing suggestion. */
|
|
4
|
+
export interface Detection {
|
|
5
|
+
element: Element;
|
|
6
|
+
suggestedFix: string;
|
|
7
|
+
/** Override the element's source location (used by `invalid-markup` parse-error locations). */
|
|
8
|
+
location?: {
|
|
9
|
+
line: number;
|
|
10
|
+
col: number;
|
|
11
|
+
};
|
|
12
|
+
/** Override the snippet (used when the faithful fragment is not the element's serialization). */
|
|
13
|
+
snippet?: string;
|
|
14
|
+
}
|
|
15
|
+
export interface Rule {
|
|
16
|
+
id: RuleId;
|
|
17
|
+
severity: Severity;
|
|
18
|
+
autoFixable: boolean;
|
|
19
|
+
/** Read-only: return every offending element in this document (no mutation). */
|
|
20
|
+
detect(doc: ParsedDocument): Detection[];
|
|
21
|
+
/** Mutate the tree to repair one detected element. Only present on auto-fixable rules. */
|
|
22
|
+
apply?(element: Element): void;
|
|
23
|
+
}
|
|
24
|
+
/** Build a `Finding` from a rule and one of its detections. */
|
|
25
|
+
export declare function toFinding(rule: Rule, detection: Detection, source: string): Finding;
|
|
26
|
+
/**
|
|
27
|
+
* A structured finding for a document nested past `MAX_NESTING_DEPTH`. Surfaced by `diagnose`/`fix`
|
|
28
|
+
* instead of recursing into parse5's serializer and overflowing the stack with a raw `RangeError`.
|
|
29
|
+
* Reuses the `invalid-markup` rule id (a depth-pathological document is structurally invalid) to
|
|
30
|
+
* avoid reshaping the stable public `RuleId` set.
|
|
31
|
+
*/
|
|
32
|
+
export declare function tooDeeplyNestedFinding(): Finding;
|
|
33
|
+
/** Deterministic ordering: by line, then column, then `rule` id as a stable tie-break. */
|
|
34
|
+
export declare function orderFindings(findings: Finding[]): Finding[];
|
package/dist/engine.js
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The shared rule-engine interface. Every rule exposes `detect` (read-only, used by both `diagnose`
|
|
3
|
+
* and `fix`) and, for auto-fixable rules, `apply` (mutates the parse5 tree in place). Sharing one
|
|
4
|
+
* detection path guarantees `diagnose` and `fix` never drift on what counts as a violation.
|
|
5
|
+
*/
|
|
6
|
+
import { compatLink } from "./compat.js";
|
|
7
|
+
import { snippetFor, startLocation } from "./dom.js";
|
|
8
|
+
/** Build a `Finding` from a rule and one of its detections. */
|
|
9
|
+
export function toFinding(rule, detection, source) {
|
|
10
|
+
const location = detection.location ?? startLocation(detection.element);
|
|
11
|
+
const snippet = detection.snippet ?? snippetFor(detection.element, source);
|
|
12
|
+
return {
|
|
13
|
+
rule: rule.id,
|
|
14
|
+
severity: rule.severity,
|
|
15
|
+
autoFixable: rule.autoFixable,
|
|
16
|
+
location,
|
|
17
|
+
suggestedFix: detection.suggestedFix,
|
|
18
|
+
snippet,
|
|
19
|
+
compatLink: compatLink(rule.id),
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* A structured finding for a document nested past `MAX_NESTING_DEPTH`. Surfaced by `diagnose`/`fix`
|
|
24
|
+
* instead of recursing into parse5's serializer and overflowing the stack with a raw `RangeError`.
|
|
25
|
+
* Reuses the `invalid-markup` rule id (a depth-pathological document is structurally invalid) to
|
|
26
|
+
* avoid reshaping the stable public `RuleId` set.
|
|
27
|
+
*/
|
|
28
|
+
export function tooDeeplyNestedFinding() {
|
|
29
|
+
return {
|
|
30
|
+
rule: "invalid-markup",
|
|
31
|
+
severity: "warning",
|
|
32
|
+
autoFixable: false,
|
|
33
|
+
location: { line: 1, col: 1 },
|
|
34
|
+
suggestedFix: "The document nests elements too deeply to process safely. Flatten the markup so nesting stays well below the supported limit.",
|
|
35
|
+
snippet: "",
|
|
36
|
+
compatLink: compatLink("invalid-markup"),
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
/** Deterministic ordering: by line, then column, then `rule` id as a stable tie-break. */
|
|
40
|
+
export function orderFindings(findings) {
|
|
41
|
+
return [...findings].sort((a, b) => {
|
|
42
|
+
if (a.location.line !== b.location.line) {
|
|
43
|
+
return a.location.line - b.location.line;
|
|
44
|
+
}
|
|
45
|
+
if (a.location.col !== b.location.col) {
|
|
46
|
+
return a.location.col - b.location.col;
|
|
47
|
+
}
|
|
48
|
+
return compareRuleId(a.rule, b.rule);
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
function compareRuleId(a, b) {
|
|
52
|
+
if (a < b) {
|
|
53
|
+
return -1;
|
|
54
|
+
}
|
|
55
|
+
if (a > b) {
|
|
56
|
+
return 1;
|
|
57
|
+
}
|
|
58
|
+
return 0;
|
|
59
|
+
}
|
|
60
|
+
//# sourceMappingURL=engine.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"engine.js","sourceRoot":"","sources":["../src/engine.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAEzC,OAAO,EAAE,UAAU,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAuBrD,+DAA+D;AAC/D,MAAM,UAAU,SAAS,CAAC,IAAU,EAAE,SAAoB,EAAE,MAAc;IACxE,MAAM,QAAQ,GAAG,SAAS,CAAC,QAAQ,IAAI,aAAa,CAAC,SAAS,CAAC,OAAO,CAAC,CAAC;IACxE,MAAM,OAAO,GAAG,SAAS,CAAC,OAAO,IAAI,UAAU,CAAC,SAAS,CAAC,OAAO,EAAE,MAAM,CAAC,CAAC;IAC3E,OAAO;QACL,IAAI,EAAE,IAAI,CAAC,EAAE;QACb,QAAQ,EAAE,IAAI,CAAC,QAAQ;QACvB,WAAW,EAAE,IAAI,CAAC,WAAW;QAC7B,QAAQ;QACR,YAAY,EAAE,SAAS,CAAC,YAAY;QACpC,OAAO;QACP,UAAU,EAAE,UAAU,CAAC,IAAI,CAAC,EAAE,CAAC;KAChC,CAAC;AACJ,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,sBAAsB;IACpC,OAAO;QACL,IAAI,EAAE,gBAAgB;QACtB,QAAQ,EAAE,SAAS;QACnB,WAAW,EAAE,KAAK;QAClB,QAAQ,EAAE,EAAE,IAAI,EAAE,CAAC,EAAE,GAAG,EAAE,CAAC,EAAE;QAC7B,YAAY,EACV,+HAA+H;QACjI,OAAO,EAAE,EAAE;QACX,UAAU,EAAE,UAAU,CAAC,gBAAgB,CAAC;KACzC,CAAC;AACJ,CAAC;AAED,0FAA0F;AAC1F,MAAM,UAAU,aAAa,CAAC,QAAmB;IAC/C,OAAO,CAAC,GAAG,QAAQ,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE;QACjC,IAAI,CAAC,CAAC,QAAQ,CAAC,IAAI,KAAK,CAAC,CAAC,QAAQ,CAAC,IAAI,EAAE,CAAC;YACxC,OAAO,CAAC,CAAC,QAAQ,CAAC,IAAI,GAAG,CAAC,CAAC,QAAQ,CAAC,IAAI,CAAC;QAC3C,CAAC;QACD,IAAI,CAAC,CAAC,QAAQ,CAAC,GAAG,KAAK,CAAC,CAAC,QAAQ,CAAC,GAAG,EAAE,CAAC;YACtC,OAAO,CAAC,CAAC,QAAQ,CAAC,GAAG,GAAG,CAAC,CAAC,QAAQ,CAAC,GAAG,CAAC;QACzC,CAAC;QACD,OAAO,aAAa,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC;IACvC,CAAC,CAAC,CAAC;AACL,CAAC;AAED,SAAS,aAAa,CAAC,CAAS,EAAE,CAAS;IACzC,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC;QACV,OAAO,CAAC,CAAC,CAAC;IACZ,CAAC;IACD,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC;QACV,OAAO,CAAC,CAAC;IACX,CAAC;IACD,OAAO,CAAC,CAAC;AACX,CAAC"}
|
package/dist/fix.d.ts
ADDED
package/dist/fix.js
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `fix(html)` — parse with parse5, apply the deterministic codemods, re-serialize, and return the
|
|
3
|
+
* rewritten HTML plus a report. The report lists every applied fix (located in the *original* source)
|
|
4
|
+
* and every remaining, non-auto-fixable finding (located in the *fixed* output). Dev-time/CI only;
|
|
5
|
+
* never runs on the render hot path.
|
|
6
|
+
*
|
|
7
|
+
* Determinism + idempotence: every codemod is a no-op on its own output, and the final step always
|
|
8
|
+
* re-serializes through parse5 — a parse→serialize fixed point — so `fix(fix(x).html).html` is
|
|
9
|
+
* byte-identical to `fix(x).html` and that second report lists no applied fixes.
|
|
10
|
+
*/
|
|
11
|
+
import { exceedsMaxDepth, parseHtml, serializeDocument } from "./dom.js";
|
|
12
|
+
import { orderFindings, toFinding, tooDeeplyNestedFinding } from "./engine.js";
|
|
13
|
+
import { RULES } from "./rules/index.js";
|
|
14
|
+
function isAutoFixable(rule) {
|
|
15
|
+
return rule.autoFixable;
|
|
16
|
+
}
|
|
17
|
+
export function fix(html) {
|
|
18
|
+
const doc = parseHtml(html);
|
|
19
|
+
// A pathologically deep document is left unchanged with a structured finding: parse5's recursive
|
|
20
|
+
// serializer would otherwise overflow the stack. The input is returned verbatim (no mutation).
|
|
21
|
+
if (exceedsMaxDepth(doc.document)) {
|
|
22
|
+
return { html, report: { conformant: false, findings: [tooDeeplyNestedFinding()] } };
|
|
23
|
+
}
|
|
24
|
+
// 1. Detect + apply every auto-fixable rule on the original tree. Findings are recorded with their
|
|
25
|
+
// original-source locations and marked `applied`. `invalid-markup` has no `apply` — its repair
|
|
26
|
+
// is the re-serialization below — but it is still recorded as applied.
|
|
27
|
+
const applied = [];
|
|
28
|
+
for (const rule of RULES) {
|
|
29
|
+
if (!isAutoFixable(rule)) {
|
|
30
|
+
continue;
|
|
31
|
+
}
|
|
32
|
+
for (const detection of rule.detect(doc)) {
|
|
33
|
+
applied.push({ ...toFinding(rule, detection, doc.source), applied: true });
|
|
34
|
+
rule.apply?.(detection.element);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
// 2. Re-serialize the normalized, codemod'd tree. This also repairs mis-nested markup.
|
|
38
|
+
const fixedHtml = serializeDocument(doc.document);
|
|
39
|
+
// 3. Re-detect on the fixed output to collect remaining non-auto-fixable findings (located in the
|
|
40
|
+
// fixed source). Auto-fixable rules no longer match their own output, so only diagnostics remain.
|
|
41
|
+
const fixedDoc = parseHtml(fixedHtml);
|
|
42
|
+
const remaining = RULES.filter((rule) => !isAutoFixable(rule)).flatMap((rule) => rule.detect(fixedDoc).map((detection) => toFinding(rule, detection, fixedDoc.source)));
|
|
43
|
+
const findings = orderFindings([...applied, ...remaining]);
|
|
44
|
+
return {
|
|
45
|
+
html: fixedHtml,
|
|
46
|
+
report: { conformant: findings.length === 0, findings },
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
//# sourceMappingURL=fix.js.map
|
package/dist/fix.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fix.js","sourceRoot":"","sources":["../src/fix.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AACH,OAAO,EAAE,eAAe,EAAE,SAAS,EAAE,iBAAiB,EAAE,MAAM,UAAU,CAAC;AACzE,OAAO,EAAa,aAAa,EAAE,SAAS,EAAE,sBAAsB,EAAE,MAAM,aAAa,CAAC;AAC1F,OAAO,EAAE,KAAK,EAAE,MAAM,kBAAkB,CAAC;AAGzC,SAAS,aAAa,CAAC,IAAU;IAC/B,OAAO,IAAI,CAAC,WAAW,CAAC;AAC1B,CAAC;AAED,MAAM,UAAU,GAAG,CAAC,IAAY;IAC9B,MAAM,GAAG,GAAG,SAAS,CAAC,IAAI,CAAC,CAAC;IAE5B,iGAAiG;IACjG,+FAA+F;IAC/F,IAAI,eAAe,CAAC,GAAG,CAAC,QAAQ,CAAC,EAAE,CAAC;QAClC,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,EAAE,UAAU,EAAE,KAAK,EAAE,QAAQ,EAAE,CAAC,sBAAsB,EAAE,CAAC,EAAE,EAAE,CAAC;IACvF,CAAC;IAED,mGAAmG;IACnG,kGAAkG;IAClG,0EAA0E;IAC1E,MAAM,OAAO,GAAc,EAAE,CAAC;IAC9B,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;QACzB,IAAI,CAAC,aAAa,CAAC,IAAI,CAAC,EAAE,CAAC;YACzB,SAAS;QACX,CAAC;QACD,KAAK,MAAM,SAAS,IAAI,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,EAAE,CAAC;YACzC,OAAO,CAAC,IAAI,CAAC,EAAE,GAAG,SAAS,CAAC,IAAI,EAAE,SAAS,EAAE,GAAG,CAAC,MAAM,CAAC,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC,CAAC;YAC3E,IAAI,CAAC,KAAK,EAAE,CAAC,SAAS,CAAC,OAAO,CAAC,CAAC;QAClC,CAAC;IACH,CAAC;IAED,uFAAuF;IACvF,MAAM,SAAS,GAAG,iBAAiB,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;IAElD,kGAAkG;IAClG,qGAAqG;IACrG,MAAM,QAAQ,GAAG,SAAS,CAAC,SAAS,CAAC,CAAC;IACtC,MAAM,SAAS,GAAc,KAAK,CAAC,MAAM,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC,aAAa,CAAC,IAAI,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,IAAI,EAAE,EAAE,CACzF,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC,GAAG,CAAC,CAAC,SAAS,EAAE,EAAE,CAAC,SAAS,CAAC,IAAI,EAAE,SAAS,EAAE,QAAQ,CAAC,MAAM,CAAC,CAAC,CACtF,CAAC;IAEF,MAAM,QAAQ,GAAG,aAAa,CAAC,CAAC,GAAG,OAAO,EAAE,GAAG,SAAS,CAAC,CAAC,CAAC;IAC3D,OAAO;QACL,IAAI,EAAE,SAAS;QACf,MAAM,EAAE,EAAE,UAAU,EAAE,QAAQ,CAAC,MAAM,KAAK,CAAC,EAAE,QAAQ,EAAE;KACxD,CAAC;AACJ,CAAC"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @vellora/lint — dev-time diagnose + fix for the vellora HTML/CSS subset.
|
|
3
|
+
*
|
|
4
|
+
* `diagnose(html)` returns a structured, AI-agent-ready report of subset violations (read-only).
|
|
5
|
+
* `fix(html)` applies four deterministic, idempotent codemods (inline-svg→PNG, flex/grid-in-<td>→
|
|
6
|
+
* table, img dims→CSS, sanitize invalid markup) and returns `{ html, report }`.
|
|
7
|
+
*
|
|
8
|
+
* This is dev-time/CI tooling and NEVER runs at render time. Importing this module is side-effect
|
|
9
|
+
* free: no network, no filesystem, no work happens until `diagnose`/`fix` is called.
|
|
10
|
+
*/
|
|
11
|
+
export { diagnose } from "./diagnose.js";
|
|
12
|
+
export { fix } from "./fix.js";
|
|
13
|
+
export { COMPAT_LINKS } from "./compat.js";
|
|
14
|
+
export type { Finding, Report, RuleId, Severity, SourceLocation } from "./types.js";
|
|
15
|
+
/** Package name, retained for the scaffold smoke test. */
|
|
16
|
+
export declare const name = "@vellora/lint";
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @vellora/lint — dev-time diagnose + fix for the vellora HTML/CSS subset.
|
|
3
|
+
*
|
|
4
|
+
* `diagnose(html)` returns a structured, AI-agent-ready report of subset violations (read-only).
|
|
5
|
+
* `fix(html)` applies four deterministic, idempotent codemods (inline-svg→PNG, flex/grid-in-<td>→
|
|
6
|
+
* table, img dims→CSS, sanitize invalid markup) and returns `{ html, report }`.
|
|
7
|
+
*
|
|
8
|
+
* This is dev-time/CI tooling and NEVER runs at render time. Importing this module is side-effect
|
|
9
|
+
* free: no network, no filesystem, no work happens until `diagnose`/`fix` is called.
|
|
10
|
+
*/
|
|
11
|
+
export { diagnose } from "./diagnose.js";
|
|
12
|
+
export { fix } from "./fix.js";
|
|
13
|
+
export { COMPAT_LINKS } from "./compat.js";
|
|
14
|
+
/** Package name, retained for the scaffold smoke test. */
|
|
15
|
+
export const name = "@vellora/lint";
|
|
16
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AACH,OAAO,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AACzC,OAAO,EAAE,GAAG,EAAE,MAAM,UAAU,CAAC;AAC/B,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAG3C,0DAA0D;AAC1D,MAAM,CAAC,MAAM,IAAI,GAAG,eAAe,CAAC"}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `css-animation` diagnostic: `@keyframes` and the `animation` property are dynamic CSS outside the
|
|
3
|
+
* static PDF subset (nothing animates in a print document). There is no deterministic codemod — we
|
|
4
|
+
* cannot guess the intended static appearance — so this is reported with `autoFixable: false` and
|
|
5
|
+
* never rewritten. The finding is anchored to the offending `<style>` element for a stable location.
|
|
6
|
+
*/
|
|
7
|
+
import { startLocation, tagName, textValue, walkElements } from "../dom.js";
|
|
8
|
+
/** `@keyframes ...` or an `animation`/`animation-*` declaration anywhere in the stylesheet text. */
|
|
9
|
+
const ANIMATION_PATTERN = /@keyframes\b|(^|[;{\s])animation(-[a-z-]+)?\s*:/m;
|
|
10
|
+
/** Drop HTML-comment spans so a `@keyframes` mentioned in a comment is not flagged as real CSS. */
|
|
11
|
+
function stripComments(css) {
|
|
12
|
+
return css.replace(/<!--[\s\S]*?-->/g, "");
|
|
13
|
+
}
|
|
14
|
+
function styleText(element) {
|
|
15
|
+
let text = "";
|
|
16
|
+
for (const child of element.childNodes) {
|
|
17
|
+
text += textValue(child);
|
|
18
|
+
}
|
|
19
|
+
return text;
|
|
20
|
+
}
|
|
21
|
+
const SUGGESTED_FIX = "Remove @keyframes and the animation property. Animation has no meaning in a static PDF; use a static style for the printed state. There is no automatic fix.";
|
|
22
|
+
export const cssAnimationRule = {
|
|
23
|
+
id: "css-animation",
|
|
24
|
+
severity: "error",
|
|
25
|
+
autoFixable: false,
|
|
26
|
+
detect(doc) {
|
|
27
|
+
const detections = [];
|
|
28
|
+
walkElements(doc.document, (element) => {
|
|
29
|
+
if (tagName(element) !== "style") {
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
if (ANIMATION_PATTERN.test(stripComments(styleText(element)))) {
|
|
33
|
+
detections.push({
|
|
34
|
+
element,
|
|
35
|
+
location: startLocation(element),
|
|
36
|
+
snippet: "@keyframes / animation",
|
|
37
|
+
suggestedFix: SUGGESTED_FIX,
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
return detections;
|
|
42
|
+
},
|
|
43
|
+
};
|
|
44
|
+
//# sourceMappingURL=css-animation.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"css-animation.js","sourceRoot":"","sources":["../../src/rules/css-animation.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,OAAO,EAAgB,aAAa,EAAE,OAAO,EAAE,SAAS,EAAE,YAAY,EAAE,MAAM,WAAW,CAAC;AAG1F,oGAAoG;AACpG,MAAM,iBAAiB,GAAG,kDAAkD,CAAC;AAE7E,mGAAmG;AACnG,SAAS,aAAa,CAAC,GAAW;IAChC,OAAO,GAAG,CAAC,OAAO,CAAC,kBAAkB,EAAE,EAAE,CAAC,CAAC;AAC7C,CAAC;AAED,SAAS,SAAS,CAAC,OAAgB;IACjC,IAAI,IAAI,GAAG,EAAE,CAAC;IACd,KAAK,MAAM,KAAK,IAAI,OAAO,CAAC,UAAU,EAAE,CAAC;QACvC,IAAI,IAAI,SAAS,CAAC,KAAK,CAAC,CAAC;IAC3B,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC;AAED,MAAM,aAAa,GACjB,8JAA8J,CAAC;AAEjK,MAAM,CAAC,MAAM,gBAAgB,GAAS;IACpC,EAAE,EAAE,eAAe;IACnB,QAAQ,EAAE,OAAO;IACjB,WAAW,EAAE,KAAK;IAClB,MAAM,CAAC,GAAG;QACR,MAAM,UAAU,GAAgB,EAAE,CAAC;QACnC,YAAY,CAAC,GAAG,CAAC,QAAQ,EAAE,CAAC,OAAO,EAAE,EAAE;YACrC,IAAI,OAAO,CAAC,OAAO,CAAC,KAAK,OAAO,EAAE,CAAC;gBACjC,OAAO;YACT,CAAC;YACD,IAAI,iBAAiB,CAAC,IAAI,CAAC,aAAa,CAAC,SAAS,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,CAAC;gBAC9D,UAAU,CAAC,IAAI,CAAC;oBACd,OAAO;oBACP,QAAQ,EAAE,aAAa,CAAC,OAAO,CAAC;oBAChC,OAAO,EAAE,wBAAwB;oBACjC,YAAY,EAAE,aAAa;iBAC5B,CAAC,CAAC;YACL,CAAC;QACH,CAAC,CAAC,CAAC;QACH,OAAO,UAAU,CAAC;IACpB,CAAC;CACF,CAAC"}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `flex-grid-in-td` codemod: a `<td>` whose inline `style` uses `display:flex` or `display:grid` is
|
|
3
|
+
* rewritten into an equivalent single-row nested `<table>`. Each child block becomes a cell of that
|
|
4
|
+
* row, preserving order and content; the `display` declaration is removed from the outer `<td>`.
|
|
5
|
+
*
|
|
6
|
+
* Scope (documented trade-off): this captures the common document pattern — a single row of inline
|
|
7
|
+
* blocks inside a cell. Wrapping, alignment, and gaps are not reproduced; cases beyond this pattern
|
|
8
|
+
* are still *diagnosed*, never silently mis-fixed. Idempotent — once `display` is gone the cell no
|
|
9
|
+
* longer matches, and the produced nested `<td>`s carry no `display:flex/grid`.
|
|
10
|
+
*/
|
|
11
|
+
import { getAttr, isCommentNode, isTextNode, removeAttr, setAttr, tagName, textValue, walkElements, } from "../dom.js";
|
|
12
|
+
import { parseStyle, serializeStyle } from "../style.js";
|
|
13
|
+
function isFlexGridDisplay(d) {
|
|
14
|
+
return d.property === "display" && (d.value === "flex" || d.value === "grid");
|
|
15
|
+
}
|
|
16
|
+
function flexOrGridDisplay(style) {
|
|
17
|
+
return parseStyle(style).some(isFlexGridDisplay);
|
|
18
|
+
}
|
|
19
|
+
function isFlexGridTd(element) {
|
|
20
|
+
if (tagName(element) !== "td") {
|
|
21
|
+
return false;
|
|
22
|
+
}
|
|
23
|
+
const style = getAttr(element, "style");
|
|
24
|
+
return style !== null && flexOrGridDisplay(style);
|
|
25
|
+
}
|
|
26
|
+
const SUGGESTED_FIX = "Replace display:flex/grid on this <td> with a nested <table> laying the children out in a single row. Flexbox and grid are outside the static PDF layout subset.";
|
|
27
|
+
export const flexGridInTdRule = {
|
|
28
|
+
id: "flex-grid-in-td",
|
|
29
|
+
severity: "warning",
|
|
30
|
+
autoFixable: true,
|
|
31
|
+
detect(doc) {
|
|
32
|
+
const detections = [];
|
|
33
|
+
walkElements(doc.document, (element) => {
|
|
34
|
+
if (isFlexGridTd(element)) {
|
|
35
|
+
detections.push({ element, suggestedFix: SUGGESTED_FIX });
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
return detections;
|
|
39
|
+
},
|
|
40
|
+
apply(td) {
|
|
41
|
+
removeDisplayDeclaration(td);
|
|
42
|
+
const cells = td.childNodes.filter(isMeaningfulChild);
|
|
43
|
+
if (cells.length === 0) {
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
const innerCells = cells.map((child) => makeCell(td, child));
|
|
47
|
+
const row = makeElement("tr", td, innerCells);
|
|
48
|
+
const tbody = makeElement("tbody", td, [row]);
|
|
49
|
+
const table = makeElement("table", td, [tbody]);
|
|
50
|
+
td.childNodes = [table];
|
|
51
|
+
},
|
|
52
|
+
};
|
|
53
|
+
/** Drop only the `display:flex`/`display:grid` declaration, keeping any other inline styles. */
|
|
54
|
+
function removeDisplayDeclaration(td) {
|
|
55
|
+
const declarations = parseStyle(getAttr(td, "style") ?? "").filter((d) => !isFlexGridDisplay(d));
|
|
56
|
+
if (declarations.length > 0) {
|
|
57
|
+
setAttr(td, "style", serializeStyle(declarations));
|
|
58
|
+
}
|
|
59
|
+
else {
|
|
60
|
+
removeAttr(td, "style");
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
/** Whitespace-only text between flex items is layout gap, not content — it does not become a cell. */
|
|
64
|
+
function isMeaningfulChild(node) {
|
|
65
|
+
if (isTextNode(node)) {
|
|
66
|
+
return textValue(node).trim() !== "";
|
|
67
|
+
}
|
|
68
|
+
if (isCommentNode(node)) {
|
|
69
|
+
return false;
|
|
70
|
+
}
|
|
71
|
+
return true;
|
|
72
|
+
}
|
|
73
|
+
function makeCell(parent, child) {
|
|
74
|
+
const cell = makeElement("td", parent, [child]);
|
|
75
|
+
child.parentNode = cell;
|
|
76
|
+
return cell;
|
|
77
|
+
}
|
|
78
|
+
function makeElement(name, parentNode, childNodes) {
|
|
79
|
+
const element = {
|
|
80
|
+
nodeName: name,
|
|
81
|
+
tagName: name,
|
|
82
|
+
attrs: [],
|
|
83
|
+
namespaceURI: parentNode.namespaceURI,
|
|
84
|
+
childNodes,
|
|
85
|
+
parentNode,
|
|
86
|
+
};
|
|
87
|
+
for (const child of childNodes) {
|
|
88
|
+
child.parentNode = element;
|
|
89
|
+
}
|
|
90
|
+
return element;
|
|
91
|
+
}
|
|
92
|
+
//# sourceMappingURL=flex-grid-in-td.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"flex-grid-in-td.js","sourceRoot":"","sources":["../../src/rules/flex-grid-in-td.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `img-dimension-attrs` codemod: move presentational `width`/`height` HTML attributes on `<img>`
|
|
3
|
+
* into equivalent CSS, preserving any existing inline `style`, and remove the original attributes.
|
|
4
|
+
* Unitless numeric values are treated as pixels. Idempotent — `detect` matches only the *attributes*,
|
|
5
|
+
* and the produced CSS is never re-matched.
|
|
6
|
+
*/
|
|
7
|
+
import { getAttr, removeAttr, setAttr, tagName, walkElements } from "../dom.js";
|
|
8
|
+
import { parseStyle, serializeStyle, toCssLength } from "../style.js";
|
|
9
|
+
const DIMENSION_ATTRS = ["width", "height"];
|
|
10
|
+
const SUGGESTED_FIX = 'Move the width/height HTML attributes into CSS (e.g. style="width:120px;height:80px"). Presentational image dimensions belong in the stylesheet, not as attributes.';
|
|
11
|
+
function hasDimensionAttr(img) {
|
|
12
|
+
return DIMENSION_ATTRS.some((name) => getAttr(img, name) !== null);
|
|
13
|
+
}
|
|
14
|
+
export const imgDimensionAttrsRule = {
|
|
15
|
+
id: "img-dimension-attrs",
|
|
16
|
+
severity: "warning",
|
|
17
|
+
autoFixable: true,
|
|
18
|
+
detect(doc) {
|
|
19
|
+
const detections = [];
|
|
20
|
+
walkElements(doc.document, (element) => {
|
|
21
|
+
if (tagName(element) === "img" && hasDimensionAttr(element)) {
|
|
22
|
+
detections.push({ element, suggestedFix: SUGGESTED_FIX });
|
|
23
|
+
}
|
|
24
|
+
});
|
|
25
|
+
return detections;
|
|
26
|
+
},
|
|
27
|
+
apply(img) {
|
|
28
|
+
const declarations = parseStyle(getAttr(img, "style") ?? "");
|
|
29
|
+
for (const name of DIMENSION_ATTRS) {
|
|
30
|
+
const value = getAttr(img, name);
|
|
31
|
+
if (value === null) {
|
|
32
|
+
continue;
|
|
33
|
+
}
|
|
34
|
+
removeAttr(img, name);
|
|
35
|
+
const cssValue = toCssLength(value);
|
|
36
|
+
const existing = declarations.find((d) => d.property === name);
|
|
37
|
+
if (existing) {
|
|
38
|
+
existing.value = cssValue;
|
|
39
|
+
}
|
|
40
|
+
else {
|
|
41
|
+
declarations.push({ property: name, value: cssValue });
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
if (declarations.length > 0) {
|
|
45
|
+
setAttr(img, "style", serializeStyle(declarations));
|
|
46
|
+
}
|
|
47
|
+
},
|
|
48
|
+
};
|
|
49
|
+
//# sourceMappingURL=img-dimension-attrs.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"img-dimension-attrs.js","sourceRoot":"","sources":["../../src/rules/img-dimension-attrs.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,OAAO,EAAgB,OAAO,EAAE,UAAU,EAAE,OAAO,EAAE,OAAO,EAAE,YAAY,EAAE,MAAM,WAAW,CAAC;AAE9F,OAAO,EAAE,UAAU,EAAE,cAAc,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAEtE,MAAM,eAAe,GAAG,CAAC,OAAO,EAAE,QAAQ,CAAU,CAAC;AAErD,MAAM,aAAa,GACjB,qKAAqK,CAAC;AAExK,SAAS,gBAAgB,CAAC,GAAY;IACpC,OAAO,eAAe,CAAC,IAAI,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,OAAO,CAAC,GAAG,EAAE,IAAI,CAAC,KAAK,IAAI,CAAC,CAAC;AACrE,CAAC;AAED,MAAM,CAAC,MAAM,qBAAqB,GAAS;IACzC,EAAE,EAAE,qBAAqB;IACzB,QAAQ,EAAE,SAAS;IACnB,WAAW,EAAE,IAAI;IACjB,MAAM,CAAC,GAAG;QACR,MAAM,UAAU,GAAgB,EAAE,CAAC;QACnC,YAAY,CAAC,GAAG,CAAC,QAAQ,EAAE,CAAC,OAAO,EAAE,EAAE;YACrC,IAAI,OAAO,CAAC,OAAO,CAAC,KAAK,KAAK,IAAI,gBAAgB,CAAC,OAAO,CAAC,EAAE,CAAC;gBAC5D,UAAU,CAAC,IAAI,CAAC,EAAE,OAAO,EAAE,YAAY,EAAE,aAAa,EAAE,CAAC,CAAC;YAC5D,CAAC;QACH,CAAC,CAAC,CAAC;QACH,OAAO,UAAU,CAAC;IACpB,CAAC;IACD,KAAK,CAAC,GAAG;QACP,MAAM,YAAY,GAAG,UAAU,CAAC,OAAO,CAAC,GAAG,EAAE,OAAO,CAAC,IAAI,EAAE,CAAC,CAAC;QAC7D,KAAK,MAAM,IAAI,IAAI,eAAe,EAAE,CAAC;YACnC,MAAM,KAAK,GAAG,OAAO,CAAC,GAAG,EAAE,IAAI,CAAC,CAAC;YACjC,IAAI,KAAK,KAAK,IAAI,EAAE,CAAC;gBACnB,SAAS;YACX,CAAC;YACD,UAAU,CAAC,GAAG,EAAE,IAAI,CAAC,CAAC;YACtB,MAAM,QAAQ,GAAG,WAAW,CAAC,KAAK,CAAC,CAAC;YACpC,MAAM,QAAQ,GAAG,YAAY,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,QAAQ,KAAK,IAAI,CAAC,CAAC;YAC/D,IAAI,QAAQ,EAAE,CAAC;gBACb,QAAQ,CAAC,KAAK,GAAG,QAAQ,CAAC;YAC5B,CAAC;iBAAM,CAAC;gBACN,YAAY,CAAC,IAAI,CAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,KAAK,EAAE,QAAQ,EAAE,CAAC,CAAC;YACzD,CAAC;QACH,CAAC;QACD,IAAI,YAAY,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YAC5B,OAAO,CAAC,GAAG,EAAE,OAAO,EAAE,cAAc,CAAC,YAAY,CAAC,CAAC,CAAC;QACtD,CAAC;IACH,CAAC;CACF,CAAC"}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The ordered rule registry. `diagnose` and `fix` both iterate this single list so the two surfaces
|
|
3
|
+
* agree on what is a violation. Order here only affects fix application order; report findings are
|
|
4
|
+
* re-sorted by `(line, col, rule)` afterward.
|
|
5
|
+
*/
|
|
6
|
+
import type { Rule } from "../engine.js";
|
|
7
|
+
export declare const RULES: Rule[];
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { cssAnimationRule } from "./css-animation.js";
|
|
2
|
+
import { flexGridInTdRule } from "./flex-grid-in-td.js";
|
|
3
|
+
import { imgDimensionAttrsRule } from "./img-dimension-attrs.js";
|
|
4
|
+
import { inlineSvgRule } from "./inline-svg.js";
|
|
5
|
+
import { invalidMarkupRule } from "./invalid-markup.js";
|
|
6
|
+
import { scriptElementRule } from "./script-element.js";
|
|
7
|
+
export const RULES = [
|
|
8
|
+
inlineSvgRule,
|
|
9
|
+
flexGridInTdRule,
|
|
10
|
+
imgDimensionAttrsRule,
|
|
11
|
+
invalidMarkupRule,
|
|
12
|
+
scriptElementRule,
|
|
13
|
+
cssAnimationRule,
|
|
14
|
+
];
|
|
15
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/rules/index.ts"],"names":[],"mappings":"AAMA,OAAO,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAC;AACtD,OAAO,EAAE,gBAAgB,EAAE,MAAM,sBAAsB,CAAC;AACxD,OAAO,EAAE,qBAAqB,EAAE,MAAM,0BAA0B,CAAC;AACjE,OAAO,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAChD,OAAO,EAAE,iBAAiB,EAAE,MAAM,qBAAqB,CAAC;AACxD,OAAO,EAAE,iBAAiB,EAAE,MAAM,qBAAqB,CAAC;AAExD,MAAM,CAAC,MAAM,KAAK,GAAW;IAC3B,aAAa;IACb,gBAAgB;IAChB,qBAAqB;IACrB,iBAAiB;IACjB,iBAAiB;IACjB,gBAAgB;CACjB,CAAC"}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `inline-svg` codemod: rasterize each inline `<svg>` to a PNG in-process via `@resvg/resvg-js` and
|
|
3
|
+
* replace the `<svg>` element with an `<img>` carrying a `data:image/png;base64` URI. Idempotent by
|
|
4
|
+
* construction — `detect` only matches `<svg>`, and the produced `<img>` is never re-matched.
|
|
5
|
+
*
|
|
6
|
+
* Determinism: resvg is pinned to `loadSystemFonts: false` and `fitTo: original`, with no time- or
|
|
7
|
+
* locale-dependent inputs, so the PNG bytes are byte-stable across runs and platforms.
|
|
8
|
+
*/
|
|
9
|
+
import { Resvg } from "@resvg/resvg-js";
|
|
10
|
+
import { html } from "parse5";
|
|
11
|
+
import { getAttr, serializeElement, tagName, walkElements } from "../dom.js";
|
|
12
|
+
import { serializeStyle, toCssLength } from "../style.js";
|
|
13
|
+
const RESVG_OPTIONS = {
|
|
14
|
+
font: { loadSystemFonts: false },
|
|
15
|
+
fitTo: { mode: "original" },
|
|
16
|
+
};
|
|
17
|
+
const SVG_NS = 'xmlns="http://www.w3.org/2000/svg"';
|
|
18
|
+
const SUGGESTED_FIX = "Rasterize the inline <svg> to a PNG and reference it via an <img> with a data: URI, or provide the asset as a bundled raster image. SVG is outside the static PDF subset.";
|
|
19
|
+
/** resvg requires a namespaced root; parse5 may serialize an inline `<svg>` without `xmlns`. */
|
|
20
|
+
function ensureSvgNamespace(svgMarkup) {
|
|
21
|
+
if (svgMarkup.includes("xmlns=")) {
|
|
22
|
+
return svgMarkup;
|
|
23
|
+
}
|
|
24
|
+
return svgMarkup.replace(/^<svg/, `<svg ${SVG_NS}`);
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Rasterize an inline `<svg>` to a PNG data URI. Returns `null` (not throws) when resvg cannot
|
|
28
|
+
* render the SVG (malformed XML, an unsupported feature, or text needing a font while
|
|
29
|
+
* `loadSystemFonts: false` is pinned), so one bad SVG leaves itself unconverted rather than crashing
|
|
30
|
+
* the whole document — the strict re-detect then reports it as a located diagnostic.
|
|
31
|
+
*/
|
|
32
|
+
function rasterizeToDataUri(svgMarkup) {
|
|
33
|
+
try {
|
|
34
|
+
const png = new Resvg(ensureSvgNamespace(svgMarkup), RESVG_OPTIONS).render().asPng();
|
|
35
|
+
return `data:image/png;base64,${png.toString("base64")}`;
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
export const inlineSvgRule = {
|
|
42
|
+
id: "inline-svg",
|
|
43
|
+
severity: "error",
|
|
44
|
+
autoFixable: true,
|
|
45
|
+
detect(doc) {
|
|
46
|
+
const detections = [];
|
|
47
|
+
walkElements(doc.document, (element) => {
|
|
48
|
+
if (tagName(element) === "svg") {
|
|
49
|
+
detections.push({ element, suggestedFix: SUGGESTED_FIX });
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
return detections;
|
|
53
|
+
},
|
|
54
|
+
apply(svg) {
|
|
55
|
+
const svgMarkup = serializeElement(svg);
|
|
56
|
+
const dataUri = rasterizeToDataUri(svgMarkup);
|
|
57
|
+
if (dataUri === null) {
|
|
58
|
+
// resvg could not render this SVG; leave it unconverted so the re-detect surfaces a located
|
|
59
|
+
// diagnostic instead of the whole render crashing on one bad element.
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
convertSvgToImg(svg, dataUri);
|
|
63
|
+
},
|
|
64
|
+
};
|
|
65
|
+
/**
|
|
66
|
+
* Rewrite an `<svg>` element in place into an `<img>` carrying the PNG data URI. The SVG's `width`
|
|
67
|
+
* and `height` are emitted as CSS (not attributes) so the produced `<img>` is already conformant and
|
|
68
|
+
* is never re-matched by the `img-dimension-attrs` rule — preserving the fixed-point guarantee.
|
|
69
|
+
*/
|
|
70
|
+
function convertSvgToImg(svg, dataUri) {
|
|
71
|
+
const width = getAttr(svg, "width");
|
|
72
|
+
const height = getAttr(svg, "height");
|
|
73
|
+
const declarations = [];
|
|
74
|
+
if (width !== null) {
|
|
75
|
+
declarations.push({ property: "width", value: toCssLength(width) });
|
|
76
|
+
}
|
|
77
|
+
if (height !== null) {
|
|
78
|
+
declarations.push({ property: "height", value: toCssLength(height) });
|
|
79
|
+
}
|
|
80
|
+
// Mutate in place so parentNode links stay valid: become an <img>, drop SVG children/attrs. The
|
|
81
|
+
// namespace must move from SVG to HTML so parse5 serializes a void <img> (no closing tag) — this
|
|
82
|
+
// is what makes the codemod a re-serialization fixed point.
|
|
83
|
+
svg.tagName = "img";
|
|
84
|
+
svg.nodeName = "img";
|
|
85
|
+
svg.namespaceURI = html.NS.HTML;
|
|
86
|
+
svg.childNodes = [];
|
|
87
|
+
svg.attrs = [{ name: "src", value: dataUri }];
|
|
88
|
+
if (declarations.length > 0) {
|
|
89
|
+
svg.attrs.push({ name: "style", value: serializeStyle(declarations) });
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
//# sourceMappingURL=inline-svg.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"inline-svg.js","sourceRoot":"","sources":["../../src/rules/inline-svg.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `invalid-markup` rule: detect and repair mis-nested tags and malformed `style=` attributes.
|
|
3
|
+
*
|
|
4
|
+
* Mis-nesting is repaired implicitly — `fix()` re-serializes parse5's normalized tree, which the
|
|
5
|
+
* HTML5 tree-construction (adoption-agency) algorithm has already re-nested correctly. Malformed
|
|
6
|
+
* `style=` is repaired explicitly in `apply` by re-parsing the attribute (which drops the invalid
|
|
7
|
+
* declarations) and re-serializing the surviving ones. Both paths are idempotent: a well-formed tree
|
|
8
|
+
* re-serializes unchanged, and a cleaned `style` re-parses to itself.
|
|
9
|
+
*
|
|
10
|
+
* Mis-nesting note: parse5 v8 silently repairs the adoption-agency case (`<b><i>x</b></i>`) WITHOUT
|
|
11
|
+
* an `onParseError` callback, so we detect it structurally: a formatting element that sits in the
|
|
12
|
+
* source (has a start-tag location) but lost its end-tag location because an enclosing formatting
|
|
13
|
+
* element was closed first. Malformed `style=` is detected on the parsed attribute.
|
|
14
|
+
*/
|
|
15
|
+
import { getAttr, removeAttr, setAttr, tagName, walkElements } from "../dom.js";
|
|
16
|
+
import { hasMalformedStyle, parseStyle, serializeStyle } from "../style.js";
|
|
17
|
+
/** Inline formatting elements subject to the HTML5 adoption-agency algorithm. */
|
|
18
|
+
const FORMATTING_ELEMENTS = new Set([
|
|
19
|
+
"a",
|
|
20
|
+
"b",
|
|
21
|
+
"big",
|
|
22
|
+
"code",
|
|
23
|
+
"em",
|
|
24
|
+
"font",
|
|
25
|
+
"i",
|
|
26
|
+
"nobr",
|
|
27
|
+
"s",
|
|
28
|
+
"small",
|
|
29
|
+
"strike",
|
|
30
|
+
"strong",
|
|
31
|
+
"tt",
|
|
32
|
+
"u",
|
|
33
|
+
]);
|
|
34
|
+
/**
|
|
35
|
+
* A mis-nested formatting element: present in source (`startTag` location) but missing its own
|
|
36
|
+
* `endTag` location while nested directly inside another source-present formatting element. This is
|
|
37
|
+
* exactly the footprint adoption-agency repair leaves on `<b><i>x</b></i>`.
|
|
38
|
+
*/
|
|
39
|
+
function isMisNested(element, parent) {
|
|
40
|
+
if (!FORMATTING_ELEMENTS.has(tagName(element))) {
|
|
41
|
+
return false;
|
|
42
|
+
}
|
|
43
|
+
const loc = element.sourceCodeLocation;
|
|
44
|
+
if (!loc || !loc.startTag || loc.endTag) {
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
47
|
+
if (!parent || !FORMATTING_ELEMENTS.has(tagName(parent))) {
|
|
48
|
+
return false;
|
|
49
|
+
}
|
|
50
|
+
const parentLoc = parent.sourceCodeLocation;
|
|
51
|
+
return Boolean(parentLoc?.startTag);
|
|
52
|
+
}
|
|
53
|
+
const MISNEST_FIX = "Close inline tags in the order they were opened. The HTML5 parser normalizes mis-nested tags; the fix re-serializes the well-formed tree.";
|
|
54
|
+
const STYLE_FIX = "Repair the malformed style attribute: every declaration needs a property:value pair. Invalid declarations are dropped.";
|
|
55
|
+
export const invalidMarkupRule = {
|
|
56
|
+
id: "invalid-markup",
|
|
57
|
+
severity: "warning",
|
|
58
|
+
autoFixable: true,
|
|
59
|
+
detect(doc) {
|
|
60
|
+
const detections = [];
|
|
61
|
+
walkElements(doc.document, (element, parent) => {
|
|
62
|
+
if (isMisNested(element, parent)) {
|
|
63
|
+
detections.push({ element, suggestedFix: MISNEST_FIX });
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
const style = getAttr(element, "style");
|
|
67
|
+
if (style !== null && hasMalformedStyle(style)) {
|
|
68
|
+
detections.push({ element, suggestedFix: STYLE_FIX });
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
return detections;
|
|
72
|
+
},
|
|
73
|
+
apply(element) {
|
|
74
|
+
// Only the malformed-`style` case needs an explicit rewrite; mis-nesting is repaired by parse5
|
|
75
|
+
// re-serialization. Re-parsing the style drops invalid declarations and keeps the valid ones.
|
|
76
|
+
const style = getAttr(element, "style");
|
|
77
|
+
if (style === null || !hasMalformedStyle(style)) {
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
const declarations = parseStyle(style);
|
|
81
|
+
if (declarations.length > 0) {
|
|
82
|
+
setAttr(element, "style", serializeStyle(declarations));
|
|
83
|
+
}
|
|
84
|
+
else {
|
|
85
|
+
removeAttr(element, "style");
|
|
86
|
+
}
|
|
87
|
+
},
|
|
88
|
+
};
|
|
89
|
+
//# sourceMappingURL=invalid-markup.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"invalid-markup.js","sourceRoot":"","sources":["../../src/rules/invalid-markup.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `script-element` diagnostic: an inline `<script>` has no meaning in a static PDF and is rejected by
|
|
3
|
+
* the strict gate. There is no deterministic codemod (we cannot infer the author's intent), so this
|
|
4
|
+
* is reported with `autoFixable: false` and never rewritten.
|
|
5
|
+
*/
|
|
6
|
+
import { tagName, walkElements } from "../dom.js";
|
|
7
|
+
const SUGGESTED_FIX = "Remove the <script> element. JavaScript does not execute in a static PDF; the strict render gate rejects it. There is no automatic fix.";
|
|
8
|
+
export const scriptElementRule = {
|
|
9
|
+
id: "script-element",
|
|
10
|
+
severity: "error",
|
|
11
|
+
autoFixable: false,
|
|
12
|
+
detect(doc) {
|
|
13
|
+
const detections = [];
|
|
14
|
+
walkElements(doc.document, (element) => {
|
|
15
|
+
if (tagName(element) === "script") {
|
|
16
|
+
detections.push({ element, suggestedFix: SUGGESTED_FIX });
|
|
17
|
+
}
|
|
18
|
+
});
|
|
19
|
+
return detections;
|
|
20
|
+
},
|
|
21
|
+
};
|
|
22
|
+
//# sourceMappingURL=script-element.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"script-element.js","sourceRoot":"","sources":["../../src/rules/script-element.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,OAAO,EAAE,OAAO,EAAE,YAAY,EAAE,MAAM,WAAW,CAAC;AAGlD,MAAM,aAAa,GACjB,yIAAyI,CAAC;AAE5I,MAAM,CAAC,MAAM,iBAAiB,GAAS;IACrC,EAAE,EAAE,gBAAgB;IACpB,QAAQ,EAAE,OAAO;IACjB,WAAW,EAAE,KAAK;IAClB,MAAM,CAAC,GAAG;QACR,MAAM,UAAU,GAAgB,EAAE,CAAC;QACnC,YAAY,CAAC,GAAG,CAAC,QAAQ,EAAE,CAAC,OAAO,EAAE,EAAE;YACrC,IAAI,OAAO,CAAC,OAAO,CAAC,KAAK,QAAQ,EAAE,CAAC;gBAClC,UAAU,CAAC,IAAI,CAAC,EAAE,OAAO,EAAE,YAAY,EAAE,aAAa,EAAE,CAAC,CAAC;YAC5D,CAAC;QACH,CAAC,CAAC,CAAC;QACH,OAAO,UAAU,CAAC;IACpB,CAAC;CACF,CAAC"}
|
package/dist/style.d.ts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A tiny, deterministic inline-`style` parser/serializer. Order-preserving so codemod output is
|
|
3
|
+
* byte-stable: declarations keep source order, and appended declarations land at the end. Property
|
|
4
|
+
* names are lower-cased for matching; values are trimmed. This is not a full CSS parser — it only
|
|
5
|
+
* needs to read/edit the `display`, `width`, and `height` declarations the codemods touch.
|
|
6
|
+
*/
|
|
7
|
+
export interface Declaration {
|
|
8
|
+
property: string;
|
|
9
|
+
value: string;
|
|
10
|
+
}
|
|
11
|
+
/** Parse an inline `style` value into ordered declarations, dropping empty/malformed segments. */
|
|
12
|
+
export declare function parseStyle(style: string): Declaration[];
|
|
13
|
+
/** Serialize declarations back into a canonical `prop:value;prop:value` string (no trailing space). */
|
|
14
|
+
export declare function serializeStyle(declarations: Declaration[]): string;
|
|
15
|
+
/** Treat a unitless numeric dimension as pixels; pass through values that already carry a unit. */
|
|
16
|
+
export declare function toCssLength(value: string): string;
|
|
17
|
+
/**
|
|
18
|
+
* A `style` value is malformed when it contains a non-empty segment that has no `:` separator (a
|
|
19
|
+
* bare token like `display flex`) — a declaration that the browser/engine would silently drop. Empty
|
|
20
|
+
* segments (trailing `;`) are tolerated, not malformed.
|
|
21
|
+
*/
|
|
22
|
+
export declare function hasMalformedStyle(style: string): boolean;
|
package/dist/style.js
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A tiny, deterministic inline-`style` parser/serializer. Order-preserving so codemod output is
|
|
3
|
+
* byte-stable: declarations keep source order, and appended declarations land at the end. Property
|
|
4
|
+
* names are lower-cased for matching; values are trimmed. This is not a full CSS parser — it only
|
|
5
|
+
* needs to read/edit the `display`, `width`, and `height` declarations the codemods touch.
|
|
6
|
+
*/
|
|
7
|
+
/** Parse an inline `style` value into ordered declarations, dropping empty/malformed segments. */
|
|
8
|
+
export function parseStyle(style) {
|
|
9
|
+
const declarations = [];
|
|
10
|
+
for (const segment of style.split(";")) {
|
|
11
|
+
const trimmed = segment.trim();
|
|
12
|
+
if (trimmed === "") {
|
|
13
|
+
continue;
|
|
14
|
+
}
|
|
15
|
+
const colon = trimmed.indexOf(":");
|
|
16
|
+
if (colon === -1) {
|
|
17
|
+
continue;
|
|
18
|
+
}
|
|
19
|
+
const property = trimmed.slice(0, colon).trim().toLowerCase();
|
|
20
|
+
const value = trimmed.slice(colon + 1).trim();
|
|
21
|
+
if (property === "" || value === "") {
|
|
22
|
+
continue;
|
|
23
|
+
}
|
|
24
|
+
declarations.push({ property, value });
|
|
25
|
+
}
|
|
26
|
+
return declarations;
|
|
27
|
+
}
|
|
28
|
+
/** Serialize declarations back into a canonical `prop:value;prop:value` string (no trailing space). */
|
|
29
|
+
export function serializeStyle(declarations) {
|
|
30
|
+
return declarations.map((d) => `${d.property}:${d.value}`).join(";");
|
|
31
|
+
}
|
|
32
|
+
/** Treat a unitless numeric dimension as pixels; pass through values that already carry a unit. */
|
|
33
|
+
export function toCssLength(value) {
|
|
34
|
+
const trimmed = value.trim();
|
|
35
|
+
return /^\d+(\.\d+)?$/.test(trimmed) ? `${trimmed}px` : trimmed;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* A `style` value is malformed when it contains a non-empty segment that has no `:` separator (a
|
|
39
|
+
* bare token like `display flex`) — a declaration that the browser/engine would silently drop. Empty
|
|
40
|
+
* segments (trailing `;`) are tolerated, not malformed.
|
|
41
|
+
*/
|
|
42
|
+
export function hasMalformedStyle(style) {
|
|
43
|
+
for (const segment of style.split(";")) {
|
|
44
|
+
const trimmed = segment.trim();
|
|
45
|
+
if (trimmed === "") {
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
const colon = trimmed.indexOf(":");
|
|
49
|
+
if (colon === -1) {
|
|
50
|
+
return true;
|
|
51
|
+
}
|
|
52
|
+
const property = trimmed.slice(0, colon).trim();
|
|
53
|
+
const value = trimmed.slice(colon + 1).trim();
|
|
54
|
+
if (property === "" || value === "") {
|
|
55
|
+
return true;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
//# sourceMappingURL=style.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"style.js","sourceRoot":"","sources":["../src/style.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAOH,kGAAkG;AAClG,MAAM,UAAU,UAAU,CAAC,KAAa;IACtC,MAAM,YAAY,GAAkB,EAAE,CAAC;IACvC,KAAK,MAAM,OAAO,IAAI,KAAK,CAAC,KAAK,CAAC,GAAG,CAAC,EAAE,CAAC;QACvC,MAAM,OAAO,GAAG,OAAO,CAAC,IAAI,EAAE,CAAC;QAC/B,IAAI,OAAO,KAAK,EAAE,EAAE,CAAC;YACnB,SAAS;QACX,CAAC;QACD,MAAM,KAAK,GAAG,OAAO,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;QACnC,IAAI,KAAK,KAAK,CAAC,CAAC,EAAE,CAAC;YACjB,SAAS;QACX,CAAC;QACD,MAAM,QAAQ,GAAG,OAAO,CAAC,KAAK,CAAC,CAAC,EAAE,KAAK,CAAC,CAAC,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC;QAC9D,MAAM,KAAK,GAAG,OAAO,CAAC,KAAK,CAAC,KAAK,GAAG,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;QAC9C,IAAI,QAAQ,KAAK,EAAE,IAAI,KAAK,KAAK,EAAE,EAAE,CAAC;YACpC,SAAS;QACX,CAAC;QACD,YAAY,CAAC,IAAI,CAAC,EAAE,QAAQ,EAAE,KAAK,EAAE,CAAC,CAAC;IACzC,CAAC;IACD,OAAO,YAAY,CAAC;AACtB,CAAC;AAED,uGAAuG;AACvG,MAAM,UAAU,cAAc,CAAC,YAA2B;IACxD,OAAO,YAAY,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,GAAG,CAAC,CAAC,QAAQ,IAAI,CAAC,CAAC,KAAK,EAAE,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;AACvE,CAAC;AAED,mGAAmG;AACnG,MAAM,UAAU,WAAW,CAAC,KAAa;IACvC,MAAM,OAAO,GAAG,KAAK,CAAC,IAAI,EAAE,CAAC;IAC7B,OAAO,eAAe,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,GAAG,OAAO,IAAI,CAAC,CAAC,CAAC,OAAO,CAAC;AAClE,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,iBAAiB,CAAC,KAAa;IAC7C,KAAK,MAAM,OAAO,IAAI,KAAK,CAAC,KAAK,CAAC,GAAG,CAAC,EAAE,CAAC;QACvC,MAAM,OAAO,GAAG,OAAO,CAAC,IAAI,EAAE,CAAC;QAC/B,IAAI,OAAO,KAAK,EAAE,EAAE,CAAC;YACnB,SAAS;QACX,CAAC;QACD,MAAM,KAAK,GAAG,OAAO,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;QACnC,IAAI,KAAK,KAAK,CAAC,CAAC,EAAE,CAAC;YACjB,OAAO,IAAI,CAAC;QACd,CAAC;QACD,MAAM,QAAQ,GAAG,OAAO,CAAC,KAAK,CAAC,CAAC,EAAE,KAAK,CAAC,CAAC,IAAI,EAAE,CAAC;QAChD,MAAM,KAAK,GAAG,OAAO,CAAC,KAAK,CAAC,KAAK,GAAG,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;QAC9C,IAAI,QAAQ,KAAK,EAAE,IAAI,KAAK,KAAK,EAAE,EAAE,CAAC;YACpC,OAAO,IAAI,CAAC;QACd,CAAC;IACH,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC"}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Stable public contract for `@vellora/lint`. These shapes are consumed programmatically by AI
|
|
3
|
+
* agents and by the public API's best-effort mode, so the field set is fixed and the `rule`
|
|
4
|
+
* ids are stable kebab-case. Do not reshape without a versioned change.
|
|
5
|
+
*/
|
|
6
|
+
/** Stable kebab-case rule ids. The first four are auto-fixable codemods; the rest are diagnostics. */
|
|
7
|
+
export type RuleId = "inline-svg" | "flex-grid-in-td" | "img-dimension-attrs" | "invalid-markup" | "script-element" | "css-animation";
|
|
8
|
+
export type Severity = "error" | "warning";
|
|
9
|
+
/** 1-based source position pointing at the offending node. */
|
|
10
|
+
export interface SourceLocation {
|
|
11
|
+
line: number;
|
|
12
|
+
col: number;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* A single detected subset violation. The field set is exactly this — humans read `suggestedFix`
|
|
16
|
+
* and `snippet`; agents key off `rule`, `severity`, `autoFixable`, and `compatLink`.
|
|
17
|
+
*/
|
|
18
|
+
export interface Finding {
|
|
19
|
+
/** Stable kebab-case rule id. */
|
|
20
|
+
rule: RuleId;
|
|
21
|
+
severity: Severity;
|
|
22
|
+
/** True iff one of the four deterministic codemods can repair this finding. */
|
|
23
|
+
autoFixable: boolean;
|
|
24
|
+
/** 1-based `{ line, col }` of the offending node. */
|
|
25
|
+
location: SourceLocation;
|
|
26
|
+
/** Human- and agent-readable description of the recommended change. */
|
|
27
|
+
suggestedFix: string;
|
|
28
|
+
/** The offending source fragment for the node. */
|
|
29
|
+
snippet: string;
|
|
30
|
+
/** Stable URL/anchor into the compatibility table for this rule. */
|
|
31
|
+
compatLink: string;
|
|
32
|
+
/** True on a `fix()` report when this finding was repaired by a codemod. */
|
|
33
|
+
applied?: boolean;
|
|
34
|
+
}
|
|
35
|
+
/** The structured report returned by `diagnose` and carried by `fix`. */
|
|
36
|
+
export interface Report {
|
|
37
|
+
/** True when no findings were detected (input is within the subset). */
|
|
38
|
+
conformant: boolean;
|
|
39
|
+
/** Findings ordered deterministically by `(line, col, rule)`. */
|
|
40
|
+
findings: Finding[];
|
|
41
|
+
}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Stable public contract for `@vellora/lint`. These shapes are consumed programmatically by AI
|
|
3
|
+
* agents and by the public API's best-effort mode, so the field set is fixed and the `rule`
|
|
4
|
+
* ids are stable kebab-case. Do not reshape without a versioned change.
|
|
5
|
+
*/
|
|
6
|
+
export {};
|
|
7
|
+
//# sourceMappingURL=types.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;;;GAIG"}
|
package/package.json
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@vellora/lint",
|
|
3
|
+
"version": "0.1.0-alpha.0",
|
|
4
|
+
"description": "Dev-time HTML diagnose + fix for the vellora HTML/CSS subset (parse5 + resvg).",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"html-to-pdf",
|
|
7
|
+
"pdf",
|
|
8
|
+
"pdf-generation",
|
|
9
|
+
"invoice",
|
|
10
|
+
"boleto",
|
|
11
|
+
"receipt",
|
|
12
|
+
"pdfa",
|
|
13
|
+
"pdfua",
|
|
14
|
+
"tagged-pdf",
|
|
15
|
+
"no-puppeteer",
|
|
16
|
+
"no-chromium",
|
|
17
|
+
"serverless",
|
|
18
|
+
"wkhtmltopdf-alternative",
|
|
19
|
+
"napi-rs",
|
|
20
|
+
"nodejs"
|
|
21
|
+
],
|
|
22
|
+
"homepage": "https://github.com/diomalta/vellora#readme",
|
|
23
|
+
"bugs": "https://github.com/diomalta/vellora/issues",
|
|
24
|
+
"repository": {
|
|
25
|
+
"type": "git",
|
|
26
|
+
"url": "git+https://github.com/diomalta/vellora.git",
|
|
27
|
+
"directory": "packages/lint"
|
|
28
|
+
},
|
|
29
|
+
"license": "MIT",
|
|
30
|
+
"author": "Diego Malta <diohmalta@gmail.com>",
|
|
31
|
+
"type": "module",
|
|
32
|
+
"main": "dist/index.js",
|
|
33
|
+
"types": "dist/index.d.ts",
|
|
34
|
+
"exports": {
|
|
35
|
+
".": {
|
|
36
|
+
"types": "./dist/index.d.ts",
|
|
37
|
+
"import": "./dist/index.js"
|
|
38
|
+
}
|
|
39
|
+
},
|
|
40
|
+
"sideEffects": false,
|
|
41
|
+
"engines": {
|
|
42
|
+
"node": ">=20"
|
|
43
|
+
},
|
|
44
|
+
"files": ["dist", "README.md", "LICENSE"],
|
|
45
|
+
"scripts": {
|
|
46
|
+
"build": "tsc"
|
|
47
|
+
},
|
|
48
|
+
"dependencies": {
|
|
49
|
+
"@resvg/resvg-js": "2.6.2",
|
|
50
|
+
"parse5": "8.0.1"
|
|
51
|
+
},
|
|
52
|
+
"devDependencies": {
|
|
53
|
+
"@vellora/test-harness": "*"
|
|
54
|
+
},
|
|
55
|
+
"publishConfig": {
|
|
56
|
+
"access": "public",
|
|
57
|
+
"provenance": true
|
|
58
|
+
}
|
|
59
|
+
}
|