@@ -3,93 +3,96 @@ import { parsePointer } from "./pointer.js";
33
44// ─── Public API ───────────────────────────────────────────────────────────────
55
6- /** Merges ordered formats into a single lazily resolved token tree. */
6+ /** Merges ordered DTCG formats into a single lazily- resolved token tree. */
77export const mergeFormats = ( formats : Format [ ] ) : Format =>
88 buildNode ( formats as RawObject [ ] , formats as RawObject [ ] , undefined , undefined , [ ] ) as unknown as Format ;
99
1010// ─── Internal ─────────────────────────────────────────────────────────────────
1111
1212type RawObject = Record < string , unknown > ;
1313
14- /** Returns `true` when a value is a plain object. */
14+ /** Returns `true` when `v` is a non-array object. */
1515const isPlainObject = ( v : unknown ) : v is RawObject => v !== null && typeof v === "object" && ! Array . isArray ( v ) ;
16- /** Returns `true` when a value is a DTCG alias string. */
16+
17+ /** Returns `true` when `v` is a DTCG alias string (e.g. `"{color.blue}"`). */
1718const isAlias = ( v : unknown ) : v is string => typeof v === "string" && v . startsWith ( "{" ) && v . endsWith ( "}" ) ;
18- /** Returns `true` when a value is a JSON reference object. */
19+
20+ /** Returns `true` when `v` is a JSON reference object (e.g. `{ "$ref": "#/…" }`). */
1921const isRef = ( v : unknown ) : v is { $ref : string } => isPlainObject ( v ) && typeof v . $ref === "string" ;
20- /** Converts a path array into a stable cycle-detection key. */
22+
23+ /** Joins a path into a dot-separated key for cycle detection. */
2124const pathToId = ( path : readonly string [ ] ) : string => path . join ( "." ) || "#" ;
2225
23- /** Collects nested object values for a key across merged formats. */
26+ /** Collects the sub-objects for ` key` across ` formats`, resetting on non-object override . */
2427const getSubFormats = ( formats : readonly RawObject [ ] , key : string ) : RawObject [ ] => {
25- const subFormats : RawObject [ ] = [ ] ;
28+ const subs : RawObject [ ] = [ ] ;
2629
2730 for ( const format of formats ) {
28- const value = format [ key ] ;
29- if ( value === undefined ) continue ;
30- if ( isPlainObject ( value ) ) {
31- subFormats . push ( value ) ;
31+ const v = format [ key ] ;
32+ if ( v === undefined ) continue ;
33+ if ( isPlainObject ( v ) ) {
34+ subs . push ( v ) ;
3235 } else {
33- subFormats . length = 0 ;
36+ subs . length = 0 ;
3437 }
3538 }
3639
37- return subFormats ;
40+ return subs ;
3841} ;
3942
40- /** Resolves all format objects that exist at a nested path . */
41- const getFormatsAtPath = ( formats : readonly RawObject [ ] , path : readonly string [ ] ) : RawObject [ ] => {
42- let currentFormats = formats as RawObject [ ] ;
43+ /** Walks `path` through `formats` and returns the group-level sub-objects found there . */
44+ const getFormatsAtPath = ( formats : readonly RawObject [ ] , path : readonly string [ ] ) : readonly RawObject [ ] => {
45+ let current : readonly RawObject [ ] = formats ;
4346
4447 for ( const segment of path ) {
45- currentFormats = getSubFormats ( currentFormats , segment ) ;
46- if ( currentFormats . length === 0 ) return [ ] ;
48+ current = getSubFormats ( current , segment ) ;
49+ if ( current . length === 0 ) return [ ] ;
4750 }
4851
49- return currentFormats . some ( ( format ) => "$value" in format ) ? [ ] : currentFormats ;
52+ return current . some ( ( f ) => "$value" in f ) ? [ ] : current ;
5053} ;
5154
52- /** Parses either an alias path or JSON Pointer path into segments. */
53- const parseReferencePath = ( reference : string ) : string [ ] | undefined => {
54- if ( isAlias ( reference ) ) return reference . slice ( 1 , - 1 ) . split ( "." ) ;
55+ /** Parses an alias string or JSON Pointer into path segments, or `undefined` on failure . */
56+ const parseReferencePath = ( ref : string ) : string [ ] | undefined => {
57+ if ( isAlias ( ref ) ) return ref . slice ( 1 , - 1 ) . split ( "." ) ;
5558 try {
56- return parsePointer ( reference ) ;
59+ return parsePointer ( ref ) ;
5760 } catch {
5861 return undefined ;
5962 }
6063} ;
6164
65+ // Module-level cycle-detection set for $extends. Safe because JS is
66+ // single-threaded; add before recursing, delete in the `finally` block.
6267const resolvingExtends = new Set < string > ( ) ;
6368
64- /** Expands `$extends` references before a node is merged . */
69+ /** Prepends inherited formats from `$extends` targets before each local format . */
6570const expandFormats = ( formats : readonly RawObject [ ] , rootFormats : readonly RawObject [ ] , path : readonly string [ ] ) : RawObject [ ] => {
66- const pathId = pathToId ( path ) ;
67- if ( resolvingExtends . has ( pathId ) ) return [ ] ;
71+ const id = pathToId ( path ) ;
72+ if ( resolvingExtends . has ( id ) ) return [ ] ;
6873
69- resolvingExtends . add ( pathId ) ;
74+ resolvingExtends . add ( id ) ;
7075 try {
71- const expandedFormats : RawObject [ ] = [ ] ;
76+ const out : RawObject [ ] = [ ] ;
7277
7378 for ( const format of formats ) {
74- const reference = typeof format . $extends === "string" ? format . $extends : undefined ;
75- if ( reference ) {
76- const targetPath = parseReferencePath ( reference ) ;
79+ const ref = typeof format . $extends === "string" ? format . $extends : undefined ;
80+ if ( ref ) {
81+ const targetPath = parseReferencePath ( ref ) ;
7782 if ( targetPath ) {
78- const targetFormats = getFormatsAtPath ( rootFormats , targetPath ) ;
79- expandedFormats . push ( ...expandFormats ( targetFormats , rootFormats , targetPath ) ) ;
83+ out . push ( ...expandFormats ( getFormatsAtPath ( rootFormats , targetPath ) , rootFormats , targetPath ) ) ;
8084 }
8185 }
82-
83- expandedFormats . push ( format ) ;
86+ out . push ( format ) ;
8487 }
8588
86- return expandedFormats ;
89+ return out ;
8790 } finally {
88- resolvingExtends . delete ( pathId ) ;
91+ resolvingExtends . delete ( id ) ;
8992 }
9093} ;
9194
92- /** Builds a merged lazy node for a specific format path. */
95+ /** Builds a merged lazy node for the formats at ` path` . */
9396const buildNode = (
9497 formats : RawObject [ ] ,
9598 rootFormats : RawObject [ ] ,
@@ -98,94 +101,77 @@ const buildNode = (
98101 path : string [ ] ,
99102) : RawObject => {
100103 const node = Object . create ( null ) as RawObject ;
101- // At the top level the node itself IS the resolution root; recursive calls
102- // receive the already-established root so all aliases resolve to the same tree.
103- const effectiveRoot = root ?? node ;
104+ const effectiveRoot = root ?? node ; // top-level call: the node IS the root
104105 const effectiveFormats = expandFormats ( formats , rootFormats , path ) ;
105106
106- // Collect every key that appears across all sources.
107+ // Collect every key across all sources.
107108 const keys = new Set < string > ( ) ;
108109 for ( const format of effectiveFormats ) {
109110 for ( const key in format ) keys . add ( key ) ;
110111 }
111112
112- // Surface the inherited $type even when no source at this level declares one,
113- // so that `tokens.color.blue.$type` correctly returns "color" when `$type` is
114- // only defined on the parent group.
113+ // Surface inherited $type so descendants see it even when no local source declares one.
115114 if ( ! keys . has ( "$type" ) && inheritedType !== undefined ) keys . add ( "$type" ) ;
116115
117- /** Returns the effective ` $type` for the current node. */
116+ // Memoised $type resolver — scans sources once and caches the result.
118117 let nodeTypeResolved = false ;
119118 let nodeType : string | undefined ;
120119 const getNodeType = ( ) : string | undefined => {
121120 if ( ! nodeTypeResolved ) {
122121 nodeTypeResolved = true ;
123- for ( const format of effectiveFormats ) {
124- if ( typeof format . $type === "string" ) nodeType = format . $type ;
122+ for ( const fmt of effectiveFormats ) {
123+ if ( typeof fmt . $type === "string" ) nodeType = fmt . $type ;
125124 }
126125 nodeType ??= inheritedType ;
127126 }
128127 return nodeType ;
129128 } ;
130129
131- // Define a self-memoizing getter for every key.
132- // On first access the getter computes and caches the value as a plain data
133- // property — subsequent reads are O(1) and `JSON.stringify` works as usual.
130+ // Self-memoising getter for every key. On first access the getter computes
131+ // the value, replaces itself with a plain data property, and never runs again.
134132 for ( const key of keys ) {
135133 Object . defineProperty ( node , key , {
136134 get ( ) : unknown {
137- // $type resolves immediately from inherited/own declarations.
138135 let value : unknown = key === "$type" ? getNodeType ( ) : undefined ;
139136
140137 if ( value === undefined && key !== "$type" ) {
141- // Single pass: partition sources for this key into sub-objects
142- // (groups/tokens) and leaf values. A later leaf resets any
143- // previously accumulated sub-objects (full override).
144- const subFormats : RawObject [ ] = [ ] ;
145- let leafValue : unknown ;
138+ // Single pass: collect sub-objects and track the last leaf value.
139+ // A later leaf resets accumulated sub-objects (full override).
140+ const subs : RawObject [ ] = [ ] ;
141+ let leaf : unknown ;
146142 let hasLeaf = false ;
147143
148- for ( const format of effectiveFormats ) {
149- const v = format [ key ] ;
144+ for ( const fmt of effectiveFormats ) {
145+ const v = fmt [ key ] ;
150146 if ( v === undefined ) continue ;
151147 if ( isPlainObject ( v ) ) {
152- subFormats . push ( v ) ;
148+ subs . push ( v ) ;
153149 } else {
154- subFormats . length = 0 ;
155- leafValue = v ;
150+ subs . length = 0 ;
151+ leaf = v ;
156152 hasLeaf = true ;
157153 }
158154 }
159155
160- if ( subFormats . length > 0 ) {
161- // Merge the sub-objects recursively.
162- // $type inheritance must NOT bleed into $value content — a
163- // token's type is DTCG metadata on the token node, not on
164- // the value object itself (e.g. a border's width inherits
165- // "border" from its parent, which is wrong).
156+ if ( subs . length > 0 ) {
157+ // $type must NOT bleed into $value — it is metadata on
158+ // the token node, not on the value object itself.
166159 const childType = key === "$value" ? undefined : getNodeType ( ) ;
167- const sub = buildNode ( subFormats , rootFormats , effectiveRoot , childType , [ ...path , key ] ) ;
160+ const sub = buildNode ( subs , rootFormats , effectiveRoot , childType , [ ...path , key ] ) ;
168161
169162 if ( "$value" in sub ) {
170- // Leaf token: auto-unwrap so callers get the value directly
171- // (e.g. tokens["focus-ring"].dark → the border object).
172- value = sub . $value ;
163+ value = sub . $value ; // auto-unwrap leaf token
173164 } else if ( key === "$value" ) {
174- // $value is a composite plain object (border, color, …) —
175- // resolve any alias strings or $ref objects nested inside it.
176- value = resolveDeep ( sub , effectiveRoot ) ;
165+ value = resolveDeep ( sub , effectiveRoot ) ; // composite value
177166 } else {
178- // Group node: expose as a navigable merged sub-tree.
179- value = sub ;
167+ value = sub ; // group sub-tree
180168 }
181169 } else if ( hasLeaf ) {
182- // Leaf scalar or array: resolve aliases/refs only inside $value.
183- value = key === "$value" ? resolveDeep ( leafValue , effectiveRoot ) : leafValue ;
170+ value = key === "$value" ? resolveDeep ( leaf , effectiveRoot ) : leaf ;
184171 }
185172 }
186173
187- // Overwrite this getter with the computed value so it behaves exactly
188- // like a plain data property from this point on.
174+ // Replace the getter with a plain data property.
189175 Object . defineProperty ( this , key , { value, enumerable : true , configurable : true } ) ;
190176 return value ;
191177 } ,
@@ -199,31 +185,28 @@ const buildNode = (
199185
200186// ─── Alias / $ref resolution ──────────────────────────────────────────────────
201187
202- // Module-level cycle-detection set. Safe because JS is single-threaded:
203- // add before recursing, delete in the `finally` block so a thrown error
204- // never leaves stale entries that would permanently block re-resolution.
188+ // Module-level cycle-detection set for aliases. Safe because JS is
189+ // single-threaded; add before recursing, delete in the `finally` block.
205190const resolvingAliases = new Set < string > ( ) ;
206191
207- /** Resolves a DTCG alias string against the merged token tree. */
192+ /** Resolves a DTCG alias string (e.g. `"{color.blue}"`) against the merged tree. */
208193const resolveAlias = ( alias : string , root : RawObject ) : unknown => {
209- const path = alias . slice ( 1 , - 1 ) ; // strip surrounding { }
210- if ( resolvingAliases . has ( path ) ) return undefined ; // circular reference guard
194+ const path = alias . slice ( 1 , - 1 ) ;
195+ if ( resolvingAliases . has ( path ) ) return undefined ;
211196 resolvingAliases . add ( path ) ;
212197 try {
213198 let node : unknown = root ;
214199 for ( const seg of path . split ( "." ) ) {
215200 if ( ! isPlainObject ( node ) ) return undefined ;
216- node = node [ seg ] ; // triggers the self-memoizing getter for that segment
201+ node = node [ seg ] ;
217202 }
218- // Because token nodes are auto-unwrapped, the node we land on IS the
219- // resolved value — return it directly.
220203 return node ;
221204 } finally {
222205 resolvingAliases . delete ( path ) ;
223206 }
224207} ;
225208
226- /** Resolves a JSON Pointer reference against the merged token tree. */
209+ /** Resolves a JSON Pointer `$ref` against the merged tree, skipping unwrapped `$value` segments . */
227210const resolveRef = ( ref : string , root : RawObject ) : unknown => {
228211 let path : string [ ] ;
229212 try {
@@ -234,31 +217,28 @@ const resolveRef = (ref: string, root: RawObject): unknown => {
234217
235218 let node : unknown = root ;
236219 for ( const seg of path ) {
237- // DTCG $ref paths are often written with "/$value/" as a literal path
238- // segment. Since the merged tree auto-unwraps $value, that key no longer
239- // exists at runtime — skip it so the walk lands in the resolved value.
240220 if ( isPlainObject ( node ) ) {
241221 if ( seg === "$value" && ! ( seg in node ) ) continue ;
242222 node = node [ seg ] ;
243223 } else if ( Array . isArray ( node ) ) {
244- const index = Number ( seg ) ;
245- node = Number . isInteger ( index ) ? node [ index ] : undefined ;
224+ const idx = Number ( seg ) ;
225+ node = Number . isInteger ( idx ) ? node [ idx ] : undefined ;
246226 } else {
247227 return undefined ;
248228 }
249229 }
250230 return node ;
251231} ;
252232
253- /** Recursively resolves aliases and `$ref` objects inside a `$ value` . */
233+ /** Recursively resolves aliases and `$ref` objects anywhere inside a value. */
254234const resolveDeep = ( value : unknown , root : RawObject ) : unknown => {
255235 if ( isAlias ( value ) ) return resolveAlias ( value , root ) ;
256236 if ( isRef ( value ) ) return resolveRef ( value . $ref , root ) ;
257- if ( Array . isArray ( value ) ) return value . map ( ( item ) => resolveDeep ( item , root ) ) ;
237+ if ( Array . isArray ( value ) ) return value . map ( ( v ) => resolveDeep ( v , root ) ) ;
258238 if ( isPlainObject ( value ) ) {
259- const resolved : RawObject = Object . create ( null ) ;
260- for ( const k in value ) resolved [ k ] = resolveDeep ( value [ k ] , root ) ;
261- return resolved ;
239+ const out : RawObject = Object . create ( null ) ;
240+ for ( const k in value ) out [ k ] = resolveDeep ( value [ k ] , root ) ;
241+ return out ;
262242 }
263- return value ; // number, boolean, null, string literal — pass through unchanged
243+ return value ;
264244} ;
0 commit comments