zemdomu 1.1.5 → 1.3.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.
Files changed (54) hide show
  1. package/README.md +128 -12
  2. package/out/linter.js +0 -164
  3. package/out/src/cli.js +88 -0
  4. package/out/{component-analyzer.js → src/component-analyzer.js} +620 -461
  5. package/out/src/html-visitor.js +13 -0
  6. package/out/src/index.js +25 -0
  7. package/out/src/linter.js +272 -0
  8. package/out/src/performance-diagnostics.js +47 -0
  9. package/out/{project-linter.js → src/project-linter.js} +114 -87
  10. package/out/{rules → src/rules}/enforceListNesting.js +81 -81
  11. package/out/{rules → src/rules}/noTabindexGreaterThanZero.js +33 -33
  12. package/out/{rules → src/rules}/preventEmptyInlineTags.js +85 -85
  13. package/out/{rules → src/rules}/requireLinkText.js +101 -86
  14. package/out/src/rules/requireNavLinks.js +88 -0
  15. package/out/src/sarif.js +54 -0
  16. package/out/{simpleHtmlParser.js → src/simpleHtmlParser.js} +4 -0
  17. package/out/src/utils/collectLocalDeps.js +132 -0
  18. package/out/tests/cli-custom-rule.test.js +42 -0
  19. package/out/tests/cross-duplicate-ids-tsx.test.js +36 -0
  20. package/out/tests/crossComponent/Button.js +7 -0
  21. package/out/tests/crossComponent/Page.js +11 -0
  22. package/out/tests/crossComponent/Section.js +11 -0
  23. package/out/tests/crossComponent/SubSection.js +11 -0
  24. package/out/tests/crossComponent/cross-heading-order-alias.test.js +31 -0
  25. package/out/tests/crossComponent/cross-heading-order-entry-only.test.js +31 -0
  26. package/out/tests/crossComponent/cross-heading-order.test.js +45 -0
  27. package/out/tests/custom-rule-tsx.test.js +24 -0
  28. package/out/tests/edge-cases.test.js +17 -0
  29. package/out/tests/html-visitor.test.js +29 -0
  30. package/out/tests/linter.test.js +41 -0
  31. package/out/tests/parse-error.test.js +11 -0
  32. package/out/tests/performance-diagnostics.test.js +12 -0
  33. package/out/tests/sarif-output.test.js +15 -0
  34. package/out/tests/tsx-parse-error.test.js +11 -0
  35. package/out/tests/unique-ids-html.test.js +19 -0
  36. package/out/tests/unique-ids-tsx.test.js +19 -0
  37. package/package.json +33 -9
  38. package/out/index.js +0 -12
  39. package/out/rules/requireNavLinks.js +0 -48
  40. /package/out/{component-path-resolver.js → src/component-path-resolver.js} +0 -0
  41. /package/out/{rules → src/rules}/enforceHeadingOrder.js +0 -0
  42. /package/out/{rules → src/rules}/requireAltText.js +0 -0
  43. /package/out/{rules → src/rules}/requireAltTextJSX.js +0 -0
  44. /package/out/{rules → src/rules}/requireButtonText.js +0 -0
  45. /package/out/{rules → src/rules}/requireHrefOnAnchors.js +0 -0
  46. /package/out/{rules → src/rules}/requireHtmlLang.js +0 -0
  47. /package/out/{rules → src/rules}/requireIframeTitle.js +0 -0
  48. /package/out/{rules → src/rules}/requireImageInputAlt.js +0 -0
  49. /package/out/{rules → src/rules}/requireLabelForFormControls.js +0 -0
  50. /package/out/{rules → src/rules}/requireSectionHeading.js +0 -0
  51. /package/out/{rules → src/rules}/requireTableCaption.js +0 -0
  52. /package/out/{rules → src/rules}/singleH1.js +0 -0
  53. /package/out/{rules → src/rules}/uniqueIds.js +0 -0
  54. /package/out/{rules → src/rules}/utils.js +0 -0
package/README.md CHANGED
@@ -1,8 +1,8 @@
1
- # ZemDomu Core
1
+ # ZemDomu
2
2
 
3
3
  Semantic HTML linting engine for clean, accessible and SEO-friendly markup. This package provides the shared core logic used by the ZemDomu VS Code extension and upcoming GitHub Action.
4
4
 
5
- ## 🧠 What is ZemDomu Core?
5
+ ## 🧠 What is ZemDomu?
6
6
 
7
7
  **ZemDomu** is a semantic-first linter that helps developers write better HTML and JSX by catching accessibility and structural issues. It parses `.html`, `.jsx` and `.tsx` files and exposes a simple `lint()` function that returns semantic violations.
8
8
 
@@ -18,16 +18,20 @@ yarn add zemdomu
18
18
 
19
19
  - ✅ Lint semantic issues in HTML, JSX and TSX
20
20
  - 📦 Works in Node.js, CI or any JS runtime
21
- - ⚙️ Extensible rule system
21
+ - ⚙️ Extensible rule system with simple custom rules
22
+ - 🔀 Cross-component analysis for React/JSX projects
23
+ - 🚀 Command line interface with `--custom` and `--cross`
24
+ - ⚠️ Configurable rule severity (`error`, `warning`, `off`)
25
+ - 📈 Performance diagnostics for profiling lint runs
22
26
  - 📚 Shared by the extension and GitHub Action
23
27
  - 🧪 Simple API: `lint(content, options)`
24
28
 
25
29
  ## ⚙️ Usage Example
26
30
 
27
31
  ```ts
28
- import { lint } from 'zemdomu';
32
+ import { lint } from "zemdomu";
29
33
 
30
- const html = '<img>';
34
+ const html = "<img>";
31
35
  const results = lint(html, { rules: { requireAltText: true } });
32
36
 
33
37
  console.log(results);
@@ -41,7 +45,7 @@ console.log(results);
41
45
  // ]
42
46
 
43
47
  // Custom rules can be supplied via the `customRules` option
44
- // const myRule = { name: 'demo', checkHtml: () => [] };
48
+ // const myRule = { name: 'demo', test: node => false, message: 'demo' };
45
49
  // lint(html, { customRules: [myRule] });
46
50
  ```
47
51
 
@@ -52,21 +56,30 @@ console.log(results);
52
56
  **Parameters**
53
57
 
54
58
  - `content` — HTML, JSX or TSX string input
55
- - `options.rules` — toggles for built-in rules
59
+ - `options.rules` — severity settings for built-in rules
56
60
  - `options.customRules` — array of additional rules
61
+ - `options.filePath` — optional source file path
62
+ - `options.perf` — attach a `PerformanceRecorder` instance
57
63
 
58
64
  **Example `LinterOptions`**
59
65
 
60
66
  ```ts
61
67
  interface LinterOptions {
62
- rules: {
63
- requireAltText: boolean;
64
- // ...more rules to come
65
- };
68
+ rules?: Record<string, 'error' | 'warning' | 'off'>;
66
69
  customRules?: Rule[];
70
+ filePath?: string;
71
+ perf?: PerformanceRecorder;
67
72
  }
68
73
  ```
69
74
 
75
+ Example enabling one rule as a warning:
76
+
77
+ ```ts
78
+ const results = lint(html, {
79
+ rules: { requireAltText: 'warning', uniqueIds: 'error' }
80
+ });
81
+ ```
82
+
70
83
  **Example `LintResult`**
71
84
 
72
85
  ```ts
@@ -78,9 +91,112 @@ interface LintResult {
78
91
  }
79
92
  ```
80
93
 
94
+ ## 🛠 CLI Usage
95
+
96
+ Run the linter from the command line by installing the package globally or using
97
+ `npx`. Provide one or more glob patterns to specify the files to lint. Patterns
98
+ may be separated by spaces, commas or newlines:
99
+
100
+ ```bash
101
+ npx zemdomu "src/**/*.{html,jsx,tsx}" --custom my-rule.js
102
+ npx zemdomu "src/**/*.html,src/**/*.jsx"
103
+ ```
104
+
105
+ Use `--custom` (or `-c`) to provide a path to a JavaScript or TypeScript module
106
+ exporting a custom rule or array of rules. Use `--cross` to enable cross
107
+ component analysis.
108
+
109
+ ### Cross-Component Analysis
110
+
111
+ When analysing JSX projects you can track `<h1>` usage or similar patterns
112
+ across component boundaries. Instantiate `ProjectLinter` with the
113
+ `crossComponentAnalysis` option or pass `--cross` to the CLI. Use
114
+ `crossComponentDepth` (or `--cross-depth`) to limit how deep component trees are
115
+ traversed during analysis:
116
+
117
+ ```ts
118
+ import { ProjectLinter } from 'zemdomu';
119
+ const linter = new ProjectLinter({ crossComponentAnalysis: true, crossComponentDepth: 2 });
120
+ await linter.lintFile('App.jsx');
121
+ ```
122
+
123
+ ```bash
124
+ npx zemdomu "src/**/*.{jsx,tsx}" --cross --cross-depth 2
125
+ ```
126
+
127
+ ### Performance Diagnostics
128
+
129
+ Attach a `PerformanceDiagnostics` recorder to gather timing information for each
130
+ file and rule:
131
+
132
+ ```ts
133
+ import { lint, PerformanceDiagnostics } from 'zemdomu';
134
+ const perf = new PerformanceDiagnostics();
135
+ lint(code, { perf });
136
+ console.log(perf.getAsJSON());
137
+ ```
138
+
139
+ ## 📝 Writing Custom Rules
140
+
141
+ Custom rules are simple objects implementing the `Rule` interface. At minimum
142
+ provide a `name`, a `test` function that returns `true` when a node violates the
143
+ rule and a `message` describing the problem:
144
+
145
+ ```ts
146
+ interface Rule {
147
+ name: string;
148
+ test(node: any): boolean;
149
+ message: string;
150
+ }
151
+ ```
152
+
153
+ ```js
154
+ // my-rule.js
155
+ module.exports = {
156
+ name: 'noFooDiv',
157
+ test: node => node.type === 'element' && node.tagName === 'foo',
158
+ message: '<foo> is not allowed'
159
+ };
160
+ ```
161
+
162
+ Use it programmatically:
163
+
164
+ ```ts
165
+ import { lint } from 'zemdomu';
166
+ const results = lint('<foo></foo>', { customRules: [require('./my-rule')] });
167
+ ```
168
+
169
+ ### Helper Utilities
170
+
171
+ For more advanced rules you may need direct access to the parsed HTML or JSX
172
+ AST. ZemDomu exposes a few helpers to make this easier:
173
+
174
+ ```ts
175
+ import {
176
+ parseHtml,
177
+ visitHtml,
178
+ getAttr,
179
+ getJsxAttr,
180
+ getTag,
181
+ ElementNode,
182
+ HtmlVisitor,
183
+ } from 'zemdomu';
184
+ ```
185
+
186
+ `parseHtml` returns the root `ElementNode`. The `visitHtml` function performs a
187
+ simple depth‑first traversal using an `HtmlVisitor` with optional `enter` and
188
+ `exit` callbacks. Utility functions like `getAttr` and `getJsxAttr` help reading
189
+ attributes, while `getTag` resolves JSX element names.
190
+
191
+ Or via the CLI:
192
+
193
+ ```bash
194
+ npx zemdomu file.html --custom my-rule.js
195
+ ```
196
+
81
197
  ## 🔗 Related Tools
82
198
 
83
- - [ZemDomu VS Code Extension](../ZemDomu-Extension)
199
+ - [ZemDomu VS Code Extension](https://marketplace.visualstudio.com/items?itemName=ZachariasErydBerlin.zemdomu)
84
200
  - ZemDomu GitHub Action (coming soon)
85
201
 
86
202
  ## 🛠 Development
package/out/linter.js CHANGED
@@ -1,164 +0,0 @@
1
- "use strict";
2
- var __importDefault = (this && this.__importDefault) || function (mod) {
3
- return (mod && mod.__esModule) ? mod : { "default": mod };
4
- };
5
- Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.lint = lint;
7
- const simpleHtmlParser_1 = require("./simpleHtmlParser");
8
- const parser_1 = require("@babel/parser");
9
- const traverse_1 = __importDefault(require("@babel/traverse"));
10
- const requireAltText_1 = __importDefault(require("./rules/requireAltText"));
11
- const requireSectionHeading_1 = __importDefault(require("./rules/requireSectionHeading"));
12
- const enforceHeadingOrder_1 = __importDefault(require("./rules/enforceHeadingOrder"));
13
- const singleH1_1 = __importDefault(require("./rules/singleH1"));
14
- const requireLabelForFormControls_1 = __importDefault(require("./rules/requireLabelForFormControls"));
15
- const enforceListNesting_1 = __importDefault(require("./rules/enforceListNesting"));
16
- const requireLinkText_1 = __importDefault(require("./rules/requireLinkText"));
17
- const requireTableCaption_1 = __importDefault(require("./rules/requireTableCaption"));
18
- const preventEmptyInlineTags_1 = __importDefault(require("./rules/preventEmptyInlineTags"));
19
- const requireHrefOnAnchors_1 = __importDefault(require("./rules/requireHrefOnAnchors"));
20
- const requireButtonText_1 = __importDefault(require("./rules/requireButtonText"));
21
- const requireIframeTitle_1 = __importDefault(require("./rules/requireIframeTitle"));
22
- const requireHtmlLang_1 = __importDefault(require("./rules/requireHtmlLang"));
23
- const requireImageInputAlt_1 = __importDefault(require("./rules/requireImageInputAlt"));
24
- const requireNavLinks_1 = __importDefault(require("./rules/requireNavLinks"));
25
- const uniqueIds_1 = __importDefault(require("./rules/uniqueIds"));
26
- const noTabindexGreaterThanZero_1 = __importDefault(require("./rules/noTabindexGreaterThanZero"));
27
- const builtInRules = {
28
- requireSectionHeading: requireSectionHeading_1.default,
29
- enforceHeadingOrder: enforceHeadingOrder_1.default,
30
- singleH1: singleH1_1.default,
31
- requireAltText: requireAltText_1.default,
32
- requireLabelForFormControls: requireLabelForFormControls_1.default,
33
- enforceListNesting: enforceListNesting_1.default,
34
- requireLinkText: requireLinkText_1.default,
35
- requireTableCaption: requireTableCaption_1.default,
36
- preventEmptyInlineTags: preventEmptyInlineTags_1.default,
37
- requireHrefOnAnchors: requireHrefOnAnchors_1.default,
38
- requireButtonText: requireButtonText_1.default,
39
- requireIframeTitle: requireIframeTitle_1.default,
40
- requireHtmlLang: requireHtmlLang_1.default,
41
- requireImageInputAlt: requireImageInputAlt_1.default,
42
- requireNavLinks: requireNavLinks_1.default,
43
- uniqueIds: uniqueIds_1.default,
44
- noTabindexGreaterThanZero: noTabindexGreaterThanZero_1.default,
45
- };
46
- const defaultOptions = {
47
- rules: {
48
- requireSectionHeading: true,
49
- enforceHeadingOrder: true,
50
- singleH1: true,
51
- requireAltText: true,
52
- requireLabelForFormControls: true,
53
- enforceListNesting: true,
54
- requireLinkText: true,
55
- requireTableCaption: true,
56
- preventEmptyInlineTags: true,
57
- requireHrefOnAnchors: true,
58
- requireButtonText: true,
59
- requireIframeTitle: true,
60
- requireHtmlLang: true,
61
- requireImageInputAlt: true,
62
- requireNavLinks: true,
63
- uniqueIds: true,
64
- noTabindexGreaterThanZero: true,
65
- },
66
- customRules: [],
67
- };
68
- /**
69
- * Lint HTML/JSX/TSX content.
70
- */
71
- function lint(content, options = defaultOptions) {
72
- var _a;
73
- const opts = {
74
- rules: { ...defaultOptions.rules, ...(options.rules || {}) },
75
- customRules: (_a = options.customRules) !== null && _a !== void 0 ? _a : defaultOptions.customRules,
76
- };
77
- const results = [];
78
- const activeRules = [];
79
- for (const name in opts.rules) {
80
- const enabled = opts.rules[name];
81
- if (enabled && builtInRules[name]) {
82
- activeRules.push(builtInRules[name]());
83
- }
84
- }
85
- if (opts.customRules)
86
- activeRules.push(...opts.customRules);
87
- activeRules.forEach(r => r.init && r.init());
88
- let ast = null;
89
- try {
90
- ast = (0, parser_1.parse)(content, {
91
- sourceType: 'module',
92
- plugins: ['typescript', 'jsx'],
93
- });
94
- }
95
- catch {
96
- ast = null;
97
- }
98
- if (ast) {
99
- (0, traverse_1.default)(ast, {
100
- JSXElement: {
101
- enter(path) {
102
- var _a;
103
- for (const rule of activeRules) {
104
- if (rule.enterJsx) {
105
- try {
106
- results.push(...rule.enterJsx(path));
107
- }
108
- catch (e) {
109
- console.error(`[ZemDomu] Error in rule ${rule.name} (${(_a = opts.filePath) !== null && _a !== void 0 ? _a : 'unknown'}):`, e);
110
- }
111
- }
112
- }
113
- },
114
- exit(path) {
115
- var _a;
116
- for (const rule of activeRules) {
117
- if (rule.exitJsx) {
118
- try {
119
- results.push(...rule.exitJsx(path));
120
- }
121
- catch (e) {
122
- console.error(`[ZemDomu] Error in rule ${rule.name} (${(_a = opts.filePath) !== null && _a !== void 0 ? _a : 'unknown'}):`, e);
123
- }
124
- }
125
- }
126
- },
127
- },
128
- });
129
- activeRules.forEach(r => r.end && results.push(...r.end()));
130
- return results;
131
- }
132
- const root = (0, simpleHtmlParser_1.parse)(content);
133
- const walk = (node) => {
134
- var _a, _b;
135
- for (const rule of activeRules) {
136
- if (rule.enterHtml) {
137
- try {
138
- results.push(...rule.enterHtml(node));
139
- }
140
- catch (e) {
141
- console.error(`[ZemDomu] Error in rule ${rule.name} (${(_a = opts.filePath) !== null && _a !== void 0 ? _a : 'unknown'}):`, e);
142
- }
143
- }
144
- }
145
- if (node.children) {
146
- for (const child of node.children) {
147
- walk(child);
148
- }
149
- }
150
- for (const rule of activeRules) {
151
- if (rule.exitHtml) {
152
- try {
153
- results.push(...rule.exitHtml(node));
154
- }
155
- catch (e) {
156
- console.error(`[ZemDomu] Error in rule ${rule.name} (${(_b = opts.filePath) !== null && _b !== void 0 ? _b : 'unknown'}):`, e);
157
- }
158
- }
159
- }
160
- };
161
- walk(root);
162
- activeRules.forEach(r => r.end && results.push(...r.end()));
163
- return results;
164
- }
package/out/src/cli.js ADDED
@@ -0,0 +1,88 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ var __importDefault = (this && this.__importDefault) || function (mod) {
4
+ return (mod && mod.__esModule) ? mod : { "default": mod };
5
+ };
6
+ Object.defineProperty(exports, "__esModule", { value: true });
7
+ const glob_1 = __importDefault(require("glob"));
8
+ const path_1 = __importDefault(require("path"));
9
+ const project_linter_1 = require("./project-linter");
10
+ function parsePatterns(inputs) {
11
+ const result = [];
12
+ for (const input of inputs) {
13
+ const splits = input
14
+ .split(/\r?\n/)
15
+ .flatMap((p) => p.split(/[ ,]+/))
16
+ .filter(Boolean);
17
+ result.push(...splits);
18
+ }
19
+ return result;
20
+ }
21
+ async function run() {
22
+ var _a;
23
+ const args = process.argv.slice(2);
24
+ const rawPatterns = [];
25
+ const customRules = [];
26
+ let cross = false;
27
+ let depth;
28
+ for (let i = 0; i < args.length; i++) {
29
+ const arg = args[i];
30
+ if (arg === '--custom' || arg === '-c') {
31
+ const file = args[++i];
32
+ if (!file)
33
+ throw new Error('Missing file for --custom');
34
+ const resolved = path_1.default.resolve(file);
35
+ const customDir = path_1.default.resolve('custom-rules');
36
+ const relative = path_1.default.relative(customDir, resolved);
37
+ if (relative.startsWith('..') || path_1.default.isAbsolute(relative)) {
38
+ throw new Error('Custom rule file must be inside ./custom-rules');
39
+ }
40
+ const mod = require(resolved);
41
+ const rules = (_a = mod.default) !== null && _a !== void 0 ? _a : mod;
42
+ if (Array.isArray(rules))
43
+ customRules.push(...rules);
44
+ else
45
+ customRules.push(rules);
46
+ }
47
+ else if (arg === '--cross') {
48
+ cross = true;
49
+ }
50
+ else if (arg === '--cross-depth') {
51
+ const val = args[++i];
52
+ if (!val)
53
+ throw new Error('Missing value for --cross-depth');
54
+ depth = parseInt(val, 10);
55
+ if (isNaN(depth))
56
+ throw new Error('Invalid number for --cross-depth');
57
+ cross = true;
58
+ }
59
+ else {
60
+ rawPatterns.push(arg);
61
+ }
62
+ }
63
+ const patterns = parsePatterns(rawPatterns);
64
+ if (patterns.length === 0) {
65
+ patterns.push('**/*.{html,jsx,tsx}');
66
+ }
67
+ const files = new Set();
68
+ for (const pattern of patterns) {
69
+ const matches = glob_1.default.sync(pattern, { nodir: true });
70
+ for (const m of matches)
71
+ files.add(m);
72
+ }
73
+ const linter = new project_linter_1.ProjectLinter({ customRules, crossComponentAnalysis: cross, crossComponentDepth: depth });
74
+ const results = await linter.lintFiles(Array.from(files));
75
+ let hasIssues = false;
76
+ for (const [file, issues] of results.entries()) {
77
+ for (const issue of issues) {
78
+ console.error(`${file}:${issue.line + 1}:${issue.column + 1} ${issue.rule}: ${issue.message}`);
79
+ hasIssues = true;
80
+ }
81
+ }
82
+ if (hasIssues)
83
+ process.exit(1);
84
+ }
85
+ run().catch((e) => {
86
+ console.error(e);
87
+ process.exit(1);
88
+ });