@jesscss/core 2.0.0-alpha.1
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/LICENSE +21 -0
- package/README.md +9 -0
- package/lib/context.d.ts +352 -0
- package/lib/context.d.ts.map +1 -0
- package/lib/context.js +636 -0
- package/lib/context.js.map +1 -0
- package/lib/conversions.d.ts +73 -0
- package/lib/conversions.d.ts.map +1 -0
- package/lib/conversions.js +253 -0
- package/lib/conversions.js.map +1 -0
- package/lib/debug-log.d.ts +2 -0
- package/lib/debug-log.d.ts.map +1 -0
- package/lib/debug-log.js +27 -0
- package/lib/debug-log.js.map +1 -0
- package/lib/define-function.d.ts +587 -0
- package/lib/define-function.d.ts.map +1 -0
- package/lib/define-function.js +726 -0
- package/lib/define-function.js.map +1 -0
- package/lib/deprecation.d.ts +34 -0
- package/lib/deprecation.d.ts.map +1 -0
- package/lib/deprecation.js +57 -0
- package/lib/deprecation.js.map +1 -0
- package/lib/index.d.ts +22 -0
- package/lib/index.d.ts.map +1 -0
- package/lib/index.js +23 -0
- package/lib/index.js.map +1 -0
- package/lib/jess-error.d.ts +343 -0
- package/lib/jess-error.d.ts.map +1 -0
- package/lib/jess-error.js +508 -0
- package/lib/jess-error.js.map +1 -0
- package/lib/logger/deprecation-processing.d.ts +41 -0
- package/lib/logger/deprecation-processing.d.ts.map +1 -0
- package/lib/logger/deprecation-processing.js +81 -0
- package/lib/logger/deprecation-processing.js.map +1 -0
- package/lib/logger.d.ts +10 -0
- package/lib/logger.d.ts.map +1 -0
- package/lib/logger.js +20 -0
- package/lib/logger.js.map +1 -0
- package/lib/plugin.d.ts +94 -0
- package/lib/plugin.d.ts.map +1 -0
- package/lib/plugin.js +174 -0
- package/lib/plugin.js.map +1 -0
- package/lib/tree/ampersand.d.ts +94 -0
- package/lib/tree/ampersand.d.ts.map +1 -0
- package/lib/tree/ampersand.js +269 -0
- package/lib/tree/ampersand.js.map +1 -0
- package/lib/tree/any.d.ts +58 -0
- package/lib/tree/any.d.ts.map +1 -0
- package/lib/tree/any.js +101 -0
- package/lib/tree/any.js.map +1 -0
- package/lib/tree/at-rule.d.ts +53 -0
- package/lib/tree/at-rule.d.ts.map +1 -0
- package/lib/tree/at-rule.js +503 -0
- package/lib/tree/at-rule.js.map +1 -0
- package/lib/tree/block.d.ts +22 -0
- package/lib/tree/block.d.ts.map +1 -0
- package/lib/tree/block.js +24 -0
- package/lib/tree/block.js.map +1 -0
- package/lib/tree/bool.d.ts +17 -0
- package/lib/tree/bool.d.ts.map +1 -0
- package/lib/tree/bool.js +24 -0
- package/lib/tree/bool.js.map +1 -0
- package/lib/tree/call.d.ts +66 -0
- package/lib/tree/call.d.ts.map +1 -0
- package/lib/tree/call.js +306 -0
- package/lib/tree/call.js.map +1 -0
- package/lib/tree/collection.d.ts +30 -0
- package/lib/tree/collection.d.ts.map +1 -0
- package/lib/tree/collection.js +37 -0
- package/lib/tree/collection.js.map +1 -0
- package/lib/tree/color.d.ts +101 -0
- package/lib/tree/color.d.ts.map +1 -0
- package/lib/tree/color.js +513 -0
- package/lib/tree/color.js.map +1 -0
- package/lib/tree/combinator.d.ts +12 -0
- package/lib/tree/combinator.d.ts.map +1 -0
- package/lib/tree/combinator.js +8 -0
- package/lib/tree/combinator.js.map +1 -0
- package/lib/tree/comment.d.ts +20 -0
- package/lib/tree/comment.d.ts.map +1 -0
- package/lib/tree/comment.js +18 -0
- package/lib/tree/comment.js.map +1 -0
- package/lib/tree/condition.d.ts +31 -0
- package/lib/tree/condition.d.ts.map +1 -0
- package/lib/tree/condition.js +103 -0
- package/lib/tree/condition.js.map +1 -0
- package/lib/tree/control.d.ts +104 -0
- package/lib/tree/control.d.ts.map +1 -0
- package/lib/tree/control.js +430 -0
- package/lib/tree/control.js.map +1 -0
- package/lib/tree/declaration-custom.d.ts +18 -0
- package/lib/tree/declaration-custom.d.ts.map +1 -0
- package/lib/tree/declaration-custom.js +24 -0
- package/lib/tree/declaration-custom.js.map +1 -0
- package/lib/tree/declaration-var.d.ts +36 -0
- package/lib/tree/declaration-var.d.ts.map +1 -0
- package/lib/tree/declaration-var.js +63 -0
- package/lib/tree/declaration-var.js.map +1 -0
- package/lib/tree/declaration.d.ts +78 -0
- package/lib/tree/declaration.d.ts.map +1 -0
- package/lib/tree/declaration.js +289 -0
- package/lib/tree/declaration.js.map +1 -0
- package/lib/tree/default-guard.d.ts +15 -0
- package/lib/tree/default-guard.d.ts.map +1 -0
- package/lib/tree/default-guard.js +19 -0
- package/lib/tree/default-guard.js.map +1 -0
- package/lib/tree/dimension.d.ts +33 -0
- package/lib/tree/dimension.d.ts.map +1 -0
- package/lib/tree/dimension.js +291 -0
- package/lib/tree/dimension.js.map +1 -0
- package/lib/tree/expression.d.ts +24 -0
- package/lib/tree/expression.d.ts.map +1 -0
- package/lib/tree/expression.js +28 -0
- package/lib/tree/expression.js.map +1 -0
- package/lib/tree/extend-list.d.ts +23 -0
- package/lib/tree/extend-list.d.ts.map +1 -0
- package/lib/tree/extend-list.js +20 -0
- package/lib/tree/extend-list.js.map +1 -0
- package/lib/tree/extend.d.ts +47 -0
- package/lib/tree/extend.d.ts.map +1 -0
- package/lib/tree/extend.js +292 -0
- package/lib/tree/extend.js.map +1 -0
- package/lib/tree/function.d.ts +48 -0
- package/lib/tree/function.d.ts.map +1 -0
- package/lib/tree/function.js +74 -0
- package/lib/tree/function.js.map +1 -0
- package/lib/tree/import-js.d.ts +35 -0
- package/lib/tree/import-js.d.ts.map +1 -0
- package/lib/tree/import-js.js +45 -0
- package/lib/tree/import-js.js.map +1 -0
- package/lib/tree/import-style.d.ts +156 -0
- package/lib/tree/import-style.d.ts.map +1 -0
- package/lib/tree/import-style.js +556 -0
- package/lib/tree/import-style.js.map +1 -0
- package/lib/tree/index.d.ts +71 -0
- package/lib/tree/index.d.ts.map +1 -0
- package/lib/tree/index.js +95 -0
- package/lib/tree/index.js.map +1 -0
- package/lib/tree/interpolated-reference.d.ts +24 -0
- package/lib/tree/interpolated-reference.d.ts.map +1 -0
- package/lib/tree/interpolated-reference.js +37 -0
- package/lib/tree/interpolated-reference.js.map +1 -0
- package/lib/tree/interpolated.d.ts +62 -0
- package/lib/tree/interpolated.d.ts.map +1 -0
- package/lib/tree/interpolated.js +204 -0
- package/lib/tree/interpolated.js.map +1 -0
- package/lib/tree/js-array.d.ts +10 -0
- package/lib/tree/js-array.d.ts.map +1 -0
- package/lib/tree/js-array.js +10 -0
- package/lib/tree/js-array.js.map +1 -0
- package/lib/tree/js-expr.d.ts +23 -0
- package/lib/tree/js-expr.d.ts.map +1 -0
- package/lib/tree/js-expr.js +28 -0
- package/lib/tree/js-expr.js.map +1 -0
- package/lib/tree/js-function.d.ts +20 -0
- package/lib/tree/js-function.d.ts.map +1 -0
- package/lib/tree/js-function.js +16 -0
- package/lib/tree/js-function.js.map +1 -0
- package/lib/tree/js-object.d.ts +10 -0
- package/lib/tree/js-object.d.ts.map +1 -0
- package/lib/tree/js-object.js +10 -0
- package/lib/tree/js-object.js.map +1 -0
- package/lib/tree/list.d.ts +38 -0
- package/lib/tree/list.d.ts.map +1 -0
- package/lib/tree/list.js +83 -0
- package/lib/tree/list.js.map +1 -0
- package/lib/tree/log.d.ts +29 -0
- package/lib/tree/log.d.ts.map +1 -0
- package/lib/tree/log.js +56 -0
- package/lib/tree/log.js.map +1 -0
- package/lib/tree/mixin.d.ts +87 -0
- package/lib/tree/mixin.d.ts.map +1 -0
- package/lib/tree/mixin.js +112 -0
- package/lib/tree/mixin.js.map +1 -0
- package/lib/tree/negative.d.ts +17 -0
- package/lib/tree/negative.d.ts.map +1 -0
- package/lib/tree/negative.js +22 -0
- package/lib/tree/negative.js.map +1 -0
- package/lib/tree/nil.d.ts +31 -0
- package/lib/tree/nil.d.ts.map +1 -0
- package/lib/tree/nil.js +36 -0
- package/lib/tree/nil.js.map +1 -0
- package/lib/tree/node-base.d.ts +359 -0
- package/lib/tree/node-base.d.ts.map +1 -0
- package/lib/tree/node-base.js +884 -0
- package/lib/tree/node-base.js.map +1 -0
- package/lib/tree/node.d.ts +10 -0
- package/lib/tree/node.d.ts.map +1 -0
- package/lib/tree/node.js +45 -0
- package/lib/tree/node.js.map +1 -0
- package/lib/tree/number.d.ts +21 -0
- package/lib/tree/number.d.ts.map +1 -0
- package/lib/tree/number.js +27 -0
- package/lib/tree/number.js.map +1 -0
- package/lib/tree/operation.d.ts +26 -0
- package/lib/tree/operation.d.ts.map +1 -0
- package/lib/tree/operation.js +103 -0
- package/lib/tree/operation.js.map +1 -0
- package/lib/tree/paren.d.ts +18 -0
- package/lib/tree/paren.d.ts.map +1 -0
- package/lib/tree/paren.js +86 -0
- package/lib/tree/paren.js.map +1 -0
- package/lib/tree/query-condition.d.ts +17 -0
- package/lib/tree/query-condition.d.ts.map +1 -0
- package/lib/tree/query-condition.js +39 -0
- package/lib/tree/query-condition.js.map +1 -0
- package/lib/tree/quoted.d.ts +27 -0
- package/lib/tree/quoted.d.ts.map +1 -0
- package/lib/tree/quoted.js +66 -0
- package/lib/tree/quoted.js.map +1 -0
- package/lib/tree/range.d.ts +33 -0
- package/lib/tree/range.d.ts.map +1 -0
- package/lib/tree/range.js +47 -0
- package/lib/tree/range.js.map +1 -0
- package/lib/tree/reference.d.ts +76 -0
- package/lib/tree/reference.d.ts.map +1 -0
- package/lib/tree/reference.js +521 -0
- package/lib/tree/reference.js.map +1 -0
- package/lib/tree/rest.d.ts +15 -0
- package/lib/tree/rest.d.ts.map +1 -0
- package/lib/tree/rest.js +32 -0
- package/lib/tree/rest.js.map +1 -0
- package/lib/tree/rules-raw.d.ts +17 -0
- package/lib/tree/rules-raw.d.ts.map +1 -0
- package/lib/tree/rules-raw.js +37 -0
- package/lib/tree/rules-raw.js.map +1 -0
- package/lib/tree/rules.d.ts +255 -0
- package/lib/tree/rules.d.ts.map +1 -0
- package/lib/tree/rules.js +2293 -0
- package/lib/tree/rules.js.map +1 -0
- package/lib/tree/ruleset.d.ts +91 -0
- package/lib/tree/ruleset.d.ts.map +1 -0
- package/lib/tree/ruleset.js +506 -0
- package/lib/tree/ruleset.js.map +1 -0
- package/lib/tree/selector-attr.d.ts +31 -0
- package/lib/tree/selector-attr.d.ts.map +1 -0
- package/lib/tree/selector-attr.js +99 -0
- package/lib/tree/selector-attr.js.map +1 -0
- package/lib/tree/selector-basic.d.ts +23 -0
- package/lib/tree/selector-basic.d.ts.map +1 -0
- package/lib/tree/selector-basic.js +34 -0
- package/lib/tree/selector-basic.js.map +1 -0
- package/lib/tree/selector-capture.d.ts +23 -0
- package/lib/tree/selector-capture.d.ts.map +1 -0
- package/lib/tree/selector-capture.js +34 -0
- package/lib/tree/selector-capture.js.map +1 -0
- package/lib/tree/selector-complex.d.ts +40 -0
- package/lib/tree/selector-complex.d.ts.map +1 -0
- package/lib/tree/selector-complex.js +143 -0
- package/lib/tree/selector-complex.js.map +1 -0
- package/lib/tree/selector-compound.d.ts +16 -0
- package/lib/tree/selector-compound.d.ts.map +1 -0
- package/lib/tree/selector-compound.js +114 -0
- package/lib/tree/selector-compound.js.map +1 -0
- package/lib/tree/selector-interpolated.d.ts +23 -0
- package/lib/tree/selector-interpolated.d.ts.map +1 -0
- package/lib/tree/selector-interpolated.js +27 -0
- package/lib/tree/selector-interpolated.js.map +1 -0
- package/lib/tree/selector-list.d.ts +17 -0
- package/lib/tree/selector-list.d.ts.map +1 -0
- package/lib/tree/selector-list.js +184 -0
- package/lib/tree/selector-list.js.map +1 -0
- package/lib/tree/selector-pseudo.d.ts +42 -0
- package/lib/tree/selector-pseudo.d.ts.map +1 -0
- package/lib/tree/selector-pseudo.js +191 -0
- package/lib/tree/selector-pseudo.js.map +1 -0
- package/lib/tree/selector-simple.d.ts +5 -0
- package/lib/tree/selector-simple.d.ts.map +1 -0
- package/lib/tree/selector-simple.js +6 -0
- package/lib/tree/selector-simple.js.map +1 -0
- package/lib/tree/selector.d.ts +43 -0
- package/lib/tree/selector.d.ts.map +1 -0
- package/lib/tree/selector.js +56 -0
- package/lib/tree/selector.js.map +1 -0
- package/lib/tree/sequence.d.ts +43 -0
- package/lib/tree/sequence.d.ts.map +1 -0
- package/lib/tree/sequence.js +148 -0
- package/lib/tree/sequence.js.map +1 -0
- package/lib/tree/tree.d.ts +87 -0
- package/lib/tree/tree.d.ts.map +1 -0
- package/lib/tree/tree.js +2 -0
- package/lib/tree/tree.js.map +1 -0
- package/lib/tree/url.d.ts +18 -0
- package/lib/tree/url.d.ts.map +1 -0
- package/lib/tree/url.js +35 -0
- package/lib/tree/url.js.map +1 -0
- package/lib/tree/util/__tests__/debug-log.d.ts +1 -0
- package/lib/tree/util/__tests__/debug-log.d.ts.map +1 -0
- package/lib/tree/util/__tests__/debug-log.js +36 -0
- package/lib/tree/util/__tests__/debug-log.js.map +1 -0
- package/lib/tree/util/calculate.d.ts +3 -0
- package/lib/tree/util/calculate.d.ts.map +1 -0
- package/lib/tree/util/calculate.js +10 -0
- package/lib/tree/util/calculate.js.map +1 -0
- package/lib/tree/util/cast.d.ts +10 -0
- package/lib/tree/util/cast.d.ts.map +1 -0
- package/lib/tree/util/cast.js +87 -0
- package/lib/tree/util/cast.js.map +1 -0
- package/lib/tree/util/cloning.d.ts +4 -0
- package/lib/tree/util/cloning.d.ts.map +1 -0
- package/lib/tree/util/cloning.js +8 -0
- package/lib/tree/util/cloning.js.map +1 -0
- package/lib/tree/util/collections.d.ts +57 -0
- package/lib/tree/util/collections.d.ts.map +1 -0
- package/lib/tree/util/collections.js +136 -0
- package/lib/tree/util/collections.js.map +1 -0
- package/lib/tree/util/compare.d.ts +11 -0
- package/lib/tree/util/compare.d.ts.map +1 -0
- package/lib/tree/util/compare.js +89 -0
- package/lib/tree/util/compare.js.map +1 -0
- package/lib/tree/util/extend-helpers.d.ts +2 -0
- package/lib/tree/util/extend-helpers.d.ts.map +1 -0
- package/lib/tree/util/extend-helpers.js +2 -0
- package/lib/tree/util/extend-helpers.js.map +1 -0
- package/lib/tree/util/extend-roots.d.ts +37 -0
- package/lib/tree/util/extend-roots.d.ts.map +1 -0
- package/lib/tree/util/extend-roots.js +682 -0
- package/lib/tree/util/extend-roots.js.map +1 -0
- package/lib/tree/util/extend-roots.old.d.ts +132 -0
- package/lib/tree/util/extend-roots.old.d.ts.map +1 -0
- package/lib/tree/util/extend-roots.old.js +2272 -0
- package/lib/tree/util/extend-roots.old.js.map +1 -0
- package/lib/tree/util/extend-trace-debug.d.ts +13 -0
- package/lib/tree/util/extend-trace-debug.d.ts.map +1 -0
- package/lib/tree/util/extend-trace-debug.js +34 -0
- package/lib/tree/util/extend-trace-debug.js.map +1 -0
- package/lib/tree/util/extend.d.ts +218 -0
- package/lib/tree/util/extend.d.ts.map +1 -0
- package/lib/tree/util/extend.js +3033 -0
- package/lib/tree/util/extend.js.map +1 -0
- package/lib/tree/util/find-extendable-locations.d.ts +2 -0
- package/lib/tree/util/find-extendable-locations.d.ts.map +1 -0
- package/lib/tree/util/find-extendable-locations.js +2 -0
- package/lib/tree/util/find-extendable-locations.js.map +1 -0
- package/lib/tree/util/format.d.ts +20 -0
- package/lib/tree/util/format.d.ts.map +1 -0
- package/lib/tree/util/format.js +67 -0
- package/lib/tree/util/format.js.map +1 -0
- package/lib/tree/util/is-node.d.ts +13 -0
- package/lib/tree/util/is-node.d.ts.map +1 -0
- package/lib/tree/util/is-node.js +43 -0
- package/lib/tree/util/is-node.js.map +1 -0
- package/lib/tree/util/print.d.ts +80 -0
- package/lib/tree/util/print.d.ts.map +1 -0
- package/lib/tree/util/print.js +205 -0
- package/lib/tree/util/print.js.map +1 -0
- package/lib/tree/util/process-leading-is.d.ts +25 -0
- package/lib/tree/util/process-leading-is.d.ts.map +1 -0
- package/lib/tree/util/process-leading-is.js +364 -0
- package/lib/tree/util/process-leading-is.js.map +1 -0
- package/lib/tree/util/recursion-helper.d.ts +15 -0
- package/lib/tree/util/recursion-helper.d.ts.map +1 -0
- package/lib/tree/util/recursion-helper.js +43 -0
- package/lib/tree/util/recursion-helper.js.map +1 -0
- package/lib/tree/util/regex.d.ts +4 -0
- package/lib/tree/util/regex.d.ts.map +1 -0
- package/lib/tree/util/regex.js +4 -0
- package/lib/tree/util/regex.js.map +1 -0
- package/lib/tree/util/registry-utils.d.ts +192 -0
- package/lib/tree/util/registry-utils.d.ts.map +1 -0
- package/lib/tree/util/registry-utils.js +1242 -0
- package/lib/tree/util/registry-utils.js.map +1 -0
- package/lib/tree/util/ruleset-trace.d.ts +4 -0
- package/lib/tree/util/ruleset-trace.d.ts.map +1 -0
- package/lib/tree/util/ruleset-trace.js +14 -0
- package/lib/tree/util/ruleset-trace.js.map +1 -0
- package/lib/tree/util/selector-compare.d.ts +2 -0
- package/lib/tree/util/selector-compare.d.ts.map +1 -0
- package/lib/tree/util/selector-compare.js +2 -0
- package/lib/tree/util/selector-compare.js.map +1 -0
- package/lib/tree/util/selector-match-core.d.ts +171 -0
- package/lib/tree/util/selector-match-core.d.ts.map +1 -0
- package/lib/tree/util/selector-match-core.js +1578 -0
- package/lib/tree/util/selector-match-core.js.map +1 -0
- package/lib/tree/util/selector-utils.d.ts +30 -0
- package/lib/tree/util/selector-utils.d.ts.map +1 -0
- package/lib/tree/util/selector-utils.js +100 -0
- package/lib/tree/util/selector-utils.js.map +1 -0
- package/lib/tree/util/serialize-helper.d.ts +13 -0
- package/lib/tree/util/serialize-helper.d.ts.map +1 -0
- package/lib/tree/util/serialize-helper.js +387 -0
- package/lib/tree/util/serialize-helper.js.map +1 -0
- package/lib/tree/util/serialize-types.d.ts +9 -0
- package/lib/tree/util/serialize-types.d.ts.map +1 -0
- package/lib/tree/util/serialize-types.js +216 -0
- package/lib/tree/util/serialize-types.js.map +1 -0
- package/lib/tree/util/should-operate.d.ts +23 -0
- package/lib/tree/util/should-operate.d.ts.map +1 -0
- package/lib/tree/util/should-operate.js +46 -0
- package/lib/tree/util/should-operate.js.map +1 -0
- package/lib/tree/util/sourcemap.d.ts +7 -0
- package/lib/tree/util/sourcemap.d.ts.map +1 -0
- package/lib/tree/util/sourcemap.js +25 -0
- package/lib/tree/util/sourcemap.js.map +1 -0
- package/lib/types/config.d.ts +205 -0
- package/lib/types/config.d.ts.map +1 -0
- package/lib/types/config.js +2 -0
- package/lib/types/config.js.map +1 -0
- package/lib/types/index.d.ts +15 -0
- package/lib/types/index.d.ts.map +1 -0
- package/lib/types/index.js +3 -0
- package/lib/types/index.js.map +1 -0
- package/lib/types/modes.d.ts +24 -0
- package/lib/types/modes.d.ts.map +1 -0
- package/lib/types/modes.js +2 -0
- package/lib/types/modes.js.map +1 -0
- package/lib/types.d.ts +61 -0
- package/lib/types.d.ts.map +1 -0
- package/lib/types.js +2 -0
- package/lib/types.js.map +1 -0
- package/lib/use-webpack-resolver.d.ts +9 -0
- package/lib/use-webpack-resolver.d.ts.map +1 -0
- package/lib/use-webpack-resolver.js +41 -0
- package/lib/use-webpack-resolver.js.map +1 -0
- package/lib/visitor/index.d.ts +136 -0
- package/lib/visitor/index.d.ts.map +1 -0
- package/lib/visitor/index.js +135 -0
- package/lib/visitor/index.js.map +1 -0
- package/lib/visitor/less-visitor.d.ts +7 -0
- package/lib/visitor/less-visitor.d.ts.map +1 -0
- package/lib/visitor/less-visitor.js +7 -0
- package/lib/visitor/less-visitor.js.map +1 -0
- package/package.json +66 -0
|
@@ -0,0 +1,1242 @@
|
|
|
1
|
+
import { isNode } from './is-node.js';
|
|
2
|
+
import { Nil } from '../nil.js';
|
|
3
|
+
import { JsFunction } from '../js-function.js';
|
|
4
|
+
import { atIndex } from './collections.js';
|
|
5
|
+
import { comparePosition } from './compare.js';
|
|
6
|
+
const { isArray } = Array;
|
|
7
|
+
export class Registry {
|
|
8
|
+
rules;
|
|
9
|
+
pendingItems = new Set();
|
|
10
|
+
constructor(rules) {
|
|
11
|
+
this.rules = rules;
|
|
12
|
+
}
|
|
13
|
+
add(item) {
|
|
14
|
+
this.pendingItems.add(item);
|
|
15
|
+
}
|
|
16
|
+
indexPendingItems() {
|
|
17
|
+
for (const item of this.pendingItems) {
|
|
18
|
+
let key = String(item.value);
|
|
19
|
+
let set = this.index.get(key);
|
|
20
|
+
if (set && set instanceof Set) {
|
|
21
|
+
set.add(item);
|
|
22
|
+
}
|
|
23
|
+
else {
|
|
24
|
+
this.index.set(key, new Set([item]));
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
this.pendingItems.clear();
|
|
28
|
+
}
|
|
29
|
+
_searchRulesChildren(key, filterType, options = {}) {
|
|
30
|
+
let rules = this.rules;
|
|
31
|
+
// CRITICAL FIX: Initialize searchedRules if not provided, and add current Rules BEFORE any recursive calls
|
|
32
|
+
// The flaw in the original algorithm: when _searchRulesChildren calls childRules.find(), that creates
|
|
33
|
+
// a new search context via DeclarationRegistry.find(), which may not preserve searchedRules tracking.
|
|
34
|
+
// By initializing it here and adding the current Rules immediately, we ensure tracking persists.
|
|
35
|
+
const searchedRules = options?.searchedRules ?? new Set();
|
|
36
|
+
if (!searchedRules.has(rules)) {
|
|
37
|
+
searchedRules.add(rules);
|
|
38
|
+
}
|
|
39
|
+
let findType = filterType === 'Mixin' ? 'mixin' : 'declaration';
|
|
40
|
+
let findAll = Boolean(options.findAll);
|
|
41
|
+
let { candidates = new Set(), start, readonly, local, childFilterType, context } = options;
|
|
42
|
+
// childFilterType is the filterType to use when calling child Rules.find
|
|
43
|
+
// If not provided, use filterType (for backward compatibility with DeclarationRegistry)
|
|
44
|
+
// Note: childFilterType can be undefined to mean "don't filter" (accept both Mixin and Ruleset)
|
|
45
|
+
const actualChildFilterType = 'childFilterType' in options ? childFilterType : filterType;
|
|
46
|
+
let firstValue = candidates.values().next().value;
|
|
47
|
+
if (rules._rulesSet) {
|
|
48
|
+
let { rulesSet } = rules;
|
|
49
|
+
/**
|
|
50
|
+
* Only consider rules after the last found declaration (if relevant)
|
|
51
|
+
* and before the start position (if relevant)
|
|
52
|
+
*/
|
|
53
|
+
rulesSet = rulesSet.filter((n) => {
|
|
54
|
+
// Check RulesEntry visibility first, then fall back to the actual Rules node's visibility
|
|
55
|
+
// Rules constructor sets defaults, so visibility should always be defined
|
|
56
|
+
const entryVisibility = n.rulesVisibility?.[filterType];
|
|
57
|
+
const nodeVisibility = n.node.options.rulesVisibility?.[filterType];
|
|
58
|
+
const visibility = entryVisibility ?? nodeVisibility;
|
|
59
|
+
const isMixinOutput = n.node.options?.isMixinOutput === true;
|
|
60
|
+
// Mixin output Rules should never participate in untargeted lookups.
|
|
61
|
+
// They are only searchable when the lookup has an explicit target.
|
|
62
|
+
// (This prevents mixin-call frame variables—including parameter bindings—from leaking.)
|
|
63
|
+
if (isMixinOutput) {
|
|
64
|
+
return options?.hasTarget === true;
|
|
65
|
+
}
|
|
66
|
+
// Otherwise, follow normal visibility rules
|
|
67
|
+
// Only 'public' and 'optional' are visible (not 'private' or undefined)
|
|
68
|
+
const isVisible = visibility === 'public' || visibility === 'optional';
|
|
69
|
+
/**
|
|
70
|
+
* Sass `@forward`:
|
|
71
|
+
* Forwarded Rules should not be visible to lookups within the current stylesheet scope
|
|
72
|
+
* (context.rulesContext === rules), but should remain visible to downstream consumers.
|
|
73
|
+
*/
|
|
74
|
+
const isForwardNode = Boolean(n.node.options?.forward);
|
|
75
|
+
const skipForwardNode = isForwardNode && context?.rulesContext === rules;
|
|
76
|
+
// Local nodes can only be searched once - if we've already passed through
|
|
77
|
+
// a local boundary (local === true), we cannot search another local node
|
|
78
|
+
// This prevents re-exporting local variables to parent's parent
|
|
79
|
+
const isLocalNode = Boolean(n.node.options?.local);
|
|
80
|
+
const skipLocalNode = local && isLocalNode; // Skip if already in local context
|
|
81
|
+
// If we already have a candidate (firstValue exists), we should still search imported Rules
|
|
82
|
+
// to compare them and determine which was declared later.
|
|
83
|
+
// The original condition (findAll || !firstValue) was preventing comparison.
|
|
84
|
+
// We need to search imported Rules when:
|
|
85
|
+
// 1. findAll is true (search all)
|
|
86
|
+
// 2. firstValue doesn't exist (initial lookup - search imported Rules to find the variable)
|
|
87
|
+
// 3. firstValue exists AND candidates was explicitly passed with items (comparison context)
|
|
88
|
+
// This happens when DeclarationRegistry.find calls _searchRulesChildren after finding a local variable
|
|
89
|
+
// The key difference: if candidates was passed from DeclarationRegistry.find, it will have the local variable
|
|
90
|
+
// If candidates is empty or wasn't passed, we're in initial lookup mode
|
|
91
|
+
const isComparisonContext = firstValue && candidates.size > 0;
|
|
92
|
+
return (findAll || !firstValue || isComparisonContext)
|
|
93
|
+
&& (start === undefined || n.node.index < start)
|
|
94
|
+
&& !skipForwardNode
|
|
95
|
+
&& !skipLocalNode
|
|
96
|
+
&& isVisible;
|
|
97
|
+
});
|
|
98
|
+
let length = rulesSet.length;
|
|
99
|
+
if (length) {
|
|
100
|
+
// searchedRules is already initialized above and includes the current Rules
|
|
101
|
+
for (let i = length - 1; i >= 0; i--) {
|
|
102
|
+
let r = rulesSet.at(i);
|
|
103
|
+
// Skip if we've already searched this Rules node to prevent infinite recursion
|
|
104
|
+
if (searchedRules && searchedRules.has(r.node)) {
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
if (r.node === rules) {
|
|
108
|
+
throw new Error(`Rules node contains itself in rulesSet`);
|
|
109
|
+
}
|
|
110
|
+
/** Locals can be searched once but not twice */
|
|
111
|
+
let newLocal = local || Boolean(r.node.options?.local);
|
|
112
|
+
let newOpts = options ? { ...options, readonly: readonly || r.readonly } : { readonly: readonly || r.readonly };
|
|
113
|
+
newOpts.local = newLocal;
|
|
114
|
+
newOpts.start = undefined;
|
|
115
|
+
// _searchRulesChildren should never search parents - only search within imported Rules
|
|
116
|
+
newOpts.searchParents = false;
|
|
117
|
+
// Pass through searchedRules to prevent circular references
|
|
118
|
+
// searchedRules is always defined (initialized above)
|
|
119
|
+
newOpts.searchedRules = searchedRules;
|
|
120
|
+
if (context) {
|
|
121
|
+
newOpts.context = context;
|
|
122
|
+
}
|
|
123
|
+
// Use actualChildFilterType which may be undefined for mixin-ruleset lookups
|
|
124
|
+
// filterType parameter is used to SELECT registry, actualChildFilterType is used to FILTER results
|
|
125
|
+
let result = r.node.find(findType, key, actualChildFilterType, newOpts);
|
|
126
|
+
if (result) {
|
|
127
|
+
// Check if this Rules has optional visibility (from RulesEntry or the actual Rules node)
|
|
128
|
+
const entryVisibility = r.rulesVisibility?.[filterType];
|
|
129
|
+
const nodeVisibility = r.node.options.rulesVisibility?.[filterType];
|
|
130
|
+
const isOptional = entryVisibility === 'optional' || nodeVisibility === 'optional';
|
|
131
|
+
const optionalCandidates = options?.optionalCandidates;
|
|
132
|
+
/**
|
|
133
|
+
* If it's a public declaration, and it's the lower-most declaration,
|
|
134
|
+
* it wins.
|
|
135
|
+
* Rules constructor sets defaults, so visibility should always be defined.
|
|
136
|
+
*/
|
|
137
|
+
const isPublic = entryVisibility === 'public' || nodeVisibility === 'public';
|
|
138
|
+
if (!findAll && isPublic) {
|
|
139
|
+
if (options && newOpts.readonly) {
|
|
140
|
+
options.readonly = true;
|
|
141
|
+
}
|
|
142
|
+
// Add to candidates and stop searching this rule
|
|
143
|
+
if (isArray(result)) {
|
|
144
|
+
for (const node of result) {
|
|
145
|
+
candidates.add(node);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
else {
|
|
149
|
+
candidates.add(result);
|
|
150
|
+
}
|
|
151
|
+
break; // Stop searching this rule
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* If we're looking for a declaration and its optional OR
|
|
155
|
+
* we're looking for a mixin, then we need to keep searching.
|
|
156
|
+
*/
|
|
157
|
+
options.readonly ||= newOpts.readonly;
|
|
158
|
+
if (isArray(result)) {
|
|
159
|
+
for (const node of result) {
|
|
160
|
+
if (isOptional && optionalCandidates) {
|
|
161
|
+
optionalCandidates.add(node);
|
|
162
|
+
}
|
|
163
|
+
else {
|
|
164
|
+
candidates.add(node);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
else {
|
|
169
|
+
if (isOptional && optionalCandidates) {
|
|
170
|
+
optionalCandidates.add(result);
|
|
171
|
+
}
|
|
172
|
+
else {
|
|
173
|
+
candidates.add(result);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
// REMOVED: Manual iteration through rules.value for child Rules nodes
|
|
181
|
+
// If a Rules node is in rules.value and should be searchable, it should be registered
|
|
182
|
+
// via registerNode() which adds it to rulesSet. We already search rulesSet above.
|
|
183
|
+
// Manually iterating through rules.value creates infinite loops when a Rules node
|
|
184
|
+
// appears in its own children, and is unnecessary since registered Rules are in rulesSet.
|
|
185
|
+
}
|
|
186
|
+
/**
|
|
187
|
+
* Find the closest declaration from start, in reverse order,
|
|
188
|
+
* using a binary search
|
|
189
|
+
*/
|
|
190
|
+
_findClosestByStart(list, start) {
|
|
191
|
+
if (start === undefined) {
|
|
192
|
+
return atIndex(list, -1);
|
|
193
|
+
}
|
|
194
|
+
/**
|
|
195
|
+
* We do this so we start looking above the given position and don't
|
|
196
|
+
* return the current node.
|
|
197
|
+
*/
|
|
198
|
+
start -= 1;
|
|
199
|
+
let bestMatch;
|
|
200
|
+
/** Binary search the queue to find a starting position */
|
|
201
|
+
let left = 0;
|
|
202
|
+
let right = list.length - 1;
|
|
203
|
+
while (left <= right) {
|
|
204
|
+
let mid = Math.floor((left + right) / 2);
|
|
205
|
+
let midVal = list.at(mid).index;
|
|
206
|
+
if (midVal === start) {
|
|
207
|
+
bestMatch = mid;
|
|
208
|
+
break;
|
|
209
|
+
}
|
|
210
|
+
if (midVal < start) {
|
|
211
|
+
bestMatch = mid;
|
|
212
|
+
left = mid + 1;
|
|
213
|
+
}
|
|
214
|
+
else {
|
|
215
|
+
right = mid - 1;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
return bestMatch !== undefined ? list.at(bestMatch) : undefined;
|
|
219
|
+
}
|
|
220
|
+
_findByKey(candidates, key) {
|
|
221
|
+
let set = this.index.get(key);
|
|
222
|
+
if (set) {
|
|
223
|
+
let newSet;
|
|
224
|
+
if (set instanceof Set) {
|
|
225
|
+
newSet = set;
|
|
226
|
+
}
|
|
227
|
+
else if (isArray(set)) {
|
|
228
|
+
newSet = new Set(set.map(({ value }) => value));
|
|
229
|
+
}
|
|
230
|
+
else {
|
|
231
|
+
return set;
|
|
232
|
+
}
|
|
233
|
+
if (candidates) {
|
|
234
|
+
if (candidates instanceof Set) {
|
|
235
|
+
// Avoid Set.prototype.union (not available in our TS lib target)
|
|
236
|
+
for (const v of newSet) {
|
|
237
|
+
candidates.add(v);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
else {
|
|
241
|
+
candidates = new Set([candidates, ...newSet]);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
else {
|
|
245
|
+
candidates = newSet;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
return candidates;
|
|
249
|
+
}
|
|
250
|
+
find(keys, _filterType, _options) {
|
|
251
|
+
this.indexPendingItems();
|
|
252
|
+
let candidates;
|
|
253
|
+
if (isArray(keys) || keys instanceof Set) {
|
|
254
|
+
for (const key of keys) {
|
|
255
|
+
candidates = this._findByKey(candidates, key);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
else {
|
|
259
|
+
candidates = this._findByKey(candidates, keys);
|
|
260
|
+
}
|
|
261
|
+
if (candidates instanceof Set) {
|
|
262
|
+
return candidates.size ? [...candidates] : undefined;
|
|
263
|
+
}
|
|
264
|
+
return candidates;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
/**
|
|
268
|
+
* Registry for fast selector-based ruleset lookups
|
|
269
|
+
*/
|
|
270
|
+
export class RulesetRegistry extends Registry {
|
|
271
|
+
index = new Map();
|
|
272
|
+
/**
|
|
273
|
+
* Add a ruleset to be indexed later
|
|
274
|
+
*/
|
|
275
|
+
add(ruleset) {
|
|
276
|
+
if (isNode(ruleset.value.selector, 'Selector')) {
|
|
277
|
+
this.pendingItems.add(ruleset);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
/**
|
|
281
|
+
* Index any pending rulesets
|
|
282
|
+
* Override the base class method to use keySet-based indexing
|
|
283
|
+
*/
|
|
284
|
+
indexPendingItems() {
|
|
285
|
+
const index = this.index;
|
|
286
|
+
for (const ruleset of this.pendingItems) {
|
|
287
|
+
/** Index using the ruleset's actual selector keySet - no need for getImplicitSelector here
|
|
288
|
+
* since we're indexing the selector as-is, not transforming it for parent context */
|
|
289
|
+
const selector = ruleset.selector;
|
|
290
|
+
if (selector instanceof Nil) {
|
|
291
|
+
continue;
|
|
292
|
+
}
|
|
293
|
+
if (!('keySet' in selector)) {
|
|
294
|
+
continue;
|
|
295
|
+
}
|
|
296
|
+
const keySet = selector.keySet;
|
|
297
|
+
for (const key of keySet) {
|
|
298
|
+
const existing = index.get(key);
|
|
299
|
+
if (existing) {
|
|
300
|
+
existing.add(ruleset);
|
|
301
|
+
}
|
|
302
|
+
else {
|
|
303
|
+
index.set(key, new Set([ruleset]));
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
this.pendingItems.clear();
|
|
308
|
+
}
|
|
309
|
+
/**
|
|
310
|
+
* Find candidate rulesets that might match the target selector.
|
|
311
|
+
* Searches only the local index - all rulesets should be registered
|
|
312
|
+
* to the extend root's registry during evaluation.
|
|
313
|
+
*/
|
|
314
|
+
find(keys) {
|
|
315
|
+
// Index any pending rulesets first
|
|
316
|
+
this.indexPendingItems();
|
|
317
|
+
let candidates;
|
|
318
|
+
let rulesets;
|
|
319
|
+
/** Just get based on first key */
|
|
320
|
+
for (const key of keys) {
|
|
321
|
+
candidates = this.index.get(key);
|
|
322
|
+
break;
|
|
323
|
+
}
|
|
324
|
+
if (!candidates) {
|
|
325
|
+
return undefined;
|
|
326
|
+
}
|
|
327
|
+
/** Now find selectors that have all keys */
|
|
328
|
+
let keySet = keys instanceof Set ? keys : new Set(keys);
|
|
329
|
+
for (const c of candidates) {
|
|
330
|
+
let sel = c.selector;
|
|
331
|
+
if (!sel || isNode(sel, 'Nil')) {
|
|
332
|
+
continue;
|
|
333
|
+
}
|
|
334
|
+
// Avoid Set.prototype.isSubsetOf (not available in our TS lib target)
|
|
335
|
+
let isSubset = true;
|
|
336
|
+
for (const k of keySet) {
|
|
337
|
+
if (!sel.keySet.has(k)) {
|
|
338
|
+
isSubset = false;
|
|
339
|
+
break;
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
if (isSubset) {
|
|
343
|
+
(rulesets ??= []).push(c);
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
return rulesets;
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
/**
|
|
350
|
+
* The mixin registry works a little differently than the selector registry
|
|
351
|
+
* in these ways:
|
|
352
|
+
*
|
|
353
|
+
* 1. The mixin registry can only be indexed by basic element, class, and
|
|
354
|
+
* id selectors.
|
|
355
|
+
* 2. The index is the start key, not any key found in the selector.
|
|
356
|
+
* 3. '>' and ' ' combinators are ignored.
|
|
357
|
+
* 4. Initial ampersands (implicit or explicit) are ignored.
|
|
358
|
+
* 5. The mixin registry is local to the rules, whereas the selector registry
|
|
359
|
+
* is global to the file tree.
|
|
360
|
+
* 6. Rulesets and mixins without params will have their children searched
|
|
361
|
+
* if the first part matches.
|
|
362
|
+
*/
|
|
363
|
+
export class MixinRegistry extends Registry {
|
|
364
|
+
index = new Map();
|
|
365
|
+
// private getSimpleKeyList(selector: Selector | Nil | undefined): string[] | undefined {
|
|
366
|
+
// let keyList: string[] | undefined;
|
|
367
|
+
// if (selector && 'keySet' in selector) {
|
|
368
|
+
// let passed = true;
|
|
369
|
+
// let foundBasic = false;
|
|
370
|
+
// for (const sel of selector.nodes()) {
|
|
371
|
+
// /** Ampersand is okay at start, but not after a basic selector */
|
|
372
|
+
// if (!foundBasic && isNode(sel, 'Ampersand')) {
|
|
373
|
+
// continue;
|
|
374
|
+
// }
|
|
375
|
+
// if (isNode(sel, 'Combinator')) {
|
|
376
|
+
// if (sel.value !== '>' && sel.value !== ' ') {
|
|
377
|
+
// passed = false;
|
|
378
|
+
// break;
|
|
379
|
+
// }
|
|
380
|
+
// continue;
|
|
381
|
+
// }
|
|
382
|
+
// /** Anything other than a universal selector is fine */
|
|
383
|
+
// if (isNode(sel, 'BasicSelector') && /^[^*]/.test(sel.value)) {
|
|
384
|
+
// (keyList ??= []).push(sel.valueOf() as string);
|
|
385
|
+
// foundBasic = true;
|
|
386
|
+
// continue;
|
|
387
|
+
// }
|
|
388
|
+
// if (isNode(sel, 'CompoundSelector') || isNode(sel, 'ComplexSelector')) {
|
|
389
|
+
// /** Might still be fine */
|
|
390
|
+
// continue;
|
|
391
|
+
// }
|
|
392
|
+
// /** Nothing else is valid, so fail */
|
|
393
|
+
// passed = false;
|
|
394
|
+
// break;
|
|
395
|
+
// }
|
|
396
|
+
// if (!passed) {
|
|
397
|
+
// return;
|
|
398
|
+
// }
|
|
399
|
+
// }
|
|
400
|
+
// return keyList;
|
|
401
|
+
// }
|
|
402
|
+
_indexSelectorStart(mixin, keySet) {
|
|
403
|
+
const index = this.index;
|
|
404
|
+
if (keySet?.size) {
|
|
405
|
+
// Keep `*` in the trailing match path so selectors like `.mixin > *` do not collide
|
|
406
|
+
// with plain `.mixin` mixin calls. Only pseudo keys are excluded from indexing.
|
|
407
|
+
const candidateKeys = Array.from(keySet).filter(key => typeof key === 'string' && !key.startsWith(':'));
|
|
408
|
+
const startIndex = candidateKeys.findIndex(key => !key.startsWith('*'));
|
|
409
|
+
if (startIndex === -1) {
|
|
410
|
+
return; // Only skip when there is no indexable key (e.g. :hover-only selector)
|
|
411
|
+
}
|
|
412
|
+
const startKey = candidateKeys[startIndex];
|
|
413
|
+
const rest = candidateKeys.slice(startIndex + 1);
|
|
414
|
+
const existing = index.get(startKey);
|
|
415
|
+
if (existing) {
|
|
416
|
+
existing.push({ value: mixin, match: rest });
|
|
417
|
+
}
|
|
418
|
+
else {
|
|
419
|
+
index.set(startKey, [{ value: mixin, match: rest }]);
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
indexPendingItems() {
|
|
424
|
+
for (const mixin of this.pendingItems) {
|
|
425
|
+
if (isNode(mixin, 'Ruleset')) {
|
|
426
|
+
// Use the ruleset's own selector, not the implicit selector with parent context
|
|
427
|
+
// This ensures nested rulesets are indexed by their local keys, not parent keys
|
|
428
|
+
// If the selector has been evaluated/flattened, use sourceNode which has the original
|
|
429
|
+
let selector = mixin.value.selector;
|
|
430
|
+
if (isNode(selector, 'Nil')) {
|
|
431
|
+
continue;
|
|
432
|
+
}
|
|
433
|
+
// `&` rulesets are structural nesting selectors, not callable mixins.
|
|
434
|
+
// Determine callability from ownSelector (before implicit selector resolution) when available.
|
|
435
|
+
const ownSelector = mixin.options?.ownSelector;
|
|
436
|
+
const callableSelector = ownSelector && !isNode(ownSelector, 'Nil') ? ownSelector : selector;
|
|
437
|
+
if (isNode(callableSelector, 'Ampersand')) {
|
|
438
|
+
continue;
|
|
439
|
+
}
|
|
440
|
+
// Prefer evaluated selector keys; they resolve interpolations (e.g. .@{a0} -> .\123).
|
|
441
|
+
// Fall back to source selector only when evaluated keys are empty.
|
|
442
|
+
const sourceSelector = selector.sourceNode;
|
|
443
|
+
const selectorToIndex = (selector.visibleKeySet?.size
|
|
444
|
+
? selector
|
|
445
|
+
: (sourceSelector?.visibleKeySet?.size ? sourceSelector : selector));
|
|
446
|
+
let keySetToUse;
|
|
447
|
+
if (isNode(selectorToIndex, 'SelectorList')) {
|
|
448
|
+
/** Selector list's selectors are individually registered */
|
|
449
|
+
for (const sel of selectorToIndex.value) {
|
|
450
|
+
this._indexSelectorStart(mixin, sel.visibleKeySet);
|
|
451
|
+
}
|
|
452
|
+
keySetToUse = undefined; // already indexed above
|
|
453
|
+
}
|
|
454
|
+
else {
|
|
455
|
+
keySetToUse = selectorToIndex.visibleKeySet;
|
|
456
|
+
}
|
|
457
|
+
// Normalize nested `&...` selectors to local keys when possible.
|
|
458
|
+
// Evaluated key sets can include inherited parent keys (e.g. [".b",".bb",".foo-xxx",...]),
|
|
459
|
+
// but recursive lookup descends with local remainder keys (e.g. [".foo-xxx", ...]).
|
|
460
|
+
if (keySetToUse
|
|
461
|
+
&& keySetToUse.size > 0
|
|
462
|
+
&& ownSelector
|
|
463
|
+
&& !isNode(ownSelector, 'Nil')) {
|
|
464
|
+
const ownSelectorText = String(ownSelector.valueOf?.() ?? '');
|
|
465
|
+
const ownKeys = Array.from(ownSelector.visibleKeySet ?? []);
|
|
466
|
+
const parentSelector = isNode(mixin.parent?.parent, 'Ruleset')
|
|
467
|
+
? mixin.parent.parent.value.selector
|
|
468
|
+
: undefined;
|
|
469
|
+
const parentKeys = (parentSelector && !isNode(parentSelector, 'Nil')
|
|
470
|
+
? Array.from(parentSelector.visibleKeySet ?? [])
|
|
471
|
+
: []);
|
|
472
|
+
if (parentKeys.length > 0
|
|
473
|
+
&& ownKeys.length > parentKeys.length
|
|
474
|
+
&& parentKeys.every((k, i) => ownKeys[i] === k)) {
|
|
475
|
+
keySetToUse = new Set(ownKeys.slice(parentKeys.length));
|
|
476
|
+
}
|
|
477
|
+
else if (ownKeys.length > 1 && ownSelectorText.trimStart().startsWith('&')) {
|
|
478
|
+
keySetToUse = new Set(ownKeys.slice(1));
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
// When the resolved selector is an Ampersand (implicit &), visibleKeySet is empty so we
|
|
482
|
+
// would not index. Use the ruleset's ownSelector (set in preEval before getImplicitSelector)
|
|
483
|
+
// to index by the callable selector that was explicitly authored.
|
|
484
|
+
if (keySetToUse !== undefined) {
|
|
485
|
+
if (keySetToUse.size === 0
|
|
486
|
+
&& ownSelector
|
|
487
|
+
&& !isNode(ownSelector, 'Nil')) {
|
|
488
|
+
const ownKeySet = ownSelector.visibleKeySet;
|
|
489
|
+
if (ownKeySet?.size) {
|
|
490
|
+
const ownKeys = Array.from(ownKeySet);
|
|
491
|
+
const selectorText = String(selectorToIndex.valueOf?.() ?? '');
|
|
492
|
+
// In nested `&...` rulesets, ownKeySet may include inherited parent key first.
|
|
493
|
+
// For local lookup chains we want the nested segment as the start key.
|
|
494
|
+
if (selectorText.startsWith('&') && ownKeys.length > 1) {
|
|
495
|
+
keySetToUse = new Set(ownKeys.slice(1));
|
|
496
|
+
}
|
|
497
|
+
else {
|
|
498
|
+
keySetToUse = ownKeySet;
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
this._indexSelectorStart(mixin, keySetToUse);
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
else {
|
|
506
|
+
this._indexSelectorStart(mixin, mixin.keySet);
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
this.pendingItems.clear();
|
|
510
|
+
}
|
|
511
|
+
/**
|
|
512
|
+
* Check if an entry matches the search criteria.
|
|
513
|
+
* Handles exact matches, partial matches (compound selector completion), and recursive searches.
|
|
514
|
+
* This consolidates the matching logic to avoid duplication.
|
|
515
|
+
*/
|
|
516
|
+
/**
|
|
517
|
+
* Check if a Ruleset/Mixin matches a given array of keys using the same logic as the registry
|
|
518
|
+
* This uses the indexed match arrays (same as _checkEntryMatch) rather than direct selector comparison
|
|
519
|
+
* @param value The Ruleset or Mixin to check
|
|
520
|
+
* @param keys The array of keys to match against (e.g., [".jo", ".ki"])
|
|
521
|
+
* @returns true if the Ruleset/Mixin matches the keys using registry matching logic
|
|
522
|
+
*/
|
|
523
|
+
checkRulesetMatchesKeys(value, keys) {
|
|
524
|
+
if (!keys || keys.length === 0) {
|
|
525
|
+
return false;
|
|
526
|
+
}
|
|
527
|
+
// Get the selector's keySet and extract indexable keys (same as _indexSelectorStart)
|
|
528
|
+
let indexableKeys = [];
|
|
529
|
+
if (isNode(value, 'Ruleset')) {
|
|
530
|
+
const selector = value.value.selector;
|
|
531
|
+
if (isNode(selector, 'Nil')) {
|
|
532
|
+
return false;
|
|
533
|
+
}
|
|
534
|
+
if (isNode(selector, 'SelectorList')) {
|
|
535
|
+
// For selector lists, check if any selector matches
|
|
536
|
+
return selector.value.some((sel) => {
|
|
537
|
+
const selKeys = Array.from(sel.keySet).filter(key => typeof key === 'string' && !key.startsWith('*') && !key.startsWith(':'));
|
|
538
|
+
if (selKeys.length === 0) {
|
|
539
|
+
return false;
|
|
540
|
+
}
|
|
541
|
+
// Check if keys appear in sequence in this selector's keys
|
|
542
|
+
return this._checkKeysSubsequence(selKeys, keys);
|
|
543
|
+
});
|
|
544
|
+
}
|
|
545
|
+
const keySet = selector.keySet;
|
|
546
|
+
if (!keySet || keySet.size === 0) {
|
|
547
|
+
return false;
|
|
548
|
+
}
|
|
549
|
+
indexableKeys = Array.from(keySet).filter((key) => {
|
|
550
|
+
return typeof key === 'string' && !key.startsWith('*') && !key.startsWith(':');
|
|
551
|
+
});
|
|
552
|
+
}
|
|
553
|
+
else {
|
|
554
|
+
const keySet = value.keySet;
|
|
555
|
+
if (!keySet || keySet.size === 0) {
|
|
556
|
+
return false;
|
|
557
|
+
}
|
|
558
|
+
indexableKeys = Array.from(keySet).filter((key) => {
|
|
559
|
+
return typeof key === 'string' && !key.startsWith('*') && !key.startsWith(':');
|
|
560
|
+
});
|
|
561
|
+
}
|
|
562
|
+
if (indexableKeys.length === 0) {
|
|
563
|
+
return false;
|
|
564
|
+
}
|
|
565
|
+
// Check if the provided keys appear in sequence in the selector's indexable keys
|
|
566
|
+
// The keySet should only contain keys from the Ruleset's own selector, not parent context
|
|
567
|
+
return this._checkKeysSubsequence(indexableKeys, keys);
|
|
568
|
+
}
|
|
569
|
+
/**
|
|
570
|
+
* Internal helper that checks if the provided keys appear in sequence within the selector's keys
|
|
571
|
+
*
|
|
572
|
+
* For compound selectors like `#header .milk .chips .jo.ki`, when we search for `.jo`, we get:
|
|
573
|
+
* - The full selector's indexable keys: `["#header", ".milk", ".chips", ".jo", ".ki"]`
|
|
574
|
+
* - When checking if accumulated keys `[".jo", ".ki"]` match, we check if they appear in sequence
|
|
575
|
+
*/
|
|
576
|
+
_checkKeysSubsequence(selectorKeys, searchKeys) {
|
|
577
|
+
if (searchKeys.length === 0) {
|
|
578
|
+
return false;
|
|
579
|
+
}
|
|
580
|
+
// Check if searchKeys is a subsequence of selectorKeys (searchKeys appear in order)
|
|
581
|
+
let searchIndex = 0;
|
|
582
|
+
for (const selectorKey of selectorKeys) {
|
|
583
|
+
if (searchIndex < searchKeys.length && selectorKey === searchKeys[searchIndex]) {
|
|
584
|
+
searchIndex++;
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
const matches = searchIndex === searchKeys.length;
|
|
588
|
+
return matches;
|
|
589
|
+
}
|
|
590
|
+
/**
|
|
591
|
+
* Find candidate mixins (or rulesets, or both) that might match the target selector
|
|
592
|
+
*
|
|
593
|
+
* ...also...
|
|
594
|
+
*
|
|
595
|
+
* @todo - Not sure how recursion works here with the match overflow and returning
|
|
596
|
+
* proper arrays.
|
|
597
|
+
*/
|
|
598
|
+
find(keys, filterType = undefined, options = {}) {
|
|
599
|
+
let keyList;
|
|
600
|
+
if (isArray(keys)) {
|
|
601
|
+
keyList = keys;
|
|
602
|
+
}
|
|
603
|
+
else {
|
|
604
|
+
keyList = [keys];
|
|
605
|
+
}
|
|
606
|
+
if (!keyList?.length) {
|
|
607
|
+
return;
|
|
608
|
+
}
|
|
609
|
+
let rules = this.rules;
|
|
610
|
+
let { searchParents = true, local = false, candidates = new Set(), context, hasTarget = false } = options ?? {};
|
|
611
|
+
const mixinHasNoRequiredParams = (mixinNode) => {
|
|
612
|
+
const params = mixinNode.value.params;
|
|
613
|
+
if (!params || params.length === 0) {
|
|
614
|
+
return true;
|
|
615
|
+
}
|
|
616
|
+
for (const param of params.value) {
|
|
617
|
+
if (isNode(param, 'Rest')) {
|
|
618
|
+
continue;
|
|
619
|
+
}
|
|
620
|
+
if (isNode(param, 'VarDeclaration')) {
|
|
621
|
+
if (param.value.value instanceof Nil) {
|
|
622
|
+
return false;
|
|
623
|
+
}
|
|
624
|
+
continue;
|
|
625
|
+
}
|
|
626
|
+
if (isNode(param, 'Any') && param.options.role === 'property') {
|
|
627
|
+
return false;
|
|
628
|
+
}
|
|
629
|
+
return false;
|
|
630
|
+
}
|
|
631
|
+
return true;
|
|
632
|
+
};
|
|
633
|
+
// Track which Rules nodes we've already searched to prevent infinite recursion
|
|
634
|
+
// Use the searchedRules from options if it exists, otherwise create a new Set
|
|
635
|
+
const searchedRules = options?.searchedRules || new Set();
|
|
636
|
+
if (options) {
|
|
637
|
+
options.searchedRules = searchedRules;
|
|
638
|
+
}
|
|
639
|
+
while (rules) {
|
|
640
|
+
// Don't add to searchedRules yet - we'll add it after we finish searching (including children)
|
|
641
|
+
let [startKey, ...search] = keyList;
|
|
642
|
+
let registry = rules.getRegistry('mixin');
|
|
643
|
+
registry.indexPendingItems();
|
|
644
|
+
const existing = registry.index.get(startKey);
|
|
645
|
+
// Resolve interpolated selector starts (e.g. "@{a2}") against current context
|
|
646
|
+
// so unresolved-index keys can still match resolved call keys (e.g. ".foo").
|
|
647
|
+
let resolvedInterpolatedStartEntries = [];
|
|
648
|
+
if (context && typeof startKey === 'string' && !existing?.length) {
|
|
649
|
+
for (const [indexedKey, indexedEntries] of registry.index) {
|
|
650
|
+
const matchInterpolated = /^@\{(.+)\}$/.exec(indexedKey);
|
|
651
|
+
if (!matchInterpolated) {
|
|
652
|
+
continue;
|
|
653
|
+
}
|
|
654
|
+
const varName = matchInterpolated[1];
|
|
655
|
+
const maybeVar = rules.find('declaration', varName, 'VarDeclaration', {
|
|
656
|
+
context,
|
|
657
|
+
hasTarget,
|
|
658
|
+
filter: options?.filter
|
|
659
|
+
});
|
|
660
|
+
if (isNode(maybeVar, 'VarDeclaration')) {
|
|
661
|
+
const resolvedValue = String(maybeVar.value.value.valueOf?.() ?? maybeVar.value.value ?? '');
|
|
662
|
+
if (resolvedValue === startKey) {
|
|
663
|
+
resolvedInterpolatedStartEntries.push(...indexedEntries);
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
// With the new indexing (by local visible keys), nested rulesets are indexed under their own keys
|
|
669
|
+
// So we only need to check entries under the startKey - no need to scan all entries
|
|
670
|
+
let allEntriesToCheck = [];
|
|
671
|
+
if (existing) {
|
|
672
|
+
allEntriesToCheck.push(...existing);
|
|
673
|
+
}
|
|
674
|
+
if (resolvedInterpolatedStartEntries.length > 0) {
|
|
675
|
+
allEntriesToCheck.push(...resolvedInterpolatedStartEntries);
|
|
676
|
+
}
|
|
677
|
+
// Also check if any entries match the full search path (for compound selectors like .foo.bar)
|
|
678
|
+
const targetMatch = search.length === 0 ? [startKey] : search;
|
|
679
|
+
for (const entries of registry.index.values()) {
|
|
680
|
+
for (const entry of entries) {
|
|
681
|
+
if (arraysEqual(entry.match, targetMatch)) {
|
|
682
|
+
allEntriesToCheck.push(entry);
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
if (allEntriesToCheck.length > 0) {
|
|
687
|
+
const targetMatch = search.length === 0 ? [startKey] : search;
|
|
688
|
+
for (const { value, match } of allEntriesToCheck) {
|
|
689
|
+
if (filterType && value.type !== filterType) {
|
|
690
|
+
continue;
|
|
691
|
+
}
|
|
692
|
+
// If match equals targetMatch (search or [startKey] when search is empty), this IS the ruleset we're looking for
|
|
693
|
+
// Also, if search is empty and match is empty, this ruleset IS the startKey we're looking for
|
|
694
|
+
// BUT: For compound search paths (keyList.length > 1), we should NOT add the startKey mixin itself
|
|
695
|
+
// as a candidate when search.length === 0 && match.length === 0, because that means we found the startKey
|
|
696
|
+
// but haven't fully matched the compound path. The startKey should only be added as a candidate if we're
|
|
697
|
+
// doing a simple lookup (keyList.length === 1), where the startKey IS the full match.
|
|
698
|
+
if (arraysEqual(match, targetMatch)) {
|
|
699
|
+
(candidates ??= new Set()).add(value);
|
|
700
|
+
continue;
|
|
701
|
+
}
|
|
702
|
+
// Only add startKey mixin as candidate if we're doing a simple lookup (not a compound path)
|
|
703
|
+
if (search.length === 0 && match.length === 0 && keyList.length === 1) {
|
|
704
|
+
(candidates ??= new Set()).add(value);
|
|
705
|
+
continue;
|
|
706
|
+
}
|
|
707
|
+
// For compound paths, we don't add startKey as a candidate, but we still need to search inside it
|
|
708
|
+
// The recursive search below will handle finding nested mixins
|
|
709
|
+
// If match equals [startKey] OR match is empty (meaning this ruleset IS the startKey),
|
|
710
|
+
// we need to search inside it for the remaining search keys
|
|
711
|
+
// NOTE: We should search inside #theme even if we're not adding it as a candidate (for compound paths)
|
|
712
|
+
if (search.length > 0 && (arraysEqual(match, [startKey]) || match.length === 0)) {
|
|
713
|
+
if ((isNode(value, 'Ruleset'))
|
|
714
|
+
|| (isNode(value, 'Mixin') && mixinHasNoRequiredParams(value))) {
|
|
715
|
+
let subRules = value.value.rules;
|
|
716
|
+
const subMixinRegistry = subRules.getRegistry('mixin');
|
|
717
|
+
subMixinRegistry.indexPendingItems();
|
|
718
|
+
// With the new indexing, nested rulesets are indexed by their local visible keys
|
|
719
|
+
// So we can just do a normal recursive search - no need to check for matches ending with search
|
|
720
|
+
// When searching inside a nested ruleset with searchParents: false, we don't need searchedRules
|
|
721
|
+
// because we're not traversing the parent chain
|
|
722
|
+
subMixinRegistry.find(search, filterType, {
|
|
723
|
+
searchParents: false,
|
|
724
|
+
local,
|
|
725
|
+
candidates: candidates,
|
|
726
|
+
context,
|
|
727
|
+
filter: options?.filter,
|
|
728
|
+
hasTarget,
|
|
729
|
+
searchedRules: undefined // Not needed when searchParents is false
|
|
730
|
+
});
|
|
731
|
+
}
|
|
732
|
+
continue;
|
|
733
|
+
}
|
|
734
|
+
// If there are more search keys than match keys, recursively search inside this ruleset
|
|
735
|
+
// This handles cases where match is a prefix of search (e.g., match=[".foo"], search=[".foo", ".bar"])
|
|
736
|
+
// Or when match is empty (ruleset IS the startKey) and we need to search inside for the full search
|
|
737
|
+
const shouldRecurse = search.length > 0 && (search.length > match.length || match.length === 0);
|
|
738
|
+
const isPrefix = match.length > 0 && arraysEqual(match, search.slice(0, match.length));
|
|
739
|
+
if (shouldRecurse) {
|
|
740
|
+
let searchKeys;
|
|
741
|
+
if (match.length === 0) {
|
|
742
|
+
// Match is empty, meaning this ruleset IS the startKey, search inside for the full search
|
|
743
|
+
searchKeys = search;
|
|
744
|
+
}
|
|
745
|
+
else if (isPrefix) {
|
|
746
|
+
// Match is a prefix of search, search for the remainder after the match
|
|
747
|
+
searchKeys = search.slice(match.length);
|
|
748
|
+
}
|
|
749
|
+
else {
|
|
750
|
+
// Match is not a prefix of search - skip this ruleset, it doesn't match
|
|
751
|
+
continue;
|
|
752
|
+
}
|
|
753
|
+
if ((isNode(value, 'Ruleset'))
|
|
754
|
+
|| (isNode(value, 'Mixin') && mixinHasNoRequiredParams(value))) {
|
|
755
|
+
let subRules = value.value.rules;
|
|
756
|
+
const subMixinRegistry = subRules.getRegistry('mixin');
|
|
757
|
+
subMixinRegistry.indexPendingItems();
|
|
758
|
+
subMixinRegistry.find(searchKeys, filterType, {
|
|
759
|
+
searchParents: false,
|
|
760
|
+
local,
|
|
761
|
+
candidates: candidates,
|
|
762
|
+
context,
|
|
763
|
+
filter: options?.filter,
|
|
764
|
+
hasTarget,
|
|
765
|
+
searchedRules: searchedRules
|
|
766
|
+
});
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
// Always search children (old behavior)
|
|
772
|
+
const candidatesBeforeChildren = candidates ? new Set(candidates) : new Set();
|
|
773
|
+
registry._searchRulesChildren(startKey, 'Mixin', {
|
|
774
|
+
searchParents: false,
|
|
775
|
+
local,
|
|
776
|
+
candidates: candidates,
|
|
777
|
+
findAll: true,
|
|
778
|
+
childFilterType: filterType,
|
|
779
|
+
context,
|
|
780
|
+
filter: options?.filter,
|
|
781
|
+
hasTarget,
|
|
782
|
+
searchedRules: searchedRules
|
|
783
|
+
});
|
|
784
|
+
// After _searchRulesChildren, check if any new candidates are mixins/rulesets we should search inside
|
|
785
|
+
// This handles the case where #theme mixin is found in imported Rules and we need to search inside it
|
|
786
|
+
// Also, for compound paths, remove #theme from candidates if it was added by _searchRulesChildren
|
|
787
|
+
// because we only want to search inside it, not include it as a final candidate
|
|
788
|
+
if (candidates) {
|
|
789
|
+
const candidatesToRemove = [];
|
|
790
|
+
for (const candidate of candidates) {
|
|
791
|
+
const candidateNode = candidate;
|
|
792
|
+
// Only check candidates that were added by _searchRulesChildren (not in allEntriesToCheck)
|
|
793
|
+
if (!candidatesBeforeChildren.has(candidateNode)) {
|
|
794
|
+
const isMixin = isNode(candidateNode, 'Mixin');
|
|
795
|
+
const isRuleset = isNode(candidateNode, 'Ruleset');
|
|
796
|
+
const hasNoParams = isMixin && mixinHasNoRequiredParams(candidateNode);
|
|
797
|
+
// Check if this candidate matches the startKey.
|
|
798
|
+
// For rulesets discovered via child-search, key-set membership is the reliable signal.
|
|
799
|
+
const candidateKey = isMixin
|
|
800
|
+
? candidateNode.value.name?.valueOf?.()
|
|
801
|
+
: (isRuleset ? candidateNode.value.selector.valueOf?.() : '');
|
|
802
|
+
const matchesStartKey = isRuleset
|
|
803
|
+
? ((!isNode(candidateNode.value.selector, 'Nil') && candidateNode.value.selector.visibleKeySet.has(startKey))
|
|
804
|
+
|| (!isNode(candidateNode.value.selector, 'Nil') && candidateNode.value.selector.keySet.has(startKey)))
|
|
805
|
+
: candidateKey === startKey;
|
|
806
|
+
// For compound paths (keyList.length > 1), remove startKey from candidates if it was added by _searchRulesChildren
|
|
807
|
+
// because we only want to search inside it, not include it as a final candidate
|
|
808
|
+
if (matchesStartKey && keyList.length > 1) {
|
|
809
|
+
candidatesToRemove.push(candidateNode);
|
|
810
|
+
}
|
|
811
|
+
// Search inside the candidate if it matches startKey and we have remaining search keys
|
|
812
|
+
if (matchesStartKey && search.length > 0 && (isRuleset || hasNoParams)) {
|
|
813
|
+
let subRules = candidateNode.value.rules;
|
|
814
|
+
const subMixinRegistry = subRules.getRegistry('mixin');
|
|
815
|
+
subMixinRegistry.indexPendingItems();
|
|
816
|
+
subMixinRegistry.find(search, filterType, {
|
|
817
|
+
searchParents: false,
|
|
818
|
+
local,
|
|
819
|
+
candidates: candidates,
|
|
820
|
+
context,
|
|
821
|
+
filter: options?.filter,
|
|
822
|
+
hasTarget,
|
|
823
|
+
searchedRules: undefined // Not needed when searchParents is false
|
|
824
|
+
});
|
|
825
|
+
}
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
// Remove candidates that shouldn't be in the final result (for compound paths)
|
|
829
|
+
for (const candidateToRemove of candidatesToRemove) {
|
|
830
|
+
candidates.delete(candidateToRemove);
|
|
831
|
+
}
|
|
832
|
+
}
|
|
833
|
+
// Mark this Rules node as searched after we've finished searching it (including children)
|
|
834
|
+
searchedRules.add(rules);
|
|
835
|
+
if (!searchParents) {
|
|
836
|
+
break;
|
|
837
|
+
}
|
|
838
|
+
do {
|
|
839
|
+
rules = rules?.parent;
|
|
840
|
+
/**
|
|
841
|
+
* If we reach an import boundary, stop unless it's an `@import`
|
|
842
|
+
* which means these rules can reach into the parent file that imports
|
|
843
|
+
* this one.
|
|
844
|
+
*/
|
|
845
|
+
if (rules && isNode(rules.sourceNode, 'StyleImport') && rules.sourceNode.options.type !== 'import') {
|
|
846
|
+
rules = undefined;
|
|
847
|
+
break;
|
|
848
|
+
}
|
|
849
|
+
} while (rules && rules.type !== 'Rules');
|
|
850
|
+
}
|
|
851
|
+
// With compound keys parsed as arrays (e.g., ['#theme', '.dark', '.navbar', '.colors']),
|
|
852
|
+
// we can find all matches in one pass. The find() method handles compound keys by
|
|
853
|
+
// recursively searching inside nested rulesets for the remaining keys.
|
|
854
|
+
return candidates.size ? [...candidates] : undefined;
|
|
855
|
+
}
|
|
856
|
+
}
|
|
857
|
+
/**
|
|
858
|
+
* For either Sass, Jess, or JS functions.
|
|
859
|
+
*
|
|
860
|
+
* Less and Sass can register global functions that can be called from the language
|
|
861
|
+
* without a `@-use` directive.
|
|
862
|
+
*
|
|
863
|
+
* @todo Should the presence of `@-use` directives anywhere in the
|
|
864
|
+
* stylesheet tree cause these global functions to be disabled?
|
|
865
|
+
*/
|
|
866
|
+
export class FunctionRegistry extends Registry {
|
|
867
|
+
index = new Map();
|
|
868
|
+
cloneForRules(rules) {
|
|
869
|
+
const next = new FunctionRegistry(rules);
|
|
870
|
+
// Preserve any functions injected directly into the registry (Less plugin style).
|
|
871
|
+
next.index = new Map(this.index);
|
|
872
|
+
next.pendingItems = new Set(this.pendingItems);
|
|
873
|
+
return next;
|
|
874
|
+
}
|
|
875
|
+
indexPendingItems() {
|
|
876
|
+
for (const item of this.pendingItems) {
|
|
877
|
+
if (item instanceof JsFunction) {
|
|
878
|
+
this.index.set(item.name, item);
|
|
879
|
+
continue;
|
|
880
|
+
}
|
|
881
|
+
// Stylesheet-defined function node
|
|
882
|
+
const nameKey = item.nameKey;
|
|
883
|
+
if (nameKey) {
|
|
884
|
+
this.index.set(nameKey, item);
|
|
885
|
+
}
|
|
886
|
+
}
|
|
887
|
+
this.pendingItems.clear();
|
|
888
|
+
}
|
|
889
|
+
find(name, filterType, options) {
|
|
890
|
+
let fn;
|
|
891
|
+
let rules = this.rules;
|
|
892
|
+
let { searchParents = true } = options ?? {};
|
|
893
|
+
let findRoot = false;
|
|
894
|
+
while (rules) {
|
|
895
|
+
let registry = rules.functionRegistry;
|
|
896
|
+
if (registry) {
|
|
897
|
+
registry.indexPendingItems();
|
|
898
|
+
fn = registry.index.get(name);
|
|
899
|
+
if (fn || !searchParents) {
|
|
900
|
+
break;
|
|
901
|
+
}
|
|
902
|
+
}
|
|
903
|
+
do {
|
|
904
|
+
rules = rules?.parent;
|
|
905
|
+
if (findRoot && rules.type === 'Rules' && rules?.parent === undefined) {
|
|
906
|
+
/** We're at the root */
|
|
907
|
+
break;
|
|
908
|
+
}
|
|
909
|
+
/**
|
|
910
|
+
* If we reach an import boundary, skip the scope until we get to the top level.
|
|
911
|
+
*/
|
|
912
|
+
if (rules && isNode(rules.sourceNode, 'StyleImport') && rules.sourceNode.options.type !== 'import') {
|
|
913
|
+
findRoot = true;
|
|
914
|
+
}
|
|
915
|
+
} while (!findRoot && rules && rules.type !== 'Rules');
|
|
916
|
+
}
|
|
917
|
+
return fn;
|
|
918
|
+
}
|
|
919
|
+
add(nameOrItem, func) {
|
|
920
|
+
// If first argument is a JsFunction or Func, use base class behavior
|
|
921
|
+
if (nameOrItem instanceof JsFunction || nameOrItem?.type === 'Func') {
|
|
922
|
+
super.add(nameOrItem);
|
|
923
|
+
return;
|
|
924
|
+
}
|
|
925
|
+
// Otherwise, it's Less.js-compatible API: add(name, func)
|
|
926
|
+
if (typeof nameOrItem !== 'string' || func === undefined) {
|
|
927
|
+
throw new Error('FunctionRegistry.add() requires either a JsFunction or (name: string, func: JsFunction | Function)');
|
|
928
|
+
}
|
|
929
|
+
// Convert name to lowercase for Less.js compatibility
|
|
930
|
+
const lowerName = nameOrItem.toLowerCase();
|
|
931
|
+
// If func is already a JsFunction, use it directly
|
|
932
|
+
// Otherwise, create a new JsFunction from the raw function
|
|
933
|
+
const jsFunc = func instanceof JsFunction
|
|
934
|
+
? func
|
|
935
|
+
: new JsFunction({ name: lowerName, fn: func });
|
|
936
|
+
// Ensure the name is set
|
|
937
|
+
if (!jsFunc.name) {
|
|
938
|
+
jsFunc.name = lowerName;
|
|
939
|
+
}
|
|
940
|
+
// Add to pendingItems directly
|
|
941
|
+
this.pendingItems.add(jsFunc);
|
|
942
|
+
}
|
|
943
|
+
/**
|
|
944
|
+
* Less.js-compatible API: Add multiple functions at once
|
|
945
|
+
* @param functions Object mapping function names to functions
|
|
946
|
+
*/
|
|
947
|
+
addMultiple(functions) {
|
|
948
|
+
for (const [name, func] of Object.entries(functions)) {
|
|
949
|
+
this.add(name, func);
|
|
950
|
+
}
|
|
951
|
+
}
|
|
952
|
+
/**
|
|
953
|
+
* Less.js-compatible API: Get a function by name
|
|
954
|
+
* Uses case-insensitive lookup and searches parent chain
|
|
955
|
+
* @param name Function name (case-insensitive)
|
|
956
|
+
* @returns The function if found, undefined otherwise
|
|
957
|
+
*/
|
|
958
|
+
get(name) {
|
|
959
|
+
// Convert to lowercase for case-insensitive lookup
|
|
960
|
+
const lowerName = name.toLowerCase();
|
|
961
|
+
// First check local registry
|
|
962
|
+
this.indexPendingItems();
|
|
963
|
+
let fn = this.index.get(lowerName);
|
|
964
|
+
if (fn) {
|
|
965
|
+
return fn;
|
|
966
|
+
}
|
|
967
|
+
// If not found locally, use find() to search parent chain
|
|
968
|
+
// find() already handles parent traversal
|
|
969
|
+
return this.find(lowerName);
|
|
970
|
+
}
|
|
971
|
+
/**
|
|
972
|
+
* Less.js-compatible API: Get all local functions (without parent chain)
|
|
973
|
+
* @returns Object mapping function names to functions
|
|
974
|
+
*/
|
|
975
|
+
getLocalFunctions() {
|
|
976
|
+
this.indexPendingItems();
|
|
977
|
+
const result = {};
|
|
978
|
+
for (const [name, func] of this.index.entries()) {
|
|
979
|
+
result[name] = func;
|
|
980
|
+
}
|
|
981
|
+
return result;
|
|
982
|
+
}
|
|
983
|
+
/**
|
|
984
|
+
* Less.js-compatible API: Create a child registry that inherits from this one
|
|
985
|
+
* In Less.js, this creates a new registry with prototype inheritance.
|
|
986
|
+
* In Jess, we create a new registry that searches this one as a parent.
|
|
987
|
+
*
|
|
988
|
+
* @returns A new FunctionRegistry that will search this registry when functions aren't found locally
|
|
989
|
+
*/
|
|
990
|
+
inherit() {
|
|
991
|
+
// Create a new registry for the same Rules
|
|
992
|
+
// The new registry will use find() which searches parent chain
|
|
993
|
+
// We need to create a registry that references this one as parent
|
|
994
|
+
// Since FunctionRegistry.find() already searches parent Rules chain,
|
|
995
|
+
// we can create a new registry on the same Rules and it will naturally
|
|
996
|
+
// find functions in parent Rules. However, for true "inherit" behavior
|
|
997
|
+
// where we want to search THIS registry specifically, we need a different approach.
|
|
998
|
+
// For now, create a new registry on the same Rules
|
|
999
|
+
// The find() method will search up the Rules parent chain, which includes
|
|
1000
|
+
// this registry's Rules, so it should work correctly.
|
|
1001
|
+
const childRegistry = new FunctionRegistry(this.rules);
|
|
1002
|
+
// Store reference to parent registry for direct lookup
|
|
1003
|
+
// This allows the child to search the parent registry even if it's on the same Rules
|
|
1004
|
+
childRegistry._parentRegistry = this;
|
|
1005
|
+
// Override get() to check parent registry first
|
|
1006
|
+
const originalGet = childRegistry.get.bind(childRegistry);
|
|
1007
|
+
childRegistry.get = function (name) {
|
|
1008
|
+
// First check local registry
|
|
1009
|
+
this.indexPendingItems();
|
|
1010
|
+
const localFn = this.index.get(name.toLowerCase());
|
|
1011
|
+
if (localFn) {
|
|
1012
|
+
return localFn;
|
|
1013
|
+
}
|
|
1014
|
+
// Then check parent registry
|
|
1015
|
+
const parentRegistry = this._parentRegistry;
|
|
1016
|
+
if (parentRegistry) {
|
|
1017
|
+
const parentFn = parentRegistry.get(name);
|
|
1018
|
+
if (parentFn) {
|
|
1019
|
+
return parentFn;
|
|
1020
|
+
}
|
|
1021
|
+
}
|
|
1022
|
+
// Finally, use find() to search Rules parent chain
|
|
1023
|
+
return originalGet(name);
|
|
1024
|
+
}.bind(childRegistry);
|
|
1025
|
+
return childRegistry;
|
|
1026
|
+
}
|
|
1027
|
+
}
|
|
1028
|
+
/**
|
|
1029
|
+
*
|
|
1030
|
+
* @note - Keys of different types may overlap, but then are filtered when searching.
|
|
1031
|
+
* As in, a variable named `$foo` and a property named `foo` will be in the
|
|
1032
|
+
* same map.
|
|
1033
|
+
*/
|
|
1034
|
+
export class DeclarationRegistry extends Registry {
|
|
1035
|
+
index = new Map();
|
|
1036
|
+
indexPendingItems() {
|
|
1037
|
+
for (const item of this.pendingItems) {
|
|
1038
|
+
let key = item.value.name.valueOf();
|
|
1039
|
+
let set = this.index.get(key);
|
|
1040
|
+
if (set && set instanceof Set) {
|
|
1041
|
+
set.add(item);
|
|
1042
|
+
}
|
|
1043
|
+
else {
|
|
1044
|
+
this.index.set(key, new Set([item]));
|
|
1045
|
+
}
|
|
1046
|
+
}
|
|
1047
|
+
this.pendingItems.clear();
|
|
1048
|
+
}
|
|
1049
|
+
/**
|
|
1050
|
+
* Get declarations from map and nested rulesets.
|
|
1051
|
+
* This will return a list of all matching nodes.
|
|
1052
|
+
*
|
|
1053
|
+
* @todo - The pattern for mixins will be similar, no? Can this be
|
|
1054
|
+
* re-used / abstracted?
|
|
1055
|
+
*
|
|
1056
|
+
* @todo - Register declarations and index them only when searching.
|
|
1057
|
+
* This would be similar to how we index rulesets for extending.
|
|
1058
|
+
*/
|
|
1059
|
+
find(key, filterType = 'VarDeclaration', options) {
|
|
1060
|
+
let declCandidate = new Set();
|
|
1061
|
+
let optionalCandidates = options?.optionalCandidates ?? new Set();
|
|
1062
|
+
let rules = this.rules;
|
|
1063
|
+
let isPublic = false;
|
|
1064
|
+
let { searchParents = true, local = false, start } = options ?? {};
|
|
1065
|
+
let newReadonly = false;
|
|
1066
|
+
// Track visited Rules nodes in the parent chain to detect circular parent chains
|
|
1067
|
+
const visitedRules = new Set();
|
|
1068
|
+
while (rules) {
|
|
1069
|
+
// CRITICAL: Check for circular parent chain
|
|
1070
|
+
if (visitedRules.has(rules)) {
|
|
1071
|
+
throw new Error(`Circular parent chain detected in DeclarationRegistry.find`);
|
|
1072
|
+
}
|
|
1073
|
+
visitedRules.add(rules);
|
|
1074
|
+
let currentReadonly = options?.readonly || rules.options.readonly;
|
|
1075
|
+
newReadonly = currentReadonly;
|
|
1076
|
+
let registry = rules.getRegistry('declaration');
|
|
1077
|
+
registry.indexPendingItems();
|
|
1078
|
+
let set = registry.index.get(key);
|
|
1079
|
+
let list = set ? [...set] : undefined;
|
|
1080
|
+
if (list) {
|
|
1081
|
+
list = list.filter(n => n.type === filterType
|
|
1082
|
+
&& (!options?.filter
|
|
1083
|
+
|| options.filter(n)));
|
|
1084
|
+
// Sort using comparePosition for proper source order comparison
|
|
1085
|
+
if (list.length > 1) {
|
|
1086
|
+
list.sort((a, b) => {
|
|
1087
|
+
const pos = comparePosition(a, b);
|
|
1088
|
+
return pos ?? 0;
|
|
1089
|
+
});
|
|
1090
|
+
}
|
|
1091
|
+
}
|
|
1092
|
+
if (list?.length) {
|
|
1093
|
+
let result = rules.getRegistry('declaration')._findClosestByStart(list, start);
|
|
1094
|
+
if (result) {
|
|
1095
|
+
newReadonly ||= result.options.readonly;
|
|
1096
|
+
// Respect visibility on the currently searched Rules scope.
|
|
1097
|
+
// - private: never directly visible to lookup
|
|
1098
|
+
// - optional: only returned if no public match is found
|
|
1099
|
+
// - public: immediate candidate
|
|
1100
|
+
const currentRulesVisibility = rules.options.rulesVisibility?.[filterType] ?? '';
|
|
1101
|
+
if (currentRulesVisibility === 'private') {
|
|
1102
|
+
// Local lookups are allowed to read private declarations in their own scope.
|
|
1103
|
+
// Additionally, targeted reference resolution may set context.rulesContext
|
|
1104
|
+
// to this same Rules scope; allow private reads in that exact in-scope case.
|
|
1105
|
+
const inContextScope = options?.context?.rulesContext === rules;
|
|
1106
|
+
const contextRules = options?.context?.rulesContext;
|
|
1107
|
+
let inContextLineage = false;
|
|
1108
|
+
let contextCursor = contextRules;
|
|
1109
|
+
while (contextCursor) {
|
|
1110
|
+
if (contextCursor === rules) {
|
|
1111
|
+
inContextLineage = true;
|
|
1112
|
+
break;
|
|
1113
|
+
}
|
|
1114
|
+
contextCursor = contextCursor.parent;
|
|
1115
|
+
}
|
|
1116
|
+
const isParamVar = isNode(result, 'VarDeclaration') && Boolean(result.options?.paramVar);
|
|
1117
|
+
if ((local || inContextScope) && rules === this.rules) {
|
|
1118
|
+
declCandidate.add(result);
|
|
1119
|
+
isPublic = true;
|
|
1120
|
+
}
|
|
1121
|
+
else if (isParamVar && inContextLineage) {
|
|
1122
|
+
// Mixin parameters are lexical bindings and remain visible to descendant
|
|
1123
|
+
// scopes in the same invocation chain, even when declaration visibility is private.
|
|
1124
|
+
declCandidate.add(result);
|
|
1125
|
+
isPublic = true;
|
|
1126
|
+
}
|
|
1127
|
+
else if (options?.hasTarget === true) {
|
|
1128
|
+
// Targeted namespace lookups (e.g. @set[@key]) should still be able to
|
|
1129
|
+
// read private declaration members from the targeted rules.
|
|
1130
|
+
declCandidate.add(result);
|
|
1131
|
+
isPublic = true;
|
|
1132
|
+
}
|
|
1133
|
+
}
|
|
1134
|
+
else if (currentRulesVisibility === 'optional') {
|
|
1135
|
+
// Optional declarations are fallback-only: keep searching for public declarations
|
|
1136
|
+
// through the lookup chain and only return optional candidates if none are found.
|
|
1137
|
+
optionalCandidates.add(result);
|
|
1138
|
+
}
|
|
1139
|
+
else {
|
|
1140
|
+
declCandidate.add(result);
|
|
1141
|
+
isPublic = true;
|
|
1142
|
+
}
|
|
1143
|
+
}
|
|
1144
|
+
}
|
|
1145
|
+
// Initialize searchedRules to prevent infinite recursion when searching child Rules
|
|
1146
|
+
// This is critical: if a Rules node appears in its own children, we need to track it
|
|
1147
|
+
const searchedRules = options?.searchedRules ?? new Set();
|
|
1148
|
+
if (!searchedRules.has(rules)) {
|
|
1149
|
+
searchedRules.add(rules);
|
|
1150
|
+
}
|
|
1151
|
+
// CRITICAL: When searching children, we MUST set searchParents: false to prevent
|
|
1152
|
+
// infinite loops. Children searches should never traverse up the parent chain.
|
|
1153
|
+
// If options.searchParents is true, we're in a parent search context, and searching
|
|
1154
|
+
// children should not trigger another parent search.
|
|
1155
|
+
let searchChildrenOptions = {
|
|
1156
|
+
...options,
|
|
1157
|
+
searchParents: false, // Always false when searching children
|
|
1158
|
+
readonly: newReadonly,
|
|
1159
|
+
candidates: declCandidate,
|
|
1160
|
+
searchedRules: searchedRules
|
|
1161
|
+
};
|
|
1162
|
+
const searchRules = rules;
|
|
1163
|
+
searchChildrenOptions.optionalCandidates = optionalCandidates;
|
|
1164
|
+
searchRules.getRegistry('declaration')._searchRulesChildren(key, filterType, searchChildrenOptions);
|
|
1165
|
+
// After searching the CURRENT scope (index + children), if we found public declarations,
|
|
1166
|
+
// sort them, find the best one (closest to start or at bottom), and return immediately.
|
|
1167
|
+
// Otherwise, continue up the parent scope.
|
|
1168
|
+
if (declCandidate.size > 0) {
|
|
1169
|
+
let bestResult;
|
|
1170
|
+
// Use comparePosition to find the last declaration by source order
|
|
1171
|
+
const candidateArray = Array.from(declCandidate);
|
|
1172
|
+
if (candidateArray.length === 1) {
|
|
1173
|
+
bestResult = candidateArray[0];
|
|
1174
|
+
}
|
|
1175
|
+
else {
|
|
1176
|
+
// Sort by comparePosition and take the last one
|
|
1177
|
+
candidateArray.sort((a, b) => {
|
|
1178
|
+
const pos = comparePosition(a, b);
|
|
1179
|
+
return pos ?? 0;
|
|
1180
|
+
});
|
|
1181
|
+
bestResult = candidateArray[candidateArray.length - 1];
|
|
1182
|
+
}
|
|
1183
|
+
if (options && searchChildrenOptions.readonly) {
|
|
1184
|
+
options.readonly = true;
|
|
1185
|
+
}
|
|
1186
|
+
return bestResult;
|
|
1187
|
+
}
|
|
1188
|
+
// If we haven't found public candidates in the current scope, continue normal parent search
|
|
1189
|
+
// (optional candidates are tracked but we keep searching up the parent chain)
|
|
1190
|
+
if (isPublic || !searchParents) {
|
|
1191
|
+
if (options && searchChildrenOptions.readonly) {
|
|
1192
|
+
options.readonly = true;
|
|
1193
|
+
}
|
|
1194
|
+
const result = declCandidate.values().next().value;
|
|
1195
|
+
return result;
|
|
1196
|
+
}
|
|
1197
|
+
do {
|
|
1198
|
+
rules = rules?.parent;
|
|
1199
|
+
/** If we're searching linearly, update the start position to the parent node index */
|
|
1200
|
+
/**
|
|
1201
|
+
* If we reach an import boundary, stop unless it's an `@import`
|
|
1202
|
+
* which means these rules can reach into the parent file that imports
|
|
1203
|
+
* this one.
|
|
1204
|
+
*/
|
|
1205
|
+
if (rules && isNode(rules.sourceNode, 'StyleImport') && rules.sourceNode.options.type !== 'import') {
|
|
1206
|
+
rules = undefined;
|
|
1207
|
+
break;
|
|
1208
|
+
}
|
|
1209
|
+
} while (rules && rules.type !== 'Rules');
|
|
1210
|
+
}
|
|
1211
|
+
if (options && newReadonly) {
|
|
1212
|
+
options.readonly = true;
|
|
1213
|
+
}
|
|
1214
|
+
// After searching all parents, if we only have optional candidates, return the best one
|
|
1215
|
+
if (declCandidate.size === 0 && optionalCandidates.size > 0) {
|
|
1216
|
+
const optionalArray = Array.from(optionalCandidates);
|
|
1217
|
+
if (optionalArray.length === 1) {
|
|
1218
|
+
return optionalArray[0];
|
|
1219
|
+
}
|
|
1220
|
+
// Sort by comparePosition and take the last one
|
|
1221
|
+
optionalArray.sort((a, b) => {
|
|
1222
|
+
const pos = comparePosition(a, b);
|
|
1223
|
+
return pos ?? 0;
|
|
1224
|
+
});
|
|
1225
|
+
const optionalResult = optionalArray[optionalArray.length - 1];
|
|
1226
|
+
return optionalResult;
|
|
1227
|
+
}
|
|
1228
|
+
return declCandidate.values().next().value;
|
|
1229
|
+
}
|
|
1230
|
+
}
|
|
1231
|
+
function arraysEqual(a, b) {
|
|
1232
|
+
if (a.length !== b.length) {
|
|
1233
|
+
return false;
|
|
1234
|
+
}
|
|
1235
|
+
for (let i = 0; i < a.length; i++) {
|
|
1236
|
+
if (a[i] !== b[i]) {
|
|
1237
|
+
return false;
|
|
1238
|
+
}
|
|
1239
|
+
}
|
|
1240
|
+
return true;
|
|
1241
|
+
}
|
|
1242
|
+
//# sourceMappingURL=registry-utils.js.map
|