zemdomu 1.3.17 → 1.3.19
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/README.md +66 -54
- package/out/linter.js +9 -0
- package/out/project-linter.js +57 -3
- package/out/rule-codes.d.ts +3 -0
- package/out/rule-codes.js +3 -0
- package/out/rules/ariaValidAttrValue.d.ts +2 -0
- package/out/rules/ariaValidAttrValue.js +230 -0
- package/out/rules/requireAltText.js +441 -18
- package/out/rules/requireDocumentTitle.d.ts +2 -0
- package/out/rules/requireDocumentTitle.js +141 -0
- package/out/rules/requireHtmlLang.js +120 -7
- package/out/rules/requireLinkText.js +289 -52
- package/out/rules/requireSingleMain.d.ts +2 -0
- package/out/rules/requireSingleMain.js +68 -0
- package/package.json +12 -4
- package/out/src/cli.js +0 -111
- package/out/src/component-analyzer.js +0 -1435
- package/out/src/component-path-resolver.js +0 -262
- package/out/src/html-visitor.js +0 -13
- package/out/src/index.js +0 -32
- package/out/src/linter.js +0 -288
- package/out/src/performance-diagnostics.js +0 -47
- package/out/src/project-linter.js +0 -168
- package/out/src/rule-codes.js +0 -36
- package/out/src/rules/enforceHeadingOrder.js +0 -83
- package/out/src/rules/enforceListNesting.js +0 -43
- package/out/src/rules/noTabindexGreaterThanZero.js +0 -33
- package/out/src/rules/preventEmptyInlineTags.js +0 -85
- package/out/src/rules/preventZemdomuPlaceholders.js +0 -236
- package/out/src/rules/requireAltText.js +0 -82
- package/out/src/rules/requireAltTextJSX.js +0 -63
- package/out/src/rules/requireButtonText.js +0 -78
- package/out/src/rules/requireHrefOnAnchors.js +0 -77
- package/out/src/rules/requireHtmlLang.js +0 -72
- package/out/src/rules/requireIframeTitle.js +0 -69
- package/out/src/rules/requireImageInputAlt.js +0 -69
- package/out/src/rules/requireLabelForFormControls.js +0 -84
- package/out/src/rules/requireLinkText.js +0 -126
- package/out/src/rules/requireNavLinks.js +0 -94
- package/out/src/rules/requireSectionHeading.js +0 -48
- package/out/src/rules/requireTableCaption.js +0 -48
- package/out/src/rules/singleH1.js +0 -112
- package/out/src/rules/uniqueIds.js +0 -33
- package/out/src/rules/utils.js +0 -231
- package/out/src/sarif.js +0 -58
- package/out/src/simpleHtmlParser.js +0 -82
- package/out/src/utils/collectLocalDeps.js +0 -168
- package/out/src/utils/vue-sfc.js +0 -93
- package/out/tests/button-accessibility-jsx.test.js +0 -53
- package/out/tests/cli-custom-rule.test.js +0 -61
- package/out/tests/cli-performance.test.js +0 -35
- package/out/tests/cross-duplicate-ids-tsx.test.js +0 -36
- package/out/tests/cross-heading-reversed.test.js +0 -24
- package/out/tests/cross-list-nesting.test.js +0 -34
- package/out/tests/cross-section-heading.test.js +0 -32
- package/out/tests/crossComponent/Button.js +0 -7
- package/out/tests/crossComponent/Page.js +0 -11
- package/out/tests/crossComponent/Section.js +0 -11
- package/out/tests/crossComponent/SubSection.js +0 -11
- package/out/tests/crossComponent/cross-button-text.test.js +0 -31
- package/out/tests/crossComponent/cross-heading-order-alias.test.js +0 -31
- package/out/tests/crossComponent/cross-heading-order-entry-only.test.js +0 -31
- package/out/tests/crossComponent/cross-heading-order.test.js +0 -60
- package/out/tests/custom-rule-tsx.test.js +0 -24
- package/out/tests/edge-cases.test.js +0 -17
- package/out/tests/exports-helpers.test.js +0 -45
- package/out/tests/heading-order.test.js +0 -24
- package/out/tests/html-visitor.test.js +0 -29
- package/out/tests/img-alt-dynamic.test.js +0 -39
- package/out/tests/label-form-control-jsx.test.js +0 -29
- package/out/tests/link-href-text.test.js +0 -52
- package/out/tests/linter.test.js +0 -41
- package/out/tests/list-nesting-dynamic.test.js +0 -27
- package/out/tests/nav-links-components.test.js +0 -27
- package/out/tests/parse-error-multifile.test.js +0 -29
- package/out/tests/parse-error.test.js +0 -11
- package/out/tests/performance-diagnostics.test.js +0 -12
- package/out/tests/prevent-placeholders-jsx.test.js +0 -34
- package/out/tests/sarif-output.test.js +0 -15
- package/out/tests/section-heading-jsx.test.js +0 -32
- package/out/tests/single-h1-returns.test.js +0 -74
- package/out/tests/tsx-parse-error.test.js +0 -11
- package/out/tests/unique-ids-html.test.js +0 -19
- package/out/tests/unique-ids-tsx.test.js +0 -19
- package/out/tests/vue-support.test.js +0 -122
package/README.md
CHANGED
|
@@ -1,15 +1,35 @@
|
|
|
1
1
|
# ZemDomu Core
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
This package provides the shared core logic used by the ZemDomu VS Code
|
|
5
|
-
extension and the GitHub Action.
|
|
3
|
+
> The semantic rules engine behind ZemDomu.
|
|
6
4
|
|
|
7
|
-
|
|
5
|
+
ZemDomu Core is the shared engine that powers the ZemDomu ecosystem. It parses
|
|
6
|
+
HTML, JSX, TSX, and Vue templates and returns semantic issues that affect
|
|
7
|
+
structure, accessibility, and search visibility.
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
9
|
+
Most linters check syntax. ZemDomu checks meaning.
|
|
10
|
+
|
|
11
|
+
## What It Is
|
|
12
|
+
|
|
13
|
+
ZemDomu Core is a semantic-first linting engine for modern frontend codebases.
|
|
14
|
+
It helps developers catch issues like missing landmarks, confusing heading
|
|
15
|
+
structure, unlabeled controls, weak semantic relationships, and cross-component
|
|
16
|
+
composition problems before those issues become late-stage audit findings.
|
|
17
|
+
|
|
18
|
+
This package provides the shared logic used by:
|
|
19
|
+
|
|
20
|
+
- the ZemDomu CLI
|
|
21
|
+
- the ZemDomu VS Code Extension
|
|
22
|
+
- the ZemDomu GitHub Action
|
|
23
|
+
|
|
24
|
+
## Why ZemDomu
|
|
25
|
+
|
|
26
|
+
Compared with generic linters and scanner-only workflows, ZemDomu is designed
|
|
27
|
+
to keep semantic analysis practical in real component codebases.
|
|
28
|
+
|
|
29
|
+
- Cross-component analysis catches issues that only appear when components are composed.
|
|
30
|
+
- One shared rules engine powers editor, CLI, and CI behavior consistently.
|
|
31
|
+
- Diagnostics focus on semantic HTML, accessible naming, and document structure.
|
|
32
|
+
- Custom-rule support lets teams extend checks without rebuilding a lint stack.
|
|
13
33
|
|
|
14
34
|
## Features
|
|
15
35
|
|
|
@@ -20,7 +40,6 @@ semantic violations.
|
|
|
20
40
|
- Command line interface with `--custom` and `--cross`.
|
|
21
41
|
- Configurable rule severity (`error`, `warning`, `off`).
|
|
22
42
|
- Performance diagnostics for profiling lint runs.
|
|
23
|
-
- Shared by the extension and GitHub Action.
|
|
24
43
|
- Simple API: `lint(content, options)`.
|
|
25
44
|
|
|
26
45
|
## Installation
|
|
@@ -44,14 +63,10 @@ console.log(results);
|
|
|
44
63
|
// {
|
|
45
64
|
// line: 0,
|
|
46
65
|
// column: 0,
|
|
47
|
-
// message:
|
|
48
|
-
// rule:
|
|
66
|
+
// message: "<img> tag missing alt attribute",
|
|
67
|
+
// rule: "requireAltText"
|
|
49
68
|
// }
|
|
50
69
|
// ]
|
|
51
|
-
|
|
52
|
-
// Custom rules can be supplied via the `customRules` option
|
|
53
|
-
// const myRule = { name: 'demo', test: node => false, message: 'demo' };
|
|
54
|
-
// lint(html, { customRules: [myRule] });
|
|
55
70
|
```
|
|
56
71
|
|
|
57
72
|
## API
|
|
@@ -64,6 +79,7 @@ console.log(results);
|
|
|
64
79
|
- `options.rules`: severity settings for built-in rules.
|
|
65
80
|
- `options.customRules`: array of additional rules.
|
|
66
81
|
- `options.filePath`: optional source file path.
|
|
82
|
+
- `options.forceHtml`: treat input as HTML.
|
|
67
83
|
- `options.perf`: attach a `PerformanceRecorder` instance.
|
|
68
84
|
|
|
69
85
|
### Example `LinterOptions`
|
|
@@ -78,7 +94,7 @@ interface LinterOptions {
|
|
|
78
94
|
}
|
|
79
95
|
```
|
|
80
96
|
|
|
81
|
-
Example enabling
|
|
97
|
+
Example enabling rule severities:
|
|
82
98
|
|
|
83
99
|
```ts
|
|
84
100
|
const results = lint(html, {
|
|
@@ -97,11 +113,11 @@ interface LintResult {
|
|
|
97
113
|
}
|
|
98
114
|
```
|
|
99
115
|
|
|
100
|
-
## CLI
|
|
116
|
+
## CLI Usage
|
|
101
117
|
|
|
102
|
-
Run the linter from the command line by installing the package globally or
|
|
118
|
+
Run the linter from the command line by installing the package globally or by
|
|
103
119
|
using `npx`. Provide one or more glob patterns to specify the files to lint.
|
|
104
|
-
Patterns may be separated by spaces, commas, or newlines
|
|
120
|
+
Patterns may be separated by spaces, commas, or newlines.
|
|
105
121
|
|
|
106
122
|
```bash
|
|
107
123
|
npx zemdomu "src/**/*.{html,jsx,tsx,vue}" --custom my-rule.js
|
|
@@ -110,27 +126,30 @@ npx zemdomu "src/**/*.html,src/**/*.jsx"
|
|
|
110
126
|
|
|
111
127
|
Use `--custom` (or `-c`) to provide a path to a JavaScript or TypeScript module
|
|
112
128
|
exporting a custom rule or array of rules. For safety, the CLI only accepts
|
|
113
|
-
files inside a `./custom-rules` directory
|
|
114
|
-
directory
|
|
115
|
-
|
|
129
|
+
files inside a `./custom-rules` directory relative to your current working
|
|
130
|
+
directory. You can repeat `--custom` to load multiple rule files.
|
|
131
|
+
|
|
132
|
+
Use `--cross` to enable cross-component analysis.
|
|
116
133
|
|
|
117
134
|
Use `--perf` to emit a JSON timing report to stdout, and `--perf-slowest` to
|
|
118
135
|
also print the slowest file and phase.
|
|
119
136
|
|
|
120
|
-
|
|
137
|
+
## Cross-Component Analysis
|
|
121
138
|
|
|
122
|
-
When analyzing JSX or Vue projects you can track
|
|
123
|
-
|
|
139
|
+
When analyzing JSX or Vue projects you can track semantic issues across
|
|
140
|
+
component boundaries. Instantiate `ProjectLinter` with the
|
|
124
141
|
`crossComponentAnalysis` option or pass `--cross` to the CLI. Use
|
|
125
|
-
`crossComponentDepth`
|
|
126
|
-
traversed during analysis
|
|
142
|
+
`crossComponentDepth` or `--cross-depth` to limit how deep component trees are
|
|
143
|
+
traversed during analysis.
|
|
127
144
|
|
|
128
145
|
```ts
|
|
129
146
|
import { ProjectLinter } from "zemdomu";
|
|
147
|
+
|
|
130
148
|
const linter = new ProjectLinter({
|
|
131
149
|
crossComponentAnalysis: true,
|
|
132
|
-
crossComponentDepth: 2
|
|
150
|
+
crossComponentDepth: 2,
|
|
133
151
|
});
|
|
152
|
+
|
|
134
153
|
await linter.lintFile("App.jsx");
|
|
135
154
|
```
|
|
136
155
|
|
|
@@ -138,23 +157,24 @@ await linter.lintFile("App.jsx");
|
|
|
138
157
|
npx zemdomu "src/**/*.{jsx,tsx,vue}" --cross --cross-depth 2
|
|
139
158
|
```
|
|
140
159
|
|
|
141
|
-
|
|
160
|
+
## Performance Diagnostics
|
|
142
161
|
|
|
143
|
-
Attach a `PerformanceDiagnostics` recorder to gather timing information for
|
|
144
|
-
|
|
162
|
+
Attach a `PerformanceDiagnostics` recorder to gather timing information for each
|
|
163
|
+
file and rule.
|
|
145
164
|
|
|
146
165
|
```ts
|
|
147
166
|
import { lint, PerformanceDiagnostics } from "zemdomu";
|
|
167
|
+
|
|
148
168
|
const perf = new PerformanceDiagnostics();
|
|
149
169
|
lint(code, { perf });
|
|
150
170
|
console.log(perf.getAsJSON());
|
|
151
171
|
```
|
|
152
172
|
|
|
153
|
-
## Writing
|
|
173
|
+
## Writing Custom Rules
|
|
154
174
|
|
|
155
|
-
Custom rules are simple objects implementing the `Rule` interface. At minimum
|
|
156
|
-
provide a `name`, a `test` function that returns `true` when a node violates
|
|
157
|
-
rule, and a `message` describing the problem
|
|
175
|
+
Custom rules are simple objects implementing the `Rule` interface. At minimum,
|
|
176
|
+
provide a `name`, a `test` function that returns `true` when a node violates
|
|
177
|
+
the rule, and a `message` describing the problem.
|
|
158
178
|
|
|
159
179
|
```ts
|
|
160
180
|
interface Rule {
|
|
@@ -177,13 +197,14 @@ Use it programmatically:
|
|
|
177
197
|
|
|
178
198
|
```ts
|
|
179
199
|
import { lint } from "zemdomu";
|
|
200
|
+
|
|
180
201
|
const results = lint("<foo></foo>", { customRules: [require("./my-rule")] });
|
|
181
202
|
```
|
|
182
203
|
|
|
183
|
-
### Helper
|
|
204
|
+
### Helper Utilities
|
|
184
205
|
|
|
185
206
|
For more advanced rules you may need direct access to the parsed HTML or JSX
|
|
186
|
-
AST. ZemDomu exposes
|
|
207
|
+
AST. ZemDomu exposes helpers for traversal and attribute inspection:
|
|
187
208
|
|
|
188
209
|
```ts
|
|
189
210
|
import {
|
|
@@ -201,13 +222,6 @@ import {
|
|
|
201
222
|
} from "zemdomu";
|
|
202
223
|
```
|
|
203
224
|
|
|
204
|
-
`parseHtml` returns the root `ElementNode`. The `visitHtml` function performs a
|
|
205
|
-
simple depth-first traversal using an `HtmlVisitor` with optional `enter` and
|
|
206
|
-
`exit` callbacks. Utility functions like `getAttr` and `getJsxAttr` help reading
|
|
207
|
-
attributes. JSX helpers like `getJsxAttribute`, `getJsxAttributeState`, and
|
|
208
|
-
`getJsxExpressionState` help interpret JSX attributes and expressions, while
|
|
209
|
-
`getTag` resolves JSX element names.
|
|
210
|
-
|
|
211
225
|
Or via the CLI:
|
|
212
226
|
|
|
213
227
|
```bash
|
|
@@ -217,9 +231,10 @@ npx zemdomu file.html --custom custom-rules/my-rule.js
|
|
|
217
231
|
npx zemdomu "src/**/*.{html,jsx,tsx,vue}" --perf --perf-slowest
|
|
218
232
|
```
|
|
219
233
|
|
|
220
|
-
There is a sample rule in `custom-rules/example-rule.js` you can copy and
|
|
234
|
+
There is a sample rule in `custom-rules/example-rule.js` that you can copy and
|
|
235
|
+
edit.
|
|
221
236
|
|
|
222
|
-
## Local
|
|
237
|
+
## Local Development
|
|
223
238
|
|
|
224
239
|
From the core package:
|
|
225
240
|
|
|
@@ -231,15 +246,12 @@ npm run build
|
|
|
231
246
|
|
|
232
247
|
## Links
|
|
233
248
|
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
- NPM package: https://www.npmjs.com/package/zemdomu
|
|
238
|
-
- Public mirror: https://github.com/Zemdomu/ZemDomu-core
|
|
239
|
-
- Issues and suggestions: https://github.com/Zemdomu/ZemDomu-core/issues
|
|
249
|
+
- npm package: https://www.npmjs.com/package/zemdomu
|
|
250
|
+
- Website: https://zemdomu.dev/
|
|
251
|
+
- Issues and suggestions: https://github.com/ZemDomu/ZemDomu-core/issues
|
|
240
252
|
- VS Code extension: https://marketplace.visualstudio.com/items?itemName=ZachariasErydBerlin.zemdomu
|
|
241
|
-
- GitHub Action: https://github.com/
|
|
253
|
+
- GitHub Action: https://github.com/ZemDomu/ZemDomu-action
|
|
242
254
|
|
|
243
255
|
## License
|
|
244
256
|
|
|
245
|
-
MIT (c) 2025 Zacharias Eryd Berlin
|
|
257
|
+
MIT (c) 2025 Zacharias Eryd Berlin
|
package/out/linter.js
CHANGED
|
@@ -25,6 +25,9 @@ const requireNavLinks_1 = __importDefault(require("./rules/requireNavLinks"));
|
|
|
25
25
|
const uniqueIds_1 = __importDefault(require("./rules/uniqueIds"));
|
|
26
26
|
const noTabindexGreaterThanZero_1 = __importDefault(require("./rules/noTabindexGreaterThanZero"));
|
|
27
27
|
const preventZemdomuPlaceholders_1 = __importDefault(require("./rules/preventZemdomuPlaceholders"));
|
|
28
|
+
const requireDocumentTitle_1 = __importDefault(require("./rules/requireDocumentTitle"));
|
|
29
|
+
const requireSingleMain_1 = __importDefault(require("./rules/requireSingleMain"));
|
|
30
|
+
const ariaValidAttrValue_1 = __importDefault(require("./rules/ariaValidAttrValue"));
|
|
28
31
|
const rule_codes_1 = require("./rule-codes");
|
|
29
32
|
const builtInRules = {
|
|
30
33
|
requireSectionHeading: requireSectionHeading_1.default,
|
|
@@ -45,6 +48,9 @@ const builtInRules = {
|
|
|
45
48
|
uniqueIds: uniqueIds_1.default,
|
|
46
49
|
noTabindexGreaterThanZero: noTabindexGreaterThanZero_1.default,
|
|
47
50
|
preventZemdomuPlaceholders: preventZemdomuPlaceholders_1.default,
|
|
51
|
+
requireDocumentTitle: requireDocumentTitle_1.default,
|
|
52
|
+
requireSingleMain: requireSingleMain_1.default,
|
|
53
|
+
ariaValidAttrValue: ariaValidAttrValue_1.default,
|
|
48
54
|
};
|
|
49
55
|
const defaultOptions = {
|
|
50
56
|
rules: {
|
|
@@ -66,6 +72,9 @@ const defaultOptions = {
|
|
|
66
72
|
uniqueIds: "error",
|
|
67
73
|
noTabindexGreaterThanZero: "warning",
|
|
68
74
|
preventZemdomuPlaceholders: "warning",
|
|
75
|
+
requireDocumentTitle: "error",
|
|
76
|
+
requireSingleMain: "error",
|
|
77
|
+
ariaValidAttrValue: "error",
|
|
69
78
|
},
|
|
70
79
|
customRules: [],
|
|
71
80
|
};
|
package/out/project-linter.js
CHANGED
|
@@ -46,6 +46,55 @@ const component_path_resolver_1 = require("./component-path-resolver");
|
|
|
46
46
|
const collectLocalDeps_1 = require("./utils/collectLocalDeps");
|
|
47
47
|
const vue_sfc_1 = require("./utils/vue-sfc");
|
|
48
48
|
const rule_codes_1 = require("./rule-codes");
|
|
49
|
+
const FRAMEWORK_HOST_DOCUMENT_RULES = [
|
|
50
|
+
"requireHtmlLang",
|
|
51
|
+
"requireDocumentTitle",
|
|
52
|
+
"requireSingleMain",
|
|
53
|
+
];
|
|
54
|
+
function isHtmlFile(filePath) {
|
|
55
|
+
return /\.(html|htm)$/i.test(filePath);
|
|
56
|
+
}
|
|
57
|
+
function getHtmlTagAttribute(tag, name) {
|
|
58
|
+
var _a, _b, _c;
|
|
59
|
+
const match = new RegExp(`\\b${name}\\s*=\\s*(?:"([^"]*)"|'([^']*)'|([^\\s>]+))`, "i").exec(tag);
|
|
60
|
+
return match ? (_c = (_b = (_a = match[1]) !== null && _a !== void 0 ? _a : match[2]) !== null && _b !== void 0 ? _b : match[3]) !== null && _c !== void 0 ? _c : "" : null;
|
|
61
|
+
}
|
|
62
|
+
function hasFrameworkMountPoint(content) {
|
|
63
|
+
return /<[a-z][\w:-]*\b(?=[^>]*\bid\s*=\s*["'](?:app|root|app-root|mount|__nuxt|__next|svelte)["'])[^>]*>/i.test(content);
|
|
64
|
+
}
|
|
65
|
+
function isFrameworkEntrySrc(src) {
|
|
66
|
+
const normalized = src.replace(/[?#].*$/, "").replace(/\\/g, "/");
|
|
67
|
+
return /(?:^|\/)(?:src\/)?(?:main|index|app)\.(?:[cm]?[jt]sx?|vue)$/i.test(normalized);
|
|
68
|
+
}
|
|
69
|
+
function hasFrameworkModuleEntry(content) {
|
|
70
|
+
const scriptRe = /<script\b[^>]*>/gi;
|
|
71
|
+
let match;
|
|
72
|
+
while ((match = scriptRe.exec(content))) {
|
|
73
|
+
const tag = match[0];
|
|
74
|
+
const type = getHtmlTagAttribute(tag, "type");
|
|
75
|
+
if (!type || type.toLowerCase() !== "module")
|
|
76
|
+
continue;
|
|
77
|
+
const src = getHtmlTagAttribute(tag, "src");
|
|
78
|
+
if (src && isFrameworkEntrySrc(src))
|
|
79
|
+
return true;
|
|
80
|
+
}
|
|
81
|
+
return /<script\b(?=[^>]*\btype\s*=\s*["']?module["']?)[^>]*>[\s\S]*?\b(?:createApp|createRoot|ReactDOM\.render|new\s+Vue)\s*\(/i.test(content);
|
|
82
|
+
}
|
|
83
|
+
function isFrameworkHostHtml(filePath, content) {
|
|
84
|
+
if (path_1.default.basename(filePath).toLowerCase() !== "index.html")
|
|
85
|
+
return false;
|
|
86
|
+
if (!hasFrameworkMountPoint(content))
|
|
87
|
+
return false;
|
|
88
|
+
return hasFrameworkModuleEntry(content);
|
|
89
|
+
}
|
|
90
|
+
function suppressRules(options, ruleNames) {
|
|
91
|
+
var _a;
|
|
92
|
+
const rules = { ...((_a = options.rules) !== null && _a !== void 0 ? _a : {}) };
|
|
93
|
+
for (const ruleName of ruleNames) {
|
|
94
|
+
rules[ruleName] = "off";
|
|
95
|
+
}
|
|
96
|
+
return { ...options, rules };
|
|
97
|
+
}
|
|
49
98
|
class ProjectLinter {
|
|
50
99
|
constructor(options = {}) {
|
|
51
100
|
var _a;
|
|
@@ -61,16 +110,21 @@ class ProjectLinter {
|
|
|
61
110
|
if (!content) {
|
|
62
111
|
content = await fs.readFile(filePath, "utf8");
|
|
63
112
|
}
|
|
64
|
-
const
|
|
113
|
+
const ext = path_1.default.extname(filePath).toLowerCase();
|
|
114
|
+
const isVue = ext === ".vue";
|
|
115
|
+
const isHtml = isHtmlFile(filePath);
|
|
65
116
|
let lintContent = content;
|
|
66
117
|
if (isVue) {
|
|
67
118
|
const template = (0, vue_sfc_1.extractVueTemplate)(content);
|
|
68
119
|
lintContent = (0, vue_sfc_1.isHtmlVueTemplate)(template) ? template.content : "";
|
|
69
120
|
}
|
|
121
|
+
const lintOptions = isHtml && isFrameworkHostHtml(filePath, content)
|
|
122
|
+
? suppressRules(this.opts, FRAMEWORK_HOST_DOCUMENT_RULES)
|
|
123
|
+
: this.opts;
|
|
70
124
|
const results = (0, linter_1.lint)(lintContent, {
|
|
71
|
-
...
|
|
125
|
+
...lintOptions,
|
|
72
126
|
filePath,
|
|
73
|
-
forceHtml: isVue || this.opts.forceHtml,
|
|
127
|
+
forceHtml: isHtml || isVue || this.opts.forceHtml,
|
|
74
128
|
});
|
|
75
129
|
const resolvedResults = results.map((result) => result.filePath ? result : { ...result, filePath });
|
|
76
130
|
const byFile = new Map();
|
package/out/rule-codes.d.ts
CHANGED
|
@@ -17,6 +17,9 @@ declare const RULE_CODES: {
|
|
|
17
17
|
readonly uniqueIds: "ZMD016";
|
|
18
18
|
readonly noTabindexGreaterThanZero: "ZMD017";
|
|
19
19
|
readonly preventZemdomuPlaceholders: "ZMD018";
|
|
20
|
+
readonly requireDocumentTitle: "ZMD019";
|
|
21
|
+
readonly requireSingleMain: "ZMD020";
|
|
22
|
+
readonly ariaValidAttrValue: "ZMD021";
|
|
20
23
|
};
|
|
21
24
|
export declare function getRuleCode(rule: string): string | undefined;
|
|
22
25
|
export declare function applyRuleCode<T extends {
|
package/out/rule-codes.js
CHANGED
|
@@ -22,6 +22,9 @@ const RULE_CODES = {
|
|
|
22
22
|
uniqueIds: "ZMD016",
|
|
23
23
|
noTabindexGreaterThanZero: "ZMD017",
|
|
24
24
|
preventZemdomuPlaceholders: "ZMD018",
|
|
25
|
+
requireDocumentTitle: "ZMD019",
|
|
26
|
+
requireSingleMain: "ZMD020",
|
|
27
|
+
ariaValidAttrValue: "ZMD021",
|
|
25
28
|
};
|
|
26
29
|
exports.RULE_CODES = RULE_CODES;
|
|
27
30
|
function getRuleCode(rule) {
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.default = ariaValidAttrValue;
|
|
37
|
+
const t = __importStar(require("@babel/types"));
|
|
38
|
+
const BOOLEAN_ATTRS = new Set([
|
|
39
|
+
"aria-hidden",
|
|
40
|
+
"aria-expanded",
|
|
41
|
+
"aria-selected",
|
|
42
|
+
"aria-disabled",
|
|
43
|
+
"aria-required",
|
|
44
|
+
"aria-modal",
|
|
45
|
+
"aria-multiline",
|
|
46
|
+
"aria-multiselectable",
|
|
47
|
+
"aria-readonly",
|
|
48
|
+
"aria-busy",
|
|
49
|
+
"aria-atomic",
|
|
50
|
+
]);
|
|
51
|
+
const TRISTATE_ATTRS = new Set(["aria-checked", "aria-pressed"]);
|
|
52
|
+
const NUMERIC_ATTRS = new Set([
|
|
53
|
+
"aria-level",
|
|
54
|
+
"aria-valuemin",
|
|
55
|
+
"aria-valuemax",
|
|
56
|
+
"aria-valuenow",
|
|
57
|
+
"aria-colindex",
|
|
58
|
+
"aria-rowindex",
|
|
59
|
+
"aria-colcount",
|
|
60
|
+
"aria-rowcount",
|
|
61
|
+
"aria-setsize",
|
|
62
|
+
"aria-posinset",
|
|
63
|
+
]);
|
|
64
|
+
const IDREF_LIST_ATTRS = new Set([
|
|
65
|
+
"aria-labelledby",
|
|
66
|
+
"aria-describedby",
|
|
67
|
+
"aria-controls",
|
|
68
|
+
"aria-owns",
|
|
69
|
+
"aria-details",
|
|
70
|
+
"aria-errormessage",
|
|
71
|
+
"aria-flowto",
|
|
72
|
+
]);
|
|
73
|
+
const TOKEN_ATTRS = {
|
|
74
|
+
"aria-current": new Set([
|
|
75
|
+
"page",
|
|
76
|
+
"step",
|
|
77
|
+
"location",
|
|
78
|
+
"date",
|
|
79
|
+
"time",
|
|
80
|
+
"true",
|
|
81
|
+
"false",
|
|
82
|
+
]),
|
|
83
|
+
"aria-live": new Set(["off", "polite", "assertive"]),
|
|
84
|
+
"aria-sort": new Set(["none", "ascending", "descending", "other"]),
|
|
85
|
+
"aria-orientation": new Set(["horizontal", "vertical"]),
|
|
86
|
+
"aria-haspopup": new Set([
|
|
87
|
+
"false",
|
|
88
|
+
"true",
|
|
89
|
+
"menu",
|
|
90
|
+
"listbox",
|
|
91
|
+
"tree",
|
|
92
|
+
"grid",
|
|
93
|
+
"dialog",
|
|
94
|
+
]),
|
|
95
|
+
"aria-autocomplete": new Set(["inline", "list", "both", "none"]),
|
|
96
|
+
"aria-invalid": new Set(["false", "true", "grammar", "spelling"]),
|
|
97
|
+
};
|
|
98
|
+
const MULTI_TOKEN_ATTRS = {
|
|
99
|
+
"aria-relevant": new Set(["additions", "removals", "text", "all"]),
|
|
100
|
+
};
|
|
101
|
+
function normalize(value) {
|
|
102
|
+
return value.trim().toLowerCase();
|
|
103
|
+
}
|
|
104
|
+
function isNumeric(value) {
|
|
105
|
+
if (!value.trim())
|
|
106
|
+
return false;
|
|
107
|
+
return Number.isFinite(Number(value));
|
|
108
|
+
}
|
|
109
|
+
function isNonEmptyIdRefList(value) {
|
|
110
|
+
const tokens = value
|
|
111
|
+
.split(/\s+/)
|
|
112
|
+
.map((token) => token.trim())
|
|
113
|
+
.filter(Boolean);
|
|
114
|
+
return tokens.length > 0;
|
|
115
|
+
}
|
|
116
|
+
function isSupportedAriaAttr(attr) {
|
|
117
|
+
return (BOOLEAN_ATTRS.has(attr) ||
|
|
118
|
+
TRISTATE_ATTRS.has(attr) ||
|
|
119
|
+
NUMERIC_ATTRS.has(attr) ||
|
|
120
|
+
IDREF_LIST_ATTRS.has(attr) ||
|
|
121
|
+
Object.prototype.hasOwnProperty.call(TOKEN_ATTRS, attr) ||
|
|
122
|
+
Object.prototype.hasOwnProperty.call(MULTI_TOKEN_ATTRS, attr));
|
|
123
|
+
}
|
|
124
|
+
function isValidAriaValue(attr, rawValue) {
|
|
125
|
+
const value = normalize(rawValue);
|
|
126
|
+
if (!isSupportedAriaAttr(attr))
|
|
127
|
+
return true;
|
|
128
|
+
if (!value)
|
|
129
|
+
return false;
|
|
130
|
+
if (BOOLEAN_ATTRS.has(attr)) {
|
|
131
|
+
return value === "true" || value === "false";
|
|
132
|
+
}
|
|
133
|
+
if (TRISTATE_ATTRS.has(attr)) {
|
|
134
|
+
return (value === "true" ||
|
|
135
|
+
value === "false" ||
|
|
136
|
+
value === "mixed" ||
|
|
137
|
+
value === "undefined");
|
|
138
|
+
}
|
|
139
|
+
if (NUMERIC_ATTRS.has(attr)) {
|
|
140
|
+
return isNumeric(value);
|
|
141
|
+
}
|
|
142
|
+
if (IDREF_LIST_ATTRS.has(attr)) {
|
|
143
|
+
return isNonEmptyIdRefList(rawValue);
|
|
144
|
+
}
|
|
145
|
+
const tokenSet = TOKEN_ATTRS[attr];
|
|
146
|
+
if (tokenSet) {
|
|
147
|
+
return tokenSet.has(value);
|
|
148
|
+
}
|
|
149
|
+
const multiTokenSet = MULTI_TOKEN_ATTRS[attr];
|
|
150
|
+
if (multiTokenSet) {
|
|
151
|
+
const tokens = value.split(/\s+/).filter(Boolean);
|
|
152
|
+
return tokens.length > 0 && tokens.every((token) => multiTokenSet.has(token));
|
|
153
|
+
}
|
|
154
|
+
return true;
|
|
155
|
+
}
|
|
156
|
+
function jsxStaticAriaValue(attr) {
|
|
157
|
+
if (!attr.value)
|
|
158
|
+
return { value: "", dynamic: false };
|
|
159
|
+
if (t.isStringLiteral(attr.value))
|
|
160
|
+
return { value: attr.value.value, dynamic: false };
|
|
161
|
+
if (!t.isJSXExpressionContainer(attr.value))
|
|
162
|
+
return { value: "", dynamic: true };
|
|
163
|
+
const expr = attr.value.expression;
|
|
164
|
+
if (t.isStringLiteral(expr))
|
|
165
|
+
return { value: expr.value, dynamic: false };
|
|
166
|
+
if (t.isBooleanLiteral(expr))
|
|
167
|
+
return { value: String(expr.value), dynamic: false };
|
|
168
|
+
if (t.isNumericLiteral(expr))
|
|
169
|
+
return { value: String(expr.value), dynamic: false };
|
|
170
|
+
if (t.isTemplateLiteral(expr) && expr.expressions.length === 0) {
|
|
171
|
+
const staticValue = expr.quasis.map((q) => { var _a; return (_a = q.value.cooked) !== null && _a !== void 0 ? _a : q.value.raw; }).join("");
|
|
172
|
+
return { value: staticValue, dynamic: false };
|
|
173
|
+
}
|
|
174
|
+
return { value: "", dynamic: true };
|
|
175
|
+
}
|
|
176
|
+
function invalidValueResult(attr, rawValue, line, column) {
|
|
177
|
+
return {
|
|
178
|
+
line,
|
|
179
|
+
column,
|
|
180
|
+
message: `ARIA attribute "${attr}" has invalid value "${rawValue}"`,
|
|
181
|
+
rule: "ariaValidAttrValue",
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
function ariaValidAttrValue() {
|
|
185
|
+
return {
|
|
186
|
+
name: "ariaValidAttrValue",
|
|
187
|
+
enterHtml(node) {
|
|
188
|
+
if (node.type !== "element")
|
|
189
|
+
return [];
|
|
190
|
+
const results = [];
|
|
191
|
+
for (const [rawName, rawValue] of Object.entries(node.attrs)) {
|
|
192
|
+
const name = rawName.toLowerCase();
|
|
193
|
+
if (name.startsWith(":aria-") || name.startsWith("v-bind:aria-"))
|
|
194
|
+
continue;
|
|
195
|
+
if (!name.startsWith("aria-"))
|
|
196
|
+
continue;
|
|
197
|
+
if (!isSupportedAriaAttr(name))
|
|
198
|
+
continue;
|
|
199
|
+
const value = String(rawValue !== null && rawValue !== void 0 ? rawValue : "");
|
|
200
|
+
if (!isValidAriaValue(name, value)) {
|
|
201
|
+
results.push(invalidValueResult(name, value, 0, 0));
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
return results;
|
|
205
|
+
},
|
|
206
|
+
enterJsx(path) {
|
|
207
|
+
var _a, _b, _c, _d, _e, _f, _g, _h;
|
|
208
|
+
const results = [];
|
|
209
|
+
const attrs = path.node.openingElement.attributes;
|
|
210
|
+
for (const attr of attrs) {
|
|
211
|
+
if (!t.isJSXAttribute(attr) || !t.isJSXIdentifier(attr.name))
|
|
212
|
+
continue;
|
|
213
|
+
const name = attr.name.name.toLowerCase();
|
|
214
|
+
if (!name.startsWith("aria-"))
|
|
215
|
+
continue;
|
|
216
|
+
if (!isSupportedAriaAttr(name))
|
|
217
|
+
continue;
|
|
218
|
+
const { value, dynamic } = jsxStaticAriaValue(attr);
|
|
219
|
+
if (dynamic)
|
|
220
|
+
continue;
|
|
221
|
+
if (!isValidAriaValue(name, value)) {
|
|
222
|
+
const line = ((_d = (_b = (_a = attr.loc) === null || _a === void 0 ? void 0 : _a.start.line) !== null && _b !== void 0 ? _b : (_c = path.node.loc) === null || _c === void 0 ? void 0 : _c.start.line) !== null && _d !== void 0 ? _d : 1) - 1;
|
|
223
|
+
const column = (_h = (_f = (_e = attr.loc) === null || _e === void 0 ? void 0 : _e.start.column) !== null && _f !== void 0 ? _f : (_g = path.node.loc) === null || _g === void 0 ? void 0 : _g.start.column) !== null && _h !== void 0 ? _h : 0;
|
|
224
|
+
results.push(invalidValueResult(name, value, line, column));
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
return results;
|
|
228
|
+
},
|
|
229
|
+
};
|
|
230
|
+
}
|