@terrazzo/parser 2.0.0-alpha.7 → 2.0.0-beta.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. package/dist/index.d.ts +39 -6
  2. package/dist/index.d.ts.map +1 -1
  3. package/dist/index.js +578 -512
  4. package/dist/index.js.map +1 -1
  5. package/package.json +3 -3
  6. package/src/build/index.ts +0 -209
  7. package/src/config.ts +0 -304
  8. package/src/index.ts +0 -95
  9. package/src/lib/code-frame.ts +0 -177
  10. package/src/lib/momoa.ts +0 -10
  11. package/src/lib/resolver-utils.ts +0 -35
  12. package/src/lint/index.ts +0 -142
  13. package/src/lint/plugin-core/index.ts +0 -103
  14. package/src/lint/plugin-core/lib/docs.ts +0 -3
  15. package/src/lint/plugin-core/rules/a11y-min-contrast.ts +0 -91
  16. package/src/lint/plugin-core/rules/a11y-min-font-size.ts +0 -66
  17. package/src/lint/plugin-core/rules/colorspace.ts +0 -108
  18. package/src/lint/plugin-core/rules/consistent-naming.ts +0 -65
  19. package/src/lint/plugin-core/rules/descriptions.ts +0 -43
  20. package/src/lint/plugin-core/rules/duplicate-values.ts +0 -85
  21. package/src/lint/plugin-core/rules/max-gamut.ts +0 -144
  22. package/src/lint/plugin-core/rules/required-children.ts +0 -106
  23. package/src/lint/plugin-core/rules/required-modes.ts +0 -75
  24. package/src/lint/plugin-core/rules/required-type.ts +0 -28
  25. package/src/lint/plugin-core/rules/required-typography-properties.ts +0 -65
  26. package/src/lint/plugin-core/rules/valid-boolean.ts +0 -41
  27. package/src/lint/plugin-core/rules/valid-border.ts +0 -57
  28. package/src/lint/plugin-core/rules/valid-color.ts +0 -265
  29. package/src/lint/plugin-core/rules/valid-cubic-bezier.ts +0 -83
  30. package/src/lint/plugin-core/rules/valid-dimension.ts +0 -199
  31. package/src/lint/plugin-core/rules/valid-duration.ts +0 -123
  32. package/src/lint/plugin-core/rules/valid-font-family.ts +0 -68
  33. package/src/lint/plugin-core/rules/valid-font-weight.ts +0 -89
  34. package/src/lint/plugin-core/rules/valid-gradient.ts +0 -79
  35. package/src/lint/plugin-core/rules/valid-link.ts +0 -41
  36. package/src/lint/plugin-core/rules/valid-number.ts +0 -63
  37. package/src/lint/plugin-core/rules/valid-shadow.ts +0 -67
  38. package/src/lint/plugin-core/rules/valid-string.ts +0 -41
  39. package/src/lint/plugin-core/rules/valid-stroke-style.ts +0 -104
  40. package/src/lint/plugin-core/rules/valid-transition.ts +0 -61
  41. package/src/lint/plugin-core/rules/valid-typography.ts +0 -67
  42. package/src/logger.ts +0 -213
  43. package/src/parse/index.ts +0 -124
  44. package/src/parse/load.ts +0 -172
  45. package/src/parse/normalize.ts +0 -163
  46. package/src/parse/process.ts +0 -251
  47. package/src/parse/token.ts +0 -553
  48. package/src/resolver/create-synthetic-resolver.ts +0 -86
  49. package/src/resolver/index.ts +0 -7
  50. package/src/resolver/load.ts +0 -215
  51. package/src/resolver/normalize.ts +0 -133
  52. package/src/resolver/validate.ts +0 -375
  53. package/src/types.ts +0 -468
@@ -1,177 +0,0 @@
1
- // This is copied from @babel/code-frame package but without the heavyweight color highlighting
2
- // (note: Babel loads both chalk AND picocolors, and doesn’t treeshake well)
3
- // Babel is MIT-licensed and unaffiliated with this project.
4
-
5
- // MIT License
6
- //
7
- // Copyright (c) 2014-present Sebastian McKenzie and other contributors
8
- //
9
- // Permission is hereby granted, free of charge, to any person obtaining
10
- // a copy of this software and associated documentation files (the
11
- // "Software"), to deal in the Software without restriction, including
12
- // without limitation the rights to use, copy, modify, merge, publish,
13
- // distribute, sublicense, and/or sell copies of the Software, and to
14
- // permit persons to whom the Software is furnished to do so, subject to
15
- // the following conditions:
16
- //
17
- // The above copyright notice and this permission notice shall be
18
- // included in all copies or substantial portions of the Software.
19
- //
20
- // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
21
- // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
22
- // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
23
- // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
24
- // LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
25
- // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
26
- // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
27
-
28
- export interface Location {
29
- line: number;
30
- column: number;
31
- }
32
-
33
- export interface NodeLocation {
34
- end?: Location;
35
- start: Location;
36
- }
37
-
38
- export interface Options {
39
- /** Syntax highlight the code as JavaScript for terminals. default: false */
40
- highlightCode?: boolean;
41
- /** The number of lines to show above the error. default: 2 */
42
- linesAbove?: number;
43
- /** The number of lines to show below the error. default: 3 */
44
- linesBelow?: number;
45
- /**
46
- * Forcibly syntax highlight the code as JavaScript (for non-terminals);
47
- * overrides highlightCode.
48
- * default: false
49
- */
50
- forceColor?: boolean;
51
- /**
52
- * Pass in a string to be displayed inline (if possible) next to the
53
- * highlighted location in the code. If it can't be positioned inline,
54
- * it will be placed above the code frame.
55
- * default: nothing
56
- */
57
- message?: string;
58
- }
59
-
60
- /**
61
- * Extract what lines should be marked and highlighted.
62
- */
63
- function getMarkerLines(loc: NodeLocation, source: string[], opts: Options = {} as Options) {
64
- const startLoc = {
65
- // @ts-expect-error this is fine
66
- column: 0,
67
- // @ts-expect-error this is fine
68
- line: -1,
69
- ...loc.start,
70
- } as Location;
71
- const endLoc: Location = {
72
- ...startLoc,
73
- ...loc.end,
74
- };
75
- const { linesAbove = 2, linesBelow = 3 } = opts || {};
76
- const startLine = startLoc.line;
77
- const startColumn = startLoc.column;
78
- const endLine = endLoc.line;
79
- const endColumn = endLoc.column;
80
-
81
- let start = Math.max(startLine - (linesAbove + 1), 0);
82
- let end = Math.min(source.length, endLine + linesBelow);
83
-
84
- if (startLine === -1) {
85
- start = 0;
86
- }
87
-
88
- if (endLine === -1) {
89
- end = source.length;
90
- }
91
-
92
- const lineDiff = endLine - startLine;
93
- const markerLines: Record<string, any> = {};
94
-
95
- if (lineDiff) {
96
- for (let i = 0; i <= lineDiff; i++) {
97
- const lineNumber = i + startLine;
98
-
99
- if (!startColumn) {
100
- markerLines[lineNumber] = true;
101
- } else if (i === 0) {
102
- const sourceLength = source[lineNumber - 1]!.length;
103
-
104
- markerLines[lineNumber] = [startColumn, sourceLength - startColumn + 1];
105
- } else if (i === lineDiff) {
106
- markerLines[lineNumber] = [0, endColumn];
107
- } else {
108
- const sourceLength = source[lineNumber - i]!.length;
109
-
110
- markerLines[lineNumber] = [0, sourceLength];
111
- }
112
- }
113
- } else {
114
- if (startColumn === endColumn) {
115
- if (startColumn) {
116
- markerLines[startLine] = [startColumn, 0];
117
- } else {
118
- markerLines[startLine] = true;
119
- }
120
- } else {
121
- markerLines[startLine] = [startColumn, endColumn - startColumn];
122
- }
123
- }
124
-
125
- return { start, end, markerLines };
126
- }
127
-
128
- /**
129
- * RegExp to test for newlines in terminal.
130
- */
131
-
132
- const NEWLINE = /\r\n|[\n\r\u2028\u2029]/;
133
-
134
- export function codeFrameColumns(rawLines: string, loc: NodeLocation, opts: Options = {} as Options) {
135
- if (typeof rawLines !== 'string') {
136
- throw new Error(`Expected string, got ${rawLines}`);
137
- }
138
- const lines = rawLines.split(NEWLINE);
139
- const { start, end, markerLines } = getMarkerLines(loc, lines, opts);
140
- const hasColumns = loc.start && typeof loc.start.column === 'number';
141
-
142
- const numberMaxWidth = String(end).length;
143
-
144
- let frame = rawLines
145
- .split(NEWLINE, end)
146
- .slice(start, end)
147
- .map((line, index) => {
148
- const number = start + 1 + index;
149
- const paddedNumber = ` ${number}`.slice(-numberMaxWidth);
150
- const gutter = ` ${paddedNumber} |`;
151
- const hasMarker = markerLines[number];
152
- const lastMarkerLine = !markerLines[number + 1];
153
- if (hasMarker) {
154
- let markerLine = '';
155
- if (Array.isArray(hasMarker)) {
156
- const markerSpacing = line.slice(0, Math.max(hasMarker[0] - 1, 0)).replace(/[^\t]/g, ' ');
157
- const numberOfMarkers = hasMarker[1] || 1;
158
-
159
- markerLine = ['\n ', gutter.replace(/\d/g, ' '), ' ', markerSpacing, '^'.repeat(numberOfMarkers)].join('');
160
-
161
- if (lastMarkerLine && opts.message) {
162
- markerLine += ` ${opts.message}`;
163
- }
164
- }
165
- return ['>', gutter, line.length > 0 ? ` ${line}` : '', markerLine].join('');
166
- } else {
167
- return ` ${gutter}${line.length > 0 ? ` ${line}` : ''}`;
168
- }
169
- })
170
- .join('\n');
171
-
172
- if (opts.message && !hasColumns) {
173
- frame = `${' '.repeat(numberMaxWidth + 1)}${opts.message}\n${frame}`;
174
- }
175
-
176
- return frame;
177
- }
package/src/lib/momoa.ts DELETED
@@ -1,10 +0,0 @@
1
- import * as momoa from '@humanwhocodes/momoa';
2
-
3
- /** Momoa’s default parser, with preferred settings. */
4
- export function toMomoa(srcRaw: any): momoa.DocumentNode {
5
- return momoa.parse(typeof srcRaw === 'string' ? srcRaw : JSON.stringify(srcRaw, undefined, 2), {
6
- mode: 'jsonc',
7
- ranges: true,
8
- tokens: true,
9
- });
10
- }
@@ -1,35 +0,0 @@
1
- /**
2
- * If tokens are found inside a resolver, strip out the resolver paths (don’t
3
- * include "sets"/"modifiers" in the token ID etc.)
4
- */
5
- export function filterResolverPaths(path: string[]): string[] {
6
- switch (path[0]) {
7
- case 'sets': {
8
- return path.slice(4);
9
- }
10
- case 'modifiers': {
11
- return path.slice(5);
12
- }
13
- case 'resolutionOrder': {
14
- switch (path[2]) {
15
- case 'sources': {
16
- return path.slice(4);
17
- }
18
- case 'contexts': {
19
- return path.slice(5);
20
- }
21
- }
22
- break;
23
- }
24
- }
25
- return path;
26
- }
27
-
28
- /**
29
- * Make a deterministic string from an object
30
- */
31
- export function makeInputKey(input: Record<string, string | undefined>): string {
32
- return JSON.stringify(
33
- Object.fromEntries(Object.entries(input).sort((a, b) => a[0].localeCompare(b[0], 'en-us', { numeric: true }))),
34
- );
35
- }
package/src/lint/index.ts DELETED
@@ -1,142 +0,0 @@
1
- import type { InputSourceWithDocument } from '@terrazzo/json-schema-tools';
2
- import { pluralize, type TokenNormalizedSet } from '@terrazzo/token-tools';
3
- import { merge } from 'merge-anything';
4
- import type { LogEntry, default as Logger } from '../logger.js';
5
- import type { ConfigInit } from '../types.js';
6
-
7
- export { RECOMMENDED_CONFIG } from './plugin-core/index.js';
8
-
9
- export interface LintRunnerOptions {
10
- tokens: TokenNormalizedSet;
11
- filename?: URL;
12
- config: ConfigInit;
13
- sources: InputSourceWithDocument[];
14
- logger: Logger;
15
- }
16
-
17
- export default async function lintRunner({
18
- tokens,
19
- filename,
20
- config = {} as ConfigInit,
21
- sources,
22
- logger,
23
- }: LintRunnerOptions): Promise<void> {
24
- const { plugins = [], lint } = config;
25
- const sourceByFilename: Record<string, InputSourceWithDocument> = {};
26
- for (const source of sources) {
27
- sourceByFilename[source.filename!.href] = source;
28
- }
29
- const unusedLintRules = Object.keys(lint?.rules ?? {});
30
-
31
- const errors: LogEntry[] = [];
32
- const warnings: LogEntry[] = [];
33
- for (const plugin of plugins) {
34
- if (typeof plugin.lint === 'function') {
35
- const s = performance.now();
36
-
37
- const linter = plugin.lint();
38
-
39
- await Promise.all(
40
- Object.entries(linter).map(async ([id, rule]) => {
41
- if (!(id in lint.rules) || lint.rules[id] === null) {
42
- return;
43
- }
44
-
45
- // tick off used rule
46
- const unusedLintRuleI = unusedLintRules.indexOf(id);
47
- if (unusedLintRuleI !== -1) {
48
- unusedLintRules.splice(unusedLintRuleI, 1);
49
- }
50
-
51
- const [severity, options] = lint.rules[id]!;
52
-
53
- if (severity === 'off') {
54
- return;
55
- }
56
- // note: this usually isn’t a Promise, but it _might_ be!
57
- await rule.create({
58
- id,
59
- report(descriptor) {
60
- let message = '';
61
- if (!descriptor.message && !descriptor.messageId) {
62
- logger.error({
63
- group: 'lint',
64
- label: `${plugin.name} › lint › ${id}`,
65
- message: 'Unable to report error: missing message or messageId',
66
- });
67
- }
68
-
69
- // handle message or messageId
70
- if (descriptor.message) {
71
- message = descriptor.message;
72
- } else {
73
- if (!(descriptor.messageId! in (rule.meta?.messages ?? {}))) {
74
- logger.error({
75
- group: 'lint',
76
- label: `${plugin.name} › lint › ${id}`,
77
- message: `messageId "${descriptor.messageId}" does not exist`,
78
- });
79
- }
80
- message = rule.meta?.messages?.[descriptor.messageId as keyof typeof rule.meta.messages] ?? '';
81
- }
82
-
83
- // replace with descriptor.data (if any)
84
- if (descriptor.data && typeof descriptor.data === 'object') {
85
- for (const [k, v] of Object.entries(descriptor.data)) {
86
- // lazy formatting
87
- const formatted = ['string', 'number', 'boolean'].includes(typeof v) ? String(v) : JSON.stringify(v);
88
- message = message.replace(/{{[^}]+}}/g, (inner) => {
89
- const key = inner.substring(2, inner.length - 2).trim();
90
- return key === k ? formatted : inner;
91
- });
92
- }
93
- }
94
-
95
- (severity === 'error' ? errors : warnings).push({
96
- group: 'lint',
97
- label: id,
98
- message,
99
- filename,
100
- node: descriptor.node,
101
- src: sourceByFilename[descriptor.filename!]?.src,
102
- });
103
- },
104
- tokens,
105
- filename,
106
- sources,
107
- options: merge(
108
- rule.meta?.defaultOptions ?? [],
109
- rule.defaultOptions ?? [], // Note: is this the correct order to merge in?
110
- options,
111
- ),
112
- });
113
- }),
114
- );
115
-
116
- logger.debug({
117
- group: 'lint',
118
- label: plugin.name,
119
- message: 'Finished',
120
- timing: performance.now() - s,
121
- });
122
- }
123
- }
124
-
125
- const errCount = errors.length ? `${errors.length} ${pluralize(errors.length, 'error', 'errors')}` : '';
126
- const warnCount = warnings.length ? `${warnings.length} ${pluralize(warnings.length, 'warning', 'warnings')}` : '';
127
- if (errors.length > 0) {
128
- logger.error(...errors, {
129
- group: 'lint',
130
- label: 'lint',
131
- message: [errCount, warnCount].filter(Boolean).join(', '),
132
- });
133
- }
134
- if (warnings.length > 0) {
135
- logger.warn(...warnings, { group: 'lint', label: 'lint', message: warnCount });
136
- }
137
-
138
- // warn user if they have unused lint rules (they might have meant to configure something!)
139
- for (const unusedRule of unusedLintRules) {
140
- logger.warn({ group: 'lint', label: 'lint', message: `Unknown lint rule "${unusedRule}"` });
141
- }
142
- }
@@ -1,103 +0,0 @@
1
- // Terrazzo internal plugin that powers lint rules. Always enabled.
2
- import type { LintRuleLonghand, Plugin } from '../../types.js';
3
-
4
- export * from './rules/a11y-min-contrast.js';
5
- export * from './rules/a11y-min-font-size.js';
6
- export * from './rules/colorspace.js';
7
- export * from './rules/consistent-naming.js';
8
- export * from './rules/descriptions.js';
9
- export * from './rules/duplicate-values.js';
10
- export * from './rules/max-gamut.js';
11
- export * from './rules/required-children.js';
12
- export * from './rules/required-modes.js';
13
- export * from './rules/required-type.js';
14
- export * from './rules/required-typography-properties.js';
15
-
16
- import a11yMinContrast, { A11Y_MIN_CONTRAST } from './rules/a11y-min-contrast.js';
17
- import a11yMinFontSize, { A11Y_MIN_FONT_SIZE } from './rules/a11y-min-font-size.js';
18
- import colorspace, { COLORSPACE } from './rules/colorspace.js';
19
- import consistentNaming, { CONSISTENT_NAMING } from './rules/consistent-naming.js';
20
- import descriptions, { DESCRIPTIONS } from './rules/descriptions.js';
21
- import duplicateValues, { DUPLICATE_VALUES } from './rules/duplicate-values.js';
22
- import maxGamut, { MAX_GAMUT } from './rules/max-gamut.js';
23
- import requiredChildren, { REQUIRED_CHILDREN } from './rules/required-children.js';
24
- import requiredModes, { REQUIRED_MODES } from './rules/required-modes.js';
25
- import requiredType, { REQUIRED_TYPE } from './rules/required-type.js';
26
- import requiredTypographyProperties, {
27
- REQUIRED_TYPOGRAPHY_PROPERTIES,
28
- } from './rules/required-typography-properties.js';
29
- import validBoolean, { VALID_BOOLEAN } from './rules/valid-boolean.js';
30
- import validBorder, { VALID_BORDER } from './rules/valid-border.js';
31
- import validColor, { VALID_COLOR } from './rules/valid-color.js';
32
- import validCubicBezier, { VALID_CUBIC_BEZIER } from './rules/valid-cubic-bezier.js';
33
- import validDimension, { VALID_DIMENSION } from './rules/valid-dimension.js';
34
- import validDuration, { VALID_DURATION } from './rules/valid-duration.js';
35
- import validFontFamily, { VALID_FONT_FAMILY } from './rules/valid-font-family.js';
36
- import validFontWeight, { VALID_FONT_WEIGHT } from './rules/valid-font-weight.js';
37
- import validGradient, { VALID_GRADIENT } from './rules/valid-gradient.js';
38
- import validLink, { VALID_LINK } from './rules/valid-link.js';
39
- import validNumber, { VALID_NUMBER } from './rules/valid-number.js';
40
- import validShadow, { VALID_SHADOW } from './rules/valid-shadow.js';
41
- import validString, { VALID_STRING } from './rules/valid-string.js';
42
- import validStrokeStyle, { VALID_STROKE_STYLE } from './rules/valid-stroke-style.js';
43
- import validTransition, { VALID_TRANSITION } from './rules/valid-transition.js';
44
- import validTypography, { VALID_TYPOGRAPHY } from './rules/valid-typography.js';
45
-
46
- const ALL_RULES = {
47
- [VALID_COLOR]: validColor,
48
- [VALID_DIMENSION]: validDimension,
49
- [VALID_FONT_FAMILY]: validFontFamily,
50
- [VALID_FONT_WEIGHT]: validFontWeight,
51
- [VALID_DURATION]: validDuration,
52
- [VALID_CUBIC_BEZIER]: validCubicBezier,
53
- [VALID_NUMBER]: validNumber,
54
- [VALID_LINK]: validLink,
55
- [VALID_BOOLEAN]: validBoolean,
56
- [VALID_STRING]: validString,
57
- [VALID_STROKE_STYLE]: validStrokeStyle,
58
- [VALID_BORDER]: validBorder,
59
- [VALID_TRANSITION]: validTransition,
60
- [VALID_SHADOW]: validShadow,
61
- [VALID_GRADIENT]: validGradient,
62
- [VALID_TYPOGRAPHY]: validTypography,
63
- [COLORSPACE]: colorspace,
64
- [CONSISTENT_NAMING]: consistentNaming,
65
- [DESCRIPTIONS]: descriptions,
66
- [DUPLICATE_VALUES]: duplicateValues,
67
- [MAX_GAMUT]: maxGamut,
68
- [REQUIRED_CHILDREN]: requiredChildren,
69
- [REQUIRED_MODES]: requiredModes,
70
- [REQUIRED_TYPE]: requiredType,
71
- [REQUIRED_TYPOGRAPHY_PROPERTIES]: requiredTypographyProperties,
72
- [A11Y_MIN_CONTRAST]: a11yMinContrast,
73
- [A11Y_MIN_FONT_SIZE]: a11yMinFontSize,
74
- };
75
-
76
- export default function coreLintPlugin(): Plugin {
77
- return {
78
- name: '@terrazzo/plugin-lint-core',
79
- lint() {
80
- return ALL_RULES;
81
- },
82
- };
83
- }
84
-
85
- export const RECOMMENDED_CONFIG: Record<string, LintRuleLonghand> = {
86
- [VALID_COLOR]: ['error', {}],
87
- [VALID_DIMENSION]: ['error', {}],
88
- [VALID_FONT_FAMILY]: ['error', {}],
89
- [VALID_FONT_WEIGHT]: ['error', {}],
90
- [VALID_DURATION]: ['error', {}],
91
- [VALID_CUBIC_BEZIER]: ['error', {}],
92
- [VALID_NUMBER]: ['error', {}],
93
- [VALID_LINK]: ['error', {}],
94
- [VALID_BOOLEAN]: ['error', {}],
95
- [VALID_STRING]: ['error', {}],
96
- [VALID_STROKE_STYLE]: ['error', {}],
97
- [VALID_BORDER]: ['error', {}],
98
- [VALID_TRANSITION]: ['error', {}],
99
- [VALID_SHADOW]: ['error', {}],
100
- [VALID_GRADIENT]: ['error', {}],
101
- [VALID_TYPOGRAPHY]: ['error', {}],
102
- [CONSISTENT_NAMING]: ['warn', { format: 'kebab-case' }],
103
- };
@@ -1,3 +0,0 @@
1
- export function docsLink(ruleName: string): string {
2
- return `https://terrazzo.app/docs/linting#${ruleName.replaceAll('/', '')}`;
3
- }
@@ -1,91 +0,0 @@
1
- import { tokenToCulori } from '@terrazzo/token-tools';
2
- import { wcagContrast } from 'culori';
3
- import type { LintRule } from '../../../types.js';
4
- import { docsLink } from '../lib/docs.js';
5
-
6
- export const A11Y_MIN_CONTRAST = 'a11y/min-contrast';
7
-
8
- export interface RuleA11yMinContrastOptions {
9
- /**
10
- * Whether to adhere to AA (minimum) or AAA (enhanced) contrast levels.
11
- * @default "AA"
12
- */
13
- level?: 'AA' | 'AAA';
14
- /** Pairs of color tokens (and optionally typography) to test */
15
- pairs: ContrastPair[];
16
- }
17
-
18
- export interface ContrastPair {
19
- /** The foreground color token ID */
20
- foreground: string;
21
- /** The background color token ID */
22
- background: string;
23
- /**
24
- * Is this pair for large text? Large text allows a smaller contrast ratio.
25
- *
26
- * Note: while WCAG has _suggested_ sizes and weights, those are merely
27
- * suggestions. It’s always more reliable to determine what constitutes “large
28
- * text” for your designs yourself, based on your typographic stack.
29
- * @see https://www.w3.org/WAI/WCAG22/quickref/#contrast-minimum
30
- */
31
- largeText?: boolean;
32
- }
33
-
34
- export const WCAG2_MIN_CONTRAST = {
35
- AA: { default: 4.5, large: 3 },
36
- AAA: { default: 7, large: 4.5 },
37
- };
38
-
39
- export const ERROR_INSUFFICIENT_CONTRAST = 'INSUFFICIENT_CONTRAST';
40
-
41
- const rule: LintRule<typeof ERROR_INSUFFICIENT_CONTRAST, RuleA11yMinContrastOptions> = {
42
- meta: {
43
- messages: {
44
- [ERROR_INSUFFICIENT_CONTRAST]: 'Pair {{ index }} failed; expected {{ expected }}, got {{ actual }} ({{ level }})',
45
- },
46
- docs: {
47
- description: 'Enforce colors meet minimum contrast checks for WCAG 2.',
48
- url: docsLink(A11Y_MIN_CONTRAST),
49
- },
50
- },
51
- defaultOptions: { level: 'AA', pairs: [] },
52
- create({ tokens, options, report }) {
53
- for (let i = 0; i < options.pairs.length; i++) {
54
- const { foreground, background, largeText } = options.pairs[i]!;
55
- if (!tokens[foreground]) {
56
- throw new Error(`Token ${foreground} does not exist`);
57
- }
58
- if (tokens[foreground].$type !== 'color') {
59
- throw new Error(`Token ${foreground} isn’t a color`);
60
- }
61
- if (!tokens[background]) {
62
- throw new Error(`Token ${background} does not exist`);
63
- }
64
- if (tokens[background].$type !== 'color') {
65
- throw new Error(`Token ${background} isn’t a color`);
66
- }
67
-
68
- // Note: if these culors were unparseable, they would have already thrown an error before the linter
69
- const a = tokenToCulori(tokens[foreground].$value)!;
70
- const b = tokenToCulori(tokens[background].$value)!;
71
-
72
- // Note: for the purposes of WCAG 2, foreground and background don’t
73
- // matter. But in other contrast algorithms, they do.
74
- const contrast = wcagContrast(a, b);
75
- const min = WCAG2_MIN_CONTRAST[options.level ?? 'AA'][largeText ? 'large' : 'default'];
76
- if (contrast < min) {
77
- report({
78
- messageId: ERROR_INSUFFICIENT_CONTRAST,
79
- data: {
80
- index: i + 1,
81
- expected: min,
82
- actual: Math.round(contrast * 100) / 100,
83
- level: options.level,
84
- },
85
- });
86
- }
87
- }
88
- },
89
- };
90
-
91
- export default rule;
@@ -1,66 +0,0 @@
1
- import wcmatch from 'wildcard-match';
2
- import type { LintRule } from '../../../types.js';
3
- import { docsLink } from '../lib/docs.js';
4
-
5
- export const A11Y_MIN_FONT_SIZE = 'a11y/min-font-size';
6
-
7
- export interface RuleA11yMinFontSizeOptions {
8
- /** Minimum font size (pixels) */
9
- minSizePx?: number;
10
- /** Minimum font size (rems) */
11
- minSizeRem?: number;
12
- /** Token IDs to ignore. Accepts globs. */
13
- ignore?: string[];
14
- }
15
-
16
- export const ERROR_TOO_SMALL = 'TOO_SMALL';
17
-
18
- const rule: LintRule<typeof ERROR_TOO_SMALL, RuleA11yMinFontSizeOptions> = {
19
- meta: {
20
- messages: {
21
- [ERROR_TOO_SMALL]: '{{ id }} font size too small. Expected minimum of {{ min }}',
22
- },
23
- docs: {
24
- description: 'Enforce font sizes are no smaller than the given value.',
25
- url: docsLink(A11Y_MIN_FONT_SIZE),
26
- },
27
- },
28
- defaultOptions: {},
29
- create({ tokens, options, report }) {
30
- if (!options.minSizePx && !options.minSizeRem) {
31
- throw new Error('Must specify at least one of minSizePx or minSizeRem');
32
- }
33
-
34
- const shouldIgnore = options.ignore ? wcmatch(options.ignore) : null;
35
-
36
- for (const t of Object.values(tokens)) {
37
- if (shouldIgnore?.(t.id)) {
38
- continue;
39
- }
40
-
41
- // skip aliases
42
- if (t.aliasOf) {
43
- continue;
44
- }
45
-
46
- if (t.$type === 'typography' && 'fontSize' in t.$value) {
47
- const fontSize = t.$value.fontSize!;
48
-
49
- if (
50
- (fontSize.unit === 'px' && options.minSizePx && fontSize.value < options.minSizePx) ||
51
- (fontSize.unit === 'rem' && options.minSizeRem && fontSize.value < options.minSizeRem)
52
- ) {
53
- report({
54
- messageId: ERROR_TOO_SMALL,
55
- data: {
56
- id: t.id,
57
- min: options.minSizePx ? `${options.minSizePx}px` : `${options.minSizeRem}rem`,
58
- },
59
- });
60
- }
61
- }
62
- }
63
- },
64
- };
65
-
66
- export default rule;