@terrazzo/parser 0.10.3 → 2.0.0-alpha.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +6 -0
- package/dist/index.d.ts +82 -333
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2203 -3660
- package/dist/index.js.map +1 -1
- package/package.json +6 -5
- package/src/build/index.ts +32 -41
- package/src/config.ts +13 -6
- package/src/lib/code-frame.ts +5 -2
- package/src/lib/momoa.ts +10 -0
- package/src/lint/index.ts +41 -37
- package/src/lint/plugin-core/index.ts +73 -16
- package/src/lint/plugin-core/rules/colorspace.ts +4 -0
- package/src/lint/plugin-core/rules/duplicate-values.ts +2 -0
- package/src/lint/plugin-core/rules/max-gamut.ts +24 -4
- package/src/lint/plugin-core/rules/no-type-on-alias.ts +29 -0
- package/src/lint/plugin-core/rules/required-modes.ts +2 -0
- package/src/lint/plugin-core/rules/required-typography-properties.ts +13 -3
- package/src/lint/plugin-core/rules/valid-boolean.ts +41 -0
- package/src/lint/plugin-core/rules/valid-border.ts +57 -0
- package/src/lint/plugin-core/rules/valid-color.ts +265 -0
- package/src/lint/plugin-core/rules/valid-cubic-bezier.ts +83 -0
- package/src/lint/plugin-core/rules/valid-dimension.ts +199 -0
- package/src/lint/plugin-core/rules/valid-duration.ts +123 -0
- package/src/lint/plugin-core/rules/valid-font-family.ts +68 -0
- package/src/lint/plugin-core/rules/valid-font-weight.ts +89 -0
- package/src/lint/plugin-core/rules/valid-gradient.ts +79 -0
- package/src/lint/plugin-core/rules/valid-link.ts +41 -0
- package/src/lint/plugin-core/rules/valid-number.ts +63 -0
- package/src/lint/plugin-core/rules/valid-shadow.ts +67 -0
- package/src/lint/plugin-core/rules/valid-string.ts +41 -0
- package/src/lint/plugin-core/rules/valid-stroke-style.ts +104 -0
- package/src/lint/plugin-core/rules/valid-transition.ts +61 -0
- package/src/lint/plugin-core/rules/valid-typography.ts +67 -0
- package/src/logger.ts +70 -59
- package/src/parse/index.ts +23 -318
- package/src/parse/load.ts +257 -0
- package/src/parse/normalize.ts +134 -170
- package/src/parse/token.ts +530 -0
- package/src/types.ts +76 -10
- package/src/parse/alias.ts +0 -369
- package/src/parse/json.ts +0 -211
- package/src/parse/validate.ts +0 -961
|
@@ -5,12 +5,16 @@ import { docsLink } from '../lib/docs.js';
|
|
|
5
5
|
export const REQUIRED_TYPOGRAPHY_PROPERTIES = 'core/required-typography-properties';
|
|
6
6
|
|
|
7
7
|
export interface RuleRequiredTypographyPropertiesOptions {
|
|
8
|
-
/**
|
|
8
|
+
/**
|
|
9
|
+
* Required typography properties.
|
|
10
|
+
* @default ["fontFamily", "fontWeight", "fontSize", "letterSpacing", "lineHeight"]
|
|
11
|
+
*/
|
|
9
12
|
properties: string[];
|
|
10
13
|
/** Token globs to ignore */
|
|
11
14
|
ignore?: string[];
|
|
12
15
|
}
|
|
13
16
|
|
|
17
|
+
/** @deprecated Use core/valid-typography instead */
|
|
14
18
|
const rule: LintRule<never, RuleRequiredTypographyPropertiesOptions> = {
|
|
15
19
|
meta: {
|
|
16
20
|
docs: {
|
|
@@ -18,7 +22,9 @@ const rule: LintRule<never, RuleRequiredTypographyPropertiesOptions> = {
|
|
|
18
22
|
url: docsLink(REQUIRED_TYPOGRAPHY_PROPERTIES),
|
|
19
23
|
},
|
|
20
24
|
},
|
|
21
|
-
defaultOptions: {
|
|
25
|
+
defaultOptions: {
|
|
26
|
+
properties: ['fontFamily', 'fontSize', 'fontWeight', 'letterSpacing', 'lineHeight'],
|
|
27
|
+
},
|
|
22
28
|
create({ tokens, options, report }) {
|
|
23
29
|
if (!options) {
|
|
24
30
|
return;
|
|
@@ -45,7 +51,11 @@ const rule: LintRule<never, RuleRequiredTypographyPropertiesOptions> = {
|
|
|
45
51
|
|
|
46
52
|
for (const p of options.properties) {
|
|
47
53
|
if (!t.partialAliasOf?.[p] && !(p in t.$value)) {
|
|
48
|
-
report({
|
|
54
|
+
report({
|
|
55
|
+
message: `This rule is deprecated. Use core/valid-typography. Missing required typographic property "${p}"`,
|
|
56
|
+
node: t.source.node,
|
|
57
|
+
filename: t.source.filename,
|
|
58
|
+
});
|
|
49
59
|
}
|
|
50
60
|
}
|
|
51
61
|
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type * as momoa from '@humanwhocodes/momoa';
|
|
2
|
+
import { getObjMember } from '@terrazzo/json-schema-tools';
|
|
3
|
+
import type { LintRule } from '../../../types.js';
|
|
4
|
+
import { docsLink } from '../lib/docs.js';
|
|
5
|
+
|
|
6
|
+
export const VALID_BOOLEAN = 'core/valid-boolean';
|
|
7
|
+
|
|
8
|
+
const ERROR = 'ERROR';
|
|
9
|
+
|
|
10
|
+
const rule: LintRule<typeof ERROR, {}> = {
|
|
11
|
+
meta: {
|
|
12
|
+
messages: {
|
|
13
|
+
[ERROR]: 'Must be a boolean.',
|
|
14
|
+
},
|
|
15
|
+
docs: {
|
|
16
|
+
description: 'Require boolean tokens to follow the Terrazzo extension.',
|
|
17
|
+
url: docsLink(VALID_BOOLEAN),
|
|
18
|
+
},
|
|
19
|
+
},
|
|
20
|
+
defaultOptions: {},
|
|
21
|
+
create({ tokens, report }) {
|
|
22
|
+
for (const t of Object.values(tokens)) {
|
|
23
|
+
if (t.aliasOf || !t.originalValue || t.$type !== 'boolean') {
|
|
24
|
+
continue;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
validateBoolean(t.originalValue.$value, {
|
|
28
|
+
node: getObjMember(t.source.node, '$value'),
|
|
29
|
+
filename: t.source.filename,
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
function validateBoolean(value: unknown, { node, filename }: { node?: momoa.AnyNode; filename?: string }) {
|
|
33
|
+
if (typeof value !== 'boolean') {
|
|
34
|
+
report({ messageId: ERROR, filename, node });
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
},
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
export default rule;
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import type * as momoa from '@humanwhocodes/momoa';
|
|
2
|
+
import { getObjMember } from '@terrazzo/json-schema-tools';
|
|
3
|
+
import { BORDER_REQUIRED_PROPERTIES } from '@terrazzo/token-tools';
|
|
4
|
+
import type { LintRule } from '../../../types.js';
|
|
5
|
+
import { docsLink } from '../lib/docs.js';
|
|
6
|
+
|
|
7
|
+
export const VALID_BORDER = 'core/valid-border';
|
|
8
|
+
|
|
9
|
+
const ERROR = 'ERROR';
|
|
10
|
+
const ERROR_INVALID_PROP = 'ERROR_INVALID_PROP';
|
|
11
|
+
|
|
12
|
+
const rule: LintRule<typeof ERROR | typeof ERROR_INVALID_PROP, {}> = {
|
|
13
|
+
meta: {
|
|
14
|
+
messages: {
|
|
15
|
+
[ERROR]: `Border token missing required properties: ${new Intl.ListFormat(undefined, { type: 'conjunction' }).format(BORDER_REQUIRED_PROPERTIES)}.`,
|
|
16
|
+
[ERROR_INVALID_PROP]: 'Unknown property: {{ key }}.',
|
|
17
|
+
},
|
|
18
|
+
docs: {
|
|
19
|
+
description: 'Require border tokens to follow the format.',
|
|
20
|
+
url: docsLink(VALID_BORDER),
|
|
21
|
+
},
|
|
22
|
+
},
|
|
23
|
+
defaultOptions: {},
|
|
24
|
+
create({ tokens, report }) {
|
|
25
|
+
for (const t of Object.values(tokens)) {
|
|
26
|
+
if (t.aliasOf || !t.originalValue || t.$type !== 'border') {
|
|
27
|
+
continue;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
validateBorder(t.originalValue.$value, {
|
|
31
|
+
node: getObjMember(t.source.node, '$value'),
|
|
32
|
+
filename: t.source.filename,
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Note: we validate sub-properties using other checks like valid-dimension, valid-font-family, etc.
|
|
37
|
+
// The only thing remaining is to check that all properties exist (since missing properties won’t appear as invalid)
|
|
38
|
+
function validateBorder(value: unknown, { node, filename }: { node?: momoa.AnyNode; filename?: string }) {
|
|
39
|
+
if (!value || typeof value !== 'object' || !BORDER_REQUIRED_PROPERTIES.every((property) => property in value)) {
|
|
40
|
+
report({ messageId: ERROR, filename, node });
|
|
41
|
+
} else {
|
|
42
|
+
for (const key of Object.keys(value)) {
|
|
43
|
+
if (!BORDER_REQUIRED_PROPERTIES.includes(key as (typeof BORDER_REQUIRED_PROPERTIES)[number])) {
|
|
44
|
+
report({
|
|
45
|
+
messageId: ERROR_INVALID_PROP,
|
|
46
|
+
data: { key: JSON.stringify(key) },
|
|
47
|
+
node: getObjMember(node as momoa.ObjectNode, key) ?? node,
|
|
48
|
+
filename,
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
},
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
export default rule;
|
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
import type * as momoa from '@humanwhocodes/momoa';
|
|
2
|
+
import { getObjMember } from '@terrazzo/json-schema-tools';
|
|
3
|
+
import {
|
|
4
|
+
type BorderValue,
|
|
5
|
+
COLORSPACE,
|
|
6
|
+
type ColorSpaceDefinition,
|
|
7
|
+
type ColorValueNormalized,
|
|
8
|
+
type GradientStopNormalized,
|
|
9
|
+
type GradientValueNormalized,
|
|
10
|
+
isAlias,
|
|
11
|
+
parseColor,
|
|
12
|
+
type ShadowValue,
|
|
13
|
+
} from '@terrazzo/token-tools';
|
|
14
|
+
import type { LintRule } from '../../../types.js';
|
|
15
|
+
import { docsLink } from '../lib/docs.js';
|
|
16
|
+
|
|
17
|
+
export const VALID_COLOR = 'core/valid-color';
|
|
18
|
+
|
|
19
|
+
const ERROR_ALPHA = 'ERROR_ALPHA';
|
|
20
|
+
const ERROR_INVALID_COLOR = 'ERROR_INVALID_COLOR';
|
|
21
|
+
const ERROR_INVALID_COLOR_SPACE = 'ERROR_INVALID_COLOR_SPACE';
|
|
22
|
+
const ERROR_INVALID_COMPONENT_LENGTH = 'ERROR_INVALID_COMPONENT_LENGTH';
|
|
23
|
+
const ERROR_INVALID_HEX8 = 'ERROR_INVALID_HEX8';
|
|
24
|
+
const ERROR_INVALID_PROP = 'ERROR_INVALID_PROP';
|
|
25
|
+
const ERROR_MISSING_COMPONENTS = 'ERROR_MISSING_COMPONENTS';
|
|
26
|
+
const ERROR_OBJ_FORMAT = 'ERROR_OBJ_FORMAT';
|
|
27
|
+
const ERROR_OUT_OF_RANGE = 'ERROR_OUT_OF_RANGE';
|
|
28
|
+
|
|
29
|
+
export interface RuleValidColorOptions {
|
|
30
|
+
/**
|
|
31
|
+
* Allow the legacy format of only a string sRGB hex code
|
|
32
|
+
* @default false
|
|
33
|
+
*/
|
|
34
|
+
legacyFormat?: boolean;
|
|
35
|
+
/**
|
|
36
|
+
* Allow colors to be defined out of the expected ranges.
|
|
37
|
+
* @default false
|
|
38
|
+
*/
|
|
39
|
+
ignoreRanges?: boolean;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const rule: LintRule<
|
|
43
|
+
| typeof ERROR_ALPHA
|
|
44
|
+
| typeof ERROR_INVALID_COLOR
|
|
45
|
+
| typeof ERROR_INVALID_COLOR_SPACE
|
|
46
|
+
| typeof ERROR_INVALID_COMPONENT_LENGTH
|
|
47
|
+
| typeof ERROR_INVALID_HEX8
|
|
48
|
+
| typeof ERROR_INVALID_PROP
|
|
49
|
+
| typeof ERROR_MISSING_COMPONENTS
|
|
50
|
+
| typeof ERROR_OBJ_FORMAT
|
|
51
|
+
| typeof ERROR_OUT_OF_RANGE,
|
|
52
|
+
RuleValidColorOptions
|
|
53
|
+
> = {
|
|
54
|
+
meta: {
|
|
55
|
+
messages: {
|
|
56
|
+
[ERROR_ALPHA]: `Alpha {{ alpha }} not in range 0 – 1.`,
|
|
57
|
+
[ERROR_INVALID_COLOR_SPACE]: `Invalid color space: {{ colorSpace }}. Expected ${new Intl.ListFormat(undefined, { type: 'disjunction' }).format(Object.keys(COLORSPACE))}`,
|
|
58
|
+
[ERROR_INVALID_COLOR]: `Could not parse color {{ color }}.`,
|
|
59
|
+
[ERROR_INVALID_COMPONENT_LENGTH]: 'Expected {{ expected }} components, received {{ got }}.',
|
|
60
|
+
[ERROR_INVALID_HEX8]: `Hex value can’t be semi-transparent.`,
|
|
61
|
+
[ERROR_INVALID_PROP]: `Unknown property {{ key }}.`,
|
|
62
|
+
[ERROR_MISSING_COMPONENTS]: 'Expected components to be array of numbers, received {{ got }}.',
|
|
63
|
+
[ERROR_OBJ_FORMAT]:
|
|
64
|
+
'Migrate to the new object format, e.g. "#ff0000" → { "colorSpace": "srgb", "components": [1, 0, 0] } }',
|
|
65
|
+
[ERROR_OUT_OF_RANGE]: `Invalid range for color space {{ colorSpace }}. Expected {{ range }}.`,
|
|
66
|
+
},
|
|
67
|
+
docs: {
|
|
68
|
+
description: 'Require color tokens to follow the format.',
|
|
69
|
+
url: docsLink(VALID_COLOR),
|
|
70
|
+
},
|
|
71
|
+
},
|
|
72
|
+
defaultOptions: {
|
|
73
|
+
legacyFormat: false,
|
|
74
|
+
ignoreRanges: false,
|
|
75
|
+
},
|
|
76
|
+
create({ tokens, options, report }) {
|
|
77
|
+
for (const t of Object.values(tokens)) {
|
|
78
|
+
if (t.aliasOf || !t.originalValue) {
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
switch (t.$type) {
|
|
83
|
+
case 'color': {
|
|
84
|
+
validateColor(t.originalValue.$value, {
|
|
85
|
+
node: getObjMember(t.source.node, '$value'),
|
|
86
|
+
filename: t.source.filename,
|
|
87
|
+
});
|
|
88
|
+
break;
|
|
89
|
+
}
|
|
90
|
+
case 'border': {
|
|
91
|
+
if ((t.originalValue.$value as any).color && !isAlias((t.originalValue.$value as any).color)) {
|
|
92
|
+
validateColor((t.originalValue.$value as BorderValue).color, {
|
|
93
|
+
node: getObjMember(getObjMember(t.source.node, '$value') as momoa.ObjectNode, 'color'),
|
|
94
|
+
filename: t.source.filename,
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
break;
|
|
98
|
+
}
|
|
99
|
+
case 'gradient': {
|
|
100
|
+
const $valueNode = getObjMember(t.source.node, '$value') as momoa.ArrayNode;
|
|
101
|
+
for (let i = 0; i < (t.originalValue.$value as GradientValueNormalized).length; i++) {
|
|
102
|
+
const stop = t.originalValue.$value[i] as GradientStopNormalized;
|
|
103
|
+
if (!stop.color || isAlias(stop.color as any)) {
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
validateColor(stop.color, {
|
|
107
|
+
node: getObjMember($valueNode.elements[i]!.value as momoa.ObjectNode, 'color'),
|
|
108
|
+
filename: t.source.filename,
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
break;
|
|
112
|
+
}
|
|
113
|
+
case 'shadow': {
|
|
114
|
+
const $value = (
|
|
115
|
+
Array.isArray(t.originalValue.$value) ? t.originalValue.$value : [t.originalValue.$value]
|
|
116
|
+
) as ShadowValue[];
|
|
117
|
+
const $valueNode = getObjMember(t.source.node, '$value') as momoa.ObjectNode | momoa.ArrayNode;
|
|
118
|
+
for (let i = 0; i < $value.length; i++) {
|
|
119
|
+
const layer = $value[i]!;
|
|
120
|
+
if (!layer.color || isAlias(layer.color as any)) {
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
validateColor(layer.color, {
|
|
124
|
+
node:
|
|
125
|
+
$valueNode.type === 'Object'
|
|
126
|
+
? getObjMember($valueNode, 'color')
|
|
127
|
+
: getObjMember($valueNode.elements[i]!.value as momoa.ObjectNode, 'color'),
|
|
128
|
+
filename: t.source.filename,
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
break;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function validateColor(value: unknown, { node, filename }: { node?: momoa.AnyNode; filename?: string }) {
|
|
136
|
+
if (!value) {
|
|
137
|
+
report({ messageId: ERROR_INVALID_COLOR, data: { color: JSON.stringify(value) }, node, filename });
|
|
138
|
+
} else if (typeof value === 'object') {
|
|
139
|
+
for (const key of Object.keys(value)) {
|
|
140
|
+
if (!['colorSpace', 'components', 'channels' /* TODO: remove */, 'hex', 'alpha'].includes(key)) {
|
|
141
|
+
report({
|
|
142
|
+
messageId: ERROR_INVALID_PROP,
|
|
143
|
+
data: { key: JSON.stringify(key) },
|
|
144
|
+
node: getObjMember(node as momoa.ObjectNode, key) ?? node,
|
|
145
|
+
filename,
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Color space
|
|
151
|
+
const colorSpace =
|
|
152
|
+
'colorSpace' in value && typeof value.colorSpace === 'string' ? value.colorSpace : undefined;
|
|
153
|
+
const csData = (COLORSPACE as Record<string, ColorSpaceDefinition>)[colorSpace!] || undefined;
|
|
154
|
+
if (!('colorSpace' in value) || !csData) {
|
|
155
|
+
report({
|
|
156
|
+
messageId: ERROR_INVALID_COLOR_SPACE,
|
|
157
|
+
data: { colorSpace },
|
|
158
|
+
node: getObjMember(node as momoa.ObjectNode, 'colorSpace') ?? node,
|
|
159
|
+
filename,
|
|
160
|
+
});
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Component ranges
|
|
165
|
+
const components = 'components' in value ? value.components : undefined;
|
|
166
|
+
if (Array.isArray(components)) {
|
|
167
|
+
if (csData?.ranges && components?.length === csData.ranges.length) {
|
|
168
|
+
for (let i = 0; i < components.length; i++) {
|
|
169
|
+
if (
|
|
170
|
+
!Number.isFinite(components[i]) ||
|
|
171
|
+
components[i]! < csData.ranges[i]![0] ||
|
|
172
|
+
components[i]! > csData.ranges[i]![1]
|
|
173
|
+
) {
|
|
174
|
+
// special case for any hue-based components: allow null
|
|
175
|
+
if (
|
|
176
|
+
!(colorSpace === 'hsl' && components[0]! === null) &&
|
|
177
|
+
!(colorSpace === 'hwb' && components[0]! === null) &&
|
|
178
|
+
!(colorSpace === 'lch' && components[2]! === null) &&
|
|
179
|
+
!(colorSpace === 'oklch' && components[2]! === null)
|
|
180
|
+
) {
|
|
181
|
+
report({
|
|
182
|
+
messageId: ERROR_OUT_OF_RANGE,
|
|
183
|
+
data: { colorSpace, range: `[${csData.ranges.map((r) => `${r[0]}–${r[1]}`).join(', ')}]` },
|
|
184
|
+
node: getObjMember(node as momoa.ObjectNode, 'components') ?? node,
|
|
185
|
+
filename,
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
} else {
|
|
191
|
+
report({
|
|
192
|
+
messageId: ERROR_INVALID_COMPONENT_LENGTH,
|
|
193
|
+
data: { expected: csData?.ranges.length, got: (components as number[] | undefined)?.length ?? 0 },
|
|
194
|
+
node: getObjMember(node as momoa.ObjectNode, 'components') ?? node,
|
|
195
|
+
filename,
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
} else {
|
|
199
|
+
report({
|
|
200
|
+
messageId: ERROR_MISSING_COMPONENTS,
|
|
201
|
+
data: { got: JSON.stringify(components) },
|
|
202
|
+
node: getObjMember(node as momoa.ObjectNode, 'components') ?? node,
|
|
203
|
+
filename,
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// Alpha
|
|
208
|
+
const alpha = 'alpha' in value ? value.alpha : undefined;
|
|
209
|
+
if (alpha !== undefined && (typeof alpha !== 'number' || alpha < 0 || alpha > 1)) {
|
|
210
|
+
report({
|
|
211
|
+
messageId: ERROR_ALPHA,
|
|
212
|
+
data: { alpha },
|
|
213
|
+
node: getObjMember(node as momoa.ObjectNode, 'alpha') ?? node,
|
|
214
|
+
filename,
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// Hex
|
|
219
|
+
const hex = 'hex' in value ? value.hex : undefined;
|
|
220
|
+
if (hex) {
|
|
221
|
+
let color: ColorValueNormalized;
|
|
222
|
+
try {
|
|
223
|
+
color = parseColor(hex as string);
|
|
224
|
+
} catch {
|
|
225
|
+
report({
|
|
226
|
+
messageId: ERROR_INVALID_COLOR,
|
|
227
|
+
data: { color: hex },
|
|
228
|
+
node: getObjMember(node as momoa.ObjectNode, 'hex') ?? node,
|
|
229
|
+
filename,
|
|
230
|
+
});
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
// since we’re only parsing hex, this should always have alpha: 1
|
|
234
|
+
if (color.alpha !== 1) {
|
|
235
|
+
report({
|
|
236
|
+
messageId: ERROR_INVALID_HEX8,
|
|
237
|
+
data: { color: hex },
|
|
238
|
+
node: getObjMember(node as momoa.ObjectNode, 'hex') ?? node,
|
|
239
|
+
filename,
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
} else if (typeof value === 'string') {
|
|
244
|
+
if (isAlias(value)) {
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
if (!options.legacyFormat) {
|
|
248
|
+
report({ messageId: ERROR_OBJ_FORMAT, data: { color: JSON.stringify(value) }, node, filename });
|
|
249
|
+
} else {
|
|
250
|
+
// Legacy format
|
|
251
|
+
try {
|
|
252
|
+
parseColor(value as string);
|
|
253
|
+
} catch {
|
|
254
|
+
report({ messageId: ERROR_INVALID_COLOR, data: { color: JSON.stringify(value) }, node, filename });
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
} else {
|
|
258
|
+
report({ messageId: ERROR_INVALID_COLOR, node, filename });
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
},
|
|
263
|
+
};
|
|
264
|
+
|
|
265
|
+
export default rule;
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import type * as momoa from '@humanwhocodes/momoa';
|
|
2
|
+
import { getObjMember, isPure$ref } from '@terrazzo/json-schema-tools';
|
|
3
|
+
import { isAlias } from '@terrazzo/token-tools';
|
|
4
|
+
import type { LintRule } from '../../../types.js';
|
|
5
|
+
import { docsLink } from '../lib/docs.js';
|
|
6
|
+
|
|
7
|
+
export const VALID_CUBIC_BEZIER = 'core/valid-cubic-bezier';
|
|
8
|
+
|
|
9
|
+
const ERROR = 'ERROR';
|
|
10
|
+
const ERROR_X = 'ERROR_X';
|
|
11
|
+
const ERROR_Y = 'ERROR_Y';
|
|
12
|
+
|
|
13
|
+
const rule: LintRule<typeof ERROR | typeof ERROR_X | typeof ERROR_Y> = {
|
|
14
|
+
meta: {
|
|
15
|
+
messages: {
|
|
16
|
+
[ERROR]: 'Expected [number, number, number, number].',
|
|
17
|
+
[ERROR_X]: 'x values must be between 0-1.',
|
|
18
|
+
[ERROR_Y]: 'y values must be a valid number.',
|
|
19
|
+
},
|
|
20
|
+
docs: {
|
|
21
|
+
description: 'Require cubicBezier tokens to follow the format.',
|
|
22
|
+
url: docsLink(VALID_CUBIC_BEZIER),
|
|
23
|
+
},
|
|
24
|
+
},
|
|
25
|
+
defaultOptions: {},
|
|
26
|
+
create({ tokens, report }) {
|
|
27
|
+
for (const t of Object.values(tokens)) {
|
|
28
|
+
if (t.aliasOf || !t.originalValue) {
|
|
29
|
+
continue;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
switch (t.$type) {
|
|
33
|
+
case 'cubicBezier': {
|
|
34
|
+
validateCubicBezier(t.originalValue.$value, {
|
|
35
|
+
node: getObjMember(t.source.node, '$value') as momoa.ArrayNode,
|
|
36
|
+
filename: t.source.filename,
|
|
37
|
+
});
|
|
38
|
+
break;
|
|
39
|
+
}
|
|
40
|
+
case 'transition': {
|
|
41
|
+
if (
|
|
42
|
+
typeof t.originalValue.$value === 'object' &&
|
|
43
|
+
t.originalValue.$value.timingFunction &&
|
|
44
|
+
!isAlias(t.originalValue.$value.timingFunction as string)
|
|
45
|
+
) {
|
|
46
|
+
const $valueNode = getObjMember(t.source.node, '$value') as momoa.ObjectNode;
|
|
47
|
+
validateCubicBezier(t.originalValue.$value.timingFunction, {
|
|
48
|
+
node: getObjMember($valueNode, 'timingFunction') as momoa.ArrayNode,
|
|
49
|
+
filename: t.source.filename,
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function validateCubicBezier(value: unknown, { node, filename }: { node: momoa.ArrayNode; filename?: string }) {
|
|
56
|
+
if (Array.isArray(value) && value.length === 4) {
|
|
57
|
+
// validate x values
|
|
58
|
+
for (const pos of [0, 2]) {
|
|
59
|
+
if (isAlias(value[pos]) || isPure$ref(value[pos])) {
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
if (!(value[pos] >= 0 && value[pos] <= 1)) {
|
|
63
|
+
report({ messageId: ERROR_X, node: (node as momoa.ArrayNode).elements[pos], filename });
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
// validate y values
|
|
67
|
+
for (const pos of [1, 3]) {
|
|
68
|
+
if (isAlias(value[pos]) || isPure$ref(value[pos])) {
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
if (typeof value[pos] !== 'number') {
|
|
72
|
+
report({ messageId: ERROR_Y, node: (node as momoa.ArrayNode).elements[pos], filename });
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
} else {
|
|
76
|
+
report({ messageId: ERROR, node, filename });
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
},
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
export default rule;
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
import type * as momoa from '@humanwhocodes/momoa';
|
|
2
|
+
import { getObjMember } from '@terrazzo/json-schema-tools';
|
|
3
|
+
import { isAlias } from '@terrazzo/token-tools';
|
|
4
|
+
import type { LintRule } from '../../../types.js';
|
|
5
|
+
import { docsLink } from '../lib/docs.js';
|
|
6
|
+
|
|
7
|
+
export const VALID_DIMENSION = 'core/valid-dimension';
|
|
8
|
+
|
|
9
|
+
const ERROR_FORMAT = 'ERROR_FORMAT';
|
|
10
|
+
const ERROR_INVALID_PROP = 'ERROR_INVALID_PROP';
|
|
11
|
+
const ERROR_LEGACY = 'ERROR_LEGACY';
|
|
12
|
+
const ERROR_UNIT = 'ERROR_UNIT';
|
|
13
|
+
const ERROR_VALUE = 'ERROR_VALUE';
|
|
14
|
+
|
|
15
|
+
export interface RuleValidDimension {
|
|
16
|
+
/**
|
|
17
|
+
* Allow the use of unknown "unit" values
|
|
18
|
+
* @default false
|
|
19
|
+
*/
|
|
20
|
+
legacyFormat?: boolean;
|
|
21
|
+
/**
|
|
22
|
+
* Only allow the following units.
|
|
23
|
+
* @default ["px", "rem"]
|
|
24
|
+
*/
|
|
25
|
+
allowedUnits?: string[];
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const rule: LintRule<
|
|
29
|
+
typeof ERROR_FORMAT | typeof ERROR_LEGACY | typeof ERROR_UNIT | typeof ERROR_VALUE | typeof ERROR_INVALID_PROP,
|
|
30
|
+
RuleValidDimension
|
|
31
|
+
> = {
|
|
32
|
+
meta: {
|
|
33
|
+
messages: {
|
|
34
|
+
[ERROR_FORMAT]: 'Invalid dimension: {{ value }}. Expected object with "value" and "unit".',
|
|
35
|
+
[ERROR_LEGACY]: 'Migrate to the new object format: { "value": 10, "unit": "px" }.',
|
|
36
|
+
[ERROR_UNIT]: 'Unit {{ unit }} not allowed. Expected {{ allowed }}.',
|
|
37
|
+
[ERROR_INVALID_PROP]: 'Unknown property {{ key }}.',
|
|
38
|
+
[ERROR_VALUE]: 'Expected number, received {{ value }}.',
|
|
39
|
+
},
|
|
40
|
+
docs: {
|
|
41
|
+
description: 'Require dimension tokens to follow the format',
|
|
42
|
+
url: docsLink(VALID_DIMENSION),
|
|
43
|
+
},
|
|
44
|
+
},
|
|
45
|
+
defaultOptions: {
|
|
46
|
+
legacyFormat: false,
|
|
47
|
+
allowedUnits: ['px', 'em', 'rem'],
|
|
48
|
+
},
|
|
49
|
+
create({ tokens, options, report }) {
|
|
50
|
+
for (const t of Object.values(tokens)) {
|
|
51
|
+
if (t.aliasOf || !t.originalValue) {
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
switch (t.$type) {
|
|
56
|
+
case 'dimension': {
|
|
57
|
+
validateDimension(t.originalValue.$value, {
|
|
58
|
+
node: getObjMember(t.source.node, '$value'),
|
|
59
|
+
filename: t.source.filename,
|
|
60
|
+
});
|
|
61
|
+
break;
|
|
62
|
+
}
|
|
63
|
+
case 'strokeStyle': {
|
|
64
|
+
if (typeof t.originalValue.$value === 'object' && Array.isArray(t.originalValue.$value.dashArray)) {
|
|
65
|
+
const $valueNode = getObjMember(t.source.node, '$value') as momoa.ObjectNode;
|
|
66
|
+
const dashArray = getObjMember($valueNode, 'dashArray') as momoa.ArrayNode;
|
|
67
|
+
for (let i = 0; i < t.originalValue.$value.dashArray.length; i++) {
|
|
68
|
+
if (isAlias(t.originalValue.$value.dashArray[i] as string)) {
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
validateDimension(t.originalValue.$value.dashArray[i], {
|
|
72
|
+
node: dashArray.elements[i]!.value,
|
|
73
|
+
filename: t.source.filename,
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
break;
|
|
78
|
+
}
|
|
79
|
+
case 'border': {
|
|
80
|
+
const $valueNode = getObjMember(t.source.node, '$value') as momoa.ObjectNode;
|
|
81
|
+
if (typeof t.originalValue.$value === 'object') {
|
|
82
|
+
if (t.originalValue.$value.width && !isAlias(t.originalValue.$value.width as string)) {
|
|
83
|
+
validateDimension(t.originalValue.$value.width, {
|
|
84
|
+
node: getObjMember($valueNode, 'width'),
|
|
85
|
+
filename: t.source.filename,
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
if (
|
|
89
|
+
typeof t.originalValue.$value.style === 'object' &&
|
|
90
|
+
Array.isArray(t.originalValue.$value.style.dashArray)
|
|
91
|
+
) {
|
|
92
|
+
const style = getObjMember($valueNode, 'style') as momoa.ObjectNode;
|
|
93
|
+
const dashArray = getObjMember(style, 'dashArray') as momoa.ArrayNode;
|
|
94
|
+
for (let i = 0; i < t.originalValue.$value.style.dashArray.length; i++) {
|
|
95
|
+
if (isAlias(t.originalValue.$value.style.dashArray[i] as string)) {
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
validateDimension(t.originalValue.$value.style.dashArray[i], {
|
|
99
|
+
node: dashArray.elements[i]!.value,
|
|
100
|
+
filename: t.source.filename,
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
break;
|
|
106
|
+
}
|
|
107
|
+
case 'shadow': {
|
|
108
|
+
if (t.originalValue.$value && typeof t.originalValue.$value === 'object') {
|
|
109
|
+
const $valueNode = getObjMember(t.source.node, '$value') as momoa.ObjectNode | momoa.ArrayNode;
|
|
110
|
+
const valueArray = Array.isArray(t.originalValue.$value)
|
|
111
|
+
? t.originalValue.$value
|
|
112
|
+
: [t.originalValue.$value];
|
|
113
|
+
for (let i = 0; i < valueArray.length; i++) {
|
|
114
|
+
const node =
|
|
115
|
+
$valueNode.type === 'Array' ? ($valueNode.elements[i]!.value as momoa.ObjectNode) : $valueNode;
|
|
116
|
+
for (const property of ['offsetX', 'offsetY', 'blur', 'spread'] as const) {
|
|
117
|
+
if (isAlias(valueArray[i]![property] as string)) {
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
validateDimension(valueArray[i]![property], {
|
|
121
|
+
node: getObjMember(node, property),
|
|
122
|
+
filename: t.source.filename,
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
break;
|
|
128
|
+
}
|
|
129
|
+
case 'typography': {
|
|
130
|
+
const $valueNode = getObjMember(t.source.node, '$value') as momoa.ObjectNode;
|
|
131
|
+
if (typeof t.originalValue.$value === 'object') {
|
|
132
|
+
for (const property of ['fontSize', 'lineHeight', 'letterSpacing'] as const) {
|
|
133
|
+
if (property in t.originalValue.$value) {
|
|
134
|
+
if (
|
|
135
|
+
isAlias(t.originalValue.$value[property] as string) ||
|
|
136
|
+
// special case: lineHeight may be a number
|
|
137
|
+
(property === 'lineHeight' && typeof t.originalValue.$value[property] === 'number')
|
|
138
|
+
) {
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
validateDimension(t.originalValue.$value[property], {
|
|
142
|
+
node: getObjMember($valueNode, property),
|
|
143
|
+
filename: t.source.filename,
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
break;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function validateDimension(value: unknown, { node, filename }: { node?: momoa.AnyNode; filename?: string }) {
|
|
153
|
+
if (value && typeof value === 'object') {
|
|
154
|
+
for (const key of Object.keys(value)) {
|
|
155
|
+
if (!['value', 'unit'].includes(key)) {
|
|
156
|
+
report({
|
|
157
|
+
messageId: ERROR_INVALID_PROP,
|
|
158
|
+
data: { key: JSON.stringify(key) },
|
|
159
|
+
node: getObjMember(node as momoa.ObjectNode, key) ?? node,
|
|
160
|
+
filename,
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const { unit, value: numValue } = value as Record<string, any>;
|
|
166
|
+
if (!('value' in value || 'unit' in value)) {
|
|
167
|
+
report({ messageId: ERROR_FORMAT, data: { value }, node, filename });
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
if (!options.allowedUnits!.includes(unit)) {
|
|
171
|
+
report({
|
|
172
|
+
messageId: ERROR_UNIT,
|
|
173
|
+
data: {
|
|
174
|
+
unit,
|
|
175
|
+
allowed: new Intl.ListFormat('en-us', { type: 'disjunction' }).format(options.allowedUnits!),
|
|
176
|
+
},
|
|
177
|
+
node: getObjMember(node as momoa.ObjectNode, 'unit') ?? node,
|
|
178
|
+
filename,
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
if (!Number.isFinite(numValue)) {
|
|
182
|
+
report({
|
|
183
|
+
messageId: ERROR_VALUE,
|
|
184
|
+
data: { value },
|
|
185
|
+
node: getObjMember(node as momoa.ObjectNode, 'value') ?? node,
|
|
186
|
+
filename,
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
} else if (typeof value === 'string' && !options.legacyFormat) {
|
|
190
|
+
report({ messageId: ERROR_LEGACY, node, filename });
|
|
191
|
+
} else {
|
|
192
|
+
report({ messageId: ERROR_FORMAT, data: { value }, node, filename });
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
},
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
export default rule;
|