@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.
- package/dist/index.d.ts +39 -6
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +578 -512
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
- package/src/build/index.ts +0 -209
- package/src/config.ts +0 -304
- package/src/index.ts +0 -95
- package/src/lib/code-frame.ts +0 -177
- package/src/lib/momoa.ts +0 -10
- package/src/lib/resolver-utils.ts +0 -35
- package/src/lint/index.ts +0 -142
- package/src/lint/plugin-core/index.ts +0 -103
- package/src/lint/plugin-core/lib/docs.ts +0 -3
- package/src/lint/plugin-core/rules/a11y-min-contrast.ts +0 -91
- package/src/lint/plugin-core/rules/a11y-min-font-size.ts +0 -66
- package/src/lint/plugin-core/rules/colorspace.ts +0 -108
- package/src/lint/plugin-core/rules/consistent-naming.ts +0 -65
- package/src/lint/plugin-core/rules/descriptions.ts +0 -43
- package/src/lint/plugin-core/rules/duplicate-values.ts +0 -85
- package/src/lint/plugin-core/rules/max-gamut.ts +0 -144
- package/src/lint/plugin-core/rules/required-children.ts +0 -106
- package/src/lint/plugin-core/rules/required-modes.ts +0 -75
- package/src/lint/plugin-core/rules/required-type.ts +0 -28
- package/src/lint/plugin-core/rules/required-typography-properties.ts +0 -65
- package/src/lint/plugin-core/rules/valid-boolean.ts +0 -41
- package/src/lint/plugin-core/rules/valid-border.ts +0 -57
- package/src/lint/plugin-core/rules/valid-color.ts +0 -265
- package/src/lint/plugin-core/rules/valid-cubic-bezier.ts +0 -83
- package/src/lint/plugin-core/rules/valid-dimension.ts +0 -199
- package/src/lint/plugin-core/rules/valid-duration.ts +0 -123
- package/src/lint/plugin-core/rules/valid-font-family.ts +0 -68
- package/src/lint/plugin-core/rules/valid-font-weight.ts +0 -89
- package/src/lint/plugin-core/rules/valid-gradient.ts +0 -79
- package/src/lint/plugin-core/rules/valid-link.ts +0 -41
- package/src/lint/plugin-core/rules/valid-number.ts +0 -63
- package/src/lint/plugin-core/rules/valid-shadow.ts +0 -67
- package/src/lint/plugin-core/rules/valid-string.ts +0 -41
- package/src/lint/plugin-core/rules/valid-stroke-style.ts +0 -104
- package/src/lint/plugin-core/rules/valid-transition.ts +0 -61
- package/src/lint/plugin-core/rules/valid-typography.ts +0 -67
- package/src/logger.ts +0 -213
- package/src/parse/index.ts +0 -124
- package/src/parse/load.ts +0 -172
- package/src/parse/normalize.ts +0 -163
- package/src/parse/process.ts +0 -251
- package/src/parse/token.ts +0 -553
- package/src/resolver/create-synthetic-resolver.ts +0 -86
- package/src/resolver/index.ts +0 -7
- package/src/resolver/load.ts +0 -215
- package/src/resolver/normalize.ts +0 -133
- package/src/resolver/validate.ts +0 -375
- package/src/types.ts +0 -468
package/src/parse/load.ts
DELETED
|
@@ -1,172 +0,0 @@
|
|
|
1
|
-
import * as momoa from '@humanwhocodes/momoa';
|
|
2
|
-
import {
|
|
3
|
-
type BundleOptions,
|
|
4
|
-
bundle,
|
|
5
|
-
encodeFragment,
|
|
6
|
-
getObjMember,
|
|
7
|
-
type InputSource,
|
|
8
|
-
type InputSourceWithDocument,
|
|
9
|
-
replaceNode,
|
|
10
|
-
traverse,
|
|
11
|
-
} from '@terrazzo/json-schema-tools';
|
|
12
|
-
import type { TokenNormalized, TokenNormalizedSet } from '@terrazzo/token-tools';
|
|
13
|
-
import { toMomoa } from '../lib/momoa.js';
|
|
14
|
-
import { filterResolverPaths } from '../lib/resolver-utils.js';
|
|
15
|
-
import type Logger from '../logger.js';
|
|
16
|
-
import { isLikelyResolver } from '../resolver/validate.js';
|
|
17
|
-
import type { ParseOptions, TransformVisitors } from '../types.js';
|
|
18
|
-
import { processTokens } from './process.js';
|
|
19
|
-
|
|
20
|
-
/** Ephemeral format that only exists while parsing the document. This is not confirmed to be DTCG yet. */
|
|
21
|
-
export interface IntermediaryToken {
|
|
22
|
-
id: string;
|
|
23
|
-
/** Was this token aliasing another? */
|
|
24
|
-
$ref?: string;
|
|
25
|
-
$type?: string;
|
|
26
|
-
$description?: string;
|
|
27
|
-
$deprecated?: string | boolean;
|
|
28
|
-
$value: unknown;
|
|
29
|
-
$extensions?: Record<string, unknown>;
|
|
30
|
-
group: TokenNormalized['group'];
|
|
31
|
-
aliasOf?: string;
|
|
32
|
-
partialAliasOf?: Record<string, any> | any[];
|
|
33
|
-
mode: Record<
|
|
34
|
-
string,
|
|
35
|
-
{
|
|
36
|
-
$type?: string;
|
|
37
|
-
$value: unknown;
|
|
38
|
-
aliasOf?: string;
|
|
39
|
-
partialAliasOf?: Record<string, any> | any[];
|
|
40
|
-
source?: { filename?: URL; node: momoa.ObjectNode };
|
|
41
|
-
}
|
|
42
|
-
>;
|
|
43
|
-
source: {
|
|
44
|
-
filename?: URL;
|
|
45
|
-
node: momoa.ObjectNode;
|
|
46
|
-
};
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
export interface LoadOptions extends Pick<ParseOptions, 'config' | 'continueOnError' | 'yamlToMomoa' | 'transform'> {
|
|
50
|
-
req: NonNullable<ParseOptions['req']>;
|
|
51
|
-
logger: Logger;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
export interface LoadSourcesResult {
|
|
55
|
-
tokens: TokenNormalizedSet;
|
|
56
|
-
sources: InputSourceWithDocument[];
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
/** Load from multiple entries, while resolving remote files */
|
|
60
|
-
export async function loadSources(
|
|
61
|
-
inputs: InputSource[],
|
|
62
|
-
{ config, logger, req, continueOnError, yamlToMomoa, transform }: LoadOptions,
|
|
63
|
-
): Promise<LoadSourcesResult> {
|
|
64
|
-
const entry = { group: 'parser' as const, label: 'init' };
|
|
65
|
-
|
|
66
|
-
// 1. Bundle root documents together
|
|
67
|
-
const firstLoad = performance.now();
|
|
68
|
-
let document = {} as momoa.DocumentNode;
|
|
69
|
-
|
|
70
|
-
/** The original user inputs, in original order, with parsed ASTs */
|
|
71
|
-
const sources = inputs.map((input, i) => ({
|
|
72
|
-
...input,
|
|
73
|
-
document: {} as momoa.DocumentNode,
|
|
74
|
-
filename: input.filename || new URL(`virtual:${i}`), // for objects created in memory, an index-based ID helps associate tokens with these
|
|
75
|
-
}));
|
|
76
|
-
/** The sources array, indexed by filename */
|
|
77
|
-
let sourceByFilename: Record<string, InputSourceWithDocument> = {};
|
|
78
|
-
try {
|
|
79
|
-
const result = await bundle(sources, {
|
|
80
|
-
req,
|
|
81
|
-
parse: transform ? transformer(transform) : undefined,
|
|
82
|
-
yamlToMomoa,
|
|
83
|
-
});
|
|
84
|
-
document = result.document;
|
|
85
|
-
sourceByFilename = result.sources;
|
|
86
|
-
for (const [filename, source] of Object.entries(result.sources)) {
|
|
87
|
-
const i = sources.findIndex((s) => s.filename.href === filename);
|
|
88
|
-
if (i === -1) {
|
|
89
|
-
sources.push(source);
|
|
90
|
-
} else {
|
|
91
|
-
sources[i]!.src = source.src; // this is a sanitized source that is easier to work with
|
|
92
|
-
sources[i]!.document = source.document;
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
} catch (err) {
|
|
96
|
-
let src = sources.find((s) => s.filename.href === (err as any).filename)?.src;
|
|
97
|
-
if (src && typeof src !== 'string') {
|
|
98
|
-
src = JSON.stringify(src, undefined, 2);
|
|
99
|
-
}
|
|
100
|
-
logger.error({
|
|
101
|
-
...entry,
|
|
102
|
-
continueOnError,
|
|
103
|
-
message: (err as Error).message,
|
|
104
|
-
node: (err as any).node,
|
|
105
|
-
src,
|
|
106
|
-
});
|
|
107
|
-
}
|
|
108
|
-
logger.debug({ ...entry, message: `JSON loaded`, timing: performance.now() - firstLoad });
|
|
109
|
-
|
|
110
|
-
const rootSource = {
|
|
111
|
-
filename: sources[0]!.filename!,
|
|
112
|
-
document,
|
|
113
|
-
src: momoa.print(document, { indent: 2 }).replace(/\\\//g, '/'),
|
|
114
|
-
};
|
|
115
|
-
|
|
116
|
-
return {
|
|
117
|
-
tokens: processTokens(rootSource, { config, logger, sources, sourceByFilename }),
|
|
118
|
-
sources,
|
|
119
|
-
};
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
function transformer(transform: TransformVisitors): BundleOptions['parse'] {
|
|
123
|
-
return async (src, filename) => {
|
|
124
|
-
let document = toMomoa(src);
|
|
125
|
-
let lastPath = '#/';
|
|
126
|
-
let last$type: string | undefined;
|
|
127
|
-
|
|
128
|
-
if (transform.root) {
|
|
129
|
-
const result = transform.root(document, { filename, parent: undefined, path: [] });
|
|
130
|
-
if (result) {
|
|
131
|
-
document = result as momoa.DocumentNode;
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
const isResolver = isLikelyResolver(document);
|
|
136
|
-
traverse(document, {
|
|
137
|
-
enter(node, parent, rawPath) {
|
|
138
|
-
const path = isResolver ? filterResolverPaths(rawPath) : rawPath;
|
|
139
|
-
if (node.type !== 'Object' || !path.length) {
|
|
140
|
-
return;
|
|
141
|
-
}
|
|
142
|
-
const ctx = { filename, parent, path };
|
|
143
|
-
const next$type = getObjMember(node, '$type');
|
|
144
|
-
if (next$type?.type === 'String') {
|
|
145
|
-
const jsonPath = encodeFragment(path);
|
|
146
|
-
if (jsonPath.startsWith(lastPath)) {
|
|
147
|
-
last$type = next$type.value;
|
|
148
|
-
}
|
|
149
|
-
lastPath = jsonPath;
|
|
150
|
-
}
|
|
151
|
-
if (getObjMember(node, '$value')) {
|
|
152
|
-
let result: any = transform.token?.(structuredClone(node), ctx);
|
|
153
|
-
if (result) {
|
|
154
|
-
replaceNode(node, result);
|
|
155
|
-
result = undefined;
|
|
156
|
-
}
|
|
157
|
-
result = transform[last$type as keyof typeof transform]?.(structuredClone(node as any), ctx);
|
|
158
|
-
if (result) {
|
|
159
|
-
replaceNode(node, result);
|
|
160
|
-
}
|
|
161
|
-
} else if (!path.includes('$value')) {
|
|
162
|
-
const result = transform.group?.(structuredClone(node), ctx);
|
|
163
|
-
if (result) {
|
|
164
|
-
replaceNode(node, result);
|
|
165
|
-
}
|
|
166
|
-
}
|
|
167
|
-
},
|
|
168
|
-
});
|
|
169
|
-
|
|
170
|
-
return document;
|
|
171
|
-
};
|
|
172
|
-
}
|
package/src/parse/normalize.ts
DELETED
|
@@ -1,163 +0,0 @@
|
|
|
1
|
-
import type * as momoa from '@humanwhocodes/momoa';
|
|
2
|
-
import { getObjMember } from '@terrazzo/json-schema-tools';
|
|
3
|
-
import { FONT_WEIGHTS, isAlias, parseColor } from '@terrazzo/token-tools';
|
|
4
|
-
import type Logger from '../logger.js';
|
|
5
|
-
|
|
6
|
-
interface PreValidatedToken {
|
|
7
|
-
id: string;
|
|
8
|
-
$type: string;
|
|
9
|
-
$value: unknown;
|
|
10
|
-
mode: Record<string, { $value: unknown; source: { node: any; filename: string | undefined } }>;
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
/**
|
|
14
|
-
* Normalize token value.
|
|
15
|
-
* The reason for the “any” typing is this aligns various user-provided inputs to the type
|
|
16
|
-
*/
|
|
17
|
-
export function normalize(token: PreValidatedToken, { logger, src }: { logger: Logger; src: string }) {
|
|
18
|
-
const entry = { group: 'parser' as const, label: 'init', src };
|
|
19
|
-
|
|
20
|
-
function normalizeFontFamily(value: unknown): string[] {
|
|
21
|
-
return typeof value === 'string' ? [value] : (value as string[]);
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
function normalizeFontWeight(value: unknown): number {
|
|
25
|
-
return (typeof value === 'string' && FONT_WEIGHTS[value as keyof typeof FONT_WEIGHTS]) || (value as number);
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
function normalizeColor(value: unknown, node: momoa.AnyNode | undefined) {
|
|
29
|
-
if (typeof value === 'string' && !isAlias(value)) {
|
|
30
|
-
logger.warn({
|
|
31
|
-
...entry,
|
|
32
|
-
node,
|
|
33
|
-
message: `${token.id}: string colors will be deprecated in a future version. Please update to object notation`,
|
|
34
|
-
});
|
|
35
|
-
try {
|
|
36
|
-
return parseColor(value);
|
|
37
|
-
} catch {
|
|
38
|
-
return { colorSpace: 'srgb', components: [0, 0, 0], alpha: 1 };
|
|
39
|
-
}
|
|
40
|
-
} else if (value && typeof value === 'object') {
|
|
41
|
-
if ((value as any).alpha === undefined) {
|
|
42
|
-
(value as any).alpha = 1;
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
return value;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
switch (token.$type) {
|
|
49
|
-
case 'color': {
|
|
50
|
-
for (const mode of Object.keys(token.mode)) {
|
|
51
|
-
token.mode[mode]!.$value = normalizeColor(token.mode[mode]!.$value, token.mode[mode]!.source.node);
|
|
52
|
-
}
|
|
53
|
-
token.$value = token.mode['.']!.$value;
|
|
54
|
-
break;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
case 'fontFamily': {
|
|
58
|
-
for (const mode of Object.keys(token.mode)) {
|
|
59
|
-
token.mode[mode]!.$value = normalizeFontFamily(token.mode[mode]!.$value);
|
|
60
|
-
}
|
|
61
|
-
token.$value = token.mode['.']!.$value;
|
|
62
|
-
break;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
case 'fontWeight': {
|
|
66
|
-
for (const mode of Object.keys(token.mode)) {
|
|
67
|
-
token.mode[mode]!.$value = normalizeFontWeight(token.mode[mode]!.$value);
|
|
68
|
-
}
|
|
69
|
-
token.$value = token.mode['.']!.$value;
|
|
70
|
-
break;
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
case 'border': {
|
|
74
|
-
for (const mode of Object.keys(token.mode)) {
|
|
75
|
-
const border = token.mode[mode]!.$value as any;
|
|
76
|
-
if (!border || typeof border !== 'object') {
|
|
77
|
-
continue;
|
|
78
|
-
}
|
|
79
|
-
if (border.color) {
|
|
80
|
-
border.color = normalizeColor(
|
|
81
|
-
border.color,
|
|
82
|
-
getObjMember(token.mode[mode]!.source.node as momoa.ObjectNode, 'color'),
|
|
83
|
-
);
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
token.$value = token.mode['.']!.$value;
|
|
87
|
-
break;
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
case 'shadow': {
|
|
91
|
-
for (const mode of Object.keys(token.mode)) {
|
|
92
|
-
// normalize to array
|
|
93
|
-
if (!Array.isArray(token.mode[mode]!.$value)) {
|
|
94
|
-
token.mode[mode]!.$value = [token.mode[mode]!.$value];
|
|
95
|
-
}
|
|
96
|
-
const $value = token.mode[mode]!.$value as any[];
|
|
97
|
-
for (let i = 0; i < $value.length; i++) {
|
|
98
|
-
const shadow = $value[i]!;
|
|
99
|
-
if (!shadow || typeof shadow !== 'object') {
|
|
100
|
-
continue;
|
|
101
|
-
}
|
|
102
|
-
const shadowNode = (
|
|
103
|
-
token.mode[mode]!.source.node.type === 'Array'
|
|
104
|
-
? token.mode[mode]!.source.node.elements[i]!.value
|
|
105
|
-
: token.mode[mode]!.source.node
|
|
106
|
-
) as momoa.ObjectNode;
|
|
107
|
-
if (shadow.color) {
|
|
108
|
-
shadow.color = normalizeColor(shadow.color, getObjMember(shadowNode, 'color'));
|
|
109
|
-
}
|
|
110
|
-
if (!('inset' in shadow)) {
|
|
111
|
-
shadow.inset = false;
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
token.$value = token.mode['.']!.$value;
|
|
116
|
-
break;
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
case 'gradient': {
|
|
120
|
-
for (const mode of Object.keys(token.mode)) {
|
|
121
|
-
if (!Array.isArray(token.mode[mode]!.$value)) {
|
|
122
|
-
continue;
|
|
123
|
-
}
|
|
124
|
-
const $value = token.mode[mode]!.$value as any[];
|
|
125
|
-
for (let i = 0; i < $value.length; i++) {
|
|
126
|
-
const stop = $value[i]!;
|
|
127
|
-
if (!stop || typeof stop !== 'object') {
|
|
128
|
-
continue;
|
|
129
|
-
}
|
|
130
|
-
const stopNode = (token.mode[mode]!.source.node as momoa.ArrayNode)?.elements?.[i]?.value as momoa.ObjectNode;
|
|
131
|
-
if (stop.color) {
|
|
132
|
-
stop.color = normalizeColor(stop.color, getObjMember(stopNode, 'color'));
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
token.$value = token.mode['.']!.$value;
|
|
137
|
-
break;
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
case 'typography': {
|
|
141
|
-
for (const mode of Object.keys(token.mode)) {
|
|
142
|
-
const $value = token.mode[mode]!.$value as any;
|
|
143
|
-
if (typeof $value !== 'object') {
|
|
144
|
-
return;
|
|
145
|
-
}
|
|
146
|
-
for (const [k, v] of Object.entries($value)) {
|
|
147
|
-
switch (k) {
|
|
148
|
-
case 'fontFamily': {
|
|
149
|
-
$value[k] = normalizeFontFamily(v);
|
|
150
|
-
break;
|
|
151
|
-
}
|
|
152
|
-
case 'fontWeight': {
|
|
153
|
-
$value[k] = normalizeFontWeight(v);
|
|
154
|
-
break;
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
|
-
token.$value = token.mode['.']!.$value;
|
|
160
|
-
break;
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
|
-
}
|
package/src/parse/process.ts
DELETED
|
@@ -1,251 +0,0 @@
|
|
|
1
|
-
import * as momoa from '@humanwhocodes/momoa';
|
|
2
|
-
import {
|
|
3
|
-
encodeFragment,
|
|
4
|
-
findNode,
|
|
5
|
-
getObjMember,
|
|
6
|
-
type InputSourceWithDocument,
|
|
7
|
-
mergeObjects,
|
|
8
|
-
parseRef,
|
|
9
|
-
replaceNode,
|
|
10
|
-
traverse,
|
|
11
|
-
} from '@terrazzo/json-schema-tools';
|
|
12
|
-
import { type GroupNormalized, isAlias, type TokenNormalizedSet } from '@terrazzo/token-tools';
|
|
13
|
-
import { filterResolverPaths } from '../lib/resolver-utils.js';
|
|
14
|
-
import type Logger from '../logger.js';
|
|
15
|
-
import { isLikelyResolver } from '../resolver/validate.js';
|
|
16
|
-
import type { ConfigInit, RefMap } from '../types.js';
|
|
17
|
-
import { normalize } from './normalize.js';
|
|
18
|
-
import {
|
|
19
|
-
aliasToGroupRef,
|
|
20
|
-
graphAliases,
|
|
21
|
-
groupFromNode,
|
|
22
|
-
refToTokenID,
|
|
23
|
-
resolveAliases,
|
|
24
|
-
tokenFromNode,
|
|
25
|
-
tokenRawValuesFromNode,
|
|
26
|
-
} from './token.js';
|
|
27
|
-
|
|
28
|
-
export interface ProcessTokensOptions {
|
|
29
|
-
config: ConfigInit;
|
|
30
|
-
logger: Logger;
|
|
31
|
-
sourceByFilename: Record<string, InputSourceWithDocument>;
|
|
32
|
-
sources: InputSourceWithDocument[];
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
export function processTokens(
|
|
36
|
-
rootSource: InputSourceWithDocument,
|
|
37
|
-
{ config, logger, sourceByFilename }: ProcessTokensOptions,
|
|
38
|
-
): TokenNormalizedSet {
|
|
39
|
-
const entry = { group: 'parser' as const, label: 'init' };
|
|
40
|
-
|
|
41
|
-
// 1. Inline $refs to discover any additional tokens
|
|
42
|
-
const refMap: RefMap = {};
|
|
43
|
-
function resolveRef(node: momoa.StringNode, chain: string[]): momoa.AnyNode {
|
|
44
|
-
const { subpath } = parseRef(node.value);
|
|
45
|
-
if (!subpath) {
|
|
46
|
-
logger.error({ ...entry, message: 'Can’t resolve $ref', node, src: rootSource.src });
|
|
47
|
-
// exit
|
|
48
|
-
}
|
|
49
|
-
const next = findNode(rootSource.document, subpath);
|
|
50
|
-
if (next?.type === 'Object') {
|
|
51
|
-
const next$ref = getObjMember(next, '$ref');
|
|
52
|
-
if (next$ref && next$ref.type === 'String') {
|
|
53
|
-
if (chain.includes(next$ref.value)) {
|
|
54
|
-
logger.error({
|
|
55
|
-
...entry,
|
|
56
|
-
message: `Circular $ref detected: ${JSON.stringify(next$ref.value)}`,
|
|
57
|
-
node: next$ref,
|
|
58
|
-
src: rootSource.src,
|
|
59
|
-
});
|
|
60
|
-
}
|
|
61
|
-
chain.push(next$ref.value);
|
|
62
|
-
return resolveRef(next$ref, chain);
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
return next;
|
|
66
|
-
}
|
|
67
|
-
const inlineStart = performance.now();
|
|
68
|
-
traverse(rootSource.document, {
|
|
69
|
-
enter(node, _parent, rawPath) {
|
|
70
|
-
if (rawPath.includes('$extensions') || node.type !== 'Object') {
|
|
71
|
-
return;
|
|
72
|
-
}
|
|
73
|
-
const $ref = node.type === 'Object' ? (getObjMember(node, '$ref') as momoa.StringNode) : undefined;
|
|
74
|
-
if (!$ref) {
|
|
75
|
-
return;
|
|
76
|
-
}
|
|
77
|
-
if ($ref.type !== 'String') {
|
|
78
|
-
logger.error({ ...entry, message: 'Invalid $ref. Expected string.', node: $ref, src: rootSource.src });
|
|
79
|
-
}
|
|
80
|
-
const jsonID = encodeFragment(rawPath);
|
|
81
|
-
refMap[jsonID] = { filename: rootSource.filename.href, refChain: [$ref.value] };
|
|
82
|
-
const resolved = resolveRef($ref, refMap[jsonID]!.refChain);
|
|
83
|
-
if (resolved.type === 'Object') {
|
|
84
|
-
node.members.splice(
|
|
85
|
-
node.members.findIndex((m) => m.name.type === 'String' && m.name.value === '$ref'),
|
|
86
|
-
1,
|
|
87
|
-
);
|
|
88
|
-
replaceNode(node, mergeObjects(resolved, node));
|
|
89
|
-
} else {
|
|
90
|
-
replaceNode(node, resolved);
|
|
91
|
-
}
|
|
92
|
-
},
|
|
93
|
-
});
|
|
94
|
-
logger.debug({ ...entry, message: 'Inline aliases', timing: performance.now() - inlineStart });
|
|
95
|
-
|
|
96
|
-
// 2. Resolve $extends to discover any more additional tokens
|
|
97
|
-
function flatten$extends(node: momoa.ObjectNode, chain: string[]) {
|
|
98
|
-
const memberKeys = node.members.map((m) => m.name.type === 'String' && m.name.value).filter(Boolean) as string[];
|
|
99
|
-
|
|
100
|
-
let extended: momoa.ObjectNode | undefined;
|
|
101
|
-
|
|
102
|
-
if (memberKeys.includes('$extends')) {
|
|
103
|
-
const $extends = getObjMember(node, '$extends') as momoa.StringNode;
|
|
104
|
-
if ($extends.type !== 'String') {
|
|
105
|
-
logger.error({ ...entry, message: '$extends must be a string', node: $extends, src: rootSource.src });
|
|
106
|
-
}
|
|
107
|
-
if (memberKeys.includes('$value')) {
|
|
108
|
-
logger.error({ ...entry, message: '$extends can’t exist within a token', node: $extends, src: rootSource.src });
|
|
109
|
-
}
|
|
110
|
-
const next = isAlias($extends.value) ? aliasToGroupRef($extends.value) : undefined;
|
|
111
|
-
if (!next) {
|
|
112
|
-
logger.error({ ...entry, message: '$extends must be a valid alias', node: $extends, src: rootSource.src });
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
if (
|
|
116
|
-
chain.includes(next!.$ref) ||
|
|
117
|
-
// Check that $extends is not importing from higher up (could go in either direction, which is why we check both ways)
|
|
118
|
-
chain.some((value) => value.startsWith(next!.$ref) || next!.$ref.startsWith(value))
|
|
119
|
-
) {
|
|
120
|
-
logger.error({ ...entry, message: 'Circular $extends detected', node: $extends, src: rootSource.src });
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
chain.push(next!.$ref);
|
|
124
|
-
extended = findNode(rootSource.document, parseRef(next!.$ref).subpath ?? []);
|
|
125
|
-
if (!extended) {
|
|
126
|
-
logger.error({ ...entry, message: 'Could not resolve $extends', node: $extends, src: rootSource.src });
|
|
127
|
-
}
|
|
128
|
-
if (extended!.type !== 'Object') {
|
|
129
|
-
logger.error({ ...entry, message: '$extends must resolve to a group of tokens', node });
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
// To ensure this is resolvable, try and flatten this node first (will catch circular refs)
|
|
133
|
-
flatten$extends(extended!, chain);
|
|
134
|
-
|
|
135
|
-
replaceNode(node, mergeObjects(extended!, node));
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
// Deeply-traverse for any interior $extends (even if it wasn’t at the top level)
|
|
139
|
-
for (const member of node.members) {
|
|
140
|
-
if (
|
|
141
|
-
member.value.type === 'Object' &&
|
|
142
|
-
member.name.type === 'String' &&
|
|
143
|
-
!['$value', '$extensions'].includes(member.name.value)
|
|
144
|
-
) {
|
|
145
|
-
traverse(member.value, {
|
|
146
|
-
enter(subnode, _parent) {
|
|
147
|
-
if (subnode.type === 'Object') {
|
|
148
|
-
flatten$extends(subnode, chain);
|
|
149
|
-
}
|
|
150
|
-
},
|
|
151
|
-
});
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
const extendsStart = performance.now();
|
|
157
|
-
const extendsChain: string[] = [];
|
|
158
|
-
flatten$extends(rootSource.document.body as momoa.ObjectNode, extendsChain);
|
|
159
|
-
logger.debug({ ...entry, message: 'Resolving $extends', timing: performance.now() - extendsStart });
|
|
160
|
-
|
|
161
|
-
// 3. Parse discovered tokens
|
|
162
|
-
const firstPass = performance.now();
|
|
163
|
-
const tokens: TokenNormalizedSet = {};
|
|
164
|
-
// micro-optimization: while we’re iterating over tokens, keeping a “hot”
|
|
165
|
-
// array in memory saves recreating arrays from object keys over and over again.
|
|
166
|
-
// it does produce a noticeable speedup > 1,000 tokens.
|
|
167
|
-
const tokenIDs: string[] = [];
|
|
168
|
-
const groups: Record<string, GroupNormalized> = {};
|
|
169
|
-
|
|
170
|
-
// 3a. Token & group population
|
|
171
|
-
const isResolver = isLikelyResolver(rootSource.document);
|
|
172
|
-
traverse(rootSource.document, {
|
|
173
|
-
enter(node, _parent, rawPath) {
|
|
174
|
-
if (node.type !== 'Object') {
|
|
175
|
-
return;
|
|
176
|
-
}
|
|
177
|
-
groupFromNode(node, { path: isResolver ? filterResolverPaths(rawPath) : rawPath, groups });
|
|
178
|
-
const token = tokenFromNode(node, {
|
|
179
|
-
groups,
|
|
180
|
-
ignore: config.ignore,
|
|
181
|
-
path: isResolver ? filterResolverPaths(rawPath) : rawPath,
|
|
182
|
-
source: rootSource,
|
|
183
|
-
});
|
|
184
|
-
if (token) {
|
|
185
|
-
tokenIDs.push(token.jsonID);
|
|
186
|
-
tokens[token.jsonID] = token;
|
|
187
|
-
}
|
|
188
|
-
},
|
|
189
|
-
});
|
|
190
|
-
|
|
191
|
-
logger.debug({ ...entry, message: 'Parsing: 1st pass', timing: performance.now() - firstPass });
|
|
192
|
-
const secondPass = performance.now();
|
|
193
|
-
|
|
194
|
-
// 3b. Resolve originalValue and original sources
|
|
195
|
-
for (const source of Object.values(sourceByFilename)) {
|
|
196
|
-
traverse(source.document, {
|
|
197
|
-
enter(node, _parent, path) {
|
|
198
|
-
if (node.type !== 'Object') {
|
|
199
|
-
return;
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
const tokenRawValues = tokenRawValuesFromNode(node, { filename: source.filename!.href, path });
|
|
203
|
-
if (tokenRawValues && tokens[tokenRawValues?.jsonID]) {
|
|
204
|
-
tokens[tokenRawValues.jsonID]!.originalValue = tokenRawValues.originalValue;
|
|
205
|
-
tokens[tokenRawValues.jsonID]!.source = tokenRawValues.source;
|
|
206
|
-
for (const mode of Object.keys(tokenRawValues.mode)) {
|
|
207
|
-
tokens[tokenRawValues.jsonID]!.mode[mode]!.originalValue = tokenRawValues.mode[mode]!.originalValue;
|
|
208
|
-
tokens[tokenRawValues.jsonID]!.mode[mode]!.source = tokenRawValues.mode[mode]!.source;
|
|
209
|
-
}
|
|
210
|
-
}
|
|
211
|
-
},
|
|
212
|
-
});
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
// 3c. DTCG alias resolution
|
|
216
|
-
// Unlike $refs which can be resolved as we go, these can’t happen until the final, flattened set
|
|
217
|
-
resolveAliases(tokens, { logger, sources: sourceByFilename, refMap });
|
|
218
|
-
logger.debug({ ...entry, message: 'Parsing: 2nd pass', timing: performance.now() - secondPass });
|
|
219
|
-
|
|
220
|
-
// 4. Alias graph
|
|
221
|
-
// We’ve resolved aliases, but we need this pass for reverse linking i.e. “aliasedBy”
|
|
222
|
-
const aliasStart = performance.now();
|
|
223
|
-
graphAliases(refMap, { tokens, logger, sources: sourceByFilename });
|
|
224
|
-
logger.debug({ ...entry, message: 'Alias graph built', timing: performance.now() - aliasStart });
|
|
225
|
-
|
|
226
|
-
// 5. normalize
|
|
227
|
-
// Allow for some minor variance in inputs, and be nice to folks.
|
|
228
|
-
const normalizeStart = performance.now();
|
|
229
|
-
for (const id of tokenIDs) {
|
|
230
|
-
const token = tokens[id]!;
|
|
231
|
-
normalize(token as any, { logger, src: sourceByFilename[token.source.filename!]?.src });
|
|
232
|
-
}
|
|
233
|
-
logger.debug({ ...entry, message: 'Normalized values', timing: performance.now() - normalizeStart });
|
|
234
|
-
|
|
235
|
-
// 6. alphabetize & filter
|
|
236
|
-
// This can’t happen until the last step, where we’re 100% sure we’ve resolved everything.
|
|
237
|
-
const sortStart = performance.now();
|
|
238
|
-
const tokensSorted: TokenNormalizedSet = {};
|
|
239
|
-
tokenIDs.sort((a, b) => a.localeCompare(b, 'en-us', { numeric: true }));
|
|
240
|
-
for (const path of tokenIDs) {
|
|
241
|
-
const id = refToTokenID(path)!;
|
|
242
|
-
tokensSorted[id] = tokens[path]!;
|
|
243
|
-
}
|
|
244
|
-
// Sort group IDs once, too
|
|
245
|
-
for (const group of Object.values(groups)) {
|
|
246
|
-
group.tokens.sort((a, b) => a.localeCompare(b, 'en-us', { numeric: true }));
|
|
247
|
-
}
|
|
248
|
-
logger.debug({ ...entry, message: 'Sorted tokens', timing: performance.now() - sortStart });
|
|
249
|
-
|
|
250
|
-
return tokensSorted;
|
|
251
|
-
}
|