@a11yfred/neighbor 0.3.0 → 1.0.3
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/CHANGELOG.md +59 -7
- package/CONTRIBUTING.md +10 -10
- package/README.md +196 -32
- package/RULES-CONTENT.md +296 -0
- package/RULES-CSS.md +61 -0
- package/RULES-MARKUP.md +156 -0
- package/RULES.md +55 -0
- package/lib/content-rules.js +858 -0
- package/lib/helpers-angular.js +146 -146
- package/lib/helpers-jsx.js +193 -193
- package/lib/helpers-vue.js +151 -151
- package/lib/helpers.js +37 -37
- package/lib/rules.js +2413 -2413
- package/lib/ulam-rules.js +301 -301
- package/neighbor-content.mjs +80 -0
- package/neighbor-eslint-angular.mjs +68 -68
- package/neighbor-eslint-vue.mjs +48 -48
- package/neighbor-eslint.mjs +56 -56
- package/neighbor-stylelint.mjs +282 -256
- package/package.json +30 -5
|
@@ -0,0 +1,858 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* neighbor/lib/content-rules.js
|
|
3
|
+
* Content and prose accessibility rules for web and app copy.
|
|
4
|
+
*
|
|
5
|
+
* These rules operate on plain text, markdown AST nodes, and JSX string literals.
|
|
6
|
+
* Each rule targets a gap not covered by jsx-a11y, stylelint, or existing markup linters.
|
|
7
|
+
*
|
|
8
|
+
* ─── Rule methodology ────────────────────────────────────────────────────────
|
|
9
|
+
*
|
|
10
|
+
* Rules were included only when all three conditions held:
|
|
11
|
+
* 1. A WCAG Success Criterion directly applies, OR the rule appears in ≥3
|
|
12
|
+
* independent authoritative style guides.
|
|
13
|
+
* 2. The rule can be expressed as a finite pattern (string match, count, or
|
|
14
|
+
* AST shape) without requiring NLP or runtime context.
|
|
15
|
+
* 3. Expert consensus is unambiguous - no credible accessibility authority
|
|
16
|
+
* argues the opposite.
|
|
17
|
+
*
|
|
18
|
+
* Rules that require subjective reading (tone, cultural sensitivity beyond a
|
|
19
|
+
* term list) or are under active community debate are excluded.
|
|
20
|
+
*
|
|
21
|
+
* ─── Primary sources ────────────────────────────────────────────────────────
|
|
22
|
+
*
|
|
23
|
+
* WCAG 2.2 / 3.x w3.org/WAI/WCAG22/Understanding
|
|
24
|
+
* W3C WAI Tips w3.org/WAI/tips/writing/
|
|
25
|
+
* wcag.com/authors wcag.com/authors/
|
|
26
|
+
* Google Dev Style developers.google.com/style/accessibility
|
|
27
|
+
* US Plain Language plainlanguage.gov / digital.gov/guides/plain-language
|
|
28
|
+
* SBA Style Guide advocacy.sba.gov/…/writing-accessible-content/
|
|
29
|
+
* GOV.UK Publishing gov.uk/guidance/publishing-accessible-documents
|
|
30
|
+
* NCDJ Style Guide cronkite.asu.edu/ncdj/disability-language-style-guide
|
|
31
|
+
* AP Stylebook amdisrights.org/ap-stylebook-primer-on-disability
|
|
32
|
+
* ADA National Network adata.org/factsheet/ADANN-writing
|
|
33
|
+
* APA Style apastyle.apa.org/style-grammar-guidelines/bias-free-language/disability
|
|
34
|
+
* SIGACCESS Guide sigaccess.org/welcome-to-sigaccess/resources/accessible-writing-guide/
|
|
35
|
+
* Nicolas Steenhout incl.ca/disability-language-is-a-nuanced-thing/ - "Nothing About Us Without Us"; language as community choice, not external rule
|
|
36
|
+
* Léonie Watson tink.uk - cited by Steenhout: "There is no right or wrong answer because it is a matter of personal choice, and the choice depends on context."
|
|
37
|
+
* A11y Collective a11y-collective.com/blog/accessible-writing/
|
|
38
|
+
* UX Content Co. uxcontent.com/accessible-ux-writing-a-guide-for-inclusive-content-design/
|
|
39
|
+
* Canadian Gov accessible.canada.ca/guidelines-creating-accessible-documents
|
|
40
|
+
* SJSU Writing Center sjsu.edu/writingcenter/docs/handouts/Accessible Writing Strategies.pdf
|
|
41
|
+
* Section 508 section508.gov/create/alternative-text/
|
|
42
|
+
*/
|
|
43
|
+
|
|
44
|
+
// ─── Shared term lists ───────────────────────────────────────────────────────
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Ableist terms with suggested replacements.
|
|
48
|
+
*
|
|
49
|
+
* Methodology: union of NCDJ, AP Stylebook, ADA National Network, APA Style,
|
|
50
|
+
* and SIGACCESS guides. Each term appears in ≥2 independent sources. Metaphorical
|
|
51
|
+
* uses ("blind to") are tracked separately in DISABILITY_METAPHORS.
|
|
52
|
+
*
|
|
53
|
+
* Note: identity-first vs person-first (e.g. "autistic person" vs "person with
|
|
54
|
+
* autism") is contested within disability communities - this linter does not flag
|
|
55
|
+
* either form. See NCDJ § "Identity-first language" for context.
|
|
56
|
+
*/
|
|
57
|
+
export const ABLEIST_TERMS = [
|
|
58
|
+
// Slurs - universal consensus across all sources
|
|
59
|
+
{ term: /\bcrip+le[sd]?\b/i, suggest: 'person who uses a wheelchair / person with a mobility disability', sources: 'NCDJ, AP, ADA NN, APA' },
|
|
60
|
+
{ term: /\bretard(ed|s)?\b/i, suggest: 'person with an intellectual disability', sources: 'NCDJ, AP, ADA NN, APA' },
|
|
61
|
+
{ term: /\bimbecile[s]?\b/i, suggest: 'person with an intellectual disability', sources: 'ADA NN, APA' },
|
|
62
|
+
{ term: /\bmoron[s]?\b/i, suggest: 'person with an intellectual disability', sources: 'ADA NN' },
|
|
63
|
+
{ term: /\bdumb\b/i, suggest: 'mute / nonverbal / does not use speech', sources: 'NCDJ, ADA NN' },
|
|
64
|
+
{ term: /\blame\b/i, suggest: 'weak / unconvincing (for the non-disability sense)', sources: 'NCDJ, A11y Collective' },
|
|
65
|
+
{ term: /\bvegetable[s]?\b/i, suggest: 'person in a persistent vegetative state', sources: 'ADA NN' },
|
|
66
|
+
{ term: /\bfreak[s]?\b/i, suggest: '(rewrite)', sources: 'ADA NN' },
|
|
67
|
+
|
|
68
|
+
// Condescending euphemisms - ≥3 sources each
|
|
69
|
+
{ term: /\bspecial needs\b/i, suggest: 'disability / person with a disability', sources: 'NCDJ, AP, ADA NN, APA' },
|
|
70
|
+
{ term: /\bdifferently[- ]abled\b/i, suggest: 'person with a disability', sources: 'NCDJ, AP, ADA NN, APA' },
|
|
71
|
+
{ term: /\bhandi[- ]?capable\b/i, suggest: 'person with a disability', sources: 'NCDJ, AP, ADA NN' },
|
|
72
|
+
{ term: /\bphysically challenged\b/i, suggest: 'person with a physical disability', sources: 'NCDJ, AP, ADA NN, APA' },
|
|
73
|
+
{ term: /\bmentally challenged\b/i, suggest: 'person with an intellectual or cognitive disability', sources: 'ADA NN, APA' },
|
|
74
|
+
{ term: /\bspecial\b(?=\s+(ed|education|class|school))/i, suggest: 'special education (acceptable in formal context) / disability services', sources: 'NCDJ' },
|
|
75
|
+
|
|
76
|
+
// Suffering / tragedy framing - ≥3 sources each
|
|
77
|
+
{ term: /\bconfined to (a |their )?wheelchair\b/i, suggest: 'wheelchair user / person who uses a wheelchair', sources: 'NCDJ, AP, ADA NN, APA, SIGACCESS' },
|
|
78
|
+
{ term: /\bwheelchair[- ]bound\b/i, suggest: 'wheelchair user / person who uses a wheelchair', sources: 'NCDJ, AP, ADA NN, APA, SIGACCESS' },
|
|
79
|
+
{ term: /\bsuffers? from\b/i, suggest: 'has / lives with / is diagnosed with', sources: 'NCDJ, AP, ADA NN, APA' },
|
|
80
|
+
{ term: /\bafflicted (with|by)\b/i, suggest: 'has / lives with', sources: 'NCDJ, AP, ADA NN, APA, SIGACCESS' },
|
|
81
|
+
{ term: /\bvictim of\b/i, suggest: 'person who has / person with', sources: 'NCDJ, AP, ADA NN, APA' },
|
|
82
|
+
{ term: /\bbound to (a |the )?(bed|chair|wheelchair)\b/i, suggest: 'uses a bed / uses a wheelchair', sources: 'ADA NN, APA' },
|
|
83
|
+
{ term: /\bstricken (with|by)\b/i, suggest: 'has / lives with', sources: 'ADA NN' },
|
|
84
|
+
{ term: /\bcrippling\b(?!\s+(blow|defeat|loss))/i, suggest: 'devastating / severe / debilitating', sources: 'NCDJ, A11y Collective' },
|
|
85
|
+
|
|
86
|
+
// Mental health - specific clinical terms misused colloquially
|
|
87
|
+
{ term: /\bcommitted suicide\b/i, suggest: 'died by suicide', sources: 'ADA NN, APA (clinical consensus, AP Stylebook 2022)' },
|
|
88
|
+
{ term: /\bcra+zy\b/i, suggest: 'wild / unexpected / intense (for non-clinical uses)', sources: 'NCDJ, A11y Collective' },
|
|
89
|
+
{ term: /\bpsycho\b/i, suggest: 'reckless / erratic / (rewrite)', sources: 'NCDJ, ADA NN' },
|
|
90
|
+
{ term: /\bschizophrenic\b(?!\s+(disorder|diagnosis|symptom))/i, suggest: 'contradictory / inconsistent (for non-clinical uses)', sources: 'NCDJ, APA' },
|
|
91
|
+
{ term: /\b(a\s+)?manic[- ]depressive\b/i, suggest: 'person with bipolar disorder', sources: 'ADA NN, APA' },
|
|
92
|
+
{ term: /\bjunkie[s]?\b/i, suggest: 'person with a substance use disorder', sources: 'ADA NN, APA' },
|
|
93
|
+
{ term: /\baddict[s]?\b(?!\s+up)/i, suggest: 'person with a substance use disorder / person in recovery', sources: 'ADA NN, APA' },
|
|
94
|
+
|
|
95
|
+
// Normalcy framing - ≥3 sources each
|
|
96
|
+
{ term: /\bnormal (people|person|individuals?|users?)\b/i, suggest: 'people without disabilities / non-disabled people', sources: 'AP, ADA NN, APA, SIGACCESS' },
|
|
97
|
+
{ term: /\bnormal (hearing|vision|sight)\b/i, suggest: 'typical hearing / full hearing / unimpaired vision', sources: 'AP, SIGACCESS' },
|
|
98
|
+
{ term: /\bhearing[- ]impaired\b/i, suggest: 'deaf / hard of hearing', sources: 'NCDJ, AP, ADA NN, APA' },
|
|
99
|
+
{ term: /\bthe (disabled|blind|deaf|mentally ill)\b/i, suggest: 'people with disabilities / blind people / deaf people / people with mental illness', sources: 'NCDJ, AP, ADA NN, APA, SIGACCESS' },
|
|
100
|
+
{ term: /\bhandicapped\b/i, suggest: 'person with a disability / accessible (for facilities)', sources: 'NCDJ, AP, ADA NN' },
|
|
101
|
+
]
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Disability metaphors - disability used figuratively in non-clinical prose.
|
|
105
|
+
*
|
|
106
|
+
* Methodology: identified in NCDJ, A11y Collective, APA, and UX Content Co.
|
|
107
|
+
* as patterns that reinforce ableism even when used innocuously. Each metaphor
|
|
108
|
+
* listed appears in ≥2 sources. This is the most novel rule in the set - no
|
|
109
|
+
* existing a11y linter flags these.
|
|
110
|
+
*/
|
|
111
|
+
export const DISABILITY_METAPHORS = [
|
|
112
|
+
{ term: /\bblind (spot|eye|to)\b/i, suggest: 'gap / oversight / unaware of', sources: 'NCDJ, A11y Collective' },
|
|
113
|
+
{ term: /\bturning a blind eye\b/i, suggest: 'ignoring / overlooking', sources: 'NCDJ, A11y Collective' },
|
|
114
|
+
{ term: /\btone[- ]deaf\b/i, suggest: 'out of touch / insensitive / unaware', sources: 'NCDJ, A11y Collective' },
|
|
115
|
+
{ term: /\bfalling on deaf ears\b/i, suggest: 'being ignored / going unheard', sources: 'NCDJ, A11y Collective' },
|
|
116
|
+
{ term: /\bdeaf ears\b/i, suggest: 'ignored / unheard', sources: 'A11y Collective' },
|
|
117
|
+
{ term: /\bparalyzed (with|by)\b/i, suggest: 'overwhelmed by / unable to act because of', sources: 'NCDJ, A11y Collective' },
|
|
118
|
+
{ term: /\bcrippling (debt|fear|blow|anxiety)\b/i, suggest: 'devastating / crushing / severe', sources: 'NCDJ, A11y Collective' },
|
|
119
|
+
{ term: /\bschizophrenic (approach|strategy|policy|market)\b/i, suggest: 'contradictory / inconsistent / unpredictable', sources: 'NCDJ, APA' },
|
|
120
|
+
{ term: /\bstands? on its own two feet\b/i, suggest: 'self-sufficient / independent', sources: 'A11y Collective' },
|
|
121
|
+
]
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* English idioms that are opaque to ESL readers and international audiences.
|
|
125
|
+
*
|
|
126
|
+
* Methodology: compiled from Canadian Gov accessible docs guide, SJSU accessible
|
|
127
|
+
* writing strategies, UX Content Co., and A11y Collective. This is the largest
|
|
128
|
+
* gap in existing a11y linting - no other tool flags idioms. Terms are those
|
|
129
|
+
* where a fluent English idiom has no transparent literal meaning.
|
|
130
|
+
*
|
|
131
|
+
* Excludes idioms with obvious constituent meaning ("stand up for yourself").
|
|
132
|
+
*/
|
|
133
|
+
export const ENGLISH_IDIOMS = [
|
|
134
|
+
// Business jargon idioms - appear in ≥2 accessible writing guides
|
|
135
|
+
{ term: /\bboil the ocean\b/i, suggest: 'attempt everything at once / do too much', sources: 'SJSU, UX Content Co.' },
|
|
136
|
+
{ term: /\bmove the needle\b/i, suggest: 'make progress / have an impact', sources: 'SJSU, Canadian Gov' },
|
|
137
|
+
{ term: /\bblue[- ]?sky thinking\b/i, suggest: 'open-ended brainstorming / creative thinking', sources: 'SJSU, A11y Collective' },
|
|
138
|
+
{ term: /\bdrink the (kool[- ]?aid|cool[- ]?aid)\b/i, suggest: 'follow without question / accept uncritically', sources: 'SJSU, Canadian Gov' },
|
|
139
|
+
{ term: /\bpeel back the onion\b/i, suggest: 'examine more closely / dig deeper', sources: 'SJSU' },
|
|
140
|
+
{ term: /\blow[- ]hanging fruit\b/i, suggest: 'easiest tasks / quick wins', sources: 'SJSU, UX Content Co.' },
|
|
141
|
+
{ term: /\bsynergy\b/i, suggest: 'collaboration / combined effect (or rewrite)', sources: 'SJSU, plain language guides' },
|
|
142
|
+
{ term: /\bpivot\b(?!\s+(table|point))/i, suggest: 'change direction / shift focus', sources: 'UX Content Co.' },
|
|
143
|
+
{ term: /\bbandwidth\b(?!\s*(of|for|between|connection|limit|cap))/i, suggest: 'time / capacity / availability', sources: 'UX Content Co., SJSU' },
|
|
144
|
+
{ term: /\bcircle back\b/i, suggest: 'follow up / return to', sources: 'SJSU, Canadian Gov' },
|
|
145
|
+
{ term: /\btake (it )?offline\b/i, suggest: 'discuss separately / talk privately', sources: 'SJSU' },
|
|
146
|
+
{ term: /\bdeep[- ]?dive\b/i, suggest: 'thorough review / detailed look', sources: 'SJSU, Canadian Gov' },
|
|
147
|
+
{ term: /\bgranular\b/i, suggest: 'detailed / specific', sources: 'SJSU' },
|
|
148
|
+
{ term: /\blevel[- ]?set\b/i, suggest: 'align / agree on expectations', sources: 'SJSU' },
|
|
149
|
+
{ term: /\bpush the envelope\b/i, suggest: 'go beyond limits / innovate', sources: 'SJSU, A11y Collective' },
|
|
150
|
+
{ term: /\bthink outside the box\b/i, suggest: 'think creatively / find new approaches', sources: 'SJSU, A11y Collective' },
|
|
151
|
+
{ term: /\bwrap (my|your|our|their) head[s]? around\b/i, suggest: 'understand / make sense of', sources: 'A11y Collective' },
|
|
152
|
+
{ term: /\bone[- ]?size[- ]?fits[- ]?all\b/i, suggest: 'universal / the same for everyone', sources: 'Canadian Gov' },
|
|
153
|
+
{ term: /\bback to square one\b/i, suggest: 'starting over / back to the beginning', sources: 'SJSU, A11y Collective' },
|
|
154
|
+
{ term: /\bin the pipeline\b/i, suggest: 'planned / coming soon / in progress', sources: 'SJSU, A11y Collective' },
|
|
155
|
+
{ term: /\bon the same page\b/i, suggest: 'in agreement / aligned', sources: 'SJSU, Canadian Gov' },
|
|
156
|
+
{ term: /\bcatch[- ]22\b/i, suggest: 'impossible situation / no-win situation', sources: 'SJSU, A11y Collective' },
|
|
157
|
+
{ term: /\bwhistle[- ]?stop\b/i, suggest: 'brief / quick', sources: 'Canadian Gov' },
|
|
158
|
+
{ term: /\braise the bar\b/i, suggest: 'set a higher standard / improve expectations', sources: 'SJSU' },
|
|
159
|
+
{ term: /\bhit the ground running\b/i, suggest: 'start immediately / begin without delay', sources: 'SJSU, A11y Collective' },
|
|
160
|
+
{ term: /\bon the fence\b/i, suggest: 'undecided / uncertain', sources: 'SJSU, A11y Collective' },
|
|
161
|
+
{ term: /\bbite the bullet\b/i, suggest: 'endure something difficult / proceed despite difficulty', sources: 'SJSU, A11y Collective' },
|
|
162
|
+
{ term: /\bunder the weather\b/i, suggest: 'unwell / sick / not feeling well', sources: 'SJSU, A11y Collective' },
|
|
163
|
+
{ term: /\bball ?park (figure|estimate|number)?\b/i, suggest: 'rough estimate / approximate', sources: 'SJSU, Canadian Gov' },
|
|
164
|
+
{ term: /\bin the ball ?park\b/i, suggest: 'approximately / roughly', sources: 'SJSU, Canadian Gov' },
|
|
165
|
+
|
|
166
|
+
// Sports idioms - opaque to non-sports audiences, appear in ≥2 guides
|
|
167
|
+
{ term: /\bhit it out of the park\b/i, suggest: 'succeed greatly / do exceptionally well', sources: 'SJSU, A11y Collective' },
|
|
168
|
+
{ term: /\bslam[- ]?dunk\b/i, suggest: 'certain success / easy win', sources: 'SJSU, A11y Collective' },
|
|
169
|
+
{ term: /\bdrop the ball\b/i, suggest: 'make a mistake / fail to follow through', sources: 'SJSU, A11y Collective' },
|
|
170
|
+
{ term: /\bgame[- ]?changer\b/i, suggest: 'major shift / significant development', sources: 'SJSU, Canadian Gov' },
|
|
171
|
+
{ term: /\blevel (the |a )?playing field\b/i, suggest: 'create equal conditions / remove advantages', sources: 'SJSU, A11y Collective' },
|
|
172
|
+
{ term: /\bmove the goal[- ]?posts\b/i, suggest: 'change the requirements / shift the target', sources: 'SJSU, A11y Collective' },
|
|
173
|
+
{ term: /\bpinch[- ]?hitter\b/i, suggest: 'substitute / stand-in / backup', sources: 'SJSU' },
|
|
174
|
+
{ term: /\bkick[- ]?off\b(?!\s+(meeting|call|event))/i, suggest: 'start / begin / launch', sources: 'A11y Collective' },
|
|
175
|
+
{ term: /\btouch ?base\b/i, suggest: 'check in / reconnect / follow up', sources: 'SJSU, Canadian Gov' },
|
|
176
|
+
{ term: /\bpar for the course\b/i, suggest: 'expected / typical / normal', sources: 'SJSU' },
|
|
177
|
+
{ term: /\bfield (a |the )?question\b/i, suggest: 'answer / respond to / address', sources: 'SJSU' },
|
|
178
|
+
]
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Vague link and button text patterns.
|
|
182
|
+
*
|
|
183
|
+
* Methodology: WCAG SC 2.4.4 (Link Purpose in Context) requires link text to be
|
|
184
|
+
* understandable out of context. These patterns are the most-cited failures in
|
|
185
|
+
* WebAIM Million annual reports and appear in every accessible writing guide
|
|
186
|
+
* surveyed. Highest-confidence rules in this linter.
|
|
187
|
+
*
|
|
188
|
+
* Sources: W3C WAI, wcag.com/authors, Google Dev Style, SBA, UX Content Co.,
|
|
189
|
+
* A11y Collective, GOV.UK.
|
|
190
|
+
*/
|
|
191
|
+
export const VAGUE_CTA_PATTERNS = [
|
|
192
|
+
/^click here$/i,
|
|
193
|
+
/^here$/i,
|
|
194
|
+
/^read more$/i,
|
|
195
|
+
/^learn more$/i,
|
|
196
|
+
/^more$/i,
|
|
197
|
+
/^this$/i,
|
|
198
|
+
/^link$/i,
|
|
199
|
+
/^this link$/i,
|
|
200
|
+
/^this page$/i,
|
|
201
|
+
/^click$/i,
|
|
202
|
+
/^tap here$/i,
|
|
203
|
+
/^tap$/i,
|
|
204
|
+
/^go$/i,
|
|
205
|
+
/^details$/i,
|
|
206
|
+
/^info$/i,
|
|
207
|
+
/^information$/i,
|
|
208
|
+
]
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Alt text anti-patterns.
|
|
212
|
+
*
|
|
213
|
+
* Methodology: WCAG SC 1.1.1 requires text alternatives for non-text content.
|
|
214
|
+
* These specific patterns appear in W3C WAI Tips, Section 508 alt text guide,
|
|
215
|
+
* Google Dev Style, and double-great/alt-text (11-rule library). Independently
|
|
216
|
+
* validated against the WebAIM Million report's most common alt text failures.
|
|
217
|
+
*/
|
|
218
|
+
export const ALT_TEXT_PREFIXES = [
|
|
219
|
+
/^image of\s/i,
|
|
220
|
+
/^photo of\s/i,
|
|
221
|
+
/^picture of\s/i,
|
|
222
|
+
/^graphic of\s/i,
|
|
223
|
+
/^icon of\s/i,
|
|
224
|
+
/^illustration of\s/i,
|
|
225
|
+
/^screenshot of\s/i,
|
|
226
|
+
/^thumbnail of\s/i,
|
|
227
|
+
/^an image of\s/i,
|
|
228
|
+
/^a photo of\s/i,
|
|
229
|
+
/^a picture of\s/i,
|
|
230
|
+
]
|
|
231
|
+
|
|
232
|
+
export const ALT_TEXT_FILENAME_PATTERN = /\.(png|jpe?g|gif|svg|webp|bmp|avif|tiff?|ico)(\s.*)?$/i
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Directional language patterns.
|
|
236
|
+
*
|
|
237
|
+
* Methodology: content that references layout position breaks when users zoom,
|
|
238
|
+
* use screen readers, or view on small screens. Appears in SBA Style Guide and
|
|
239
|
+
* Google Dev Style as an explicit rule. Also aligns with WCAG SC 1.3.3
|
|
240
|
+
* (Sensory Characteristics).
|
|
241
|
+
*/
|
|
242
|
+
export const DIRECTIONAL_PATTERNS = [
|
|
243
|
+
{ term: /\b(the )?(right[- ]hand |left[- ]hand )?(side)?bar\b/i, suggest: 'use a heading or section name instead', sources: 'SBA, Google Dev Style' },
|
|
244
|
+
{ term: /\bin the (right|left) (column|panel|sidebar|navigation|nav|menu)\b/i, suggest: 'use a heading or section name instead', sources: 'SBA, Google Dev Style' },
|
|
245
|
+
{ term: /\b(see|refer to|check|click) (the )?(above|below)\b/i, suggest: '"see [section name]" or restructure', sources: 'SBA, Google Dev Style' },
|
|
246
|
+
{ term: /as (shown|seen|described|mentioned|noted) (above|below)\b/i, suggest: '"as described in [section]" or restructure', sources: 'SBA, Google Dev Style' },
|
|
247
|
+
{ term: /\bthe following (image|figure|table|chart|diagram) (above|below)\b/i, suggest: 'refer to the item by its caption or figure number', sources: 'SBA' },
|
|
248
|
+
]
|
|
249
|
+
|
|
250
|
+
// ─── Rule factories ───────────────────────────────────────────────────────────
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* no-ableist-language
|
|
254
|
+
*
|
|
255
|
+
* Flags slurs, condescending euphemisms, suffering/tragedy framing, and
|
|
256
|
+
* normalcy framing when writing about disability.
|
|
257
|
+
*
|
|
258
|
+
* WCAG basis: SC 3.1.1 (Language of Page) - content must be perceivable and
|
|
259
|
+
* understandable. While WCAG does not enumerate specific words, the intent of
|
|
260
|
+
* SC 3.1.1 and the WCAG understanding document explicitly notes that language
|
|
261
|
+
* that demeans or excludes users undermines accessibility.
|
|
262
|
+
*
|
|
263
|
+
* Expert consensus: Every disability language guide surveyed (NCDJ, AP, ADA NN,
|
|
264
|
+
* APA, SIGACCESS) independently prohibits these terms. Zero credible sources
|
|
265
|
+
* defend them. Severity: error for slurs, warn for euphemisms.
|
|
266
|
+
*
|
|
267
|
+
* Sources: NCDJ, AP Stylebook, ADA National Network, APA Style, SIGACCESS,
|
|
268
|
+
* A11y Collective.
|
|
269
|
+
*/
|
|
270
|
+
export function createNoAbleistLanguageRule() {
|
|
271
|
+
return {
|
|
272
|
+
meta: {
|
|
273
|
+
type: 'suggestion',
|
|
274
|
+
docs: {
|
|
275
|
+
description: 'Disallow ableist language, slurs, and suffering-framing when writing about disability',
|
|
276
|
+
url: 'https://github.com/a11yfred/neighbor#no-ableist-language',
|
|
277
|
+
},
|
|
278
|
+
messages: {
|
|
279
|
+
ableist: '"{{term}}" is ableist language. Suggestion: {{suggest}}. ({{sources}})',
|
|
280
|
+
},
|
|
281
|
+
schema: [
|
|
282
|
+
{
|
|
283
|
+
type: 'object',
|
|
284
|
+
properties: {
|
|
285
|
+
allow: { type: 'array', items: { type: 'string' } },
|
|
286
|
+
},
|
|
287
|
+
additionalProperties: false,
|
|
288
|
+
},
|
|
289
|
+
],
|
|
290
|
+
},
|
|
291
|
+
create(context) {
|
|
292
|
+
const allow = new Set((context.options[0]?.allow ?? []).map(s => s.toLowerCase()))
|
|
293
|
+
return {
|
|
294
|
+
Literal(node) {
|
|
295
|
+
if (typeof node.value !== 'string') return
|
|
296
|
+
checkTermList(node, node.value, ABLEIST_TERMS, allow, context, 'ableist')
|
|
297
|
+
},
|
|
298
|
+
TemplateLiteral(node) {
|
|
299
|
+
for (const quasi of node.quasis) {
|
|
300
|
+
checkTermList(quasi, quasi.value.raw, ABLEIST_TERMS, allow, context, 'ableist')
|
|
301
|
+
}
|
|
302
|
+
},
|
|
303
|
+
}
|
|
304
|
+
},
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/**
|
|
309
|
+
* no-disability-metaphor
|
|
310
|
+
*
|
|
311
|
+
* Flags figurative uses of disability-related language ("blind spot",
|
|
312
|
+
* "tone deaf", "paralyzed by").
|
|
313
|
+
*
|
|
314
|
+
* WCAG basis: No direct SC. Rule is grounded in expert consensus across ≥2
|
|
315
|
+
* independent authoritative disability language guides (NCDJ, A11y Collective,
|
|
316
|
+
* APA) and the W3C WAI guidance on inclusive writing. Severity: warn - these
|
|
317
|
+
* are common and authors may need context to revise.
|
|
318
|
+
*
|
|
319
|
+
* Sources: NCDJ, A11y Collective, APA Style.
|
|
320
|
+
*/
|
|
321
|
+
export function createNoDisabilityMetaphorRule() {
|
|
322
|
+
return {
|
|
323
|
+
meta: {
|
|
324
|
+
type: 'suggestion',
|
|
325
|
+
docs: {
|
|
326
|
+
description: 'Disallow figurative use of disability-related terms (e.g. "blind spot", "tone deaf")',
|
|
327
|
+
url: 'https://github.com/a11yfred/neighbor#no-disability-metaphor',
|
|
328
|
+
},
|
|
329
|
+
messages: {
|
|
330
|
+
metaphor: '"{{term}}" uses disability as a metaphor. Suggestion: {{suggest}}. ({{sources}})',
|
|
331
|
+
},
|
|
332
|
+
schema: [
|
|
333
|
+
{
|
|
334
|
+
type: 'object',
|
|
335
|
+
properties: {
|
|
336
|
+
allow: { type: 'array', items: { type: 'string' } },
|
|
337
|
+
},
|
|
338
|
+
additionalProperties: false,
|
|
339
|
+
},
|
|
340
|
+
],
|
|
341
|
+
},
|
|
342
|
+
create(context) {
|
|
343
|
+
const allow = new Set((context.options[0]?.allow ?? []).map(s => s.toLowerCase()))
|
|
344
|
+
return {
|
|
345
|
+
Literal(node) {
|
|
346
|
+
if (typeof node.value !== 'string') return
|
|
347
|
+
checkTermList(node, node.value, DISABILITY_METAPHORS, allow, context, 'metaphor')
|
|
348
|
+
},
|
|
349
|
+
TemplateLiteral(node) {
|
|
350
|
+
for (const quasi of node.quasis) {
|
|
351
|
+
checkTermList(quasi, quasi.value.raw, DISABILITY_METAPHORS, allow, context, 'metaphor')
|
|
352
|
+
}
|
|
353
|
+
},
|
|
354
|
+
}
|
|
355
|
+
},
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
/**
|
|
360
|
+
* no-english-idiom
|
|
361
|
+
*
|
|
362
|
+
* Flags English-language idioms and sports metaphors that are opaque to
|
|
363
|
+
* ESL readers, non-native English speakers, and international audiences.
|
|
364
|
+
*
|
|
365
|
+
* WCAG basis: SC 3.1.5 (Reading Level) - content should not require reading
|
|
366
|
+
* ability beyond lower secondary education level. Idioms systematically
|
|
367
|
+
* fail this requirement for non-native English speakers because their meaning
|
|
368
|
+
* cannot be inferred from constituent words.
|
|
369
|
+
*
|
|
370
|
+
* Expert consensus: Flagged in Canadian Gov accessible documents guide, SJSU
|
|
371
|
+
* accessible writing strategies, UX Content Co., and A11y Collective as a
|
|
372
|
+
* significant barrier for international and ESL users. This is the most novel
|
|
373
|
+
* rule in this linter - no existing a11y linting tool covers it.
|
|
374
|
+
*
|
|
375
|
+
* Sources: Canadian Gov, SJSU, UX Content Co., A11y Collective.
|
|
376
|
+
*/
|
|
377
|
+
export function createNoEnglishIdiomRule() {
|
|
378
|
+
return {
|
|
379
|
+
meta: {
|
|
380
|
+
type: 'suggestion',
|
|
381
|
+
docs: {
|
|
382
|
+
description: 'Disallow English idioms and sports metaphors that are opaque to ESL and international readers',
|
|
383
|
+
url: 'https://github.com/a11yfred/neighbor#no-english-idiom',
|
|
384
|
+
},
|
|
385
|
+
messages: {
|
|
386
|
+
idiom: '"{{term}}" is an idiom that may be unclear to ESL readers. Suggestion: {{suggest}}. ({{sources}})',
|
|
387
|
+
},
|
|
388
|
+
schema: [
|
|
389
|
+
{
|
|
390
|
+
type: 'object',
|
|
391
|
+
properties: {
|
|
392
|
+
allow: { type: 'array', items: { type: 'string' } },
|
|
393
|
+
},
|
|
394
|
+
additionalProperties: false,
|
|
395
|
+
},
|
|
396
|
+
],
|
|
397
|
+
},
|
|
398
|
+
create(context) {
|
|
399
|
+
const allow = new Set((context.options[0]?.allow ?? []).map(s => s.toLowerCase()))
|
|
400
|
+
return {
|
|
401
|
+
Literal(node) {
|
|
402
|
+
if (typeof node.value !== 'string') return
|
|
403
|
+
checkTermList(node, node.value, ENGLISH_IDIOMS, allow, context, 'idiom')
|
|
404
|
+
},
|
|
405
|
+
TemplateLiteral(node) {
|
|
406
|
+
for (const quasi of node.quasis) {
|
|
407
|
+
checkTermList(quasi, quasi.value.raw, ENGLISH_IDIOMS, allow, context, 'idiom')
|
|
408
|
+
}
|
|
409
|
+
},
|
|
410
|
+
}
|
|
411
|
+
},
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
/**
|
|
416
|
+
* no-vague-cta
|
|
417
|
+
*
|
|
418
|
+
* Flags vague call-to-action and link text like "click here", "read more",
|
|
419
|
+
* "learn more", or "here" used as the visible text of a link or button.
|
|
420
|
+
*
|
|
421
|
+
* WCAG basis: SC 2.4.4 (Link Purpose, In Context) - link purpose shall be
|
|
422
|
+
* determinable from the link text alone or from the link text together with
|
|
423
|
+
* its programmatically determined context. These patterns systematically fail
|
|
424
|
+
* the "link text alone" criterion.
|
|
425
|
+
*
|
|
426
|
+
* Expert consensus: Most-cited failure in WebAIM Million annual reports.
|
|
427
|
+
* Present in every accessible writing guide surveyed. Severity: error.
|
|
428
|
+
*
|
|
429
|
+
* Sources: W3C WAI, wcag.com/authors (SC 2.4.4), Google Dev Style, SBA,
|
|
430
|
+
* UX Content Co., A11y Collective, GOV.UK, WebAIM Million.
|
|
431
|
+
*/
|
|
432
|
+
export function createNoVagueCTARule() {
|
|
433
|
+
return {
|
|
434
|
+
meta: {
|
|
435
|
+
type: 'problem',
|
|
436
|
+
docs: {
|
|
437
|
+
description: 'Disallow vague link and button text like "click here", "read more", or "here" (WCAG 2.4.4)',
|
|
438
|
+
url: 'https://github.com/a11yfred/neighbor#no-vague-cta',
|
|
439
|
+
},
|
|
440
|
+
messages: {
|
|
441
|
+
vagueCta:
|
|
442
|
+
'"{{text}}" is vague link or button text that fails WCAG 2.4.4. Screen reader users navigating by links will hear "{{text}}" with no context. Use descriptive text that explains the destination or action. (W3C WAI, WebAIM Million)',
|
|
443
|
+
},
|
|
444
|
+
schema: [
|
|
445
|
+
{
|
|
446
|
+
type: 'object',
|
|
447
|
+
properties: {
|
|
448
|
+
allow: { type: 'array', items: { type: 'string' } },
|
|
449
|
+
},
|
|
450
|
+
additionalProperties: false,
|
|
451
|
+
},
|
|
452
|
+
],
|
|
453
|
+
},
|
|
454
|
+
create(context) {
|
|
455
|
+
const allow = new Set((context.options[0]?.allow ?? []).map(s => s.toLowerCase()))
|
|
456
|
+
return {
|
|
457
|
+
JSXElement(node) {
|
|
458
|
+
const opening = node.openingElement
|
|
459
|
+
const tag = opening.name?.name
|
|
460
|
+
if (tag !== 'a' && tag !== 'button' && tag !== 'Link') return
|
|
461
|
+
const text = extractJSXText(node).trim()
|
|
462
|
+
if (!text || allow.has(text.toLowerCase())) return
|
|
463
|
+
if (VAGUE_CTA_PATTERNS.some(p => p.test(text))) {
|
|
464
|
+
context.report({ node: opening, messageId: 'vagueCta', data: { text } })
|
|
465
|
+
}
|
|
466
|
+
},
|
|
467
|
+
}
|
|
468
|
+
},
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
/**
|
|
473
|
+
* no-directional-language
|
|
474
|
+
*
|
|
475
|
+
* Flags content that references position on screen using layout-dependent
|
|
476
|
+
* direction ("above", "in the right sidebar", "see below").
|
|
477
|
+
*
|
|
478
|
+
* WCAG basis: SC 1.3.3 (Sensory Characteristics) - instructions shall not
|
|
479
|
+
* rely solely on sensory characteristics of components, including location.
|
|
480
|
+
* Directional references fail for screen reader users, keyboard users, and
|
|
481
|
+
* users who zoom or reflowed the page.
|
|
482
|
+
*
|
|
483
|
+
* Expert consensus: Explicit rule in SBA Style Guide and Google Dev Style.
|
|
484
|
+
* Also covered in US Plain Language guide and A11y Collective.
|
|
485
|
+
*
|
|
486
|
+
* Sources: SBA Style Guide, Google Dev Style, A11y Collective, WCAG SC 1.3.3.
|
|
487
|
+
*/
|
|
488
|
+
export function createNoDirectionalLanguageRule() {
|
|
489
|
+
return {
|
|
490
|
+
meta: {
|
|
491
|
+
type: 'suggestion',
|
|
492
|
+
docs: {
|
|
493
|
+
description: 'Disallow layout-dependent directional references ("above", "in the right sidebar") (WCAG 1.3.3)',
|
|
494
|
+
url: 'https://github.com/a11yfred/neighbor#no-directional-language',
|
|
495
|
+
},
|
|
496
|
+
messages: {
|
|
497
|
+
directional:
|
|
498
|
+
'{{match}} uses layout position to give instructions. Position-based references break when users zoom, reflow, or use screen readers. {{suggest}} (SBA, Google Dev Style, WCAG SC 1.3.3)',
|
|
499
|
+
},
|
|
500
|
+
schema: [],
|
|
501
|
+
},
|
|
502
|
+
create(context) {
|
|
503
|
+
return {
|
|
504
|
+
Literal(node) {
|
|
505
|
+
if (typeof node.value !== 'string') return
|
|
506
|
+
for (const { term, suggest } of DIRECTIONAL_PATTERNS) {
|
|
507
|
+
const m = node.value.match(term)
|
|
508
|
+
if (m) {
|
|
509
|
+
context.report({ node, messageId: 'directional', data: { match: m[0], suggest } })
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
},
|
|
513
|
+
}
|
|
514
|
+
},
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
/**
|
|
519
|
+
* no-unexplained-abbreviation
|
|
520
|
+
*
|
|
521
|
+
* Flags abbreviations and acronyms used without a prior explanation in the
|
|
522
|
+
* same file.
|
|
523
|
+
*
|
|
524
|
+
* WCAG basis: SC 3.1.4 (Abbreviations) - a mechanism is available for
|
|
525
|
+
* identifying the expanded form or meaning of abbreviations. Also SC 3.1.5
|
|
526
|
+
* (Reading Level) - unexplained jargon raises effective reading level.
|
|
527
|
+
*
|
|
528
|
+
* Expert consensus: Present in Google Dev Style, GOV.UK, wcag.com/authors,
|
|
529
|
+
* SBA Style Guide, US Plain Language guide, and Canadian Gov guide.
|
|
530
|
+
*
|
|
531
|
+
* Note: This rule tracks first use within each file and exempts known common
|
|
532
|
+
* abbreviations. Authors may configure additional exemptions.
|
|
533
|
+
*
|
|
534
|
+
* Sources: Google Dev Style, GOV.UK, wcag.com/authors (SC 3.1.4), SBA, US PL,
|
|
535
|
+
* Canadian Gov.
|
|
536
|
+
*/
|
|
537
|
+
export function createNoUnexplainedAbbreviationRule() {
|
|
538
|
+
// Abbreviations universally understood without expansion per plain language guides
|
|
539
|
+
const ALWAYS_KNOWN = new Set([
|
|
540
|
+
'HTML', 'CSS', 'JS', 'URL', 'API', 'PDF', 'UI', 'UX', 'ID', 'FAQ',
|
|
541
|
+
'OK', 'US', 'UK', 'EU', 'UN', 'NATO', 'NASA', 'FBI', 'CIA', 'CDC',
|
|
542
|
+
'WHO', 'GPS', 'WIFI', 'USB', 'HDMI', 'TV', 'PC', 'AM', 'PM', 'EST',
|
|
543
|
+
'PST', 'GMT', 'UTC', 'HTTP', 'HTTPS', 'FTP', 'SSH', 'SQL', 'JSON',
|
|
544
|
+
'XML', 'SVG', 'PNG', 'JPG', 'GIF', 'MP4', 'MP3', 'AI', 'ML', 'CEO',
|
|
545
|
+
'CFO', 'CTO', 'HR', 'IT', 'PR', 'ROI', 'KPI', 'SLA', 'MVP', 'Q1',
|
|
546
|
+
'Q2', 'Q3', 'Q4', 'BC', 'AD', 'AKA', 'ETA', 'RSVP', 'DIY', 'ADA',
|
|
547
|
+
'WCAG', 'ARIA', 'WAI', 'W3C', 'ISO', 'RFC', 'IPv4', 'IPv6', 'DNS',
|
|
548
|
+
'VPN', 'RAM', 'CPU', 'GPU', 'SSD', 'iOS', 'macOS', 'ARIA',
|
|
549
|
+
])
|
|
550
|
+
|
|
551
|
+
return {
|
|
552
|
+
meta: {
|
|
553
|
+
type: 'suggestion',
|
|
554
|
+
docs: {
|
|
555
|
+
description: 'Require abbreviations and acronyms to be expanded on first use (WCAG 3.1.4)',
|
|
556
|
+
url: 'https://github.com/a11yfred/neighbor#no-unexplained-abbreviation',
|
|
557
|
+
},
|
|
558
|
+
messages: {
|
|
559
|
+
unexplained:
|
|
560
|
+
'"{{abbr}}" is used without explanation. Expand it on first use: "{{abbr}} (full name)" or "full name ({{abbr}})". (WCAG SC 3.1.4, Google Dev Style, GOV.UK)',
|
|
561
|
+
},
|
|
562
|
+
schema: [
|
|
563
|
+
{
|
|
564
|
+
type: 'object',
|
|
565
|
+
properties: {
|
|
566
|
+
known: { type: 'array', items: { type: 'string' } },
|
|
567
|
+
},
|
|
568
|
+
additionalProperties: false,
|
|
569
|
+
},
|
|
570
|
+
],
|
|
571
|
+
},
|
|
572
|
+
create(context) {
|
|
573
|
+
const extraKnown = new Set(context.options[0]?.known ?? [])
|
|
574
|
+
const isKnown = abbr => ALWAYS_KNOWN.has(abbr) || extraKnown.has(abbr)
|
|
575
|
+
const defined = new Set()
|
|
576
|
+
const EXPANSION_PATTERN = /\b([A-Z][A-Z0-9]{1,9})\s*\(([^)]{3,})\)|\b([^()]{3,})\s*\(([A-Z][A-Z0-9]{1,9})\)/g
|
|
577
|
+
const ABBR_PATTERN = /\b([A-Z][A-Z0-9]{1,9})\b/g
|
|
578
|
+
|
|
579
|
+
return {
|
|
580
|
+
Program() {
|
|
581
|
+
defined.clear()
|
|
582
|
+
},
|
|
583
|
+
Literal(node) {
|
|
584
|
+
if (typeof node.value !== 'string') return
|
|
585
|
+
const text = node.value
|
|
586
|
+
let m
|
|
587
|
+
EXPANSION_PATTERN.lastIndex = 0
|
|
588
|
+
while ((m = EXPANSION_PATTERN.exec(text)) !== null) {
|
|
589
|
+
defined.add(m[1] ?? m[4])
|
|
590
|
+
}
|
|
591
|
+
ABBR_PATTERN.lastIndex = 0
|
|
592
|
+
while ((m = ABBR_PATTERN.exec(text)) !== null) {
|
|
593
|
+
const abbr = m[1]
|
|
594
|
+
if (isKnown(abbr) || defined.has(abbr)) continue
|
|
595
|
+
context.report({
|
|
596
|
+
node,
|
|
597
|
+
messageId: 'unexplained',
|
|
598
|
+
data: { abbr },
|
|
599
|
+
loc: {
|
|
600
|
+
start: { line: node.loc.start.line, column: node.loc.start.column + 1 + m.index },
|
|
601
|
+
end: { line: node.loc.start.line, column: node.loc.start.column + 1 + m.index + abbr.length },
|
|
602
|
+
},
|
|
603
|
+
})
|
|
604
|
+
defined.add(abbr)
|
|
605
|
+
}
|
|
606
|
+
},
|
|
607
|
+
}
|
|
608
|
+
},
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
/**
|
|
613
|
+
* no-all-caps-prose
|
|
614
|
+
*
|
|
615
|
+
* Flags words written in ALL CAPS in prose content.
|
|
616
|
+
*
|
|
617
|
+
* WCAG basis: No direct SC. However, screen readers using certain verbosity
|
|
618
|
+
* settings read ALL CAPS letter-by-letter (Google Dev Style cites this
|
|
619
|
+
* explicitly). Also degrades readability for users with dyslexia and cognitive
|
|
620
|
+
* disabilities.
|
|
621
|
+
*
|
|
622
|
+
* Expert consensus: Explicit rule in Google Dev Style, GOV.UK publishing guide,
|
|
623
|
+
* and Canadian Gov guide. Excluded: known uppercase acronyms (HTML, CSS, etc.)
|
|
624
|
+
* and words < 3 characters.
|
|
625
|
+
*
|
|
626
|
+
* Sources: Google Dev Style, GOV.UK, Canadian Gov.
|
|
627
|
+
*/
|
|
628
|
+
export function createNoAllCapsProse() {
|
|
629
|
+
const KNOWN_ACRONYMS = new Set([
|
|
630
|
+
'HTML', 'CSS', 'JS', 'URL', 'API', 'PDF', 'UI', 'UX', 'ID', 'FAQ',
|
|
631
|
+
'OK', 'US', 'UK', 'EU', 'UN', 'NATO', 'NASA', 'FBI', 'CIA', 'CDC',
|
|
632
|
+
'WHO', 'GPS', 'USB', 'HDMI', 'TV', 'PC', 'AM', 'PM', 'EST', 'PST',
|
|
633
|
+
'GMT', 'UTC', 'HTTP', 'HTTPS', 'FTP', 'SSH', 'SQL', 'JSON', 'XML',
|
|
634
|
+
'SVG', 'PNG', 'JPG', 'GIF', 'MP4', 'MP3', 'AI', 'ML', 'CEO', 'CFO',
|
|
635
|
+
'CTO', 'HR', 'IT', 'PR', 'ROI', 'KPI', 'SLA', 'MVP', 'ADA', 'WCAG',
|
|
636
|
+
'ARIA', 'WAI', 'W3C', 'ISO', 'RFC', 'VPN', 'RAM', 'CPU', 'GPU', 'SSD',
|
|
637
|
+
'DIY', 'AKA', 'ETA', 'RSVP', 'TBD', 'TBA', 'FYI', 'ASAP', 'NOTE',
|
|
638
|
+
'IMPORTANT', 'WARNING', 'CAUTION', 'DEPRECATED', 'TODO',
|
|
639
|
+
])
|
|
640
|
+
const ALL_CAPS = /\b([A-Z]{3,})\b/g
|
|
641
|
+
|
|
642
|
+
return {
|
|
643
|
+
meta: {
|
|
644
|
+
type: 'suggestion',
|
|
645
|
+
docs: {
|
|
646
|
+
description: 'Disallow ALL CAPS in prose content - screen readers may spell it out letter by letter',
|
|
647
|
+
url: 'https://github.com/a11yfred/neighbor#no-all-caps-prose',
|
|
648
|
+
},
|
|
649
|
+
messages: {
|
|
650
|
+
allCaps:
|
|
651
|
+
'"{{word}}" is written in ALL CAPS. Screen readers using high verbosity may read it letter-by-letter. Use regular casing; add to the `known` option if this is a recognized acronym. (Google Dev Style, GOV.UK)',
|
|
652
|
+
},
|
|
653
|
+
schema: [
|
|
654
|
+
{
|
|
655
|
+
type: 'object',
|
|
656
|
+
properties: {
|
|
657
|
+
known: { type: 'array', items: { type: 'string' } },
|
|
658
|
+
},
|
|
659
|
+
additionalProperties: false,
|
|
660
|
+
},
|
|
661
|
+
],
|
|
662
|
+
},
|
|
663
|
+
create(context) {
|
|
664
|
+
const extraKnown = new Set(context.options[0]?.known ?? [])
|
|
665
|
+
const isKnown = w => KNOWN_ACRONYMS.has(w) || extraKnown.has(w)
|
|
666
|
+
|
|
667
|
+
return {
|
|
668
|
+
Literal(node) {
|
|
669
|
+
if (typeof node.value !== 'string') return
|
|
670
|
+
let m
|
|
671
|
+
ALL_CAPS.lastIndex = 0
|
|
672
|
+
while ((m = ALL_CAPS.exec(node.value)) !== null) {
|
|
673
|
+
const word = m[1]
|
|
674
|
+
if (isKnown(word)) continue
|
|
675
|
+
context.report({
|
|
676
|
+
node,
|
|
677
|
+
messageId: 'allCaps',
|
|
678
|
+
data: { word },
|
|
679
|
+
loc: {
|
|
680
|
+
start: { line: node.loc.start.line, column: node.loc.start.column + 1 + m.index },
|
|
681
|
+
end: { line: node.loc.start.line, column: node.loc.start.column + 1 + m.index + word.length },
|
|
682
|
+
},
|
|
683
|
+
})
|
|
684
|
+
}
|
|
685
|
+
},
|
|
686
|
+
}
|
|
687
|
+
},
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
/**
|
|
692
|
+
* no-vague-error-message
|
|
693
|
+
*
|
|
694
|
+
* Flags error messages that do not explain what went wrong.
|
|
695
|
+
*
|
|
696
|
+
* WCAG basis: SC 3.3.1 (Error Identification) - if an input error is automatically
|
|
697
|
+
* detected, the item in error shall be described to the user. A message like
|
|
698
|
+
* "An error occurred" fails this because it identifies no item. SC 3.3.3
|
|
699
|
+
* (Error Suggestion) - if input error is detected, suggestions for correction
|
|
700
|
+
* shall be provided.
|
|
701
|
+
*
|
|
702
|
+
* Expert consensus: UX Content Co. and Google Dev Style both explicitly call out
|
|
703
|
+
* these patterns. Severity: warn - false positives possible when the string is
|
|
704
|
+
* a template partial.
|
|
705
|
+
*
|
|
706
|
+
* Sources: UX Content Co., Google Dev Style, WCAG SC 3.3.1, SC 3.3.3.
|
|
707
|
+
*/
|
|
708
|
+
export function createNoVagueErrorMessageRule() {
|
|
709
|
+
const VAGUE_ERRORS = [
|
|
710
|
+
/^an? error occurred\.?$/i,
|
|
711
|
+
/^something went wrong\.?$/i,
|
|
712
|
+
/^error\.?$/i,
|
|
713
|
+
/^unknown error\.?$/i,
|
|
714
|
+
/^unexpected error\.?$/i,
|
|
715
|
+
/^oops[!.]?$/i,
|
|
716
|
+
/^oops,? something went wrong[!.]?$/i,
|
|
717
|
+
/^request failed\.?$/i,
|
|
718
|
+
/^operation failed\.?$/i,
|
|
719
|
+
/^failed\.?$/i,
|
|
720
|
+
/^try again later\.?$/i,
|
|
721
|
+
/^please try again\.?$/i,
|
|
722
|
+
]
|
|
723
|
+
|
|
724
|
+
return {
|
|
725
|
+
meta: {
|
|
726
|
+
type: 'suggestion',
|
|
727
|
+
docs: {
|
|
728
|
+
description: 'Disallow vague error messages that do not explain what went wrong (WCAG 3.3.1)',
|
|
729
|
+
url: 'https://github.com/a11yfred/neighbor#no-vague-error-message',
|
|
730
|
+
},
|
|
731
|
+
messages: {
|
|
732
|
+
vagueError:
|
|
733
|
+
'"{{text}}" does not explain what went wrong or how to fix it, failing WCAG SC 3.3.1 and 3.3.3. Describe the specific error and provide a corrective action. (UX Content Co., Google Dev Style)',
|
|
734
|
+
},
|
|
735
|
+
schema: [],
|
|
736
|
+
},
|
|
737
|
+
create(context) {
|
|
738
|
+
return {
|
|
739
|
+
Literal(node) {
|
|
740
|
+
if (typeof node.value !== 'string') return
|
|
741
|
+
const text = node.value.trim()
|
|
742
|
+
if (text.length > 120) return
|
|
743
|
+
if (VAGUE_ERRORS.some(p => p.test(text))) {
|
|
744
|
+
context.report({ node, messageId: 'vagueError', data: { text } })
|
|
745
|
+
}
|
|
746
|
+
},
|
|
747
|
+
}
|
|
748
|
+
},
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
/**
|
|
753
|
+
* no-ampersand-in-prose
|
|
754
|
+
*
|
|
755
|
+
* Flags `&` used in place of "and" in prose text.
|
|
756
|
+
*
|
|
757
|
+
* WCAG basis: No direct SC. Screen readers may announce `&` inconsistently
|
|
758
|
+
* across AT vendors - some say "ampersand", some skip it. Google Dev Style
|
|
759
|
+
* cites this as an explicit accessibility concern.
|
|
760
|
+
*
|
|
761
|
+
* Expert consensus: Google Dev Style is the primary source; also noted in
|
|
762
|
+
* plain language guides as informal register that reduces clarity.
|
|
763
|
+
* Excludes legitimate code/UI uses (checking for literal `&` not `&`).
|
|
764
|
+
*
|
|
765
|
+
* Sources: Google Dev Style, US Plain Language guide.
|
|
766
|
+
*/
|
|
767
|
+
export function createNoAmpersandInProseRule() {
|
|
768
|
+
return {
|
|
769
|
+
meta: {
|
|
770
|
+
type: 'suggestion',
|
|
771
|
+
docs: {
|
|
772
|
+
description: 'Disallow & as a substitute for "and" in prose - screen readers may not announce it consistently',
|
|
773
|
+
url: 'https://github.com/a11yfred/neighbor#no-ampersand-in-prose',
|
|
774
|
+
},
|
|
775
|
+
messages: {
|
|
776
|
+
ampersand:
|
|
777
|
+
'"&" may be announced inconsistently by screen readers. Use "and" in prose. (Google Dev Style)',
|
|
778
|
+
},
|
|
779
|
+
schema: [],
|
|
780
|
+
},
|
|
781
|
+
create(context) {
|
|
782
|
+
return {
|
|
783
|
+
Literal(node) {
|
|
784
|
+
if (typeof node.value !== 'string') return
|
|
785
|
+
if (/\s&\s/.test(node.value)) {
|
|
786
|
+
context.report({ node, messageId: 'ampersand' })
|
|
787
|
+
}
|
|
788
|
+
},
|
|
789
|
+
}
|
|
790
|
+
},
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
// ─── Rule export map ─────────────────────────────────────────────────────────
|
|
795
|
+
|
|
796
|
+
export const CONTENT_RULE_FACTORIES = {
|
|
797
|
+
'no-ableist-language': createNoAbleistLanguageRule,
|
|
798
|
+
'no-disability-metaphor': createNoDisabilityMetaphorRule,
|
|
799
|
+
'no-english-idiom': createNoEnglishIdiomRule,
|
|
800
|
+
'no-vague-cta': createNoVagueCTARule,
|
|
801
|
+
'no-directional-language': createNoDirectionalLanguageRule,
|
|
802
|
+
'no-unexplained-abbreviation': createNoUnexplainedAbbreviationRule,
|
|
803
|
+
'no-all-caps-prose': createNoAllCapsProse,
|
|
804
|
+
'no-vague-error-message': createNoVagueErrorMessageRule,
|
|
805
|
+
'no-ampersand-in-prose': createNoAmpersandInProseRule,
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
/**
|
|
809
|
+
* Recommended severity config.
|
|
810
|
+
*
|
|
811
|
+
* Tiers:
|
|
812
|
+
* error - WCAG statutory basis + universal expert consensus + low false-positive rate
|
|
813
|
+
* warn - strong expert consensus but higher false-positive risk or context-dependence
|
|
814
|
+
* off - valid rule but too noisy for most codebases; opt in individually
|
|
815
|
+
*/
|
|
816
|
+
export function buildContentRecommendedRules(ns) {
|
|
817
|
+
return {
|
|
818
|
+
[`${ns}/no-ableist-language`]: 'warn',
|
|
819
|
+
[`${ns}/no-disability-metaphor`]: 'warn',
|
|
820
|
+
[`${ns}/no-english-idiom`]: 'warn',
|
|
821
|
+
[`${ns}/no-vague-cta`]: 'warn',
|
|
822
|
+
[`${ns}/no-directional-language`]: 'warn',
|
|
823
|
+
[`${ns}/no-unexplained-abbreviation`]: 'warn',
|
|
824
|
+
[`${ns}/no-all-caps-prose`]: 'warn',
|
|
825
|
+
[`${ns}/no-vague-error-message`]: 'warn',
|
|
826
|
+
[`${ns}/no-ampersand-in-prose`]: 'warn',
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
// ─── Helpers ─────────────────────────────────────────────────────────────────
|
|
831
|
+
|
|
832
|
+
function checkTermList(node, text, termList, allow, context, messageId) {
|
|
833
|
+
for (const { term, suggest, sources } of termList) {
|
|
834
|
+
const m = text.match(term)
|
|
835
|
+
if (!m) continue
|
|
836
|
+
const matched = m[0]
|
|
837
|
+
if (allow.has(matched.toLowerCase())) continue
|
|
838
|
+
context.report({
|
|
839
|
+
node,
|
|
840
|
+
messageId,
|
|
841
|
+
data: { term: matched, suggest, sources },
|
|
842
|
+
})
|
|
843
|
+
}
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
function extractJSXText(node) {
|
|
847
|
+
let text = ''
|
|
848
|
+
for (const child of node.children ?? []) {
|
|
849
|
+
if (child.type === 'JSXText') {
|
|
850
|
+
text += child.value
|
|
851
|
+
} else if (child.type === 'JSXExpressionContainer' && child.expression.type === 'Literal') {
|
|
852
|
+
text += String(child.expression.value)
|
|
853
|
+
} else if (child.type === 'JSXElement') {
|
|
854
|
+
text += extractJSXText(child)
|
|
855
|
+
}
|
|
856
|
+
}
|
|
857
|
+
return text
|
|
858
|
+
}
|