@a11yfred/neighbor 0.3.0 → 1.0.1

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.
@@ -0,0 +1,80 @@
1
+ /**
2
+ * @a11yfred/neighbor - Content linting plugin
3
+ *
4
+ * Flags accessibility and inclusion problems in web and app copy: ableist
5
+ * language, disability metaphors, English idioms that exclude ESL readers,
6
+ * vague link text, directional layout references, unexplained abbreviations,
7
+ * ALL CAPS prose, and vague error messages.
8
+ *
9
+ * These rules operate on string literals and JSX text in JavaScript,
10
+ * TypeScript, JSX, and TSX files. They target the gap between markup
11
+ * accessibility (jsx-a11y, neighbor ESLint) and prose accessibility -
12
+ * no existing a11y linting tool covers this space.
13
+ *
14
+ * Entry point: @a11yfred/neighbor/content
15
+ *
16
+ * ─── Rule methodology ────────────────────────────────────────────────────────
17
+ *
18
+ * A rule is included only when all three conditions hold:
19
+ * 1. A WCAG Success Criterion directly applies, OR the rule appears in ≥3
20
+ * independent authoritative style guides.
21
+ * 2. The rule can be expressed as a finite, deterministic pattern (string
22
+ * match, token count, or AST shape) - no NLP, no runtime context.
23
+ * 3. Expert consensus is unambiguous across sources. No credible authority
24
+ * argues the opposite.
25
+ *
26
+ * Rules that require subjective reading (tone, full cultural sensitivity
27
+ * beyond a term list) or are under active community debate are excluded.
28
+ *
29
+ * ─── Sources ────────────────────────────────────────────────────────────────
30
+ *
31
+ * International accessible writing guides:
32
+ * United States plain language.gov, digital.gov/guides/plain-language
33
+ * United States advocacy.sba.gov - SBA Content Style Guide
34
+ * United Kingdom gov.uk/guidance/publishing-accessible-documents
35
+ * United Kingdom accessibility-manual.dwp.gov.uk/best-practice/writing-content
36
+ * Australia stylemanual.gov.au/accessible-and-inclusive-content
37
+ * Canada accessible.canada.ca/guidelines-creating-accessible-documents
38
+ * Global (W3C) w3.org/WAI/tips/writing/ - primary authority
39
+ * Global (W3C) wcag.com/authors/
40
+ *
41
+ * Disability language authorities:
42
+ * NCDJ cronkite.asu.edu/ncdj/disability-language-style-guide
43
+ * AP Stylebook amdisrights.org/ap-stylebook-primer-on-disability
44
+ * ADA Nat. Network adata.org/factsheet/ADANN-writing
45
+ * APA Style apastyle.apa.org/style-grammar-guidelines/bias-free-language/disability
46
+ * SIGACCESS sigaccess.org/…/accessible-writing-guide/
47
+ * Nicolas Steenhout incl.ca/disability-language-is-a-nuanced-thing/ - Nothing About Us Without Us; language as community choice
48
+ * Léonie Watson tink.uk - "There is no right or wrong answer because it is a matter of personal choice, and the choice depends on context." (cited by Steenhout)
49
+ *
50
+ * Technical writing:
51
+ * Google Dev Style developers.google.com/style/accessibility
52
+ * Section 508 section508.gov/create/alternative-text/
53
+ *
54
+ * UX and content design:
55
+ * UX Content Co. uxcontent.com/accessible-ux-writing-a-guide-for-inclusive-content-design/
56
+ * A11y Collective a11y-collective.com/blog/accessible-writing/
57
+ * SJSU Writing Ctr sjsu.edu/writingcenter/docs/handouts/Accessible Writing Strategies.pdf
58
+ */
59
+
60
+ import { CONTENT_RULE_FACTORIES, buildContentRecommendedRules } from './lib/content-rules.js'
61
+
62
+ const NS = '@a11yfred/neighbor/content'
63
+
64
+ const rules = Object.fromEntries(
65
+ Object.entries(CONTENT_RULE_FACTORIES).map(([name, factory]) => [name, factory()])
66
+ )
67
+
68
+ const plugin = { meta: { name: NS }, rules }
69
+
70
+ export default {
71
+ ...plugin,
72
+ configs: {
73
+ recommended: {
74
+ plugins: {
75
+ [NS]: plugin,
76
+ },
77
+ rules: buildContentRecommendedRules(NS),
78
+ },
79
+ },
80
+ }
@@ -1,68 +1,68 @@
1
- /**
2
- * @a11yfred/neighborESLint plugin (Angular templates)
3
- *
4
- * Flags the same ARIA anti-patterns as neighbor-eslint.mjs but for Angular
5
- * component templates. Requires @angular-eslint/template-parser.
6
- *
7
- * Rules that require ancestor walking (no-log-with-interactive-children,
8
- * no-menu-role-on-nav, no-heading-inside-interactive) are limited because
9
- * @angular-eslint/template-parser does not attach parent references to nodes.
10
- * Those rules will still fire for direct matches but cannot walk the tree.
11
- *
12
- * Usage in eslint.config.js:
13
- * import angularTemplateParser from '@angular-eslint/template-parser'
14
- * import neighbor from '@a11yfred/neighbor/angular'
15
- *
16
- * export default [
17
- * {
18
- * files: ['**\/*.html'],
19
- * languageOptions: { parser: angularTemplateParser },
20
- * plugins: { '@a11yfred/neighbor': neighbor },
21
- * rules: neighbor.configs.recommended.rules,
22
- * },
23
- * ]
24
- */
25
-
26
- import { h } from './lib/helpers-angular.js'
27
- import { buildRules, buildRecommendedRules, buildPortabilityRules } from './lib/rules.js'
28
- import { buildUlamRulesAngular, buildUlamRecommendedRulesFramework } from './lib/ulam-rules.js'
29
-
30
- const NS = '@a11yfred/neighbor'
31
- const rules = { ...buildRules(h), ...buildUlamRulesAngular() }
32
- const plugin = { meta: { name: `${NS}/angular` }, rules }
33
-
34
- let angularA11y = null
35
- try { angularA11y = (await import('@angular-eslint/eslint-plugin-template')).default } catch {}
36
-
37
- const ANGULAR_A11Y_RULES = [
38
- 'alt-text', 'click-events-have-key-events', 'elements-content',
39
- 'interactive-supports-focus', 'label-has-associated-control',
40
- 'mouse-events-have-key-events', 'no-autofocus', 'no-distracting-elements',
41
- 'no-positive-tabindex', 'role-has-required-aria', 'table-scope', 'valid-aria',
42
- ]
43
-
44
- function getAngularA11yRules(plugin) {
45
- const out = {}
46
- for (const rule of ANGULAR_A11Y_RULES) {
47
- if (plugin.rules?.[rule]) out[`@angular-eslint/template/${rule}`] = 'error'
48
- }
49
- return out
50
- }
51
-
52
- export default {
53
- ...plugin,
54
- configs: {
55
- recommended: {
56
- plugins: {
57
- [NS]: plugin,
58
- ...(angularA11y ? { '@angular-eslint/template': angularA11y } : {}),
59
- },
60
- rules: {
61
- ...(angularA11y ? getAngularA11yRules(angularA11y) : {}),
62
- ...buildRecommendedRules(NS),
63
- ...buildPortabilityRules(NS),
64
- ...buildUlamRecommendedRulesFramework(NS),
65
- },
66
- },
67
- },
68
- }
1
+ /**
2
+ * @a11yfred/neighbor - ESLint plugin (Angular templates)
3
+ *
4
+ * Flags the same ARIA anti-patterns as neighbor-eslint.mjs but for Angular
5
+ * component templates. Requires @angular-eslint/template-parser.
6
+ *
7
+ * Rules that require ancestor walking (no-log-with-interactive-children,
8
+ * no-menu-role-on-nav, no-heading-inside-interactive) are limited because
9
+ * @angular-eslint/template-parser does not attach parent references to nodes.
10
+ * Those rules will still fire for direct matches but cannot walk the tree.
11
+ *
12
+ * Usage in eslint.config.js:
13
+ * import angularTemplateParser from '@angular-eslint/template-parser'
14
+ * import neighbor from '@a11yfred/neighbor/angular'
15
+ *
16
+ * export default [
17
+ * {
18
+ * files: ['**\/*.html'],
19
+ * languageOptions: { parser: angularTemplateParser },
20
+ * plugins: { '@a11yfred/neighbor': neighbor },
21
+ * rules: neighbor.configs.recommended.rules,
22
+ * },
23
+ * ]
24
+ */
25
+
26
+ import { h } from './lib/helpers-angular.js'
27
+ import { buildRules, buildRecommendedRules, buildPortabilityRules } from './lib/rules.js'
28
+ import { buildUlamRulesAngular, buildUlamRecommendedRulesFramework } from './lib/ulam-rules.js'
29
+
30
+ const NS = '@a11yfred/neighbor'
31
+ const rules = { ...buildRules(h), ...buildUlamRulesAngular() }
32
+ const plugin = { meta: { name: `${NS}/angular` }, rules }
33
+
34
+ let angularA11y = null
35
+ try { angularA11y = (await import('@angular-eslint/eslint-plugin-template')).default } catch {}
36
+
37
+ const ANGULAR_A11Y_RULES = [
38
+ 'alt-text', 'click-events-have-key-events', 'elements-content',
39
+ 'interactive-supports-focus', 'label-has-associated-control',
40
+ 'mouse-events-have-key-events', 'no-autofocus', 'no-distracting-elements',
41
+ 'no-positive-tabindex', 'role-has-required-aria', 'table-scope', 'valid-aria',
42
+ ]
43
+
44
+ function getAngularA11yRules(plugin) {
45
+ const out = {}
46
+ for (const rule of ANGULAR_A11Y_RULES) {
47
+ if (plugin.rules?.[rule]) out[`@angular-eslint/template/${rule}`] = 'error'
48
+ }
49
+ return out
50
+ }
51
+
52
+ export default {
53
+ ...plugin,
54
+ configs: {
55
+ recommended: {
56
+ plugins: {
57
+ [NS]: plugin,
58
+ ...(angularA11y ? { '@angular-eslint/template': angularA11y } : {}),
59
+ },
60
+ rules: {
61
+ ...(angularA11y ? getAngularA11yRules(angularA11y) : {}),
62
+ ...buildRecommendedRules(NS),
63
+ ...buildPortabilityRules(NS),
64
+ ...buildUlamRecommendedRulesFramework(NS),
65
+ },
66
+ },
67
+ },
68
+ }
@@ -1,48 +1,48 @@
1
- /**
2
- * @a11yfred/neighborESLint plugin (Vue SFCs)
3
- *
4
- * Flags the same ARIA anti-patterns as neighbor-eslint.mjs but for Vue templates.
5
- * Requires vue-eslint-parser as the project's ESLint parser for .vue files.
6
- *
7
- * Usage in eslint.config.js:
8
- * import vueParser from 'vue-eslint-parser'
9
- * import neighbor from '@a11yfred/neighbor/vue'
10
- *
11
- * export default [
12
- * {
13
- * files: ['**\/*.vue'],
14
- * languageOptions: { parser: vueParser },
15
- * plugins: { '@a11yfred/neighbor': neighbor },
16
- * rules: neighbor.configs.recommended.rules,
17
- * },
18
- * ]
19
- */
20
-
21
- import { h } from './lib/helpers-vue.js'
22
- import { buildRules, buildRecommendedRules, buildPortabilityRules } from './lib/rules.js'
23
- import { buildUlamRulesVue, buildUlamRecommendedRulesFramework } from './lib/ulam-rules.js'
24
-
25
- const NS = '@a11yfred/neighbor'
26
- const rules = { ...buildRules(h), ...buildUlamRulesVue() }
27
- const plugin = { meta: { name: `${NS}/vue` }, rules }
28
-
29
- let vueA11y = null
30
- try { vueA11y = (await import('eslint-plugin-vuejs-accessibility')).default } catch {}
31
-
32
- export default {
33
- ...plugin,
34
- configs: {
35
- recommended: {
36
- plugins: {
37
- [NS]: plugin,
38
- ...(vueA11y ? { 'vuejs-accessibility': vueA11y } : {}),
39
- },
40
- rules: {
41
- ...(vueA11y ? vueA11y.configs['flat/recommended'].rules : {}),
42
- ...buildRecommendedRules(NS),
43
- ...buildPortabilityRules(NS),
44
- ...buildUlamRecommendedRulesFramework(NS),
45
- },
46
- },
47
- },
48
- }
1
+ /**
2
+ * @a11yfred/neighbor - ESLint plugin (Vue SFCs)
3
+ *
4
+ * Flags the same ARIA anti-patterns as neighbor-eslint.mjs but for Vue templates.
5
+ * Requires vue-eslint-parser as the project's ESLint parser for .vue files.
6
+ *
7
+ * Usage in eslint.config.js:
8
+ * import vueParser from 'vue-eslint-parser'
9
+ * import neighbor from '@a11yfred/neighbor/vue'
10
+ *
11
+ * export default [
12
+ * {
13
+ * files: ['**\/*.vue'],
14
+ * languageOptions: { parser: vueParser },
15
+ * plugins: { '@a11yfred/neighbor': neighbor },
16
+ * rules: neighbor.configs.recommended.rules,
17
+ * },
18
+ * ]
19
+ */
20
+
21
+ import { h } from './lib/helpers-vue.js'
22
+ import { buildRules, buildRecommendedRules, buildPortabilityRules } from './lib/rules.js'
23
+ import { buildUlamRulesVue, buildUlamRecommendedRulesFramework } from './lib/ulam-rules.js'
24
+
25
+ const NS = '@a11yfred/neighbor'
26
+ const rules = { ...buildRules(h), ...buildUlamRulesVue() }
27
+ const plugin = { meta: { name: `${NS}/vue` }, rules }
28
+
29
+ let vueA11y = null
30
+ try { vueA11y = (await import('eslint-plugin-vuejs-accessibility')).default } catch {}
31
+
32
+ export default {
33
+ ...plugin,
34
+ configs: {
35
+ recommended: {
36
+ plugins: {
37
+ [NS]: plugin,
38
+ ...(vueA11y ? { 'vuejs-accessibility': vueA11y } : {}),
39
+ },
40
+ rules: {
41
+ ...(vueA11y ? vueA11y.configs['flat/recommended'].rules : {}),
42
+ ...buildRecommendedRules(NS),
43
+ ...buildPortabilityRules(NS),
44
+ ...buildUlamRecommendedRulesFramework(NS),
45
+ },
46
+ },
47
+ },
48
+ }
@@ -1,56 +1,56 @@
1
- /**
2
- * @a11yfred/neighborESLint plugin (React / JSX)
3
- *
4
- * Flags ARIA patterns that are widely derided, semantically wrong, or have
5
- * poor/no AT supportbut are not caught by eslint-plugin-jsx-a11y recommended.
6
- *
7
- * Sources and credits:
8
- * Adrian Roselli adrianroselli.com
9
- * Heydon Pickering heydonworks.com, inclusive-components.design
10
- * Scott O'Hara scottohara.me
11
- * Patrick Lauke splintered.co.uk, patrickhlauke.github.io/aria
12
- * Karl Groves karlgroves.com
13
- * Marcy Sutton marcysutton.com
14
- * Eric Eggert yatil.net
15
- * WAI-ARIA APG w3.org/WAI/ARIA/apg
16
- * ARIA 1.2 spec w3.org/TR/wai-aria-1.2
17
- *
18
- * Rules already covered by jsx-a11y recommended (not duplicated here):
19
- * aria-hidden on focusable → jsx-a11y/no-aria-hidden-on-focusable
20
- * presentation/none on interactive → jsx-a11y/no-interactive-element-to-noninteractive-role
21
- * redundant role → jsx-a11y/no-redundant-roles
22
- * prefer semantic element → jsx-a11y/prefer-tag-over-role
23
- * invalid role value → jsx-a11y/aria-role
24
- * invalid aria prop → jsx-a11y/aria-props
25
- * tabindex > 0 → jsx-a11y/tabindex-no-positive
26
- * tabindex on non-interactive → jsx-a11y/no-noninteractive-tabindex
27
- * img missing alt → jsx-a11y/alt-text
28
- * input missing label → jsx-a11y/label-has-associated-control
29
- */
30
-
31
- import jsxA11y from 'eslint-plugin-jsx-a11y'
32
- import { h } from './lib/helpers-jsx.js'
33
- import { buildRules, buildRecommendedRules } from './lib/rules.js'
34
- import { buildUlamRules, buildUlamRecommendedRules } from './lib/ulam-rules.js'
35
-
36
- const NS = '@a11yfred/neighbor'
37
- const rules = { ...buildRules(h), ...buildUlamRules() }
38
-
39
- const plugin = { meta: { name: NS }, rules }
40
-
41
- export default {
42
- ...plugin,
43
- configs: {
44
- recommended: {
45
- plugins: {
46
- [NS]: plugin,
47
- 'jsx-a11y': jsxA11y,
48
- },
49
- rules: {
50
- ...jsxA11y.configs.recommended.rules,
51
- ...buildRecommendedRules(NS),
52
- ...buildUlamRecommendedRules(NS),
53
- },
54
- },
55
- },
56
- }
1
+ /**
2
+ * @a11yfred/neighbor - ESLint plugin (React / JSX)
3
+ *
4
+ * Flags ARIA patterns that are widely derided, semantically wrong, or have
5
+ * poor/no AT support - but are not caught by eslint-plugin-jsx-a11y recommended.
6
+ *
7
+ * Sources and credits:
8
+ * Adrian Roselli adrianroselli.com
9
+ * Heydon Pickering heydonworks.com, inclusive-components.design
10
+ * Scott O'Hara scottohara.me
11
+ * Patrick Lauke splintered.co.uk, patrickhlauke.github.io/aria
12
+ * Karl Groves karlgroves.com
13
+ * Marcy Sutton marcysutton.com
14
+ * Eric Eggert yatil.net
15
+ * WAI-ARIA APG w3.org/WAI/ARIA/apg
16
+ * ARIA 1.2 spec w3.org/TR/wai-aria-1.2
17
+ *
18
+ * Rules already covered by jsx-a11y recommended (not duplicated here):
19
+ * aria-hidden on focusable → jsx-a11y/no-aria-hidden-on-focusable
20
+ * presentation/none on interactive → jsx-a11y/no-interactive-element-to-noninteractive-role
21
+ * redundant role → jsx-a11y/no-redundant-roles
22
+ * prefer semantic element → jsx-a11y/prefer-tag-over-role
23
+ * invalid role value → jsx-a11y/aria-role
24
+ * invalid aria prop → jsx-a11y/aria-props
25
+ * tabindex > 0 → jsx-a11y/tabindex-no-positive
26
+ * tabindex on non-interactive → jsx-a11y/no-noninteractive-tabindex
27
+ * img missing alt → jsx-a11y/alt-text
28
+ * input missing label → jsx-a11y/label-has-associated-control
29
+ */
30
+
31
+ import jsxA11y from 'eslint-plugin-jsx-a11y'
32
+ import { h } from './lib/helpers-jsx.js'
33
+ import { buildRules, buildRecommendedRules } from './lib/rules.js'
34
+ import { buildUlamRules, buildUlamRecommendedRules } from './lib/ulam-rules.js'
35
+
36
+ const NS = '@a11yfred/neighbor'
37
+ const rules = { ...buildRules(h), ...buildUlamRules() }
38
+
39
+ const plugin = { meta: { name: NS }, rules }
40
+
41
+ export default {
42
+ ...plugin,
43
+ configs: {
44
+ recommended: {
45
+ plugins: {
46
+ [NS]: plugin,
47
+ 'jsx-a11y': jsxA11y,
48
+ },
49
+ rules: {
50
+ ...jsxA11y.configs.recommended.rules,
51
+ ...buildRecommendedRules(NS),
52
+ ...buildUlamRecommendedRules(NS),
53
+ },
54
+ },
55
+ },
56
+ }