@terrazzo/parser 0.10.4 → 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/dist/index.d.ts +82 -307
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2186 -3621
- package/dist/index.js.map +1 -1
- package/package.json +4 -3
- package/src/build/index.ts +32 -41
- package/src/config.ts +13 -6
- package/src/lib/code-frame.ts +3 -0
- 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 -328
- 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
|
@@ -0,0 +1,123 @@
|
|
|
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_DURATION = 'core/valid-duration';
|
|
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
|
+
* Allow the use of unknown "unit" values
|
|
23
|
+
* @default false
|
|
24
|
+
*/
|
|
25
|
+
unknownUnits?: boolean;
|
|
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]: 'Migrate to the new object format: { "value": 2, "unit": "ms" }.',
|
|
35
|
+
[ERROR_LEGACY]: 'Migrate to the new object format: { "value": 10, "unit": "px" }.',
|
|
36
|
+
[ERROR_INVALID_PROP]: 'Unknown property: {{ key }}.',
|
|
37
|
+
[ERROR_UNIT]: 'Unknown unit {{ unit }}. Expected "ms" or "s".',
|
|
38
|
+
[ERROR_VALUE]: 'Expected number, received {{ value }}.',
|
|
39
|
+
},
|
|
40
|
+
docs: {
|
|
41
|
+
description: 'Require duration tokens to follow the format',
|
|
42
|
+
url: docsLink(VALID_DURATION),
|
|
43
|
+
},
|
|
44
|
+
},
|
|
45
|
+
defaultOptions: {
|
|
46
|
+
legacyFormat: false,
|
|
47
|
+
unknownUnits: false,
|
|
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 'duration': {
|
|
57
|
+
validateDuration(t.originalValue.$value, {
|
|
58
|
+
node: getObjMember(t.source.node, '$value')!,
|
|
59
|
+
filename: t.source.filename,
|
|
60
|
+
});
|
|
61
|
+
break;
|
|
62
|
+
}
|
|
63
|
+
case 'transition': {
|
|
64
|
+
if (typeof t.originalValue.$value === 'object') {
|
|
65
|
+
const $valueNode = getObjMember(t.source.node, '$value');
|
|
66
|
+
for (const property of ['duration', 'delay'] as const) {
|
|
67
|
+
if (t.originalValue.$value[property] && !isAlias(t.originalValue.$value[property] as string)) {
|
|
68
|
+
validateDuration(t.originalValue.$value[property], {
|
|
69
|
+
node: getObjMember($valueNode as momoa.ObjectNode, property)!,
|
|
70
|
+
filename: t.source.filename,
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
break;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function validateDuration(value: unknown, { node, filename }: { node: momoa.AnyNode; filename?: string }) {
|
|
80
|
+
if (value && typeof value === 'object') {
|
|
81
|
+
for (const key of Object.keys(value)) {
|
|
82
|
+
if (!['value', 'unit'].includes(key)) {
|
|
83
|
+
report({
|
|
84
|
+
messageId: ERROR_INVALID_PROP,
|
|
85
|
+
data: { key: JSON.stringify(key) },
|
|
86
|
+
node: getObjMember(node as momoa.ObjectNode, key) ?? node,
|
|
87
|
+
filename,
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const { unit, value: numValue } = value as Record<string, any>;
|
|
93
|
+
if (!('value' in value || 'unit' in value)) {
|
|
94
|
+
report({ messageId: ERROR_FORMAT, data: { value }, node, filename });
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
if (!options.unknownUnits && !['ms', 's'].includes(unit)) {
|
|
98
|
+
report({
|
|
99
|
+
messageId: ERROR_UNIT,
|
|
100
|
+
data: { unit },
|
|
101
|
+
node: getObjMember(node as momoa.ObjectNode, 'unit') ?? node,
|
|
102
|
+
filename,
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
if (!Number.isFinite(numValue)) {
|
|
106
|
+
report({
|
|
107
|
+
messageId: ERROR_VALUE,
|
|
108
|
+
data: { value },
|
|
109
|
+
node: getObjMember(node as momoa.ObjectNode, 'value') ?? node,
|
|
110
|
+
filename,
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
} else if (typeof value === 'string' && !options.legacyFormat) {
|
|
114
|
+
report({ messageId: ERROR_FORMAT, node, filename });
|
|
115
|
+
} else {
|
|
116
|
+
report({ messageId: ERROR_FORMAT, data: { value }, node, filename });
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
},
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
export default rule;
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import type * as momoa from '@humanwhocodes/momoa';
|
|
2
|
+
import { getObjMember, getObjMembers } from '@terrazzo/json-schema-tools';
|
|
3
|
+
import type { LintRule } from '../../../types.js';
|
|
4
|
+
import { docsLink } from '../lib/docs.js';
|
|
5
|
+
|
|
6
|
+
export const VALID_FONT_FAMILY = 'core/valid-font-family';
|
|
7
|
+
|
|
8
|
+
const ERROR = 'ERROR';
|
|
9
|
+
|
|
10
|
+
const rule: LintRule<typeof ERROR> = {
|
|
11
|
+
meta: {
|
|
12
|
+
messages: {
|
|
13
|
+
[ERROR]: 'Must be a string, or array of strings.',
|
|
14
|
+
},
|
|
15
|
+
docs: {
|
|
16
|
+
description: 'Require fontFamily tokens to follow the format.',
|
|
17
|
+
url: docsLink(VALID_FONT_FAMILY),
|
|
18
|
+
},
|
|
19
|
+
},
|
|
20
|
+
defaultOptions: {},
|
|
21
|
+
create({ tokens, report }) {
|
|
22
|
+
for (const t of Object.values(tokens)) {
|
|
23
|
+
if (t.aliasOf || !t.originalValue) {
|
|
24
|
+
continue;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
switch (t.$type) {
|
|
28
|
+
case 'fontFamily': {
|
|
29
|
+
validateFontFamily(t.originalValue.$value, {
|
|
30
|
+
node: getObjMember(t.source.node, '$value') as momoa.ArrayNode,
|
|
31
|
+
filename: t.source.filename,
|
|
32
|
+
});
|
|
33
|
+
break;
|
|
34
|
+
}
|
|
35
|
+
case 'typography': {
|
|
36
|
+
if (typeof t.originalValue.$value === 'object' && t.originalValue.$value.fontFamily) {
|
|
37
|
+
if (t.partialAliasOf?.fontFamily) {
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
const $value = getObjMember(t.source.node, '$value');
|
|
41
|
+
const properties = getObjMembers($value as momoa.ObjectNode);
|
|
42
|
+
validateFontFamily(t.originalValue.$value.fontFamily, {
|
|
43
|
+
node: properties.fontFamily as momoa.ArrayNode,
|
|
44
|
+
filename: t.source.filename,
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
break;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function validateFontFamily(value: unknown, { node, filename }: { node: momoa.ArrayNode; filename?: string }) {
|
|
52
|
+
if (typeof value === 'string') {
|
|
53
|
+
if (!value) {
|
|
54
|
+
report({ messageId: ERROR, node, filename });
|
|
55
|
+
}
|
|
56
|
+
} else if (Array.isArray(value)) {
|
|
57
|
+
if (!value.every((v) => v && typeof v === 'string')) {
|
|
58
|
+
report({ messageId: ERROR, node, filename });
|
|
59
|
+
}
|
|
60
|
+
} else {
|
|
61
|
+
report({ messageId: ERROR, node, filename });
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
},
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
export default rule;
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import type * as momoa from '@humanwhocodes/momoa';
|
|
2
|
+
import { getObjMember, getObjMembers } from '@terrazzo/json-schema-tools';
|
|
3
|
+
import { FONT_WEIGHTS } from '@terrazzo/token-tools';
|
|
4
|
+
import type { LintRule } from '../../../types.js';
|
|
5
|
+
import { docsLink } from '../lib/docs.js';
|
|
6
|
+
|
|
7
|
+
export const VALID_FONT_WEIGHT = 'core/valid-font-weight';
|
|
8
|
+
|
|
9
|
+
const ERROR = 'ERROR';
|
|
10
|
+
const ERROR_STYLE = 'ERROR_STYLE';
|
|
11
|
+
|
|
12
|
+
export interface RuleFontWeightOptions {
|
|
13
|
+
/**
|
|
14
|
+
* Enforce either:
|
|
15
|
+
* - "numbers" (0-999)
|
|
16
|
+
* - "names" ("light", "medium", "bold", etc.)
|
|
17
|
+
*/
|
|
18
|
+
style?: 'numbers' | 'names';
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const rule: LintRule<typeof ERROR | typeof ERROR_STYLE, RuleFontWeightOptions> = {
|
|
22
|
+
meta: {
|
|
23
|
+
messages: {
|
|
24
|
+
[ERROR]: `Must either be a valid number (0 - 999) or a valid font weight: ${new Intl.ListFormat(undefined, { type: 'disjunction' }).format(Object.keys(FONT_WEIGHTS))}.`,
|
|
25
|
+
[ERROR_STYLE]: 'Expected style {{ style }}, received {{ value }}.',
|
|
26
|
+
},
|
|
27
|
+
docs: {
|
|
28
|
+
description: 'Require number tokens to follow the format.',
|
|
29
|
+
url: docsLink(VALID_FONT_WEIGHT),
|
|
30
|
+
},
|
|
31
|
+
},
|
|
32
|
+
defaultOptions: {
|
|
33
|
+
style: undefined,
|
|
34
|
+
},
|
|
35
|
+
create({ tokens, options, report }) {
|
|
36
|
+
for (const t of Object.values(tokens)) {
|
|
37
|
+
if (t.aliasOf || !t.originalValue) {
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
switch (t.$type) {
|
|
42
|
+
case 'fontWeight': {
|
|
43
|
+
validateFontWeight(t.originalValue.$value, {
|
|
44
|
+
node: getObjMember(t.source.node, '$value') as momoa.StringNode,
|
|
45
|
+
filename: t.source.filename,
|
|
46
|
+
});
|
|
47
|
+
break;
|
|
48
|
+
}
|
|
49
|
+
case 'typography': {
|
|
50
|
+
if (typeof t.originalValue.$value === 'object' && t.originalValue.$value.fontWeight) {
|
|
51
|
+
if (t.partialAliasOf?.fontWeight) {
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
const $value = getObjMember(t.source.node, '$value');
|
|
55
|
+
const properties = getObjMembers($value as momoa.ObjectNode);
|
|
56
|
+
validateFontWeight(t.originalValue.$value.fontWeight, {
|
|
57
|
+
node: properties.fontWeight as momoa.StringNode,
|
|
58
|
+
filename: t.source.filename,
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
break;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function validateFontWeight(
|
|
66
|
+
value: unknown,
|
|
67
|
+
{ node, filename }: { node: momoa.StringNode | momoa.NumberNode; filename?: string },
|
|
68
|
+
) {
|
|
69
|
+
if (typeof value === 'string') {
|
|
70
|
+
if (options.style === 'numbers') {
|
|
71
|
+
report({ messageId: ERROR_STYLE, data: { style: 'numbers', value }, node, filename });
|
|
72
|
+
} else if (!(value in FONT_WEIGHTS)) {
|
|
73
|
+
report({ messageId: ERROR, node, filename });
|
|
74
|
+
}
|
|
75
|
+
} else if (typeof value === 'number') {
|
|
76
|
+
if (options.style === 'names') {
|
|
77
|
+
report({ messageId: ERROR_STYLE, data: { style: 'names', value }, node, filename });
|
|
78
|
+
} else if (!(value >= 0 && value < 1000)) {
|
|
79
|
+
report({ messageId: ERROR, node, filename });
|
|
80
|
+
}
|
|
81
|
+
} else {
|
|
82
|
+
report({ messageId: ERROR, node, filename });
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
},
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
export default rule;
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import type * as momoa from '@humanwhocodes/momoa';
|
|
2
|
+
import { getObjMember } from '@terrazzo/json-schema-tools';
|
|
3
|
+
import { GRADIENT_REQUIRED_STOP_PROPERTIES, isAlias } from '@terrazzo/token-tools';
|
|
4
|
+
import type { LintRule } from '../../../types.js';
|
|
5
|
+
import { docsLink } from '../lib/docs.js';
|
|
6
|
+
|
|
7
|
+
export const VALID_GRADIENT = 'core/valid-gradient';
|
|
8
|
+
|
|
9
|
+
const ERROR_MISSING = 'ERROR_MISSING';
|
|
10
|
+
const ERROR_POSITION = 'ERROR_POSITION';
|
|
11
|
+
const ERROR_INVALID_PROP = 'ERROR_INVALID_PROP';
|
|
12
|
+
|
|
13
|
+
const rule: LintRule<typeof ERROR_MISSING | typeof ERROR_POSITION | typeof ERROR_INVALID_PROP> = {
|
|
14
|
+
meta: {
|
|
15
|
+
messages: {
|
|
16
|
+
[ERROR_MISSING]: 'Must be an array of { color, position } objects.',
|
|
17
|
+
[ERROR_POSITION]: 'Expected number 0-1, received {{ value }}.',
|
|
18
|
+
[ERROR_INVALID_PROP]: 'Unknown property {{ key }}.',
|
|
19
|
+
},
|
|
20
|
+
docs: {
|
|
21
|
+
description: 'Require gradient tokens to follow the format.',
|
|
22
|
+
url: docsLink(VALID_GRADIENT),
|
|
23
|
+
},
|
|
24
|
+
},
|
|
25
|
+
defaultOptions: {},
|
|
26
|
+
create({ tokens, report }) {
|
|
27
|
+
for (const t of Object.values(tokens)) {
|
|
28
|
+
if (t.aliasOf || !t.originalValue || t.$type !== 'gradient') {
|
|
29
|
+
continue;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
validateGradient(t.originalValue.$value, {
|
|
33
|
+
node: getObjMember(t.source.node, '$value') as momoa.ArrayNode,
|
|
34
|
+
filename: t.source.filename,
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
function validateGradient(value: unknown, { node, filename }: { node: momoa.ArrayNode; filename?: string }) {
|
|
38
|
+
if (Array.isArray(value)) {
|
|
39
|
+
for (let i = 0; i < value.length; i++) {
|
|
40
|
+
const stop = value[i]!;
|
|
41
|
+
if (!stop || typeof stop !== 'object') {
|
|
42
|
+
report({ messageId: ERROR_MISSING, node, filename });
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
for (const property of GRADIENT_REQUIRED_STOP_PROPERTIES) {
|
|
46
|
+
if (!(property in stop)) {
|
|
47
|
+
report({ messageId: ERROR_MISSING, node: node.elements[i], filename });
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
for (const key of Object.keys(stop)) {
|
|
51
|
+
if (
|
|
52
|
+
!GRADIENT_REQUIRED_STOP_PROPERTIES.includes(key as (typeof GRADIENT_REQUIRED_STOP_PROPERTIES)[number])
|
|
53
|
+
) {
|
|
54
|
+
report({
|
|
55
|
+
messageId: ERROR_INVALID_PROP,
|
|
56
|
+
data: { key: JSON.stringify(key) },
|
|
57
|
+
node: node.elements[i],
|
|
58
|
+
filename,
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
if ('position' in stop && typeof stop.position !== 'number' && !isAlias(stop.position as string)) {
|
|
63
|
+
report({
|
|
64
|
+
messageId: ERROR_POSITION,
|
|
65
|
+
data: { value: stop.position },
|
|
66
|
+
node: getObjMember(node.elements[i]!.value as momoa.ObjectNode, 'position'),
|
|
67
|
+
filename,
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
} else {
|
|
72
|
+
report({ messageId: ERROR_MISSING, node, filename });
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
},
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
export default rule;
|
|
@@ -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_LINK = 'core/valid-link';
|
|
7
|
+
|
|
8
|
+
const ERROR = 'ERROR';
|
|
9
|
+
|
|
10
|
+
const rule: LintRule<typeof ERROR, {}> = {
|
|
11
|
+
meta: {
|
|
12
|
+
messages: {
|
|
13
|
+
[ERROR]: 'Must be a string.',
|
|
14
|
+
},
|
|
15
|
+
docs: {
|
|
16
|
+
description: 'Require link tokens to follow the Terrazzo extension.',
|
|
17
|
+
url: docsLink(VALID_LINK),
|
|
18
|
+
},
|
|
19
|
+
},
|
|
20
|
+
defaultOptions: {},
|
|
21
|
+
create({ tokens, report }) {
|
|
22
|
+
for (const t of Object.values(tokens)) {
|
|
23
|
+
if (t.aliasOf || !t.originalValue || t.$type !== 'link') {
|
|
24
|
+
continue;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
validateLink(t.originalValue.$value, {
|
|
28
|
+
node: getObjMember(t.source.node, '$value') as momoa.StringNode,
|
|
29
|
+
filename: t.source.filename,
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
function validateLink(value: unknown, { node, filename }: { node: momoa.StringNode; filename?: string }) {
|
|
33
|
+
if (!value || typeof value !== 'string') {
|
|
34
|
+
report({ messageId: ERROR, node, filename });
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
},
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
export default rule;
|
|
@@ -0,0 +1,63 @@
|
|
|
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_NUMBER = 'core/valid-number';
|
|
8
|
+
|
|
9
|
+
const ERROR_NAN = 'ERROR_NAN';
|
|
10
|
+
|
|
11
|
+
const rule: LintRule<typeof ERROR_NAN> = {
|
|
12
|
+
meta: {
|
|
13
|
+
messages: {
|
|
14
|
+
[ERROR_NAN]: 'Must be a number.',
|
|
15
|
+
},
|
|
16
|
+
docs: {
|
|
17
|
+
description: 'Require number tokens to follow the format.',
|
|
18
|
+
url: docsLink(VALID_NUMBER),
|
|
19
|
+
},
|
|
20
|
+
},
|
|
21
|
+
defaultOptions: {},
|
|
22
|
+
create({ tokens, report }) {
|
|
23
|
+
for (const t of Object.values(tokens)) {
|
|
24
|
+
if (t.aliasOf || !t.originalValue) {
|
|
25
|
+
continue;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
switch (t.$type) {
|
|
29
|
+
case 'number': {
|
|
30
|
+
validateNumber(t.originalValue.$value, {
|
|
31
|
+
node: getObjMember(t.source.node, '$value') as momoa.NumberNode,
|
|
32
|
+
filename: t.source.filename,
|
|
33
|
+
});
|
|
34
|
+
break;
|
|
35
|
+
}
|
|
36
|
+
// Note: gradient not needed, validated in gradient
|
|
37
|
+
case 'typography': {
|
|
38
|
+
const $valueNode = getObjMember(t.source.node, '$value') as momoa.ObjectNode;
|
|
39
|
+
if (typeof t.originalValue.$value === 'object') {
|
|
40
|
+
if (
|
|
41
|
+
t.originalValue.$value.lineHeight &&
|
|
42
|
+
!isAlias(t.originalValue.$value.lineHeight as string) &&
|
|
43
|
+
typeof t.originalValue.$value.lineHeight !== 'object'
|
|
44
|
+
) {
|
|
45
|
+
validateNumber(t.originalValue.$value.lineHeight, {
|
|
46
|
+
node: getObjMember($valueNode, 'lineHeight') as momoa.NumberNode,
|
|
47
|
+
filename: t.source.filename,
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function validateNumber(value: unknown, { node, filename }: { node: momoa.NumberNode; filename?: string }) {
|
|
55
|
+
if (typeof value !== 'number' || Number.isNaN(value)) {
|
|
56
|
+
report({ messageId: ERROR_NAN, node, filename });
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
},
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
export default rule;
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import type * as momoa from '@humanwhocodes/momoa';
|
|
2
|
+
import { getObjMember } from '@terrazzo/json-schema-tools';
|
|
3
|
+
import { SHADOW_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_SHADOW = 'core/valid-shadow';
|
|
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]: `Missing required properties: ${new Intl.ListFormat(undefined, { type: 'conjunction' }).format(SHADOW_REQUIRED_PROPERTIES)}.`,
|
|
16
|
+
[ERROR_INVALID_PROP]: 'Unknown property {{ key }}.',
|
|
17
|
+
},
|
|
18
|
+
docs: {
|
|
19
|
+
description: 'Require shadow tokens to follow the format.',
|
|
20
|
+
url: docsLink(VALID_SHADOW),
|
|
21
|
+
},
|
|
22
|
+
},
|
|
23
|
+
defaultOptions: {},
|
|
24
|
+
create({ tokens, report }) {
|
|
25
|
+
for (const t of Object.values(tokens)) {
|
|
26
|
+
if (t.aliasOf || !t.originalValue || t.$type !== 'shadow') {
|
|
27
|
+
continue;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
validateShadow(t.originalValue.$value, {
|
|
31
|
+
node: getObjMember(t.source.node, '$value') as momoa.ObjectNode,
|
|
32
|
+
filename: t.source.filename,
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
// Note: we validate sub-properties using other checks like valid-dimension, valid-font-family, etc.
|
|
36
|
+
// The only thing remaining is to check that all properties exist (since missing properties won’t appear as invalid)
|
|
37
|
+
function validateShadow(
|
|
38
|
+
value: unknown,
|
|
39
|
+
{ node, filename }: { node: momoa.ObjectNode | momoa.ArrayNode; filename?: string },
|
|
40
|
+
) {
|
|
41
|
+
const wrappedValue = Array.isArray(value) ? value : [value];
|
|
42
|
+
for (let i = 0; i < wrappedValue.length; i++) {
|
|
43
|
+
if (
|
|
44
|
+
!wrappedValue[i] ||
|
|
45
|
+
typeof wrappedValue[i] !== 'object' ||
|
|
46
|
+
!SHADOW_REQUIRED_PROPERTIES.every((property) => property in wrappedValue[i])
|
|
47
|
+
) {
|
|
48
|
+
report({ messageId: ERROR, node, filename });
|
|
49
|
+
} else {
|
|
50
|
+
for (const key of Object.keys(wrappedValue[i])) {
|
|
51
|
+
if (![...SHADOW_REQUIRED_PROPERTIES, 'inset'].includes(key)) {
|
|
52
|
+
report({
|
|
53
|
+
messageId: ERROR_INVALID_PROP,
|
|
54
|
+
data: { key: JSON.stringify(key) },
|
|
55
|
+
node: getObjMember(node.type === 'Array' ? (node.elements[i]!.value as momoa.ObjectNode) : node, key),
|
|
56
|
+
filename,
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
},
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
export default rule;
|
|
@@ -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_STRING = 'core/valid-string';
|
|
7
|
+
|
|
8
|
+
const ERROR = 'ERROR';
|
|
9
|
+
|
|
10
|
+
const rule: LintRule<typeof ERROR> = {
|
|
11
|
+
meta: {
|
|
12
|
+
messages: {
|
|
13
|
+
[ERROR]: 'Must be a string.',
|
|
14
|
+
},
|
|
15
|
+
docs: {
|
|
16
|
+
description: 'Require string tokens to follow the Terrazzo extension.',
|
|
17
|
+
url: docsLink(VALID_STRING),
|
|
18
|
+
},
|
|
19
|
+
},
|
|
20
|
+
defaultOptions: {},
|
|
21
|
+
create({ tokens, report }) {
|
|
22
|
+
for (const t of Object.values(tokens)) {
|
|
23
|
+
if (t.aliasOf || !t.originalValue || t.$type !== 'string') {
|
|
24
|
+
continue;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
validateString(t.originalValue.$value, {
|
|
28
|
+
node: getObjMember(t.source.node, '$value') as momoa.StringNode,
|
|
29
|
+
filename: t.source.filename,
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
function validateString(value: unknown, { node, filename }: { node: momoa.StringNode; filename?: string }) {
|
|
33
|
+
if (typeof value !== 'string') {
|
|
34
|
+
report({ messageId: ERROR, node, filename });
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
},
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
export default rule;
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import type * as momoa from '@humanwhocodes/momoa';
|
|
2
|
+
import { getObjMember } from '@terrazzo/json-schema-tools';
|
|
3
|
+
import {
|
|
4
|
+
isAlias,
|
|
5
|
+
STROKE_STYLE_LINE_CAP_VALUES,
|
|
6
|
+
STROKE_STYLE_OBJECT_REQUIRED_PROPERTIES,
|
|
7
|
+
STROKE_STYLE_STRING_VALUES,
|
|
8
|
+
TRANSITION_REQUIRED_PROPERTIES,
|
|
9
|
+
} from '@terrazzo/token-tools';
|
|
10
|
+
import type { LintRule } from '../../../types.js';
|
|
11
|
+
import { docsLink } from '../lib/docs.js';
|
|
12
|
+
|
|
13
|
+
export const VALID_STROKE_STYLE = 'core/valid-stroke-style';
|
|
14
|
+
|
|
15
|
+
const ERROR_STR = 'ERROR_STR';
|
|
16
|
+
const ERROR_OBJ = 'ERROR_OBJ';
|
|
17
|
+
const ERROR_LINE_CAP = 'ERROR_LINE_CAP';
|
|
18
|
+
const ERROR_INVALID_PROP = 'ERROR_INVALID_PROP';
|
|
19
|
+
|
|
20
|
+
const rule: LintRule<typeof ERROR_STR | typeof ERROR_OBJ | typeof ERROR_LINE_CAP | typeof ERROR_INVALID_PROP> = {
|
|
21
|
+
meta: {
|
|
22
|
+
messages: {
|
|
23
|
+
[ERROR_STR]: `Value most be one of ${new Intl.ListFormat(undefined, { type: 'disjunction' }).format(STROKE_STYLE_STRING_VALUES)}.`,
|
|
24
|
+
[ERROR_OBJ]: `Missing required properties: ${new Intl.ListFormat(undefined, { type: 'conjunction' }).format(TRANSITION_REQUIRED_PROPERTIES)}.`,
|
|
25
|
+
[ERROR_LINE_CAP]: `lineCap must be one of ${new Intl.ListFormat(undefined, { type: 'disjunction' }).format(STROKE_STYLE_LINE_CAP_VALUES)}.`,
|
|
26
|
+
[ERROR_INVALID_PROP]: 'Unknown property: {{ key }}.',
|
|
27
|
+
},
|
|
28
|
+
docs: {
|
|
29
|
+
description: 'Require strokeStyle tokens to follow the format.',
|
|
30
|
+
url: docsLink(VALID_STROKE_STYLE),
|
|
31
|
+
},
|
|
32
|
+
},
|
|
33
|
+
defaultOptions: {},
|
|
34
|
+
create({ tokens, report }) {
|
|
35
|
+
for (const t of Object.values(tokens)) {
|
|
36
|
+
if (t.aliasOf || !t.originalValue) {
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
switch (t.$type) {
|
|
41
|
+
case 'strokeStyle': {
|
|
42
|
+
validateStrokeStyle(t.originalValue.$value, {
|
|
43
|
+
node: getObjMember(t.source.node, '$value') as momoa.ObjectNode,
|
|
44
|
+
filename: t.source.filename,
|
|
45
|
+
});
|
|
46
|
+
break;
|
|
47
|
+
}
|
|
48
|
+
case 'border': {
|
|
49
|
+
if (t.originalValue.$value && typeof t.originalValue.$value === 'object') {
|
|
50
|
+
const $valueNode = getObjMember(t.source.node, '$value') as momoa.ObjectNode;
|
|
51
|
+
if (t.originalValue.$value.style) {
|
|
52
|
+
validateStrokeStyle(t.originalValue.$value.style, {
|
|
53
|
+
node: getObjMember($valueNode, 'style') as momoa.ObjectNode,
|
|
54
|
+
filename: t.source.filename,
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
break;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Note: we validate sub-properties using other checks like valid-dimension, valid-font-family, etc.
|
|
63
|
+
// The only thing remaining is to check that all properties exist (since missing properties won’t appear as invalid)
|
|
64
|
+
function validateStrokeStyle(
|
|
65
|
+
value: unknown,
|
|
66
|
+
{ node, filename }: { node: momoa.StringNode | momoa.ObjectNode; filename?: string },
|
|
67
|
+
) {
|
|
68
|
+
if (typeof value === 'string') {
|
|
69
|
+
if (
|
|
70
|
+
!isAlias(value) &&
|
|
71
|
+
!STROKE_STYLE_STRING_VALUES.includes(value as (typeof STROKE_STYLE_STRING_VALUES)[number])
|
|
72
|
+
) {
|
|
73
|
+
report({ messageId: ERROR_STR, node, filename });
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
} else if (value && typeof value === 'object') {
|
|
77
|
+
if (!STROKE_STYLE_OBJECT_REQUIRED_PROPERTIES.every((property) => property in value)) {
|
|
78
|
+
report({ messageId: ERROR_OBJ, node, filename });
|
|
79
|
+
}
|
|
80
|
+
if (!Array.isArray((value as any).dashArray)) {
|
|
81
|
+
report({ messageId: ERROR_OBJ, node: getObjMember(node as momoa.ObjectNode, 'dashArray'), filename });
|
|
82
|
+
}
|
|
83
|
+
if (!STROKE_STYLE_LINE_CAP_VALUES.includes((value as any).lineCap)) {
|
|
84
|
+
report({ messageId: ERROR_OBJ, node: getObjMember(node as momoa.ObjectNode, 'lineCap'), filename });
|
|
85
|
+
}
|
|
86
|
+
for (const key of Object.keys(value)) {
|
|
87
|
+
if (!['dashArray', 'lineCap'].includes(key)) {
|
|
88
|
+
report({
|
|
89
|
+
messageId: ERROR_INVALID_PROP,
|
|
90
|
+
data: { key: JSON.stringify(key) },
|
|
91
|
+
node: getObjMember(node as momoa.ObjectNode, key),
|
|
92
|
+
filename,
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
} else {
|
|
97
|
+
report({ messageId: ERROR_OBJ, node, filename });
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
},
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
export default rule;
|