@jesscss/core 2.0.0-alpha.4 → 2.0.0-alpha.6
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/lib/index.cjs +20159 -0
- package/lib/index.d.cts +5993 -0
- package/lib/index.d.cts.map +1 -0
- package/lib/index.d.ts +5992 -21
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +19926 -22
- package/lib/index.js.map +1 -1
- package/package.json +15 -14
- package/src/__tests__/define-function-record.test.ts +58 -0
- package/src/__tests__/define-function-simple.test.ts +55 -0
- package/src/__tests__/define-function-split-sequence.test.ts +547 -0
- package/src/__tests__/define-function-type-parity.test.ts +9 -0
- package/src/__tests__/define-function.test.ts +763 -0
- package/src/__tests__/num-operations.test.ts +91 -0
- package/src/__tests__/safe-parse.test.ts +374 -0
- package/src/context.ts +896 -0
- package/src/conversions.ts +282 -0
- package/src/debug-log.ts +29 -0
- package/src/define-function.ts +1006 -0
- package/src/deprecation.ts +67 -0
- package/src/globals.d.ts +26 -0
- package/src/index.ts +31 -0
- package/src/jess-error.ts +773 -0
- package/src/logger/deprecation-processing.ts +109 -0
- package/src/logger.ts +31 -0
- package/src/plugin.ts +292 -0
- package/src/tree/LOOKUP_CHAINS.md +35 -0
- package/src/tree/README.md +18 -0
- package/src/tree/__tests__/__snapshots__/extend-eval-integration.test.ts.snap +1455 -0
- package/src/tree/__tests__/ampersand.test.ts +382 -0
- package/src/tree/__tests__/at-rule.test.ts +2047 -0
- package/src/tree/__tests__/basic-render.test.ts +212 -0
- package/src/tree/__tests__/block.test.ts +40 -0
- package/src/tree/__tests__/call.test.ts +346 -0
- package/src/tree/__tests__/color.test.ts +537 -0
- package/src/tree/__tests__/condition.test.ts +186 -0
- package/src/tree/__tests__/control.test.ts +564 -0
- package/src/tree/__tests__/declaration.test.ts +253 -0
- package/src/tree/__tests__/dependency-graph.test.ts +177 -0
- package/src/tree/__tests__/detached-rulesets.test.ts +213 -0
- package/src/tree/__tests__/dimension.test.ts +236 -0
- package/src/tree/__tests__/expression.test.ts +73 -0
- package/src/tree/__tests__/ext-node.test.ts +31 -0
- package/src/tree/__tests__/extend-eval-integration.test.ts +1033 -0
- package/src/tree/__tests__/extend-import-style.test.ts +929 -0
- package/src/tree/__tests__/extend-less-fixtures.test.ts +851 -0
- package/src/tree/__tests__/extend-list.test.ts +31 -0
- package/src/tree/__tests__/extend-roots.test.ts +1045 -0
- package/src/tree/__tests__/extend-rules.test.ts +740 -0
- package/src/tree/__tests__/func.test.ts +171 -0
- package/src/tree/__tests__/import-js.test.ts +33 -0
- package/src/tree/__tests__/import-style-test-helpers.ts +56 -0
- package/src/tree/__tests__/import-style.test.ts +1967 -0
- package/src/tree/__tests__/interpolated-reference.test.ts +44 -0
- package/src/tree/__tests__/interpolated.test.ts +41 -0
- package/src/tree/__tests__/list.test.ts +177 -0
- package/src/tree/__tests__/log.test.ts +83 -0
- package/src/tree/__tests__/mixin-recursion.test.ts +639 -0
- package/src/tree/__tests__/mixin.test.ts +2171 -0
- package/src/tree/__tests__/negative.test.ts +45 -0
- package/src/tree/__tests__/nesting-collapse.test.ts +519 -0
- package/src/tree/__tests__/node-flags-perf.test.ts +195 -0
- package/src/tree/__tests__/node-flags.test.ts +410 -0
- package/src/tree/__tests__/node-graph.test.ts +598 -0
- package/src/tree/__tests__/node-mutation.test.ts +182 -0
- package/src/tree/__tests__/operation.test.ts +18 -0
- package/src/tree/__tests__/paren.test.ts +90 -0
- package/src/tree/__tests__/preserve-mode-output.test.ts +50 -0
- package/src/tree/__tests__/quoted.test.ts +72 -0
- package/src/tree/__tests__/range.test.ts +59 -0
- package/src/tree/__tests__/reference.test.ts +743 -0
- package/src/tree/__tests__/rest.test.ts +29 -0
- package/src/tree/__tests__/rules-raw.test.ts +14 -0
- package/src/tree/__tests__/rules.test.ts +1271 -0
- package/src/tree/__tests__/ruleset.test.ts +597 -0
- package/src/tree/__tests__/selector-attr.test.ts +50 -0
- package/src/tree/__tests__/selector-basic.test.ts +44 -0
- package/src/tree/__tests__/selector-capture.test.ts +22 -0
- package/src/tree/__tests__/selector-complex.test.ts +120 -0
- package/src/tree/__tests__/selector-compound.test.ts +74 -0
- package/src/tree/__tests__/selector-interpolated.test.ts +50 -0
- package/src/tree/__tests__/selector-list.test.ts +59 -0
- package/src/tree/__tests__/selector-pseudo.test.ts +23 -0
- package/src/tree/__tests__/selector.test.ts +182 -0
- package/src/tree/__tests__/sequence.test.ts +226 -0
- package/src/tree/__tests__/serialize-types.test.ts +529 -0
- package/src/tree/__tests__/spaced.test.ts +8 -0
- package/src/tree/__tests__/url.test.ts +72 -0
- package/src/tree/__tests__/var-declaration.test.ts +90 -0
- package/src/tree/ampersand.ts +538 -0
- package/src/tree/any.ts +169 -0
- package/src/tree/at-rule.ts +760 -0
- package/src/tree/block.ts +72 -0
- package/src/tree/bool.ts +46 -0
- package/src/tree/call.ts +593 -0
- package/src/tree/collection.ts +52 -0
- package/src/tree/color.ts +629 -0
- package/src/tree/combinator.ts +30 -0
- package/src/tree/comment.ts +36 -0
- package/src/tree/condition.ts +194 -0
- package/src/tree/control.ts +452 -0
- package/src/tree/declaration-custom.ts +56 -0
- package/src/tree/declaration-var.ts +87 -0
- package/src/tree/declaration.ts +742 -0
- package/src/tree/default-guard.ts +35 -0
- package/src/tree/dimension.ts +392 -0
- package/src/tree/expression.ts +97 -0
- package/src/tree/extend-list.ts +51 -0
- package/src/tree/extend.ts +391 -0
- package/src/tree/function.ts +254 -0
- package/src/tree/import-js.ts +130 -0
- package/src/tree/import-style.ts +875 -0
- package/{lib/tree/index.js → src/tree/index.ts} +49 -22
- package/src/tree/interpolated.ts +346 -0
- package/src/tree/js-array.ts +21 -0
- package/src/tree/js-expr.ts +50 -0
- package/src/tree/js-function.ts +31 -0
- package/src/tree/js-object.ts +22 -0
- package/src/tree/list.ts +415 -0
- package/src/tree/log.ts +89 -0
- package/src/tree/mixin.ts +331 -0
- package/src/tree/negative.ts +58 -0
- package/src/tree/nil.ts +57 -0
- package/src/tree/node-base.ts +1716 -0
- package/src/tree/node-type.ts +122 -0
- package/src/tree/node.ts +118 -0
- package/src/tree/number.ts +54 -0
- package/src/tree/operation.ts +187 -0
- package/src/tree/paren.ts +132 -0
- package/src/tree/query-condition.ts +47 -0
- package/src/tree/quoted.ts +119 -0
- package/src/tree/range.ts +101 -0
- package/src/tree/reference.ts +1099 -0
- package/src/tree/rest.ts +55 -0
- package/src/tree/rules-raw.ts +52 -0
- package/src/tree/rules.ts +2896 -0
- package/src/tree/ruleset.ts +1217 -0
- package/src/tree/selector-attr.ts +172 -0
- package/src/tree/selector-basic.ts +75 -0
- package/src/tree/selector-capture.ts +85 -0
- package/src/tree/selector-complex.ts +189 -0
- package/src/tree/selector-compound.ts +205 -0
- package/src/tree/selector-interpolated.ts +95 -0
- package/src/tree/selector-list.ts +245 -0
- package/src/tree/selector-pseudo.ts +173 -0
- package/src/tree/selector-simple.ts +10 -0
- package/src/tree/selector.ts +152 -0
- package/src/tree/sequence.ts +463 -0
- package/src/tree/tree.ts +130 -0
- package/src/tree/url.ts +95 -0
- package/src/tree/util/EXTEND_ARCHITECTURE_ANALYSIS.md +215 -0
- package/src/tree/util/EXTEND_AUDIT.md +233 -0
- package/src/tree/util/EXTEND_BASELINE.md +64 -0
- package/src/tree/util/EXTEND_CALL_GRAPH_ANALYSIS.md +244 -0
- package/src/tree/util/EXTEND_DOCS.md +24 -0
- package/src/tree/util/EXTEND_FINAL_SUMMARY.md +95 -0
- package/src/tree/util/EXTEND_FUNCTION_AUDIT.md +1433 -0
- package/src/tree/util/EXTEND_OPTIMIZATION_PLAN.md +114 -0
- package/src/tree/util/EXTEND_REFACTORING_SUMMARY.md +152 -0
- package/src/tree/util/EXTEND_RULES.md +74 -0
- package/src/tree/util/EXTEND_UNUSED_FUNCTIONS.md +127 -0
- package/src/tree/util/EXTEND_UNUSED_FUNCTIONS_ANALYSIS.md +227 -0
- package/src/tree/util/NODE_COPY_REDUCTION_PLAN.md +12 -0
- package/src/tree/util/__tests__/EXTEND_TEST_INDEX.md +59 -0
- package/src/tree/util/__tests__/OPTIMIZATION-ANALYSIS.md +130 -0
- package/src/tree/util/__tests__/WALK_AND_CONSUME_DESIGN.md +138 -0
- package/src/tree/util/__tests__/_archive/2026-02-09__OPTIMIZATION-ANALYSIS.md +9 -0
- package/src/tree/util/__tests__/_archive/README.md +4 -0
- package/src/tree/util/__tests__/bitset.test.ts +142 -0
- package/src/tree/util/__tests__/debug-log.ts +50 -0
- package/src/tree/util/__tests__/extend-comment-handling.test.ts +187 -0
- package/src/tree/util/__tests__/extend-core-unit.test.ts +941 -0
- package/src/tree/util/__tests__/extend-pipeline-bench.test.ts +154 -0
- package/src/tree/util/__tests__/extend-pipeline-bench.ts +190 -0
- package/src/tree/util/__tests__/fast-reject.test.ts +377 -0
- package/src/tree/util/__tests__/is-node.test.ts +63 -0
- package/src/tree/util/__tests__/list-like.test.ts +63 -0
- package/src/tree/util/__tests__/outputwriter.test.ts +523 -0
- package/src/tree/util/__tests__/print.test.ts +183 -0
- package/src/tree/util/__tests__/process-extends.test.ts +226 -0
- package/src/tree/util/__tests__/process-leading-is.test.ts +205 -0
- package/src/tree/util/__tests__/recursion-helper.test.ts +184 -0
- package/src/tree/util/__tests__/selector-match-unit.test.ts +1427 -0
- package/src/tree/util/__tests__/sourcemap.test.ts +117 -0
- package/src/tree/util/ampersand-template.ts +9 -0
- package/src/tree/util/bitset.ts +194 -0
- package/src/tree/util/calculate.ts +11 -0
- package/src/tree/util/cast.ts +89 -0
- package/src/tree/util/cloning.ts +8 -0
- package/src/tree/util/collections.ts +299 -0
- package/src/tree/util/compare.ts +90 -0
- package/src/tree/util/cursor.ts +171 -0
- package/src/tree/util/extend-core.ts +2139 -0
- package/src/tree/util/extend-roots.ts +1108 -0
- package/src/tree/util/field-helpers.ts +354 -0
- package/src/tree/util/is-node.ts +43 -0
- package/src/tree/util/list-like.ts +93 -0
- package/src/tree/util/mixin-instance-primitives.ts +2020 -0
- package/src/tree/util/print.ts +303 -0
- package/src/tree/util/process-leading-is.ts +421 -0
- package/src/tree/util/recursion-helper.ts +54 -0
- package/src/tree/util/regex.ts +2 -0
- package/src/tree/util/registry-utils.ts +1953 -0
- package/src/tree/util/ruleset-trace.ts +17 -0
- package/src/tree/util/scoped-body-eval.ts +320 -0
- package/src/tree/util/selector-match-core.ts +2005 -0
- package/src/tree/util/selector-utils.ts +757 -0
- package/src/tree/util/serialize-helper.ts +535 -0
- package/src/tree/util/serialize-types.ts +318 -0
- package/src/tree/util/should-operate.ts +78 -0
- package/src/tree/util/sourcemap.ts +37 -0
- package/src/types/config.ts +247 -0
- package/src/types/index.ts +12 -0
- package/{lib/types/modes.d.ts → src/types/modes.ts} +2 -1
- package/src/types.d.ts +9 -0
- package/src/types.ts +68 -0
- package/src/use-webpack-resolver.ts +56 -0
- package/src/visitor/__tests__/visitor.test.ts +136 -0
- package/src/visitor/index.ts +263 -0
- package/{lib/visitor/less-visitor.js → src/visitor/less-visitor.ts} +3 -2
- package/lib/context.d.ts +0 -352
- package/lib/context.d.ts.map +0 -1
- package/lib/context.js +0 -636
- package/lib/context.js.map +0 -1
- package/lib/conversions.d.ts +0 -73
- package/lib/conversions.d.ts.map +0 -1
- package/lib/conversions.js +0 -253
- package/lib/conversions.js.map +0 -1
- package/lib/debug-log.d.ts +0 -2
- package/lib/debug-log.d.ts.map +0 -1
- package/lib/debug-log.js +0 -27
- package/lib/debug-log.js.map +0 -1
- package/lib/define-function.d.ts +0 -587
- package/lib/define-function.d.ts.map +0 -1
- package/lib/define-function.js +0 -726
- package/lib/define-function.js.map +0 -1
- package/lib/deprecation.d.ts +0 -34
- package/lib/deprecation.d.ts.map +0 -1
- package/lib/deprecation.js +0 -57
- package/lib/deprecation.js.map +0 -1
- package/lib/jess-error.d.ts +0 -343
- package/lib/jess-error.d.ts.map +0 -1
- package/lib/jess-error.js +0 -508
- package/lib/jess-error.js.map +0 -1
- package/lib/logger/deprecation-processing.d.ts +0 -41
- package/lib/logger/deprecation-processing.d.ts.map +0 -1
- package/lib/logger/deprecation-processing.js +0 -81
- package/lib/logger/deprecation-processing.js.map +0 -1
- package/lib/logger.d.ts +0 -10
- package/lib/logger.d.ts.map +0 -1
- package/lib/logger.js +0 -20
- package/lib/logger.js.map +0 -1
- package/lib/plugin.d.ts +0 -94
- package/lib/plugin.d.ts.map +0 -1
- package/lib/plugin.js +0 -174
- package/lib/plugin.js.map +0 -1
- package/lib/tree/ampersand.d.ts +0 -94
- package/lib/tree/ampersand.d.ts.map +0 -1
- package/lib/tree/ampersand.js +0 -269
- package/lib/tree/ampersand.js.map +0 -1
- package/lib/tree/any.d.ts +0 -58
- package/lib/tree/any.d.ts.map +0 -1
- package/lib/tree/any.js +0 -104
- package/lib/tree/any.js.map +0 -1
- package/lib/tree/at-rule.d.ts +0 -53
- package/lib/tree/at-rule.d.ts.map +0 -1
- package/lib/tree/at-rule.js +0 -503
- package/lib/tree/at-rule.js.map +0 -1
- package/lib/tree/block.d.ts +0 -22
- package/lib/tree/block.d.ts.map +0 -1
- package/lib/tree/block.js +0 -24
- package/lib/tree/block.js.map +0 -1
- package/lib/tree/bool.d.ts +0 -18
- package/lib/tree/bool.d.ts.map +0 -1
- package/lib/tree/bool.js +0 -28
- package/lib/tree/bool.js.map +0 -1
- package/lib/tree/call.d.ts +0 -66
- package/lib/tree/call.d.ts.map +0 -1
- package/lib/tree/call.js +0 -306
- package/lib/tree/call.js.map +0 -1
- package/lib/tree/collection.d.ts +0 -30
- package/lib/tree/collection.d.ts.map +0 -1
- package/lib/tree/collection.js +0 -37
- package/lib/tree/collection.js.map +0 -1
- package/lib/tree/color.d.ts +0 -101
- package/lib/tree/color.d.ts.map +0 -1
- package/lib/tree/color.js +0 -513
- package/lib/tree/color.js.map +0 -1
- package/lib/tree/combinator.d.ts +0 -13
- package/lib/tree/combinator.d.ts.map +0 -1
- package/lib/tree/combinator.js +0 -12
- package/lib/tree/combinator.js.map +0 -1
- package/lib/tree/comment.d.ts +0 -20
- package/lib/tree/comment.d.ts.map +0 -1
- package/lib/tree/comment.js +0 -19
- package/lib/tree/comment.js.map +0 -1
- package/lib/tree/condition.d.ts +0 -31
- package/lib/tree/condition.d.ts.map +0 -1
- package/lib/tree/condition.js +0 -103
- package/lib/tree/condition.js.map +0 -1
- package/lib/tree/control.d.ts +0 -104
- package/lib/tree/control.d.ts.map +0 -1
- package/lib/tree/control.js +0 -430
- package/lib/tree/control.js.map +0 -1
- package/lib/tree/declaration-custom.d.ts +0 -18
- package/lib/tree/declaration-custom.d.ts.map +0 -1
- package/lib/tree/declaration-custom.js +0 -24
- package/lib/tree/declaration-custom.js.map +0 -1
- package/lib/tree/declaration-var.d.ts +0 -35
- package/lib/tree/declaration-var.d.ts.map +0 -1
- package/lib/tree/declaration-var.js +0 -63
- package/lib/tree/declaration-var.js.map +0 -1
- package/lib/tree/declaration.d.ts +0 -78
- package/lib/tree/declaration.d.ts.map +0 -1
- package/lib/tree/declaration.js +0 -286
- package/lib/tree/declaration.js.map +0 -1
- package/lib/tree/default-guard.d.ts +0 -15
- package/lib/tree/default-guard.d.ts.map +0 -1
- package/lib/tree/default-guard.js +0 -19
- package/lib/tree/default-guard.js.map +0 -1
- package/lib/tree/dimension.d.ts +0 -34
- package/lib/tree/dimension.d.ts.map +0 -1
- package/lib/tree/dimension.js +0 -294
- package/lib/tree/dimension.js.map +0 -1
- package/lib/tree/expression.d.ts +0 -25
- package/lib/tree/expression.d.ts.map +0 -1
- package/lib/tree/expression.js +0 -32
- package/lib/tree/expression.js.map +0 -1
- package/lib/tree/extend-list.d.ts +0 -23
- package/lib/tree/extend-list.d.ts.map +0 -1
- package/lib/tree/extend-list.js +0 -23
- package/lib/tree/extend-list.js.map +0 -1
- package/lib/tree/extend.d.ts +0 -47
- package/lib/tree/extend.d.ts.map +0 -1
- package/lib/tree/extend.js +0 -296
- package/lib/tree/extend.js.map +0 -1
- package/lib/tree/function.d.ts +0 -48
- package/lib/tree/function.d.ts.map +0 -1
- package/lib/tree/function.js +0 -74
- package/lib/tree/function.js.map +0 -1
- package/lib/tree/import-js.d.ts +0 -35
- package/lib/tree/import-js.d.ts.map +0 -1
- package/lib/tree/import-js.js +0 -45
- package/lib/tree/import-js.js.map +0 -1
- package/lib/tree/import-style.d.ts +0 -156
- package/lib/tree/import-style.d.ts.map +0 -1
- package/lib/tree/import-style.js +0 -566
- package/lib/tree/import-style.js.map +0 -1
- package/lib/tree/index.d.ts +0 -71
- package/lib/tree/index.d.ts.map +0 -1
- package/lib/tree/index.js.map +0 -1
- package/lib/tree/interpolated-reference.d.ts +0 -24
- package/lib/tree/interpolated-reference.d.ts.map +0 -1
- package/lib/tree/interpolated-reference.js +0 -37
- package/lib/tree/interpolated-reference.js.map +0 -1
- package/lib/tree/interpolated.d.ts +0 -62
- package/lib/tree/interpolated.d.ts.map +0 -1
- package/lib/tree/interpolated.js +0 -204
- package/lib/tree/interpolated.js.map +0 -1
- package/lib/tree/js-array.d.ts +0 -10
- package/lib/tree/js-array.d.ts.map +0 -1
- package/lib/tree/js-array.js +0 -10
- package/lib/tree/js-array.js.map +0 -1
- package/lib/tree/js-expr.d.ts +0 -23
- package/lib/tree/js-expr.d.ts.map +0 -1
- package/lib/tree/js-expr.js +0 -28
- package/lib/tree/js-expr.js.map +0 -1
- package/lib/tree/js-function.d.ts +0 -20
- package/lib/tree/js-function.d.ts.map +0 -1
- package/lib/tree/js-function.js +0 -16
- package/lib/tree/js-function.js.map +0 -1
- package/lib/tree/js-object.d.ts +0 -10
- package/lib/tree/js-object.d.ts.map +0 -1
- package/lib/tree/js-object.js +0 -10
- package/lib/tree/js-object.js.map +0 -1
- package/lib/tree/list.d.ts +0 -38
- package/lib/tree/list.d.ts.map +0 -1
- package/lib/tree/list.js +0 -83
- package/lib/tree/list.js.map +0 -1
- package/lib/tree/log.d.ts +0 -29
- package/lib/tree/log.d.ts.map +0 -1
- package/lib/tree/log.js +0 -56
- package/lib/tree/log.js.map +0 -1
- package/lib/tree/mixin.d.ts +0 -87
- package/lib/tree/mixin.d.ts.map +0 -1
- package/lib/tree/mixin.js +0 -112
- package/lib/tree/mixin.js.map +0 -1
- package/lib/tree/negative.d.ts +0 -17
- package/lib/tree/negative.d.ts.map +0 -1
- package/lib/tree/negative.js +0 -22
- package/lib/tree/negative.js.map +0 -1
- package/lib/tree/nil.d.ts +0 -30
- package/lib/tree/nil.d.ts.map +0 -1
- package/lib/tree/nil.js +0 -35
- package/lib/tree/nil.js.map +0 -1
- package/lib/tree/node-base.d.ts +0 -361
- package/lib/tree/node-base.d.ts.map +0 -1
- package/lib/tree/node-base.js +0 -930
- package/lib/tree/node-base.js.map +0 -1
- package/lib/tree/node.d.ts +0 -10
- package/lib/tree/node.d.ts.map +0 -1
- package/lib/tree/node.js +0 -45
- package/lib/tree/node.js.map +0 -1
- package/lib/tree/number.d.ts +0 -21
- package/lib/tree/number.d.ts.map +0 -1
- package/lib/tree/number.js +0 -27
- package/lib/tree/number.js.map +0 -1
- package/lib/tree/operation.d.ts +0 -26
- package/lib/tree/operation.d.ts.map +0 -1
- package/lib/tree/operation.js +0 -103
- package/lib/tree/operation.js.map +0 -1
- package/lib/tree/paren.d.ts +0 -19
- package/lib/tree/paren.d.ts.map +0 -1
- package/lib/tree/paren.js +0 -92
- package/lib/tree/paren.js.map +0 -1
- package/lib/tree/query-condition.d.ts +0 -17
- package/lib/tree/query-condition.d.ts.map +0 -1
- package/lib/tree/query-condition.js +0 -39
- package/lib/tree/query-condition.js.map +0 -1
- package/lib/tree/quoted.d.ts +0 -28
- package/lib/tree/quoted.d.ts.map +0 -1
- package/lib/tree/quoted.js +0 -75
- package/lib/tree/quoted.js.map +0 -1
- package/lib/tree/range.d.ts +0 -33
- package/lib/tree/range.d.ts.map +0 -1
- package/lib/tree/range.js +0 -47
- package/lib/tree/range.js.map +0 -1
- package/lib/tree/reference.d.ts +0 -76
- package/lib/tree/reference.d.ts.map +0 -1
- package/lib/tree/reference.js +0 -521
- package/lib/tree/reference.js.map +0 -1
- package/lib/tree/rest.d.ts +0 -15
- package/lib/tree/rest.d.ts.map +0 -1
- package/lib/tree/rest.js +0 -32
- package/lib/tree/rest.js.map +0 -1
- package/lib/tree/rules-raw.d.ts +0 -17
- package/lib/tree/rules-raw.d.ts.map +0 -1
- package/lib/tree/rules-raw.js +0 -37
- package/lib/tree/rules-raw.js.map +0 -1
- package/lib/tree/rules.d.ts +0 -262
- package/lib/tree/rules.d.ts.map +0 -1
- package/lib/tree/rules.js +0 -2359
- package/lib/tree/rules.js.map +0 -1
- package/lib/tree/ruleset.d.ts +0 -92
- package/lib/tree/ruleset.d.ts.map +0 -1
- package/lib/tree/ruleset.js +0 -528
- package/lib/tree/ruleset.js.map +0 -1
- package/lib/tree/selector-attr.d.ts +0 -31
- package/lib/tree/selector-attr.d.ts.map +0 -1
- package/lib/tree/selector-attr.js +0 -99
- package/lib/tree/selector-attr.js.map +0 -1
- package/lib/tree/selector-basic.d.ts +0 -24
- package/lib/tree/selector-basic.d.ts.map +0 -1
- package/lib/tree/selector-basic.js +0 -38
- package/lib/tree/selector-basic.js.map +0 -1
- package/lib/tree/selector-capture.d.ts +0 -23
- package/lib/tree/selector-capture.d.ts.map +0 -1
- package/lib/tree/selector-capture.js +0 -34
- package/lib/tree/selector-capture.js.map +0 -1
- package/lib/tree/selector-complex.d.ts +0 -40
- package/lib/tree/selector-complex.d.ts.map +0 -1
- package/lib/tree/selector-complex.js +0 -143
- package/lib/tree/selector-complex.js.map +0 -1
- package/lib/tree/selector-compound.d.ts +0 -16
- package/lib/tree/selector-compound.d.ts.map +0 -1
- package/lib/tree/selector-compound.js +0 -114
- package/lib/tree/selector-compound.js.map +0 -1
- package/lib/tree/selector-interpolated.d.ts +0 -23
- package/lib/tree/selector-interpolated.d.ts.map +0 -1
- package/lib/tree/selector-interpolated.js +0 -27
- package/lib/tree/selector-interpolated.js.map +0 -1
- package/lib/tree/selector-list.d.ts +0 -17
- package/lib/tree/selector-list.d.ts.map +0 -1
- package/lib/tree/selector-list.js +0 -174
- package/lib/tree/selector-list.js.map +0 -1
- package/lib/tree/selector-pseudo.d.ts +0 -42
- package/lib/tree/selector-pseudo.d.ts.map +0 -1
- package/lib/tree/selector-pseudo.js +0 -204
- package/lib/tree/selector-pseudo.js.map +0 -1
- package/lib/tree/selector-simple.d.ts +0 -5
- package/lib/tree/selector-simple.d.ts.map +0 -1
- package/lib/tree/selector-simple.js +0 -6
- package/lib/tree/selector-simple.js.map +0 -1
- package/lib/tree/selector.d.ts +0 -43
- package/lib/tree/selector.d.ts.map +0 -1
- package/lib/tree/selector.js +0 -56
- package/lib/tree/selector.js.map +0 -1
- package/lib/tree/sequence.d.ts +0 -43
- package/lib/tree/sequence.d.ts.map +0 -1
- package/lib/tree/sequence.js +0 -151
- package/lib/tree/sequence.js.map +0 -1
- package/lib/tree/tree.d.ts +0 -87
- package/lib/tree/tree.d.ts.map +0 -1
- package/lib/tree/tree.js +0 -2
- package/lib/tree/tree.js.map +0 -1
- package/lib/tree/url.d.ts +0 -18
- package/lib/tree/url.d.ts.map +0 -1
- package/lib/tree/url.js +0 -35
- package/lib/tree/url.js.map +0 -1
- package/lib/tree/util/__tests__/debug-log.d.ts +0 -1
- package/lib/tree/util/__tests__/debug-log.d.ts.map +0 -1
- package/lib/tree/util/__tests__/debug-log.js +0 -36
- package/lib/tree/util/__tests__/debug-log.js.map +0 -1
- package/lib/tree/util/calculate.d.ts +0 -3
- package/lib/tree/util/calculate.d.ts.map +0 -1
- package/lib/tree/util/calculate.js +0 -10
- package/lib/tree/util/calculate.js.map +0 -1
- package/lib/tree/util/cast.d.ts +0 -10
- package/lib/tree/util/cast.d.ts.map +0 -1
- package/lib/tree/util/cast.js +0 -87
- package/lib/tree/util/cast.js.map +0 -1
- package/lib/tree/util/cloning.d.ts +0 -4
- package/lib/tree/util/cloning.d.ts.map +0 -1
- package/lib/tree/util/cloning.js +0 -8
- package/lib/tree/util/cloning.js.map +0 -1
- package/lib/tree/util/collections.d.ts +0 -57
- package/lib/tree/util/collections.d.ts.map +0 -1
- package/lib/tree/util/collections.js +0 -136
- package/lib/tree/util/collections.js.map +0 -1
- package/lib/tree/util/compare.d.ts +0 -11
- package/lib/tree/util/compare.d.ts.map +0 -1
- package/lib/tree/util/compare.js +0 -89
- package/lib/tree/util/compare.js.map +0 -1
- package/lib/tree/util/extend-helpers.d.ts +0 -2
- package/lib/tree/util/extend-helpers.d.ts.map +0 -1
- package/lib/tree/util/extend-helpers.js +0 -2
- package/lib/tree/util/extend-helpers.js.map +0 -1
- package/lib/tree/util/extend-roots.d.ts +0 -37
- package/lib/tree/util/extend-roots.d.ts.map +0 -1
- package/lib/tree/util/extend-roots.js +0 -700
- package/lib/tree/util/extend-roots.js.map +0 -1
- package/lib/tree/util/extend-roots.old.d.ts +0 -132
- package/lib/tree/util/extend-roots.old.d.ts.map +0 -1
- package/lib/tree/util/extend-roots.old.js +0 -2272
- package/lib/tree/util/extend-roots.old.js.map +0 -1
- package/lib/tree/util/extend-trace-debug.d.ts +0 -13
- package/lib/tree/util/extend-trace-debug.d.ts.map +0 -1
- package/lib/tree/util/extend-trace-debug.js +0 -34
- package/lib/tree/util/extend-trace-debug.js.map +0 -1
- package/lib/tree/util/extend-walk.d.ts +0 -53
- package/lib/tree/util/extend-walk.d.ts.map +0 -1
- package/lib/tree/util/extend-walk.js +0 -881
- package/lib/tree/util/extend-walk.js.map +0 -1
- package/lib/tree/util/extend.d.ts +0 -218
- package/lib/tree/util/extend.d.ts.map +0 -1
- package/lib/tree/util/extend.js +0 -3182
- package/lib/tree/util/extend.js.map +0 -1
- package/lib/tree/util/find-extendable-locations.d.ts +0 -2
- package/lib/tree/util/find-extendable-locations.d.ts.map +0 -1
- package/lib/tree/util/find-extendable-locations.js +0 -2
- package/lib/tree/util/find-extendable-locations.js.map +0 -1
- package/lib/tree/util/format.d.ts +0 -20
- package/lib/tree/util/format.d.ts.map +0 -1
- package/lib/tree/util/format.js +0 -67
- package/lib/tree/util/format.js.map +0 -1
- package/lib/tree/util/is-node.d.ts +0 -13
- package/lib/tree/util/is-node.d.ts.map +0 -1
- package/lib/tree/util/is-node.js +0 -43
- package/lib/tree/util/is-node.js.map +0 -1
- package/lib/tree/util/print.d.ts +0 -80
- package/lib/tree/util/print.d.ts.map +0 -1
- package/lib/tree/util/print.js +0 -205
- package/lib/tree/util/print.js.map +0 -1
- package/lib/tree/util/process-leading-is.d.ts +0 -25
- package/lib/tree/util/process-leading-is.d.ts.map +0 -1
- package/lib/tree/util/process-leading-is.js +0 -364
- package/lib/tree/util/process-leading-is.js.map +0 -1
- package/lib/tree/util/recursion-helper.d.ts +0 -15
- package/lib/tree/util/recursion-helper.d.ts.map +0 -1
- package/lib/tree/util/recursion-helper.js +0 -43
- package/lib/tree/util/recursion-helper.js.map +0 -1
- package/lib/tree/util/regex.d.ts +0 -4
- package/lib/tree/util/regex.d.ts.map +0 -1
- package/lib/tree/util/regex.js +0 -4
- package/lib/tree/util/regex.js.map +0 -1
- package/lib/tree/util/registry-utils.d.ts +0 -192
- package/lib/tree/util/registry-utils.d.ts.map +0 -1
- package/lib/tree/util/registry-utils.js +0 -1214
- package/lib/tree/util/registry-utils.js.map +0 -1
- package/lib/tree/util/ruleset-trace.d.ts +0 -4
- package/lib/tree/util/ruleset-trace.d.ts.map +0 -1
- package/lib/tree/util/ruleset-trace.js +0 -14
- package/lib/tree/util/ruleset-trace.js.map +0 -1
- package/lib/tree/util/selector-compare.d.ts +0 -2
- package/lib/tree/util/selector-compare.d.ts.map +0 -1
- package/lib/tree/util/selector-compare.js +0 -2
- package/lib/tree/util/selector-compare.js.map +0 -1
- package/lib/tree/util/selector-match-core.d.ts +0 -184
- package/lib/tree/util/selector-match-core.d.ts.map +0 -1
- package/lib/tree/util/selector-match-core.js +0 -1603
- package/lib/tree/util/selector-match-core.js.map +0 -1
- package/lib/tree/util/selector-utils.d.ts +0 -30
- package/lib/tree/util/selector-utils.d.ts.map +0 -1
- package/lib/tree/util/selector-utils.js +0 -100
- package/lib/tree/util/selector-utils.js.map +0 -1
- package/lib/tree/util/serialize-helper.d.ts +0 -13
- package/lib/tree/util/serialize-helper.d.ts.map +0 -1
- package/lib/tree/util/serialize-helper.js +0 -387
- package/lib/tree/util/serialize-helper.js.map +0 -1
- package/lib/tree/util/serialize-types.d.ts +0 -9
- package/lib/tree/util/serialize-types.d.ts.map +0 -1
- package/lib/tree/util/serialize-types.js +0 -216
- package/lib/tree/util/serialize-types.js.map +0 -1
- package/lib/tree/util/should-operate.d.ts +0 -23
- package/lib/tree/util/should-operate.d.ts.map +0 -1
- package/lib/tree/util/should-operate.js +0 -46
- package/lib/tree/util/should-operate.js.map +0 -1
- package/lib/tree/util/sourcemap.d.ts +0 -7
- package/lib/tree/util/sourcemap.d.ts.map +0 -1
- package/lib/tree/util/sourcemap.js +0 -25
- package/lib/tree/util/sourcemap.js.map +0 -1
- package/lib/types/config.d.ts +0 -205
- package/lib/types/config.d.ts.map +0 -1
- package/lib/types/config.js +0 -2
- package/lib/types/config.js.map +0 -1
- package/lib/types/index.d.ts +0 -15
- package/lib/types/index.d.ts.map +0 -1
- package/lib/types/index.js +0 -3
- package/lib/types/index.js.map +0 -1
- package/lib/types/modes.d.ts.map +0 -1
- package/lib/types/modes.js +0 -2
- package/lib/types/modes.js.map +0 -1
- package/lib/types.d.ts +0 -61
- package/lib/types.d.ts.map +0 -1
- package/lib/types.js +0 -2
- package/lib/types.js.map +0 -1
- package/lib/use-webpack-resolver.d.ts +0 -9
- package/lib/use-webpack-resolver.d.ts.map +0 -1
- package/lib/use-webpack-resolver.js +0 -41
- package/lib/use-webpack-resolver.js.map +0 -1
- package/lib/visitor/index.d.ts +0 -136
- package/lib/visitor/index.d.ts.map +0 -1
- package/lib/visitor/index.js +0 -135
- package/lib/visitor/index.js.map +0 -1
- package/lib/visitor/less-visitor.d.ts +0 -7
- package/lib/visitor/less-visitor.d.ts.map +0 -1
- package/lib/visitor/less-visitor.js.map +0 -1
|
@@ -0,0 +1,1953 @@
|
|
|
1
|
+
import type { Ruleset } from '../ruleset.js';
|
|
2
|
+
import type { Selector } from '../selector.js';
|
|
3
|
+
import type { Rules } from '../rules.js';
|
|
4
|
+
import { isNode } from './is-node.js';
|
|
5
|
+
import { N } from '../node-type.js';
|
|
6
|
+
import type { Mixin } from '../mixin.js';
|
|
7
|
+
import { Nil } from '../nil.js';
|
|
8
|
+
import { CALLER, CANONICAL, EVAL, Node } from '../node.js';
|
|
9
|
+
import { JsFunction } from '../js-function.js';
|
|
10
|
+
import type { Func } from '../function.js';
|
|
11
|
+
import type { Declaration } from '../declaration.js';
|
|
12
|
+
import type { Context } from '../../context.js';
|
|
13
|
+
import { atIndex } from './collections.js';
|
|
14
|
+
import { comparePosition } from './compare.js';
|
|
15
|
+
import { type BitSet, type BitSetLibrary, isSubsetOf } from './bitset.js';
|
|
16
|
+
import { getParent, setParent } from './field-helpers.js';
|
|
17
|
+
import { getCurrentParentNode } from './selector-utils.js';
|
|
18
|
+
|
|
19
|
+
const { isArray } = Array;
|
|
20
|
+
|
|
21
|
+
type SelectorKeySet = Set<string> | BitSet<string>;
|
|
22
|
+
type SelectorKeySource = SelectorKeySet | string[];
|
|
23
|
+
|
|
24
|
+
const NON_INDEXABLE_SELECTOR_KEYS = new Set(['', ' ', '>', '+', '~', '||']);
|
|
25
|
+
|
|
26
|
+
function getNodeValueArray(node: Node): unknown[] | undefined {
|
|
27
|
+
const value = Reflect.get(node, 'value');
|
|
28
|
+
return Array.isArray(value) ? value : undefined;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function getSelectorKeySetLibrary(node: unknown): BitSetLibrary<string> | undefined {
|
|
32
|
+
return typeof node === 'object' && node !== null
|
|
33
|
+
? Reflect.get(node, 'keySetLibrary')
|
|
34
|
+
: undefined;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function setSelectorKeySetLibrary(node: unknown, library: BitSetLibrary<string>): void {
|
|
38
|
+
if (typeof node === 'object' && node !== null) {
|
|
39
|
+
Reflect.set(node, 'keySetLibrary', library);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function isSelectorLikeNode(value: unknown): value is Selector {
|
|
44
|
+
return value instanceof Node
|
|
45
|
+
&& typeof value === 'object'
|
|
46
|
+
&& 'isSelector' in value
|
|
47
|
+
&& value.isSelector === true;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function isSelectorLikeOrNil(value: unknown): value is Selector | Nil {
|
|
51
|
+
return isSelectorLikeNode(value) || isNode(value, N.Nil);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function getRulesetSelector(ruleset: Ruleset, context?: Context): Selector | Nil | undefined {
|
|
55
|
+
const selector = ruleset.get('selector', context);
|
|
56
|
+
return isSelectorLikeOrNil(selector) ? selector : undefined;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function getMixinOwnSelector(mixin: Ruleset): Selector | Nil | undefined {
|
|
60
|
+
const ownSelector = mixin.options.ownSelector;
|
|
61
|
+
return isSelectorLikeOrNil(ownSelector) ? ownSelector : undefined;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function isIndexableSelectorKey(key: string): boolean {
|
|
65
|
+
return !key.startsWith(':') && !NON_INDEXABLE_SELECTOR_KEYS.has(key);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function tryGetSelectorKeySet(
|
|
69
|
+
selector: Selector | Nil | undefined,
|
|
70
|
+
visible: boolean = true
|
|
71
|
+
): SelectorKeySet | undefined {
|
|
72
|
+
if (!selector || selector instanceof Nil) {
|
|
73
|
+
return undefined;
|
|
74
|
+
}
|
|
75
|
+
try {
|
|
76
|
+
return visible ? selector.visibleKeySet : selector.keySet;
|
|
77
|
+
} catch {
|
|
78
|
+
return undefined;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function getSelectorKeyValues(keySet: SelectorKeySource | undefined): string[] {
|
|
83
|
+
if (!keySet) {
|
|
84
|
+
return [];
|
|
85
|
+
}
|
|
86
|
+
if (isArray(keySet)) {
|
|
87
|
+
return keySet;
|
|
88
|
+
}
|
|
89
|
+
if (keySet instanceof Set) {
|
|
90
|
+
return [...keySet];
|
|
91
|
+
}
|
|
92
|
+
return keySet._library?.valuesOf(keySet) ?? [];
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function getFallbackSelectorIndexKeys(selector: Node | undefined): string[] {
|
|
96
|
+
if (!selector) {
|
|
97
|
+
return [];
|
|
98
|
+
}
|
|
99
|
+
const value = Reflect.get(selector, 'value');
|
|
100
|
+
if (isArray(value)) {
|
|
101
|
+
const keys: string[] = [];
|
|
102
|
+
for (const child of value) {
|
|
103
|
+
if (!(child instanceof Node)) {
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
if (isNode(child, N.Combinator)) {
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
keys.push(...getFallbackSelectorIndexKeys(child));
|
|
110
|
+
}
|
|
111
|
+
return keys;
|
|
112
|
+
}
|
|
113
|
+
const key = String(selector.valueOf?.() ?? '');
|
|
114
|
+
return key && isIndexableSelectorKey(key) ? [key] : [];
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function hasSelectorKey(keySet: SelectorKeySet | undefined, key: string): boolean {
|
|
118
|
+
if (!keySet) {
|
|
119
|
+
return false;
|
|
120
|
+
}
|
|
121
|
+
if (keySet instanceof Set) {
|
|
122
|
+
return keySet.has(key);
|
|
123
|
+
}
|
|
124
|
+
return keySet._library?.hasBit(keySet, key) ?? false;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function getIndexableSelectorKeys(keySet: SelectorKeySource | undefined): string[] {
|
|
128
|
+
return getSelectorKeyValues(keySet).filter(
|
|
129
|
+
key => typeof key === 'string' && !key.startsWith('*') && isIndexableSelectorKey(key)
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function isNonImportStyleBoundary(rules: Rules | undefined): boolean {
|
|
134
|
+
return Boolean(
|
|
135
|
+
rules
|
|
136
|
+
&& rules.sourceNode?.type === 'StyleImport'
|
|
137
|
+
&& rules.sourceNode.options.type !== 'import'
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export type DeclarationFindOptions = {
|
|
142
|
+
filter?: (n: Node) => boolean;
|
|
143
|
+
candidates?: Set<Node>;
|
|
144
|
+
optionalCandidates?: Set<Node>;
|
|
145
|
+
findAll?: boolean;
|
|
146
|
+
/** This gets set if any parent is set to readonly */
|
|
147
|
+
readonly?: boolean;
|
|
148
|
+
searchParents?: boolean;
|
|
149
|
+
start?: number;
|
|
150
|
+
local?: boolean;
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
export type FindOptions = DeclarationFindOptions & {
|
|
154
|
+
childFilterType?: 'Mixin' | 'Ruleset' | undefined;
|
|
155
|
+
context?: Context;
|
|
156
|
+
searchedRules?: Set<Rules>;
|
|
157
|
+
candidateContexts?: WeakMap<Node, Context>;
|
|
158
|
+
/**
|
|
159
|
+
* Whether this lookup has an explicit target (e.g., #ns[@foo]).
|
|
160
|
+
* When true, Rules with isMixinOutput=true will be searchable.
|
|
161
|
+
* When false or undefined, mixin output Rules will be excluded.
|
|
162
|
+
*/
|
|
163
|
+
hasTarget?: boolean;
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
export type MixinRegistryEntry = {
|
|
167
|
+
value: Mixin | Ruleset;
|
|
168
|
+
match: string[];
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
// [DELETED: RegistryData, globalRegistryCache, peekRegistryData, ensureRegistryData,
|
|
172
|
+
// getRegistryDelta, ensureSessionRegistryIndex, getRegistryIndex, isRegistryIndexing,
|
|
173
|
+
// syncRegistryCache, registerCanonicalNode, registerSessionNode — see registry-state-plan.md]
|
|
174
|
+
|
|
175
|
+
function addRulesetToIndex(
|
|
176
|
+
index: Map<string, Set<Ruleset>>,
|
|
177
|
+
rules: Rules,
|
|
178
|
+
ruleset: Ruleset,
|
|
179
|
+
context?: Context
|
|
180
|
+
): void {
|
|
181
|
+
if (!isSelectorLikeNode(ruleset.get('selector', context))) {
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const selector = ruleset.get('selector', context);
|
|
186
|
+
if (!isSelectorLikeNode(selector)) {
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
const selectorBits = (rules.treeContext as { selectorBits?: BitSetLibrary<string>; opts?: { selectorBits?: BitSetLibrary<string> } } | undefined)?.selectorBits
|
|
190
|
+
?? rules.treeContext?.opts?.selectorBits;
|
|
191
|
+
if (selectorBits && !getSelectorKeySetLibrary(selector)) {
|
|
192
|
+
setSelectorKeySetLibrary(selector, selectorBits);
|
|
193
|
+
const selectorValue = getNodeValueArray(selector);
|
|
194
|
+
if (selectorValue) {
|
|
195
|
+
for (const child of selectorValue) {
|
|
196
|
+
if (isSelectorLikeNode(child) && !getSelectorKeySetLibrary(child)) {
|
|
197
|
+
setSelectorKeySetLibrary(child, selectorBits);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
let keySet: SelectorKeySource;
|
|
204
|
+
try {
|
|
205
|
+
keySet = selector.keySet;
|
|
206
|
+
} catch {
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
for (const key of getSelectorKeyValues(keySet)) {
|
|
211
|
+
const existing = index.get(key);
|
|
212
|
+
if (existing) {
|
|
213
|
+
existing.add(ruleset);
|
|
214
|
+
} else {
|
|
215
|
+
index.set(key, new Set([ruleset]));
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function addMixinToIndex(
|
|
221
|
+
index: Map<string, MixinRegistryEntry[]>,
|
|
222
|
+
rules: Rules,
|
|
223
|
+
mixin: Mixin | Ruleset,
|
|
224
|
+
context?: Context
|
|
225
|
+
): void {
|
|
226
|
+
if (isNode(mixin, N.Ruleset)) {
|
|
227
|
+
const ruleset = mixin;
|
|
228
|
+
let selector = getRulesetSelector(ruleset, context);
|
|
229
|
+
if (isNode(selector, N.Nil)) {
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
const ownSelector = getMixinOwnSelector(ruleset);
|
|
233
|
+
const callableSelector = ownSelector && !isNode(ownSelector, N.Nil) ? ownSelector : selector;
|
|
234
|
+
if (isNode(callableSelector, N.Ampersand)) {
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
const sourceSelector = selector?.sourceNode;
|
|
238
|
+
const selectorVisibleKeySet = tryGetSelectorKeySet(selector);
|
|
239
|
+
const sourceVisibleKeySet = tryGetSelectorKeySet(sourceSelector);
|
|
240
|
+
const selectorToIndex = getIndexableSelectorKeys(selectorVisibleKeySet).length
|
|
241
|
+
? selector
|
|
242
|
+
: (getIndexableSelectorKeys(sourceVisibleKeySet).length ? sourceSelector : selector);
|
|
243
|
+
let keySetToUse: SelectorKeySet | string[] | undefined;
|
|
244
|
+
if (isNode(selectorToIndex, N.SelectorList)) {
|
|
245
|
+
for (const sel of selectorToIndex.get('value')) {
|
|
246
|
+
const selKeySet = tryGetSelectorKeySet(sel);
|
|
247
|
+
if (selKeySet) {
|
|
248
|
+
indexMixinSelectorStart(index, mixin, selKeySet);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
keySetToUse = undefined;
|
|
252
|
+
} else {
|
|
253
|
+
keySetToUse = tryGetSelectorKeySet(selectorToIndex);
|
|
254
|
+
}
|
|
255
|
+
if (
|
|
256
|
+
keySetToUse
|
|
257
|
+
&& getIndexableSelectorKeys(keySetToUse).length > 0
|
|
258
|
+
&& ownSelector
|
|
259
|
+
&& !isNode(ownSelector, N.Nil)
|
|
260
|
+
) {
|
|
261
|
+
const resolvedKeys = getIndexableSelectorKeys(keySetToUse);
|
|
262
|
+
const ownSelectorText = String(ownSelector?.valueOf?.() ?? '');
|
|
263
|
+
if (ownSelector && !getSelectorKeySetLibrary(ownSelector) && getSelectorKeySetLibrary(selectorToIndex)) {
|
|
264
|
+
setSelectorKeySetLibrary(ownSelector, getSelectorKeySetLibrary(selectorToIndex)!);
|
|
265
|
+
}
|
|
266
|
+
const ownKeys = getIndexableSelectorKeys(tryGetSelectorKeySet(ownSelector));
|
|
267
|
+
const parentRules = context ? getParent(mixin, context) : mixin.parent;
|
|
268
|
+
const parentRuleset = parentRules
|
|
269
|
+
? (context ? getParent(parentRules, context) : parentRules.parent)
|
|
270
|
+
: undefined;
|
|
271
|
+
const parentSelector = isNode(parentRuleset, N.Ruleset)
|
|
272
|
+
? parentRuleset.get('selector', context)
|
|
273
|
+
: undefined;
|
|
274
|
+
const parentKeys = (
|
|
275
|
+
parentSelector && !isNode(parentSelector, N.Nil)
|
|
276
|
+
? getIndexableSelectorKeys(tryGetSelectorKeySet(parentSelector))
|
|
277
|
+
: []
|
|
278
|
+
);
|
|
279
|
+
if (
|
|
280
|
+
parentKeys.length > 0
|
|
281
|
+
&& resolvedKeys.length > parentKeys.length
|
|
282
|
+
) {
|
|
283
|
+
const parentKeySet = new Set(parentKeys);
|
|
284
|
+
const localKeys = resolvedKeys.filter(k => !parentKeySet.has(k));
|
|
285
|
+
if (localKeys.length > 0 && localKeys.length < resolvedKeys.length) {
|
|
286
|
+
keySetToUse = localKeys;
|
|
287
|
+
}
|
|
288
|
+
} else if (ownKeys.length > 1 && ownSelectorText.trimStart().startsWith('&')) {
|
|
289
|
+
keySetToUse = ownKeys.slice(1);
|
|
290
|
+
} else if (
|
|
291
|
+
parentKeys.length === 0
|
|
292
|
+
&& ownKeys.length > 0
|
|
293
|
+
&& resolvedKeys.length > ownKeys.length
|
|
294
|
+
) {
|
|
295
|
+
keySetToUse = ownKeys;
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
if (
|
|
299
|
+
keySetToUse !== undefined
|
|
300
|
+
&& getIndexableSelectorKeys(keySetToUse).length === 0
|
|
301
|
+
&& ownSelector
|
|
302
|
+
&& !isNode(ownSelector, N.Nil)
|
|
303
|
+
) {
|
|
304
|
+
const ownKeySet = tryGetSelectorKeySet(ownSelector);
|
|
305
|
+
if (ownKeySet && getIndexableSelectorKeys(ownKeySet).length) {
|
|
306
|
+
const ownKeys = getIndexableSelectorKeys(ownKeySet);
|
|
307
|
+
const selectorText = String(selectorToIndex.valueOf?.() ?? '');
|
|
308
|
+
keySetToUse = selectorText.startsWith('&') && ownKeys.length > 1
|
|
309
|
+
? new Set(ownKeys.slice(1))
|
|
310
|
+
: ownKeySet;
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
if (keySetToUse !== undefined) {
|
|
314
|
+
indexMixinSelectorStart(index, mixin, keySetToUse);
|
|
315
|
+
} else {
|
|
316
|
+
// Fallback: test-created compound selectors may not have a keySet yet.
|
|
317
|
+
// Derive indexable selector segments directly from the selector tree so
|
|
318
|
+
// namespace/compound lookups can still use startKey + remainder matching.
|
|
319
|
+
const fallbackKeys = getFallbackSelectorIndexKeys(callableSelector);
|
|
320
|
+
if (fallbackKeys.length > 0) {
|
|
321
|
+
indexMixinSelectorStart(index, mixin, fallbackKeys);
|
|
322
|
+
} else {
|
|
323
|
+
const selectorStr = String(callableSelector.valueOf?.() ?? '');
|
|
324
|
+
if (selectorStr) {
|
|
325
|
+
indexMixinSelectorStart(index, mixin, [selectorStr]);
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
return;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
const keys = isNode(mixin, N.Mixin) && context ? mixin.getKeySet(context) : mixin.keySet;
|
|
333
|
+
indexMixinSelectorStart(index, mixin, keys);
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
function addDeclarationToIndex(
|
|
337
|
+
index: Map<string, Set<Declaration>>,
|
|
338
|
+
item: Declaration
|
|
339
|
+
): void {
|
|
340
|
+
const key = item.get('name').valueOf();
|
|
341
|
+
const set = index.get(key);
|
|
342
|
+
if (set) {
|
|
343
|
+
set.add(item);
|
|
344
|
+
} else {
|
|
345
|
+
index.set(key, new Set([item]));
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
function indexMixinSelectorStart(
|
|
350
|
+
index: Map<string, MixinRegistryEntry[]>,
|
|
351
|
+
mixin: Mixin | Ruleset,
|
|
352
|
+
keys: SelectorKeySource
|
|
353
|
+
): void {
|
|
354
|
+
let candidateKeys = getIndexableSelectorKeys(keys);
|
|
355
|
+
for (let i = 0; i < candidateKeys.length; i++) {
|
|
356
|
+
const startKey = candidateKeys[i]!;
|
|
357
|
+
const rest = candidateKeys.filter(k => k !== startKey);
|
|
358
|
+
const existing = index.get(startKey);
|
|
359
|
+
if (existing) {
|
|
360
|
+
existing.push({ value: mixin, match: rest });
|
|
361
|
+
} else {
|
|
362
|
+
index.set(startKey, [{ value: mixin, match: rest }]);
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
export abstract class Registry<
|
|
368
|
+
Type extends Node,
|
|
369
|
+
IndexType extends Type | Set<Type> | Array<{
|
|
370
|
+
value: Type;
|
|
371
|
+
[key: string]: any;
|
|
372
|
+
}> = Set<Type>
|
|
373
|
+
> {
|
|
374
|
+
abstract index: Map<string, IndexType>;
|
|
375
|
+
protected pendingItems = new Set<Type>();
|
|
376
|
+
|
|
377
|
+
constructor(
|
|
378
|
+
public rules: Rules,
|
|
379
|
+
protected context?: Context
|
|
380
|
+
) {}
|
|
381
|
+
|
|
382
|
+
add(item: Type): void {
|
|
383
|
+
this.pendingItems.add(item);
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
indexPendingItems() {
|
|
387
|
+
if (this.pendingItems.size === 0) {
|
|
388
|
+
return;
|
|
389
|
+
}
|
|
390
|
+
for (const item of this.pendingItems) {
|
|
391
|
+
let key = String(item.valueOf());
|
|
392
|
+
let set = this.index.get(key);
|
|
393
|
+
if (set && set instanceof Set) {
|
|
394
|
+
set.add(item);
|
|
395
|
+
} else {
|
|
396
|
+
this.index.set(key, new Set([item]));
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
this.pendingItems.clear();
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
_searchRulesChildren(
|
|
403
|
+
key: string,
|
|
404
|
+
filterType: 'VarDeclaration' | 'Declaration' | 'Mixin',
|
|
405
|
+
options: FindOptions = {}
|
|
406
|
+
) {
|
|
407
|
+
let rules = this.rules;
|
|
408
|
+
// CRITICAL FIX: Initialize searchedRules if not provided, and add current Rules BEFORE any recursive calls
|
|
409
|
+
// The flaw in the original algorithm: when _searchRulesChildren calls childRules.find(), that creates
|
|
410
|
+
// a new search context via DeclarationRegistry.find(), which may not preserve searchedRules tracking.
|
|
411
|
+
// By initializing it here and adding the current Rules immediately, we ensure tracking persists.
|
|
412
|
+
const searchedRules = options?.searchedRules ?? new Set<Rules>();
|
|
413
|
+
if (!searchedRules.has(rules)) {
|
|
414
|
+
searchedRules.add(rules);
|
|
415
|
+
}
|
|
416
|
+
let findType = filterType === 'Mixin' ? 'mixin' as const : 'declaration' as const;
|
|
417
|
+
let findAll = Boolean(options.findAll);
|
|
418
|
+
let {
|
|
419
|
+
candidates = new Set(),
|
|
420
|
+
start,
|
|
421
|
+
readonly,
|
|
422
|
+
local,
|
|
423
|
+
childFilterType,
|
|
424
|
+
context
|
|
425
|
+
} = options;
|
|
426
|
+
// childFilterType is the filterType to use when calling child Rules.find
|
|
427
|
+
// If not provided, use filterType (for backward compatibility with DeclarationRegistry)
|
|
428
|
+
// Note: childFilterType can be undefined to mean "don't filter" (accept both Mixin and Ruleset)
|
|
429
|
+
const actualChildFilterType = 'childFilterType' in options ? childFilterType : filterType;
|
|
430
|
+
let firstValue = candidates.values().next().value;
|
|
431
|
+
rules.ensureCurrentRenderRulesRegistered(context);
|
|
432
|
+
if (rules._rulesSet) {
|
|
433
|
+
const { rulesSet } = rules;
|
|
434
|
+
const length = rulesSet.length;
|
|
435
|
+
if (length) {
|
|
436
|
+
// Create one shared child options object, reused across loop iterations
|
|
437
|
+
const childOpts: FindOptions = options
|
|
438
|
+
? {
|
|
439
|
+
...options,
|
|
440
|
+
searchParents: false,
|
|
441
|
+
start: undefined,
|
|
442
|
+
searchedRules,
|
|
443
|
+
context
|
|
444
|
+
}
|
|
445
|
+
: { searchParents: false, start: undefined, readonly, searchedRules, context };
|
|
446
|
+
const optionalCandidates = options?.optionalCandidates;
|
|
447
|
+
const isComparisonContext = firstValue && candidates.size > 0;
|
|
448
|
+
const hasTarget = options?.hasTarget === true;
|
|
449
|
+
|
|
450
|
+
// searchedRules is already initialized above and includes the current Rules
|
|
451
|
+
// Inline the filter logic into the loop to avoid creating an intermediate array
|
|
452
|
+
for (let i = length - 1; i >= 0; i--) {
|
|
453
|
+
let r = rulesSet.at(i)!;
|
|
454
|
+
// --- inline filter logic ---
|
|
455
|
+
const entryVisibility = r.rulesVisibility?.[filterType];
|
|
456
|
+
const nodeVisibility = r.node.options.rulesVisibility?.[filterType];
|
|
457
|
+
const visibility = entryVisibility ?? nodeVisibility;
|
|
458
|
+
const isMixinOutput = r.node.options?.isMixinOutput === true;
|
|
459
|
+
if (isMixinOutput && !hasTarget) {
|
|
460
|
+
continue;
|
|
461
|
+
}
|
|
462
|
+
if (!isMixinOutput) {
|
|
463
|
+
const isVisible = visibility === 'public' || visibility === 'optional';
|
|
464
|
+
if (!isVisible) {
|
|
465
|
+
continue;
|
|
466
|
+
}
|
|
467
|
+
if (r.node.options?.forward && context?.rulesContext === rules) {
|
|
468
|
+
continue;
|
|
469
|
+
}
|
|
470
|
+
if (local && r.node.options?.local) {
|
|
471
|
+
continue;
|
|
472
|
+
}
|
|
473
|
+
if (!(findAll || !firstValue || isComparisonContext)) {
|
|
474
|
+
continue;
|
|
475
|
+
}
|
|
476
|
+
if (start !== undefined && r.node.index >= start) {
|
|
477
|
+
continue;
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
// --- end inline filter logic ---
|
|
481
|
+
// Skip if we've already searched this Rules node to prevent infinite recursion
|
|
482
|
+
if (searchedRules && searchedRules.has(r.node)) {
|
|
483
|
+
continue;
|
|
484
|
+
}
|
|
485
|
+
if (r.node === rules) {
|
|
486
|
+
throw new Error(`Rules node contains itself in rulesSet`);
|
|
487
|
+
}
|
|
488
|
+
// Update per-iteration fields on the shared object
|
|
489
|
+
childOpts.readonly = readonly || r.readonly;
|
|
490
|
+
childOpts.local = local || Boolean(r.node.options?.local);
|
|
491
|
+
// Use actualChildFilterType which may be undefined for mixin-ruleset lookups
|
|
492
|
+
// filterType parameter is used to SELECT registry, actualChildFilterType is used to FILTER results
|
|
493
|
+
let result = r.node.find(findType, key, actualChildFilterType, childOpts);
|
|
494
|
+
if (result) {
|
|
495
|
+
// Check if this Rules has optional visibility (from RulesEntry or the actual Rules node)
|
|
496
|
+
const entryVisibility = r.rulesVisibility?.[filterType];
|
|
497
|
+
const nodeVisibility = r.node.options.rulesVisibility?.[filterType];
|
|
498
|
+
const isOptional = entryVisibility === 'optional' || nodeVisibility === 'optional';
|
|
499
|
+
|
|
500
|
+
const isPublic = entryVisibility === 'public' || nodeVisibility === 'public';
|
|
501
|
+
if (!findAll && isPublic) {
|
|
502
|
+
if (options && childOpts.readonly) {
|
|
503
|
+
options.readonly = true;
|
|
504
|
+
}
|
|
505
|
+
if (isArray(result)) {
|
|
506
|
+
for (const node of result) {
|
|
507
|
+
candidates.add(node);
|
|
508
|
+
}
|
|
509
|
+
} else {
|
|
510
|
+
candidates.add(result);
|
|
511
|
+
}
|
|
512
|
+
break;
|
|
513
|
+
}
|
|
514
|
+
if (options) {
|
|
515
|
+
options.readonly ||= childOpts.readonly;
|
|
516
|
+
}
|
|
517
|
+
if (isArray(result)) {
|
|
518
|
+
for (const node of result) {
|
|
519
|
+
if (isOptional && optionalCandidates) {
|
|
520
|
+
optionalCandidates.add(node);
|
|
521
|
+
} else {
|
|
522
|
+
candidates.add(node);
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
} else {
|
|
526
|
+
if (isOptional && optionalCandidates) {
|
|
527
|
+
optionalCandidates.add(result);
|
|
528
|
+
} else {
|
|
529
|
+
candidates.add(result);
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
// REMOVED: Manual iteration through rules.value for child Rules nodes
|
|
537
|
+
// If a Rules node is in rules.value and should be searchable, it should be registered
|
|
538
|
+
// via registerNode() which adds it to rulesSet. We already search rulesSet above.
|
|
539
|
+
// Manually iterating through rules.value creates infinite loops when a Rules node
|
|
540
|
+
// appears in its own children, and is unnecessary since registered Rules are in rulesSet.
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
/**
|
|
544
|
+
* Find the closest declaration from start, in reverse order,
|
|
545
|
+
* using a binary search
|
|
546
|
+
*/
|
|
547
|
+
_findClosestByStart(list: Type[], start?: number) {
|
|
548
|
+
if (start === undefined) {
|
|
549
|
+
return atIndex(list, -1);
|
|
550
|
+
}
|
|
551
|
+
/**
|
|
552
|
+
* We do this so we start looking above the given position and don't
|
|
553
|
+
* return the current node.
|
|
554
|
+
*/
|
|
555
|
+
start -= 1;
|
|
556
|
+
let bestMatch: number | undefined;
|
|
557
|
+
|
|
558
|
+
/** Binary search the queue to find a starting position */
|
|
559
|
+
let left = 0;
|
|
560
|
+
let right = list.length - 1;
|
|
561
|
+
|
|
562
|
+
while (left <= right) {
|
|
563
|
+
let mid = Math.floor((left + right) / 2);
|
|
564
|
+
let midVal = list.at(mid)!.index;
|
|
565
|
+
if (midVal === start) {
|
|
566
|
+
bestMatch = mid;
|
|
567
|
+
break;
|
|
568
|
+
}
|
|
569
|
+
if (midVal < start) {
|
|
570
|
+
bestMatch = mid;
|
|
571
|
+
left = mid + 1;
|
|
572
|
+
} else {
|
|
573
|
+
right = mid - 1;
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
return bestMatch !== undefined ? list.at(bestMatch) : undefined;
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
private _findByKey(candidates: Set<Type> | Type | undefined, key: string): Set<Type> | Type | undefined {
|
|
581
|
+
let set = this.index.get(key);
|
|
582
|
+
if (set) {
|
|
583
|
+
let newSet: Set<Type> | undefined;
|
|
584
|
+
if (set instanceof Set) {
|
|
585
|
+
newSet = set;
|
|
586
|
+
} else if (isArray(set)) {
|
|
587
|
+
newSet = new Set(set.map(({ value }) => value));
|
|
588
|
+
} else {
|
|
589
|
+
return set;
|
|
590
|
+
}
|
|
591
|
+
if (candidates) {
|
|
592
|
+
if (candidates instanceof Set) {
|
|
593
|
+
// Avoid Set.prototype.union (not available in our TS lib target)
|
|
594
|
+
for (const v of newSet) {
|
|
595
|
+
candidates.add(v);
|
|
596
|
+
}
|
|
597
|
+
} else {
|
|
598
|
+
candidates = new Set([candidates, ...newSet]);
|
|
599
|
+
}
|
|
600
|
+
} else {
|
|
601
|
+
candidates = newSet;
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
return candidates;
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
find(keys: string | string[] | Set<string>, _filterType?: string, _options?: FindOptions): Type[] | Type | Array<{ value: Type; [key: string]: any }> | undefined {
|
|
608
|
+
this.indexPendingItems();
|
|
609
|
+
let candidates: Set<Type> | Type | undefined;
|
|
610
|
+
if (isArray(keys) || keys instanceof Set) {
|
|
611
|
+
for (const key of keys) {
|
|
612
|
+
candidates = this._findByKey(candidates, key);
|
|
613
|
+
}
|
|
614
|
+
} else {
|
|
615
|
+
candidates = this._findByKey(candidates, keys);
|
|
616
|
+
}
|
|
617
|
+
if (candidates instanceof Set) {
|
|
618
|
+
return candidates.size ? [...candidates] : undefined;
|
|
619
|
+
}
|
|
620
|
+
return candidates;
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
/**
|
|
625
|
+
* Registry for fast selector-based ruleset lookups
|
|
626
|
+
*/
|
|
627
|
+
export class RulesetRegistry extends Registry<Ruleset> {
|
|
628
|
+
private _index = new Map<string, Set<Ruleset>>();
|
|
629
|
+
get index(): Map<string, Set<Ruleset>> {
|
|
630
|
+
return this._index;
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
/**
|
|
634
|
+
* Add a ruleset to be indexed later
|
|
635
|
+
*/
|
|
636
|
+
override add(ruleset: Ruleset) {
|
|
637
|
+
addRulesetToIndex(this.index, this.rules, ruleset, this.context);
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
/**
|
|
641
|
+
* Index any pending rulesets
|
|
642
|
+
* Override the base class method to use keySet-based indexing
|
|
643
|
+
*/
|
|
644
|
+
override indexPendingItems() {
|
|
645
|
+
return;
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
/**
|
|
649
|
+
* Find candidate rulesets that might match the target selector.
|
|
650
|
+
* Searches only the local index - all rulesets should be registered
|
|
651
|
+
* to the extend root's registry during evaluation.
|
|
652
|
+
*/
|
|
653
|
+
override find(keys: string[] | Set<string>): Ruleset[] | undefined {
|
|
654
|
+
// Index any pending rulesets first
|
|
655
|
+
this.indexPendingItems();
|
|
656
|
+
|
|
657
|
+
let candidates: Set<Ruleset> | undefined;
|
|
658
|
+
let rulesets: Ruleset[] | undefined;
|
|
659
|
+
|
|
660
|
+
/** Just get based on first key */
|
|
661
|
+
const indices = [this.index];
|
|
662
|
+
for (const key of keys) {
|
|
663
|
+
for (const index of indices) {
|
|
664
|
+
const set = index.get(key);
|
|
665
|
+
if (set) {
|
|
666
|
+
candidates ??= new Set<Ruleset>();
|
|
667
|
+
for (const candidate of set) {
|
|
668
|
+
candidates.add(candidate);
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
break;
|
|
673
|
+
}
|
|
674
|
+
if (!candidates) {
|
|
675
|
+
return undefined;
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
/** Now find selectors that have all keys */
|
|
679
|
+
const searchKeys = keys instanceof Set ? [...keys] : keys;
|
|
680
|
+
let searchKeySet = keys instanceof Set ? keys : new Set(keys);
|
|
681
|
+
let searchBitSet: BitSet<string> | undefined;
|
|
682
|
+
for (const c of candidates) {
|
|
683
|
+
let sel = c.get('selector');
|
|
684
|
+
if (!sel || isNode(sel, N.Nil)) {
|
|
685
|
+
continue;
|
|
686
|
+
}
|
|
687
|
+
let isSubset: boolean;
|
|
688
|
+
const selectorKeySet = sel.keySet;
|
|
689
|
+
if (!(selectorKeySet instanceof Set) && selectorKeySet._library) {
|
|
690
|
+
searchBitSet ??= selectorKeySet._library.getBitset(searchKeySet);
|
|
691
|
+
isSubset = isSubsetOf(searchBitSet, selectorKeySet);
|
|
692
|
+
} else {
|
|
693
|
+
isSubset = true;
|
|
694
|
+
for (const k of searchKeys) {
|
|
695
|
+
if (!hasSelectorKey(selectorKeySet, k)) {
|
|
696
|
+
isSubset = false;
|
|
697
|
+
break;
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
if (isSubset) {
|
|
702
|
+
(rulesets ??= []).push(c);
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
return rulesets;
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
/**
|
|
711
|
+
* The mixin registry works a little differently than the selector registry
|
|
712
|
+
* in these ways:
|
|
713
|
+
*
|
|
714
|
+
* 1. The mixin registry can only be indexed by basic element, class, and
|
|
715
|
+
* id selectors.
|
|
716
|
+
* 2. The index is the start key, not any key found in the selector.
|
|
717
|
+
* 3. '>' and ' ' combinators are ignored.
|
|
718
|
+
* 4. Initial ampersands (implicit or explicit) are ignored.
|
|
719
|
+
* 5. The mixin registry is local to the rules, whereas the selector registry
|
|
720
|
+
* is global to the file tree.
|
|
721
|
+
* 6. Rulesets and mixins without params will have their children searched
|
|
722
|
+
* if the first part matches.
|
|
723
|
+
*/
|
|
724
|
+
export class MixinRegistry extends Registry<
|
|
725
|
+
Mixin | Ruleset,
|
|
726
|
+
MixinRegistryEntry[]
|
|
727
|
+
> {
|
|
728
|
+
private _index = new Map<string, MixinRegistryEntry[]>();
|
|
729
|
+
get index(): Map<string, MixinRegistryEntry[]> {
|
|
730
|
+
return this._index;
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
// private getSimpleKeyList(selector: Selector | Nil | undefined): string[] | undefined {
|
|
734
|
+
// let keyList: string[] | undefined;
|
|
735
|
+
// if (selector && 'keySet' in selector) {
|
|
736
|
+
// let passed = true;
|
|
737
|
+
// let foundBasic = false;
|
|
738
|
+
// for (const sel of selector.nodes()) {
|
|
739
|
+
// /** Ampersand is okay at start, but not after a basic selector */
|
|
740
|
+
// if (!foundBasic && isNode(sel, 'Ampersand')) {
|
|
741
|
+
// continue;
|
|
742
|
+
// }
|
|
743
|
+
|
|
744
|
+
// if (isNode(sel, 'Combinator')) {
|
|
745
|
+
// if (sel.value !== '>' && sel.value !== ' ') {
|
|
746
|
+
// passed = false;
|
|
747
|
+
// break;
|
|
748
|
+
// }
|
|
749
|
+
// continue;
|
|
750
|
+
// }
|
|
751
|
+
|
|
752
|
+
// /** Anything other than a universal selector is fine */
|
|
753
|
+
// if (isNode(sel, 'BasicSelector') && /^[^*]/.test(sel.value)) {
|
|
754
|
+
// (keyList ??= []).push(sel.valueOf() as string);
|
|
755
|
+
// foundBasic = true;
|
|
756
|
+
// continue;
|
|
757
|
+
// }
|
|
758
|
+
// if (isNode(sel, 'CompoundSelector') || isNode(sel, 'ComplexSelector')) {
|
|
759
|
+
// /** Might still be fine */
|
|
760
|
+
// continue;
|
|
761
|
+
// }
|
|
762
|
+
// /** Nothing else is valid, so fail */
|
|
763
|
+
// passed = false;
|
|
764
|
+
// break;
|
|
765
|
+
// }
|
|
766
|
+
// if (!passed) {
|
|
767
|
+
// return;
|
|
768
|
+
// }
|
|
769
|
+
// }
|
|
770
|
+
// return keyList;
|
|
771
|
+
// }
|
|
772
|
+
|
|
773
|
+
private _indexSelectorStart(mixin: Ruleset | Mixin, keySet: SelectorKeySet | string[]) {
|
|
774
|
+
indexMixinSelectorStart(this.index, mixin, keySet);
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
/**
|
|
778
|
+
* For un-preEvaluated mixin rules, register child Rulesets/Mixins
|
|
779
|
+
* so namespace lookup can descend into them. Also propagate
|
|
780
|
+
* keySetLibrary so selectors can compute their keySets.
|
|
781
|
+
*/
|
|
782
|
+
private _ensureChildrenRegistered(rules: Rules, selectorBits?: BitSetLibrary<string>) {
|
|
783
|
+
for (const child of rules.getRegistryChildren(this.context)) {
|
|
784
|
+
if (isNode(child, N.Ruleset)) {
|
|
785
|
+
const sel = child.get('selector', this.context);
|
|
786
|
+
if (isSelectorLikeNode(sel) && selectorBits && !sel.keySetLibrary) {
|
|
787
|
+
sel.keySetLibrary = selectorBits;
|
|
788
|
+
const selValue = Reflect.get(sel, 'value');
|
|
789
|
+
if (isArray(selValue)) {
|
|
790
|
+
for (const sub of selValue) {
|
|
791
|
+
if (isSelectorLikeNode(sub) && !sub.keySetLibrary) {
|
|
792
|
+
sub.keySetLibrary = selectorBits;
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
}
|
|
797
|
+
rules.registerNode(child, undefined, this.context);
|
|
798
|
+
} else if (isNode(child, N.Mixin)) {
|
|
799
|
+
rules.registerNode(child, undefined, this.context);
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
override add(mixin: Mixin | Ruleset) {
|
|
805
|
+
addMixinToIndex(this.index, this.rules, mixin, this.context);
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
override indexPendingItems() {
|
|
809
|
+
return;
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
/**
|
|
813
|
+
* Check if an entry matches the search criteria.
|
|
814
|
+
* Handles exact matches, partial matches (compound selector completion), and recursive searches.
|
|
815
|
+
* This consolidates the matching logic to avoid duplication.
|
|
816
|
+
*/
|
|
817
|
+
/**
|
|
818
|
+
* Check if a Ruleset/Mixin matches a given array of keys using the same logic as the registry
|
|
819
|
+
* This uses the indexed match arrays (same as _checkEntryMatch) rather than direct selector comparison
|
|
820
|
+
* @param value The Ruleset or Mixin to check
|
|
821
|
+
* @param keys The array of keys to match against (e.g., [".jo", ".ki"])
|
|
822
|
+
* @returns true if the Ruleset/Mixin matches the keys using registry matching logic
|
|
823
|
+
*/
|
|
824
|
+
checkRulesetMatchesKeys(value: Mixin | Ruleset, keys: string[]): boolean {
|
|
825
|
+
if (!keys || keys.length === 0) {
|
|
826
|
+
return false;
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
// Get the selector's keySet and extract indexable keys (same as _indexSelectorStart)
|
|
830
|
+
let indexableKeys: string[] = [];
|
|
831
|
+
if (isNode(value, N.Ruleset)) {
|
|
832
|
+
const selector = value.get('selector', this.context);
|
|
833
|
+
if (isNode(selector, N.Nil)) {
|
|
834
|
+
return false;
|
|
835
|
+
}
|
|
836
|
+
if (isNode(selector, N.SelectorList)) {
|
|
837
|
+
// For selector lists, check if any selector matches
|
|
838
|
+
return selector.get('value').some((sel) => {
|
|
839
|
+
const selKeys = getIndexableSelectorKeys(tryGetSelectorKeySet(sel, false));
|
|
840
|
+
if (selKeys.length === 0) {
|
|
841
|
+
return false;
|
|
842
|
+
}
|
|
843
|
+
// Check if keys appear in sequence in this selector's keys
|
|
844
|
+
return this._checkKeysSubsequence(selKeys, keys);
|
|
845
|
+
});
|
|
846
|
+
}
|
|
847
|
+
const keySet = tryGetSelectorKeySet(selector, false);
|
|
848
|
+
if (!keySet || getSelectorKeyValues(keySet).length === 0) {
|
|
849
|
+
return false;
|
|
850
|
+
}
|
|
851
|
+
indexableKeys = getIndexableSelectorKeys(keySet);
|
|
852
|
+
} else {
|
|
853
|
+
const keySet = value.keySet;
|
|
854
|
+
if (!keySet || getSelectorKeyValues(keySet).length === 0) {
|
|
855
|
+
return false;
|
|
856
|
+
}
|
|
857
|
+
indexableKeys = getIndexableSelectorKeys(keySet);
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
if (indexableKeys.length === 0) {
|
|
861
|
+
return false;
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
// Check if the provided keys appear in sequence in the selector's indexable keys
|
|
865
|
+
// The keySet should only contain keys from the Ruleset's own selector, not parent context
|
|
866
|
+
return this._checkKeysSubsequence(indexableKeys, keys);
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
/**
|
|
870
|
+
* Internal helper that checks if the provided keys appear in sequence within the selector's keys
|
|
871
|
+
*
|
|
872
|
+
* For compound selectors like `#header .milk .chips .jo.ki`, when we search for `.jo`, we get:
|
|
873
|
+
* - The full selector's indexable keys: `["#header", ".milk", ".chips", ".jo", ".ki"]`
|
|
874
|
+
* - When checking if accumulated keys `[".jo", ".ki"]` match, we check if they appear in sequence
|
|
875
|
+
*/
|
|
876
|
+
private _checkKeysSubsequence(selectorKeys: string[], searchKeys: string[]): boolean {
|
|
877
|
+
if (searchKeys.length === 0) {
|
|
878
|
+
return false;
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
// Check if searchKeys is a subsequence of selectorKeys (searchKeys appear in order)
|
|
882
|
+
let searchIndex = 0;
|
|
883
|
+
for (const selectorKey of selectorKeys) {
|
|
884
|
+
if (searchIndex < searchKeys.length && selectorKey === searchKeys[searchIndex]) {
|
|
885
|
+
searchIndex++;
|
|
886
|
+
}
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
const matches = searchIndex === searchKeys.length;
|
|
890
|
+
return matches;
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
/**
|
|
894
|
+
* Find candidate mixins (or rulesets, or both) that might match the target selector
|
|
895
|
+
*
|
|
896
|
+
* ...also...
|
|
897
|
+
*
|
|
898
|
+
* @todo - Not sure how recursion works here with the match overflow and returning
|
|
899
|
+
* proper arrays.
|
|
900
|
+
*/
|
|
901
|
+
override find(
|
|
902
|
+
keys: string | string[],
|
|
903
|
+
filterType: 'Mixin' | 'Ruleset' | undefined = undefined,
|
|
904
|
+
options: FindOptions = {}
|
|
905
|
+
): (Mixin | Ruleset)[] | undefined {
|
|
906
|
+
let keyList: string[] | undefined;
|
|
907
|
+
|
|
908
|
+
if (isArray(keys)) {
|
|
909
|
+
keyList = keys;
|
|
910
|
+
} else {
|
|
911
|
+
keyList = [keys];
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
if (!keyList?.length) {
|
|
915
|
+
return;
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
let rules: Rules | undefined = this.rules;
|
|
919
|
+
let {
|
|
920
|
+
searchParents = true,
|
|
921
|
+
local = false,
|
|
922
|
+
candidates = new Set<Node>(),
|
|
923
|
+
context,
|
|
924
|
+
hasTarget = false
|
|
925
|
+
} = options ?? {};
|
|
926
|
+
const candidateContexts = options?.candidateContexts ?? new WeakMap<Node, Context>();
|
|
927
|
+
const getCandidateIdentity = (node: Node): Node => node.sourceNode ?? node;
|
|
928
|
+
const getContextRenderPriority = (ctx?: Context): number => {
|
|
929
|
+
const renderKey = ctx?.renderKey;
|
|
930
|
+
if (renderKey === undefined || renderKey === CANONICAL) {
|
|
931
|
+
return 0;
|
|
932
|
+
}
|
|
933
|
+
if (renderKey === EVAL) {
|
|
934
|
+
return 1;
|
|
935
|
+
}
|
|
936
|
+
return 2;
|
|
937
|
+
};
|
|
938
|
+
const getCandidateScore = (node: Node, ctx?: Context): [number, number, number] => {
|
|
939
|
+
const nonCanonicalParentEdgeKeys = node.parentEdges
|
|
940
|
+
? [...node.parentEdges.keys()].filter(key => key !== CANONICAL && key !== CALLER)
|
|
941
|
+
: [];
|
|
942
|
+
const ctxRenderKey = ctx?.renderKey;
|
|
943
|
+
const matchesContextKey = ctxRenderKey !== undefined && nonCanonicalParentEdgeKeys.includes(ctxRenderKey) ? 1 : 0;
|
|
944
|
+
const contextPriority = getContextRenderPriority(ctx);
|
|
945
|
+
const fewerEdgesScore = -nonCanonicalParentEdgeKeys.length;
|
|
946
|
+
return [matchesContextKey, contextPriority, fewerEdgesScore];
|
|
947
|
+
};
|
|
948
|
+
const rememberCandidateContext = (node: Node): void => {
|
|
949
|
+
if (!context || !rules) {
|
|
950
|
+
return;
|
|
951
|
+
}
|
|
952
|
+
const identity = getCandidateIdentity(node);
|
|
953
|
+
const nextContext: Context = {
|
|
954
|
+
...context,
|
|
955
|
+
rulesContext: rules
|
|
956
|
+
};
|
|
957
|
+
const existingContext = candidateContexts.get(identity);
|
|
958
|
+
if (
|
|
959
|
+
!existingContext
|
|
960
|
+
|| (
|
|
961
|
+
existingContext.renderKey === CANONICAL
|
|
962
|
+
&& nextContext.renderKey !== CANONICAL
|
|
963
|
+
)
|
|
964
|
+
|| (
|
|
965
|
+
existingContext.renderKey === undefined
|
|
966
|
+
&& nextContext.renderKey !== undefined
|
|
967
|
+
)
|
|
968
|
+
) {
|
|
969
|
+
candidateContexts.set(identity, nextContext);
|
|
970
|
+
}
|
|
971
|
+
};
|
|
972
|
+
const addCandidate = (node: Node): void => {
|
|
973
|
+
const sourceNode = node.sourceNode ?? node;
|
|
974
|
+
for (const existing of candidates) {
|
|
975
|
+
const existingNode = existing;
|
|
976
|
+
if ((existingNode.sourceNode ?? existingNode) !== sourceNode) {
|
|
977
|
+
continue;
|
|
978
|
+
}
|
|
979
|
+
const existingIdentity = getCandidateIdentity(existingNode);
|
|
980
|
+
const existingContext = candidateContexts.get(existingIdentity);
|
|
981
|
+
const nextContext = context && rules
|
|
982
|
+
? {
|
|
983
|
+
...context,
|
|
984
|
+
rulesContext: rules
|
|
985
|
+
}
|
|
986
|
+
: context;
|
|
987
|
+
const existingScore = getCandidateScore(existingNode, existingContext);
|
|
988
|
+
const nextScore = getCandidateScore(node, nextContext);
|
|
989
|
+
rememberCandidateContext(existingNode);
|
|
990
|
+
if (
|
|
991
|
+
existingScore[0] > nextScore[0]
|
|
992
|
+
|| (existingScore[0] === nextScore[0] && existingScore[1] > nextScore[1])
|
|
993
|
+
|| (existingScore[0] === nextScore[0] && existingScore[1] === nextScore[1] && existingScore[2] >= nextScore[2])
|
|
994
|
+
) {
|
|
995
|
+
return;
|
|
996
|
+
}
|
|
997
|
+
candidates.delete(existingNode);
|
|
998
|
+
break;
|
|
999
|
+
}
|
|
1000
|
+
if (context && rules) {
|
|
1001
|
+
setParent(node, rules, context);
|
|
1002
|
+
rememberCandidateContext(node);
|
|
1003
|
+
}
|
|
1004
|
+
(candidates ??= new Set()).add(node);
|
|
1005
|
+
};
|
|
1006
|
+
const mixinHasNoRequiredParams = (mixinNode: Mixin): boolean => {
|
|
1007
|
+
const params = mixinNode.get('params');
|
|
1008
|
+
if (!params || params.length === 0) {
|
|
1009
|
+
return true;
|
|
1010
|
+
}
|
|
1011
|
+
for (const param of params.get('value')) {
|
|
1012
|
+
if (param.type === 'Rest') {
|
|
1013
|
+
continue;
|
|
1014
|
+
}
|
|
1015
|
+
if (isNode(param, N.VarDeclaration)) {
|
|
1016
|
+
if (param.get('value') instanceof Nil) {
|
|
1017
|
+
return false;
|
|
1018
|
+
}
|
|
1019
|
+
continue;
|
|
1020
|
+
}
|
|
1021
|
+
if (isNode(param, N.Any) && param.role === 'property') {
|
|
1022
|
+
return false;
|
|
1023
|
+
}
|
|
1024
|
+
return false;
|
|
1025
|
+
}
|
|
1026
|
+
return true;
|
|
1027
|
+
};
|
|
1028
|
+
const getDescendContext = (node: Node, baseContext?: Context): Context | undefined => {
|
|
1029
|
+
if (!baseContext) {
|
|
1030
|
+
return undefined;
|
|
1031
|
+
}
|
|
1032
|
+
let nodeRenderKey = node.renderKey;
|
|
1033
|
+
if (nodeRenderKey === undefined || nodeRenderKey === CANONICAL || nodeRenderKey === baseContext.renderKey) {
|
|
1034
|
+
const nonCanonicalParentEdgeKeys = node.parentEdges
|
|
1035
|
+
? [...node.parentEdges.keys()].filter(key => key !== CANONICAL && key !== CALLER)
|
|
1036
|
+
: [];
|
|
1037
|
+
if (nonCanonicalParentEdgeKeys.length === 1) {
|
|
1038
|
+
nodeRenderKey = nonCanonicalParentEdgeKeys[0]!;
|
|
1039
|
+
}
|
|
1040
|
+
}
|
|
1041
|
+
if (
|
|
1042
|
+
nodeRenderKey === undefined
|
|
1043
|
+
|| nodeRenderKey === CANONICAL
|
|
1044
|
+
|| nodeRenderKey === baseContext.renderKey
|
|
1045
|
+
) {
|
|
1046
|
+
return baseContext;
|
|
1047
|
+
}
|
|
1048
|
+
return {
|
|
1049
|
+
...baseContext,
|
|
1050
|
+
renderKey: nodeRenderKey
|
|
1051
|
+
};
|
|
1052
|
+
};
|
|
1053
|
+
|
|
1054
|
+
// Track which Rules nodes we've already searched to prevent infinite recursion
|
|
1055
|
+
// Use the searchedRules from options if it exists, otherwise create a new Set
|
|
1056
|
+
let mixinChildSearchOpts: FindOptions | undefined;
|
|
1057
|
+
const searchedRules = options?.searchedRules || new Set<Rules>();
|
|
1058
|
+
if (options) {
|
|
1059
|
+
options.searchedRules = searchedRules;
|
|
1060
|
+
options.candidateContexts = candidateContexts;
|
|
1061
|
+
}
|
|
1062
|
+
while (rules) {
|
|
1063
|
+
// Don't add to searchedRules yet - we'll add it after we finish searching (including children)
|
|
1064
|
+
let [startKey, ...search] = keyList;
|
|
1065
|
+
const registry = rules.getRegistry('mixin', context);
|
|
1066
|
+
if (registry) {
|
|
1067
|
+
registry.indexPendingItems();
|
|
1068
|
+
}
|
|
1069
|
+
const mixinIndices = registry ? [registry.index] : [];
|
|
1070
|
+
const existing: MixinRegistryEntry[] = [];
|
|
1071
|
+
for (const index of mixinIndices) {
|
|
1072
|
+
const entries = index.get(startKey!);
|
|
1073
|
+
if (entries) {
|
|
1074
|
+
existing.push(...entries);
|
|
1075
|
+
}
|
|
1076
|
+
}
|
|
1077
|
+
// Resolve interpolated selector starts (e.g. "@{a2}") against current context
|
|
1078
|
+
// so unresolved-index keys can still match resolved call keys (e.g. ".foo").
|
|
1079
|
+
let resolvedInterpolatedStartEntries: Array<{ value: Mixin | Ruleset; match: string[] }> = [];
|
|
1080
|
+
if (context && typeof startKey === 'string' && existing.length === 0) {
|
|
1081
|
+
for (const index of mixinIndices) {
|
|
1082
|
+
for (const [indexedKey, indexedEntries] of index) {
|
|
1083
|
+
const matchInterpolated = /^@\{(.+)\}$/.exec(indexedKey);
|
|
1084
|
+
if (!matchInterpolated) {
|
|
1085
|
+
continue;
|
|
1086
|
+
}
|
|
1087
|
+
const varName = matchInterpolated[1]!;
|
|
1088
|
+
const maybeVar = rules.find('declaration', varName, 'VarDeclaration', {
|
|
1089
|
+
context,
|
|
1090
|
+
hasTarget,
|
|
1091
|
+
filter: options?.filter
|
|
1092
|
+
});
|
|
1093
|
+
if (isNode(maybeVar, N.VarDeclaration)) {
|
|
1094
|
+
const resolvedValue = String(maybeVar.get('value').valueOf?.() ?? maybeVar.get('value') ?? '');
|
|
1095
|
+
if (resolvedValue === startKey) {
|
|
1096
|
+
resolvedInterpolatedStartEntries.push(...indexedEntries);
|
|
1097
|
+
}
|
|
1098
|
+
}
|
|
1099
|
+
}
|
|
1100
|
+
}
|
|
1101
|
+
}
|
|
1102
|
+
|
|
1103
|
+
// With the new indexing (by local visible keys), nested rulesets are indexed under their own keys
|
|
1104
|
+
// So we only need to check entries under the startKey - no need to scan all entries
|
|
1105
|
+
let allEntriesToCheck: Array<{ value: Mixin | Ruleset; match: string[] }> = [];
|
|
1106
|
+
if (existing) {
|
|
1107
|
+
allEntriesToCheck.push(...existing);
|
|
1108
|
+
}
|
|
1109
|
+
if (resolvedInterpolatedStartEntries.length > 0) {
|
|
1110
|
+
allEntriesToCheck.push(...resolvedInterpolatedStartEntries);
|
|
1111
|
+
}
|
|
1112
|
+
|
|
1113
|
+
if (allEntriesToCheck.length > 0) {
|
|
1114
|
+
const targetMatch = search.length === 0 ? [startKey!] : search;
|
|
1115
|
+
const deferredExactMatches: Array<Mixin | Ruleset> = [];
|
|
1116
|
+
const candidateSizeBeforeEntries = candidates.size;
|
|
1117
|
+
for (const { value, match } of allEntriesToCheck) {
|
|
1118
|
+
if (filterType && value.type !== filterType) {
|
|
1119
|
+
continue;
|
|
1120
|
+
}
|
|
1121
|
+
|
|
1122
|
+
// If match equals targetMatch (search or [startKey] when search is empty), this IS the ruleset we're looking for
|
|
1123
|
+
// Also, if search is empty and match is empty, this ruleset IS the startKey we're looking for
|
|
1124
|
+
// BUT: For compound search paths (keyList.length > 1), we should NOT add the startKey mixin itself
|
|
1125
|
+
// as a candidate when search.length === 0 && match.length === 0, because that means we found the startKey
|
|
1126
|
+
// but haven't fully matched the compound path. The startKey should only be added as a candidate if we're
|
|
1127
|
+
// doing a simple lookup (keyList.length === 1), where the startKey IS the full match.
|
|
1128
|
+
if (arraysEqualAsSet(match, targetMatch)) {
|
|
1129
|
+
if (keyList.length > 1) {
|
|
1130
|
+
deferredExactMatches.push(value);
|
|
1131
|
+
} else {
|
|
1132
|
+
addCandidate(value);
|
|
1133
|
+
}
|
|
1134
|
+
continue;
|
|
1135
|
+
}
|
|
1136
|
+
// Only add startKey mixin as candidate if we're doing a simple lookup (not a compound path)
|
|
1137
|
+
if (search.length === 0 && match.length === 0 && keyList.length === 1) {
|
|
1138
|
+
addCandidate(value);
|
|
1139
|
+
continue;
|
|
1140
|
+
}
|
|
1141
|
+
// For compound paths, we don't add startKey as a candidate, but we still need to search inside it
|
|
1142
|
+
// The recursive search below will handle finding nested mixins
|
|
1143
|
+
|
|
1144
|
+
// If match equals [startKey] OR match is empty (meaning this ruleset IS the startKey),
|
|
1145
|
+
// we need to search inside it for the remaining search keys
|
|
1146
|
+
// NOTE: We should search inside #theme even if we're not adding it as a candidate (for compound paths)
|
|
1147
|
+
if (search.length > 0 && (arraysEqual(match, [startKey!]) || match.length === 0)) {
|
|
1148
|
+
const isRuleset = isNode(value, N.Ruleset);
|
|
1149
|
+
const isMixin = isNode(value, N.Mixin);
|
|
1150
|
+
const hasNoParams = isMixin && mixinHasNoRequiredParams(value);
|
|
1151
|
+
if (isRuleset || hasNoParams) {
|
|
1152
|
+
const subRules = isRuleset
|
|
1153
|
+
? value.enterRules(context)
|
|
1154
|
+
: value.get('rules', context).withRenderOwner(
|
|
1155
|
+
value,
|
|
1156
|
+
context?.renderKey,
|
|
1157
|
+
context
|
|
1158
|
+
);
|
|
1159
|
+
// Mixin rules aren't preEvaluated during registration — register
|
|
1160
|
+
// child rulesets/mixins now so namespace lookup can descend.
|
|
1161
|
+
// Always ensure children are registered for namespace descent —
|
|
1162
|
+
// preEvaluated children still need keySetLibrary on their selectors
|
|
1163
|
+
// for the mixin registry to index them.
|
|
1164
|
+
this._ensureChildrenRegistered(subRules, context?.selectorBits);
|
|
1165
|
+
const subMixinRegistry = subRules.getRegistry('mixin', context);
|
|
1166
|
+
subMixinRegistry?.indexPendingItems();
|
|
1167
|
+
subMixinRegistry?.find(search, filterType, {
|
|
1168
|
+
searchParents: false,
|
|
1169
|
+
local,
|
|
1170
|
+
candidates,
|
|
1171
|
+
context,
|
|
1172
|
+
candidateContexts,
|
|
1173
|
+
filter: options?.filter,
|
|
1174
|
+
hasTarget,
|
|
1175
|
+
searchedRules: undefined // Not needed when searchParents is false
|
|
1176
|
+
});
|
|
1177
|
+
}
|
|
1178
|
+
continue;
|
|
1179
|
+
}
|
|
1180
|
+
|
|
1181
|
+
// If there are more search keys than match keys, recursively search inside this ruleset
|
|
1182
|
+
// This handles cases where match is a prefix of search (e.g., match=[".foo"], search=[".foo", ".bar"])
|
|
1183
|
+
// Or when match is empty (ruleset IS the startKey) and we need to search inside for the full search
|
|
1184
|
+
const shouldRecurse = search.length > 0 && (search.length > match.length || match.length === 0);
|
|
1185
|
+
const matchKeysInSearch = match.length > 0 && arrayContainsAll(search, match);
|
|
1186
|
+
if (shouldRecurse) {
|
|
1187
|
+
let searchKeys: string[];
|
|
1188
|
+
if (match.length === 0) {
|
|
1189
|
+
// Match is empty, meaning this ruleset IS the startKey, search inside for the full search
|
|
1190
|
+
searchKeys = search;
|
|
1191
|
+
} else if (matchKeysInSearch) {
|
|
1192
|
+
// Match keys are all contained in search — remove them (set-based) to get the remainder
|
|
1193
|
+
const matchSet = new Set(match);
|
|
1194
|
+
searchKeys = search.filter(k => !matchSet.has(k));
|
|
1195
|
+
} else {
|
|
1196
|
+
// Match is not a prefix of search - skip this ruleset, it doesn't match
|
|
1197
|
+
continue;
|
|
1198
|
+
}
|
|
1199
|
+
const isRuleset = isNode(value, N.Ruleset);
|
|
1200
|
+
const isMixin = isNode(value, N.Mixin);
|
|
1201
|
+
const hasNoParams = isMixin && mixinHasNoRequiredParams(value);
|
|
1202
|
+
if (isRuleset || hasNoParams) {
|
|
1203
|
+
const subRules = isRuleset
|
|
1204
|
+
? value.enterRules(context)
|
|
1205
|
+
: value.get('rules', context).withRenderOwner(
|
|
1206
|
+
value,
|
|
1207
|
+
context?.renderKey,
|
|
1208
|
+
context
|
|
1209
|
+
);
|
|
1210
|
+
this._ensureChildrenRegistered(subRules, context?.selectorBits);
|
|
1211
|
+
const subMixinRegistry = subRules.getRegistry('mixin', context);
|
|
1212
|
+
subMixinRegistry?.indexPendingItems();
|
|
1213
|
+
subMixinRegistry?.find(searchKeys, filterType, {
|
|
1214
|
+
searchParents: false,
|
|
1215
|
+
local,
|
|
1216
|
+
candidates,
|
|
1217
|
+
context,
|
|
1218
|
+
candidateContexts,
|
|
1219
|
+
filter: options?.filter,
|
|
1220
|
+
hasTarget,
|
|
1221
|
+
searchedRules: searchedRules
|
|
1222
|
+
});
|
|
1223
|
+
}
|
|
1224
|
+
}
|
|
1225
|
+
}
|
|
1226
|
+
|
|
1227
|
+
if (keyList.length > 1 && candidates.size === candidateSizeBeforeEntries) {
|
|
1228
|
+
for (const candidate of deferredExactMatches) {
|
|
1229
|
+
addCandidate(candidate);
|
|
1230
|
+
}
|
|
1231
|
+
}
|
|
1232
|
+
}
|
|
1233
|
+
|
|
1234
|
+
// Track which candidates existed before searching children (by snapshot of the set)
|
|
1235
|
+
const candidateSizeBefore = candidates ? candidates.size : 0;
|
|
1236
|
+
const candidatesBefore = candidateSizeBefore > 0 ? new Set(candidates) : undefined;
|
|
1237
|
+
// Reuse a single child search options object
|
|
1238
|
+
// For compound paths (keyList.length > 1), the first segment acts as a namespace
|
|
1239
|
+
// target — allow searching inside mixin output rulesets so that e.g.
|
|
1240
|
+
// `.Person("Male")` output containing `.person { .sayGender() {} }` is reachable.
|
|
1241
|
+
const childHasTarget = hasTarget || keyList.length > 1;
|
|
1242
|
+
if (!mixinChildSearchOpts) {
|
|
1243
|
+
mixinChildSearchOpts = {
|
|
1244
|
+
searchParents: false,
|
|
1245
|
+
local,
|
|
1246
|
+
candidates,
|
|
1247
|
+
findAll: true,
|
|
1248
|
+
childFilterType: filterType,
|
|
1249
|
+
context,
|
|
1250
|
+
filter: options?.filter,
|
|
1251
|
+
hasTarget: childHasTarget,
|
|
1252
|
+
searchedRules
|
|
1253
|
+
};
|
|
1254
|
+
} else {
|
|
1255
|
+
mixinChildSearchOpts.searchedRules = searchedRules;
|
|
1256
|
+
}
|
|
1257
|
+
registry?._searchRulesChildren(startKey!, 'Mixin', mixinChildSearchOpts);
|
|
1258
|
+
|
|
1259
|
+
// After _searchRulesChildren, check if any new candidates are mixins/rulesets we should search inside
|
|
1260
|
+
// This handles the case where #theme mixin is found in imported Rules and we need to search inside it
|
|
1261
|
+
// Also, for compound paths, remove #theme from candidates if it was added by _searchRulesChildren
|
|
1262
|
+
// because we only want to search inside it, not include it as a final candidate
|
|
1263
|
+
if (candidates && candidates.size > candidateSizeBefore) {
|
|
1264
|
+
const candidatesToRemove: (Mixin | Ruleset)[] = [];
|
|
1265
|
+
for (const candidate of candidates) {
|
|
1266
|
+
const candidateNode = candidate;
|
|
1267
|
+
// Only check candidates that were added by _searchRulesChildren (not in original set)
|
|
1268
|
+
if (candidatesBefore && candidatesBefore.has(candidateNode)) {
|
|
1269
|
+
continue;
|
|
1270
|
+
}
|
|
1271
|
+
{
|
|
1272
|
+
const isMixin = isNode(candidateNode, N.Mixin);
|
|
1273
|
+
const isRuleset = isNode(candidateNode, N.Ruleset);
|
|
1274
|
+
const hasNoParams = isMixin && mixinHasNoRequiredParams(candidateNode);
|
|
1275
|
+
// Check if this candidate matches the startKey.
|
|
1276
|
+
// For rulesets discovered via child-search, key-set membership is the reliable signal.
|
|
1277
|
+
const candidateKey = isMixin
|
|
1278
|
+
? candidateNode.get('name')?.valueOf?.()
|
|
1279
|
+
: (isRuleset ? candidateNode.get('selector').valueOf?.() : '');
|
|
1280
|
+
const candidateSelector = isRuleset
|
|
1281
|
+
? candidateNode.get('selector')
|
|
1282
|
+
: undefined;
|
|
1283
|
+
const candidateVisibleKeySet = candidateSelector && !isNode(candidateSelector, N.Nil)
|
|
1284
|
+
? tryGetSelectorKeySet(candidateSelector, true)
|
|
1285
|
+
: undefined;
|
|
1286
|
+
const candidateKeySet = candidateSelector && !isNode(candidateSelector, N.Nil)
|
|
1287
|
+
? tryGetSelectorKeySet(candidateSelector, false)
|
|
1288
|
+
: undefined;
|
|
1289
|
+
const matchesStartKey = isRuleset
|
|
1290
|
+
? (
|
|
1291
|
+
hasSelectorKey(candidateVisibleKeySet, startKey!)
|
|
1292
|
+
|| hasSelectorKey(candidateKeySet, startKey!)
|
|
1293
|
+
|| candidateKey === startKey
|
|
1294
|
+
)
|
|
1295
|
+
: candidateKey === startKey;
|
|
1296
|
+
|
|
1297
|
+
// For compound paths (keyList.length > 1), remove startKey from candidates if it was added by _searchRulesChildren
|
|
1298
|
+
// because we only want to search inside it, not include it as a final candidate
|
|
1299
|
+
if (matchesStartKey && keyList.length > 1) {
|
|
1300
|
+
candidatesToRemove.push(candidateNode);
|
|
1301
|
+
}
|
|
1302
|
+
|
|
1303
|
+
// Search inside the candidate if it matches startKey and we have remaining search keys
|
|
1304
|
+
if (matchesStartKey && search.length > 0 && (isRuleset || hasNoParams)) {
|
|
1305
|
+
const foundContext = candidateContexts.get(getCandidateIdentity(candidateNode));
|
|
1306
|
+
const descendContext = getDescendContext(candidateNode, foundContext ?? context);
|
|
1307
|
+
let subRules = isRuleset
|
|
1308
|
+
? candidateNode.enterRules(descendContext)
|
|
1309
|
+
: candidateNode.get('rules', descendContext).withRenderOwner(
|
|
1310
|
+
candidateNode,
|
|
1311
|
+
descendContext?.renderKey,
|
|
1312
|
+
descendContext
|
|
1313
|
+
);
|
|
1314
|
+
const searchContext = descendContext
|
|
1315
|
+
? { ...descendContext, rulesContext: subRules }
|
|
1316
|
+
: undefined;
|
|
1317
|
+
const subMixinRegistry = subRules.getRegistry('mixin', searchContext);
|
|
1318
|
+
subMixinRegistry?.indexPendingItems();
|
|
1319
|
+
subMixinRegistry?.find(search, filterType, {
|
|
1320
|
+
searchParents: false,
|
|
1321
|
+
local,
|
|
1322
|
+
candidates,
|
|
1323
|
+
context: searchContext,
|
|
1324
|
+
candidateContexts,
|
|
1325
|
+
filter: options?.filter,
|
|
1326
|
+
hasTarget,
|
|
1327
|
+
searchedRules: undefined // Not needed when searchParents is false
|
|
1328
|
+
});
|
|
1329
|
+
}
|
|
1330
|
+
}
|
|
1331
|
+
}
|
|
1332
|
+
// Remove candidates that shouldn't be in the final result (for compound paths)
|
|
1333
|
+
for (const candidateToRemove of candidatesToRemove) {
|
|
1334
|
+
candidates.delete(candidateToRemove);
|
|
1335
|
+
}
|
|
1336
|
+
}
|
|
1337
|
+
|
|
1338
|
+
// Mark this Rules node as searched after we've finished searching it (including children)
|
|
1339
|
+
searchedRules.add(rules);
|
|
1340
|
+
|
|
1341
|
+
if (isNonImportStyleBoundary(rules)) {
|
|
1342
|
+
searchParents = false;
|
|
1343
|
+
}
|
|
1344
|
+
|
|
1345
|
+
if (!searchParents) {
|
|
1346
|
+
break;
|
|
1347
|
+
}
|
|
1348
|
+
do {
|
|
1349
|
+
rules = rules?.getRegistryParent(context);
|
|
1350
|
+
} while (rules && rules.type !== 'Rules');
|
|
1351
|
+
}
|
|
1352
|
+
|
|
1353
|
+
// With compound keys parsed as arrays (e.g., ['#theme', '.dark', '.navbar', '.colors']),
|
|
1354
|
+
// we can find all matches in one pass. The find() method handles compound keys by
|
|
1355
|
+
// recursively searching inside nested rulesets for the remaining keys.
|
|
1356
|
+
|
|
1357
|
+
return candidates.size ? [...candidates] : undefined;
|
|
1358
|
+
}
|
|
1359
|
+
}
|
|
1360
|
+
|
|
1361
|
+
/**
|
|
1362
|
+
* For either Sass, Jess, or JS functions.
|
|
1363
|
+
*
|
|
1364
|
+
* Less and Sass can register global functions that can be called from the language
|
|
1365
|
+
* without a `@-use` directive.
|
|
1366
|
+
*
|
|
1367
|
+
* @todo Should the presence of `@-use` directives anywhere in the
|
|
1368
|
+
* stylesheet tree cause these global functions to be disabled?
|
|
1369
|
+
*/
|
|
1370
|
+
export class FunctionRegistry extends Registry<JsFunction | Func, JsFunction | Func> {
|
|
1371
|
+
index = new Map<string, JsFunction | Func>();
|
|
1372
|
+
|
|
1373
|
+
cloneForRules(rules: Rules): FunctionRegistry {
|
|
1374
|
+
const next = new FunctionRegistry(rules);
|
|
1375
|
+
// Preserve any functions injected directly into the registry (Less plugin style).
|
|
1376
|
+
next.index = new Map(this.index);
|
|
1377
|
+
next.pendingItems = new Set(this.pendingItems);
|
|
1378
|
+
return next;
|
|
1379
|
+
}
|
|
1380
|
+
|
|
1381
|
+
override indexPendingItems() {
|
|
1382
|
+
if (this.pendingItems.size === 0) {
|
|
1383
|
+
return;
|
|
1384
|
+
}
|
|
1385
|
+
for (const item of this.pendingItems) {
|
|
1386
|
+
if (item instanceof JsFunction) {
|
|
1387
|
+
this.index.set(item.name!, item);
|
|
1388
|
+
continue;
|
|
1389
|
+
}
|
|
1390
|
+
// Stylesheet-defined function node
|
|
1391
|
+
const nameKey = (item as Func).nameKey;
|
|
1392
|
+
if (nameKey) {
|
|
1393
|
+
this.index.set(nameKey, item);
|
|
1394
|
+
}
|
|
1395
|
+
}
|
|
1396
|
+
this.pendingItems.clear();
|
|
1397
|
+
}
|
|
1398
|
+
|
|
1399
|
+
override find(name: string, filterType?: string, options?: FindOptions): JsFunction | Func | undefined {
|
|
1400
|
+
let fn: JsFunction | Func | undefined;
|
|
1401
|
+
let rules: Rules | undefined = this.rules;
|
|
1402
|
+
let { searchParents = true } = options ?? {};
|
|
1403
|
+
let findRoot = false;
|
|
1404
|
+
while (rules) {
|
|
1405
|
+
if (isNonImportStyleBoundary(rules)) {
|
|
1406
|
+
searchParents = false;
|
|
1407
|
+
}
|
|
1408
|
+
let registry = rules.functionRegistry;
|
|
1409
|
+
if (registry) {
|
|
1410
|
+
registry.indexPendingItems();
|
|
1411
|
+
fn = registry.index.get(name);
|
|
1412
|
+
|
|
1413
|
+
if (fn || !searchParents) {
|
|
1414
|
+
break;
|
|
1415
|
+
}
|
|
1416
|
+
}
|
|
1417
|
+
|
|
1418
|
+
do {
|
|
1419
|
+
rules = rules?.getRegistryParent(this.context);
|
|
1420
|
+
if (
|
|
1421
|
+
findRoot
|
|
1422
|
+
&& rules?.type === 'Rules'
|
|
1423
|
+
&& rules.getRegistryParent(this.context) === undefined
|
|
1424
|
+
) {
|
|
1425
|
+
/** We're at the root */
|
|
1426
|
+
break;
|
|
1427
|
+
}
|
|
1428
|
+
} while (!findRoot && rules && rules.type !== 'Rules');
|
|
1429
|
+
}
|
|
1430
|
+
|
|
1431
|
+
return fn;
|
|
1432
|
+
}
|
|
1433
|
+
|
|
1434
|
+
/**
|
|
1435
|
+
* Override add() to support both Jess API (add(item)) and Less.js API (add(name, func))
|
|
1436
|
+
*/
|
|
1437
|
+
override add(item: JsFunction | Func): void;
|
|
1438
|
+
override add(name: string, func: JsFunction | ((...args: any[]) => any)): void;
|
|
1439
|
+
override add(
|
|
1440
|
+
nameOrItem: string | JsFunction | Func,
|
|
1441
|
+
func?: JsFunction | ((...args: any[]) => any)
|
|
1442
|
+
): void {
|
|
1443
|
+
// If first argument is a JsFunction or Func, use base class behavior
|
|
1444
|
+
if (nameOrItem instanceof JsFunction || (typeof nameOrItem === 'object' && nameOrItem !== null && Reflect.get(nameOrItem, 'type') === 'Func')) {
|
|
1445
|
+
super.add(nameOrItem as JsFunction | Func);
|
|
1446
|
+
return;
|
|
1447
|
+
}
|
|
1448
|
+
|
|
1449
|
+
// Otherwise, it's Less.js-compatible API: add(name, func)
|
|
1450
|
+
if (typeof nameOrItem !== 'string' || func === undefined) {
|
|
1451
|
+
throw new Error('FunctionRegistry.add() requires either a JsFunction or (name: string, func: JsFunction | Function)');
|
|
1452
|
+
}
|
|
1453
|
+
|
|
1454
|
+
// Convert name to lowercase for Less.js compatibility
|
|
1455
|
+
const lowerName = nameOrItem.toLowerCase();
|
|
1456
|
+
|
|
1457
|
+
// If func is already a JsFunction, use it directly
|
|
1458
|
+
// Otherwise, create a new JsFunction from the raw function
|
|
1459
|
+
const jsFunc = func instanceof JsFunction
|
|
1460
|
+
? func
|
|
1461
|
+
: new JsFunction({ name: lowerName, fn: func });
|
|
1462
|
+
|
|
1463
|
+
// Ensure the name is set
|
|
1464
|
+
if (!jsFunc.name) {
|
|
1465
|
+
jsFunc.name = lowerName;
|
|
1466
|
+
}
|
|
1467
|
+
|
|
1468
|
+
// Add to pendingItems directly
|
|
1469
|
+
this.pendingItems.add(jsFunc);
|
|
1470
|
+
}
|
|
1471
|
+
|
|
1472
|
+
/**
|
|
1473
|
+
* Less.js-compatible API: Add multiple functions at once
|
|
1474
|
+
* @param functions Object mapping function names to functions
|
|
1475
|
+
*/
|
|
1476
|
+
addMultiple(functions: Record<string, JsFunction | ((...args: any[]) => any)>): void {
|
|
1477
|
+
for (const [name, func] of Object.entries(functions)) {
|
|
1478
|
+
this.add(name, func);
|
|
1479
|
+
}
|
|
1480
|
+
}
|
|
1481
|
+
|
|
1482
|
+
/**
|
|
1483
|
+
* Less.js-compatible API: Get a function by name
|
|
1484
|
+
* Uses case-insensitive lookup and searches parent chain
|
|
1485
|
+
* @param name Function name (case-insensitive)
|
|
1486
|
+
* @returns The function if found, undefined otherwise
|
|
1487
|
+
*/
|
|
1488
|
+
get(name: string): JsFunction | Func | undefined {
|
|
1489
|
+
// Convert to lowercase for case-insensitive lookup
|
|
1490
|
+
const lowerName = name.toLowerCase();
|
|
1491
|
+
|
|
1492
|
+
// First check local registry
|
|
1493
|
+
this.indexPendingItems();
|
|
1494
|
+
let fn = this.index.get(lowerName);
|
|
1495
|
+
|
|
1496
|
+
if (fn) {
|
|
1497
|
+
return fn;
|
|
1498
|
+
}
|
|
1499
|
+
|
|
1500
|
+
// If not found locally, use find() to search parent chain
|
|
1501
|
+
// find() already handles parent traversal
|
|
1502
|
+
return this.find(lowerName);
|
|
1503
|
+
}
|
|
1504
|
+
|
|
1505
|
+
/**
|
|
1506
|
+
* Less.js-compatible API: Get all local functions (without parent chain)
|
|
1507
|
+
* @returns Object mapping function names to functions
|
|
1508
|
+
*/
|
|
1509
|
+
getLocalFunctions(): Record<string, JsFunction | Func> {
|
|
1510
|
+
this.indexPendingItems();
|
|
1511
|
+
const result: Record<string, JsFunction | Func> = {};
|
|
1512
|
+
for (const [name, func] of this.index.entries()) {
|
|
1513
|
+
result[name] = func;
|
|
1514
|
+
}
|
|
1515
|
+
return result;
|
|
1516
|
+
}
|
|
1517
|
+
|
|
1518
|
+
/**
|
|
1519
|
+
* Less.js-compatible API: Create a child registry that inherits from this one
|
|
1520
|
+
* In Less.js, this creates a new registry with prototype inheritance.
|
|
1521
|
+
* In Jess, we create a new registry that searches this one as a parent.
|
|
1522
|
+
*
|
|
1523
|
+
* @returns A new FunctionRegistry that will search this registry when functions aren't found locally
|
|
1524
|
+
*/
|
|
1525
|
+
inherit(): FunctionRegistry {
|
|
1526
|
+
// Create a new registry for the same Rules
|
|
1527
|
+
// The new registry will use find() which searches parent chain
|
|
1528
|
+
// We need to create a registry that references this one as parent
|
|
1529
|
+
// Since FunctionRegistry.find() already searches parent Rules chain,
|
|
1530
|
+
// we can create a new registry on the same Rules and it will naturally
|
|
1531
|
+
// find functions in parent Rules. However, for true "inherit" behavior
|
|
1532
|
+
// where we want to search THIS registry specifically, we need a different approach.
|
|
1533
|
+
|
|
1534
|
+
// For now, create a new registry on the same Rules
|
|
1535
|
+
// The find() method will search up the Rules parent chain, which includes
|
|
1536
|
+
// this registry's Rules, so it should work correctly.
|
|
1537
|
+
const childRegistry = new FunctionRegistry(this.rules);
|
|
1538
|
+
|
|
1539
|
+
// Store reference to parent registry for direct lookup
|
|
1540
|
+
// This allows the child to search the parent registry even if it's on the same Rules
|
|
1541
|
+
Reflect.set(childRegistry, '_parentRegistry', this);
|
|
1542
|
+
|
|
1543
|
+
// Override get() to check parent registry first
|
|
1544
|
+
const originalGet = childRegistry.get.bind(childRegistry);
|
|
1545
|
+
childRegistry.get = function(this: FunctionRegistry, name: string): JsFunction | Func | undefined {
|
|
1546
|
+
// First check local registry
|
|
1547
|
+
this.indexPendingItems();
|
|
1548
|
+
const localFn = this.index.get(name.toLowerCase());
|
|
1549
|
+
if (localFn) {
|
|
1550
|
+
return localFn;
|
|
1551
|
+
}
|
|
1552
|
+
|
|
1553
|
+
// Then check parent registry
|
|
1554
|
+
const parentRegistry = Reflect.get(this, '_parentRegistry');
|
|
1555
|
+
if (parentRegistry instanceof FunctionRegistry) {
|
|
1556
|
+
const parentFn = parentRegistry.get(name);
|
|
1557
|
+
if (parentFn) {
|
|
1558
|
+
return parentFn;
|
|
1559
|
+
}
|
|
1560
|
+
}
|
|
1561
|
+
|
|
1562
|
+
// Finally, use find() to search Rules parent chain
|
|
1563
|
+
return originalGet(name);
|
|
1564
|
+
}.bind(childRegistry);
|
|
1565
|
+
|
|
1566
|
+
return childRegistry;
|
|
1567
|
+
}
|
|
1568
|
+
}
|
|
1569
|
+
|
|
1570
|
+
/**
|
|
1571
|
+
*
|
|
1572
|
+
* @note - Keys of different types may overlap, but then are filtered when searching.
|
|
1573
|
+
* As in, a variable named `$foo` and a property named `foo` will be in the
|
|
1574
|
+
* same map.
|
|
1575
|
+
*/
|
|
1576
|
+
export class DeclarationRegistry extends Registry<Declaration> {
|
|
1577
|
+
private _index = new Map<string, Set<Declaration>>();
|
|
1578
|
+
get index(): Map<string, Set<Declaration>> {
|
|
1579
|
+
return this._index;
|
|
1580
|
+
}
|
|
1581
|
+
|
|
1582
|
+
override add(item: Declaration): void {
|
|
1583
|
+
addDeclarationToIndex(this.index, item);
|
|
1584
|
+
}
|
|
1585
|
+
|
|
1586
|
+
override indexPendingItems() {
|
|
1587
|
+
return;
|
|
1588
|
+
}
|
|
1589
|
+
|
|
1590
|
+
/**
|
|
1591
|
+
* Get declarations from map and nested rulesets.
|
|
1592
|
+
* This will return a list of all matching nodes.
|
|
1593
|
+
*
|
|
1594
|
+
* @todo - The pattern for mixins will be similar, no? Can this be
|
|
1595
|
+
* re-used / abstracted?
|
|
1596
|
+
*
|
|
1597
|
+
* @todo - Register declarations and index them only when searching.
|
|
1598
|
+
* This would be similar to how we index rulesets for extending.
|
|
1599
|
+
*/
|
|
1600
|
+
override find(
|
|
1601
|
+
key: string,
|
|
1602
|
+
filterType: 'VarDeclaration' | 'Declaration' = 'VarDeclaration',
|
|
1603
|
+
options?: FindOptions
|
|
1604
|
+
): Declaration | undefined {
|
|
1605
|
+
const candidateContexts = options?.candidateContexts ?? new WeakMap<Node, Context>();
|
|
1606
|
+
const getCandidateIdentity = (node: Declaration): Node => node.sourceNode ?? node;
|
|
1607
|
+
const getDeclarationCandidateScore = (node: Declaration, activeRules: Rules): [number, number, number, number] => {
|
|
1608
|
+
const activeRenderKey = context?.renderKey ?? activeRules.renderKey;
|
|
1609
|
+
const nonCanonicalParentEdgeKeys = node.parentEdges
|
|
1610
|
+
? [...node.parentEdges.keys()].filter(key => key !== CANONICAL && key !== CALLER)
|
|
1611
|
+
: [];
|
|
1612
|
+
const matchesActiveKey = activeRenderKey !== undefined && nonCanonicalParentEdgeKeys.includes(activeRenderKey) ? 1 : 0;
|
|
1613
|
+
const isDerived = node !== getCandidateIdentity(node) ? 1 : 0;
|
|
1614
|
+
const isPreEvaluated = node.preEvaluated ? 1 : 0;
|
|
1615
|
+
const isEvaluated = node.evaluated ? 1 : 0;
|
|
1616
|
+
return [matchesActiveKey, isDerived, isPreEvaluated, isEvaluated];
|
|
1617
|
+
};
|
|
1618
|
+
const shouldReplaceCandidate = (existing: Declaration, next: Declaration, activeRules: Rules): boolean => {
|
|
1619
|
+
const existingScore = getDeclarationCandidateScore(existing, activeRules);
|
|
1620
|
+
const nextScore = getDeclarationCandidateScore(next, activeRules);
|
|
1621
|
+
for (let i = 0; i < existingScore.length; i++) {
|
|
1622
|
+
if (nextScore[i]! > existingScore[i]!) {
|
|
1623
|
+
return true;
|
|
1624
|
+
}
|
|
1625
|
+
if (nextScore[i]! < existingScore[i]!) {
|
|
1626
|
+
return false;
|
|
1627
|
+
}
|
|
1628
|
+
}
|
|
1629
|
+
return false;
|
|
1630
|
+
};
|
|
1631
|
+
const rememberCandidateContext = (node: Declaration, activeRules: Rules): void => {
|
|
1632
|
+
if (!context) {
|
|
1633
|
+
return;
|
|
1634
|
+
}
|
|
1635
|
+
setParent(node, activeRules, context);
|
|
1636
|
+
candidateContexts.set(node, {
|
|
1637
|
+
...context,
|
|
1638
|
+
rulesContext: activeRules,
|
|
1639
|
+
renderKey: context.renderKey ?? activeRules.renderKey
|
|
1640
|
+
});
|
|
1641
|
+
};
|
|
1642
|
+
const getCandidateContext = (node: Declaration, activeRules: Rules): Context | undefined => {
|
|
1643
|
+
return candidateContexts.get(node) ?? (
|
|
1644
|
+
context
|
|
1645
|
+
? {
|
|
1646
|
+
...context,
|
|
1647
|
+
rulesContext: activeRules
|
|
1648
|
+
}
|
|
1649
|
+
: context
|
|
1650
|
+
);
|
|
1651
|
+
};
|
|
1652
|
+
const getDeclarationOrderPath = (node: Declaration, boundaryRules: Rules): number[] | undefined => {
|
|
1653
|
+
const path: number[] = [];
|
|
1654
|
+
let current: Node | undefined = node;
|
|
1655
|
+
const nodeContext = getCandidateContext(node, boundaryRules);
|
|
1656
|
+
while (current) {
|
|
1657
|
+
const parent = nodeContext ? getParent(current, nodeContext) : current.parent;
|
|
1658
|
+
if (!parent) {
|
|
1659
|
+
return undefined;
|
|
1660
|
+
}
|
|
1661
|
+
if (current.index === undefined) {
|
|
1662
|
+
return undefined;
|
|
1663
|
+
}
|
|
1664
|
+
path.unshift(current.index);
|
|
1665
|
+
if (parent === boundaryRules) {
|
|
1666
|
+
return path;
|
|
1667
|
+
}
|
|
1668
|
+
if (!isNode(parent, N.Rules)) {
|
|
1669
|
+
return undefined;
|
|
1670
|
+
}
|
|
1671
|
+
current = parent;
|
|
1672
|
+
}
|
|
1673
|
+
return undefined;
|
|
1674
|
+
};
|
|
1675
|
+
const compareDeclarationsForLookup = (a: Declaration, b: Declaration): number => {
|
|
1676
|
+
const aPath = getDeclarationOrderPath(a, rules);
|
|
1677
|
+
const bPath = getDeclarationOrderPath(b, rules);
|
|
1678
|
+
if (aPath && bPath) {
|
|
1679
|
+
const length = Math.min(aPath.length, bPath.length);
|
|
1680
|
+
for (let i = 0; i < length; i++) {
|
|
1681
|
+
const diff = aPath[i]! - bPath[i]!;
|
|
1682
|
+
if (diff !== 0) {
|
|
1683
|
+
return diff;
|
|
1684
|
+
}
|
|
1685
|
+
}
|
|
1686
|
+
if (aPath.length !== bPath.length) {
|
|
1687
|
+
return aPath.length - bPath.length;
|
|
1688
|
+
}
|
|
1689
|
+
}
|
|
1690
|
+
const pos = comparePosition(a, b);
|
|
1691
|
+
return pos ?? 0;
|
|
1692
|
+
};
|
|
1693
|
+
let declCandidate = new Set<Declaration>();
|
|
1694
|
+
let optionalCandidates = options?.optionalCandidates ?? new Set<Declaration>();
|
|
1695
|
+
let rules: Rules | undefined = this.rules;
|
|
1696
|
+
let isPublic = false;
|
|
1697
|
+
let {
|
|
1698
|
+
searchParents = true,
|
|
1699
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
1700
|
+
local = false,
|
|
1701
|
+
start,
|
|
1702
|
+
context
|
|
1703
|
+
} = options ?? {};
|
|
1704
|
+
if (options) {
|
|
1705
|
+
options.candidateContexts = candidateContexts;
|
|
1706
|
+
}
|
|
1707
|
+
|
|
1708
|
+
let newReadonly: boolean | undefined = false;
|
|
1709
|
+
let searchChildrenOptions: FindOptions | undefined;
|
|
1710
|
+
// Track visited Rules nodes in the parent chain to detect circular parent chains
|
|
1711
|
+
const visitedRules = new Set<Rules>();
|
|
1712
|
+
while (rules) {
|
|
1713
|
+
// CRITICAL: Check for circular parent chain
|
|
1714
|
+
if (visitedRules.has(rules)) {
|
|
1715
|
+
throw new Error(`Circular parent chain detected in DeclarationRegistry.find`);
|
|
1716
|
+
}
|
|
1717
|
+
visitedRules.add(rules);
|
|
1718
|
+
let currentReadonly = options?.readonly || rules.options.readonly;
|
|
1719
|
+
newReadonly = currentReadonly;
|
|
1720
|
+
const invocationBinding = filterType === 'VarDeclaration'
|
|
1721
|
+
? rules.getInvocationBinding(key, context)
|
|
1722
|
+
: undefined;
|
|
1723
|
+
if (invocationBinding && (!options?.filter || options.filter(invocationBinding))) {
|
|
1724
|
+
rememberCandidateContext(invocationBinding, rules);
|
|
1725
|
+
newReadonly ||= invocationBinding.options.readonly;
|
|
1726
|
+
if (options && newReadonly) {
|
|
1727
|
+
options.readonly = true;
|
|
1728
|
+
}
|
|
1729
|
+
return invocationBinding;
|
|
1730
|
+
}
|
|
1731
|
+
const registry = rules.getRegistry('declaration', context);
|
|
1732
|
+
registry?.indexPendingItems();
|
|
1733
|
+
let list: Declaration[] | undefined;
|
|
1734
|
+
const filter = options?.filter;
|
|
1735
|
+
const indexSet = registry?.index.get(key);
|
|
1736
|
+
if (indexSet) {
|
|
1737
|
+
const deduped = new Map<Node, Declaration>();
|
|
1738
|
+
for (const n of indexSet) {
|
|
1739
|
+
if (n.type === filterType && (!filter || filter(n))) {
|
|
1740
|
+
const identity = getCandidateIdentity(n);
|
|
1741
|
+
const existing = deduped.get(identity);
|
|
1742
|
+
if (!existing || shouldReplaceCandidate(existing, n, rules)) {
|
|
1743
|
+
deduped.set(identity, n);
|
|
1744
|
+
}
|
|
1745
|
+
}
|
|
1746
|
+
}
|
|
1747
|
+
for (const n of deduped.values()) {
|
|
1748
|
+
rememberCandidateContext(n, rules);
|
|
1749
|
+
(list ??= []).push(n);
|
|
1750
|
+
}
|
|
1751
|
+
}
|
|
1752
|
+
if (list) {
|
|
1753
|
+
if (list.length > 1) {
|
|
1754
|
+
list.sort(compareDeclarationsForLookup);
|
|
1755
|
+
}
|
|
1756
|
+
let result = registry._findClosestByStart(list, start);
|
|
1757
|
+
if (result) {
|
|
1758
|
+
rememberCandidateContext(result, rules);
|
|
1759
|
+
newReadonly ||= result.options.readonly;
|
|
1760
|
+
// Visibility determines how declarations are found:
|
|
1761
|
+
// - 'private': only visible from INSIDE (children looking up) or same scope,
|
|
1762
|
+
// NOT from outside looking in (child Rules searches).
|
|
1763
|
+
// - 'optional': fallback only — returned if no public match is found.
|
|
1764
|
+
// - 'public': immediate candidate.
|
|
1765
|
+
//
|
|
1766
|
+
// IMPORTANT: Walking UP the parent chain is always an "inside" lookup — the
|
|
1767
|
+
// search originates from a descendant of this scope, so private does NOT block.
|
|
1768
|
+
// Private only blocks _searchRulesChildren (outside looking in).
|
|
1769
|
+
const currentRulesVisibility = rules.options.rulesVisibility?.[filterType] ?? '';
|
|
1770
|
+
const currentRulesOwner = getCurrentParentNode(rules, context);
|
|
1771
|
+
const shouldPreferLexicalVar = (
|
|
1772
|
+
filterType === 'VarDeclaration'
|
|
1773
|
+
&& (
|
|
1774
|
+
currentRulesVisibility !== 'optional'
|
|
1775
|
+
|| currentRulesOwner?.type === 'Ruleset'
|
|
1776
|
+
)
|
|
1777
|
+
);
|
|
1778
|
+
if (shouldPreferLexicalVar) {
|
|
1779
|
+
if (options) {
|
|
1780
|
+
options.readonly ||= newReadonly;
|
|
1781
|
+
}
|
|
1782
|
+
return result;
|
|
1783
|
+
}
|
|
1784
|
+
if (currentRulesVisibility === 'optional') {
|
|
1785
|
+
optionalCandidates.add(result);
|
|
1786
|
+
} else {
|
|
1787
|
+
declCandidate.add(result);
|
|
1788
|
+
isPublic = true;
|
|
1789
|
+
}
|
|
1790
|
+
}
|
|
1791
|
+
}
|
|
1792
|
+
// Initialize searchedRules to prevent infinite recursion when searching child Rules
|
|
1793
|
+
// This is critical: if a Rules node appears in its own children, we need to track it
|
|
1794
|
+
const searchedRules = options?.searchedRules ?? new Set<Rules>();
|
|
1795
|
+
if (!searchedRules.has(rules)) {
|
|
1796
|
+
searchedRules.add(rules);
|
|
1797
|
+
}
|
|
1798
|
+
// CRITICAL: When searching children, we MUST set searchParents: false to prevent
|
|
1799
|
+
// Reuse a single child options object — update mutable fields per iteration
|
|
1800
|
+
// instead of spreading a new object every loop
|
|
1801
|
+
if (!searchChildrenOptions) {
|
|
1802
|
+
searchChildrenOptions = options
|
|
1803
|
+
? {
|
|
1804
|
+
...options,
|
|
1805
|
+
searchParents: false,
|
|
1806
|
+
readonly: newReadonly,
|
|
1807
|
+
candidates: declCandidate,
|
|
1808
|
+
searchedRules: searchedRules,
|
|
1809
|
+
optionalCandidates
|
|
1810
|
+
}
|
|
1811
|
+
: {
|
|
1812
|
+
searchParents: false,
|
|
1813
|
+
readonly: newReadonly,
|
|
1814
|
+
candidates: declCandidate,
|
|
1815
|
+
searchedRules: searchedRules,
|
|
1816
|
+
optionalCandidates
|
|
1817
|
+
};
|
|
1818
|
+
} else {
|
|
1819
|
+
searchChildrenOptions.readonly = newReadonly;
|
|
1820
|
+
}
|
|
1821
|
+
rules.getRegistry('declaration', context)._searchRulesChildren(key, filterType, searchChildrenOptions);
|
|
1822
|
+
|
|
1823
|
+
// After searching the CURRENT scope (index + children), if we found public declarations,
|
|
1824
|
+
// sort them, find the best one (closest to start or at bottom), and return immediately.
|
|
1825
|
+
// Otherwise, continue up the parent scope.
|
|
1826
|
+
if (declCandidate.size > 0) {
|
|
1827
|
+
let bestResult: Declaration | undefined;
|
|
1828
|
+
// Use comparePosition to find the last declaration by source order
|
|
1829
|
+
const candidateArray = Array.from(declCandidate);
|
|
1830
|
+
if (candidateArray.length === 1) {
|
|
1831
|
+
bestResult = candidateArray[0];
|
|
1832
|
+
} else {
|
|
1833
|
+
candidateArray.sort((a, b) => {
|
|
1834
|
+
const order = compareDeclarationsForLookup(a, b);
|
|
1835
|
+
if (order !== 0) {
|
|
1836
|
+
return order;
|
|
1837
|
+
}
|
|
1838
|
+
const aContext = getCandidateContext(a, rules);
|
|
1839
|
+
const bContext = getCandidateContext(b, rules);
|
|
1840
|
+
const aDirect = (aContext ? getParent(a, aContext) : a.parent) === rules;
|
|
1841
|
+
const bDirect = (bContext ? getParent(b, bContext) : b.parent) === rules;
|
|
1842
|
+
if (aDirect !== bDirect) {
|
|
1843
|
+
return aDirect ? 1 : -1;
|
|
1844
|
+
}
|
|
1845
|
+
return 0;
|
|
1846
|
+
});
|
|
1847
|
+
bestResult = candidateArray[candidateArray.length - 1];
|
|
1848
|
+
}
|
|
1849
|
+
if (options && searchChildrenOptions?.readonly) {
|
|
1850
|
+
options.readonly = true;
|
|
1851
|
+
}
|
|
1852
|
+
return bestResult;
|
|
1853
|
+
}
|
|
1854
|
+
|
|
1855
|
+
// If we haven't found public candidates in the current scope, continue normal parent search
|
|
1856
|
+
// (optional candidates are tracked but we keep searching up the parent chain)
|
|
1857
|
+
if (isNonImportStyleBoundary(rules)) {
|
|
1858
|
+
searchParents = false;
|
|
1859
|
+
}
|
|
1860
|
+
if (isPublic || !searchParents) {
|
|
1861
|
+
if (options && searchChildrenOptions?.readonly) {
|
|
1862
|
+
options.readonly = true;
|
|
1863
|
+
}
|
|
1864
|
+
const result = declCandidate.values().next().value;
|
|
1865
|
+
return result;
|
|
1866
|
+
}
|
|
1867
|
+
|
|
1868
|
+
do {
|
|
1869
|
+
rules = rules?.getRegistryParent(context);
|
|
1870
|
+
} while (rules && rules.type !== 'Rules');
|
|
1871
|
+
// The start constraint only applies within the originating scope.
|
|
1872
|
+
// When walking up to a parent scope, drop it so declarations at any
|
|
1873
|
+
// position in the parent are eligible.
|
|
1874
|
+
start = undefined;
|
|
1875
|
+
}
|
|
1876
|
+
if (options && newReadonly) {
|
|
1877
|
+
options.readonly = true;
|
|
1878
|
+
}
|
|
1879
|
+
// After searching all parents, if we only have optional candidates, return the best one
|
|
1880
|
+
if (declCandidate.size === 0 && optionalCandidates.size > 0) {
|
|
1881
|
+
const optionalArray = Array.from(optionalCandidates);
|
|
1882
|
+
if (optionalArray.length === 1) {
|
|
1883
|
+
return optionalArray[0];
|
|
1884
|
+
}
|
|
1885
|
+
optionalArray.sort(compareDeclarationsForLookup);
|
|
1886
|
+
const optionalResult = optionalArray[optionalArray.length - 1];
|
|
1887
|
+
return optionalResult;
|
|
1888
|
+
}
|
|
1889
|
+
return declCandidate.values().next().value;
|
|
1890
|
+
}
|
|
1891
|
+
}
|
|
1892
|
+
|
|
1893
|
+
export function getDirectDeclarationsByKey(
|
|
1894
|
+
rules: Rules,
|
|
1895
|
+
key: string | undefined,
|
|
1896
|
+
context?: Context
|
|
1897
|
+
): Declaration[] {
|
|
1898
|
+
const children = rules.getRegistryChildren(context);
|
|
1899
|
+
const matches: Declaration[] = [];
|
|
1900
|
+
for (const child of children) {
|
|
1901
|
+
if (!isNode(child, N.Declaration | N.VarDeclaration)) {
|
|
1902
|
+
continue;
|
|
1903
|
+
}
|
|
1904
|
+
const name = child.get('name', context);
|
|
1905
|
+
if (key === undefined || name?.valueOf?.() === key) {
|
|
1906
|
+
matches.push(child);
|
|
1907
|
+
}
|
|
1908
|
+
}
|
|
1909
|
+
return matches;
|
|
1910
|
+
}
|
|
1911
|
+
|
|
1912
|
+
function arraysEqual(a: string[], b: string[]) {
|
|
1913
|
+
if (a.length !== b.length) {
|
|
1914
|
+
return false;
|
|
1915
|
+
}
|
|
1916
|
+
for (let i = 0; i < a.length; i++) {
|
|
1917
|
+
if (a[i] !== b[i]) {
|
|
1918
|
+
return false;
|
|
1919
|
+
}
|
|
1920
|
+
}
|
|
1921
|
+
return true;
|
|
1922
|
+
}
|
|
1923
|
+
|
|
1924
|
+
/**
|
|
1925
|
+
* Does `a` contain all elements of `b`? (order-independent)
|
|
1926
|
+
*
|
|
1927
|
+
* Uses linear scan instead of `Set.prototype.isSubsetOf` because
|
|
1928
|
+
* selector key arrays are typically 1–3 elements, where the overhead
|
|
1929
|
+
* of allocating a Set dominates.
|
|
1930
|
+
*/
|
|
1931
|
+
function arrayContainsAll(a: string[], b: string[]): boolean {
|
|
1932
|
+
if (b.length > a.length) {
|
|
1933
|
+
return false;
|
|
1934
|
+
}
|
|
1935
|
+
if (b.length === 0) {
|
|
1936
|
+
return true;
|
|
1937
|
+
}
|
|
1938
|
+
// For small arrays, just use includes
|
|
1939
|
+
for (const item of b) {
|
|
1940
|
+
if (!a.includes(item)) {
|
|
1941
|
+
return false;
|
|
1942
|
+
}
|
|
1943
|
+
}
|
|
1944
|
+
return true;
|
|
1945
|
+
}
|
|
1946
|
+
|
|
1947
|
+
/** Are `a` and `b` equal as unordered sets? See {@link arrayContainsAll}. */
|
|
1948
|
+
function arraysEqualAsSet(a: string[], b: string[]): boolean {
|
|
1949
|
+
if (a.length !== b.length) {
|
|
1950
|
+
return false;
|
|
1951
|
+
}
|
|
1952
|
+
return arrayContainsAll(a, b);
|
|
1953
|
+
}
|