-
Notifications
You must be signed in to change notification settings - Fork 30
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
26 changed files
with
631 additions
and
180 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
91 changes: 91 additions & 0 deletions
91
packages/parser/src/lint/plugin-core/rules/a11y-min-contrast.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,91 @@ | ||
import { tokenToCulori } from '@terrazzo/token-tools'; | ||
import { wcagContrast } from 'culori'; | ||
import type { LintRule } from '../../../types.js'; | ||
import { docsLink } from '../lib/docs.js'; | ||
|
||
export const A11Y_MIN_CONTRAST = 'a11y/min-contrast'; | ||
|
||
export interface RuleA11yMinContrastOptions { | ||
/** | ||
* Whether to adhere to AA (minimum) or AAA (enhanced) contrast levels. | ||
* @default "AA" | ||
*/ | ||
level?: 'AA' | 'AAA'; | ||
/** Pairs of color tokens (and optionally typography) to test */ | ||
pairs: ContrastPair[]; | ||
} | ||
|
||
export interface ContrastPair { | ||
/** The foreground color token ID */ | ||
foreground: string; | ||
/** The background color token ID */ | ||
background: string; | ||
/** | ||
* Is this pair for large text? Large text allows a smaller contrast ratio. | ||
* | ||
* Note: while WCAG has _suggested_ sizes and weights, those are merely | ||
* suggestions. It’s always more reliable to determine what constitutes “large | ||
* text” for your designs yourself, based on your typographic stack. | ||
* @see https://www.w3.org/WAI/WCAG22/quickref/#contrast-minimum | ||
*/ | ||
largeText?: boolean; | ||
} | ||
|
||
export const WCAG2_MIN_CONTRAST = { | ||
AA: { default: 4.5, large: 3 }, | ||
AAA: { default: 7, large: 4.5 }, | ||
}; | ||
|
||
export const ERROR_INSUFFICIENT_CONTRAST = 'INSUFFICIENT_CONTRAST'; | ||
|
||
const rule: LintRule<typeof ERROR_INSUFFICIENT_CONTRAST, RuleA11yMinContrastOptions> = { | ||
meta: { | ||
messages: { | ||
[ERROR_INSUFFICIENT_CONTRAST]: 'Pair {{ index }} failed; expected {{ expected }}, got {{ actual }} ({{ level }})', | ||
}, | ||
docs: { | ||
description: 'Enforce colors meet minimum contrast checks for WCAG 2.', | ||
url: docsLink(A11Y_MIN_CONTRAST), | ||
}, | ||
}, | ||
defaultOptions: { level: 'AA', pairs: [] }, | ||
create({ tokens, options, report }) { | ||
for (let i = 0; i < options.pairs.length; i++) { | ||
const { foreground, background, largeText } = options.pairs[i]!; | ||
if (!tokens[foreground]) { | ||
throw new Error(`Token ${foreground} does not exist`); | ||
} | ||
if (tokens[foreground].$type !== 'color') { | ||
throw new Error(`Token ${foreground} isn’t a color`); | ||
} | ||
if (!tokens[background]) { | ||
throw new Error(`Token ${background} does not exist`); | ||
} | ||
if (tokens[background].$type !== 'color') { | ||
throw new Error(`Token ${background} isn’t a color`); | ||
} | ||
|
||
// Note: if these culors were unparseable, they would have already thrown an error before the linter | ||
const a = tokenToCulori(tokens[foreground].$value)!; | ||
const b = tokenToCulori(tokens[background].$value)!; | ||
|
||
// Note: for the purposes of WCAG 2, foreground and background don’t | ||
// matter. But in other contrast algorithms, they do. | ||
const contrast = wcagContrast(a, b); | ||
const min = WCAG2_MIN_CONTRAST[options.level ?? 'AA'][largeText ? 'large' : 'default']; | ||
if (contrast < min) { | ||
report({ | ||
messageId: ERROR_INSUFFICIENT_CONTRAST, | ||
data: { | ||
index: i + 1, | ||
expected: min, | ||
actual: Math.round(contrast * 100) / 100, | ||
level: options.level, | ||
}, | ||
}); | ||
} | ||
} | ||
}, | ||
}; | ||
|
||
export default rule; |
64 changes: 64 additions & 0 deletions
64
packages/parser/src/lint/plugin-core/rules/a11y-min-font-size.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,64 @@ | ||
import { isTokenMatch } from '@terrazzo/token-tools'; | ||
import type { LintRule } from '../../../types.js'; | ||
import { docsLink } from '../lib/docs.js'; | ||
|
||
export const A11Y_MIN_FONT_SIZE = 'a11y/min-font-size'; | ||
|
||
export interface RuleA11yMinFontSizeOptions { | ||
/** Minimum font size (pixels) */ | ||
minSizePx?: number; | ||
/** Minimum font size (rems) */ | ||
minSizeRem?: number; | ||
/** Token IDs to ignore. Accepts globs. */ | ||
ignore?: string[]; | ||
} | ||
|
||
export const ERROR_TOO_SMALL = 'TOO_SMALL'; | ||
|
||
const rule: LintRule<typeof ERROR_TOO_SMALL, RuleA11yMinFontSizeOptions> = { | ||
meta: { | ||
messages: { | ||
[ERROR_TOO_SMALL]: '{{ id }} font size too small. Expected minimum of {{ min }}', | ||
}, | ||
docs: { | ||
description: 'Enforce font sizes are no smaller than the given value.', | ||
url: docsLink(A11Y_MIN_FONT_SIZE), | ||
}, | ||
}, | ||
defaultOptions: {}, | ||
create({ tokens, options, report }) { | ||
if (!options.minSizePx && !options.minSizeRem) { | ||
throw new Error('Must specify at least one of minSizePx or minSizeRem'); | ||
} | ||
|
||
for (const t of Object.values(tokens)) { | ||
if (options.ignore && isTokenMatch(t.id, options.ignore)) { | ||
continue; | ||
} | ||
|
||
// skip aliases | ||
if (t.aliasOf) { | ||
continue; | ||
} | ||
|
||
if (t.$type === 'typography' && 'fontSize' in t.$value) { | ||
const fontSize = t.$value.fontSize!; | ||
|
||
if ( | ||
(fontSize.unit === 'px' && options.minSizePx && fontSize.value < options.minSizePx) || | ||
(fontSize.unit === 'rem' && options.minSizeRem && fontSize.value < options.minSizeRem) | ||
) { | ||
report({ | ||
messageId: ERROR_TOO_SMALL, | ||
data: { | ||
id: t.id, | ||
min: options.minSizePx ? `${options.minSizePx}px` : `${options.minSizeRem}rem`, | ||
}, | ||
}); | ||
} | ||
} | ||
} | ||
}, | ||
}; | ||
|
||
export default rule; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
41 changes: 41 additions & 0 deletions
41
packages/parser/src/lint/plugin-core/rules/descriptions.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,41 @@ | ||
import { isTokenMatch } from '@terrazzo/token-tools'; | ||
import type { LintRule } from '../../../types.js'; | ||
import { docsLink } from '../lib/docs.js'; | ||
|
||
export const DESCRIPTIONS = 'core/descriptions'; | ||
|
||
export interface RuleDescriptionsOptions { | ||
/** Token IDs to ignore. Supports globs (`*`). */ | ||
ignore?: string[]; | ||
} | ||
|
||
const ERROR_MISSING_DESCRIPTION = 'MISSING_DESCRIPTION'; | ||
|
||
const rule: LintRule<typeof ERROR_MISSING_DESCRIPTION, RuleDescriptionsOptions> = { | ||
meta: { | ||
messages: { | ||
[ERROR_MISSING_DESCRIPTION]: '{{ id }} missing description', | ||
}, | ||
docs: { | ||
description: 'Enforce tokens have descriptions.', | ||
url: docsLink(DESCRIPTIONS), | ||
}, | ||
}, | ||
defaultOptions: {}, | ||
create({ tokens, options, report }) { | ||
for (const t of Object.values(tokens)) { | ||
if (options.ignore && isTokenMatch(t.id, options.ignore)) { | ||
continue; | ||
} | ||
if (!t.$description) { | ||
report({ | ||
messageId: ERROR_MISSING_DESCRIPTION, | ||
data: { id: t.id }, | ||
node: t.source.node, | ||
}); | ||
} | ||
} | ||
}, | ||
}; | ||
|
||
export default rule; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.