@gesslar/sassy 3.0.0 → 3.2.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/package.json +1 -1
- package/src/Compiler.js +1 -1
- package/src/ResolveCommand.js +120 -5
package/package.json
CHANGED
package/src/Compiler.js
CHANGED
|
@@ -117,7 +117,7 @@ export default class Compiler {
|
|
|
117
117
|
// Assemble into one object with the proper keys
|
|
118
118
|
const colors = workColors.reduce(reducer, {})
|
|
119
119
|
const tokenColors = this.#composeArray(workTokenColors)
|
|
120
|
-
const semanticTokenColors = workSemanticTokenColors
|
|
120
|
+
const semanticTokenColors = this.#composeObject(workSemanticTokenColors)
|
|
121
121
|
|
|
122
122
|
// Mix and maaatch all jumbly wumbly...
|
|
123
123
|
const output = Data.mergeObject(
|
package/src/ResolveCommand.js
CHANGED
|
@@ -176,11 +176,35 @@ export default class ResolveCommand extends Command {
|
|
|
176
176
|
const matches = this.#findScopeMatches(tokenColors, scopeName)
|
|
177
177
|
|
|
178
178
|
if(matches.length === 0) {
|
|
179
|
+
// Try precedence-based fallback
|
|
180
|
+
const precedenceMatch = this.#findBestPrecedenceMatch(tokenColors, scopeName)
|
|
181
|
+
|
|
182
|
+
if(precedenceMatch) {
|
|
183
|
+
await this.#resolveScopeMatch(
|
|
184
|
+
theme, precedenceMatch.entry, scopeName,
|
|
185
|
+
{scope: precedenceMatch.matchedScope, relation: "via"}
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
return
|
|
189
|
+
}
|
|
190
|
+
|
|
179
191
|
return Term.info(`No tokenColors entries found for scope '${scopeName}'`)
|
|
180
192
|
}
|
|
181
193
|
|
|
182
194
|
if(matches.length === 1) {
|
|
183
|
-
//
|
|
195
|
+
// Check if a broader scope earlier in the array masks this exact match
|
|
196
|
+
const maskingScope = this.#findMaskingScope(tokenColors, matches[0], scopeName)
|
|
197
|
+
|
|
198
|
+
if(maskingScope) {
|
|
199
|
+
await this.#resolveScopeMatch(
|
|
200
|
+
theme, maskingScope.entry, scopeName,
|
|
201
|
+
{scope: maskingScope.matchedScope, relation: "masked by"}
|
|
202
|
+
)
|
|
203
|
+
|
|
204
|
+
return
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// No masking - resolve directly
|
|
184
208
|
await this.#resolveScopeMatch(theme, matches[0], scopeName)
|
|
185
209
|
} else {
|
|
186
210
|
// Multiple matches - show disambiguation options
|
|
@@ -205,7 +229,94 @@ export default class ResolveCommand extends Command {
|
|
|
205
229
|
})
|
|
206
230
|
}
|
|
207
231
|
|
|
208
|
-
|
|
232
|
+
/**
|
|
233
|
+
* Finds the best precedence match for a target scope that has no exact match.
|
|
234
|
+
* Uses TextMate scope hierarchy rules: a broader scope (fewer segments) that
|
|
235
|
+
* is a prefix of the target scope will match. Returns the most specific
|
|
236
|
+
* (longest) broader scope.
|
|
237
|
+
*
|
|
238
|
+
* @param {Array} tokenColors - Array of tokenColors entries
|
|
239
|
+
* @param {string} targetScope - The scope to find a precedence match for
|
|
240
|
+
* @returns {{entry: object, matchedScope: string}|null} The best match or null
|
|
241
|
+
* @private
|
|
242
|
+
*/
|
|
243
|
+
#findBestPrecedenceMatch(tokenColors, targetScope) {
|
|
244
|
+
const targetSegments = targetScope.split(".")
|
|
245
|
+
let bestMatch = null
|
|
246
|
+
let bestLength = 0
|
|
247
|
+
|
|
248
|
+
for(const entry of tokenColors) {
|
|
249
|
+
if(!entry.scope)
|
|
250
|
+
continue
|
|
251
|
+
|
|
252
|
+
const scopes = entry.scope.split(",").map(s => s.trim())
|
|
253
|
+
|
|
254
|
+
for(const scope of scopes) {
|
|
255
|
+
const scopeSegments = scope.split(".")
|
|
256
|
+
|
|
257
|
+
// Must be fewer segments (broader) and a prefix of the target
|
|
258
|
+
if(scopeSegments.length >= targetSegments.length)
|
|
259
|
+
continue
|
|
260
|
+
|
|
261
|
+
const isPrefix = scopeSegments.every((seg, i) =>
|
|
262
|
+
seg === targetSegments[i]
|
|
263
|
+
)
|
|
264
|
+
|
|
265
|
+
if(isPrefix && scopeSegments.length > bestLength) {
|
|
266
|
+
bestLength = scopeSegments.length
|
|
267
|
+
bestMatch = {entry, matchedScope: scope}
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
return bestMatch
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* Finds a broader scope that appears earlier in the tokenColors array
|
|
277
|
+
* and would mask the given exact match entry.
|
|
278
|
+
*
|
|
279
|
+
* @param {Array} tokenColors - Array of tokenColors entries
|
|
280
|
+
* @param {object} exactMatch - The exact match entry
|
|
281
|
+
* @param {string} targetScope - The scope being resolved
|
|
282
|
+
* @returns {{entry: object, matchedScope: string}|null} The masking entry or null
|
|
283
|
+
* @private
|
|
284
|
+
*/
|
|
285
|
+
#findMaskingScope(tokenColors, exactMatch, targetScope) {
|
|
286
|
+
const targetSegments = targetScope.split(".")
|
|
287
|
+
const exactIndex = tokenColors.indexOf(exactMatch)
|
|
288
|
+
let bestMatch = null
|
|
289
|
+
let bestLength = 0
|
|
290
|
+
|
|
291
|
+
for(let i = 0; i < exactIndex; i++) {
|
|
292
|
+
const entry = tokenColors[i]
|
|
293
|
+
|
|
294
|
+
if(!entry.scope)
|
|
295
|
+
continue
|
|
296
|
+
|
|
297
|
+
const scopes = entry.scope.split(",").map(s => s.trim())
|
|
298
|
+
|
|
299
|
+
for(const scope of scopes) {
|
|
300
|
+
const scopeSegments = scope.split(".")
|
|
301
|
+
|
|
302
|
+
if(scopeSegments.length >= targetSegments.length)
|
|
303
|
+
continue
|
|
304
|
+
|
|
305
|
+
const isPrefix = scopeSegments.every((seg, idx) =>
|
|
306
|
+
seg === targetSegments[idx]
|
|
307
|
+
)
|
|
308
|
+
|
|
309
|
+
if(isPrefix && scopeSegments.length > bestLength) {
|
|
310
|
+
bestLength = scopeSegments.length
|
|
311
|
+
bestMatch = {entry, matchedScope: scope}
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
return bestMatch
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
async #resolveScopeMatch(theme, match, displayName, resolvedVia = null) {
|
|
209
320
|
const pool = theme.getPool()
|
|
210
321
|
const settings = match.settings || {}
|
|
211
322
|
const name = match.name || "Unnamed"
|
|
@@ -255,9 +366,13 @@ export default class ResolveCommand extends Command {
|
|
|
255
366
|
const fullTrail = this.#buildCompleteTrail(bestToken, trail)
|
|
256
367
|
const finalValue = bestToken.getValue()
|
|
257
368
|
const [formattedFinalValue] = this.#formatLeaf(finalValue)
|
|
258
|
-
const
|
|
259
|
-
|
|
260
|
-
|
|
369
|
+
const header = resolvedVia
|
|
370
|
+
? c`{<BU}${displayName}{/} {<I}${resolvedVia.relation}{/} {<BU}${resolvedVia.scope}{/} {<I}in{/} {hex}${(`${name}`)}{/}\n`
|
|
371
|
+
: c`{<BU}${displayName}{/} {<I}in{/} {hex}${(`${name}`)}{/}\n`
|
|
372
|
+
|
|
373
|
+
const output = header +
|
|
374
|
+
`${this.#formatOutput(fullTrail)}\n\n`+
|
|
375
|
+
c`{head}${"Resolution:"}{/} ${formattedFinalValue}`
|
|
261
376
|
|
|
262
377
|
Term.info(output)
|
|
263
378
|
}
|