@aishware/react-a11y-core 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.
- package/LICENSE +21 -0
- package/README.md +31 -0
- package/dist/aria.d.ts +4 -0
- package/dist/aria.d.ts.map +1 -0
- package/dist/aria.js +10 -0
- package/dist/color.d.ts +21 -0
- package/dist/color.d.ts.map +1 -0
- package/dist/color.js +93 -0
- package/dist/config.d.ts +5 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +39 -0
- package/dist/element.d.ts +50 -0
- package/dist/element.d.ts.map +1 -0
- package/dist/element.js +162 -0
- package/dist/engine.d.ts +18 -0
- package/dist/engine.d.ts.map +1 -0
- package/dist/engine.js +52 -0
- package/dist/fixes.d.ts +15 -0
- package/dist/fixes.d.ts.map +1 -0
- package/dist/fixes.js +32 -0
- package/dist/helpers.d.ts +74 -0
- package/dist/helpers.d.ts.map +1 -0
- package/dist/helpers.js +209 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +14 -0
- package/dist/parse.d.ts +3 -0
- package/dist/parse.d.ts.map +1 -0
- package/dist/parse.js +14 -0
- package/dist/pkg-meta.d.ts +21 -0
- package/dist/pkg-meta.d.ts.map +1 -0
- package/dist/pkg-meta.js +31 -0
- package/dist/reporters/json.d.ts +3 -0
- package/dist/reporters/json.d.ts.map +1 -0
- package/dist/reporters/json.js +9 -0
- package/dist/reporters/sarif.d.ts +9 -0
- package/dist/reporters/sarif.d.ts.map +1 -0
- package/dist/reporters/sarif.js +61 -0
- package/dist/scanner.d.ts +16 -0
- package/dist/scanner.d.ts.map +1 -0
- package/dist/scanner.js +113 -0
- package/dist/types.d.ts +109 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +6 -0
- package/dist/wcag.d.ts +28 -0
- package/dist/wcag.d.ts.map +1 -0
- package/dist/wcag.js +117 -0
- package/package.json +36 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Aishwarya Sharma
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# @aishware/react-a11y-core
|
|
2
|
+
|
|
3
|
+
Platform-agnostic static-analysis engine behind
|
|
4
|
+
[`@aishware/react-a11y`](https://www.npmjs.com/package/@aishware/react-a11y). Parses
|
|
5
|
+
TSX/JSX with the TypeScript compiler API into a normalized `ElementNode` model,
|
|
6
|
+
runs a rule engine over it, and emits diagnostics mapped to WCAG 2.2.
|
|
7
|
+
Web and React Native rule packs are built on this same engine.
|
|
8
|
+
|
|
9
|
+
Use it to embed accessibility checks in editor extensions, custom CI, or agents:
|
|
10
|
+
|
|
11
|
+
```ts
|
|
12
|
+
import { analyze } from '@aishware/react-a11y-core';
|
|
13
|
+
import { webRules } from '@aishware/react-a11y-rules-web';
|
|
14
|
+
|
|
15
|
+
const diagnostics = analyze({
|
|
16
|
+
code,
|
|
17
|
+
filename: 'App.tsx',
|
|
18
|
+
platform: 'web',
|
|
19
|
+
rules: webRules,
|
|
20
|
+
});
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
It also exports `scanProject`, `applyFixes`, the WCAG 2.2 metadata
|
|
24
|
+
(`WCAG22_TOTALS`, `MANUAL_CHECKS`), color/contrast helpers, and the SARIF
|
|
25
|
+
reporter.
|
|
26
|
+
|
|
27
|
+
Full documentation: <https://github.com/1aishwaryasharma/react-a11y>
|
|
28
|
+
|
|
29
|
+
## License
|
|
30
|
+
|
|
31
|
+
MIT
|
package/dist/aria.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"aria.d.ts","sourceRoot":"","sources":["../src/aria.ts"],"names":[],"mappings":"AAAA,+EAA+E;AAE/E,eAAO,MAAM,iBAAiB,aAI5B,CAAC;AAEH,eAAO,MAAM,gBAAgB,aAG3B,CAAC"}
|
package/dist/aria.js
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/** Interactive ARIA roles and HTML tags, used by the web/native rule packs. */
|
|
2
|
+
export const INTERACTIVE_ROLES = new Set([
|
|
3
|
+
'button', 'checkbox', 'combobox', 'gridcell', 'link', 'listbox', 'menuitem',
|
|
4
|
+
'menuitemcheckbox', 'menuitemradio', 'option', 'radio', 'scrollbar',
|
|
5
|
+
'searchbox', 'slider', 'spinbutton', 'switch', 'tab', 'textbox', 'treeitem',
|
|
6
|
+
]);
|
|
7
|
+
export const INTERACTIVE_TAGS = new Set([
|
|
8
|
+
'a', 'area', 'audio', 'button', 'embed', 'iframe', 'input', 'label',
|
|
9
|
+
'option', 'select', 'summary', 'textarea', 'video',
|
|
10
|
+
]);
|
package/dist/color.d.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/** Color parsing + WCAG contrast math for statically-known style values. */
|
|
2
|
+
export interface Rgb {
|
|
3
|
+
r: number;
|
|
4
|
+
g: number;
|
|
5
|
+
b: number;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Parse a statically-known color. Returns null for anything uncertain:
|
|
9
|
+
* unknown formats, named colors outside the common set, or any alpha < 1
|
|
10
|
+
* (the composited color depends on what's behind it).
|
|
11
|
+
*/
|
|
12
|
+
export declare function parseColor(value: unknown): Rgb | null;
|
|
13
|
+
export declare function relativeLuminance(c: Rgb): number;
|
|
14
|
+
/** WCAG contrast ratio between two colors, 1–21. */
|
|
15
|
+
export declare function contrastRatio(a: Rgb, b: Rgb): number;
|
|
16
|
+
/**
|
|
17
|
+
* WCAG 1.4.3 "large text": ≥24px, or ≥18.66px (14pt) bold.
|
|
18
|
+
* Large text needs 3:1; normal text needs 4.5:1.
|
|
19
|
+
*/
|
|
20
|
+
export declare function isLargeText(fontSizePx: number | undefined, bold: boolean): boolean;
|
|
21
|
+
//# sourceMappingURL=color.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"color.d.ts","sourceRoot":"","sources":["../src/color.ts"],"names":[],"mappings":"AAAA,4EAA4E;AAE5E,MAAM,WAAW,GAAG;IAClB,CAAC,EAAE,MAAM,CAAC;IACV,CAAC,EAAE,MAAM,CAAC;IACV,CAAC,EAAE,MAAM,CAAC;CACX;AA4BD;;;;GAIG;AACH,wBAAgB,UAAU,CAAC,KAAK,EAAE,OAAO,GAAG,GAAG,GAAG,IAAI,CAkCrD;AAOD,wBAAgB,iBAAiB,CAAC,CAAC,EAAE,GAAG,GAAG,MAAM,CAEhD;AAED,oDAAoD;AACpD,wBAAgB,aAAa,CAAC,CAAC,EAAE,GAAG,EAAE,CAAC,EAAE,GAAG,GAAG,MAAM,CAKpD;AAED;;;GAGG;AACH,wBAAgB,WAAW,CAAC,UAAU,EAAE,MAAM,GAAG,SAAS,EAAE,IAAI,EAAE,OAAO,GAAG,OAAO,CAGlF"}
|
package/dist/color.js
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/** Color parsing + WCAG contrast math for statically-known style values. */
|
|
2
|
+
const NAMED = {
|
|
3
|
+
black: { r: 0, g: 0, b: 0 },
|
|
4
|
+
white: { r: 255, g: 255, b: 255 },
|
|
5
|
+
red: { r: 255, g: 0, b: 0 },
|
|
6
|
+
green: { r: 0, g: 128, b: 0 },
|
|
7
|
+
blue: { r: 0, g: 0, b: 255 },
|
|
8
|
+
yellow: { r: 255, g: 255, b: 0 },
|
|
9
|
+
orange: { r: 255, g: 165, b: 0 },
|
|
10
|
+
purple: { r: 128, g: 0, b: 128 },
|
|
11
|
+
gray: { r: 128, g: 128, b: 128 },
|
|
12
|
+
grey: { r: 128, g: 128, b: 128 },
|
|
13
|
+
silver: { r: 192, g: 192, b: 192 },
|
|
14
|
+
maroon: { r: 128, g: 0, b: 0 },
|
|
15
|
+
navy: { r: 0, g: 0, b: 128 },
|
|
16
|
+
teal: { r: 0, g: 128, b: 128 },
|
|
17
|
+
olive: { r: 128, g: 128, b: 0 },
|
|
18
|
+
lime: { r: 0, g: 255, b: 0 },
|
|
19
|
+
aqua: { r: 0, g: 255, b: 255 },
|
|
20
|
+
cyan: { r: 0, g: 255, b: 255 },
|
|
21
|
+
fuchsia: { r: 255, g: 0, b: 255 },
|
|
22
|
+
magenta: { r: 255, g: 0, b: 255 },
|
|
23
|
+
brown: { r: 165, g: 42, b: 42 },
|
|
24
|
+
pink: { r: 255, g: 192, b: 203 },
|
|
25
|
+
gold: { r: 255, g: 215, b: 0 },
|
|
26
|
+
};
|
|
27
|
+
/**
|
|
28
|
+
* Parse a statically-known color. Returns null for anything uncertain:
|
|
29
|
+
* unknown formats, named colors outside the common set, or any alpha < 1
|
|
30
|
+
* (the composited color depends on what's behind it).
|
|
31
|
+
*/
|
|
32
|
+
export function parseColor(value) {
|
|
33
|
+
if (typeof value !== 'string')
|
|
34
|
+
return null;
|
|
35
|
+
const v = value.trim().toLowerCase();
|
|
36
|
+
if (v in NAMED)
|
|
37
|
+
return NAMED[v];
|
|
38
|
+
const hex = /^#([0-9a-f]{3,8})$/.exec(v)?.[1];
|
|
39
|
+
if (hex) {
|
|
40
|
+
if (hex.length === 3 || hex.length === 4) {
|
|
41
|
+
if (hex.length === 4 && hex[3] !== 'f')
|
|
42
|
+
return null;
|
|
43
|
+
return {
|
|
44
|
+
r: parseInt(hex[0] + hex[0], 16),
|
|
45
|
+
g: parseInt(hex[1] + hex[1], 16),
|
|
46
|
+
b: parseInt(hex[2] + hex[2], 16),
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
if (hex.length === 6 || hex.length === 8) {
|
|
50
|
+
if (hex.length === 8 && hex.slice(6) !== 'ff')
|
|
51
|
+
return null;
|
|
52
|
+
return {
|
|
53
|
+
r: parseInt(hex.slice(0, 2), 16),
|
|
54
|
+
g: parseInt(hex.slice(2, 4), 16),
|
|
55
|
+
b: parseInt(hex.slice(4, 6), 16),
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
const fn = /^rgba?\(\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(\d{1,3})\s*(?:,\s*([\d.]+)\s*)?\)$/.exec(v);
|
|
61
|
+
if (fn) {
|
|
62
|
+
if (fn[4] !== undefined && Number(fn[4]) < 1)
|
|
63
|
+
return null;
|
|
64
|
+
const [r, g, b] = [Number(fn[1]), Number(fn[2]), Number(fn[3])];
|
|
65
|
+
if (r > 255 || g > 255 || b > 255)
|
|
66
|
+
return null;
|
|
67
|
+
return { r, g, b };
|
|
68
|
+
}
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
function channel(c) {
|
|
72
|
+
const s = c / 255;
|
|
73
|
+
return s <= 0.03928 ? s / 12.92 : Math.pow((s + 0.055) / 1.055, 2.4);
|
|
74
|
+
}
|
|
75
|
+
export function relativeLuminance(c) {
|
|
76
|
+
return 0.2126 * channel(c.r) + 0.7152 * channel(c.g) + 0.0722 * channel(c.b);
|
|
77
|
+
}
|
|
78
|
+
/** WCAG contrast ratio between two colors, 1–21. */
|
|
79
|
+
export function contrastRatio(a, b) {
|
|
80
|
+
const la = relativeLuminance(a);
|
|
81
|
+
const lb = relativeLuminance(b);
|
|
82
|
+
const [lighter, darker] = la >= lb ? [la, lb] : [lb, la];
|
|
83
|
+
return (lighter + 0.05) / (darker + 0.05);
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* WCAG 1.4.3 "large text": ≥24px, or ≥18.66px (14pt) bold.
|
|
87
|
+
* Large text needs 3:1; normal text needs 4.5:1.
|
|
88
|
+
*/
|
|
89
|
+
export function isLargeText(fontSizePx, bold) {
|
|
90
|
+
if (fontSizePx === undefined)
|
|
91
|
+
return false;
|
|
92
|
+
return fontSizePx >= 24 || (bold && fontSizePx >= 18.66);
|
|
93
|
+
}
|
package/dist/config.d.ts
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { A11yConfig } from './types.js';
|
|
2
|
+
export declare function loadConfig(root: string): A11yConfig;
|
|
3
|
+
/** Minimal glob support: `*` (within a segment) and `**` (across segments). */
|
|
4
|
+
export declare function globToRegExp(glob: string): RegExp;
|
|
5
|
+
//# sourceMappingURL=config.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAI7C,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,UAAU,CAmBnD;AAMD,+EAA+E;AAC/E,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAUjD"}
|
package/dist/config.js
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
const CONFIG_FILES = ['react-a11y.config.json', '.react-a11yrc.json'];
|
|
4
|
+
export function loadConfig(root) {
|
|
5
|
+
for (const file of CONFIG_FILES) {
|
|
6
|
+
const full = path.join(root, file);
|
|
7
|
+
if (fs.existsSync(full)) {
|
|
8
|
+
return JSON.parse(fs.readFileSync(full, 'utf8'));
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
const pkgPath = path.join(root, 'package.json');
|
|
12
|
+
if (fs.existsSync(pkgPath)) {
|
|
13
|
+
try {
|
|
14
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
|
|
15
|
+
if (pkg['react-a11y'] && typeof pkg['react-a11y'] === 'object') {
|
|
16
|
+
return pkg['react-a11y'];
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
// malformed package.json — fall through to defaults
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
return {};
|
|
24
|
+
}
|
|
25
|
+
// Placeholders keep the globstar expansions safe from the later `*`/`?` passes.
|
|
26
|
+
const GLOBSTAR_SLASH = '\u0000';
|
|
27
|
+
const GLOBSTAR = '\u0001';
|
|
28
|
+
/** Minimal glob support: `*` (within a segment) and `**` (across segments). */
|
|
29
|
+
export function globToRegExp(glob) {
|
|
30
|
+
const escaped = glob
|
|
31
|
+
.replace(/[.+^${}()|[\]\\]/g, '\\$&')
|
|
32
|
+
.replace(/\*\*\//g, GLOBSTAR_SLASH)
|
|
33
|
+
.replace(/\*\*/g, GLOBSTAR)
|
|
34
|
+
.replace(/\*/g, '[^/]*')
|
|
35
|
+
.replace(/\?/g, '[^/]')
|
|
36
|
+
.replaceAll(GLOBSTAR_SLASH, '(?:.*/)?')
|
|
37
|
+
.replaceAll(GLOBSTAR, '.*');
|
|
38
|
+
return new RegExp(`^${escaped}$`);
|
|
39
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import ts from 'typescript';
|
|
2
|
+
export type AttrValue = {
|
|
3
|
+
kind: 'static';
|
|
4
|
+
value: string | number | boolean | null | undefined;
|
|
5
|
+
attrNode?: ts.JsxAttribute;
|
|
6
|
+
} | {
|
|
7
|
+
kind: 'expression';
|
|
8
|
+
text: string;
|
|
9
|
+
node?: ts.Expression;
|
|
10
|
+
attrNode?: ts.JsxAttribute;
|
|
11
|
+
};
|
|
12
|
+
export interface SourceLocation {
|
|
13
|
+
line: number;
|
|
14
|
+
column: number;
|
|
15
|
+
endLine: number;
|
|
16
|
+
endColumn: number;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Normalized JSX element. This is the platform-agnostic surface rules are
|
|
20
|
+
* written against — the same model backs React DOM, Next.js and React Native.
|
|
21
|
+
*/
|
|
22
|
+
export interface ElementNode {
|
|
23
|
+
/** Tag as written: "img", "Image", "Animated.View" */
|
|
24
|
+
name: string;
|
|
25
|
+
/** True for components (uppercase / member expressions), false for DOM tags. */
|
|
26
|
+
isComponent: boolean;
|
|
27
|
+
/** Module the component's root identifier was imported from, if resolvable. */
|
|
28
|
+
importSource: string | null;
|
|
29
|
+
attrs: Map<string, AttrValue>;
|
|
30
|
+
hasSpread: boolean;
|
|
31
|
+
parent: ElementNode | null;
|
|
32
|
+
childElements: ElementNode[];
|
|
33
|
+
/** Direct non-whitespace JSX text child. */
|
|
34
|
+
hasTextChild: boolean;
|
|
35
|
+
/** Concatenated direct JSX text, whitespace-collapsed. */
|
|
36
|
+
directText: string;
|
|
37
|
+
/** Direct `{expression}` child. */
|
|
38
|
+
hasExpressionChild: boolean;
|
|
39
|
+
selfClosing: boolean;
|
|
40
|
+
loc: SourceLocation;
|
|
41
|
+
}
|
|
42
|
+
export interface FileModel {
|
|
43
|
+
elements: ElementNode[];
|
|
44
|
+
imports: Map<string, string>;
|
|
45
|
+
sourceFile: ts.SourceFile;
|
|
46
|
+
}
|
|
47
|
+
export declare function buildFileModel(sf: ts.SourceFile): FileModel;
|
|
48
|
+
export declare function walkDescendants(el: ElementNode, cb: (child: ElementNode) => void): void;
|
|
49
|
+
export declare function findAncestor(el: ElementNode, predicate: (ancestor: ElementNode) => boolean): ElementNode | null;
|
|
50
|
+
//# sourceMappingURL=element.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"element.d.ts","sourceRoot":"","sources":["../src/element.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,YAAY,CAAC;AAE5B,MAAM,MAAM,SAAS,GACjB;IAAE,IAAI,EAAE,QAAQ,CAAC;IAAC,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,OAAO,GAAG,IAAI,GAAG,SAAS,CAAC;IAAC,QAAQ,CAAC,EAAE,EAAE,CAAC,YAAY,CAAA;CAAE,GACnG;IAAE,IAAI,EAAE,YAAY,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,EAAE,CAAC,UAAU,CAAC;IAAC,QAAQ,CAAC,EAAE,EAAE,CAAC,YAAY,CAAA;CAAE,CAAC;AAE3F,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;CACnB;AAED;;;GAGG;AACH,MAAM,WAAW,WAAW;IAC1B,sDAAsD;IACtD,IAAI,EAAE,MAAM,CAAC;IACb,gFAAgF;IAChF,WAAW,EAAE,OAAO,CAAC;IACrB,+EAA+E;IAC/E,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,KAAK,EAAE,GAAG,CAAC,MAAM,EAAE,SAAS,CAAC,CAAC;IAC9B,SAAS,EAAE,OAAO,CAAC;IACnB,MAAM,EAAE,WAAW,GAAG,IAAI,CAAC;IAC3B,aAAa,EAAE,WAAW,EAAE,CAAC;IAC7B,4CAA4C;IAC5C,YAAY,EAAE,OAAO,CAAC;IACtB,0DAA0D;IAC1D,UAAU,EAAE,MAAM,CAAC;IACnB,mCAAmC;IACnC,kBAAkB,EAAE,OAAO,CAAC;IAC5B,WAAW,EAAE,OAAO,CAAC;IACrB,GAAG,EAAE,cAAc,CAAC;CACrB;AAED,MAAM,WAAW,SAAS;IACxB,QAAQ,EAAE,WAAW,EAAE,CAAC;IACxB,OAAO,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC7B,UAAU,EAAE,EAAE,CAAC,UAAU,CAAC;CAC3B;AAoDD,wBAAgB,cAAc,CAAC,EAAE,EAAE,EAAE,CAAC,UAAU,GAAG,SAAS,CAwF3D;AAED,wBAAgB,eAAe,CAAC,EAAE,EAAE,WAAW,EAAE,EAAE,EAAE,CAAC,KAAK,EAAE,WAAW,KAAK,IAAI,GAAG,IAAI,CAKvF;AAED,wBAAgB,YAAY,CAC1B,EAAE,EAAE,WAAW,EACf,SAAS,EAAE,CAAC,QAAQ,EAAE,WAAW,KAAK,OAAO,GAC5C,WAAW,GAAG,IAAI,CAOpB"}
|
package/dist/element.js
ADDED
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import ts from 'typescript';
|
|
2
|
+
function collectImports(sf) {
|
|
3
|
+
const imports = new Map();
|
|
4
|
+
for (const stmt of sf.statements) {
|
|
5
|
+
if (!ts.isImportDeclaration(stmt) || !ts.isStringLiteral(stmt.moduleSpecifier))
|
|
6
|
+
continue;
|
|
7
|
+
const source = stmt.moduleSpecifier.text;
|
|
8
|
+
const clause = stmt.importClause;
|
|
9
|
+
if (!clause)
|
|
10
|
+
continue;
|
|
11
|
+
if (clause.name)
|
|
12
|
+
imports.set(clause.name.text, source);
|
|
13
|
+
const bindings = clause.namedBindings;
|
|
14
|
+
if (bindings) {
|
|
15
|
+
if (ts.isNamespaceImport(bindings)) {
|
|
16
|
+
imports.set(bindings.name.text, source);
|
|
17
|
+
}
|
|
18
|
+
else {
|
|
19
|
+
for (const spec of bindings.elements)
|
|
20
|
+
imports.set(spec.name.text, source);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
return imports;
|
|
25
|
+
}
|
|
26
|
+
function attrValue(init) {
|
|
27
|
+
if (init === undefined)
|
|
28
|
+
return { kind: 'static', value: true };
|
|
29
|
+
if (ts.isStringLiteral(init))
|
|
30
|
+
return { kind: 'static', value: init.text };
|
|
31
|
+
if (ts.isJsxExpression(init)) {
|
|
32
|
+
const expr = init.expression;
|
|
33
|
+
if (!expr)
|
|
34
|
+
return { kind: 'expression', text: '' };
|
|
35
|
+
if (ts.isStringLiteralLike(expr))
|
|
36
|
+
return { kind: 'static', value: expr.text };
|
|
37
|
+
if (ts.isNumericLiteral(expr))
|
|
38
|
+
return { kind: 'static', value: Number(expr.text) };
|
|
39
|
+
if (expr.kind === ts.SyntaxKind.TrueKeyword)
|
|
40
|
+
return { kind: 'static', value: true };
|
|
41
|
+
if (expr.kind === ts.SyntaxKind.FalseKeyword)
|
|
42
|
+
return { kind: 'static', value: false };
|
|
43
|
+
if (expr.kind === ts.SyntaxKind.NullKeyword)
|
|
44
|
+
return { kind: 'static', value: null };
|
|
45
|
+
if (ts.isIdentifier(expr) && expr.text === 'undefined')
|
|
46
|
+
return { kind: 'static', value: undefined };
|
|
47
|
+
if (ts.isPrefixUnaryExpression(expr) &&
|
|
48
|
+
expr.operator === ts.SyntaxKind.MinusToken &&
|
|
49
|
+
ts.isNumericLiteral(expr.operand)) {
|
|
50
|
+
return { kind: 'static', value: -Number(expr.operand.text) };
|
|
51
|
+
}
|
|
52
|
+
return { kind: 'expression', text: expr.getText(), node: expr };
|
|
53
|
+
}
|
|
54
|
+
return { kind: 'expression', text: init.getText() };
|
|
55
|
+
}
|
|
56
|
+
function rootIdentifier(tag) {
|
|
57
|
+
let node = tag;
|
|
58
|
+
while (ts.isPropertyAccessExpression(node))
|
|
59
|
+
node = node.expression;
|
|
60
|
+
return ts.isIdentifier(node) ? node.text : node.getText();
|
|
61
|
+
}
|
|
62
|
+
export function buildFileModel(sf) {
|
|
63
|
+
const imports = collectImports(sf);
|
|
64
|
+
const elements = [];
|
|
65
|
+
function makeElement(opening, parent, selfClosing) {
|
|
66
|
+
const name = opening.tagName.getText(sf);
|
|
67
|
+
const root = rootIdentifier(opening.tagName);
|
|
68
|
+
const isComponent = !/^[a-z]/.test(name);
|
|
69
|
+
const attrs = new Map();
|
|
70
|
+
let hasSpread = false;
|
|
71
|
+
for (const prop of opening.attributes.properties) {
|
|
72
|
+
if (ts.isJsxSpreadAttribute(prop)) {
|
|
73
|
+
hasSpread = true;
|
|
74
|
+
}
|
|
75
|
+
else if (ts.isJsxAttribute(prop)) {
|
|
76
|
+
const value = attrValue(prop.initializer);
|
|
77
|
+
value.attrNode = prop;
|
|
78
|
+
attrs.set(prop.name.getText(sf), value);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
const start = sf.getLineAndCharacterOfPosition(opening.getStart(sf));
|
|
82
|
+
const end = sf.getLineAndCharacterOfPosition(opening.getEnd());
|
|
83
|
+
const el = {
|
|
84
|
+
name,
|
|
85
|
+
isComponent,
|
|
86
|
+
importSource: isComponent ? imports.get(root) ?? null : null,
|
|
87
|
+
attrs,
|
|
88
|
+
hasSpread,
|
|
89
|
+
parent,
|
|
90
|
+
childElements: [],
|
|
91
|
+
hasTextChild: false,
|
|
92
|
+
directText: '',
|
|
93
|
+
hasExpressionChild: false,
|
|
94
|
+
selfClosing,
|
|
95
|
+
loc: {
|
|
96
|
+
line: start.line + 1,
|
|
97
|
+
column: start.character + 1,
|
|
98
|
+
endLine: end.line + 1,
|
|
99
|
+
endColumn: end.character + 1,
|
|
100
|
+
},
|
|
101
|
+
};
|
|
102
|
+
if (parent)
|
|
103
|
+
parent.childElements.push(el);
|
|
104
|
+
elements.push(el);
|
|
105
|
+
return el;
|
|
106
|
+
}
|
|
107
|
+
function markDirectChildren(children, el) {
|
|
108
|
+
const textParts = [];
|
|
109
|
+
for (const child of children) {
|
|
110
|
+
if (ts.isJsxText(child)) {
|
|
111
|
+
const text = child.text.replace(/\s+/g, ' ').trim();
|
|
112
|
+
if (text.length > 0) {
|
|
113
|
+
el.hasTextChild = true;
|
|
114
|
+
textParts.push(text);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
else if (ts.isJsxExpression(child) && child.expression !== undefined) {
|
|
118
|
+
if (ts.isStringLiteralLike(child.expression)) {
|
|
119
|
+
// {"literal"} children are still static text
|
|
120
|
+
const text = child.expression.text.replace(/\s+/g, ' ').trim();
|
|
121
|
+
if (text.length > 0) {
|
|
122
|
+
el.hasTextChild = true;
|
|
123
|
+
textParts.push(text);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
else {
|
|
127
|
+
el.hasExpressionChild = true;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
el.directText = textParts.join(' ');
|
|
132
|
+
}
|
|
133
|
+
function visit(node, parentEl) {
|
|
134
|
+
let nextParent = parentEl;
|
|
135
|
+
if (ts.isJsxElement(node)) {
|
|
136
|
+
const el = makeElement(node.openingElement, parentEl, false);
|
|
137
|
+
markDirectChildren(node.children, el);
|
|
138
|
+
nextParent = el;
|
|
139
|
+
}
|
|
140
|
+
else if (ts.isJsxSelfClosingElement(node)) {
|
|
141
|
+
nextParent = makeElement(node, parentEl, true);
|
|
142
|
+
}
|
|
143
|
+
node.forEachChild((c) => visit(c, nextParent));
|
|
144
|
+
}
|
|
145
|
+
visit(sf, null);
|
|
146
|
+
return { elements, imports, sourceFile: sf };
|
|
147
|
+
}
|
|
148
|
+
export function walkDescendants(el, cb) {
|
|
149
|
+
for (const child of el.childElements) {
|
|
150
|
+
cb(child);
|
|
151
|
+
walkDescendants(child, cb);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
export function findAncestor(el, predicate) {
|
|
155
|
+
let cur = el.parent;
|
|
156
|
+
while (cur) {
|
|
157
|
+
if (predicate(cur))
|
|
158
|
+
return cur;
|
|
159
|
+
cur = cur.parent;
|
|
160
|
+
}
|
|
161
|
+
return null;
|
|
162
|
+
}
|
package/dist/engine.d.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { type FileModel } from './element.js';
|
|
2
|
+
import type { Diagnostic, Platform, Rule, RuleSetting } from './types.js';
|
|
3
|
+
export interface AnalyzeOptions {
|
|
4
|
+
code: string;
|
|
5
|
+
filename: string;
|
|
6
|
+
platform: Platform;
|
|
7
|
+
rules: Rule[];
|
|
8
|
+
ruleSettings?: Record<string, RuleSetting>;
|
|
9
|
+
}
|
|
10
|
+
export type AnalyzeModelOptions = Omit<AnalyzeOptions, 'code'>;
|
|
11
|
+
/** Run rules over an already-built file model (lets callers reuse the parse). */
|
|
12
|
+
export declare function analyzeModel(model: FileModel, options: AnalyzeModelOptions): Diagnostic[];
|
|
13
|
+
/**
|
|
14
|
+
* Analyze a single file. Pure and synchronous — this is the unit both the CLI
|
|
15
|
+
* and any editor/CI integration build on.
|
|
16
|
+
*/
|
|
17
|
+
export declare function analyze(options: AnalyzeOptions): Diagnostic[];
|
|
18
|
+
//# sourceMappingURL=engine.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"engine.d.ts","sourceRoot":"","sources":["../src/engine.ts"],"names":[],"mappings":"AAAA,OAAO,EAAkB,KAAK,SAAS,EAAE,MAAM,cAAc,CAAC;AAG9D,OAAO,KAAK,EAAE,UAAU,EAAE,QAAQ,EAAE,IAAI,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAE1E,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,QAAQ,CAAC;IACnB,KAAK,EAAE,IAAI,EAAE,CAAC;IACd,YAAY,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,WAAW,CAAC,CAAC;CAC5C;AAED,MAAM,MAAM,mBAAmB,GAAG,IAAI,CAAC,cAAc,EAAE,MAAM,CAAC,CAAC;AAE/D,iFAAiF;AACjF,wBAAgB,YAAY,CAAC,KAAK,EAAE,SAAS,EAAE,OAAO,EAAE,mBAAmB,GAAG,UAAU,EAAE,CAwCzF;AAED;;;GAGG;AACH,wBAAgB,OAAO,CAAC,OAAO,EAAE,cAAc,GAAG,UAAU,EAAE,CAG7D"}
|
package/dist/engine.js
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { buildFileModel } from './element.js';
|
|
2
|
+
import { parseSource } from './parse.js';
|
|
3
|
+
import { resolveWcag } from './wcag.js';
|
|
4
|
+
/** Run rules over an already-built file model (lets callers reuse the parse). */
|
|
5
|
+
export function analyzeModel(model, options) {
|
|
6
|
+
const { filename, platform, rules, ruleSettings = {} } = options;
|
|
7
|
+
if (model.elements.length === 0)
|
|
8
|
+
return [];
|
|
9
|
+
const diagnostics = [];
|
|
10
|
+
for (const rule of rules) {
|
|
11
|
+
if (!rule.meta.platforms.includes(platform))
|
|
12
|
+
continue;
|
|
13
|
+
const setting = ruleSettings[rule.meta.id];
|
|
14
|
+
if (setting === 'off')
|
|
15
|
+
continue;
|
|
16
|
+
const wcag = resolveWcag(rule.meta.wcag);
|
|
17
|
+
const visitor = rule.create({
|
|
18
|
+
filename,
|
|
19
|
+
platform,
|
|
20
|
+
sourceFile: model.sourceFile,
|
|
21
|
+
report({ el, message, severity, fix }) {
|
|
22
|
+
diagnostics.push({
|
|
23
|
+
ruleId: rule.meta.id,
|
|
24
|
+
message,
|
|
25
|
+
severity: setting ?? severity ?? rule.meta.severity,
|
|
26
|
+
file: filename,
|
|
27
|
+
line: el.loc.line,
|
|
28
|
+
column: el.loc.column,
|
|
29
|
+
endLine: el.loc.endLine,
|
|
30
|
+
endColumn: el.loc.endColumn,
|
|
31
|
+
wcag,
|
|
32
|
+
helpUrl: rule.meta.helpUrl,
|
|
33
|
+
...(fix ? { fix } : {}),
|
|
34
|
+
});
|
|
35
|
+
},
|
|
36
|
+
});
|
|
37
|
+
if (visitor.element) {
|
|
38
|
+
for (const el of model.elements)
|
|
39
|
+
visitor.element(el);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
diagnostics.sort((a, b) => a.line - b.line || a.column - b.column || a.ruleId.localeCompare(b.ruleId));
|
|
43
|
+
return diagnostics;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Analyze a single file. Pure and synchronous — this is the unit both the CLI
|
|
47
|
+
* and any editor/CI integration build on.
|
|
48
|
+
*/
|
|
49
|
+
export function analyze(options) {
|
|
50
|
+
const model = buildFileModel(parseSource(options.code, options.filename));
|
|
51
|
+
return analyzeModel(model, options);
|
|
52
|
+
}
|
package/dist/fixes.d.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { ElementNode } from './element.js';
|
|
2
|
+
import type { Fix } from './types.js';
|
|
3
|
+
/** Fix that deletes an attribute (including its leading whitespace). */
|
|
4
|
+
export declare function fixRemoveAttr(el: ElementNode, name: string): Fix | undefined;
|
|
5
|
+
/** Fix that renames an attribute, keeping its value untouched. */
|
|
6
|
+
export declare function fixRenameAttr(el: ElementNode, name: string, newName: string): Fix | undefined;
|
|
7
|
+
/**
|
|
8
|
+
* Apply fixes to a source string. Fixes are applied last-to-first so offsets
|
|
9
|
+
* stay valid; overlapping fixes are skipped (re-run to converge).
|
|
10
|
+
*/
|
|
11
|
+
export declare function applyFixes(source: string, fixes: Fix[]): {
|
|
12
|
+
output: string;
|
|
13
|
+
applied: number;
|
|
14
|
+
};
|
|
15
|
+
//# sourceMappingURL=fixes.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fixes.d.ts","sourceRoot":"","sources":["../src/fixes.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AAChD,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,YAAY,CAAC;AAEtC,wEAAwE;AACxE,wBAAgB,aAAa,CAAC,EAAE,EAAE,WAAW,EAAE,IAAI,EAAE,MAAM,GAAG,GAAG,GAAG,SAAS,CAI5E;AAED,kEAAkE;AAClE,wBAAgB,aAAa,CAAC,EAAE,EAAE,WAAW,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,GAAG,GAAG,SAAS,CAI7F;AAED;;;GAGG;AACH,wBAAgB,UAAU,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,GAAG,EAAE,GAAG;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAY5F"}
|
package/dist/fixes.js
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/** Fix that deletes an attribute (including its leading whitespace). */
|
|
2
|
+
export function fixRemoveAttr(el, name) {
|
|
3
|
+
const node = el.attrs.get(name)?.attrNode;
|
|
4
|
+
if (!node)
|
|
5
|
+
return undefined;
|
|
6
|
+
return { start: node.getFullStart(), end: node.getEnd(), replacement: '' };
|
|
7
|
+
}
|
|
8
|
+
/** Fix that renames an attribute, keeping its value untouched. */
|
|
9
|
+
export function fixRenameAttr(el, name, newName) {
|
|
10
|
+
const node = el.attrs.get(name)?.attrNode;
|
|
11
|
+
if (!node)
|
|
12
|
+
return undefined;
|
|
13
|
+
return { start: node.name.getStart(), end: node.name.getEnd(), replacement: newName };
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Apply fixes to a source string. Fixes are applied last-to-first so offsets
|
|
17
|
+
* stay valid; overlapping fixes are skipped (re-run to converge).
|
|
18
|
+
*/
|
|
19
|
+
export function applyFixes(source, fixes) {
|
|
20
|
+
const sorted = [...fixes].sort((a, b) => b.start - a.start || b.end - a.end);
|
|
21
|
+
let output = source;
|
|
22
|
+
let applied = 0;
|
|
23
|
+
let lastStart = Infinity;
|
|
24
|
+
for (const fix of sorted) {
|
|
25
|
+
if (fix.end > lastStart || fix.start > fix.end)
|
|
26
|
+
continue;
|
|
27
|
+
output = output.slice(0, fix.start) + fix.replacement + output.slice(fix.end);
|
|
28
|
+
lastStart = fix.start;
|
|
29
|
+
applied++;
|
|
30
|
+
}
|
|
31
|
+
return { output, applied };
|
|
32
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import type { AttrValue, ElementNode } from './element.js';
|
|
2
|
+
import type { Severity } from './types.js';
|
|
3
|
+
export declare function getAttr(el: ElementNode, name: string): AttrValue | undefined;
|
|
4
|
+
export declare function hasAttr(el: ElementNode, name: string): boolean;
|
|
5
|
+
/** Static value when statically known, otherwise undefined. */
|
|
6
|
+
export declare function staticValue(el: ElementNode, name: string): string | number | boolean | null | undefined;
|
|
7
|
+
/** Static string value (trimmed) when statically known to be a string. */
|
|
8
|
+
export declare function staticString(el: ElementNode, name: string): string | undefined;
|
|
9
|
+
/** True when the attribute is present as a dynamic expression. */
|
|
10
|
+
export declare function isExpression(el: ElementNode, name: string): boolean;
|
|
11
|
+
export declare function isStaticTrue(el: ElementNode, name: string): boolean;
|
|
12
|
+
export declare function isAriaHidden(el: ElementNode): boolean;
|
|
13
|
+
/**
|
|
14
|
+
* True when the attribute could plausibly carry a usable value: present as a
|
|
15
|
+
* non-empty static string, or dynamic (benefit of the doubt to avoid false
|
|
16
|
+
* positives in a purely static analysis).
|
|
17
|
+
*/
|
|
18
|
+
export declare function attrProvidesValue(el: ElementNode, name: string): boolean;
|
|
19
|
+
/**
|
|
20
|
+
* Conservative check that an element's *content* can supply an accessible
|
|
21
|
+
* name: direct text, dynamic children, unknown components (which may render
|
|
22
|
+
* text), spreads, or an <img> with non-empty alt.
|
|
23
|
+
*/
|
|
24
|
+
export declare function contentProvidesName(el: ElementNode): boolean;
|
|
25
|
+
/** Accessible name via attributes or content, for web elements. */
|
|
26
|
+
export declare function hasAccessibleName(el: ElementNode): boolean;
|
|
27
|
+
export declare function isPresentational(el: ElementNode): boolean;
|
|
28
|
+
/**
|
|
29
|
+
* Statically-known value of a property in an inline style object literal,
|
|
30
|
+
* e.g. style={{ width: 20 }}. Returns undefined for dynamic styles,
|
|
31
|
+
* StyleSheet references, or non-literal values.
|
|
32
|
+
*/
|
|
33
|
+
export declare function inlineStyleValue(el: ElementNode, prop: string): string | number | undefined;
|
|
34
|
+
export declare function inlineStyleNumber(el: ElementNode, prop: string): number | undefined;
|
|
35
|
+
/**
|
|
36
|
+
* The full visible text of an element, when it is entirely static. Returns
|
|
37
|
+
* null as soon as an expression child or component child makes the text
|
|
38
|
+
* unknowable. aria-hidden subtrees are excluded (they are not "visible
|
|
39
|
+
* label" for 2.5.3 purposes).
|
|
40
|
+
*/
|
|
41
|
+
export declare function deepStaticText(el: ElementNode): string | null;
|
|
42
|
+
export interface ContrastInfo {
|
|
43
|
+
ratio: number;
|
|
44
|
+
required: number;
|
|
45
|
+
large: boolean;
|
|
46
|
+
fontSizeKnown: boolean;
|
|
47
|
+
fg: string;
|
|
48
|
+
bg: string;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Contrast of statically-known inline color/backgroundColor on the same
|
|
52
|
+
* element. Null when either color is dynamic, unparseable or translucent.
|
|
53
|
+
*/
|
|
54
|
+
export declare function inlineStyleContrast(el: ElementNode): ContrastInfo | null;
|
|
55
|
+
/**
|
|
56
|
+
* WCAG 1.4.3 contrast check for an element's inline literal color/background.
|
|
57
|
+
* Returns the finding (message + severity) to report, or null when it passes,
|
|
58
|
+
* the colors aren't static, or the size makes it indeterminate. Shared by the
|
|
59
|
+
* web and native `color-contrast` rules so the thresholds live in one place.
|
|
60
|
+
*/
|
|
61
|
+
export declare function colorContrastFinding(el: ElementNode): {
|
|
62
|
+
message: string;
|
|
63
|
+
severity: Severity;
|
|
64
|
+
} | null;
|
|
65
|
+
/**
|
|
66
|
+
* The WCAG target-size tier for a pointer target, by its smaller dimension.
|
|
67
|
+
* `below-min` < 24px (WCAG 2.5.8 AA), `below-recommended` < 44px (2.5.5 AAA /
|
|
68
|
+
* Apple HIG / Material). Keeps the 24/44 thresholds in one place; each rule
|
|
69
|
+
* maps the tier to its own severity and platform wording.
|
|
70
|
+
*/
|
|
71
|
+
export declare function targetSizeTier(width: number, height: number): 'below-min' | 'below-recommended' | null;
|
|
72
|
+
/** Statically-known keys of an object-literal prop, e.g. accessibilityState={{...}}. */
|
|
73
|
+
export declare function objectLiteralKeys(el: ElementNode, attrName: string): string[] | undefined;
|
|
74
|
+
//# sourceMappingURL=helpers.d.ts.map
|