praxis-kit 0.1.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 (45) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +77 -0
  3. package/dist/_shared/diagnostics.d.ts +312 -0
  4. package/dist/_shared/diagnostics.js +360 -0
  5. package/dist/build-runtime-CJ_nQEaZ.js +5065 -0
  6. package/dist/codemod/index.d.ts +2 -0
  7. package/dist/codemod/index.js +176520 -0
  8. package/dist/contract/index.d.ts +677 -0
  9. package/dist/contract/index.js +341 -0
  10. package/dist/eslint/index.d.ts +90 -0
  11. package/dist/eslint/index.js +1047 -0
  12. package/dist/guards/index.d.ts +78 -0
  13. package/dist/guards/index.js +118 -0
  14. package/dist/html/index.d.ts +151 -0
  15. package/dist/html/index.js +1244 -0
  16. package/dist/index-BIBd_iPD.d.ts +951 -0
  17. package/dist/lit/index.d.ts +862 -0
  18. package/dist/lit/index.js +4893 -0
  19. package/dist/preact/index.d.ts +796 -0
  20. package/dist/preact/index.js +5043 -0
  21. package/dist/react/index.d.ts +28 -0
  22. package/dist/react/index.js +205 -0
  23. package/dist/react/legacy.d.ts +29 -0
  24. package/dist/react/legacy.js +80 -0
  25. package/dist/solid/index.d.ts +728 -0
  26. package/dist/solid/index.js +4821 -0
  27. package/dist/svelte/Polymorphic.svelte +190 -0
  28. package/dist/svelte/_polymorphic-runtime.d.ts +102 -0
  29. package/dist/svelte/_polymorphic-runtime.js +371 -0
  30. package/dist/svelte/index.d.ts +994 -0
  31. package/dist/svelte/index.js +4482 -0
  32. package/dist/tailwind/index.d.ts +197 -0
  33. package/dist/tailwind/index.js +767 -0
  34. package/dist/tailwind/safelist.css +20 -0
  35. package/dist/ts-plugin/index.cjs +166 -0
  36. package/dist/ts-plugin/index.d.cts +9 -0
  37. package/dist/utils/index.d.ts +19 -0
  38. package/dist/utils/index.js +21 -0
  39. package/dist/vite-plugin/index.d.ts +200 -0
  40. package/dist/vite-plugin/index.js +2106 -0
  41. package/dist/vue/index.d.ts +729 -0
  42. package/dist/vue/index.js +4945 -0
  43. package/dist/web/index.d.ts +832 -0
  44. package/dist/web/index.js +4868 -0
  45. package/package.json +258 -0
@@ -0,0 +1,1244 @@
1
+ import { DiagnosticCategory, DiagnosticCode } from "../_shared/diagnostics.js";
2
+ //#region ../../lib/foundation/src/type-guards.ts
3
+ function isString(value) {
4
+ return typeof value === "string";
5
+ }
6
+ function isNumber(value) {
7
+ return typeof value === "number";
8
+ }
9
+ function isDefined(value) {
10
+ return value !== void 0;
11
+ }
12
+ function isUndefined(value) {
13
+ return value === void 0;
14
+ }
15
+ function isNull(value) {
16
+ return value === null;
17
+ }
18
+ function isNonNull(value) {
19
+ return value != null;
20
+ }
21
+ function isNullish(value) {
22
+ return isNull(value) || isUndefined(value);
23
+ }
24
+ //#endregion
25
+ //#region ../../lib/contract/src/diagnostics/anchor-accessibility.ts
26
+ function createDiagnostic(input) {
27
+ return {
28
+ category: DiagnosticCategory.Accessibility,
29
+ ...input
30
+ };
31
+ }
32
+ const AnchorAccessibilityDiagnostics = {
33
+ roleButtonWithHref() {
34
+ return createDiagnostic({
35
+ code: DiagnosticCode.A11yAnchorRoleButtonWithHref,
36
+ severity: "warning",
37
+ message: "role=\"button\" on an <a> with an href overrides its navigation semantics for assistive technology; use a real <button> element, or remove href, if this element should not navigate.",
38
+ rationale: "Assistive technology announces role=\"button\" as a button, not a link — but the element still follows the link when activated by mouse or keyboard, which is confusing and inconsistent with how a button is expected to behave.",
39
+ suggestions: [{
40
+ title: "Use a real <button>",
41
+ description: "If this element should not navigate, render a <button> instead of an <a>."
42
+ }, {
43
+ title: "Remove role=\"button\"",
44
+ description: "If this element should navigate, keep the default link role."
45
+ }]
46
+ });
47
+ },
48
+ ariaDisabledInert() {
49
+ return createDiagnostic({
50
+ code: DiagnosticCode.A11yAnchorAriaDisabledInert,
51
+ severity: "warning",
52
+ message: "aria-disabled does not prevent an <a> from being focused, clicked, or navigated via keyboard; remove href, or prevent navigation yourself, if this link should be inert.",
53
+ rationale: "Unlike a native form control, an <a> has no disabled state the browser enforces — aria-disabled only changes what assistive technology announces, not what actually happens when the link is activated by mouse or keyboard.",
54
+ suggestions: [{
55
+ title: "Remove href",
56
+ description: "If this element is temporarily unavailable, remove href until it can be activated."
57
+ }, {
58
+ title: "Prevent navigation when disabled",
59
+ description: "Prevent the click and keyboard activation when the link is disabled so it behaves consistently for all users."
60
+ }]
61
+ });
62
+ }
63
+ };
64
+ //#endregion
65
+ //#region ../../lib/contract/src/diagnostics/aria.ts
66
+ const AriaDiagnostics = {
67
+ /** Generic bridge for violations produced by external AriaRule functions. */
68
+ fromViolation(v) {
69
+ return {
70
+ code: DiagnosticCode.AriaViolation,
71
+ category: DiagnosticCategory.ARIA,
72
+ message: v.message
73
+ };
74
+ },
75
+ attributeInvalid(key, role) {
76
+ return {
77
+ code: DiagnosticCode.AriaAttributeInvalid,
78
+ category: DiagnosticCategory.ARIA,
79
+ message: `"${key}" is not valid on role="${role}". It will be removed.`,
80
+ rationale: "Invalid ARIA attributes are ignored by assistive technology and may trigger accessibility-tree warnings in browser devtools.",
81
+ suggestions: [{
82
+ title: "Remove the attribute",
83
+ description: `"${key}" is not in the allowed attribute set for role="${role}".`
84
+ }]
85
+ };
86
+ },
87
+ missingLiveRegion(role, impliedLive) {
88
+ return {
89
+ code: DiagnosticCode.AriaMissingLiveRegion,
90
+ category: DiagnosticCategory.ARIA,
91
+ message: `role="${role}" implies aria-live="${impliedLive}" but it is missing. It has been injected.`,
92
+ rationale: "Live-region roles announce dynamic content changes to screen readers. Without aria-live the politeness level is unspecified and announcements may be silent.",
93
+ suggestions: [{
94
+ title: `Add aria-live="${impliedLive}"`,
95
+ description: `role="${role}" conventionally implies aria-live="${impliedLive}".`,
96
+ fix: `aria-live="${impliedLive}"`
97
+ }]
98
+ };
99
+ },
100
+ missingAtomic(role) {
101
+ return {
102
+ code: DiagnosticCode.AriaMissingAtomic,
103
+ category: DiagnosticCategory.ARIA,
104
+ message: `role="${role}" is a live region. Consider setting aria-atomic="true" if the full region should be announced as a unit, or aria-atomic="false" if only changed nodes should be read.`,
105
+ rationale: "aria-atomic controls whether assistive technology announces the entire live region or only the changed nodes. Omitting it leaves the behaviour browser-defined."
106
+ };
107
+ },
108
+ relevantInvalidTokens(invalid) {
109
+ const quoted = invalid.map((t) => `"${t}"`).join(", ");
110
+ return {
111
+ code: DiagnosticCode.AriaRelevantInvalidToken,
112
+ category: DiagnosticCategory.ARIA,
113
+ message: `aria-relevant contains invalid token(s): ${quoted}. Valid tokens are: additions, removals, text, all.`,
114
+ rationale: "aria-relevant accepts a space-separated list of change types. Unrecognised tokens are silently ignored by assistive technology, making the attribute ineffective.",
115
+ suggestions: [{
116
+ title: "Use only valid tokens",
117
+ description: "Valid values are: additions, removals, text, all (or a space-separated combination)."
118
+ }]
119
+ };
120
+ },
121
+ relevantSuperseded() {
122
+ return {
123
+ code: DiagnosticCode.AriaRelevantSuperseded,
124
+ category: DiagnosticCategory.ARIA,
125
+ message: "aria-relevant includes \"all\" alongside other tokens. \"all\" supersedes additions, removals, and text — use aria-relevant=\"all\" alone.",
126
+ rationale: "\"all\" is equivalent to \"additions removals text\". Combining it with other tokens is redundant and may confuse readers of the markup.",
127
+ suggestions: [{
128
+ title: "Use aria-relevant=\"all\"",
129
+ fix: "aria-relevant=\"all\""
130
+ }]
131
+ };
132
+ },
133
+ missingAccessibleName(tag) {
134
+ return {
135
+ code: DiagnosticCode.AriaMissingAccessibleName,
136
+ category: DiagnosticCategory.ARIA,
137
+ message: `<${tag}> has no accessible name. Add aria-label or aria-labelledby.`,
138
+ rationale: "Elements with a landmark or interactive role must have an accessible name so that assistive technology can identify them when presenting the page outline.",
139
+ suggestions: [{
140
+ title: "Add aria-label",
141
+ description: `Add aria-label="…" directly to the <${tag}> element.`
142
+ }, {
143
+ title: "Add aria-labelledby",
144
+ description: "Point aria-labelledby at the id of an existing heading or label element."
145
+ }]
146
+ };
147
+ },
148
+ nameProhibited(attr, role) {
149
+ return {
150
+ code: DiagnosticCode.AriaNameProhibited,
151
+ category: DiagnosticCategory.ARIA,
152
+ message: `"${attr}" is prohibited on role="${role}" — this role does not support a name from the author. It will be removed.`,
153
+ rationale: "WAI-ARIA 1.2 lists a set of roles (generic, presentation/none, and the inline text-level roles) as Name Prohibited: aria-label / aria-labelledby on them are a conformance error and are ignored by assistive technology.",
154
+ suggestions: [{
155
+ title: "Remove the attribute",
156
+ description: `role="${role}" cannot be named. If the element needs an accessible name, give it a role that supports one (or use visible text / a wrapping labelled element).`
157
+ }]
158
+ };
159
+ },
160
+ attributeOnPresentational(attr, tag) {
161
+ return {
162
+ code: DiagnosticCode.AriaAttributeOnPresentational,
163
+ category: DiagnosticCategory.ARIA,
164
+ message: `"${attr}" is not allowed on a presentational <${tag}>. Presentational elements are invisible to assistive technology.`,
165
+ rationale: "role=\"none\" and role=\"presentation\" (including <img alt=\"\">) remove an element from the accessibility tree. ARIA attributes on such elements are ignored by assistive technology.",
166
+ suggestions: [{
167
+ title: "Remove the attribute",
168
+ description: `"${attr}" has no effect when the element has role="none" or role="presentation".`
169
+ }]
170
+ };
171
+ },
172
+ ariaHiddenOnFocusable(tag) {
173
+ return {
174
+ code: DiagnosticCode.AriaHiddenOnFocusable,
175
+ category: DiagnosticCategory.ARIA,
176
+ message: `aria-hidden="true" must not be used on focusable <${tag}> elements. Screen reader users who navigate by keyboard will encounter the element but receive no information about it.`,
177
+ rationale: "aria-hidden removes an element from the accessibility tree while leaving it keyboard-reachable. This creates a \"ghost\" — a focusable element assistive technology cannot describe.",
178
+ suggestions: [{
179
+ title: "Remove aria-hidden",
180
+ description: "If the element should be hidden from all users, use the HTML hidden attribute or CSS display:none instead."
181
+ }, {
182
+ title: "Make the element non-focusable",
183
+ description: "If the element is intentionally decorative, add tabindex=\"-1\" and disable it so it is not reachable by keyboard."
184
+ }]
185
+ };
186
+ },
187
+ invalidAttributeValue(attr, value, expected) {
188
+ const got = value === null ? "null" : value === void 0 ? "undefined" : typeof value === "string" ? `"${value}"` : String(value);
189
+ return {
190
+ code: DiagnosticCode.AriaInvalidAttributeValue,
191
+ category: DiagnosticCategory.ARIA,
192
+ message: `"${attr}" has an invalid value (${got}). Expected: ${expected}.`,
193
+ rationale: "ARIA attributes with invalid values are silently ignored by assistive technology, making the markup semantically inert.",
194
+ suggestions: [{
195
+ title: `Use a valid value for ${attr}`,
196
+ description: `Valid values are: ${expected}.`
197
+ }]
198
+ };
199
+ },
200
+ redundantAriaLevel(tag, level) {
201
+ return {
202
+ code: DiagnosticCode.AriaRedundantLevelAttribute,
203
+ category: DiagnosticCategory.ARIA,
204
+ message: `aria-level="${level}" is redundant on <${tag}>: the element already has an implicit heading level of ${level}. Remove the attribute.`,
205
+ rationale: "Restating the implicit aria-level adds noise without semantic value. Use aria-level only to override the native heading level (e.g. aria-level=\"3\" on <h2>).",
206
+ suggestions: [{
207
+ title: "Remove aria-level",
208
+ description: `<${tag}> already implies aria-level="${level}".`
209
+ }]
210
+ };
211
+ },
212
+ requiredProperty(attr, role) {
213
+ return {
214
+ code: DiagnosticCode.AriaRequiredProperty,
215
+ category: DiagnosticCategory.ARIA,
216
+ message: `"${attr}" is required for role="${role}" but is missing.`,
217
+ rationale: `WAI-ARIA 1.2 specifies required states and properties for certain roles. Without "${attr}", assistive technology cannot correctly communicate the element's state to users.`,
218
+ suggestions: [{
219
+ title: `Add ${attr}`,
220
+ description: `role="${role}" requires "${attr}" to be present.`
221
+ }]
222
+ };
223
+ },
224
+ invalidRole(role, tag) {
225
+ return {
226
+ code: DiagnosticCode.AriaInvalidRole,
227
+ category: DiagnosticCategory.ARIA,
228
+ message: `Invalid role "${role ?? ""}" on <${tag}>.`,
229
+ rationale: "An unrecognised or misapplied ARIA role is ignored by assistive technology and may degrade the accessibility of the element."
230
+ };
231
+ }
232
+ };
233
+ //#endregion
234
+ //#region ../../lib/contract/src/diagnostics/html.ts
235
+ const ATTRIBUTE_IGNORED_CODES = {
236
+ checked: DiagnosticCode.HtmlInputCheckedIgnoredForType,
237
+ multiple: DiagnosticCode.HtmlInputMultipleIgnoredForType,
238
+ maxLength: DiagnosticCode.HtmlInputMaxLengthIgnoredForType,
239
+ minLength: DiagnosticCode.HtmlInputMinLengthIgnoredForType,
240
+ pattern: DiagnosticCode.HtmlInputPatternIgnoredForType,
241
+ min: DiagnosticCode.HtmlInputMinIgnoredForType,
242
+ max: DiagnosticCode.HtmlInputMaxIgnoredForType,
243
+ step: DiagnosticCode.HtmlInputStepIgnoredForType,
244
+ accept: DiagnosticCode.HtmlInputAcceptIgnoredForType,
245
+ capture: DiagnosticCode.HtmlInputCaptureIgnoredForType,
246
+ size: DiagnosticCode.HtmlInputSizeIgnoredForType,
247
+ alt: DiagnosticCode.HtmlInputAltIgnoredForType,
248
+ height: DiagnosticCode.HtmlInputHeightIgnoredForType,
249
+ width: DiagnosticCode.HtmlInputWidthIgnoredForType
250
+ };
251
+ const HtmlDiagnostics = {
252
+ emptyRole(tag) {
253
+ return {
254
+ code: DiagnosticCode.HtmlEmptyRole,
255
+ category: DiagnosticCategory.HTML,
256
+ severity: "warning",
257
+ message: `<${tag}> has an explicit empty role="". Omit the attribute instead.`
258
+ };
259
+ },
260
+ implicitRoleRedundant(tag, implicitRole) {
261
+ return {
262
+ code: DiagnosticCode.HtmlImplicitRoleRedundant,
263
+ category: DiagnosticCategory.HTML,
264
+ severity: "warning",
265
+ message: `<${tag}> already has implicit role="${implicitRole}". Avoid redundant role assignment.`
266
+ };
267
+ },
268
+ implicitRoleOverride(tag, implicitRole, role) {
269
+ return {
270
+ code: DiagnosticCode.HtmlImplicitRoleOverride,
271
+ category: DiagnosticCategory.HTML,
272
+ severity: "error",
273
+ message: `<${tag}> should not override its implicit role="${implicitRole}" with role="${role}".`
274
+ };
275
+ },
276
+ standaloneRegionOverride(tag, implicitRole) {
277
+ return {
278
+ code: DiagnosticCode.HtmlStandaloneRegionOverride,
279
+ category: DiagnosticCategory.HTML,
280
+ severity: "error",
281
+ message: `<${tag}> is a self-contained element with implicit role="${implicitRole}". Assigning role="region" has been removed.`
282
+ };
283
+ },
284
+ landmarkRoleOverride(tag, implicitRole, role) {
285
+ return {
286
+ code: DiagnosticCode.HtmlLandmarkRoleOverride,
287
+ category: DiagnosticCategory.HTML,
288
+ severity: "error",
289
+ message: `<${tag}> has a fixed landmark role="${implicitRole}". role="${role}" overrides it and confuses assistive technology. The override has been removed.`
290
+ };
291
+ },
292
+ invalidChild(child, parent, allowed) {
293
+ return {
294
+ code: DiagnosticCode.HtmlInvalidChild,
295
+ category: DiagnosticCategory.HTML,
296
+ severity: "error",
297
+ message: `<${child}> is not a valid direct child of <${parent}>. Allowed: ${allowed}.`
298
+ };
299
+ },
300
+ roleNotPermitted(tag, role, allowedRoles) {
301
+ const allowed = allowedRoles.length > 0 ? allowedRoles.map((r) => `"${r}"`).join(", ") : "none — no explicit role is permitted on this element";
302
+ return {
303
+ code: DiagnosticCode.HtmlRoleNotPermitted,
304
+ category: DiagnosticCategory.HTML,
305
+ severity: "error",
306
+ message: `role="${role}" is not permitted on <${tag}>. Allowed alternate role(s): ${allowed}.`,
307
+ rationale: "The WAI-ARIA \"ARIA in HTML\" specification restricts which explicit roles a native element may take. A role outside that set is ignored or produces undefined behavior in assistive technology."
308
+ };
309
+ },
310
+ input: {
311
+ unsupportedType(type) {
312
+ return {
313
+ code: DiagnosticCode.HtmlInputUnsupportedType,
314
+ category: DiagnosticCategory.HTML,
315
+ severity: "warning",
316
+ message: `type="${type}" is not a value defined by the HTML specification. Browsers silently fall back to type="text".`,
317
+ rationale: "An unrecognized input type is not invalid markup — the spec requires the \"text\" fallback — but it usually means a typo, since the input keeps working while silently losing the intended type-specific behavior (validation, virtual keyboard, picker UI).",
318
+ suggestions: [{
319
+ title: "Check for a typo in the type value",
320
+ description: `"${type}" does not match any HTML5 input type.`
321
+ }]
322
+ };
323
+ },
324
+ attributeIgnoredForType(attribute, type, allowedTypes) {
325
+ const allowed = allowedTypes.map((t) => `"${t}"`).join(", ");
326
+ return {
327
+ code: ATTRIBUTE_IGNORED_CODES[attribute],
328
+ category: DiagnosticCategory.HTML,
329
+ severity: "warning",
330
+ message: `"${attribute}" is ignored on <input type="${type}">.`,
331
+ rationale: `"${attribute}" only has an effect when type is one of: ${allowed}. Browsers silently ignore it on other input types.`,
332
+ suggestions: [{
333
+ title: `Remove "${attribute}"`,
334
+ description: `"${attribute}" only affects <input> when type is one of: ${allowed}.`
335
+ }]
336
+ };
337
+ }
338
+ },
339
+ anchor: { dangerousHref(href) {
340
+ return {
341
+ code: DiagnosticCode.HtmlAnchorDangerousHref,
342
+ category: DiagnosticCategory.HTML,
343
+ severity: "warning",
344
+ message: `href="${href}" uses a scheme that executes attacker-controlled content when navigated to. It has been removed.`,
345
+ rationale: "\"javascript:\", \"data:\", and \"vbscript:\" URLs are a common XSS vector when href comes from untrusted input — none of the three are needed for legitimate navigation."
346
+ };
347
+ } }
348
+ };
349
+ //#endregion
350
+ //#region ../../lib/contract/src/diagnostics/input-accessibility.ts
351
+ function accessibilityFact(input) {
352
+ return {
353
+ category: DiagnosticCategory.Accessibility,
354
+ ...input
355
+ };
356
+ }
357
+ const InputAccessibilityDiagnostics = {
358
+ missingAccessibleName() {
359
+ return accessibilityFact({
360
+ code: DiagnosticCode.A11yInputMissingAccessibleName,
361
+ severity: "warning",
362
+ message: "This input has no accessible name. Add an associated <label>, aria-label, or aria-labelledby.",
363
+ rationale: "Assistive technology announces a form field by its accessible name; without one, users of screen readers cannot tell what the field is for.",
364
+ suggestions: [{
365
+ title: "Add aria-label",
366
+ description: "Set aria-label=\"…\" directly on the input."
367
+ }, {
368
+ title: "Add an associated <label>",
369
+ description: "Wrap the input in a <label>, or point a <label for=\"…\"> at its id."
370
+ }]
371
+ });
372
+ },
373
+ placeholderIsNotLabel() {
374
+ return accessibilityFact({
375
+ code: DiagnosticCode.A11yInputPlaceholderNotLabel,
376
+ severity: "warning",
377
+ message: "Placeholder text does not provide an accessible name. Add an associated <label>, aria-label, or aria-labelledby.",
378
+ rationale: "Placeholder text disappears as users interact with the field and is not treated as the control's accessible name by many assistive technologies.",
379
+ suggestions: [{
380
+ title: "Add aria-label",
381
+ description: "Set aria-label=\"…\" directly on the input."
382
+ }, {
383
+ title: "Add an associated <label>",
384
+ description: "Wrap the input in a <label>, or point a <label for=\"…\"> at its id."
385
+ }]
386
+ });
387
+ },
388
+ passwordMissingAutocomplete() {
389
+ return accessibilityFact({
390
+ code: DiagnosticCode.A11yInputPasswordAutocomplete,
391
+ severity: "warning",
392
+ message: "Password inputs should specify an autoComplete value.",
393
+ rationale: "Without an explicit autocomplete hint, password managers and browsers cannot reliably tell a sign-in field apart from a password-creation field.",
394
+ suggestions: [{
395
+ title: "Set autoComplete=\"current-password\"",
396
+ description: "Use this for sign-in forms."
397
+ }, {
398
+ title: "Set autoComplete=\"new-password\"",
399
+ description: "Use this for sign-up / change-password forms."
400
+ }]
401
+ });
402
+ },
403
+ requiredReadOnlyConflict() {
404
+ return accessibilityFact({
405
+ code: DiagnosticCode.A11yInputRequiredReadOnlyConflict,
406
+ severity: "warning",
407
+ message: "\"required\" has no effect while this field is readOnly — the two attributes together usually signal an unintended state.",
408
+ rationale: "The combination is valid HTML, but a readOnly control cannot be edited, so its \"required\" constraint can never be satisfied or violated through user interaction. Consider using disabled instead of readOnly, or only applying required when the field is editable."
409
+ });
410
+ }
411
+ };
412
+ //#endregion
413
+ //#region ../../lib/contract/src/aria/factories.ts
414
+ /**
415
+ * Builds a correctly-literal-typed `fixable: false` `AriaResult`. Exists so a rule author can
416
+ * extract shared branch logic (severity/attribute/message computed once, reused across multiple
417
+ * `return`s) without TypeScript silently widening `valid: false`/`fixable: false` to `boolean`
418
+ * the moment those values leave an object-literal-in-return-position — the widening only happens
419
+ * on plain object literals; a function's declared return type narrows unconditionally.
420
+ */
421
+ function invalidWithoutFix(input) {
422
+ return {
423
+ valid: false,
424
+ fixable: false,
425
+ severity: input.severity,
426
+ ...isDefined(input.attribute) && { attribute: input.attribute },
427
+ ...isDefined(input.message) && { message: input.message },
428
+ ...isDefined(input.diagnostic) && { diagnostic: input.diagnostic }
429
+ };
430
+ }
431
+ /** Same as {@link invalidWithoutFix}, for the `fixable: true` branch — requires a `fix`. */
432
+ function invalidWithFix(input) {
433
+ return {
434
+ valid: false,
435
+ fixable: true,
436
+ severity: input.severity,
437
+ ...isDefined(input.attribute) && { attribute: input.attribute },
438
+ ...isDefined(input.message) && { message: input.message },
439
+ ...isDefined(input.diagnostic) && { diagnostic: input.diagnostic },
440
+ fix: input.fix
441
+ };
442
+ }
443
+ function removeProp(props, key) {
444
+ const next = { ...props };
445
+ delete next[key];
446
+ return next;
447
+ }
448
+ /**
449
+ * Builds an `AriaFix` that strips a single attribute — the shape `dangerousHrefRule`-style
450
+ * "strip this attribute when it's dangerous/redundant" rules need. A no-op (`applied: false`) when
451
+ * the attribute isn't present, so applying the fix twice (or applying it when nothing triggered it)
452
+ * is always safe. Frozen — a fix is a value object; nothing should mutate `kind`/`attribute`/`apply`
453
+ * after construction.
454
+ */
455
+ function removeAttributeFix(attribute) {
456
+ return Object.freeze({
457
+ kind: "removeAttribute",
458
+ attribute,
459
+ apply: ({ props }) => {
460
+ if (!(attribute in props)) return {
461
+ applied: false,
462
+ next: props
463
+ };
464
+ return {
465
+ applied: true,
466
+ next: removeProp(props, attribute),
467
+ previous: props
468
+ };
469
+ }
470
+ });
471
+ }
472
+ function defineRuleMetadata(rule, metadata) {
473
+ const descriptors = {};
474
+ for (const key of Object.keys(metadata)) descriptors[key] = {
475
+ value: metadata[key],
476
+ enumerable: false
477
+ };
478
+ Object.defineProperties(rule, descriptors);
479
+ return rule;
480
+ }
481
+ /**
482
+ * Convenience factory for the single most common `enforcement.aria`/`enforcement.rules` shape:
483
+ * "strip this attribute when some condition on the element's own props holds" — covers
484
+ * security-style guards (a dangerous URL scheme on `href`) and redundant-attribute rules alike,
485
+ * without hand-writing the rule function, the `AriaFix`, and the `invalidWithFix` call each time.
486
+ * A rule with no fix (a warn-only advisory) still needs the raw `AriaRule` shape directly — this
487
+ * factory is deliberately scoped to the strip-on-match case, not a general rule builder.
488
+ */
489
+ function createRemoveAttributeRule(attribute, options) {
490
+ const { when, severity = "warning", message, diagnostic, readsProps, tags } = options;
491
+ const fix = removeAttributeFix(attribute);
492
+ const rule = (context) => {
493
+ if (!when(context)) return [];
494
+ return [invalidWithFix({
495
+ severity,
496
+ attribute,
497
+ ...isDefined(message) && { message },
498
+ ...isDefined(diagnostic) && { diagnostic: diagnostic(context) },
499
+ fix
500
+ })];
501
+ };
502
+ return defineRuleMetadata(rule, {
503
+ ...isDefined(readsProps) && { readsProps },
504
+ ...isDefined(tags) && { tags }
505
+ });
506
+ }
507
+ //#endregion
508
+ //#region ../core/src/html/contracts/categories.ts
509
+ /**
510
+ * HTML elements with implicit landmark roles.
511
+ *
512
+ * `<section>` and `<form>` are intentionally excluded because their landmark
513
+ * semantics depend on having an accessible name.
514
+ */
515
+ const LANDMARK_TAGS = [
516
+ "article",
517
+ "aside",
518
+ "footer",
519
+ "header",
520
+ "main",
521
+ "nav"
522
+ ];
523
+ //#endregion
524
+ //#region ../core/src/html/anchor-rules.ts
525
+ const DANGEROUS_URL_SCHEMES = [
526
+ "javascript:",
527
+ "data:",
528
+ "vbscript:"
529
+ ];
530
+ function normalizeForSchemeCheck(href) {
531
+ return href.replace(/[\x00-\x20]/g, "").toLowerCase();
532
+ }
533
+ function isDangerousUrl(href) {
534
+ if (!isString(href)) return false;
535
+ const normalized = normalizeForSchemeCheck(href);
536
+ return DANGEROUS_URL_SCHEMES.some((scheme) => normalized.startsWith(scheme));
537
+ }
538
+ const dangerousHrefRule = createRemoveAttributeRule("href", {
539
+ when: ({ props }) => isDangerousUrl(props.href),
540
+ severity: "warning",
541
+ diagnostic: ({ props }) => HtmlDiagnostics.anchor.dangerousHref(props.href),
542
+ readsProps: ["href"],
543
+ tags: ["a"]
544
+ });
545
+ const roleButtonWithHrefRule = Object.assign(({ props }) => {
546
+ if (props.role !== "button" || !isString(props.href) || props.href.length === 0) return [];
547
+ const diagnostic = AnchorAccessibilityDiagnostics.roleButtonWithHref();
548
+ return [invalidWithoutFix({
549
+ severity: diagnostic.severity,
550
+ attribute: "role",
551
+ diagnostic
552
+ })];
553
+ }, {
554
+ readsProps: ["role", "href"],
555
+ tags: ["a"]
556
+ });
557
+ const ariaDisabledInertRule = Object.assign(({ props }) => {
558
+ const ariaDisabled = props["aria-disabled"];
559
+ const isDisabled = ariaDisabled === true || ariaDisabled === "true";
560
+ const hasHref = isString(props.href) && props.href.length > 0;
561
+ if (!isDisabled || !hasHref) return [];
562
+ const diagnostic = AnchorAccessibilityDiagnostics.ariaDisabledInert();
563
+ return [invalidWithoutFix({
564
+ severity: diagnostic.severity,
565
+ attribute: "aria-disabled",
566
+ diagnostic
567
+ })];
568
+ }, {
569
+ readsProps: ["aria-disabled", "href"],
570
+ tags: ["a"]
571
+ });
572
+ const ANCHOR_RULES = [
573
+ dangerousHrefRule,
574
+ roleButtonWithHrefRule,
575
+ ariaDisabledInertRule
576
+ ];
577
+ //#endregion
578
+ //#region ../core/src/html/spec/vocabulary/input.ts
579
+ const TEXT_INPUT_TYPES = [
580
+ "text",
581
+ "search",
582
+ "url",
583
+ "tel",
584
+ "email",
585
+ "password"
586
+ ];
587
+ const NUMERIC_INPUT_TYPES = [
588
+ "number",
589
+ "range",
590
+ "date",
591
+ "month",
592
+ "week",
593
+ "time",
594
+ "datetime-local"
595
+ ];
596
+ const HTML_INPUT_TYPES = /* @__PURE__ */ new Set([
597
+ ...TEXT_INPUT_TYPES,
598
+ ...NUMERIC_INPUT_TYPES,
599
+ "checkbox",
600
+ "radio",
601
+ "file",
602
+ "color",
603
+ "hidden",
604
+ "button",
605
+ "submit",
606
+ "reset",
607
+ "image"
608
+ ]);
609
+ //#endregion
610
+ //#region ../core/src/html/spec/attributes/input.ts
611
+ const INPUT_ATTRIBUTE_TYPE_POLICIES = [
612
+ {
613
+ attribute: "checked",
614
+ allowedTypes: ["checkbox", "radio"]
615
+ },
616
+ {
617
+ attribute: "multiple",
618
+ allowedTypes: ["email", "file"]
619
+ },
620
+ {
621
+ attribute: "maxLength",
622
+ allowedTypes: TEXT_INPUT_TYPES
623
+ },
624
+ {
625
+ attribute: "minLength",
626
+ allowedTypes: TEXT_INPUT_TYPES
627
+ },
628
+ {
629
+ attribute: "pattern",
630
+ allowedTypes: TEXT_INPUT_TYPES
631
+ },
632
+ {
633
+ attribute: "min",
634
+ allowedTypes: NUMERIC_INPUT_TYPES
635
+ },
636
+ {
637
+ attribute: "max",
638
+ allowedTypes: NUMERIC_INPUT_TYPES
639
+ },
640
+ {
641
+ attribute: "step",
642
+ allowedTypes: NUMERIC_INPUT_TYPES
643
+ },
644
+ {
645
+ attribute: "accept",
646
+ allowedTypes: ["file"]
647
+ },
648
+ {
649
+ attribute: "capture",
650
+ allowedTypes: ["file"]
651
+ },
652
+ {
653
+ attribute: "size",
654
+ allowedTypes: TEXT_INPUT_TYPES
655
+ },
656
+ {
657
+ attribute: "alt",
658
+ allowedTypes: ["image"]
659
+ },
660
+ {
661
+ attribute: "height",
662
+ allowedTypes: ["image"]
663
+ },
664
+ {
665
+ attribute: "width",
666
+ allowedTypes: ["image"]
667
+ }
668
+ ];
669
+ //#endregion
670
+ //#region ../core/src/html/spec/constraints/input.ts
671
+ const REQUIRED_READONLY_CONFLICT = {
672
+ props: ["required", "readOnly"],
673
+ diagnostic: () => InputAccessibilityDiagnostics.requiredReadOnlyConflict()
674
+ };
675
+ const INPUT_MUTUALLY_EXCLUSIVE_POLICIES = [REQUIRED_READONLY_CONFLICT];
676
+ //#endregion
677
+ //#region ../core/src/html/spec/validators/attribute-type-validator.ts
678
+ const DEFAULT_INPUT_TYPE = "text";
679
+ function createInputAttributeTypeRule({ attribute, allowedTypes }) {
680
+ const rule = ({ tag, props, variantKeys }) => {
681
+ if (tag !== "input" || !(attribute in props)) return [];
682
+ if (variantKeys?.has(attribute)) return [];
683
+ const type = typeof props.type === "string" ? props.type : DEFAULT_INPUT_TYPE;
684
+ if (allowedTypes.includes(type)) return [];
685
+ const diagnostic = HtmlDiagnostics.input.attributeIgnoredForType(attribute, type, allowedTypes);
686
+ return [{
687
+ valid: false,
688
+ fixable: true,
689
+ severity: diagnostic.severity,
690
+ fix: removeAttributeFix(attribute),
691
+ diagnostic
692
+ }];
693
+ };
694
+ return Object.assign(rule, {
695
+ readsProps: ["type", attribute],
696
+ tags: ["input"]
697
+ });
698
+ }
699
+ //#endregion
700
+ //#region ../core/src/html/spec/validators/mutually-exclusive-validator.ts
701
+ function isBooleanAttrSet(value) {
702
+ return value !== void 0 && value !== null && value !== false;
703
+ }
704
+ function createMutuallyExclusiveRule({ props: conflictingProps, diagnostic: createDiagnostic }) {
705
+ const [first, second] = conflictingProps;
706
+ const rule = ({ tag, props }) => {
707
+ if (tag !== "input" || !isBooleanAttrSet(props[first]) || !isBooleanAttrSet(props[second])) return [];
708
+ const diagnostic = createDiagnostic();
709
+ return [{
710
+ valid: false,
711
+ fixable: false,
712
+ severity: diagnostic.severity,
713
+ diagnostic
714
+ }];
715
+ };
716
+ return Object.assign(rule, {
717
+ readsProps: conflictingProps,
718
+ tags: ["input"]
719
+ });
720
+ }
721
+ //#endregion
722
+ //#region ../core/src/html/input-rules.ts
723
+ const policyByAttribute = Object.fromEntries(INPUT_ATTRIBUTE_TYPE_POLICIES.map((policy) => [policy.attribute, policy]));
724
+ function policyFor(attribute) {
725
+ return policyByAttribute[attribute];
726
+ }
727
+ const supportedInputTypeRule = Object.assign(({ tag, props }) => {
728
+ if (tag !== "input" || typeof props.type !== "string") return [];
729
+ const type = props.type;
730
+ if (HTML_INPUT_TYPES.has(type)) return [];
731
+ const diagnostic = HtmlDiagnostics.input.unsupportedType(type);
732
+ return [{
733
+ valid: false,
734
+ fixable: false,
735
+ severity: diagnostic.severity,
736
+ diagnostic
737
+ }];
738
+ }, {
739
+ readsProps: ["type"],
740
+ tags: ["input"]
741
+ });
742
+ const checkedRequiresCheckableTypeRule = createInputAttributeTypeRule(policyFor("checked"));
743
+ const multipleRequiresSupportedTypeRule = createInputAttributeTypeRule(policyFor("multiple"));
744
+ const maxLengthRequiresTextTypeRule = createInputAttributeTypeRule(policyFor("maxLength"));
745
+ const minLengthRequiresTextTypeRule = createInputAttributeTypeRule(policyFor("minLength"));
746
+ const patternRequiresTextTypeRule = createInputAttributeTypeRule(policyFor("pattern"));
747
+ const minRequiresNumericTypeRule = createInputAttributeTypeRule(policyFor("min"));
748
+ const maxRequiresNumericTypeRule = createInputAttributeTypeRule(policyFor("max"));
749
+ const stepRequiresNumericTypeRule = createInputAttributeTypeRule(policyFor("step"));
750
+ const acceptRequiresFileTypeRule = createInputAttributeTypeRule(policyFor("accept"));
751
+ const captureRequiresFileTypeRule = createInputAttributeTypeRule(policyFor("capture"));
752
+ const sizeRequiresTextTypeRule = createInputAttributeTypeRule(policyFor("size"));
753
+ const altRequiresImageTypeRule = createInputAttributeTypeRule(policyFor("alt"));
754
+ const heightRequiresImageTypeRule = createInputAttributeTypeRule(policyFor("height"));
755
+ const widthRequiresImageTypeRule = createInputAttributeTypeRule(policyFor("width"));
756
+ const inputAccessibleNameRule = Object.assign(({ tag, props }) => {
757
+ if (tag !== "input" || props.type === "hidden") return [];
758
+ if ("aria-label" in props || "aria-labelledby" in props) return [];
759
+ const diagnostic = typeof props.placeholder === "string" && props.placeholder.length > 0 ? InputAccessibilityDiagnostics.placeholderIsNotLabel() : InputAccessibilityDiagnostics.missingAccessibleName();
760
+ return [{
761
+ valid: false,
762
+ fixable: false,
763
+ severity: diagnostic.severity,
764
+ diagnostic
765
+ }];
766
+ }, {
767
+ readsProps: [
768
+ "type",
769
+ "aria-label",
770
+ "aria-labelledby",
771
+ "placeholder"
772
+ ],
773
+ tags: ["input"]
774
+ });
775
+ const PASSWORD_AUTOCOMPLETE_VALUES = ["current-password", "new-password"];
776
+ const passwordAutocompleteRule = Object.assign(({ tag, props }) => {
777
+ if (tag !== "input" || props.type !== "password") return [];
778
+ const autoComplete = props.autoComplete;
779
+ const tokens = typeof autoComplete === "string" ? autoComplete.split(" ") : [];
780
+ if (PASSWORD_AUTOCOMPLETE_VALUES.some((value) => tokens.includes(value))) return [];
781
+ const diagnostic = InputAccessibilityDiagnostics.passwordMissingAutocomplete();
782
+ return [{
783
+ valid: false,
784
+ fixable: false,
785
+ severity: diagnostic.severity,
786
+ diagnostic
787
+ }];
788
+ }, {
789
+ readsProps: ["type", "autoComplete"],
790
+ tags: ["input"]
791
+ });
792
+ const requiredReadOnlyConflictRule = createMutuallyExclusiveRule(REQUIRED_READONLY_CONFLICT);
793
+ const INPUT_RULES = [
794
+ supportedInputTypeRule,
795
+ checkedRequiresCheckableTypeRule,
796
+ multipleRequiresSupportedTypeRule,
797
+ maxLengthRequiresTextTypeRule,
798
+ minLengthRequiresTextTypeRule,
799
+ patternRequiresTextTypeRule,
800
+ minRequiresNumericTypeRule,
801
+ maxRequiresNumericTypeRule,
802
+ stepRequiresNumericTypeRule,
803
+ acceptRequiresFileTypeRule,
804
+ captureRequiresFileTypeRule,
805
+ sizeRequiresTextTypeRule,
806
+ altRequiresImageTypeRule,
807
+ heightRequiresImageTypeRule,
808
+ widthRequiresImageTypeRule,
809
+ inputAccessibleNameRule,
810
+ passwordAutocompleteRule,
811
+ requiredReadOnlyConflictRule
812
+ ];
813
+ //#endregion
814
+ //#region ../core/src/html/spec/types.ts
815
+ function resolveAllowedRoles(spec, props) {
816
+ const policy = spec.allowedRoles;
817
+ if (!policy) return void 0;
818
+ switch (policy.kind) {
819
+ case "fixed": return policy.roles;
820
+ case "byProp": {
821
+ const raw = props[policy.prop];
822
+ const value = typeof raw === "string" && raw in policy.map ? raw : policy.fallback;
823
+ return policy.map[value];
824
+ }
825
+ case "dynamic": return policy.resolve({ props });
826
+ }
827
+ }
828
+ //#endregion
829
+ //#region ../core/src/html/spec/roles/input.ts
830
+ const ALLOWED_INPUT_ROLES = {
831
+ checkbox: [
832
+ "menuitemcheckbox",
833
+ "option",
834
+ "switch",
835
+ "button"
836
+ ],
837
+ radio: ["menuitemradio"],
838
+ range: [],
839
+ number: [],
840
+ search: [],
841
+ text: [
842
+ "combobox",
843
+ "searchbox",
844
+ "spinbutton"
845
+ ],
846
+ email: [],
847
+ tel: [],
848
+ url: [],
849
+ button: [
850
+ "checkbox",
851
+ "combobox",
852
+ "gridcell",
853
+ "link",
854
+ "menuitem",
855
+ "menuitemcheckbox",
856
+ "menuitemradio",
857
+ "option",
858
+ "radio",
859
+ "separator",
860
+ "slider",
861
+ "switch",
862
+ "tab",
863
+ "treeitem"
864
+ ],
865
+ submit: [
866
+ "checkbox",
867
+ "combobox",
868
+ "gridcell",
869
+ "link",
870
+ "menuitem",
871
+ "menuitemcheckbox",
872
+ "menuitemradio",
873
+ "option",
874
+ "radio",
875
+ "separator",
876
+ "slider",
877
+ "switch",
878
+ "tab",
879
+ "treeitem"
880
+ ],
881
+ reset: [
882
+ "checkbox",
883
+ "combobox",
884
+ "gridcell",
885
+ "link",
886
+ "menuitem",
887
+ "menuitemcheckbox",
888
+ "menuitemradio",
889
+ "option",
890
+ "radio",
891
+ "separator",
892
+ "slider",
893
+ "switch",
894
+ "tab",
895
+ "treeitem"
896
+ ],
897
+ image: [
898
+ "checkbox",
899
+ "gridcell",
900
+ "link",
901
+ "menuitem",
902
+ "menuitemcheckbox",
903
+ "menuitemradio",
904
+ "option",
905
+ "radio",
906
+ "separator",
907
+ "slider",
908
+ "switch",
909
+ "tab",
910
+ "treeitem"
911
+ ],
912
+ hidden: []
913
+ };
914
+ //#endregion
915
+ //#region ../core/src/html/spec/elements/input.ts
916
+ const LIST_ELIGIBLE_TYPES = /* @__PURE__ */ new Set([
917
+ "text",
918
+ "search",
919
+ "tel",
920
+ "url",
921
+ "email"
922
+ ]);
923
+ function rolesForType(type) {
924
+ return ALLOWED_INPUT_ROLES[type in ALLOWED_INPUT_ROLES ? type : "text"];
925
+ }
926
+ const inputElementSpec = {
927
+ tag: "input",
928
+ allowedRoles: {
929
+ kind: "dynamic",
930
+ resolve: ({ props }) => {
931
+ const type = isString(props.type) ? props.type : "text";
932
+ if (isNonNull(props.list) && LIST_ELIGIBLE_TYPES.has(type)) return [];
933
+ return rolesForType(type);
934
+ }
935
+ },
936
+ attributes: INPUT_ATTRIBUTE_TYPE_POLICIES,
937
+ mutuallyExclusive: INPUT_MUTUALLY_EXCLUSIVE_POLICIES
938
+ };
939
+ //#endregion
940
+ //#region ../core/src/html/spec/roles/img.ts
941
+ const IMG_DECORATIVE_ROLES = ["none", "presentation"];
942
+ const IMG_NAMED_ROLES = [
943
+ "button",
944
+ "checkbox",
945
+ "link",
946
+ "math",
947
+ "menuitem",
948
+ "menuitemcheckbox",
949
+ "menuitemradio",
950
+ "meter",
951
+ "option",
952
+ "progressbar",
953
+ "radio",
954
+ "scrollbar",
955
+ "separator",
956
+ "slider",
957
+ "switch",
958
+ "tab",
959
+ "treeitem"
960
+ ];
961
+ //#endregion
962
+ //#region ../core/src/html/spec/elements/img.ts
963
+ const imgElementSpec = {
964
+ tag: "img",
965
+ allowedRoles: {
966
+ kind: "dynamic",
967
+ resolve: ({ props }) => {
968
+ if (isNullish(props.alt)) return IMG_DECORATIVE_ROLES;
969
+ return props.alt === "" ? [] : IMG_NAMED_ROLES;
970
+ }
971
+ }
972
+ };
973
+ //#endregion
974
+ //#region ../core/src/html/spec/roles/select.ts
975
+ const ALLOWED_SELECT_ROLES = ["menu"];
976
+ //#endregion
977
+ //#region ../core/src/html/spec/elements/select.ts
978
+ function isListBoxSelect(props) {
979
+ const { multiple, size } = props;
980
+ if (isNonNull(multiple) && multiple !== false && (!isString(multiple) || multiple.toLowerCase() !== "false")) return true;
981
+ const parsed = isNumber(size) ? size : isString(size) ? Number(size) : NaN;
982
+ return Number.isFinite(parsed) && parsed > 1;
983
+ }
984
+ const selectElementSpec = {
985
+ tag: "select",
986
+ allowedRoles: {
987
+ kind: "dynamic",
988
+ resolve: ({ props }) => isListBoxSelect(props) ? [] : ALLOWED_SELECT_ROLES
989
+ }
990
+ };
991
+ //#endregion
992
+ //#region ../core/src/html/spec/elements/table.ts
993
+ const tableElementSpec = {
994
+ tag: "table",
995
+ allowedRoles: {
996
+ kind: "fixed",
997
+ roles: ["grid", "treegrid"]
998
+ }
999
+ };
1000
+ //#endregion
1001
+ //#region ../core/src/html/role-restrictions.ts
1002
+ const ALLOWED_ROLES = {
1003
+ article: [
1004
+ "application",
1005
+ "document",
1006
+ "feed",
1007
+ "main",
1008
+ "none",
1009
+ "presentation",
1010
+ "region"
1011
+ ],
1012
+ aside: [
1013
+ "feed",
1014
+ "none",
1015
+ "note",
1016
+ "presentation",
1017
+ "region",
1018
+ "search"
1019
+ ],
1020
+ footer: [
1021
+ "group",
1022
+ "none",
1023
+ "presentation"
1024
+ ],
1025
+ header: [
1026
+ "group",
1027
+ "none",
1028
+ "presentation"
1029
+ ],
1030
+ main: [],
1031
+ nav: [
1032
+ "menu",
1033
+ "menubar",
1034
+ "none",
1035
+ "presentation",
1036
+ "tablist"
1037
+ ],
1038
+ a: [
1039
+ "button",
1040
+ "checkbox",
1041
+ "menuitem",
1042
+ "menuitemcheckbox",
1043
+ "menuitemradio",
1044
+ "option",
1045
+ "radio",
1046
+ "switch",
1047
+ "tab",
1048
+ "treeitem"
1049
+ ],
1050
+ button: [
1051
+ "checkbox",
1052
+ "combobox",
1053
+ "gridcell",
1054
+ "link",
1055
+ "menuitem",
1056
+ "menuitemcheckbox",
1057
+ "menuitemradio",
1058
+ "option",
1059
+ "radio",
1060
+ "separator",
1061
+ "slider",
1062
+ "switch",
1063
+ "tab",
1064
+ "treeitem"
1065
+ ],
1066
+ h1: [
1067
+ "tab",
1068
+ "presentation",
1069
+ "none"
1070
+ ],
1071
+ h2: [
1072
+ "tab",
1073
+ "presentation",
1074
+ "none"
1075
+ ],
1076
+ h3: [
1077
+ "tab",
1078
+ "presentation",
1079
+ "none"
1080
+ ],
1081
+ h4: [
1082
+ "tab",
1083
+ "presentation",
1084
+ "none"
1085
+ ],
1086
+ h5: [
1087
+ "tab",
1088
+ "presentation",
1089
+ "none"
1090
+ ],
1091
+ h6: [
1092
+ "tab",
1093
+ "presentation",
1094
+ "none"
1095
+ ],
1096
+ ul: [
1097
+ "group",
1098
+ "listbox",
1099
+ "menu",
1100
+ "menubar",
1101
+ "none",
1102
+ "presentation",
1103
+ "radiogroup",
1104
+ "tablist",
1105
+ "toolbar",
1106
+ "tree"
1107
+ ],
1108
+ ol: [
1109
+ "group",
1110
+ "listbox",
1111
+ "menu",
1112
+ "menubar",
1113
+ "none",
1114
+ "presentation",
1115
+ "radiogroup",
1116
+ "tablist",
1117
+ "toolbar",
1118
+ "tree"
1119
+ ],
1120
+ li: [
1121
+ "menuitem",
1122
+ "menuitemcheckbox",
1123
+ "menuitemradio",
1124
+ "option",
1125
+ "none",
1126
+ "presentation",
1127
+ "radio",
1128
+ "separator",
1129
+ "tab",
1130
+ "treeitem"
1131
+ ],
1132
+ dialog: ["alertdialog"],
1133
+ fieldset: [
1134
+ "none",
1135
+ "presentation",
1136
+ "radiogroup"
1137
+ ],
1138
+ label: []
1139
+ };
1140
+ const ELEMENT_SPECS = {
1141
+ input: inputElementSpec,
1142
+ img: imgElementSpec,
1143
+ select: selectElementSpec,
1144
+ table: tableElementSpec
1145
+ };
1146
+ function getAllowedRoles(tag, props) {
1147
+ const spec = ELEMENT_SPECS[tag];
1148
+ if (spec) return resolveAllowedRoles(spec, props);
1149
+ return ALLOWED_ROLES[tag];
1150
+ }
1151
+ const removeRoleFix = {
1152
+ kind: "removeRole",
1153
+ apply: ({ props }) => {
1154
+ if (!("role" in props)) return {
1155
+ applied: false,
1156
+ next: props
1157
+ };
1158
+ const { role: _role, ...rest } = props;
1159
+ return {
1160
+ applied: true,
1161
+ next: rest,
1162
+ previous: props
1163
+ };
1164
+ }
1165
+ };
1166
+ const roleNotPermittedRule = Object.assign(({ tag, props, implicitRole }) => {
1167
+ const role = props.role;
1168
+ if (typeof role !== "string" || role.length === 0 || role === implicitRole) return [];
1169
+ const allowed = getAllowedRoles(tag, props);
1170
+ if (allowed === void 0 || allowed.includes(role)) return [];
1171
+ const diagnostic = HtmlDiagnostics.roleNotPermitted(tag, role, allowed);
1172
+ return [{
1173
+ valid: false,
1174
+ fixable: true,
1175
+ severity: diagnostic.severity,
1176
+ fix: removeRoleFix,
1177
+ diagnostic
1178
+ }];
1179
+ }, { readsProps: [
1180
+ "role",
1181
+ "type",
1182
+ "alt",
1183
+ "list",
1184
+ "multiple",
1185
+ "size"
1186
+ ] });
1187
+ //#endregion
1188
+ //#region ../core/src/html/aria-rules.ts
1189
+ function defineAriaRule(tags, rule) {
1190
+ return Object.assign(rule, { tags });
1191
+ }
1192
+ const LANDMARK_TAG_SET = new Set(LANDMARK_TAGS);
1193
+ const removeLandmarkRoleOverride = {
1194
+ kind: "removeRole",
1195
+ apply: ({ props }) => {
1196
+ if (!("role" in props)) return {
1197
+ applied: false,
1198
+ next: props
1199
+ };
1200
+ const { role: _r, ...rest } = props;
1201
+ return {
1202
+ applied: true,
1203
+ next: rest,
1204
+ previous: props
1205
+ };
1206
+ }
1207
+ };
1208
+ const landmarkRoleRule = defineAriaRule(LANDMARK_TAGS, ({ tag, props, implicitRole }) => {
1209
+ if (!LANDMARK_TAG_SET.has(tag) || !implicitRole) return [];
1210
+ const { role } = props;
1211
+ if (!role || role === implicitRole) return [];
1212
+ const diagnostic = HtmlDiagnostics.landmarkRoleOverride(tag, implicitRole, role);
1213
+ return [{
1214
+ valid: false,
1215
+ fixable: true,
1216
+ severity: diagnostic.severity,
1217
+ fix: removeLandmarkRoleOverride,
1218
+ diagnostic
1219
+ }];
1220
+ });
1221
+ function requireAccessibleName({ tag, props }) {
1222
+ if ("aria-label" in props || "aria-labelledby" in props) return [];
1223
+ return [{
1224
+ valid: false,
1225
+ fixable: false,
1226
+ severity: "warning",
1227
+ diagnostic: AriaDiagnostics.missingAccessibleName(tag)
1228
+ }];
1229
+ }
1230
+ const NAMED_LANDMARK_TAGS = ["nav", "aside"];
1231
+ const NAMED_LANDMARK_TAG_SET = new Set(NAMED_LANDMARK_TAGS);
1232
+ const landmarkAccessibleNameRule = defineAriaRule(NAMED_LANDMARK_TAGS, (ctx) => {
1233
+ if (!ctx.implicitRole || !NAMED_LANDMARK_TAG_SET.has(ctx.tag)) return [];
1234
+ return requireAccessibleName(ctx);
1235
+ });
1236
+ const HTML_ARIA_RULES = [
1237
+ landmarkRoleRule,
1238
+ landmarkAccessibleNameRule,
1239
+ roleNotPermittedRule,
1240
+ ...INPUT_RULES,
1241
+ ...ANCHOR_RULES
1242
+ ];
1243
+ //#endregion
1244
+ export { ANCHOR_RULES, HTML_ARIA_RULES, INPUT_RULES, acceptRequiresFileTypeRule, altRequiresImageTypeRule, ariaDisabledInertRule, captureRequiresFileTypeRule, checkedRequiresCheckableTypeRule, dangerousHrefRule, heightRequiresImageTypeRule, inputAccessibleNameRule, landmarkAccessibleNameRule, landmarkRoleRule, maxLengthRequiresTextTypeRule, maxRequiresNumericTypeRule, minLengthRequiresTextTypeRule, minRequiresNumericTypeRule, multipleRequiresSupportedTypeRule, passwordAutocompleteRule, patternRequiresTextTypeRule, requireAccessibleName, requiredReadOnlyConflictRule, roleButtonWithHrefRule, roleNotPermittedRule, sizeRequiresTextTypeRule, stepRequiresNumericTypeRule, supportedInputTypeRule, widthRequiresImageTypeRule };