@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,2293 @@
|
|
|
1
|
+
import { Node, defineType, F_STATIC, F_VISIBLE } from './node.js';
|
|
2
|
+
import { Context } from '../context.js';
|
|
3
|
+
import { isNode } from './util/is-node.js';
|
|
4
|
+
import { comparePosition } from './util/compare.js';
|
|
5
|
+
import { cast } from './util/cast.js';
|
|
6
|
+
import { spaced, Sequence } from './sequence.js';
|
|
7
|
+
import { getPrintOptions } from './util/print.js';
|
|
8
|
+
import { atIndex } from './util/collections.js';
|
|
9
|
+
import { Bool } from './bool.js';
|
|
10
|
+
import * as Registries from './util/registry-utils.js';
|
|
11
|
+
import { processExtends } from './util/extend-roots.js';
|
|
12
|
+
import { pipe, isThenable, serialForEach } from '@jesscss/awaitable-pipe';
|
|
13
|
+
import { Nil } from './nil.js';
|
|
14
|
+
import { VarDeclaration } from './declaration-var.js';
|
|
15
|
+
import { Any } from './any.js';
|
|
16
|
+
import { List } from './list.js';
|
|
17
|
+
import { indent, normalizeIndent } from './util/serialize-helper.js';
|
|
18
|
+
import { freezeChildren } from './util/cloning.js';
|
|
19
|
+
const { isArray } = Array;
|
|
20
|
+
/**
|
|
21
|
+
* The class representing a "declaration list".
|
|
22
|
+
* CSS calls it this even though CSS Nesting
|
|
23
|
+
* adds a bunch more things that aren't declarations.
|
|
24
|
+
*
|
|
25
|
+
* Used by Ruleset and Mixin. Additionally, imports / use statements
|
|
26
|
+
* return rules.
|
|
27
|
+
*
|
|
28
|
+
* @example
|
|
29
|
+
* [
|
|
30
|
+
* (Declaration color: black;)
|
|
31
|
+
* (Declaration background-color: white;)
|
|
32
|
+
* ]
|
|
33
|
+
*/
|
|
34
|
+
export class Rules extends Node {
|
|
35
|
+
type = 'Rules';
|
|
36
|
+
shortType = 'rules';
|
|
37
|
+
allowRuleRoot = true;
|
|
38
|
+
allowRoot = true;
|
|
39
|
+
rulesetRegistry;
|
|
40
|
+
mixinRegistry;
|
|
41
|
+
declarationRegistry;
|
|
42
|
+
functionRegistry;
|
|
43
|
+
rulesIndexed = 0;
|
|
44
|
+
_indexing = false;
|
|
45
|
+
_indexRules() {
|
|
46
|
+
if (this._indexing) {
|
|
47
|
+
return; // Prevent recursive indexing
|
|
48
|
+
}
|
|
49
|
+
this._indexing = true;
|
|
50
|
+
try {
|
|
51
|
+
let value = this.value;
|
|
52
|
+
let length = value.length;
|
|
53
|
+
for (let i = this.rulesIndexed; i < length; i++) {
|
|
54
|
+
const node = value[i];
|
|
55
|
+
this.registerNode(node);
|
|
56
|
+
}
|
|
57
|
+
this.rulesIndexed = length;
|
|
58
|
+
}
|
|
59
|
+
finally {
|
|
60
|
+
this._indexing = false;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Rules are often cloned during `preEval()` when `context.preserveOriginalNodes`
|
|
65
|
+
* is enabled. If callers register functions/mixins/declarations on the parsed tree
|
|
66
|
+
* before evaluation (e.g. via visitors), those registries must survive cloning so
|
|
67
|
+
* lookups during evaluation work as expected.
|
|
68
|
+
*/
|
|
69
|
+
clone(deep, cloneFn) {
|
|
70
|
+
const newRules = super.clone(deep, cloneFn);
|
|
71
|
+
// Only preserve *function* registry across clones.
|
|
72
|
+
// This supports Less plugin compat, where plugins can inject functions into the registry
|
|
73
|
+
// without creating AST nodes that would be re-registered on clone.
|
|
74
|
+
//
|
|
75
|
+
// Do NOT reuse declaration/mixin/ruleset registries across clones; those should always
|
|
76
|
+
// be rebuilt from AST nodes via lazy indexing.
|
|
77
|
+
if (this.functionRegistry) {
|
|
78
|
+
newRules.functionRegistry = this.functionRegistry.cloneForRules(newRules);
|
|
79
|
+
}
|
|
80
|
+
// IMPORTANT: cloned Rules must re-index their own registries.
|
|
81
|
+
// Otherwise, a clone can inherit `rulesIndexed` from the source Rules (often == value.length),
|
|
82
|
+
// while having an empty/incorrect registry state, causing lookup misses (e.g. @c in detached-rulesets).
|
|
83
|
+
newRules.rulesIndexed = 0;
|
|
84
|
+
newRules._indexing = false;
|
|
85
|
+
newRules._rulesSet = undefined;
|
|
86
|
+
return newRules;
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Lazily create registries for types as needed.
|
|
90
|
+
*/
|
|
91
|
+
register(type, node) {
|
|
92
|
+
let registry = this[`${type}Registry`];
|
|
93
|
+
if (!registry) {
|
|
94
|
+
let className = `${type.charAt(0).toUpperCase()}${type.slice(1)}`;
|
|
95
|
+
let RegistryClass = Registries[`${className}Registry`];
|
|
96
|
+
registry = new RegistryClass(this);
|
|
97
|
+
this[`${type}Registry`] = registry;
|
|
98
|
+
}
|
|
99
|
+
const result = registry.add(node);
|
|
100
|
+
return result;
|
|
101
|
+
}
|
|
102
|
+
getRegistry(type) {
|
|
103
|
+
let registry = this[`${type}Registry`];
|
|
104
|
+
if (!registry) {
|
|
105
|
+
/**
|
|
106
|
+
* @note - Ideally we wouldn't create a registry object if we didn't have to,
|
|
107
|
+
* just to find. But the find methods have complex logic for searching parent
|
|
108
|
+
* and children rules / registries.
|
|
109
|
+
*/
|
|
110
|
+
let className = `${type.charAt(0).toUpperCase()}${type.slice(1)}`;
|
|
111
|
+
let RegistryClass = Registries[`${className}Registry`];
|
|
112
|
+
registry = new RegistryClass(this);
|
|
113
|
+
this[`${type}Registry`] = registry;
|
|
114
|
+
}
|
|
115
|
+
if (this.rulesIndexed < this.value.length) {
|
|
116
|
+
this._indexRules();
|
|
117
|
+
}
|
|
118
|
+
return registry;
|
|
119
|
+
}
|
|
120
|
+
find(type, keys, filterType, options = {}) {
|
|
121
|
+
let registry = this.getRegistry(type);
|
|
122
|
+
return registry.find(keys, filterType, options);
|
|
123
|
+
}
|
|
124
|
+
toString(options) {
|
|
125
|
+
if (!this.visible && !this.fullRender) {
|
|
126
|
+
return '';
|
|
127
|
+
}
|
|
128
|
+
options = getPrintOptions(options);
|
|
129
|
+
const w = options.writer;
|
|
130
|
+
const depth = options.depth;
|
|
131
|
+
const mark = w.mark();
|
|
132
|
+
const ctx = options.context;
|
|
133
|
+
const suppressedLeadingComments = [];
|
|
134
|
+
if (depth === 0) {
|
|
135
|
+
// Snapshot global emit-tracking so repeated `.toString()` calls remain stable.
|
|
136
|
+
const prevCharsetEmitted = ctx?.charsetEmitted;
|
|
137
|
+
const prevTopImports = ctx?.topImports ? [...ctx.topImports] : undefined;
|
|
138
|
+
// @charset must be first
|
|
139
|
+
if (ctx?.currentCharset && !ctx.charsetEmitted) {
|
|
140
|
+
const charset = ctx.currentCharset;
|
|
141
|
+
// Use capture to avoid double-writing (toTrimmedString writes to writer AND returns the string)
|
|
142
|
+
const charsetStr = w.capture(() => charset.toTrimmedString(options));
|
|
143
|
+
w.add(charsetStr, charset);
|
|
144
|
+
w.add('\n');
|
|
145
|
+
// Do not permanently flip `charsetEmitted` here; restore at end.
|
|
146
|
+
ctx.charsetEmitted = true;
|
|
147
|
+
}
|
|
148
|
+
// Less keeps leading comments before hoisted @import output.
|
|
149
|
+
const isCommentLike = (node) => {
|
|
150
|
+
const text = String(node.valueOf?.() ?? '').trimStart();
|
|
151
|
+
if (!text.startsWith('/*')) {
|
|
152
|
+
return false;
|
|
153
|
+
}
|
|
154
|
+
return isNode(node, 'Comment') || isNode(node, 'Any');
|
|
155
|
+
};
|
|
156
|
+
if (ctx?.topImports?.length) {
|
|
157
|
+
for (const node of this.value) {
|
|
158
|
+
if (!isCommentLike(node)) {
|
|
159
|
+
break;
|
|
160
|
+
}
|
|
161
|
+
const commentStr = w.capture(() => node.toTrimmedString(options));
|
|
162
|
+
w.add(normalizeIndent(commentStr, ''), node);
|
|
163
|
+
w.add('\n');
|
|
164
|
+
const wasVisible = node.hasFlag(F_VISIBLE);
|
|
165
|
+
suppressedLeadingComments.push({ node, visible: wasVisible });
|
|
166
|
+
if (wasVisible) {
|
|
167
|
+
node.removeFlag(F_VISIBLE);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
// @import must come after @charset but before other rules
|
|
172
|
+
if (ctx?.topImports?.length) {
|
|
173
|
+
for (const importRule of ctx.topImports) {
|
|
174
|
+
if (isNode(importRule, 'AtRule')) {
|
|
175
|
+
const importPrelude = importRule.value.prelude;
|
|
176
|
+
if (importPrelude && String(importPrelude.valueOf?.() ?? '').includes('$')) {
|
|
177
|
+
const maybePrelude = importPrelude.eval(ctx);
|
|
178
|
+
if (!isThenable(maybePrelude)) {
|
|
179
|
+
importRule.value.prelude = maybePrelude;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
const importStr = w.capture(() => importRule.toString(options));
|
|
184
|
+
w.add(normalizeIndent(importStr, ''), importRule);
|
|
185
|
+
w.add('\n');
|
|
186
|
+
}
|
|
187
|
+
// Do not permanently clear; restore at end.
|
|
188
|
+
}
|
|
189
|
+
// Restore global tracking (we only needed it during this print).
|
|
190
|
+
if (ctx) {
|
|
191
|
+
ctx.charsetEmitted = prevCharsetEmitted;
|
|
192
|
+
if (prevTopImports) {
|
|
193
|
+
ctx.topImports = prevTopImports;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
this.processPrePost('pre', '', options);
|
|
198
|
+
const bodyMark = w.mark();
|
|
199
|
+
const bodyStr = this.toTrimmedString(options);
|
|
200
|
+
const bodyEmitted = w.getSince(bodyMark);
|
|
201
|
+
if (bodyEmitted.length === 0 && bodyStr) {
|
|
202
|
+
w.add(bodyStr);
|
|
203
|
+
}
|
|
204
|
+
// At root level, ensure output ends with a single newline (standard for CSS files)
|
|
205
|
+
// Don't propagate all the last child's post content (which may have extra whitespace)
|
|
206
|
+
if (depth === 0) {
|
|
207
|
+
for (const suppressed of suppressedLeadingComments) {
|
|
208
|
+
if (suppressed.visible) {
|
|
209
|
+
suppressed.node.addFlag(F_VISIBLE);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
const result = w.getSince(mark).trimEnd();
|
|
213
|
+
// Ensure exactly one trailing newline (only if there's content)
|
|
214
|
+
return result ? result + '\n' : '';
|
|
215
|
+
}
|
|
216
|
+
return w.getSince(mark);
|
|
217
|
+
}
|
|
218
|
+
pendingExtends = new Set();
|
|
219
|
+
constructor(value, options, location, treeContext) {
|
|
220
|
+
let rulesVisibility = options?.rulesVisibility ?? {};
|
|
221
|
+
// Set defaults for API-created Rules. Parsers will override these as needed:
|
|
222
|
+
// - Less mixins/rulesets: VarDeclaration = 'optional', Mixin = 'public'
|
|
223
|
+
// - Sass mixins/rulesets: VarDeclaration = 'private', Mixin = 'private'
|
|
224
|
+
// - Imports: VarDeclaration = 'public', Mixin = 'public'
|
|
225
|
+
// Default to 'public' for API-created Rules (better DX - variables are accessible).
|
|
226
|
+
// If you want nested Rules to be private, set it explicitly.
|
|
227
|
+
rulesVisibility.Declaration ??= 'public';
|
|
228
|
+
rulesVisibility.Ruleset ??= 'public';
|
|
229
|
+
rulesVisibility.VarDeclaration ??= 'public';
|
|
230
|
+
rulesVisibility.Mixin ??= 'public';
|
|
231
|
+
// Merge with existing options to preserve rulesVisibility
|
|
232
|
+
const mergedOptions = { ...options, rulesVisibility };
|
|
233
|
+
super(value ?? [], mergedOptions, location, treeContext);
|
|
234
|
+
}
|
|
235
|
+
*[Symbol.iterator]() {
|
|
236
|
+
let value = this.value;
|
|
237
|
+
/**
|
|
238
|
+
* This should always be the case? But at one point something somewhere
|
|
239
|
+
* set the value to undefined I think, so just leaving this defensively.
|
|
240
|
+
*/
|
|
241
|
+
if (isArray(value)) {
|
|
242
|
+
yield* value.entries();
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
/**
|
|
246
|
+
* Used by Ruleset, Mixins, and AtRules etc to render
|
|
247
|
+
* rules with braces.
|
|
248
|
+
*/
|
|
249
|
+
toBraced(options) {
|
|
250
|
+
let opts = getPrintOptions(options);
|
|
251
|
+
// Use options.depth if provided, otherwise calculate from frameState
|
|
252
|
+
const depth = opts.depth;
|
|
253
|
+
const w = opts.writer;
|
|
254
|
+
const mark = w.mark();
|
|
255
|
+
let space = ''.padStart(depth * 2);
|
|
256
|
+
w.add('{');
|
|
257
|
+
// Set depth for _emitRulesBody - children should be one level deeper
|
|
258
|
+
const childOptions = { ...opts, depth: depth + 1 };
|
|
259
|
+
childOptions.writer.add('\n');
|
|
260
|
+
this._emitRulesBody(childOptions);
|
|
261
|
+
// ensure closing brace is on its own properly indented line
|
|
262
|
+
w.add('\n');
|
|
263
|
+
if (depth !== 0) {
|
|
264
|
+
w.add(space);
|
|
265
|
+
}
|
|
266
|
+
w.add('}');
|
|
267
|
+
// At root level (depth === 0), don't add a newline after the closing brace
|
|
268
|
+
// The parent _emitRulesBody will add the newline before the next item
|
|
269
|
+
// For nested rules (depth > 0), the newline is handled by the parent's _emitRulesBody
|
|
270
|
+
return w.getSince(mark);
|
|
271
|
+
}
|
|
272
|
+
_emitRulesBody(options) {
|
|
273
|
+
const w = options.writer;
|
|
274
|
+
const depth = options.depth ?? 0;
|
|
275
|
+
const space = indent(depth);
|
|
276
|
+
const { value } = this;
|
|
277
|
+
const referenceMode = Boolean(options.referenceMode);
|
|
278
|
+
const referenceRenderEnabled = referenceMode ? Boolean(options.referenceRenderEnabled) : true;
|
|
279
|
+
// Skip charset nodes - they are collected and prepended at root level
|
|
280
|
+
// Nil nodes are now non-visible, so they're automatically filtered by n.visible
|
|
281
|
+
const items = value.filter(n => n.visible);
|
|
282
|
+
if (items.length === 0) {
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
285
|
+
// No spacing flags; writer.capture is used where needed
|
|
286
|
+
const isInlineSourceRules = (node) => {
|
|
287
|
+
if (node.type !== 'Rules') {
|
|
288
|
+
return false;
|
|
289
|
+
}
|
|
290
|
+
const rulesNode = node;
|
|
291
|
+
if (rulesNode.value.length !== 1) {
|
|
292
|
+
return false;
|
|
293
|
+
}
|
|
294
|
+
const only = rulesNode.value[0];
|
|
295
|
+
return only.type === 'Any' && only.options?.role === 'any';
|
|
296
|
+
};
|
|
297
|
+
let emittedCount = 0;
|
|
298
|
+
let lastEmittedType;
|
|
299
|
+
let lastEmittedWasInlineSourceRules = false;
|
|
300
|
+
const isInMixinOutputScope = (node) => {
|
|
301
|
+
const seen = new Set();
|
|
302
|
+
const queue = [node];
|
|
303
|
+
while (queue.length > 0) {
|
|
304
|
+
const current = queue.shift();
|
|
305
|
+
if (seen.has(current)) {
|
|
306
|
+
continue;
|
|
307
|
+
}
|
|
308
|
+
seen.add(current);
|
|
309
|
+
if (current.options?.isMixinOutput === true) {
|
|
310
|
+
return true;
|
|
311
|
+
}
|
|
312
|
+
if (current.parent) {
|
|
313
|
+
queue.push(current.parent);
|
|
314
|
+
}
|
|
315
|
+
if (current.sourceParent && isNode(current.sourceParent)) {
|
|
316
|
+
queue.push(current.sourceParent);
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
return false;
|
|
320
|
+
};
|
|
321
|
+
for (let idx = 0; idx < items.length; idx++) {
|
|
322
|
+
const n = items[idx];
|
|
323
|
+
const isContainer = n.type === 'Ruleset' || n.type === 'AtRule' || n.type === 'Rules';
|
|
324
|
+
if (referenceMode && !referenceRenderEnabled && !isContainer) {
|
|
325
|
+
continue;
|
|
326
|
+
}
|
|
327
|
+
if (emittedCount > 0) {
|
|
328
|
+
// Check actual buffer state - not just previous captured output
|
|
329
|
+
// Frame closing in serializeRulesContainer adds newlines that aren't in the capture
|
|
330
|
+
const currentBuffer = w.getSince(0);
|
|
331
|
+
const bufferEndsWithNewline = currentBuffer.endsWith('\n');
|
|
332
|
+
const needsInlineBoundarySpacing = ((lastEmittedType === 'Any' && n.type !== 'Any')
|
|
333
|
+
|| (lastEmittedWasInlineSourceRules && n.type !== 'Any'));
|
|
334
|
+
if (!bufferEndsWithNewline || needsInlineBoundarySpacing) {
|
|
335
|
+
w.add('\n');
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
const isChildRules = n.type === 'Rules';
|
|
339
|
+
const isRulesetOrAtRule = n.type === 'Ruleset' || n.type === 'AtRule';
|
|
340
|
+
// Add indentation only for simple nodes (declarations, etc.)
|
|
341
|
+
// Ruleset and AtRule nodes indent themselves in renderOpening
|
|
342
|
+
if (!isChildRules && !isRulesetOrAtRule && depth !== 0) {
|
|
343
|
+
w.add(space);
|
|
344
|
+
}
|
|
345
|
+
// Emit directly to preserve source map segments
|
|
346
|
+
// For child Rules nodes, pass the same depth (don't increment depth)
|
|
347
|
+
// Rules nodes inside Rules nodes are at the same level
|
|
348
|
+
let childOptions = isChildRules
|
|
349
|
+
? { ...options, depth }
|
|
350
|
+
: { ...options, depth };
|
|
351
|
+
if (isChildRules) {
|
|
352
|
+
const inMixinOutputScope = isInMixinOutputScope(n);
|
|
353
|
+
const sourceIsCall = (n.sourceParent?.type === 'Call'
|
|
354
|
+
|| n.sourceNode?.sourceParent?.type === 'Call');
|
|
355
|
+
const ownReferenceMode = (n.options?.referenceMode === true
|
|
356
|
+
&& (!inMixinOutputScope || !sourceIsCall));
|
|
357
|
+
const childReferenceMode = referenceMode || ownReferenceMode;
|
|
358
|
+
const enteringReferenceMode = !referenceMode && ownReferenceMode;
|
|
359
|
+
const childReferenceRenderEnabled = childReferenceMode
|
|
360
|
+
? (enteringReferenceMode ? false : referenceRenderEnabled)
|
|
361
|
+
: true;
|
|
362
|
+
childOptions = {
|
|
363
|
+
...childOptions,
|
|
364
|
+
referenceMode: childReferenceMode,
|
|
365
|
+
referenceRenderEnabled: childReferenceRenderEnabled
|
|
366
|
+
};
|
|
367
|
+
}
|
|
368
|
+
let rule = w.capture(() => n.toTrimmedString(childOptions));
|
|
369
|
+
if (!rule && (n.type === 'Ruleset' || n.type === 'AtRule' || n.type === 'Rules')) {
|
|
370
|
+
continue;
|
|
371
|
+
}
|
|
372
|
+
w.add(rule, n); // Pass node as origin to preserve location info
|
|
373
|
+
if (n.requiredSemi && n.options.semi !== false) {
|
|
374
|
+
w.add(';', n);
|
|
375
|
+
}
|
|
376
|
+
emittedCount++;
|
|
377
|
+
lastEmittedType = n.type;
|
|
378
|
+
lastEmittedWasInlineSourceRules = isInlineSourceRules(n);
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
toTrimmedString(options) {
|
|
382
|
+
options = getPrintOptions(options);
|
|
383
|
+
const w = options.writer;
|
|
384
|
+
const mark = w.mark();
|
|
385
|
+
this._emitRulesBody(options);
|
|
386
|
+
return w.getSince(mark);
|
|
387
|
+
}
|
|
388
|
+
/** All rules, with nested rules flattened */
|
|
389
|
+
flatRules(visibleOnly = false) {
|
|
390
|
+
const finalRules = [];
|
|
391
|
+
const iterateRules = (rules) => {
|
|
392
|
+
for (let n of rules.value) {
|
|
393
|
+
if (isNode(n, 'Rules')) {
|
|
394
|
+
iterateRules(n);
|
|
395
|
+
continue;
|
|
396
|
+
}
|
|
397
|
+
if (!visibleOnly || n.visible || n.fullRender) {
|
|
398
|
+
finalRules.push(n);
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
};
|
|
402
|
+
iterateRules(this);
|
|
403
|
+
return finalRules;
|
|
404
|
+
}
|
|
405
|
+
visibleRules() {
|
|
406
|
+
return this.value.filter(n => n.visible);
|
|
407
|
+
}
|
|
408
|
+
toObject(convertToPrimitives = true) {
|
|
409
|
+
let output = new Map();
|
|
410
|
+
const iterateRules = (rules) => {
|
|
411
|
+
for (let n of rules.value) {
|
|
412
|
+
if (isNode(n, 'Declaration')) {
|
|
413
|
+
let { name, value, important } = n.value;
|
|
414
|
+
if (convertToPrimitives) {
|
|
415
|
+
let primitive = value.valueOf();
|
|
416
|
+
let outputValue = important ? `${primitive} ${important}` : primitive;
|
|
417
|
+
if (outputValue === undefined) {
|
|
418
|
+
continue;
|
|
419
|
+
}
|
|
420
|
+
output.set(name.toString(), outputValue);
|
|
421
|
+
}
|
|
422
|
+
else {
|
|
423
|
+
let outputValue = important ? new Sequence([n, important]) : n;
|
|
424
|
+
output.set(name.toString(), outputValue);
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
else if (n instanceof Rules) {
|
|
428
|
+
iterateRules(n);
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
};
|
|
432
|
+
iterateRules(this);
|
|
433
|
+
return Object.fromEntries(output);
|
|
434
|
+
}
|
|
435
|
+
/** @todo - Refactor? */
|
|
436
|
+
_rulesSet;
|
|
437
|
+
get rulesSet() {
|
|
438
|
+
return (this._rulesSet ??= []);
|
|
439
|
+
}
|
|
440
|
+
registerNode(node, options, _context) {
|
|
441
|
+
if (isNode(node, 'Rules')) {
|
|
442
|
+
// Use options if provided, otherwise use node's settings, otherwise empty
|
|
443
|
+
// Then merge with node's settings to preserve any values not in options
|
|
444
|
+
let optionsVisibility = options?.rulesVisibility;
|
|
445
|
+
let nodeVisibility = node.options.rulesVisibility ?? {};
|
|
446
|
+
let rulesVisibility = optionsVisibility
|
|
447
|
+
? { ...nodeVisibility, ...optionsVisibility }
|
|
448
|
+
: nodeVisibility;
|
|
449
|
+
/** Only Declaration and Ruleset are public by default.
|
|
450
|
+
* VarDeclaration visibility should be set by the parser (optional for Less, private for Jess/Sass).
|
|
451
|
+
* Mixin visibility should be set by the parser.
|
|
452
|
+
*/
|
|
453
|
+
rulesVisibility.Declaration ??= 'public';
|
|
454
|
+
rulesVisibility.Ruleset ??= 'public';
|
|
455
|
+
rulesVisibility.Mixin ??= 'public';
|
|
456
|
+
/** Either one set as readonly will win */
|
|
457
|
+
let readonly = Boolean(options?.readonly || node.options.readonly);
|
|
458
|
+
this.rulesSet.push({
|
|
459
|
+
node,
|
|
460
|
+
rulesVisibility,
|
|
461
|
+
readonly
|
|
462
|
+
});
|
|
463
|
+
// Note: Rulesets from imported Rules are registered in treeRoot's registry
|
|
464
|
+
// after evaluation completes (in evalNode), when treeRoot is guaranteed to be set
|
|
465
|
+
}
|
|
466
|
+
else if (isNode(node, 'Declaration')) {
|
|
467
|
+
/**
|
|
468
|
+
* setDefined works like Sass's !default flag - it finds the original variable
|
|
469
|
+
* declaration and inserts a new declaration at the same rules level as the
|
|
470
|
+
* found variable, but before the current nested node.
|
|
471
|
+
*/
|
|
472
|
+
if (node.options?.setDefined) {
|
|
473
|
+
// Skip setDefined logic if we're currently indexing to avoid recursive calls
|
|
474
|
+
if (this._indexing) {
|
|
475
|
+
// We'll handle setDefined after indexing is complete
|
|
476
|
+
return;
|
|
477
|
+
}
|
|
478
|
+
let key = node.value.name?.toString();
|
|
479
|
+
/** Don't set within sibling rules */
|
|
480
|
+
let opts = {};
|
|
481
|
+
opts.searchParents = true;
|
|
482
|
+
// Don't use start when searching parents - we want to find variables in parent regardless of position
|
|
483
|
+
// start is only relevant for finding variables before the current node in the same Rules
|
|
484
|
+
opts.start = undefined;
|
|
485
|
+
// node.type is 'VarDeclaration' or 'Declaration', use it directly as filterType
|
|
486
|
+
let result = this.find('declaration', key, node.type, opts);
|
|
487
|
+
if (result) {
|
|
488
|
+
if (result.options?.readonly || opts.readonly) {
|
|
489
|
+
throw new ReferenceError(`"${key}" is readonly`);
|
|
490
|
+
}
|
|
491
|
+
// Find the Rules node that contains the found declaration
|
|
492
|
+
let foundRules = result.parent;
|
|
493
|
+
if (!foundRules) {
|
|
494
|
+
throw new Error(`Could not find parent Rules for declaration '${key}'`);
|
|
495
|
+
}
|
|
496
|
+
// Create a new declaration with the same name but our value
|
|
497
|
+
const newDeclaration = node.copy();
|
|
498
|
+
newDeclaration.options = { ...newDeclaration.options };
|
|
499
|
+
newDeclaration.options.setDefined = undefined; // Remove setDefined flag
|
|
500
|
+
// Adopt the new declaration to the found Rules
|
|
501
|
+
foundRules.adopt(newDeclaration);
|
|
502
|
+
// Add to the value array AFTER the found declaration
|
|
503
|
+
// This ensures it shadows the original and is evaluated after it
|
|
504
|
+
const foundIndex = foundRules.value.indexOf(result);
|
|
505
|
+
if (foundIndex !== -1) {
|
|
506
|
+
foundRules.value.splice(foundIndex + 1, 0, newDeclaration);
|
|
507
|
+
}
|
|
508
|
+
else {
|
|
509
|
+
// If not found in array, add at the beginning
|
|
510
|
+
foundRules.value.unshift(newDeclaration);
|
|
511
|
+
}
|
|
512
|
+
// Register it via registerNode to ensure it's properly indexed
|
|
513
|
+
// Note: registerNode will call register('declaration', ...) which adds to registry
|
|
514
|
+
// We skip setDefined processing since we already removed the flag
|
|
515
|
+
foundRules.registerNode(newDeclaration);
|
|
516
|
+
}
|
|
517
|
+
else {
|
|
518
|
+
throw new ReferenceError(`"${key}" is not defined`);
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
this.register('declaration', node);
|
|
522
|
+
}
|
|
523
|
+
else if (isNode(node, 'Ruleset')) {
|
|
524
|
+
// Register to 'mixin' for mixin calls
|
|
525
|
+
// Always register - guard filtering happens at call time in getFunctionFromMixins
|
|
526
|
+
// Note: 'ruleset' registration for extends now happens in Ruleset.preEval to the extend root's registry
|
|
527
|
+
this.register('mixin', node);
|
|
528
|
+
}
|
|
529
|
+
else if (isNode(node, 'Mixin')) {
|
|
530
|
+
this.register('mixin', node);
|
|
531
|
+
}
|
|
532
|
+
else if (isNode(node, 'Func')) {
|
|
533
|
+
this.register('function', node);
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
push(...nodes) {
|
|
537
|
+
for (let node of nodes) {
|
|
538
|
+
this.adopt(node);
|
|
539
|
+
this.value.push(node);
|
|
540
|
+
this.registerNode(node);
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
at(index) {
|
|
544
|
+
return atIndex(this.value, index);
|
|
545
|
+
}
|
|
546
|
+
/**
|
|
547
|
+
* This traverses deeply to visit all nodes, but indexes locally.
|
|
548
|
+
*/
|
|
549
|
+
preEval(context) {
|
|
550
|
+
if (!this.preEvaluated) {
|
|
551
|
+
context.depth++;
|
|
552
|
+
let rules = this.maybeClone(context);
|
|
553
|
+
// When this is the nestable at-rule wrapper (one child Ruleset(&)), do not clone so
|
|
554
|
+
// inner rulesets register to the same object we push and register as extend root.
|
|
555
|
+
const nestableAtRuleNames = new Set(['@media', '@supports', '@layer', '@container', '@scope']);
|
|
556
|
+
const parentAtRule = this.parent?.type === 'AtRule' ? this.parent : null;
|
|
557
|
+
const isNestableAtRuleBody = parentAtRule
|
|
558
|
+
&& nestableAtRuleNames.has(String(parentAtRule.value?.name?.valueOf?.() ?? ''));
|
|
559
|
+
const first = rules.value?.[0];
|
|
560
|
+
const isWrapper = isNestableAtRuleBody
|
|
561
|
+
&& rules.value?.length === 1
|
|
562
|
+
&& isNode(first, 'Ruleset')
|
|
563
|
+
&& isNode(first.value?.selector, 'Ampersand');
|
|
564
|
+
if (isWrapper) {
|
|
565
|
+
rules = this;
|
|
566
|
+
}
|
|
567
|
+
rules.preEvaluated = true;
|
|
568
|
+
// Save current context and set up new context for variable lookups during preEval
|
|
569
|
+
const saved = this._snapshotContext(context);
|
|
570
|
+
this._setupContextForRules(context, rules);
|
|
571
|
+
// Set context.root early if this is the main root
|
|
572
|
+
const isMainRoot = !context.root;
|
|
573
|
+
if (isMainRoot) {
|
|
574
|
+
context.root = rules;
|
|
575
|
+
}
|
|
576
|
+
/**
|
|
577
|
+
* I think maybe we can just set the index to the actual order?
|
|
578
|
+
*/
|
|
579
|
+
for (let i = 0; i < rules.value.length; i++) {
|
|
580
|
+
let n = rules.value[i];
|
|
581
|
+
n.index = i;
|
|
582
|
+
}
|
|
583
|
+
// Preserve parent when cloning - if this Rules is inside a ruleset, maintain the parent relationship
|
|
584
|
+
if (this.parent && !rules.parent) {
|
|
585
|
+
this.parent.adopt(rules);
|
|
586
|
+
}
|
|
587
|
+
// Set context.root if not already set (needed for preEval visitors)
|
|
588
|
+
if (!context.root) {
|
|
589
|
+
context.root = rules;
|
|
590
|
+
}
|
|
591
|
+
// When getTree() set context.root to the original Rules but we're processing a clone,
|
|
592
|
+
// use the clone as context.root so registerRoot/pushExtendRoot run and rulesets register to the clone (extend fix).
|
|
593
|
+
if (context.root === this && this !== rules) {
|
|
594
|
+
context.root = rules;
|
|
595
|
+
}
|
|
596
|
+
// Register main root as extend root if this is the root (needed for extends in preEval)
|
|
597
|
+
// Check rules === context.root at registration time (not using stale isMainRoot)
|
|
598
|
+
if (rules === context.root && !context.extendRoots.root) {
|
|
599
|
+
context.extendRoots.registerRoot(rules);
|
|
600
|
+
context.extendRoots.pushExtendRoot(rules);
|
|
601
|
+
}
|
|
602
|
+
// Always push nestable at-rule body so inner rulesets register to it (not document root).
|
|
603
|
+
// Needed for both: wrapper (collapseNesting) and direct body (collapseNesting: false).
|
|
604
|
+
if (isNestableAtRuleBody) {
|
|
605
|
+
context.extendRoots.pushExtendRoot(rules);
|
|
606
|
+
}
|
|
607
|
+
// Multi-pass registration system for handling interpolated names
|
|
608
|
+
const mp = this._multiPassPreEval(rules, context, saved);
|
|
609
|
+
const popNestableBody = () => {
|
|
610
|
+
if (isNestableAtRuleBody) {
|
|
611
|
+
context.extendRoots.popExtendRoot();
|
|
612
|
+
}
|
|
613
|
+
};
|
|
614
|
+
if (isThenable(mp)) {
|
|
615
|
+
return mp.then((result) => {
|
|
616
|
+
popNestableBody();
|
|
617
|
+
return result;
|
|
618
|
+
});
|
|
619
|
+
}
|
|
620
|
+
popNestableBody();
|
|
621
|
+
return mp;
|
|
622
|
+
}
|
|
623
|
+
return this;
|
|
624
|
+
}
|
|
625
|
+
/**
|
|
626
|
+
* Multi-pass preEval system to handle interpolated names and dependencies
|
|
627
|
+
*/
|
|
628
|
+
_multiPassPreEval(rules, context, saved) {
|
|
629
|
+
// First pass: Only register nodes with static names
|
|
630
|
+
const staticNodes = [];
|
|
631
|
+
const dynamicNodes = [];
|
|
632
|
+
// Process each node with static name, handling both sync and async preEval
|
|
633
|
+
const processResult = serialForEach(rules.value, (node, index) => {
|
|
634
|
+
// Check if node has a static name (can be registered immediately)
|
|
635
|
+
if (node.type === 'Any' && node.options.role === 'charset') {
|
|
636
|
+
/** Special case where we register the charset node immediately */
|
|
637
|
+
rules.value[index] = node.preEval(context);
|
|
638
|
+
return;
|
|
639
|
+
}
|
|
640
|
+
if (this._hasStaticName(node)) {
|
|
641
|
+
// Pre-evaluate nodes with static names before registration
|
|
642
|
+
// This ensures selectors are evaluated and keySets are available for rulesets
|
|
643
|
+
const preEvald = node.preEval(context);
|
|
644
|
+
if (isThenable(preEvald)) {
|
|
645
|
+
return preEvald.then((preEvaldNode) => {
|
|
646
|
+
rules.value[index] = preEvaldNode;
|
|
647
|
+
preEvaldNode.index = index;
|
|
648
|
+
// After async preEval, check if it still has a static name
|
|
649
|
+
if (this._hasStaticName(preEvaldNode)) {
|
|
650
|
+
staticNodes.push(preEvaldNode);
|
|
651
|
+
this._registerNodeIfEligible(rules, preEvaldNode, context);
|
|
652
|
+
}
|
|
653
|
+
else {
|
|
654
|
+
dynamicNodes.push(preEvaldNode);
|
|
655
|
+
}
|
|
656
|
+
});
|
|
657
|
+
}
|
|
658
|
+
rules.value[index] = preEvald;
|
|
659
|
+
preEvald.index = index;
|
|
660
|
+
const nodeToRegister = preEvald;
|
|
661
|
+
staticNodes.push(nodeToRegister);
|
|
662
|
+
this._registerNodeIfEligible(rules, nodeToRegister, context);
|
|
663
|
+
}
|
|
664
|
+
else {
|
|
665
|
+
dynamicNodes.push(node);
|
|
666
|
+
}
|
|
667
|
+
});
|
|
668
|
+
const finish = () => {
|
|
669
|
+
// If no dynamic nodes, we're done
|
|
670
|
+
if (dynamicNodes.length === 0) {
|
|
671
|
+
// Restore context after preEval is complete
|
|
672
|
+
context.rulesContext = saved.rulesContext;
|
|
673
|
+
context.treeRoot = saved.treeRoot;
|
|
674
|
+
// Only restore context.root if saved.root is defined (not the outermost root)
|
|
675
|
+
// If saved.root is undefined, it means we're at the outermost level, so keep context.root as is
|
|
676
|
+
if (saved.root !== undefined) {
|
|
677
|
+
context.root = saved.root;
|
|
678
|
+
}
|
|
679
|
+
return rules;
|
|
680
|
+
}
|
|
681
|
+
// Multi-pass resolution of dynamic nodes
|
|
682
|
+
return this._resolveDynamicNodes(rules, context, saved, dynamicNodes);
|
|
683
|
+
};
|
|
684
|
+
if (isThenable(processResult)) {
|
|
685
|
+
return processResult.then(() => finish());
|
|
686
|
+
}
|
|
687
|
+
return finish();
|
|
688
|
+
}
|
|
689
|
+
/**
|
|
690
|
+
* Helper to check if a value is static (either a Node with F_STATIC flag or a primitive value)
|
|
691
|
+
*/
|
|
692
|
+
_isStatic(value) {
|
|
693
|
+
if (value && typeof value.hasFlag === 'function') {
|
|
694
|
+
return value.hasFlag(F_STATIC);
|
|
695
|
+
}
|
|
696
|
+
// Primitive values (strings, numbers, etc.) are considered static
|
|
697
|
+
return true;
|
|
698
|
+
}
|
|
699
|
+
/**
|
|
700
|
+
* Check if a node has a static name that can be registered immediately
|
|
701
|
+
*/
|
|
702
|
+
_hasStaticName(node) {
|
|
703
|
+
if (isNode(node, 'VarDeclaration')) {
|
|
704
|
+
const name = node.value.name;
|
|
705
|
+
return this._isStatic(name);
|
|
706
|
+
}
|
|
707
|
+
if (isNode(node, 'Mixin')) {
|
|
708
|
+
const name = node.value.name;
|
|
709
|
+
return this._isStatic(name);
|
|
710
|
+
}
|
|
711
|
+
if (isNode(node, 'StyleImport')) {
|
|
712
|
+
const path = node.value.path;
|
|
713
|
+
return this._isStatic(path);
|
|
714
|
+
}
|
|
715
|
+
if (isNode(node, 'Ruleset')) {
|
|
716
|
+
const selector = node.value.selector;
|
|
717
|
+
// BasicSelector, CompoundSelector, ComplexSelector etc. are always static
|
|
718
|
+
// Only Interpolated selectors need resolution
|
|
719
|
+
if (isNode(selector, ['BasicSelector', 'CompoundSelector', 'ComplexSelector', 'SelectorList'])) {
|
|
720
|
+
return true;
|
|
721
|
+
}
|
|
722
|
+
// After preEval, the selector should be resolved to static identifiers
|
|
723
|
+
if (node.preEvaluated) {
|
|
724
|
+
return true;
|
|
725
|
+
}
|
|
726
|
+
// Check F_STATIC flag for other selector types
|
|
727
|
+
if (selector && 'hasFlag' in selector && typeof selector.hasFlag === 'function') {
|
|
728
|
+
return selector.hasFlag(F_STATIC);
|
|
729
|
+
}
|
|
730
|
+
return false;
|
|
731
|
+
}
|
|
732
|
+
// For other node types, assume they can be registered if they have static names
|
|
733
|
+
return node.hasFlag(F_STATIC);
|
|
734
|
+
}
|
|
735
|
+
/**
|
|
736
|
+
* Register a node if it's eligible for registration
|
|
737
|
+
*/
|
|
738
|
+
_registerNodeIfEligible(rules, node, _context) {
|
|
739
|
+
if (isNode(node, 'Declaration')) {
|
|
740
|
+
rules.registerNode(node);
|
|
741
|
+
}
|
|
742
|
+
else if (isNode(node, 'Mixin')) {
|
|
743
|
+
rules.registerNode(node);
|
|
744
|
+
}
|
|
745
|
+
else if (isNode(node, 'Ruleset')) {
|
|
746
|
+
// registerNode handles both 'mixin' and 'ruleset' registries
|
|
747
|
+
rules.registerNode(node);
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
/**
|
|
751
|
+
* Multi-pass resolution of dynamic nodes with interpolated names
|
|
752
|
+
*/
|
|
753
|
+
_resolveDynamicNodes(rules, context, saved, dynamicNodes) {
|
|
754
|
+
const unresolvedNodes = [...dynamicNodes];
|
|
755
|
+
const resolvedNodes = [];
|
|
756
|
+
let firstError;
|
|
757
|
+
let resolutionAttempts = 0;
|
|
758
|
+
const MAX_RESOLUTION_ATTEMPTS = 5;
|
|
759
|
+
const attemptResolution = () => {
|
|
760
|
+
resolutionAttempts++;
|
|
761
|
+
if (resolutionAttempts > MAX_RESOLUTION_ATTEMPTS) {
|
|
762
|
+
throw new Error(`Could not resolve node.`);
|
|
763
|
+
}
|
|
764
|
+
const stillUnresolved = [];
|
|
765
|
+
let madeProgress = false;
|
|
766
|
+
for (const node of unresolvedNodes) {
|
|
767
|
+
try {
|
|
768
|
+
// Try to preEval the node
|
|
769
|
+
const result = node.preEval(context);
|
|
770
|
+
if (isThenable(result)) {
|
|
771
|
+
// Handle async preEval
|
|
772
|
+
return result.then((resolvedNode) => {
|
|
773
|
+
if (resolvedNode.index === undefined) {
|
|
774
|
+
resolvedNode.index = node.index;
|
|
775
|
+
}
|
|
776
|
+
if (!resolvedNode.sourceNode) {
|
|
777
|
+
resolvedNode.sourceNode = node.sourceNode ?? node;
|
|
778
|
+
}
|
|
779
|
+
// Register rulesets after preEval regardless of static name
|
|
780
|
+
if (resolvedNode.type === 'Ruleset') {
|
|
781
|
+
// registerNode handles both 'mixin' and 'ruleset' registries
|
|
782
|
+
rules.registerNode(resolvedNode);
|
|
783
|
+
}
|
|
784
|
+
if (isNode(resolvedNode, 'Nil') || this._hasStaticName(resolvedNode)) {
|
|
785
|
+
resolvedNodes.push(resolvedNode);
|
|
786
|
+
this._registerNodeIfEligible(rules, resolvedNode, context);
|
|
787
|
+
madeProgress = true;
|
|
788
|
+
}
|
|
789
|
+
else {
|
|
790
|
+
stillUnresolved.push(resolvedNode);
|
|
791
|
+
}
|
|
792
|
+
return attemptResolution();
|
|
793
|
+
});
|
|
794
|
+
}
|
|
795
|
+
// Register rulesets after preEval regardless of static name
|
|
796
|
+
if (result.index === undefined) {
|
|
797
|
+
result.index = node.index;
|
|
798
|
+
}
|
|
799
|
+
if (!result.sourceNode) {
|
|
800
|
+
result.sourceNode = node.sourceNode ?? node;
|
|
801
|
+
}
|
|
802
|
+
if (result.type === 'Ruleset') {
|
|
803
|
+
// registerNode handles both 'mixin' and 'ruleset' registries
|
|
804
|
+
rules.registerNode(result);
|
|
805
|
+
}
|
|
806
|
+
// Check if the node now has a static name
|
|
807
|
+
if (isNode(result, 'Nil') || this._hasStaticName(result)) {
|
|
808
|
+
resolvedNodes.push(result);
|
|
809
|
+
this._registerNodeIfEligible(rules, result, context);
|
|
810
|
+
madeProgress = true;
|
|
811
|
+
}
|
|
812
|
+
else {
|
|
813
|
+
stillUnresolved.push(result);
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
catch (error) {
|
|
817
|
+
if (!firstError) {
|
|
818
|
+
firstError = error;
|
|
819
|
+
}
|
|
820
|
+
stillUnresolved.push(node);
|
|
821
|
+
}
|
|
822
|
+
}
|
|
823
|
+
// Update the rules with resolved nodes
|
|
824
|
+
for (let i = 0; i < rules.value.length; i++) {
|
|
825
|
+
const node = rules.value[i];
|
|
826
|
+
const resolvedNode = resolvedNodes.find(n => n.index === node.index);
|
|
827
|
+
if (resolvedNode && resolvedNode !== node) {
|
|
828
|
+
rules.value[i] = resolvedNode.inherit(node);
|
|
829
|
+
rules.adopt(resolvedNode);
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
// If we made progress, try again
|
|
833
|
+
if (madeProgress && stillUnresolved.length > 0) {
|
|
834
|
+
unresolvedNodes.length = 0;
|
|
835
|
+
unresolvedNodes.push(...stillUnresolved);
|
|
836
|
+
return attemptResolution();
|
|
837
|
+
}
|
|
838
|
+
// If we still have unresolved nodes and we're done with rules evaluation, throw the first error
|
|
839
|
+
if (stillUnresolved.length > 0 && firstError) {
|
|
840
|
+
throw firstError;
|
|
841
|
+
}
|
|
842
|
+
// Restore context after preEval is complete
|
|
843
|
+
context.rulesContext = saved.rulesContext;
|
|
844
|
+
context.treeRoot = saved.treeRoot;
|
|
845
|
+
// Only restore context.root if saved.root is defined (not the outermost root)
|
|
846
|
+
// If saved.root is undefined, it means we're at the outermost level, so keep context.root as is
|
|
847
|
+
if (saved.root !== undefined) {
|
|
848
|
+
context.root = saved.root;
|
|
849
|
+
}
|
|
850
|
+
return rules;
|
|
851
|
+
};
|
|
852
|
+
return attemptResolution();
|
|
853
|
+
}
|
|
854
|
+
/**
|
|
855
|
+
* Helper method to continue preEval'ing remaining children after an async preEval.
|
|
856
|
+
*/
|
|
857
|
+
_preEvalRemainingChildren(rules, context, startIndex, saved) {
|
|
858
|
+
for (let i = startIndex; i < rules.value.length; i++) {
|
|
859
|
+
const node = rules.value[i];
|
|
860
|
+
// Always call preEval to ensure deep traversal and name resolution
|
|
861
|
+
const result = node.preEval(context);
|
|
862
|
+
if (isThenable(result)) {
|
|
863
|
+
// Handle async preEval by returning a promise that resolves after all children
|
|
864
|
+
return result.then((resolvedNode) => {
|
|
865
|
+
// Update the node if preEval returned a different instance
|
|
866
|
+
if (resolvedNode !== node) {
|
|
867
|
+
rules.value[i] = resolvedNode;
|
|
868
|
+
rules.adopt(resolvedNode);
|
|
869
|
+
}
|
|
870
|
+
// Register the node after preEval (name resolution) if not already registered
|
|
871
|
+
if (!isNode(node, 'VarDeclaration')) {
|
|
872
|
+
rules.registerNode(resolvedNode);
|
|
873
|
+
}
|
|
874
|
+
// Continue with the rest of the children
|
|
875
|
+
return this._preEvalRemainingChildren(rules, context, i + 1, saved);
|
|
876
|
+
});
|
|
877
|
+
}
|
|
878
|
+
// Update the node if preEval returned a different instance
|
|
879
|
+
if (result !== node) {
|
|
880
|
+
rules.value[i] = result;
|
|
881
|
+
rules.adopt(result);
|
|
882
|
+
}
|
|
883
|
+
// Register the node after preEval (name resolution) if not already registered
|
|
884
|
+
if (!isNode(node, 'VarDeclaration')) {
|
|
885
|
+
rules.registerNode(result);
|
|
886
|
+
}
|
|
887
|
+
}
|
|
888
|
+
// Restore context after preEval is complete (for async case)
|
|
889
|
+
if (saved) {
|
|
890
|
+
context.rulesContext = saved.rulesContext;
|
|
891
|
+
context.treeRoot = saved.treeRoot;
|
|
892
|
+
// Only restore context.root if saved.root is defined (not the outermost root)
|
|
893
|
+
// If saved.root is undefined, it means we're at the outermost level, so keep context.root as is
|
|
894
|
+
if (saved.root !== undefined) {
|
|
895
|
+
context.root = saved.root;
|
|
896
|
+
}
|
|
897
|
+
}
|
|
898
|
+
return rules;
|
|
899
|
+
}
|
|
900
|
+
/** Save current context roots to restore later */
|
|
901
|
+
_snapshotContext(context) {
|
|
902
|
+
return {
|
|
903
|
+
rulesContext: context.rulesContext,
|
|
904
|
+
treeContext: context.treeContext,
|
|
905
|
+
treeRoot: context.treeRoot,
|
|
906
|
+
root: context.root,
|
|
907
|
+
extendRootStackLength: context.extendRoots.extendRootStack.length
|
|
908
|
+
};
|
|
909
|
+
}
|
|
910
|
+
/** Setup context for evaluating these rules */
|
|
911
|
+
_setupContextForRules(context, rules) {
|
|
912
|
+
const treeContext = context.treeContext;
|
|
913
|
+
// Only switch treeContext if the rules have one AND it's different
|
|
914
|
+
// Dynamically created Rules (e.g., mixin parameter wrappers) may not have treeContext
|
|
915
|
+
// and we don't want to lose leakyRules and other settings
|
|
916
|
+
// IMPORTANT: Check _treeContext (private field) not treeContext (getter that lazily creates)
|
|
917
|
+
const rulesTreeContext = rules._treeContext;
|
|
918
|
+
if (rulesTreeContext && (!treeContext || treeContext !== rulesTreeContext)) {
|
|
919
|
+
context.allRoots.push(rules);
|
|
920
|
+
context.treeContext = rulesTreeContext;
|
|
921
|
+
context.treeRoot = rules;
|
|
922
|
+
}
|
|
923
|
+
// Always set root if not set - needed for extends to work with API-created Rules
|
|
924
|
+
context.root ??= rules;
|
|
925
|
+
context.rulesContext = rules;
|
|
926
|
+
}
|
|
927
|
+
/** Assign depth-first document order to every Ruleset under the given Rules (single walk, source order). */
|
|
928
|
+
_assignDocumentOrderDepthFirst(rules, map, counter) {
|
|
929
|
+
const value = rules.value;
|
|
930
|
+
if (!isArray(value)) {
|
|
931
|
+
return;
|
|
932
|
+
}
|
|
933
|
+
for (const node of value) {
|
|
934
|
+
if (isNode(node, 'Ruleset')) {
|
|
935
|
+
map.set(node, counter.value);
|
|
936
|
+
counter.value++;
|
|
937
|
+
}
|
|
938
|
+
const innerRules = node.value?.rules;
|
|
939
|
+
if (innerRules && isNode(innerRules, 'Rules')) {
|
|
940
|
+
this._assignDocumentOrderDepthFirst(innerRules, map, counter);
|
|
941
|
+
}
|
|
942
|
+
}
|
|
943
|
+
}
|
|
944
|
+
/** Build the evaluation queue partitioned by priority */
|
|
945
|
+
_buildEvalQueue(rules) {
|
|
946
|
+
let evalQueue = new Map();
|
|
947
|
+
for (let item of rules) {
|
|
948
|
+
let [, rule] = item;
|
|
949
|
+
let priority = NodeTypeToPriority.get(rule.type) ?? 0 /* Priority.None */;
|
|
950
|
+
// Less variable-calls `@foo();` are parsed as Expression(Call(variable-ref)).
|
|
951
|
+
// We *selectively* boost only those calls that "unlock mixins" (i.e. calling a variable whose
|
|
952
|
+
// value is a detached ruleset containing mixin definitions). This avoids changing evaluation
|
|
953
|
+
// order for regular detached rulesets like `@ruleset()` used for property blocks.
|
|
954
|
+
if (priority === 0 /* Priority.None */ && rules.treeContext?.leakyRules === true && isNode(rule, 'Expression')) {
|
|
955
|
+
const inner = rule.value;
|
|
956
|
+
if (isNode(inner, 'Call') && isNode(inner.value?.name, 'Reference')) {
|
|
957
|
+
const ref = inner.value.name;
|
|
958
|
+
const refType = String(ref?.options?.type ?? '');
|
|
959
|
+
if (refType === 'variable') {
|
|
960
|
+
const raw = ref.value?.key;
|
|
961
|
+
const keyStr = Array.isArray(raw) ? raw.join('') : String(raw?.valueOf?.() ?? raw ?? '');
|
|
962
|
+
// Only if variable exists and its value is a detached ruleset Mixin with nested Mixin definitions.
|
|
963
|
+
const decl = rules.find('declaration', keyStr, 'VarDeclaration');
|
|
964
|
+
const val = decl?.value?.value;
|
|
965
|
+
const hasNestedMixinDefinitions = isNode(val, 'Mixin')
|
|
966
|
+
&& Array.isArray(val.value?.rules?.value)
|
|
967
|
+
&& val.value.rules.value.some((n) => n?.type === 'Mixin');
|
|
968
|
+
if (hasNestedMixinDefinitions) {
|
|
969
|
+
priority = 3 /* Priority.High */;
|
|
970
|
+
}
|
|
971
|
+
}
|
|
972
|
+
}
|
|
973
|
+
}
|
|
974
|
+
let queue = evalQueue.get(priority) ?? [];
|
|
975
|
+
queue.push(item);
|
|
976
|
+
evalQueue.set(priority, queue);
|
|
977
|
+
}
|
|
978
|
+
return evalQueue;
|
|
979
|
+
}
|
|
980
|
+
/** Evaluate the built queues in priority order */
|
|
981
|
+
_evaluateQueue(rules, evalQueue, context) {
|
|
982
|
+
let rulesToHoist = false;
|
|
983
|
+
const scheduledPriority = new WeakMap();
|
|
984
|
+
const failuresByPriority = new WeakMap();
|
|
985
|
+
const priorities = Array.from({ length: 4 /* Priority.Highest */ + 1 }).map((_, i) => (4 /* Priority.Highest */ - i));
|
|
986
|
+
const runPriority = (p) => {
|
|
987
|
+
const queue = evalQueue.get(p);
|
|
988
|
+
if (!queue) {
|
|
989
|
+
return;
|
|
990
|
+
}
|
|
991
|
+
const enqueueRetry = (priority, item, rule) => {
|
|
992
|
+
const retryQueue = evalQueue.get(priority) ?? [];
|
|
993
|
+
retryQueue.push(item);
|
|
994
|
+
evalQueue.set(priority, retryQueue);
|
|
995
|
+
scheduledPriority.set(rule, priority);
|
|
996
|
+
};
|
|
997
|
+
const countFailure = (rule, priority) => {
|
|
998
|
+
const byPriority = failuresByPriority.get(rule) ?? new Map();
|
|
999
|
+
const nextCount = (byPriority.get(priority) ?? 0) + 1;
|
|
1000
|
+
byPriority.set(priority, nextCount);
|
|
1001
|
+
failuresByPriority.set(rule, byPriority);
|
|
1002
|
+
return nextCount;
|
|
1003
|
+
};
|
|
1004
|
+
const runSingleEntry = (q) => {
|
|
1005
|
+
const [idx, rule] = queue[q];
|
|
1006
|
+
/**
|
|
1007
|
+
* Var declarations have late evaluation, so they are skipped.
|
|
1008
|
+
* (Meaning: they are not evaluated until they are referenced.)
|
|
1009
|
+
*/
|
|
1010
|
+
if (isNode(rule, 'VarDeclaration')) {
|
|
1011
|
+
return;
|
|
1012
|
+
}
|
|
1013
|
+
// Skip stale entries for nodes that were re-queued to a different priority.
|
|
1014
|
+
const expectedPriority = scheduledPriority.get(rule);
|
|
1015
|
+
if (expectedPriority !== undefined && expectedPriority !== p) {
|
|
1016
|
+
return;
|
|
1017
|
+
}
|
|
1018
|
+
const onEvalError = (error) => {
|
|
1019
|
+
// Most node failures are semantic failures and should throw immediately.
|
|
1020
|
+
// Retry scheduling is reserved for StyleImport ordering/interpolation cases.
|
|
1021
|
+
if (!isNode(rule, 'StyleImport')) {
|
|
1022
|
+
throw error;
|
|
1023
|
+
}
|
|
1024
|
+
// Final pass: no retries remain.
|
|
1025
|
+
if (p === 0 /* Priority.None */) {
|
|
1026
|
+
throw error;
|
|
1027
|
+
}
|
|
1028
|
+
// Retry policy:
|
|
1029
|
+
// 1) first failure at a priority -> retry once at same priority
|
|
1030
|
+
// 2) second+ failure at that priority -> step down one level
|
|
1031
|
+
const failures = countFailure(rule, p);
|
|
1032
|
+
const nextPriority = failures === 1 ? p : (p - 1);
|
|
1033
|
+
enqueueRetry(nextPriority, [idx, rule], rule);
|
|
1034
|
+
return;
|
|
1035
|
+
};
|
|
1036
|
+
const tryStepResult = () => {
|
|
1037
|
+
try {
|
|
1038
|
+
const result = rule.eval(context);
|
|
1039
|
+
if (isThenable(result)) {
|
|
1040
|
+
return result.catch(onEvalError);
|
|
1041
|
+
}
|
|
1042
|
+
return result;
|
|
1043
|
+
}
|
|
1044
|
+
catch (error) {
|
|
1045
|
+
return onEvalError(error);
|
|
1046
|
+
}
|
|
1047
|
+
};
|
|
1048
|
+
const stepResult = pipe(tryStepResult, (result) => {
|
|
1049
|
+
// Undefined means we re-queued this node for retry.
|
|
1050
|
+
if (result === undefined) {
|
|
1051
|
+
return;
|
|
1052
|
+
}
|
|
1053
|
+
scheduledPriority.delete(rule);
|
|
1054
|
+
// Apply the result
|
|
1055
|
+
if (result !== rule) {
|
|
1056
|
+
rules.value[idx] = result;
|
|
1057
|
+
queue[q] = [idx, result];
|
|
1058
|
+
// If a StyleImport evaluated to Rules, register them in the parent's _rulesSet
|
|
1059
|
+
// so variables from the import can be found by the parent
|
|
1060
|
+
// Also register Rules from Call results (mixin calls) in the same way
|
|
1061
|
+
if (isNode(result, 'Rules')) {
|
|
1062
|
+
// Set the index of the imported Rules to the StyleImport's index
|
|
1063
|
+
// so we can compare Rules indices when determining which variable was declared later
|
|
1064
|
+
result.index = idx;
|
|
1065
|
+
rules.adopt(result);
|
|
1066
|
+
rules.registerNode(result, {
|
|
1067
|
+
rulesVisibility: result.options.rulesVisibility,
|
|
1068
|
+
readonly: result.options.readonly
|
|
1069
|
+
}, context);
|
|
1070
|
+
}
|
|
1071
|
+
else {
|
|
1072
|
+
// For non-Rules results, adopt them to set up parent chain
|
|
1073
|
+
rules.adopt(result);
|
|
1074
|
+
}
|
|
1075
|
+
}
|
|
1076
|
+
if (result.hoistToRoot) {
|
|
1077
|
+
rulesToHoist = true;
|
|
1078
|
+
}
|
|
1079
|
+
return;
|
|
1080
|
+
});
|
|
1081
|
+
// If stepResult is a thenable, propagate any errors
|
|
1082
|
+
if (isThenable(stepResult)) {
|
|
1083
|
+
return stepResult;
|
|
1084
|
+
}
|
|
1085
|
+
return;
|
|
1086
|
+
};
|
|
1087
|
+
const runFromIndex = (q) => {
|
|
1088
|
+
if (q >= queue.length) {
|
|
1089
|
+
return;
|
|
1090
|
+
}
|
|
1091
|
+
const step = runSingleEntry(q);
|
|
1092
|
+
if (isThenable(step)) {
|
|
1093
|
+
return step.then(() => runFromIndex(q + 1));
|
|
1094
|
+
}
|
|
1095
|
+
return runFromIndex(q + 1);
|
|
1096
|
+
};
|
|
1097
|
+
return runFromIndex(0);
|
|
1098
|
+
};
|
|
1099
|
+
const phaseRun = serialForEach(priorities, runPriority);
|
|
1100
|
+
if (isThenable(phaseRun)) {
|
|
1101
|
+
return phaseRun.then(() => {
|
|
1102
|
+
return rulesToHoist;
|
|
1103
|
+
}).catch((error) => {
|
|
1104
|
+
throw error;
|
|
1105
|
+
});
|
|
1106
|
+
}
|
|
1107
|
+
return rulesToHoist;
|
|
1108
|
+
}
|
|
1109
|
+
/**
|
|
1110
|
+
* Coalesce assignment-normalized declaration chains in one stage after evaluation.
|
|
1111
|
+
* This handles both in-scope merges and merges that span call-produced Rules blocks.
|
|
1112
|
+
*/
|
|
1113
|
+
_coalesceMergedDeclarations(rules) {
|
|
1114
|
+
const isMergedAssign = (assign) => (assign === '+:' || assign === '&,:' || assign === '&_:');
|
|
1115
|
+
const isDeclarationOnlyRules = (node) => (isNode(node, 'Rules')
|
|
1116
|
+
&& node.value.length > 0
|
|
1117
|
+
&& node.value.every(child => isNode(child, ['Declaration', 'Comment'])));
|
|
1118
|
+
const composeMergedValue = (decl, prior, assign) => {
|
|
1119
|
+
if (!isNode(decl, 'Declaration') || !isNode(prior, 'Declaration')) {
|
|
1120
|
+
return;
|
|
1121
|
+
}
|
|
1122
|
+
const priorValue = prior.value.value.copy(true, freezeChildren);
|
|
1123
|
+
const nextValue = decl.value.value.copy(true, freezeChildren);
|
|
1124
|
+
decl.value.value = assign === '&_:'
|
|
1125
|
+
? spaced([priorValue, nextValue])
|
|
1126
|
+
: new List([priorValue, nextValue]);
|
|
1127
|
+
if (!decl.value.important && prior.value.important) {
|
|
1128
|
+
decl.value.important = prior.value.important;
|
|
1129
|
+
}
|
|
1130
|
+
};
|
|
1131
|
+
const normalizeMergedDeclarationValue = (node) => {
|
|
1132
|
+
if (!isNode(node, 'Declaration')) {
|
|
1133
|
+
return;
|
|
1134
|
+
}
|
|
1135
|
+
const current = node.value.value;
|
|
1136
|
+
if (!isNode(current, 'List') || current.value.length === 0) {
|
|
1137
|
+
return;
|
|
1138
|
+
}
|
|
1139
|
+
const [first, ...rest] = current.value;
|
|
1140
|
+
let firstIsEmptyString = false;
|
|
1141
|
+
try {
|
|
1142
|
+
firstIsEmptyString = String(first?.valueOf?.() ?? '') === '';
|
|
1143
|
+
}
|
|
1144
|
+
catch {
|
|
1145
|
+
firstIsEmptyString = false;
|
|
1146
|
+
}
|
|
1147
|
+
const isEmptyPlaceholder = Boolean(first
|
|
1148
|
+
&& (isNode(first, 'Nil')
|
|
1149
|
+
|| (isNode(first, 'List') && first.value.length === 0)
|
|
1150
|
+
|| firstIsEmptyString));
|
|
1151
|
+
if (!isEmptyPlaceholder) {
|
|
1152
|
+
return;
|
|
1153
|
+
}
|
|
1154
|
+
if (rest.length === 0) {
|
|
1155
|
+
node.value.value = new Nil();
|
|
1156
|
+
return;
|
|
1157
|
+
}
|
|
1158
|
+
if (rest.length === 1) {
|
|
1159
|
+
node.value.value = rest[0].copy(true, freezeChildren);
|
|
1160
|
+
return;
|
|
1161
|
+
}
|
|
1162
|
+
node.value.value = new List(rest.map(item => item.copy(true, freezeChildren)));
|
|
1163
|
+
};
|
|
1164
|
+
const lastVisibleByName = new Map();
|
|
1165
|
+
const mergedAnchorByName = new Map();
|
|
1166
|
+
const stream = [];
|
|
1167
|
+
for (const node of rules.value) {
|
|
1168
|
+
if (isNode(node, 'Declaration')) {
|
|
1169
|
+
stream.push(node);
|
|
1170
|
+
continue;
|
|
1171
|
+
}
|
|
1172
|
+
if (isDeclarationOnlyRules(node)) {
|
|
1173
|
+
for (const child of node.value) {
|
|
1174
|
+
if (isNode(child, 'Declaration')) {
|
|
1175
|
+
stream.push(child);
|
|
1176
|
+
}
|
|
1177
|
+
}
|
|
1178
|
+
}
|
|
1179
|
+
}
|
|
1180
|
+
for (const node of stream) {
|
|
1181
|
+
if (!isNode(node, 'Declaration')) {
|
|
1182
|
+
continue;
|
|
1183
|
+
}
|
|
1184
|
+
const name = String(node.value.name);
|
|
1185
|
+
const assign = String(node.options.normalizedFromAssign ?? '');
|
|
1186
|
+
const merged = isMergedAssign(assign);
|
|
1187
|
+
if (!merged) {
|
|
1188
|
+
mergedAnchorByName.delete(name);
|
|
1189
|
+
if (node.visible) {
|
|
1190
|
+
lastVisibleByName.set(name, node);
|
|
1191
|
+
}
|
|
1192
|
+
continue;
|
|
1193
|
+
}
|
|
1194
|
+
normalizeMergedDeclarationValue(node);
|
|
1195
|
+
const prior = lastVisibleByName.get(name);
|
|
1196
|
+
if (prior && prior !== node && prior.parent !== node.parent) {
|
|
1197
|
+
composeMergedValue(node, prior, assign);
|
|
1198
|
+
}
|
|
1199
|
+
const existingAnchor = mergedAnchorByName.get(name);
|
|
1200
|
+
if (existingAnchor && existingAnchor !== node && isNode(existingAnchor, 'Declaration')) {
|
|
1201
|
+
existingAnchor.value.value = node.value.value.copy(true);
|
|
1202
|
+
if (!existingAnchor.value.important && node.value.important) {
|
|
1203
|
+
existingAnchor.value.important = node.value.important;
|
|
1204
|
+
}
|
|
1205
|
+
node.removeFlag(F_VISIBLE);
|
|
1206
|
+
if (existingAnchor.visible) {
|
|
1207
|
+
lastVisibleByName.set(name, existingAnchor);
|
|
1208
|
+
}
|
|
1209
|
+
continue;
|
|
1210
|
+
}
|
|
1211
|
+
mergedAnchorByName.set(name, node);
|
|
1212
|
+
if (node.visible) {
|
|
1213
|
+
lastVisibleByName.set(name, node);
|
|
1214
|
+
}
|
|
1215
|
+
}
|
|
1216
|
+
}
|
|
1217
|
+
/**
|
|
1218
|
+
* Normalize call-produced declaration-only Rules ordering so declarations
|
|
1219
|
+
* emitted from late-evaluated calls (e.g. each/$for) appear before nested
|
|
1220
|
+
* rulesets/at-rules in the same parent Rules container.
|
|
1221
|
+
*
|
|
1222
|
+
* This runs after queue evaluation to avoid mutating rule indices mid-eval.
|
|
1223
|
+
*/
|
|
1224
|
+
_normalizeCallDeclarationRulesOrder(rules) {
|
|
1225
|
+
const firstNestedIdx = rules.value.findIndex(n => isNode(n, ['Ruleset', 'AtRule']));
|
|
1226
|
+
if (firstNestedIdx < 0) {
|
|
1227
|
+
return;
|
|
1228
|
+
}
|
|
1229
|
+
const beforeNested = rules.value.slice(0, firstNestedIdx);
|
|
1230
|
+
const afterNested = rules.value.slice(firstNestedIdx);
|
|
1231
|
+
const shouldMove = (n) => {
|
|
1232
|
+
if (!isNode(n, 'Rules')
|
|
1233
|
+
|| !isNode(n.sourceParent, 'Call')
|
|
1234
|
+
|| n.value.length === 0
|
|
1235
|
+
|| !n.value.every(child => isNode(child, ['Declaration', 'Comment']))) {
|
|
1236
|
+
return false;
|
|
1237
|
+
}
|
|
1238
|
+
const sourceName = n.sourceParent.value.name;
|
|
1239
|
+
// Keep mixin-call declaration blocks in source order relative to nested rulesets.
|
|
1240
|
+
if (isNode(sourceName, 'Reference')
|
|
1241
|
+
&& (sourceName.options?.type === 'mixin'
|
|
1242
|
+
|| sourceName.options?.type === 'mixin-ruleset'
|
|
1243
|
+
|| sourceName.options?.type === 'ruleset')) {
|
|
1244
|
+
return false;
|
|
1245
|
+
}
|
|
1246
|
+
return true;
|
|
1247
|
+
};
|
|
1248
|
+
const moved = afterNested.filter(shouldMove);
|
|
1249
|
+
if (moved.length === 0) {
|
|
1250
|
+
return;
|
|
1251
|
+
}
|
|
1252
|
+
const remainder = afterNested.filter(n => !shouldMove(n));
|
|
1253
|
+
rules.value = [...beforeNested, ...moved, ...remainder];
|
|
1254
|
+
}
|
|
1255
|
+
/**
|
|
1256
|
+
* After preEval: ensure root on extend stack, build eval queue, run evaluation.
|
|
1257
|
+
* Used by evalNode so that when eval() is called without preEval (e.g. jess compile()),
|
|
1258
|
+
* we still have all rulesets registered and root set for extend lookups.
|
|
1259
|
+
*/
|
|
1260
|
+
_afterPreEvalStep(rules, context) {
|
|
1261
|
+
const isMainRoot = rules === context.root;
|
|
1262
|
+
if (isMainRoot && context.extendRoots.extendRootStack.length === 0) {
|
|
1263
|
+
if (!context.extendRoots.root) {
|
|
1264
|
+
context.extendRoots.registerRoot(rules);
|
|
1265
|
+
}
|
|
1266
|
+
context.extendRoots.pushExtendRoot(rules);
|
|
1267
|
+
}
|
|
1268
|
+
if (rules.evaluated) {
|
|
1269
|
+
return { rules, rulesToHoist: false };
|
|
1270
|
+
}
|
|
1271
|
+
if (rules === context.root) {
|
|
1272
|
+
const map = new WeakMap();
|
|
1273
|
+
context.documentOrderByRuleset = map;
|
|
1274
|
+
this._assignDocumentOrderDepthFirst(rules, map, { value: 0 });
|
|
1275
|
+
}
|
|
1276
|
+
const evalQueue = this._buildEvalQueue(rules);
|
|
1277
|
+
const maybeHoist = this._evaluateQueue(rules, evalQueue, context);
|
|
1278
|
+
if (isThenable(maybeHoist)) {
|
|
1279
|
+
return maybeHoist.then((rulesToHoist) => {
|
|
1280
|
+
this._normalizeCallDeclarationRulesOrder(rules);
|
|
1281
|
+
this._coalesceMergedDeclarations(rules);
|
|
1282
|
+
return {
|
|
1283
|
+
rules,
|
|
1284
|
+
rulesToHoist
|
|
1285
|
+
};
|
|
1286
|
+
});
|
|
1287
|
+
}
|
|
1288
|
+
this._normalizeCallDeclarationRulesOrder(rules);
|
|
1289
|
+
this._coalesceMergedDeclarations(rules);
|
|
1290
|
+
return { rules, rulesToHoist: maybeHoist };
|
|
1291
|
+
}
|
|
1292
|
+
evalNode(context) {
|
|
1293
|
+
const saved = this._snapshotContext(context);
|
|
1294
|
+
context.rulesEvalStack.push(this.sourceNode);
|
|
1295
|
+
const restoreContextOnError = () => {
|
|
1296
|
+
context.rulesContext = saved.rulesContext;
|
|
1297
|
+
if (saved.treeRoot !== undefined) {
|
|
1298
|
+
context.treeRoot = saved.treeRoot;
|
|
1299
|
+
}
|
|
1300
|
+
if (saved.root !== undefined) {
|
|
1301
|
+
context.root = saved.root;
|
|
1302
|
+
}
|
|
1303
|
+
const currentLength = context.extendRoots.extendRootStack.length;
|
|
1304
|
+
if (saved.extendRootStackLength !== undefined && currentLength > saved.extendRootStackLength) {
|
|
1305
|
+
while (context.extendRoots.extendRootStack.length > saved.extendRootStackLength) {
|
|
1306
|
+
context.extendRoots.popExtendRoot();
|
|
1307
|
+
}
|
|
1308
|
+
}
|
|
1309
|
+
if (context.rulesEvalStack[context.rulesEvalStack.length - 1] === this.sourceNode) {
|
|
1310
|
+
context.rulesEvalStack.pop();
|
|
1311
|
+
}
|
|
1312
|
+
context.depth--;
|
|
1313
|
+
};
|
|
1314
|
+
let pipeResult;
|
|
1315
|
+
try {
|
|
1316
|
+
pipeResult = pipe(() => {
|
|
1317
|
+
this._setupContextForRules(context, this);
|
|
1318
|
+
// Run preEval first if not yet run (e.g. when jess compile() calls eval() without preEval).
|
|
1319
|
+
// preEval registers the root and all nested rulesets so extend lookups find targets in child roots (e.g. .ma inside @media).
|
|
1320
|
+
const runPreEvalIfNeeded = (rules) => {
|
|
1321
|
+
if (rules.preEvaluated) {
|
|
1322
|
+
return rules;
|
|
1323
|
+
}
|
|
1324
|
+
const result = rules.preEval(context);
|
|
1325
|
+
return isThenable(result) ? result : result;
|
|
1326
|
+
};
|
|
1327
|
+
const rulesAfterPreEval = runPreEvalIfNeeded(this);
|
|
1328
|
+
const afterPreEval = (rules) => {
|
|
1329
|
+
// When we're the outermost Rules, use the tree we're evaling as root (may differ from context.root set in getTree, or be preEval's clone).
|
|
1330
|
+
if (context.rulesEvalStack.length === 1) {
|
|
1331
|
+
context.root = rules;
|
|
1332
|
+
}
|
|
1333
|
+
return this._afterPreEvalStep(rules, context);
|
|
1334
|
+
};
|
|
1335
|
+
if (isThenable(rulesAfterPreEval)) {
|
|
1336
|
+
return rulesAfterPreEval.then(afterPreEval);
|
|
1337
|
+
}
|
|
1338
|
+
return afterPreEval(rulesAfterPreEval);
|
|
1339
|
+
}, ({ rules }) => {
|
|
1340
|
+
// Note: Rulesets from imported Rules are already registered to their own treeRoot
|
|
1341
|
+
// during preEval when the imported Rules node is evaluated. The extend search
|
|
1342
|
+
// loops through allRoots, so it should find them. The _searchRulesChildrenForRulesets
|
|
1343
|
+
// method in RulesetRegistry also searches imported Rules' registries.
|
|
1344
|
+
// After all evaluation stages, check if any variables in the current Rules
|
|
1345
|
+
// shadow readonly variables from imported Rules (compose type) at the same level
|
|
1346
|
+
// Only check direct children of the Rules node, not nested variables (e.g., inside rulesets)
|
|
1347
|
+
if (rules.rulesSet.length > 0) {
|
|
1348
|
+
let currentRegistry = rules.getRegistry('declaration');
|
|
1349
|
+
currentRegistry.indexPendingItems();
|
|
1350
|
+
for (const entry of rules.rulesSet) {
|
|
1351
|
+
if (entry.readonly) {
|
|
1352
|
+
let importedRegistry = entry.node.getRegistry('declaration');
|
|
1353
|
+
importedRegistry.indexPendingItems();
|
|
1354
|
+
for (const [key, declarations] of importedRegistry.index) {
|
|
1355
|
+
for (const decl of declarations) {
|
|
1356
|
+
if (isNode(decl, 'VarDeclaration')) {
|
|
1357
|
+
// Check if a variable with this name exists in the current Rules' registry
|
|
1358
|
+
let currentDeclarations = currentRegistry.index.get(key);
|
|
1359
|
+
if (currentDeclarations) {
|
|
1360
|
+
for (const currentDecl of currentDeclarations) {
|
|
1361
|
+
if (isNode(currentDecl, 'VarDeclaration') && !currentDecl.options?.setDefined) {
|
|
1362
|
+
// Only throw if the variable is a direct child of the Rules node (same level)
|
|
1363
|
+
// Nested variables (e.g., inside rulesets) are allowed to shadow
|
|
1364
|
+
if (currentDecl.parent === rules) {
|
|
1365
|
+
throw new ReferenceError(`"${key}" is readonly`);
|
|
1366
|
+
}
|
|
1367
|
+
}
|
|
1368
|
+
}
|
|
1369
|
+
}
|
|
1370
|
+
}
|
|
1371
|
+
}
|
|
1372
|
+
}
|
|
1373
|
+
}
|
|
1374
|
+
}
|
|
1375
|
+
}
|
|
1376
|
+
// Check if we're at the outermost level BEFORE restoring context
|
|
1377
|
+
// Only process extends at the TRUE outermost root (context.root)
|
|
1378
|
+
// This ensures extends are processed AFTER all evaluation completes,
|
|
1379
|
+
// including imports and nested Rules
|
|
1380
|
+
const isOutermost = rules === context.root;
|
|
1381
|
+
if (isOutermost) {
|
|
1382
|
+
// Process all registered extends using the extend roots registry system
|
|
1383
|
+
processExtends(context);
|
|
1384
|
+
}
|
|
1385
|
+
/** Restore contexts */
|
|
1386
|
+
context.rulesContext = saved.rulesContext;
|
|
1387
|
+
// Only restore context.treeRoot if saved.treeRoot is defined and we're not at the outermost level
|
|
1388
|
+
// If saved.treeRoot is undefined, it means we're at the outermost level, so keep context.treeRoot as is
|
|
1389
|
+
// This ensures extends evaluated during selector evaluation can still access the correct treeRoot
|
|
1390
|
+
if (saved.treeRoot !== undefined && !isOutermost) {
|
|
1391
|
+
context.treeRoot = saved.treeRoot;
|
|
1392
|
+
}
|
|
1393
|
+
// Only restore context.root if we're not at the outermost level (where it was originally set)
|
|
1394
|
+
// If saved.root is undefined, it means we're at the outermost level, so keep context.root as is
|
|
1395
|
+
if (saved.root !== undefined && !isOutermost) {
|
|
1396
|
+
context.root = saved.root;
|
|
1397
|
+
}
|
|
1398
|
+
// Restore extend root stack to its original length (if we're not the main root)
|
|
1399
|
+
// The main root manages its own push/pop, but nested Rules should restore the stack
|
|
1400
|
+
if (!isOutermost && saved.extendRootStackLength !== undefined) {
|
|
1401
|
+
const currentLength = context.extendRoots.extendRootStack.length;
|
|
1402
|
+
if (currentLength > saved.extendRootStackLength) {
|
|
1403
|
+
// Pop any extend roots that were pushed during this Rules evaluation
|
|
1404
|
+
while (context.extendRoots.extendRootStack.length > saved.extendRootStackLength) {
|
|
1405
|
+
context.extendRoots.popExtendRoot();
|
|
1406
|
+
}
|
|
1407
|
+
}
|
|
1408
|
+
}
|
|
1409
|
+
// Pop extend root if we pushed it (check if this is still the root)
|
|
1410
|
+
if (rules === context.root) {
|
|
1411
|
+
context.extendRoots.popExtendRoot();
|
|
1412
|
+
}
|
|
1413
|
+
context.rulesEvalStack.pop();
|
|
1414
|
+
context.depth--;
|
|
1415
|
+
return rules;
|
|
1416
|
+
});
|
|
1417
|
+
}
|
|
1418
|
+
catch (error) {
|
|
1419
|
+
restoreContextOnError();
|
|
1420
|
+
throw error;
|
|
1421
|
+
}
|
|
1422
|
+
if (isThenable(pipeResult)) {
|
|
1423
|
+
return pipeResult.catch((error) => {
|
|
1424
|
+
restoreContextOnError();
|
|
1425
|
+
throw error;
|
|
1426
|
+
});
|
|
1427
|
+
}
|
|
1428
|
+
return pipeResult;
|
|
1429
|
+
}
|
|
1430
|
+
}
|
|
1431
|
+
export const rules = defineType(Rules, 'Rules');
|
|
1432
|
+
/**
|
|
1433
|
+
* @todo - Will need lots of massaging, to resolve things like
|
|
1434
|
+
* mixins which rely on variables which have interpolated names,
|
|
1435
|
+
* and variables with interpolated names that rely on mixins.
|
|
1436
|
+
*
|
|
1437
|
+
* @note - Registration of declaration names and mixins / selectors
|
|
1438
|
+
* should have already happened in pre-eval.
|
|
1439
|
+
*/
|
|
1440
|
+
const NodeTypeToPriority = new Map([
|
|
1441
|
+
/** First, resolve imports */
|
|
1442
|
+
['StyleImport', 4 /* Priority.Highest */],
|
|
1443
|
+
/** Then, resolve calls */
|
|
1444
|
+
['Call', 3 /* Priority.High */],
|
|
1445
|
+
/** Then, resolve declarations */
|
|
1446
|
+
['VarDeclaration', 2 /* Priority.Medium */],
|
|
1447
|
+
['Declaration', 2 /* Priority.Medium */],
|
|
1448
|
+
/** Then... */
|
|
1449
|
+
['Mixin', 1 /* Priority.Low */],
|
|
1450
|
+
['Ruleset', 1 /* Priority.Low */],
|
|
1451
|
+
/** Extend should evaluate at the same priority as Ruleset to ensure it evaluates before nested rulesets */
|
|
1452
|
+
['Extend', 1 /* Priority.Low */],
|
|
1453
|
+
/** AtRule (e.g., @media) should evaluate at the same priority as Ruleset to preserve source order */
|
|
1454
|
+
['AtRule', 1 /* Priority.Low */]
|
|
1455
|
+
/** Then, everything else? */
|
|
1456
|
+
]);
|
|
1457
|
+
/**
|
|
1458
|
+
* Returns a plain JS function for calling a set of mixins
|
|
1459
|
+
*
|
|
1460
|
+
* This is in the same file as Rules to avoid circular dependencies.
|
|
1461
|
+
*
|
|
1462
|
+
* @note this will be called as a result after a mixin find is executed.
|
|
1463
|
+
*/
|
|
1464
|
+
export function getFunctionFromMixins(mixins) {
|
|
1465
|
+
let mixinArr = isArray(mixins) ? mixins : [mixins];
|
|
1466
|
+
async function returnFunc(...args) {
|
|
1467
|
+
const mixinLength = mixinArr.length;
|
|
1468
|
+
let mixinCandidates = [];
|
|
1469
|
+
let evalCandidates;
|
|
1470
|
+
// When called via callWithContext, 'this' is functionThis, not Context
|
|
1471
|
+
// We need to extract the context from functionThis or use a fallback
|
|
1472
|
+
let thisContext;
|
|
1473
|
+
if (this instanceof Context) {
|
|
1474
|
+
thisContext = this;
|
|
1475
|
+
}
|
|
1476
|
+
else if (this && typeof this === 'object' && 'context' in this) {
|
|
1477
|
+
// This is functionThis from callWithContext
|
|
1478
|
+
thisContext = this.context;
|
|
1479
|
+
}
|
|
1480
|
+
else {
|
|
1481
|
+
thisContext = new Context();
|
|
1482
|
+
}
|
|
1483
|
+
let caller = thisContext.caller;
|
|
1484
|
+
let sourceParent = caller?.value.name instanceof Node
|
|
1485
|
+
? caller.value.name.sourceParent
|
|
1486
|
+
: caller?.sourceParent;
|
|
1487
|
+
let nodeArgs = [];
|
|
1488
|
+
const savedRulesContext = thisContext.rulesContext;
|
|
1489
|
+
const argEvalRulesContext = caller?.rulesParent ?? caller?.sourceRulesParent ?? savedRulesContext;
|
|
1490
|
+
thisContext.rulesContext = argEvalRulesContext;
|
|
1491
|
+
try {
|
|
1492
|
+
for (let arg of args) {
|
|
1493
|
+
/**
|
|
1494
|
+
* I think they should always be nodes?
|
|
1495
|
+
* But leaving this for future expansion.
|
|
1496
|
+
*/
|
|
1497
|
+
if (isNode(arg)) {
|
|
1498
|
+
// IMPORTANT: Do not evaluate VarDeclaration args (named arguments) here.
|
|
1499
|
+
// Evaluating them can register/override variables in the current scope.
|
|
1500
|
+
// They should only be used for parameter binding.
|
|
1501
|
+
if (isNode(arg, 'VarDeclaration')) {
|
|
1502
|
+
const cloned = arg.copy(true, freezeChildren);
|
|
1503
|
+
cloned.frozen = true;
|
|
1504
|
+
nodeArgs.push(cloned);
|
|
1505
|
+
continue;
|
|
1506
|
+
}
|
|
1507
|
+
try {
|
|
1508
|
+
const evald = await arg.clonedEval(thisContext);
|
|
1509
|
+
if (isNode(evald, 'Rest')) {
|
|
1510
|
+
const restValue = evald.value;
|
|
1511
|
+
if (isNode(restValue, 'Sequence') || isNode(restValue, 'List')) {
|
|
1512
|
+
for (const restArg of restValue.value) {
|
|
1513
|
+
const frozenRestArg = restArg.copy(true, freezeChildren);
|
|
1514
|
+
frozenRestArg.frozen = true;
|
|
1515
|
+
nodeArgs.push(frozenRestArg);
|
|
1516
|
+
}
|
|
1517
|
+
continue;
|
|
1518
|
+
}
|
|
1519
|
+
}
|
|
1520
|
+
evald.frozen = true;
|
|
1521
|
+
nodeArgs.push(evald);
|
|
1522
|
+
}
|
|
1523
|
+
catch (error) {
|
|
1524
|
+
throw error;
|
|
1525
|
+
}
|
|
1526
|
+
}
|
|
1527
|
+
else {
|
|
1528
|
+
nodeArgs.push(cast(arg));
|
|
1529
|
+
}
|
|
1530
|
+
}
|
|
1531
|
+
}
|
|
1532
|
+
finally {
|
|
1533
|
+
thisContext.rulesContext = savedRulesContext;
|
|
1534
|
+
}
|
|
1535
|
+
/**
|
|
1536
|
+
* Check named and positional arguments
|
|
1537
|
+
* against mixins, to see which ones match.
|
|
1538
|
+
* (Any mixin with a mis-match of
|
|
1539
|
+
* arguments fails.)
|
|
1540
|
+
*/
|
|
1541
|
+
const normalizeBoundLeadingItemWhitespace = (node) => {
|
|
1542
|
+
if (!isNode(node, ['List', 'Sequence'])) {
|
|
1543
|
+
return;
|
|
1544
|
+
}
|
|
1545
|
+
const items = node.value;
|
|
1546
|
+
if (items.length > 0) {
|
|
1547
|
+
items[0].pre = 0;
|
|
1548
|
+
}
|
|
1549
|
+
for (const item of items) {
|
|
1550
|
+
if (isNode(item, ['List', 'Sequence'])) {
|
|
1551
|
+
normalizeBoundLeadingItemWhitespace(item);
|
|
1552
|
+
}
|
|
1553
|
+
}
|
|
1554
|
+
};
|
|
1555
|
+
for (let i = 0; i < mixinLength; i++) {
|
|
1556
|
+
let mixin = mixinArr[i];
|
|
1557
|
+
let isPlainRule = isNode(mixin, 'Rules');
|
|
1558
|
+
let paramLength = isPlainRule ? 0 : mixin.value.params?.length ?? 0;
|
|
1559
|
+
if (!paramLength) {
|
|
1560
|
+
/** Exit early if args were passed in, but no args are possible */
|
|
1561
|
+
if (args.length) {
|
|
1562
|
+
continue;
|
|
1563
|
+
}
|
|
1564
|
+
mixinCandidates.push(mixin);
|
|
1565
|
+
}
|
|
1566
|
+
else {
|
|
1567
|
+
/** The mixin has parameters, so let's check args to see if there's a match */
|
|
1568
|
+
let params = mixin.value.params.copy(true);
|
|
1569
|
+
const hasRestParamOriginal = mixin.value.params.value.some(p => isNode(p, 'Rest'));
|
|
1570
|
+
const maxPositionalArgs = hasRestParamOriginal ? Number.POSITIVE_INFINITY : params.length;
|
|
1571
|
+
let positions = params.length;
|
|
1572
|
+
let requiredPositions = 0;
|
|
1573
|
+
for (let param of params.value) {
|
|
1574
|
+
if (isNode(param, 'VarDeclaration')) {
|
|
1575
|
+
if (param.value.value instanceof Nil) {
|
|
1576
|
+
requiredPositions++;
|
|
1577
|
+
}
|
|
1578
|
+
}
|
|
1579
|
+
else if (isNode(param, 'Any') && param.options.role === 'property') {
|
|
1580
|
+
// Any with role: 'property' is a parameter without default (consistent with variable names)
|
|
1581
|
+
requiredPositions++;
|
|
1582
|
+
}
|
|
1583
|
+
else if (!isNode(param, 'Rest')) {
|
|
1584
|
+
requiredPositions++;
|
|
1585
|
+
}
|
|
1586
|
+
}
|
|
1587
|
+
let argPos = 0;
|
|
1588
|
+
let match = true;
|
|
1589
|
+
for (let i = 0; i < positions; i++) {
|
|
1590
|
+
let arg = nodeArgs[argPos];
|
|
1591
|
+
if (!arg) {
|
|
1592
|
+
continue;
|
|
1593
|
+
}
|
|
1594
|
+
let param;
|
|
1595
|
+
let argValue;
|
|
1596
|
+
if (isNode(arg, 'VarDeclaration')) {
|
|
1597
|
+
param = params.value.find((p) => {
|
|
1598
|
+
if (isNode(p, 'VarDeclaration')) {
|
|
1599
|
+
return p.value.name.valueOf() === arg.value.name.valueOf();
|
|
1600
|
+
}
|
|
1601
|
+
if (isNode(p, 'Any') && p.options.role === 'property') {
|
|
1602
|
+
return p.valueOf() === arg.value.name.valueOf();
|
|
1603
|
+
}
|
|
1604
|
+
return false;
|
|
1605
|
+
});
|
|
1606
|
+
if (param) {
|
|
1607
|
+
argValue = arg.value.value;
|
|
1608
|
+
}
|
|
1609
|
+
else {
|
|
1610
|
+
match = false;
|
|
1611
|
+
break;
|
|
1612
|
+
}
|
|
1613
|
+
}
|
|
1614
|
+
else {
|
|
1615
|
+
param = params.value[i];
|
|
1616
|
+
if (!param) {
|
|
1617
|
+
match = false;
|
|
1618
|
+
break;
|
|
1619
|
+
}
|
|
1620
|
+
argValue = arg;
|
|
1621
|
+
}
|
|
1622
|
+
if (!param) {
|
|
1623
|
+
match = false;
|
|
1624
|
+
break;
|
|
1625
|
+
}
|
|
1626
|
+
if (isNode(param, 'VarDeclaration')) {
|
|
1627
|
+
const boundValue = argValue.copy(true, freezeChildren);
|
|
1628
|
+
boundValue.frozen = true;
|
|
1629
|
+
normalizeBoundLeadingItemWhitespace(boundValue);
|
|
1630
|
+
param.value.value = boundValue;
|
|
1631
|
+
}
|
|
1632
|
+
else if (isNode(param, 'Any') && param.options.role === 'property') {
|
|
1633
|
+
// Convert Any with role: 'property' to VarDeclaration for registration
|
|
1634
|
+
const boundValue = argValue.copy(true, freezeChildren);
|
|
1635
|
+
boundValue.frozen = true;
|
|
1636
|
+
normalizeBoundLeadingItemWhitespace(boundValue);
|
|
1637
|
+
const varDecl = new VarDeclaration({
|
|
1638
|
+
name: param,
|
|
1639
|
+
value: boundValue
|
|
1640
|
+
}, { paramVar: true });
|
|
1641
|
+
params.value[i] = varDecl;
|
|
1642
|
+
}
|
|
1643
|
+
else if (isNode(param, 'Rest')) {
|
|
1644
|
+
/** We assume that the rest args are values */
|
|
1645
|
+
const rest = nodeArgs.slice(argPos).map((restArg) => {
|
|
1646
|
+
const cloned = restArg.copy(true, freezeChildren);
|
|
1647
|
+
cloned.frozen = true;
|
|
1648
|
+
return cloned;
|
|
1649
|
+
});
|
|
1650
|
+
/** Create a new variable with the rest name */
|
|
1651
|
+
params.value[i] = new VarDeclaration({
|
|
1652
|
+
name: new Any(param.value ? `${param.value}` : `rest${i}`, { role: 'property' }),
|
|
1653
|
+
value: new Sequence(rest)
|
|
1654
|
+
});
|
|
1655
|
+
/** Check a pattern-matching node */
|
|
1656
|
+
}
|
|
1657
|
+
else {
|
|
1658
|
+
if (param.compare(argValue) !== 0) {
|
|
1659
|
+
/** This mixin is not a match */
|
|
1660
|
+
match = false;
|
|
1661
|
+
break;
|
|
1662
|
+
}
|
|
1663
|
+
}
|
|
1664
|
+
argPos++;
|
|
1665
|
+
}
|
|
1666
|
+
const positionalArgCount = nodeArgs.filter(argNode => !isNode(argNode, 'VarDeclaration')).length;
|
|
1667
|
+
if (positionalArgCount > maxPositionalArgs) {
|
|
1668
|
+
continue;
|
|
1669
|
+
}
|
|
1670
|
+
/**
|
|
1671
|
+
* Now we can check remaining positional matches
|
|
1672
|
+
* against the remaining parameters.
|
|
1673
|
+
*/
|
|
1674
|
+
if (argPos < requiredPositions) {
|
|
1675
|
+
/** This mixin is not a match */
|
|
1676
|
+
continue;
|
|
1677
|
+
}
|
|
1678
|
+
if (nodeArgs.length > 1 && params.value.length === 1 && requiredPositions === 1) {
|
|
1679
|
+
// Less should not match single required-parameter overloads against extra positional args.
|
|
1680
|
+
continue;
|
|
1681
|
+
}
|
|
1682
|
+
if (match) {
|
|
1683
|
+
/** Make a shallow copy to attach our resolved params (w/ args) */
|
|
1684
|
+
let originalMixin = mixin;
|
|
1685
|
+
mixin = mixin.copy();
|
|
1686
|
+
originalMixin.parent.adopt(mixin);
|
|
1687
|
+
mixin.value.params = params;
|
|
1688
|
+
mixinCandidates.push(mixin);
|
|
1689
|
+
}
|
|
1690
|
+
}
|
|
1691
|
+
}
|
|
1692
|
+
/**
|
|
1693
|
+
* Alright, we have mixin candidates (mixins that match
|
|
1694
|
+
* by arity, pattern, and/or named arguments), now what?
|
|
1695
|
+
*
|
|
1696
|
+
* First, let's make an evaluation order that evaluates
|
|
1697
|
+
* default guards last.
|
|
1698
|
+
*/
|
|
1699
|
+
let hasDefault = false;
|
|
1700
|
+
const guardContainsDefault = (node) => {
|
|
1701
|
+
if (!node) {
|
|
1702
|
+
return false;
|
|
1703
|
+
}
|
|
1704
|
+
if (node.type === 'DefaultGuard') {
|
|
1705
|
+
return true;
|
|
1706
|
+
}
|
|
1707
|
+
if (node.type === 'Call') {
|
|
1708
|
+
const callName = String(node.value?.name?.valueOf?.() ?? node.value?.name ?? '');
|
|
1709
|
+
if (callName === 'default' || callName === '??') {
|
|
1710
|
+
return true;
|
|
1711
|
+
}
|
|
1712
|
+
}
|
|
1713
|
+
const value = node.value;
|
|
1714
|
+
if (Array.isArray(value)) {
|
|
1715
|
+
for (const item of value) {
|
|
1716
|
+
if (isNode(item) && guardContainsDefault(item)) {
|
|
1717
|
+
return true;
|
|
1718
|
+
}
|
|
1719
|
+
}
|
|
1720
|
+
return false;
|
|
1721
|
+
}
|
|
1722
|
+
if (value && typeof value === 'object') {
|
|
1723
|
+
const record = value;
|
|
1724
|
+
for (const item of Object.values(record)) {
|
|
1725
|
+
if (isNode(item) && guardContainsDefault(item)) {
|
|
1726
|
+
return true;
|
|
1727
|
+
}
|
|
1728
|
+
if (Array.isArray(item)) {
|
|
1729
|
+
for (const child of item) {
|
|
1730
|
+
if (isNode(child) && guardContainsDefault(child)) {
|
|
1731
|
+
return true;
|
|
1732
|
+
}
|
|
1733
|
+
}
|
|
1734
|
+
}
|
|
1735
|
+
}
|
|
1736
|
+
}
|
|
1737
|
+
return false;
|
|
1738
|
+
};
|
|
1739
|
+
const hasFailedGuardAncestor = (node) => {
|
|
1740
|
+
let current = node.parent;
|
|
1741
|
+
while (current) {
|
|
1742
|
+
if (isNode(current, 'Ruleset')) {
|
|
1743
|
+
const guardNode = current.value.guard;
|
|
1744
|
+
if (guardNode instanceof Nil) {
|
|
1745
|
+
return true;
|
|
1746
|
+
}
|
|
1747
|
+
}
|
|
1748
|
+
current = current.parent;
|
|
1749
|
+
}
|
|
1750
|
+
return false;
|
|
1751
|
+
};
|
|
1752
|
+
evalCandidates = mixinCandidates
|
|
1753
|
+
.filter((candidate) => {
|
|
1754
|
+
const inStack = thisContext.rulesEvalStack.includes(candidate.value.rules.sourceNode);
|
|
1755
|
+
const blockedByFailedGuardAncestor = hasFailedGuardAncestor(candidate);
|
|
1756
|
+
return !inStack && !blockedByFailedGuardAncestor;
|
|
1757
|
+
})
|
|
1758
|
+
.map((candidate) => {
|
|
1759
|
+
const hasDefaultGuard = Boolean(candidate.options?.hasDefault) || guardContainsDefault(candidate.value.guard);
|
|
1760
|
+
if (hasDefaultGuard) {
|
|
1761
|
+
candidate.options ??= {};
|
|
1762
|
+
candidate.options.hasDefault = true;
|
|
1763
|
+
hasDefault = true;
|
|
1764
|
+
}
|
|
1765
|
+
return candidate;
|
|
1766
|
+
});
|
|
1767
|
+
if (hasDefault) {
|
|
1768
|
+
/** There is a default guard, so sort candidates */
|
|
1769
|
+
evalCandidates = evalCandidates.slice(0).sort((a, b) => {
|
|
1770
|
+
let aDefault = a.options?.hasDefault;
|
|
1771
|
+
let bDefault = b.options?.hasDefault;
|
|
1772
|
+
/** No guard (or is just a plain ruleset) */
|
|
1773
|
+
if (!aDefault && !bDefault) {
|
|
1774
|
+
return 0;
|
|
1775
|
+
}
|
|
1776
|
+
if (!aDefault) {
|
|
1777
|
+
return -1;
|
|
1778
|
+
}
|
|
1779
|
+
if (!bDefault) {
|
|
1780
|
+
return 1;
|
|
1781
|
+
}
|
|
1782
|
+
return 0;
|
|
1783
|
+
});
|
|
1784
|
+
}
|
|
1785
|
+
if (evalCandidates.length === 0) {
|
|
1786
|
+
throw new ReferenceError('No matching mixins found.');
|
|
1787
|
+
}
|
|
1788
|
+
/**
|
|
1789
|
+
* Now we have a set of mixins that can return rulesets,
|
|
1790
|
+
* but first we need to create a new scope for each mixin,
|
|
1791
|
+
* and create variable declarations for each parameter.
|
|
1792
|
+
*/
|
|
1793
|
+
let outputRules = [];
|
|
1794
|
+
const restrictMixinOutputLookup = thisContext.leakyRules !== true;
|
|
1795
|
+
const originatesFromReferenceImport = (node) => {
|
|
1796
|
+
const queue = [node, node.sourceNode, node.sourceParent];
|
|
1797
|
+
const seen = new Set();
|
|
1798
|
+
while (queue.length > 0) {
|
|
1799
|
+
const current = queue.shift();
|
|
1800
|
+
if (!current || seen.has(current)) {
|
|
1801
|
+
continue;
|
|
1802
|
+
}
|
|
1803
|
+
seen.add(current);
|
|
1804
|
+
if (current.type === 'StyleImport') {
|
|
1805
|
+
const importOptions = current.options?.importOptions;
|
|
1806
|
+
if (importOptions?.reference === true || importOptions?._dedupe === true) {
|
|
1807
|
+
return true;
|
|
1808
|
+
}
|
|
1809
|
+
}
|
|
1810
|
+
queue.push(current.sourceNode, current.sourceParent, current.parent);
|
|
1811
|
+
}
|
|
1812
|
+
return false;
|
|
1813
|
+
};
|
|
1814
|
+
const clearReferenceModeForMixinOutput = (node) => {
|
|
1815
|
+
if (originatesFromReferenceImport(node)) {
|
|
1816
|
+
return;
|
|
1817
|
+
}
|
|
1818
|
+
if (node.options?.referenceMode === true) {
|
|
1819
|
+
node.options.referenceMode = false;
|
|
1820
|
+
}
|
|
1821
|
+
const nestedRules = node.value?.rules;
|
|
1822
|
+
if (nestedRules && isNode(nestedRules, 'Rules')) {
|
|
1823
|
+
clearReferenceModeForMixinOutput(nestedRules);
|
|
1824
|
+
}
|
|
1825
|
+
const children = node.value;
|
|
1826
|
+
if (Array.isArray(children)) {
|
|
1827
|
+
for (const child of children) {
|
|
1828
|
+
if (isNode(child, ['Rules', 'Ruleset', 'AtRule'])) {
|
|
1829
|
+
clearReferenceModeForMixinOutput(child);
|
|
1830
|
+
}
|
|
1831
|
+
}
|
|
1832
|
+
}
|
|
1833
|
+
};
|
|
1834
|
+
const resetEvalStateDeep = (node) => {
|
|
1835
|
+
node.preEvaluated = false;
|
|
1836
|
+
node.evaluated = false;
|
|
1837
|
+
if (isNode(node, 'Ruleset')) {
|
|
1838
|
+
const rulesetNode = node;
|
|
1839
|
+
const selector = rulesetNode.value.selector;
|
|
1840
|
+
const sourceSelector = selector?.sourceNode;
|
|
1841
|
+
if (sourceSelector && isNode(sourceSelector)) {
|
|
1842
|
+
// Recover definition-time selector shape (e.g. raw `&`) so call-site
|
|
1843
|
+
// preEval can rebuild selectors in the caller's frame.
|
|
1844
|
+
rulesetNode.value.selector = sourceSelector.copy(true);
|
|
1845
|
+
rulesetNode.value.selector.sourceNode = sourceSelector;
|
|
1846
|
+
}
|
|
1847
|
+
}
|
|
1848
|
+
if (isNode(node, 'Ampersand')) {
|
|
1849
|
+
// Ampersands cloned from mixin definitions can carry a stale selector container
|
|
1850
|
+
// that points at definition-time selectors. Clear it so call-site frames rebind `&`.
|
|
1851
|
+
const ampNode = node;
|
|
1852
|
+
ampNode._selectorContainer = undefined;
|
|
1853
|
+
ampNode._storedSelector = undefined;
|
|
1854
|
+
}
|
|
1855
|
+
const value = node.value;
|
|
1856
|
+
if (Array.isArray(value)) {
|
|
1857
|
+
for (const child of value) {
|
|
1858
|
+
if (isNode(child)) {
|
|
1859
|
+
resetEvalStateDeep(child);
|
|
1860
|
+
}
|
|
1861
|
+
}
|
|
1862
|
+
return;
|
|
1863
|
+
}
|
|
1864
|
+
if (value && typeof value === 'object') {
|
|
1865
|
+
const record = value;
|
|
1866
|
+
for (const propValue of Object.values(record)) {
|
|
1867
|
+
if (isNode(propValue)) {
|
|
1868
|
+
resetEvalStateDeep(propValue);
|
|
1869
|
+
continue;
|
|
1870
|
+
}
|
|
1871
|
+
if (Array.isArray(propValue)) {
|
|
1872
|
+
for (const item of propValue) {
|
|
1873
|
+
if (isNode(item)) {
|
|
1874
|
+
resetEvalStateDeep(item);
|
|
1875
|
+
}
|
|
1876
|
+
}
|
|
1877
|
+
}
|
|
1878
|
+
}
|
|
1879
|
+
}
|
|
1880
|
+
};
|
|
1881
|
+
const getRootSourceRules = (rules) => {
|
|
1882
|
+
let current = rules;
|
|
1883
|
+
const seen = new Set();
|
|
1884
|
+
while (current.sourceNode && isNode(current.sourceNode, 'Rules')) {
|
|
1885
|
+
const next = current.sourceNode;
|
|
1886
|
+
if (next === current || seen.has(next)) {
|
|
1887
|
+
break;
|
|
1888
|
+
}
|
|
1889
|
+
seen.add(current);
|
|
1890
|
+
current = next;
|
|
1891
|
+
}
|
|
1892
|
+
return current;
|
|
1893
|
+
};
|
|
1894
|
+
const DEF_FALSE_EITHER = -1;
|
|
1895
|
+
const DEF_NONE = 0;
|
|
1896
|
+
const DEF_TRUE = 1;
|
|
1897
|
+
const DEF_FALSE = 2;
|
|
1898
|
+
const pendingDefaultCandidates = [];
|
|
1899
|
+
let hasDefNoneCandidate = false;
|
|
1900
|
+
const evaluateCandidateOutput = async (candidate, rules, outerRules, params) => {
|
|
1901
|
+
const currentCall = thisContext.callStack.at(-1);
|
|
1902
|
+
// to prevent infinite loops (e.g., .recursion { .recursion(); })
|
|
1903
|
+
if (currentCall && thisContext.callMap.add(currentCall, params)) {
|
|
1904
|
+
// Recursive call detected - skip this candidate (don't add to outputRules)
|
|
1905
|
+
// This allows other candidates to still match
|
|
1906
|
+
return;
|
|
1907
|
+
}
|
|
1908
|
+
try {
|
|
1909
|
+
let newRules;
|
|
1910
|
+
if (!outerRules) {
|
|
1911
|
+
candidate.parent.adopt(rules);
|
|
1912
|
+
newRules = await rules.eval(thisContext);
|
|
1913
|
+
}
|
|
1914
|
+
else {
|
|
1915
|
+
// Evaluate in the wrapper scope so params are visible, but preserve the wrapper's
|
|
1916
|
+
// rulesVisibility (it keeps VarDeclaration public). Overwriting visibility here can
|
|
1917
|
+
// hide param vars from registry-based lookup.
|
|
1918
|
+
outerRules.push(...rules.value);
|
|
1919
|
+
newRules = await outerRules.eval(thisContext);
|
|
1920
|
+
}
|
|
1921
|
+
candidate.parent.adopt(newRules);
|
|
1922
|
+
// Rules should have index from eval, but ensure it matches candidate for sorting
|
|
1923
|
+
newRules.index = candidate.index;
|
|
1924
|
+
// Visibility should be preserved by Rules.eval - no need to set it explicitly here
|
|
1925
|
+
// The eval'd rules should already have their nodes registered
|
|
1926
|
+
// Ensure the registry is indexed before checking
|
|
1927
|
+
// Mark output Rules as mixin output - accessible only when lookup has a target
|
|
1928
|
+
newRules.options.isMixinOutput = restrictMixinOutputLookup;
|
|
1929
|
+
newRules.options.referenceMode = false;
|
|
1930
|
+
clearReferenceModeForMixinOutput(newRules);
|
|
1931
|
+
if (thisContext.treeContext?.file) {
|
|
1932
|
+
/**
|
|
1933
|
+
* NOTE (debug policy):
|
|
1934
|
+
* `hasParamVar` / `hasNestedMixin` visibility branching was removed and
|
|
1935
|
+
* should NOT be reintroduced.
|
|
1936
|
+
*
|
|
1937
|
+
* If this causes regressions, fix lookup/parenting behavior instead:
|
|
1938
|
+
* - declaration/mixin registry traversal semantics
|
|
1939
|
+
* - sourceParent/rulesParent/sourceRulesParent propagation
|
|
1940
|
+
*
|
|
1941
|
+
* Do not solve those regressions by adding new visibility heuristics based on
|
|
1942
|
+
* "contains param vars" or "contains nested mixins".
|
|
1943
|
+
*/
|
|
1944
|
+
newRules.options.rulesVisibility ??= {};
|
|
1945
|
+
newRules.options.rulesVisibility.VarDeclaration = 'private';
|
|
1946
|
+
}
|
|
1947
|
+
outputRules.push(newRules);
|
|
1948
|
+
}
|
|
1949
|
+
catch (error) {
|
|
1950
|
+
// If recursion was detected (ReferenceError), skip this candidate
|
|
1951
|
+
// This allows other candidates to still match
|
|
1952
|
+
if (error instanceof ReferenceError && error.message?.includes('Recursive mixin call')) {
|
|
1953
|
+
// Skip this candidate - recursion detected
|
|
1954
|
+
return;
|
|
1955
|
+
}
|
|
1956
|
+
// Re-throw other errors
|
|
1957
|
+
throw error;
|
|
1958
|
+
}
|
|
1959
|
+
finally {
|
|
1960
|
+
if (currentCall) {
|
|
1961
|
+
thisContext.callMap.delete(currentCall);
|
|
1962
|
+
}
|
|
1963
|
+
}
|
|
1964
|
+
};
|
|
1965
|
+
for (let candidate of evalCandidates) {
|
|
1966
|
+
if (isNode(candidate, 'Ruleset')) {
|
|
1967
|
+
// For Rulesets, guard was already evaluated at definition time in Ruleset.evalNode
|
|
1968
|
+
// guard === undefined means passed, guard instanceof Nil means failed
|
|
1969
|
+
const rulesetGuard = candidate.value.guard;
|
|
1970
|
+
if (rulesetGuard instanceof Nil) {
|
|
1971
|
+
// Guard failed at definition time - skip this ruleset
|
|
1972
|
+
continue;
|
|
1973
|
+
}
|
|
1974
|
+
const candidateRules = candidate.value.rules;
|
|
1975
|
+
const sourceRules = getRootSourceRules(candidateRules);
|
|
1976
|
+
let rules = sourceRules.clone(true);
|
|
1977
|
+
resetEvalStateDeep(rules);
|
|
1978
|
+
/** Adopt for lookup, then adopt for sorting */
|
|
1979
|
+
candidate.parent.adopt(rules);
|
|
1980
|
+
rules.sourceParent = sourceParent;
|
|
1981
|
+
let originalContext = thisContext.rulesContext;
|
|
1982
|
+
thisContext.rulesContext = rules;
|
|
1983
|
+
rules = await rules.eval(thisContext);
|
|
1984
|
+
thisContext.rulesContext = originalContext;
|
|
1985
|
+
candidate.parent.adopt(rules);
|
|
1986
|
+
// Rules should have index from eval, but ensure it matches candidate for sorting
|
|
1987
|
+
rules.index = candidate.index;
|
|
1988
|
+
// Skip empty Rules (e.g., containing only invisible nodes like comments)
|
|
1989
|
+
// Mark output Rules as mixin output - accessible only when lookup has a target
|
|
1990
|
+
rules.options.isMixinOutput = restrictMixinOutputLookup;
|
|
1991
|
+
rules.options.referenceMode = false;
|
|
1992
|
+
clearReferenceModeForMixinOutput(rules);
|
|
1993
|
+
outputRules.push(rules);
|
|
1994
|
+
continue;
|
|
1995
|
+
}
|
|
1996
|
+
// Less detached rulesets are represented as anonymous mixins (name is undefined).
|
|
1997
|
+
// Calling `@rulesetVar();` should *unlock* the rules into scope (including mixin definitions),
|
|
1998
|
+
// not eagerly execute/flatten them.
|
|
1999
|
+
if (!candidate.value.name && !candidate.value.params && !candidate.value.guard) {
|
|
2000
|
+
const sourceRules = getRootSourceRules(candidate.value.rules);
|
|
2001
|
+
let unlocked = sourceRules.clone(true);
|
|
2002
|
+
resetEvalStateDeep(unlocked);
|
|
2003
|
+
candidate.parent.adopt(unlocked);
|
|
2004
|
+
unlocked.sourceParent = sourceParent ?? caller;
|
|
2005
|
+
// Mark as mixin output; caller may override when leakyRules=true
|
|
2006
|
+
unlocked.options.isMixinOutput = restrictMixinOutputLookup;
|
|
2007
|
+
unlocked.options.referenceMode = false;
|
|
2008
|
+
clearReferenceModeForMixinOutput(unlocked);
|
|
2009
|
+
unlocked.index = candidate.index;
|
|
2010
|
+
outputRules.push(unlocked);
|
|
2011
|
+
continue;
|
|
2012
|
+
}
|
|
2013
|
+
let rules = candidate.value.rules;
|
|
2014
|
+
/** Create new rules, and add the candidate rules, to add to scope */
|
|
2015
|
+
rules = rules.clone(true);
|
|
2016
|
+
resetEvalStateDeep(rules);
|
|
2017
|
+
// During mixin evaluation, local declarations must be directly visible in the current scope
|
|
2018
|
+
// so they properly shadow outer params/variables while the body executes.
|
|
2019
|
+
rules.options.rulesVisibility ??= {};
|
|
2020
|
+
rules.options.rulesVisibility.VarDeclaration = 'public';
|
|
2021
|
+
candidate.parent.adopt(rules);
|
|
2022
|
+
rules.sourceParent = sourceParent;
|
|
2023
|
+
// Don't set index before evaluation - let evaluation assign the correct index
|
|
2024
|
+
/**
|
|
2025
|
+
* If we have params or a guard, we need to create a wrapper rules object,
|
|
2026
|
+
* so that the lookups of params and guard do not look at the cloned rules,
|
|
2027
|
+
* but instead look upwards / outwards.
|
|
2028
|
+
*/
|
|
2029
|
+
let outerRules;
|
|
2030
|
+
/** Now we need to add our parameters, if any */
|
|
2031
|
+
let params = candidate.value.params;
|
|
2032
|
+
if (params) {
|
|
2033
|
+
outerRules = Rules.create([], {
|
|
2034
|
+
rulesVisibility: {
|
|
2035
|
+
Ruleset: 'public',
|
|
2036
|
+
Declaration: 'public',
|
|
2037
|
+
VarDeclaration: 'public',
|
|
2038
|
+
Mixin: 'public'
|
|
2039
|
+
}
|
|
2040
|
+
});
|
|
2041
|
+
candidate.parent.adopt(outerRules);
|
|
2042
|
+
outerRules.index = candidate.index;
|
|
2043
|
+
for (let i = 0; i < params.value.length; i++) {
|
|
2044
|
+
let param = params.value[i];
|
|
2045
|
+
if (isNode(param, 'Rest')) {
|
|
2046
|
+
// Rest parameters need to be converted to VarDeclaration for registration
|
|
2047
|
+
// Auto-generate a name if Rest doesn't have one (Less allows unnamed rest params)
|
|
2048
|
+
let restName;
|
|
2049
|
+
if (typeof param.value === 'string') {
|
|
2050
|
+
restName = param.value;
|
|
2051
|
+
}
|
|
2052
|
+
else {
|
|
2053
|
+
// Auto-generate name: "rest", "rest1", "rest2", etc. based on position
|
|
2054
|
+
// Check if there are other rest params to avoid conflicts
|
|
2055
|
+
let restCount = 0;
|
|
2056
|
+
for (let j = 0; j < i; j++) {
|
|
2057
|
+
const p = params.value[j];
|
|
2058
|
+
if (isNode(p, 'Rest')) {
|
|
2059
|
+
restCount++;
|
|
2060
|
+
}
|
|
2061
|
+
}
|
|
2062
|
+
restName = restCount === 0 ? 'rest' : `rest${restCount + 1}`;
|
|
2063
|
+
}
|
|
2064
|
+
// Convert Rest to VarDeclaration so it can be registered and referenced.
|
|
2065
|
+
// If matching did not populate a node value, default to an empty sequence
|
|
2066
|
+
// (not a literal name/Nil), so @tail... behaves as "no remaining args".
|
|
2067
|
+
const restValue = isNode(param.value)
|
|
2068
|
+
? param.value
|
|
2069
|
+
: (thisContext.treeContext?.file
|
|
2070
|
+
? new Sequence([])
|
|
2071
|
+
: new Any(restName, { role: 'property' }));
|
|
2072
|
+
const restVarDecl = new VarDeclaration({
|
|
2073
|
+
name: new Any(restName, { role: 'property' }),
|
|
2074
|
+
value: restValue
|
|
2075
|
+
}, { paramVar: true });
|
|
2076
|
+
// Replace Rest with VarDeclaration in params
|
|
2077
|
+
params.value[i] = restVarDecl;
|
|
2078
|
+
param = restVarDecl;
|
|
2079
|
+
}
|
|
2080
|
+
if (isNode(param, 'VarDeclaration')) {
|
|
2081
|
+
// Assign negative indices so they're conceptually "before" the rules and found first
|
|
2082
|
+
if (param.index === undefined) {
|
|
2083
|
+
// Use negative indices starting from -1, -2, etc. so they sort before regular rules
|
|
2084
|
+
param.index = -(i + 1);
|
|
2085
|
+
}
|
|
2086
|
+
// Mark as parameter var so it can be stripped from mixin output after evaluation.
|
|
2087
|
+
param.options ??= {};
|
|
2088
|
+
param.options.paramVar = true;
|
|
2089
|
+
// Keep parameter vars lookupable but hidden in normal output.
|
|
2090
|
+
// They still render in tests that set Node.fullRender=true.
|
|
2091
|
+
param.removeFlag(F_VISIBLE);
|
|
2092
|
+
outerRules.push(param);
|
|
2093
|
+
}
|
|
2094
|
+
// Note: Any with role: 'property' should have been converted to VarDeclaration during matching
|
|
2095
|
+
// If we see one here, it's an error - params should all be VarDeclaration by now
|
|
2096
|
+
}
|
|
2097
|
+
const shouldDefineArguments = Boolean(thisContext.treeContext?.file);
|
|
2098
|
+
if (shouldDefineArguments) {
|
|
2099
|
+
const argumentsArgs = [];
|
|
2100
|
+
const argumentsDecl = new VarDeclaration({
|
|
2101
|
+
name: new Any('arguments', { role: 'property' }),
|
|
2102
|
+
value: new Sequence(argumentsArgs)
|
|
2103
|
+
}, { readonly: true, paramVar: true });
|
|
2104
|
+
argumentsDecl.removeFlag(F_VISIBLE);
|
|
2105
|
+
outerRules.push(argumentsDecl);
|
|
2106
|
+
const paramValues = params?.value
|
|
2107
|
+
.filter((p) => isNode(p, 'VarDeclaration'))
|
|
2108
|
+
.map(p => p.value.value);
|
|
2109
|
+
const argumentNodes = (paramValues && paramValues.length > 0) ? paramValues : nodeArgs;
|
|
2110
|
+
for (const argNode of argumentNodes) {
|
|
2111
|
+
const cloned = argNode.copy(true, freezeChildren);
|
|
2112
|
+
cloned.frozen = true;
|
|
2113
|
+
argumentsArgs.push(cloned);
|
|
2114
|
+
}
|
|
2115
|
+
}
|
|
2116
|
+
}
|
|
2117
|
+
/** Now we can evaluate our guards, if any */
|
|
2118
|
+
let guard = candidate.value.guard?.copy(true);
|
|
2119
|
+
let passes = true;
|
|
2120
|
+
let rulesContext = thisContext.rulesContext;
|
|
2121
|
+
// Call-time resolution is handled by the current context.rulesContext
|
|
2122
|
+
thisContext.rulesContext = outerRules ?? rules;
|
|
2123
|
+
try {
|
|
2124
|
+
if (guard) {
|
|
2125
|
+
outerRules ??= Rules.create([]);
|
|
2126
|
+
outerRules.adopt(guard);
|
|
2127
|
+
candidate.parent.adopt(outerRules);
|
|
2128
|
+
/** Allow lookup on the inherited rules */
|
|
2129
|
+
passes = false;
|
|
2130
|
+
let guardPasses = false;
|
|
2131
|
+
let defaultGroup = DEF_FALSE_EITHER;
|
|
2132
|
+
if (hasDefault) {
|
|
2133
|
+
const originalIsDefault = thisContext.isDefault;
|
|
2134
|
+
const evalWithDefault = async (isDefaultValue) => {
|
|
2135
|
+
const probeGuard = candidate.value.guard?.copy(true);
|
|
2136
|
+
if (!probeGuard) {
|
|
2137
|
+
return false;
|
|
2138
|
+
}
|
|
2139
|
+
outerRules.adopt(probeGuard);
|
|
2140
|
+
thisContext.isDefault = isDefaultValue;
|
|
2141
|
+
const probeResult = await probeGuard.eval(thisContext);
|
|
2142
|
+
return probeResult instanceof Bool && probeResult.value === true;
|
|
2143
|
+
};
|
|
2144
|
+
const passWhenDefaultFalse = await evalWithDefault(false);
|
|
2145
|
+
const passWhenDefaultTrue = await evalWithDefault(true);
|
|
2146
|
+
thisContext.isDefault = originalIsDefault;
|
|
2147
|
+
if (passWhenDefaultFalse || passWhenDefaultTrue) {
|
|
2148
|
+
passes = true;
|
|
2149
|
+
if (passWhenDefaultFalse && passWhenDefaultTrue) {
|
|
2150
|
+
defaultGroup = DEF_NONE;
|
|
2151
|
+
hasDefNoneCandidate = true;
|
|
2152
|
+
}
|
|
2153
|
+
else {
|
|
2154
|
+
defaultGroup = passWhenDefaultTrue ? DEF_TRUE : DEF_FALSE;
|
|
2155
|
+
}
|
|
2156
|
+
}
|
|
2157
|
+
guardPasses = passes;
|
|
2158
|
+
if (passes) {
|
|
2159
|
+
pendingDefaultCandidates.push({
|
|
2160
|
+
candidate: candidate,
|
|
2161
|
+
rules,
|
|
2162
|
+
outerRules,
|
|
2163
|
+
params,
|
|
2164
|
+
group: defaultGroup
|
|
2165
|
+
});
|
|
2166
|
+
}
|
|
2167
|
+
}
|
|
2168
|
+
else {
|
|
2169
|
+
/** All nodes need context to be evaluated */
|
|
2170
|
+
thisContext.isDefault = false;
|
|
2171
|
+
guard = await guard.eval(thisContext);
|
|
2172
|
+
/** Less guards only pass on explicit Bool(true), never JS truthiness. */
|
|
2173
|
+
guardPasses = guard instanceof Bool && guard.value === true;
|
|
2174
|
+
if (guardPasses) {
|
|
2175
|
+
passes = true;
|
|
2176
|
+
hasDefNoneCandidate = true;
|
|
2177
|
+
}
|
|
2178
|
+
}
|
|
2179
|
+
}
|
|
2180
|
+
if (!passes) {
|
|
2181
|
+
continue;
|
|
2182
|
+
}
|
|
2183
|
+
if (!guard || !hasDefault) {
|
|
2184
|
+
// Non-default candidates are equivalent to Less's defNone group
|
|
2185
|
+
// (match regardless of default() assumption), so they suppress ambiguity.
|
|
2186
|
+
hasDefNoneCandidate = true;
|
|
2187
|
+
}
|
|
2188
|
+
if (guard && hasDefault) {
|
|
2189
|
+
continue;
|
|
2190
|
+
}
|
|
2191
|
+
await evaluateCandidateOutput(candidate, rules, outerRules, params);
|
|
2192
|
+
}
|
|
2193
|
+
finally {
|
|
2194
|
+
thisContext.rulesContext = rulesContext;
|
|
2195
|
+
}
|
|
2196
|
+
}
|
|
2197
|
+
if (pendingDefaultCandidates.length > 0) {
|
|
2198
|
+
let defTrueCount = 0;
|
|
2199
|
+
let defFalseCount = 0;
|
|
2200
|
+
for (const pending of pendingDefaultCandidates) {
|
|
2201
|
+
if (pending.group === DEF_TRUE) {
|
|
2202
|
+
defTrueCount++;
|
|
2203
|
+
}
|
|
2204
|
+
else if (pending.group === DEF_FALSE) {
|
|
2205
|
+
defFalseCount++;
|
|
2206
|
+
}
|
|
2207
|
+
else if (pending.group === DEF_NONE) {
|
|
2208
|
+
hasDefNoneCandidate = true;
|
|
2209
|
+
}
|
|
2210
|
+
}
|
|
2211
|
+
const defaultResult = hasDefNoneCandidate ? DEF_FALSE : DEF_TRUE;
|
|
2212
|
+
if (!hasDefNoneCandidate && (defTrueCount + defFalseCount) > 1) {
|
|
2213
|
+
throw new ReferenceError('Ambiguous use of default() while matching mixins.');
|
|
2214
|
+
}
|
|
2215
|
+
for (const pending of pendingDefaultCandidates) {
|
|
2216
|
+
if (pending.group !== DEF_NONE && pending.group !== defaultResult) {
|
|
2217
|
+
continue;
|
|
2218
|
+
}
|
|
2219
|
+
const previousRulesContext = thisContext.rulesContext;
|
|
2220
|
+
thisContext.rulesContext = pending.outerRules ?? pending.rules;
|
|
2221
|
+
try {
|
|
2222
|
+
await evaluateCandidateOutput(pending.candidate, pending.rules, pending.outerRules, pending.params);
|
|
2223
|
+
}
|
|
2224
|
+
finally {
|
|
2225
|
+
thisContext.rulesContext = previousRulesContext;
|
|
2226
|
+
}
|
|
2227
|
+
}
|
|
2228
|
+
}
|
|
2229
|
+
/**
|
|
2230
|
+
* Now that we have output rules, sort them by
|
|
2231
|
+
* their original order
|
|
2232
|
+
*/
|
|
2233
|
+
outputRules.sort(comparePosition);
|
|
2234
|
+
/** Create a rules wrapper - but optimize to avoid unnecessary nesting */
|
|
2235
|
+
let output;
|
|
2236
|
+
if (outputRules.length === 1) {
|
|
2237
|
+
output = outputRules[0];
|
|
2238
|
+
// Ensure single output rule is marked as mixin output
|
|
2239
|
+
output.options.isMixinOutput = restrictMixinOutputLookup;
|
|
2240
|
+
output.options.referenceMode = false;
|
|
2241
|
+
clearReferenceModeForMixinOutput(output);
|
|
2242
|
+
}
|
|
2243
|
+
else {
|
|
2244
|
+
/**
|
|
2245
|
+
* Wrap these in rules marked as mixin output - accessible only when lookup has a target.
|
|
2246
|
+
* This prevents mixin output from being searched by untargeted lookups.
|
|
2247
|
+
*/
|
|
2248
|
+
output = Rules.create([], {
|
|
2249
|
+
rulesVisibility: {
|
|
2250
|
+
Ruleset: 'public',
|
|
2251
|
+
Declaration: 'public',
|
|
2252
|
+
VarDeclaration: 'public',
|
|
2253
|
+
Mixin: 'public'
|
|
2254
|
+
},
|
|
2255
|
+
isMixinOutput: restrictMixinOutputLookup,
|
|
2256
|
+
referenceMode: false
|
|
2257
|
+
});
|
|
2258
|
+
/**
|
|
2259
|
+
* Add rules but keep their original parents for further lazy lookups.
|
|
2260
|
+
* Ensure each rule has VarDeclaration: 'optional' before pushing (registerNode uses node's own rulesVisibility)
|
|
2261
|
+
*/
|
|
2262
|
+
for (let i = 0; i < outputRules.length; i++) {
|
|
2263
|
+
let rule = outputRules[i];
|
|
2264
|
+
rule.frozen = true;
|
|
2265
|
+
/** Set a sequential index for lookup sorting */
|
|
2266
|
+
rule.index = i;
|
|
2267
|
+
output.push(rule);
|
|
2268
|
+
}
|
|
2269
|
+
}
|
|
2270
|
+
/**
|
|
2271
|
+
* IMPORTANT: Do NOT force `output` to be evaluated here.
|
|
2272
|
+
*
|
|
2273
|
+
* Even though candidate rule bodies are usually evaluated during mixin execution, callers
|
|
2274
|
+
* (e.g. `Call.evalNode`) rely on `.eval(context)` to finish evaluation. Marking these flags
|
|
2275
|
+
* true can skip evaluation and leak unevaluated nodes (like `Call`) into serialization.
|
|
2276
|
+
*/
|
|
2277
|
+
/** Now push all rules into the rules value */
|
|
2278
|
+
if (this instanceof Context) {
|
|
2279
|
+
output.index ??= this.ruleCounter++;
|
|
2280
|
+
// If the output Rules is empty, return Nil instead
|
|
2281
|
+
if (output.value.length === 0) {
|
|
2282
|
+
return new Nil();
|
|
2283
|
+
}
|
|
2284
|
+
return output;
|
|
2285
|
+
}
|
|
2286
|
+
else {
|
|
2287
|
+
const obj = output.toObject();
|
|
2288
|
+
return obj;
|
|
2289
|
+
}
|
|
2290
|
+
}
|
|
2291
|
+
return returnFunc;
|
|
2292
|
+
}
|
|
2293
|
+
//# sourceMappingURL=rules.js.map
|