rintenki 0.3.0 → 0.5.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/README.md CHANGED
@@ -8,7 +8,7 @@ The name "rintenki" comes from the Japanese word "輪転機" (rintenki), meaning
8
8
 
9
9
  - Parsing by html5ever (Rust)
10
10
  - Node.js native binding via napi-rs
11
- - 38 built-in rules
11
+ - 58 built-in rules
12
12
  - CLI / API / VS Code extension / LSP server
13
13
  - Auto-fix with `--fix`
14
14
  - JSON output for CI integration
@@ -33,7 +33,7 @@ npx rintenki "src/**/*.html"
33
33
  rintenki [options] <files...>
34
34
 
35
35
  -c, --config <path> Path to config file (default: .rintenkirc.json)
36
- -f, --format <format> Output format: stylish (default), json
36
+ -f, --format <format> Output format: stylish (default), json, sarif
37
37
  --fix Auto-fix fixable rules
38
38
  --max-warnings <number> Exit with error if warnings exceed this number
39
39
  -h, --help Show help
@@ -118,6 +118,7 @@ rintenki supports non-HTML files via optional parser plugins.
118
118
  | `@rintenki/vue-parser` | Vue SFC (`.vue`) | `npm install @rintenki/vue-parser` |
119
119
  | `@rintenki/jsx-parser` | JSX/TSX (`.jsx`, `.tsx`) | `npm install @rintenki/jsx-parser` |
120
120
  | `@rintenki/erb-parser` | eRuby (`.erb`) | `npm install @rintenki/erb-parser` |
121
+ | `@rintenki/astro-parser` | Astro (`.astro`) | `npm install @rintenki/astro-parser` |
121
122
 
122
123
  Parsers are auto-detected from installed packages, or can be explicitly configured in `.rintenkirc.json`.
123
124
 
@@ -126,6 +127,7 @@ Parsers are auto-detected from installed packages, or can be explicitly configur
126
127
  - **Vue**: Extracts `<template>` block via `@vue/compiler-sfc`, masks `{{ }}` interpolations
127
128
  - **JSX/TSX**: Parses AST via `oxc-parser` (Rust), extracts HTML elements, maps React attributes (`className` → `class`)
128
129
  - **eRuby**: Masks `<% %>` tags with same-length placeholders, preserving line numbers
130
+ - **Astro**: Strips frontmatter (`---...---`), masks `{expression}` with nested brace support
129
131
 
130
132
  ## Rules
131
133
 
@@ -134,31 +136,50 @@ Parsers are auto-detected from installed packages, or can be explicitly configur
134
136
  | Rule | Default | Description |
135
137
  |------|---------|-------------|
136
138
  | `attr-duplication` | error | Detect duplicate attributes |
139
+ | `colspan-rowspan-range` | error | Validate colspan (1-1000) and rowspan (0-65534) ranges |
137
140
  | `deprecated-attr` | error | Detect deprecated or obsolete attributes |
138
141
  | `deprecated-element` | error | Detect deprecated or obsolete elements |
139
142
  | `disallowed-element` | off | Detect disallowed elements |
140
143
  | `doctype` | error | Detect missing DOCTYPE declaration |
144
+ | `header-footer-nesting` | error | Detect header/footer/main nesting inside header or footer |
141
145
  | `heading-levels` | error | Detect skipped heading levels |
142
146
  | `id-duplication` | error | Detect duplicate id attribute values |
147
+ | `input-attr-applicability` | warning | Detect attributes that do not apply to the input type |
143
148
  | `invalid-attr` | error | Detect attributes not in the spec |
149
+ | `meta-constraints` | error | Validate meta element attribute constraints |
150
+ | `no-duplicate-base` | error | Detect multiple base or title elements |
144
151
  | `no-duplicate-dt` | error | Detect duplicate dt names in dl |
145
152
  | `no-empty-palpable-content` | warning | Detect empty palpable content elements |
153
+ | `no-nested-forms` | error | Detect nested form elements |
154
+ | `no-nested-interactive` | error | Detect interactive content inside a or button |
146
155
  | `no-orphaned-end-tag` | error | Detect end tags without matching start tags |
156
+ | `no-tabindex-on-dialog` | error | Detect tabindex on dialog elements |
157
+ | `obsolete-but-conforming` | warning | Detect obsolete but conforming features |
147
158
  | `permitted-contents` | error | Detect children not permitted by the spec |
159
+ | `picture-structure` | error | Validate picture element structure |
148
160
  | `placeholder-label-option` | warning | Detect missing placeholder option in required select |
149
161
  | `require-datetime` | error | Detect missing datetime attribute on time element |
150
162
  | `required-attr` | error | Detect missing required attributes |
151
163
  | `required-element` | error | Detect missing required child elements |
164
+ | `summary-first-child` | error | Require summary as first child of details |
165
+ | `th-content-restrictions` | error | Detect disallowed elements inside th |
166
+ | `unique-main` | error | Require at most one visible main element |
167
+ | `valid-attr-value` | error | Validate enumerated attribute values |
168
+ | `valid-autocomplete` | warning | Validate autocomplete attribute values |
169
+ | `valid-rel` | error | Validate rel attribute values |
152
170
 
153
171
  ### Accessibility
154
172
 
155
173
  | Rule | Default | Description |
156
174
  |------|---------|-------------|
175
+ | `aria-attr-valid-values` | warning | Validate ARIA attribute values |
176
+ | `aria-role-conflicts` | warning | Detect conflicts between explicit role and implicit role |
157
177
  | `label-has-control` | error | Detect label elements without associated control |
158
178
  | `landmark-roles` | warning | Detect nested landmark roles |
159
179
  | `neighbor-popovers` | off | Detect non-adjacent popover triggers and targets |
160
180
  | `no-ambiguous-navigable-target-names` | warning | Detect invalid target name keywords |
161
181
  | `no-consecutive-br` | warning | Detect consecutive br elements |
182
+ | `no-positive-tabindex` | warning | Detect positive tabindex values |
162
183
  | `no-refer-to-non-existent-id` | error | Detect references to non-existent ids |
163
184
  | `require-accessible-name` | error | Detect missing accessible names |
164
185
  | `required-h1` | error | Detect missing h1 element |
@@ -177,6 +198,7 @@ Parsers are auto-detected from installed packages, or can be explicitly configur
177
198
  | Rule | Default | Description |
178
199
  |------|---------|-------------|
179
200
  | `no-hard-code-id` | off | Detect hardcoded id attributes |
201
+ | `no-inline-style` | warning | Detect inline style attributes |
180
202
  | `no-use-event-handler-attr` | warning | Detect event handler attributes |
181
203
 
182
204
  ### Style
@@ -196,8 +218,11 @@ Parsers are auto-detected from installed packages, or can be explicitly configur
196
218
 
197
219
  The following rules can be auto-fixed with `--fix`:
198
220
 
199
- - `case-sensitive-tag-name`
221
+ - `attr-value-quotes`
200
222
  - `case-sensitive-attr-name`
223
+ - `case-sensitive-tag-name`
224
+ - `character-reference`
225
+ - `doctype`
201
226
  - `no-boolean-attr-value`
202
227
  - `no-default-value`
203
228
 
@@ -235,6 +260,7 @@ code . # Open project root in VS Code
235
260
  | `@rintenki/vue-parser` | Vue SFC parser plugin |
236
261
  | `@rintenki/jsx-parser` | JSX/TSX parser plugin |
237
262
  | `@rintenki/erb-parser` | eRuby parser plugin |
263
+ | `@rintenki/astro-parser` | Astro parser plugin |
238
264
 
239
265
  ## Supported Platforms
240
266
 
package/bin/rintenki.js CHANGED
@@ -1,7 +1,8 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- const { existsSync, readFileSync, writeFileSync } = require("fs");
4
- const { resolve, relative } = require("path");
3
+ const { existsSync, readFileSync, writeFileSync, watch } = require("fs");
4
+ const { resolve, relative, dirname, join } = require("path");
5
+ const { createHash } = require("crypto");
5
6
  const { glob } = require("tinyglobby");
6
7
  const { lint, fix } = require("../index");
7
8
 
@@ -85,11 +86,15 @@ function formatDiagnostic(diagnostic) {
85
86
  ? `${COLORS.red}error${COLORS.reset}`
86
87
  : `${COLORS.yellow}warning${COLORS.reset}`;
87
88
  const loc = diagnostic.line != null ? `${COLORS.gray}line ${diagnostic.line}${COLORS.reset} ` : "";
88
- return ` ${loc}${severity} ${diagnostic.message} ${COLORS.gray}${diagnostic.rule}${COLORS.reset}`;
89
+ let line = ` ${loc}${severity} ${diagnostic.message} ${COLORS.gray}${diagnostic.rule}${COLORS.reset}`;
90
+ if (diagnostic.hint) {
91
+ line += `\n ${COLORS.gray}hint: ${diagnostic.hint}${COLORS.reset}`;
92
+ }
93
+ return line;
89
94
  }
90
95
 
91
96
  function parseArgs(argv) {
92
- const args = { files: [], config: undefined, help: false, maxWarnings: -1, format: "stylish", fix: false };
97
+ const args = { files: [], config: undefined, help: false, maxWarnings: -1, format: "stylish", fix: false, watch: false };
93
98
  let i = 0;
94
99
  while (i < argv.length) {
95
100
  const arg = argv[i];
@@ -103,6 +108,8 @@ function parseArgs(argv) {
103
108
  args.format = argv[++i];
104
109
  } else if (arg === "--fix") {
105
110
  args.fix = true;
111
+ } else if (arg === "--watch" || arg === "-w") {
112
+ args.watch = true;
106
113
  } else if (!arg.startsWith("-")) {
107
114
  args.files.push(arg);
108
115
  }
@@ -111,6 +118,69 @@ function parseArgs(argv) {
111
118
  return args;
112
119
  }
113
120
 
121
+ function buildSarif(jsonOutput) {
122
+ const ruleSet = new Map();
123
+ const results = [];
124
+
125
+ for (const { file, diagnostics } of jsonOutput) {
126
+ for (const d of diagnostics) {
127
+ if (!ruleSet.has(d.rule)) {
128
+ ruleSet.set(d.rule, {
129
+ id: d.rule,
130
+ shortDescription: { text: d.message },
131
+ });
132
+ }
133
+
134
+ const region = {};
135
+ if (d.line != null) {
136
+ region.startLine = d.line;
137
+ region.startColumn = (d.col ?? 0) + 1;
138
+ }
139
+ if (d.endLine != null) {
140
+ region.endLine = d.endLine;
141
+ region.endColumn = (d.endCol ?? 0) + 1;
142
+ }
143
+
144
+ const result = {
145
+ ruleId: d.rule,
146
+ level: d.severity === "error" ? "error" : "warning",
147
+ message: { text: d.hint ? `${d.message}\nhint: ${d.hint}` : d.message },
148
+ locations: [
149
+ {
150
+ physicalLocation: {
151
+ artifactLocation: { uri: file, uriBaseId: "%SRCROOT%" },
152
+ region,
153
+ },
154
+ },
155
+ ],
156
+ };
157
+
158
+ if (d.fixable) {
159
+ result.properties = { fixable: true };
160
+ }
161
+
162
+ results.push(result);
163
+ }
164
+ }
165
+
166
+ return {
167
+ $schema: "https://json.schemastore.org/sarif-2.1.0.json",
168
+ version: "2.1.0",
169
+ runs: [
170
+ {
171
+ tool: {
172
+ driver: {
173
+ name: "rintenki",
174
+ informationUri: "https://github.com/kzhrk/rintenki",
175
+ rules: [...ruleSet.values()],
176
+ },
177
+ },
178
+ results,
179
+ },
180
+ ],
181
+ };
182
+ }
183
+
114
184
  async function main() {
115
185
  const args = parseArgs(process.argv.slice(2));
116
186
 
@@ -119,8 +189,9 @@ async function main() {
119
189
 
120
190
  Options:
121
191
  -c, --config <path> Path to config file (default: .rintenkirc.json)
122
- -f, --format <format> Output format: stylish (default), json
192
+ -f, --format <format> Output format: stylish (default), json, sarif
123
193
  --fix Auto-fix fixable issues
194
+ -w, --watch Watch files for changes and re-lint
124
195
  --max-warnings <number> Exit with error if warnings exceed this number
125
196
  -h, --help Show help
126
197
 
@@ -144,69 +215,179 @@ Examples:
144
215
  process.exit(1);
145
216
  }
146
217
 
147
- let totalErrors = 0;
148
- let totalWarnings = 0;
149
- const jsonOutput = [];
218
+ const { totalErrors, totalWarnings } = await lintFiles(files, config, args);
150
219
 
151
- let totalFixed = 0;
220
+ if (args.watch) {
221
+ console.log(`\n${COLORS.gray}Watching for changes...${COLORS.reset}\n`);
222
+
223
+ const cache = new Map();
224
+ const dirs = [...new Set(files.map((f) => dirname(f)))];
225
+ let debounceTimer = null;
226
+
227
+ for (const dir of dirs) {
228
+ watch(dir, { recursive: true }, (_eventType, filename) => {
229
+ if (!filename) return;
230
+ const abs = resolve(dir, filename);
231
+ if (!files.includes(abs)) return;
152
232
 
153
- for (const file of files) {
154
- let source = readFileSync(file, "utf-8");
155
- let lineOffset = 0;
156
-
157
- // Preprocess non-HTML files (Vue, ERB, etc.)
158
- const parser = loadParser(file, config?.parser);
159
- if (parser) {
160
- const preprocessed = parser.preprocess(source);
161
- source = preprocessed.html;
162
- lineOffset = preprocessed.lineOffset;
163
- if (!source) continue;
233
+ // Invalidate cache for changed file
234
+ cache.delete(abs);
235
+
236
+ clearTimeout(debounceTimer);
237
+ debounceTimer = setTimeout(async () => {
238
+ process.stdout.write("\x1Bc");
239
+ await lintFiles(files, config, args, cache);
240
+ console.log(`\n${COLORS.gray}Watching for changes...${COLORS.reset}\n`);
241
+ }, 100);
242
+ });
164
243
  }
244
+ return;
245
+ }
165
246
 
166
- let html = source;
247
+ if (totalErrors > 0) {
248
+ process.exit(1);
249
+ }
167
250
 
168
- if (args.fix) {
169
- const fixResult = fix(html, config);
170
- if (fixResult.fixedCount > 0) {
171
- writeFileSync(file, fixResult.output, "utf-8");
172
- html = fixResult.output;
173
- totalFixed += fixResult.fixedCount;
174
- }
251
+ if (args.maxWarnings >= 0 && totalWarnings > args.maxWarnings) {
252
+ process.exit(1);
253
+ }
254
+ }
255
+
256
+ const PARALLEL_THRESHOLD = 20;
257
+
258
+ function lintOneFile(file, config, args) {
259
+ let source = readFileSync(file, "utf-8");
260
+ let lineOffset = 0;
261
+
262
+ const parser = loadParser(file, config?.parser);
263
+ if (parser) {
264
+ const preprocessed = parser.preprocess(source);
265
+ source = preprocessed.html;
266
+ lineOffset = preprocessed.lineOffset;
267
+ if (!source) return null;
268
+ }
269
+
270
+ let html = source;
271
+ let fixedCount = 0;
272
+
273
+ if (args.fix) {
274
+ const fixResult = fix(html, config);
275
+ if (fixResult.fixedCount > 0) {
276
+ writeFileSync(file, fixResult.output, "utf-8");
277
+ html = fixResult.output;
278
+ fixedCount = fixResult.fixedCount;
175
279
  }
280
+ }
281
+
282
+ const result = lint(html, config);
283
+
284
+ if (lineOffset > 0) {
285
+ for (const d of result.diagnostics) {
286
+ if (d.line != null) d.line += lineOffset;
287
+ if (d.endLine != null) d.endLine += lineOffset;
288
+ }
289
+ }
290
+
291
+ return { diagnostics: result.diagnostics, fixedCount };
292
+ }
293
+
294
+ function fileHash(content) {
295
+ return createHash("md5").update(content).digest("hex");
296
+ }
297
+
298
+ async function lintFilesParallel(files, config, args) {
299
+ const { Worker } = require("worker_threads");
300
+ const cpus = require("os").cpus().length;
301
+ const workerCount = Math.min(cpus, 4, files.length);
302
+ const workerPath = join(__dirname, "worker.js");
303
+
304
+ const chunks = Array.from({ length: workerCount }, () => []);
305
+ files.forEach((f, i) => chunks[i % workerCount].push(f));
306
+
307
+ const results = await Promise.all(
308
+ chunks.map(
309
+ (chunk) =>
310
+ new Promise((resolve, reject) => {
311
+ const worker = new Worker(workerPath, {
312
+ workerData: { files: chunk, config, fix: args.fix },
313
+ });
314
+ worker.on("message", resolve);
315
+ worker.on("error", reject);
316
+ })
317
+ )
318
+ );
319
+
320
+ // Merge results: array of { file, diagnostics, fixedCount }[]
321
+ return results.flat();
322
+ }
323
+
324
+ async function lintFiles(files, config, args, cache) {
325
+ let totalErrors = 0;
326
+ let totalWarnings = 0;
327
+ const jsonOutput = [];
328
+ let totalFixed = 0;
329
+
330
+ let fileResults;
331
+
332
+ if (!args.fix && !cache && files.length >= PARALLEL_THRESHOLD) {
333
+ // Parallel mode: no fix, no cache, many files
334
+ const parallel = await lintFilesParallel(files, config, args);
335
+ fileResults = parallel.map((r) => ({
336
+ file: r.file,
337
+ rel: relative(process.cwd(), r.file),
338
+ diagnostics: r.diagnostics,
339
+ fixedCount: r.fixedCount,
340
+ }));
341
+ } else {
342
+ // Sequential mode (with optional cache for watch)
343
+ fileResults = [];
344
+ for (const file of files) {
345
+ const source = readFileSync(file, "utf-8");
346
+ const hash = fileHash(source);
347
+
348
+ if (cache) {
349
+ const cached = cache.get(file);
350
+ if (cached && cached.hash === hash) {
351
+ fileResults.push({ file, rel: relative(process.cwd(), file), diagnostics: cached.diagnostics, fixedCount: 0 });
352
+ continue;
353
+ }
354
+ }
176
355
 
177
- const result = lint(html, config);
178
- const rel = relative(process.cwd(), file);
356
+ const result = lintOneFile(file, config, args);
357
+ if (!result) continue;
179
358
 
180
- // Remap line numbers to original file positions
181
- if (lineOffset > 0) {
182
- for (const d of result.diagnostics) {
183
- if (d.line != null) d.line += lineOffset;
184
- if (d.endLine != null) d.endLine += lineOffset;
359
+ if (cache) {
360
+ cache.set(file, { hash, diagnostics: result.diagnostics });
185
361
  }
362
+
363
+ fileResults.push({ file, rel: relative(process.cwd(), file), diagnostics: result.diagnostics, fixedCount: result.fixedCount });
186
364
  }
365
+ }
187
366
 
188
- for (const d of result.diagnostics) {
367
+ for (const { rel, diagnostics, fixedCount } of fileResults) {
368
+ totalFixed += fixedCount;
369
+
370
+ for (const d of diagnostics) {
189
371
  if (d.severity === "error") totalErrors++;
190
372
  else totalWarnings++;
191
373
  }
192
374
 
193
- if (args.format === "json") {
194
- if (result.diagnostics.length > 0) {
195
- jsonOutput.push({
196
- file: rel,
197
- diagnostics: result.diagnostics,
198
- });
375
+ if (args.format === "json" || args.format === "sarif") {
376
+ if (diagnostics.length > 0) {
377
+ jsonOutput.push({ file: rel, diagnostics });
199
378
  }
200
379
  } else {
201
- if (result.diagnostics.length === 0) continue;
380
+ if (diagnostics.length === 0) continue;
202
381
  console.log(`\n${COLORS.bold}${rel}${COLORS.reset}`);
203
- for (const d of result.diagnostics) {
382
+ for (const d of diagnostics) {
204
383
  console.log(formatDiagnostic(d));
205
384
  }
206
385
  }
207
386
  }
208
387
 
209
- if (args.format === "json") {
388
+ if (args.format === "sarif") {
389
+ console.log(JSON.stringify(buildSarif(jsonOutput), null, 2));
390
+ } else if (args.format === "json") {
210
391
  console.log(JSON.stringify(jsonOutput, null, 2));
211
392
  } else if (totalErrors > 0 || totalWarnings > 0) {
212
393
  const parts = [];
@@ -221,13 +402,7 @@ Examples:
221
402
  console.log("No issues found.");
222
403
  }
223
404
 
224
- if (totalErrors > 0) {
225
- process.exit(1);
226
- }
227
-
228
- if (args.maxWarnings >= 0 && totalWarnings > args.maxWarnings) {
229
- process.exit(1);
230
- }
405
+ return { totalErrors, totalWarnings };
231
406
  }
232
407
 
233
408
  main().catch((err) => {
package/bin/worker.js ADDED
@@ -0,0 +1,51 @@
1
+ const { workerData, parentPort } = require("worker_threads");
2
+ const { readFileSync, writeFileSync } = require("fs");
3
+ const { lint, fix } = require("../index");
4
+
5
+ let loadParser;
6
+ try {
7
+ loadParser = require("@rintenki/parser-utils").loadParser;
8
+ } catch {
9
+ loadParser = () => undefined;
10
+ }
11
+
12
+ const { files, config, fix: shouldFix } = workerData;
13
+ const results = [];
14
+
15
+ for (const file of files) {
16
+ let source = readFileSync(file, "utf-8");
17
+ let lineOffset = 0;
18
+
19
+ const parser = loadParser(file, config?.parser);
20
+ if (parser) {
21
+ const preprocessed = parser.preprocess(source);
22
+ source = preprocessed.html;
23
+ lineOffset = preprocessed.lineOffset;
24
+ if (!source) continue;
25
+ }
26
+
27
+ let html = source;
28
+ let fixedCount = 0;
29
+
30
+ if (shouldFix) {
31
+ const fixResult = fix(html, config);
32
+ if (fixResult.fixedCount > 0) {
33
+ writeFileSync(file, fixResult.output, "utf-8");
34
+ html = fixResult.output;
35
+ fixedCount = fixResult.fixedCount;
36
+ }
37
+ }
38
+
39
+ const result = lint(html, config);
40
+
41
+ if (lineOffset > 0) {
42
+ for (const d of result.diagnostics) {
43
+ if (d.line != null) d.line += lineOffset;
44
+ if (d.endLine != null) d.endLine += lineOffset;
45
+ }
46
+ }
47
+
48
+ results.push({ file, diagnostics: result.diagnostics, fixedCount });
49
+ }
50
+
51
+ parentPort.postMessage(results);
package/index.d.ts CHANGED
@@ -7,6 +7,7 @@ export interface LintDiagnostic {
7
7
  endLine: number | null;
8
8
  endCol: number | null;
9
9
  fixable: boolean;
10
+ hint: string | null;
10
11
  }
11
12
 
12
13
  export interface LintResult {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rintenki",
3
- "version": "0.3.0",
3
+ "version": "0.5.0",
4
4
  "description": "A fast HTML linter powered by html5ever + napi-rs",
5
5
  "author": "Kazuhiro Kobayashi <https://github.com/kzhrk>",
6
6
  "license": "MIT",
@@ -32,8 +32,10 @@
32
32
  "index.js",
33
33
  "index.d.ts",
34
34
  "bin/rintenki.js",
35
+ "bin/worker.js",
35
36
  "rintenki.*.node",
36
- "README.md"
37
+ "README.md",
38
+ "rintenkirc.schema.json"
37
39
  ],
38
40
  "napi": {
39
41
  "binaryName": "rintenki",
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
@@ -0,0 +1,336 @@
1
+ {
2
+ "$schema": "http://json-schema.org/draft-07/schema#",
3
+ "title": "Rintenki Configuration",
4
+ "description": "Configuration file for the rintenki HTML linter",
5
+ "type": "object",
6
+ "properties": {
7
+ "rules": {
8
+ "type": "object",
9
+ "description": "Rule severity overrides",
10
+ "properties": {
11
+ "aria-attr-valid-values": {
12
+ "$ref": "#/definitions/severity",
13
+ "description": "Validate ARIA attribute values",
14
+ "default": "warning"
15
+ },
16
+ "aria-role-conflicts": {
17
+ "$ref": "#/definitions/severity",
18
+ "description": "Detect conflicts between explicit role and element's implicit role",
19
+ "default": "warning"
20
+ },
21
+ "attr-duplication": {
22
+ "$ref": "#/definitions/severity",
23
+ "description": "Disallow duplicate attributes on the same element",
24
+ "default": "error"
25
+ },
26
+ "attr-value-quotes": {
27
+ "$ref": "#/definitions/severity",
28
+ "description": "Require attribute values to be quoted",
29
+ "default": "warning"
30
+ },
31
+ "case-sensitive-attr-name": {
32
+ "$ref": "#/definitions/severity",
33
+ "description": "Enforce lowercase attribute names (auto-fixable)",
34
+ "default": "warning"
35
+ },
36
+ "colspan-rowspan-range": {
37
+ "$ref": "#/definitions/severity",
38
+ "description": "Require colspan and rowspan values to be within valid ranges",
39
+ "default": "error"
40
+ },
41
+ "case-sensitive-tag-name": {
42
+ "$ref": "#/definitions/severity",
43
+ "description": "Enforce lowercase tag names (auto-fixable)",
44
+ "default": "warning"
45
+ },
46
+ "character-reference": {
47
+ "$ref": "#/definitions/severity",
48
+ "description": "Require unescaped & to be written as &amp;",
49
+ "default": "warning"
50
+ },
51
+ "class-naming": {
52
+ "$ref": "#/definitions/severity",
53
+ "description": "Enforce class name conventions (opt-in)",
54
+ "default": "off"
55
+ },
56
+ "deprecated-attr": {
57
+ "$ref": "#/definitions/severity",
58
+ "description": "Disallow deprecated HTML attributes",
59
+ "default": "error"
60
+ },
61
+ "deprecated-element": {
62
+ "$ref": "#/definitions/severity",
63
+ "description": "Disallow deprecated HTML elements",
64
+ "default": "error"
65
+ },
66
+ "disallowed-element": {
67
+ "$ref": "#/definitions/severity",
68
+ "description": "Disallow specified elements (opt-in)",
69
+ "default": "off"
70
+ },
71
+ "doctype": {
72
+ "$ref": "#/definitions/severity",
73
+ "description": "Require DOCTYPE declaration",
74
+ "default": "error"
75
+ },
76
+ "end-tag": {
77
+ "$ref": "#/definitions/severity",
78
+ "description": "Require closing tags for non-void elements",
79
+ "default": "warning"
80
+ },
81
+ "header-footer-nesting": {
82
+ "$ref": "#/definitions/severity",
83
+ "description": "Disallow header/footer/main nesting inside header or footer",
84
+ "default": "error"
85
+ },
86
+ "heading-levels": {
87
+ "$ref": "#/definitions/severity",
88
+ "description": "Disallow skipping heading levels (e.g. h1 to h3)",
89
+ "default": "error"
90
+ },
91
+ "id-duplication": {
92
+ "$ref": "#/definitions/severity",
93
+ "description": "Disallow duplicate id attribute values",
94
+ "default": "error"
95
+ },
96
+ "input-attr-applicability": {
97
+ "$ref": "#/definitions/severity",
98
+ "description": "Detect attributes that do not apply to the input type",
99
+ "default": "warning"
100
+ },
101
+ "ineffective-attr": {
102
+ "$ref": "#/definitions/severity",
103
+ "description": "Disallow attributes that have no effect on the element",
104
+ "default": "warning"
105
+ },
106
+ "invalid-attr": {
107
+ "$ref": "#/definitions/severity",
108
+ "description": "Disallow non-standard attributes",
109
+ "default": "error"
110
+ },
111
+ "meta-constraints": {
112
+ "$ref": "#/definitions/severity",
113
+ "description": "Validate meta element attribute constraints",
114
+ "default": "error"
115
+ },
116
+ "label-has-control": {
117
+ "$ref": "#/definitions/severity",
118
+ "description": "Require label elements to have an associated control",
119
+ "default": "error"
120
+ },
121
+ "landmark-roles": {
122
+ "$ref": "#/definitions/severity",
123
+ "description": "Disallow nested landmark roles",
124
+ "default": "warning"
125
+ },
126
+ "neighbor-popovers": {
127
+ "$ref": "#/definitions/severity",
128
+ "description": "Require popover triggers to be adjacent to their targets (opt-in)",
129
+ "default": "off"
130
+ },
131
+ "no-ambiguous-navigable-target-names": {
132
+ "$ref": "#/definitions/severity",
133
+ "description": "Disallow ambiguous target attribute values",
134
+ "default": "warning"
135
+ },
136
+ "no-boolean-attr-value": {
137
+ "$ref": "#/definitions/severity",
138
+ "description": "Disallow redundant boolean attribute values (auto-fixable)",
139
+ "default": "warning"
140
+ },
141
+ "no-consecutive-br": {
142
+ "$ref": "#/definitions/severity",
143
+ "description": "Disallow consecutive br elements",
144
+ "default": "warning"
145
+ },
146
+ "no-default-value": {
147
+ "$ref": "#/definitions/severity",
148
+ "description": "Disallow default attribute values (auto-fixable)",
149
+ "default": "warning"
150
+ },
151
+ "no-duplicate-base": {
152
+ "$ref": "#/definitions/severity",
153
+ "description": "Disallow multiple base or title elements in the document",
154
+ "default": "error"
155
+ },
156
+ "no-duplicate-dt": {
157
+ "$ref": "#/definitions/severity",
158
+ "description": "Disallow duplicate dt elements in a dl",
159
+ "default": "error"
160
+ },
161
+ "no-empty-palpable-content": {
162
+ "$ref": "#/definitions/severity",
163
+ "description": "Disallow empty interactive or palpable content elements",
164
+ "default": "warning"
165
+ },
166
+ "no-hard-code-id": {
167
+ "$ref": "#/definitions/severity",
168
+ "description": "Disallow hardcoded id attributes (opt-in)",
169
+ "default": "off"
170
+ },
171
+ "no-nested-forms": {
172
+ "$ref": "#/definitions/severity",
173
+ "description": "Disallow nested form elements",
174
+ "default": "error"
175
+ },
176
+ "no-nested-interactive": {
177
+ "$ref": "#/definitions/severity",
178
+ "description": "Disallow interactive content inside a or button elements",
179
+ "default": "error"
180
+ },
181
+ "no-inline-style": {
182
+ "$ref": "#/definitions/severity",
183
+ "description": "Disallow inline style attributes",
184
+ "default": "warning"
185
+ },
186
+ "no-orphaned-end-tag": {
187
+ "$ref": "#/definitions/severity",
188
+ "description": "Disallow unmatched closing tags",
189
+ "default": "error"
190
+ },
191
+ "no-tabindex-on-dialog": {
192
+ "$ref": "#/definitions/severity",
193
+ "description": "Disallow tabindex attribute on dialog elements",
194
+ "default": "error"
195
+ },
196
+ "no-refer-to-non-existent-id": {
197
+ "$ref": "#/definitions/severity",
198
+ "description": "Disallow references to non-existent id values",
199
+ "default": "error"
200
+ },
201
+ "no-positive-tabindex": {
202
+ "$ref": "#/definitions/severity",
203
+ "description": "Disallow positive tabindex values",
204
+ "default": "warning"
205
+ },
206
+ "no-use-event-handler-attr": {
207
+ "$ref": "#/definitions/severity",
208
+ "description": "Disallow inline event handler attributes",
209
+ "default": "warning"
210
+ },
211
+ "obsolete-but-conforming": {
212
+ "$ref": "#/definitions/severity",
213
+ "description": "Warn about obsolete but conforming features",
214
+ "default": "warning"
215
+ },
216
+ "picture-structure": {
217
+ "$ref": "#/definitions/severity",
218
+ "description": "Validate picture element structure",
219
+ "default": "error"
220
+ },
221
+ "permitted-contents": {
222
+ "$ref": "#/definitions/severity",
223
+ "description": "Enforce HTML content model restrictions",
224
+ "default": "error"
225
+ },
226
+ "placeholder-label-option": {
227
+ "$ref": "#/definitions/severity",
228
+ "description": "Require placeholder option in select elements with required attribute",
229
+ "default": "warning"
230
+ },
231
+ "require-accessible-name": {
232
+ "$ref": "#/definitions/severity",
233
+ "description": "Require accessible names for interactive elements",
234
+ "default": "error"
235
+ },
236
+ "require-datetime": {
237
+ "$ref": "#/definitions/severity",
238
+ "description": "Require datetime attribute on time elements",
239
+ "default": "error"
240
+ },
241
+ "required-attr": {
242
+ "$ref": "#/definitions/severity",
243
+ "description": "Require specification-mandated attributes",
244
+ "default": "error"
245
+ },
246
+ "required-element": {
247
+ "$ref": "#/definitions/severity",
248
+ "description": "Require specification-mandated child elements",
249
+ "default": "error"
250
+ },
251
+ "required-h1": {
252
+ "$ref": "#/definitions/severity",
253
+ "description": "Require exactly one h1 element per document",
254
+ "default": "error"
255
+ },
256
+ "summary-first-child": {
257
+ "$ref": "#/definitions/severity",
258
+ "description": "Require summary to be the first child of details",
259
+ "default": "error"
260
+ },
261
+ "table-row-column-alignment": {
262
+ "$ref": "#/definitions/severity",
263
+ "description": "Require consistent column counts in table rows",
264
+ "default": "warning"
265
+ },
266
+ "th-content-restrictions": {
267
+ "$ref": "#/definitions/severity",
268
+ "description": "Disallow header, footer, sectioning, and heading elements inside th",
269
+ "default": "error"
270
+ },
271
+ "unique-main": {
272
+ "$ref": "#/definitions/severity",
273
+ "description": "Require at most one visible main element per document",
274
+ "default": "error"
275
+ },
276
+ "use-list": {
277
+ "$ref": "#/definitions/severity",
278
+ "description": "Suggest using list elements for bullet-like text patterns",
279
+ "default": "warning"
280
+ },
281
+ "valid-attr-value": {
282
+ "$ref": "#/definitions/severity",
283
+ "description": "Validate enumerated attribute values",
284
+ "default": "error"
285
+ },
286
+ "valid-autocomplete": {
287
+ "$ref": "#/definitions/severity",
288
+ "description": "Validate autocomplete attribute values on form elements",
289
+ "default": "warning"
290
+ },
291
+ "valid-rel": {
292
+ "$ref": "#/definitions/severity",
293
+ "description": "Validate rel attribute values for a, area, link, and form elements",
294
+ "default": "error"
295
+ },
296
+ "wai-aria": {
297
+ "$ref": "#/definitions/severity",
298
+ "description": "Validate WAI-ARIA roles and attributes",
299
+ "default": "error"
300
+ }
301
+ },
302
+ "additionalProperties": {
303
+ "$ref": "#/definitions/severity"
304
+ }
305
+ },
306
+ "ignore": {
307
+ "type": "array",
308
+ "description": "Glob patterns for files to exclude from linting",
309
+ "items": {
310
+ "type": "string"
311
+ }
312
+ },
313
+ "parser": {
314
+ "type": "object",
315
+ "description": "File extension to parser package mapping (e.g. { \".vue\": \"@rintenki/vue-parser\" })",
316
+ "additionalProperties": {
317
+ "type": "string"
318
+ }
319
+ }
320
+ },
321
+ "additionalProperties": false,
322
+ "definitions": {
323
+ "severity": {
324
+ "oneOf": [
325
+ {
326
+ "type": "string",
327
+ "enum": ["error", "warning", "warn", "off"]
328
+ },
329
+ {
330
+ "type": "boolean"
331
+ }
332
+ ],
333
+ "description": "Rule severity: \"error\", \"warning\"/\"warn\", \"off\", true (default severity), or false (off)"
334
+ }
335
+ }
336
+ }