Skip to content

Commit 3d1caec

Browse files
committed
checkpoint
1 parent e83c4d0 commit 3d1caec

3 files changed

Lines changed: 58 additions & 65 deletions

File tree

src/loader/om.ts

Lines changed: 40 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import type { Format } from "../types/format.js";
22
import type { Resolver } from "../types/resolver.js";
3+
import type { LoaderSys, LoadOptions } from "./index.js";
34
import { getAtPath, parsePointer } from "./pointer.js";
4-
import type { LoadOptions, LoaderSys } from "./index.js";
55

66
/**
77
* Experimental: a Typed-OM-like representation of DTCG tokens.
@@ -81,7 +81,7 @@ export type EvalContext = {
8181
doc: GroupNode;
8282
memo: WeakMap<object, ValueNode>;
8383
resolving: Set<object>;
84-
aliasMemo: WeakMap<object, string[] | ErrorNode>;
84+
aliasMemo: WeakMap<object, string[]>;
8585
pointerMemo: WeakMap<object, string[] | ErrorNode>;
8686
};
8787

@@ -98,15 +98,15 @@ export const createEvalContext = (doc: GroupNode): EvalContext => ({
9898
export type PathLike = string | readonly string[];
9999

100100
/** Splits a dot-path (`"a.b.c"`) into segments (or returns an existing segment array). */
101-
export const toDotPath = (path: PathLike): string[] => (typeof path === "string" ? (path ? path.split(".") : []) : [...path]);
101+
export const toDotPath = (path: PathLike): readonly string[] => (typeof path === "string" ? (path ? path.split(".") : []) : path);
102102

103103
/** Returns the entry node (group or token) at a dot-path. */
104104
export const getEntry = (doc: GroupNode, path: PathLike): EntryNode | undefined => {
105105
const segs = toDotPath(path);
106106
let cur: EntryNode = doc;
107107
for (const seg of segs) {
108108
if (cur.t !== T.Group) return undefined;
109-
const next: EntryNode | undefined = (cur as GroupNode).entries[seg];
109+
const next: EntryNode | undefined = cur.entries[seg];
110110
if (!next) return undefined;
111111
cur = next;
112112
}
@@ -127,8 +127,7 @@ export const getToken = (doc: GroupNode, path: PathLike): TokenNode | undefined
127127

128128
/** Returns any node reachable by a local JSON Pointer (string or segment array). */
129129
export const getNodeByPointer = (doc: GroupNode, pointer: string | readonly string[]): EntryNode | ValueNode | undefined => {
130-
const segs = typeof pointer === "string" ? parsePointer(pointer) : [...pointer];
131-
return getByPointer(doc, segs);
130+
return getByPointer(doc, typeof pointer === "string" ? parsePointer(pointer) : pointer);
132131
};
133132

134133
/** Returns a value node by pointer; if the pointer lands on a token, returns its `$value` node. */
@@ -197,8 +196,8 @@ export class OMLoaderHost {
197196
if (set == null) continue;
198197

199198
for (const source of set.sources) {
200-
if (isPlainObject(source) && typeof (source as any).$ref === "string") {
201-
const url = new URL((source as any).$ref as string, resolverBase);
199+
if (hasStringRef(source)) {
200+
const url = new URL(source.$ref, resolverBase);
202201
sources.push(url);
203202
formats.push(this.readJSON<Format>(url));
204203
} else {
@@ -223,6 +222,7 @@ type RawObject = Record<string, unknown>;
223222

224223
const isPlainObject = (v: unknown): v is RawObject => v !== null && typeof v === "object" && !Array.isArray(v);
225224
const isAlias = (v: unknown): v is string => typeof v === "string" && v.startsWith("{") && v.endsWith("}");
225+
const hasStringRef = (v: unknown): v is { $ref: string } => isPlainObject(v) && typeof v.$ref === "string";
226226
const isPointerRefObject = (v: unknown): v is { $ref: string } => isPlainObject(v) && typeof v.$ref === "string" && Object.keys(v).length === 1;
227227

228228
/** Collects sub-objects for `key` across formats, resetting on non-object override. */
@@ -246,7 +246,7 @@ const getFormatsAtPath = (formats: readonly RawObject[], path: readonly string[]
246246
if (current.length === 0) return [];
247247
}
248248
// Stop: a token cannot be extended as a group.
249-
return current.some((f) => "$value" in f || typeof (f as any).$ref === "string") ? [] : current;
249+
return current.some((f) => "$value" in f || typeof f.$ref === "string") ? [] : current;
250250
};
251251

252252
const parseReferencePath = (ref: string): string[] | undefined => {
@@ -268,7 +268,7 @@ const expandFormats = (formats: readonly RawObject[], rootFormats: readonly RawO
268268
try {
269269
const out: RawObject[] = [];
270270
for (const format of formats) {
271-
const ref = typeof format.$extends === "string" ? (format.$extends as string) : undefined;
271+
const ref = typeof format.$extends === "string" ? format.$extends : undefined;
272272
if (ref) {
273273
const targetPath = parseReferencePath(ref);
274274
if (targetPath) {
@@ -295,11 +295,11 @@ const buildEntry = (formats: RawObject[], rootFormats: RawObject[], inheritedTyp
295295

296296
let nodeType: string | undefined;
297297
for (const fmt of effectiveFormats) {
298-
if (typeof (fmt as any).$type === "string") nodeType = (fmt as any).$type as string;
298+
if (typeof fmt.$type === "string") nodeType = fmt.$type;
299299
}
300300
nodeType ??= inheritedType;
301301

302-
const isToken = effectiveFormats.some((f) => "$value" in f || typeof (f as any).$ref === "string");
302+
const isToken = effectiveFormats.some((f) => "$value" in f || typeof f.$ref === "string");
303303
if (isToken) {
304304
const rawValue = getEffectiveTokenValue(effectiveFormats);
305305
const token: TokenNode = { t: T.Token, value: parseTokenValue(nodeType, rawValue) };
@@ -331,16 +331,17 @@ const getEffectiveTokenValue = (effectiveFormats: readonly RawObject[]): unknown
331331
let hasLeaf = false;
332332

333333
for (const fmt of effectiveFormats) {
334-
const ref = (fmt as any).$ref;
335-
if (typeof ref === "string") {
334+
if (typeof fmt.$ref === "string") {
336335
valueObjs.length = 0;
337-
leaf = { $ref: ref };
336+
leaf = { $ref: fmt.$ref };
338337
hasLeaf = true;
339338
}
340339

341340
if ("$value" in fmt) {
342-
const v = (fmt as any).$value as unknown;
341+
const v = fmt.$value;
343342
if (isPlainObject(v)) {
343+
hasLeaf = false;
344+
leaf = undefined;
344345
valueObjs.push(v);
345346
} else {
346347
valueObjs.length = 0;
@@ -417,24 +418,19 @@ export const parseValue = (raw: unknown): ValueNode => {
417418
// ─── Evaluation ───────────────────────────────────────────────────────────────
418419

419420
export const compute = (v: ValueNode, ctx: EvalContext): ValueNode => {
420-
const key = v as unknown as object;
421-
const cached = ctx.memo.get(key);
421+
const cached = ctx.memo.get(v);
422422
if (cached) return cached;
423423

424-
if (ctx.resolving.has(key)) return { t: T.Error, message: "circular reference" };
425-
ctx.resolving.add(key);
424+
if (ctx.resolving.has(v)) return { t: T.Error, message: "circular reference" };
425+
ctx.resolving.add(v);
426426

427427
try {
428428
let out: ValueNode = v;
429429

430430
if (v.t === T.AliasRef) {
431431
const segs = aliasSegments(v, ctx);
432-
if (isError(segs)) {
433-
out = segs;
434-
} else {
435-
const tok = getTokenByPath(ctx.doc, segs);
436-
out = tok ? compute(tok.value, ctx) : { t: T.Error, message: "alias target is not a token" };
437-
}
432+
const tok = getToken(ctx.doc, segs);
433+
out = tok ? compute(tok.value, ctx) : { t: T.Error, message: "alias target is not a token" };
438434
} else if (v.t === T.PointerRef) {
439435
const segs = pointerSegments(v, ctx);
440436
if (isError(segs)) {
@@ -450,10 +446,10 @@ export const compute = (v: ValueNode, ctx: EvalContext): ValueNode => {
450446
}
451447
}
452448

453-
ctx.memo.set(key, out);
449+
ctx.memo.set(v, out);
454450
return out;
455451
} finally {
456-
ctx.resolving.delete(key);
452+
ctx.resolving.delete(v);
457453
}
458454
};
459455

@@ -502,7 +498,7 @@ export const toJSONComputed = (v: ValueNode, ctx: EvalContext): unknown => {
502498
case T.Bool:
503499
case T.Num:
504500
case T.Str:
505-
return (c as any).v;
501+
return c.v;
506502
case T.Arr:
507503
return c.items.map((it) => toJSONComputed(it, ctx));
508504
case T.Obj: {
@@ -525,46 +521,33 @@ export const toJSONComputed = (v: ValueNode, ctx: EvalContext): unknown => {
525521

526522
// ─── Reference lookup ─────────────────────────────────────────────────────────
527523

528-
const isError = (v: unknown): v is ErrorNode => typeof v === "object" && v !== null && (v as any).t === T.Error;
524+
const isError = (v: unknown): v is ErrorNode => typeof v === "object" && v !== null && (v as ErrorNode).t === T.Error;
529525
const isValueNode = (n: EntryNode | ValueNode): n is ValueNode => n.t !== T.Group && n.t !== T.Token;
530526

531-
const aliasSegments = (n: AliasRefNode, ctx: EvalContext): string[] | ErrorNode => {
532-
const cached = ctx.aliasMemo.get(n as unknown as object);
527+
/** AliasRefNode is only created for valid aliases, so parsing always succeeds. */
528+
const aliasSegments = (n: AliasRefNode, ctx: EvalContext): string[] => {
529+
const cached = ctx.aliasMemo.get(n);
533530
if (cached) return cached;
534-
535-
const raw = n.raw;
536-
const segs = isAlias(raw) ? raw.slice(1, -1).split(".") : undefined;
537-
const out: string[] | ErrorNode = segs ?? ({ t: T.Error, message: "invalid alias" } as ErrorNode);
538-
ctx.aliasMemo.set(n as unknown as object, out);
539-
return out;
531+
const segs = n.raw.slice(1, -1).split(".");
532+
ctx.aliasMemo.set(n, segs);
533+
return segs;
540534
};
541535

542536
const pointerSegments = (n: PointerRefNode, ctx: EvalContext): string[] | ErrorNode => {
543-
const cached = ctx.pointerMemo.get(n as unknown as object);
537+
const cached = ctx.pointerMemo.get(n);
544538
if (cached) return cached;
545539

546540
try {
547541
const segs = parsePointer(n.$ref);
548-
ctx.pointerMemo.set(n as unknown as object, segs);
542+
ctx.pointerMemo.set(n, segs);
549543
return segs;
550544
} catch {
551545
const err: ErrorNode = { t: T.Error, message: "invalid JSON Pointer" };
552-
ctx.pointerMemo.set(n as unknown as object, err);
546+
ctx.pointerMemo.set(n, err);
553547
return err;
554548
}
555549
};
556550

557-
const getTokenByPath = (doc: GroupNode, path: readonly string[]): TokenNode | undefined => {
558-
let cur: EntryNode = doc;
559-
for (const seg of path) {
560-
if (cur.t !== T.Group) return undefined;
561-
const next: EntryNode | undefined = cur.entries[seg];
562-
if (!next) return undefined;
563-
cur = next;
564-
}
565-
return cur.t === T.Token ? cur : undefined;
566-
};
567-
568551
const getByPointer = (doc: GroupNode, ptr: readonly string[]): EntryNode | ValueNode | undefined => {
569552
let cur: EntryNode | ValueNode = doc;
570553

@@ -588,15 +571,15 @@ const getByPointer = (doc: GroupNode, ptr: readonly string[]): EntryNode | Value
588571
return undefined;
589572
}
590573

591-
// value node traversal
592-
cur = getValueChild(cur, seg) ?? (undefined as any);
593-
if (!cur) return undefined;
574+
const next = getValueChild(cur, seg);
575+
if (!next) return undefined;
576+
cur = next;
594577
}
595578

596579
return cur;
597580
};
598581

599-
const getValueChild = (v: ValueNode, seg: string): EntryNode | ValueNode | undefined => {
582+
const getValueChild = (v: ValueNode, seg: string): ValueNode | undefined => {
600583
switch (v.t) {
601584
case T.Obj:
602585
return v.props[seg];
@@ -625,7 +608,7 @@ const resolveSet = (item: Resolver["resolutionOrder"][number], resolver: Resolve
625608
return (item as SetLike).type === "set" ? (item as any) : null;
626609
};
627610

628-
const isSet = (v: unknown): v is { sources: unknown[] } => isPlainObject(v) && Array.isArray((v as any).sources);
611+
const isSet = (v: unknown): v is { sources: unknown[] } => isPlainObject(v) && Array.isArray(v.sources);
629612

630613
const toBaseURL = (base: URL | string | undefined, fallback: URL): URL => {
631614
if (base == null) return fallback;

src/scratch.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
1-
import { load } from "./loader/node.js";
2-
import { OMLoaderHost, T, getToken, toJSONComputed } from "./loader/om.js";
3-
import { nodeSys } from "./loader/node.js";
1+
import { load, nodeSys } from "./loader/node.js";
2+
import { getToken, OMLoaderHost, T, toJSONComputed } from "./loader/om.js";
43

54
const resolverURL = new URL("../src/test/example/design-tokens.resolver.json", import.meta.url);
65

test/om.test.ts

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,22 @@
11
import { describe, expect, it } from "vitest";
22

3-
import { typedBase, typedColor, typedFocusRing } from "../src/test/example.ts";
3+
import { nodeSys } from "../src/loader/node.ts";
44

55
import {
6-
OMLoaderHost,
7-
T,
86
asCubicBezier,
97
asDuration,
108
buildOM,
119
createEvalContext,
1210
getToken,
1311
getValueByPointer,
14-
toJSONComputed,
1512
type ObjNode,
13+
OMLoaderHost,
1614
parseValue,
15+
T,
16+
toJSONComputed,
1717
} from "../src/loader/om.ts";
1818

19-
import { nodeSys } from "../src/loader/node.ts";
19+
import { typedBase, typedColor, typedFocusRing } from "../src/test/example.ts";
2020

2121
describe("loader/om", () => {
2222
it("resolves alias + JSON Pointer refs inside composite values", () => {
@@ -78,6 +78,17 @@ describe("loader/om", () => {
7878
expect(asDuration(slow.value, ctx)).toEqual({ value: 120, unit: "ms" });
7979
});
8080

81+
it("lets a later object-valued token override an earlier leaf-valued token", () => {
82+
const doc = buildOM([
83+
{ transition: { $type: "duration", fast: { $value: "{transition.slow}" }, slow: { $value: { value: 120, unit: "ms" } } } } as any,
84+
{ transition: { fast: { $value: { value: 80, unit: "ms" } } } } as any,
85+
]);
86+
const ctx = createEvalContext(doc);
87+
const fast = getToken(doc, "transition.fast");
88+
if (!fast) throw new Error("missing token");
89+
expect(asDuration(fast.value, ctx)).toEqual({ value: 80, unit: "ms" });
90+
});
91+
8192
it("can load from a resolver via OMLoaderHost", () => {
8293
const host = new OMLoaderHost(nodeSys);
8394
const resolverURL = new URL("../src/test/example/design-tokens.resolver.json", import.meta.url);

0 commit comments

Comments
 (0)