@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,3033 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* EXTEND UTILITY - REQUIREMENTS AND FEATURE SET
|
|
3
|
+
* ==============================================
|
|
4
|
+
*
|
|
5
|
+
* This module implements the core extend functionality for Jess, allowing selectors to
|
|
6
|
+
* "extend" other selectors, adding them to selector lists or wrapping them in :is() pseudo-classes.
|
|
7
|
+
*
|
|
8
|
+
* ## Core Concept
|
|
9
|
+
*
|
|
10
|
+
* Extend allows a selector to "inherit" styles from another selector by adding the extending
|
|
11
|
+
* selector to the target selector's selector list, or by creating :is() wrappers when appropriate.
|
|
12
|
+
*
|
|
13
|
+
* Example: `.child:extend(.parent)` means "add .child to .parent's selector list"
|
|
14
|
+
* Result: `.parent, .child { ... }`
|
|
15
|
+
*
|
|
16
|
+
* ## Two Modes: Partial vs Full
|
|
17
|
+
*
|
|
18
|
+
* ### Partial Mode (partial: true)
|
|
19
|
+
* - Used when the `!all` flag is NOT specified
|
|
20
|
+
* - Creates :is() wrappers for component-level matches
|
|
21
|
+
* - Example: `.a>.b:extend(.b !all)` → `.a>:is(.b,.c)` (if .b extended with .c)
|
|
22
|
+
*
|
|
23
|
+
* ### Full Mode (partial: false)
|
|
24
|
+
* - Used when the `!all` flag IS specified
|
|
25
|
+
* - Creates selector lists for root-level matches
|
|
26
|
+
* - Creates :is() wrappers for component matches in compound selectors (to preserve other components)
|
|
27
|
+
* - Example: `.btn:hover:extend(.btn !all)` → `:is(.btn,.primary):hover` (if .btn extended with .primary)
|
|
28
|
+
* - **CRITICAL**: Rejects ALL partial matches - if a match is only PARTIAL (e.g., `.i` matching within `.i.j`),
|
|
29
|
+
* the selector is returned unchanged, regardless of context (SelectorList, :is(), compound, complex, etc.)
|
|
30
|
+
* - The partial match is determined at the level of the matched selector itself (e.g., `.i` is partial within `.i.j`)
|
|
31
|
+
* - Outer context (SelectorList, :is(), components after) is irrelevant for determining if a match is partial
|
|
32
|
+
* - **Exception**: Even if a match is a FULL match of an item within `:is()`, if there are components AFTER the `:is()`,
|
|
33
|
+
* it becomes a partial match of the entire selector and is rejected
|
|
34
|
+
* - Example: `:is(.i).j` matching `.i` (full match of item in :is()) is partial because `.j` comes after the `:is()`
|
|
35
|
+
*
|
|
36
|
+
* ## When to Create :is() Wrappers vs Selector Lists
|
|
37
|
+
*
|
|
38
|
+
* ### Create Selector List (.a, .b) when:
|
|
39
|
+
* 1. Root-level full match (entire selector matches): `.a:extend(.a !all)` → `.a, .b`
|
|
40
|
+
* - This applies regardless of selector type (simple, compound, complex, etc.)
|
|
41
|
+
* - Example: `.a.b:extend(.a.b !all)` → `.a.b, .c` (not because it's compound, but because entire selector matches)
|
|
42
|
+
* 2. Partial match where extendWith is a complex selector and matches a segment:
|
|
43
|
+
* - Example: `.a.b > .c.d {}` with `.g:extend(.b > .c !all)` → `.a.b > .c.d, .g {}`
|
|
44
|
+
* - Reasoning: In compounds, order doesn't matter. The matched segment is replaced entirely.
|
|
45
|
+
* - Example: `.a > .b.c > .d {}` with `.e:extend(.a > .c !all)` → `:is(.a > .b.c, .e) > .d {}`
|
|
46
|
+
*
|
|
47
|
+
* ### Create :is() Wrapper (:is(.a, .b)) when:
|
|
48
|
+
* 1. Component match in compound selector (FULL mode): `.btn:hover:extend(.btn !all)` → `:is(.btn,.primary):hover`
|
|
49
|
+
* - REASON: Must preserve other components (like :hover) that aren't being extended
|
|
50
|
+
* 2. Component match in compound selector (PARTIAL mode): `.a.b:extend(.b)` → `.a:is(.b,.c)`
|
|
51
|
+
* 3. Component match in complex selector (FULL mode): `.aa .dd:extend(.aa !all)` → `:is(.aa,.cc) .dd`
|
|
52
|
+
* - REASON: Anything that's "part of" a selector gets wrapped in :is()
|
|
53
|
+
* 4. Component match in complex selector (PARTIAL mode): `.a>.b:extend(.b)` → `.a>:is(.b,.c)`
|
|
54
|
+
*
|
|
55
|
+
* ## Partial match: what gets wrapped
|
|
56
|
+
*
|
|
57
|
+
* - **Match within one compound**: Wrap only the matched part. E.g. `.a.b` in `.a.c.b` + extend .q → `:is(.a.b, .q).c`
|
|
58
|
+
* - **Match spans a combinator**: Wrap the FULL segment from first to last matched compound. E.g. `.a.b > .x` in
|
|
59
|
+
* `div + .a.c.b > .y.x` + extend .q → `div + :is(.a.c.b > .y.x, .q)`. See EXTEND_RULES.md §3a.
|
|
60
|
+
* Do NOT decide by target type or path length (target can be :is(complex), SelectorList, etc.). Use what the
|
|
61
|
+
* match PRODUCES (e.g. includes combinators?) and keySet/equivalency.
|
|
62
|
+
*
|
|
63
|
+
* 5. Full match of entire selector within :is() argument: `:is(.a,.b):extend(.a !all)` → `:is(.a,.b,.c)`
|
|
64
|
+
* - REASON: When matching an entire selector within a SelectorList (the :is() argument),
|
|
65
|
+
* we just add to that list, same as root-level matches. No special handling needed.
|
|
66
|
+
* - The recursive extend applies the same logic: full match = add to list, component match = wrap in :is()
|
|
67
|
+
*
|
|
68
|
+
* ## Critical Distinction: Component Matches in Compound Selectors
|
|
69
|
+
*
|
|
70
|
+
* **IMPORTANT**: Even in FULL mode, component matches within compound selectors create :is() wrappers,
|
|
71
|
+
* NOT selector lists. This is because:
|
|
72
|
+
* - `.btn:hover` extending with `.primary` should become `:is(.btn,.primary):hover`
|
|
73
|
+
* - NOT `.btn:hover,.primary:hover` (which would be wrong - `.primary:hover` doesn't exist in original)
|
|
74
|
+
*
|
|
75
|
+
* The other components of the compound selector (like `:hover`) must be preserved, which requires
|
|
76
|
+
* wrapping in :is() rather than creating a selector list.
|
|
77
|
+
*
|
|
78
|
+
* ## Special Cases
|
|
79
|
+
*
|
|
80
|
+
* ### Boundary Crossing
|
|
81
|
+
* - When a match crosses an :is() boundary (e.g., `:is(.a, .b).c` matching `.b.c`), the selector
|
|
82
|
+
* must be flattened first: `:is(.a, .b).c` → `:is(.a.c, .b.c)`
|
|
83
|
+
* - Then, if extending the flattened result, apply normal extend rules:
|
|
84
|
+
* - Example: `:is(.a, .x).c > :is(.b > .y).d {}` with `.e:extend(.a.c) {}`
|
|
85
|
+
* - Step 1: Flatten boundary crossing → `:is(.a.c, .x.c) > :is(.b > .y).d {}`
|
|
86
|
+
* - Step 2: Extend `.a.c` with `.e` (full match in SelectorList) → `:is(.a.c, .x.c, .e) > :is(.b > .y).d {}`
|
|
87
|
+
* - REASON: `.a.c` is a full match in the SelectorList, so we add `.e` to the list (same as root-level)
|
|
88
|
+
*
|
|
89
|
+
* ### Self-Referencing Extends
|
|
90
|
+
* - `.a:extend(.a)` should be ignored (handled by shouldSkipRuleset in extend-roots.ts)
|
|
91
|
+
*
|
|
92
|
+
* ### Pseudo-Selector Arguments
|
|
93
|
+
* - Matches inside :is(), :where(), :not(), :has() arguments are extended recursively
|
|
94
|
+
* - Only :is() allows boundary crossing
|
|
95
|
+
*
|
|
96
|
+
* ## Multiple Component Matches
|
|
97
|
+
*
|
|
98
|
+
* When multiple components in a compound selector match, each component is wrapped separately
|
|
99
|
+
* in its own :is() wrapper:
|
|
100
|
+
* - Example: `.a.b.c` with `.a` extended by `.x` and `.b` extended by `.y`
|
|
101
|
+
* - Result: `:is(.a, .x):is(.b, .y).c`
|
|
102
|
+
* - Each match is independent and gets its own :is() wrapper
|
|
103
|
+
*
|
|
104
|
+
* For a concise "rules of extend" checklist, see `EXTEND_RULES.md`.
|
|
105
|
+
* For "where are the tests / where to add coverage", see `__tests__/EXTEND_TEST_INDEX.md`.
|
|
106
|
+
*
|
|
107
|
+
* CORE PRINCIPLE: All extend matching (finding + full-match decision) is by selector equivalency
|
|
108
|
+
* only — never by exact AST or exact serialization. See EXTEND_RULES.md §0.
|
|
109
|
+
*/
|
|
110
|
+
import { SelectorList } from '../selector-list.js';
|
|
111
|
+
import { ComplexSelector } from '../selector-complex.js';
|
|
112
|
+
import { CompoundSelector } from '../selector-compound.js';
|
|
113
|
+
import { PseudoSelector, is as isSelectorPseudo } from '../selector-pseudo.js';
|
|
114
|
+
import { Ampersand } from '../ampersand.js';
|
|
115
|
+
import { Combinator } from '../combinator.js';
|
|
116
|
+
import { isNode } from './is-node.js';
|
|
117
|
+
import { findExtendableLocations } from './extend-helpers.js';
|
|
118
|
+
import { normalizeSelectorForExtend } from './find-extendable-locations.js';
|
|
119
|
+
import { F_EXTENDED, F_EXTEND_TARGET, F_IMPLICIT_AMPERSAND, F_VISIBLE } from '../node.js';
|
|
120
|
+
import { selectorCompare } from './selector-compare.js';
|
|
121
|
+
const { isArray } = Array;
|
|
122
|
+
let extendOrderMap = null;
|
|
123
|
+
/** Fallback for clones: selectors inside :is() may be clones, so WeakMap lookup fails. Key by valueOf() string. */
|
|
124
|
+
let extendOrderByValueOf = null;
|
|
125
|
+
export function setExtendOrderMap(map, orderByValueOf) {
|
|
126
|
+
extendOrderMap = map;
|
|
127
|
+
extendOrderByValueOf = orderByValueOf ?? null;
|
|
128
|
+
}
|
|
129
|
+
function isSelectorNode(value) {
|
|
130
|
+
return !!value && typeof value === 'object' && value.isSelector === true;
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Error type constants for extend operations
|
|
134
|
+
*/
|
|
135
|
+
export const ExtendErrorType = {
|
|
136
|
+
NOT_FOUND: 'NOT_FOUND',
|
|
137
|
+
ELEMENT_CONFLICT: 'ELEMENT_CONFLICT',
|
|
138
|
+
ID_CONFLICT: 'ID_CONFLICT',
|
|
139
|
+
AMPERSAND_BOUNDARY: 'AMPERSAND_BOUNDARY',
|
|
140
|
+
PARTIAL_MATCH: 'PARTIAL_MATCH'
|
|
141
|
+
};
|
|
142
|
+
export class ExtendError extends Error {
|
|
143
|
+
type;
|
|
144
|
+
context;
|
|
145
|
+
constructor(type, message, context) {
|
|
146
|
+
super(message);
|
|
147
|
+
this.type = type;
|
|
148
|
+
this.context = context;
|
|
149
|
+
this.name = 'ExtendError';
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
export function applyExtendsToSelector(initialSelector, extendsList) {
|
|
153
|
+
let selector = initialSelector;
|
|
154
|
+
const instructions = extendsList.slice();
|
|
155
|
+
let changed = true;
|
|
156
|
+
while (changed && instructions.length > 0) {
|
|
157
|
+
changed = false;
|
|
158
|
+
for (let i = 0; i < instructions.length; i += 1) {
|
|
159
|
+
const instruction = instructions[i];
|
|
160
|
+
if (!instruction) {
|
|
161
|
+
continue;
|
|
162
|
+
}
|
|
163
|
+
const { target, extendWith, partial } = instruction;
|
|
164
|
+
const result = tryExtendSelector(selector, target, extendWith, partial);
|
|
165
|
+
if (result && !result.error) {
|
|
166
|
+
const beforeValue = selector.valueOf();
|
|
167
|
+
const afterValue = result.value.valueOf();
|
|
168
|
+
if (afterValue !== beforeValue) {
|
|
169
|
+
selector = result.value;
|
|
170
|
+
instructions.splice(i, 1);
|
|
171
|
+
changed = true;
|
|
172
|
+
break;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
return selector;
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* Helper to create successful extend results
|
|
181
|
+
*/
|
|
182
|
+
function createSuccessResult(selector) {
|
|
183
|
+
return { value: selector };
|
|
184
|
+
}
|
|
185
|
+
/**
|
|
186
|
+
* Helper to create error extend results
|
|
187
|
+
*/
|
|
188
|
+
function createErrorResult(selector, error) {
|
|
189
|
+
return { value: selector, error };
|
|
190
|
+
}
|
|
191
|
+
/**
|
|
192
|
+
* Creates a deduplicated selector list using simple valueOf() comparison
|
|
193
|
+
* @param selectors - Array of selectors to deduplicate
|
|
194
|
+
* @returns Deduplicated array of selectors
|
|
195
|
+
*/
|
|
196
|
+
function deduplicateSelectors(selectors) {
|
|
197
|
+
const seen = new Set();
|
|
198
|
+
const result = [];
|
|
199
|
+
for (const selector of selectors) {
|
|
200
|
+
const stringValue = selector.valueOf();
|
|
201
|
+
if (!seen.has(stringValue)) {
|
|
202
|
+
seen.add(stringValue);
|
|
203
|
+
result.push(selector);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
return result;
|
|
207
|
+
}
|
|
208
|
+
/**
|
|
209
|
+
* Wrap a matched selector/component in an :is() including extendWith.
|
|
210
|
+
* Centralizes:
|
|
211
|
+
* - extracting selectors from extendWith when it's already :is()
|
|
212
|
+
* - validation and error context plumbing
|
|
213
|
+
*/
|
|
214
|
+
function wrapMatchInIs(matched, inheritFrom, extendWith, contextSelector, context, extendWithSelectors) {
|
|
215
|
+
const computed = extendWithSelectors ?? extractSelectorsFromIs(extendWith);
|
|
216
|
+
// Self-extends on the exact same matched component are visibility-only;
|
|
217
|
+
// avoid generating :is(.x,.x) wrappers and preserve the original shape.
|
|
218
|
+
if (computed.length === 1 && computed[0].valueOf() === matched.valueOf()) {
|
|
219
|
+
return matched.copy(true);
|
|
220
|
+
}
|
|
221
|
+
const matchedForList = matched.copy(true);
|
|
222
|
+
if (context?.find && context.find.valueOf() !== context.extendWith?.valueOf()) {
|
|
223
|
+
matchedForList.addFlag(F_EXTEND_TARGET);
|
|
224
|
+
}
|
|
225
|
+
const extendWithForList = computed.map((item) => {
|
|
226
|
+
const out = item.copy(true);
|
|
227
|
+
out.addFlag(F_EXTENDED);
|
|
228
|
+
return out;
|
|
229
|
+
});
|
|
230
|
+
const deduped = deduplicateSelectors([matchedForList, ...extendWithForList]);
|
|
231
|
+
return createValidatedIsWrapperWithErrors(deduped, inheritFrom, contextSelector, context);
|
|
232
|
+
}
|
|
233
|
+
/**
|
|
234
|
+
* Processes selectors in a single pass by:
|
|
235
|
+
* 1. Flattening generated :is() wrappers
|
|
236
|
+
* 2. Deduplicating selectors
|
|
237
|
+
* 3. Discarding or flattening ampersands.
|
|
238
|
+
*/
|
|
239
|
+
export function createProcessedSelector(selectors, root) {
|
|
240
|
+
let out = [];
|
|
241
|
+
// Only deduplicate at root level (SelectorList context), not for compound selector components
|
|
242
|
+
// Compound selectors can have duplicate components (e.g., .v.w.v), so we must preserve all
|
|
243
|
+
let selectorValues = root ? new Set() : null;
|
|
244
|
+
const push = (selector) => {
|
|
245
|
+
if (selectorValues) {
|
|
246
|
+
// Root level (SelectorList) - deduplicate
|
|
247
|
+
let value = selector.valueOf();
|
|
248
|
+
if (!selectorValues.has(value)) {
|
|
249
|
+
selectorValues.add(value);
|
|
250
|
+
out.push(selector);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
else {
|
|
254
|
+
// Non-root (compound components, etc.) - preserve all, no deduplication
|
|
255
|
+
out.push(selector);
|
|
256
|
+
}
|
|
257
|
+
};
|
|
258
|
+
if (!isArray(selectors)) {
|
|
259
|
+
selectors = [selectors];
|
|
260
|
+
}
|
|
261
|
+
else {
|
|
262
|
+
selectors = [...selectors];
|
|
263
|
+
}
|
|
264
|
+
for (let el of selectors) {
|
|
265
|
+
const originalEl = el;
|
|
266
|
+
// Copy-on-write: only copy if we might modify the selector
|
|
267
|
+
// Simple selectors that won't be modified don't need copying
|
|
268
|
+
let needsCopy = isNode(el, 'PseudoSelector') || isNode(el, 'SelectorList')
|
|
269
|
+
|| isNode(el, 'CompoundSelector') || isNode(el, 'ComplexSelector') || isNode(el, 'Ampersand');
|
|
270
|
+
if (needsCopy) {
|
|
271
|
+
el = el.copy();
|
|
272
|
+
}
|
|
273
|
+
if (isNode(el, 'PseudoSelector')) {
|
|
274
|
+
if (el.value.name === ':is') {
|
|
275
|
+
const arg = el.value.arg;
|
|
276
|
+
if (arg && isNode(arg, 'SelectorList')) {
|
|
277
|
+
const deduped = deduplicateSelectors(arg.value);
|
|
278
|
+
if (deduped.length === 1) {
|
|
279
|
+
push(deduped[0]);
|
|
280
|
+
continue;
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
if (root && el.value.name === ':is' && el.generated) {
|
|
285
|
+
let result = createProcessedSelector(el.value.arg);
|
|
286
|
+
/**
|
|
287
|
+
* Result will be a single selector, which we want to bubble
|
|
288
|
+
* into the parent selector array if we're at the root.
|
|
289
|
+
*/
|
|
290
|
+
if (isNode(result, 'SelectorList')) {
|
|
291
|
+
for (let el of result.value) {
|
|
292
|
+
push(el);
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
else {
|
|
296
|
+
push(result);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
else {
|
|
300
|
+
if (el.value.arg) {
|
|
301
|
+
let result = createProcessedSelector(el.value.arg, root);
|
|
302
|
+
// If result is a SelectorList, check if it contains generated :is() wrappers to flatten
|
|
303
|
+
if (isArray(result)) {
|
|
304
|
+
// Flatten any generated :is() wrappers in the result
|
|
305
|
+
const flattened = [];
|
|
306
|
+
for (const sel of result) {
|
|
307
|
+
if (isNode(sel, 'PseudoSelector') && sel.value.name === ':is' && sel.generated) {
|
|
308
|
+
// Unwrap generated :is() - extract its argument selectors
|
|
309
|
+
const arg = sel.value.arg;
|
|
310
|
+
if (arg && isNode(arg, 'SelectorList')) {
|
|
311
|
+
flattened.push(...arg.value);
|
|
312
|
+
}
|
|
313
|
+
else if (arg) {
|
|
314
|
+
flattened.push(arg);
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
else {
|
|
318
|
+
flattened.push(sel);
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
el.value.arg = SelectorList.create(flattened);
|
|
322
|
+
}
|
|
323
|
+
else {
|
|
324
|
+
// Single selector result - check if it's a generated :is() to unwrap
|
|
325
|
+
if (isNode(result, 'PseudoSelector') && result.value.name === ':is' && result.generated) {
|
|
326
|
+
// Unwrap - use the argument directly
|
|
327
|
+
el.value.arg = result.value.arg;
|
|
328
|
+
}
|
|
329
|
+
else {
|
|
330
|
+
el.value.arg = result;
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
push(el);
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
else if (isNode(el, 'SelectorList')) {
|
|
338
|
+
let processed = createProcessedSelector(el.value, true);
|
|
339
|
+
// Flatten any generated :is() wrappers in the SelectorList
|
|
340
|
+
const flattened = [];
|
|
341
|
+
for (const sel of processed) {
|
|
342
|
+
if (isNode(sel, 'PseudoSelector') && sel.value.name === ':is' && sel.generated) {
|
|
343
|
+
// Unwrap generated :is() - extract its argument selectors
|
|
344
|
+
const arg = sel.value.arg;
|
|
345
|
+
if (arg && isNode(arg, 'SelectorList')) {
|
|
346
|
+
flattened.push(...arg.value);
|
|
347
|
+
}
|
|
348
|
+
else if (arg) {
|
|
349
|
+
flattened.push(arg);
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
else {
|
|
353
|
+
flattened.push(sel);
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
// Preserve document order when merging multiple :is() from different extends (e.g. :is(.clearfix,.foo):after + :is(.clearfix,.bar):after → :is(.clearfix,.foo,.bar):after). Only sort when at least two items have document order so we don't reorder single :is() unwraps (e.g. .replace, .c).
|
|
357
|
+
if (extendOrderMap && flattened.length >= 2 && extendOrderByValueOf) {
|
|
358
|
+
const orderMap = extendOrderMap;
|
|
359
|
+
const orderByValue = extendOrderByValueOf;
|
|
360
|
+
const orderFor = (s) => {
|
|
361
|
+
const fromMap = orderMap.get(s);
|
|
362
|
+
if (fromMap !== undefined) {
|
|
363
|
+
return fromMap;
|
|
364
|
+
}
|
|
365
|
+
const key = String(typeof s.valueOf === 'function' ? s.valueOf() : '').trim();
|
|
366
|
+
let order = orderByValue.get(key);
|
|
367
|
+
if (order === undefined && key) {
|
|
368
|
+
const lastPart = key.split(/\s+/).pop();
|
|
369
|
+
if (lastPart) {
|
|
370
|
+
order = orderByValue.get(lastPart);
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
return order ?? 999999;
|
|
374
|
+
};
|
|
375
|
+
const withOrder = flattened.filter(s => orderFor(s) !== 999999);
|
|
376
|
+
if (withOrder.length >= 2) {
|
|
377
|
+
const NO_ORDER = 999999;
|
|
378
|
+
flattened.sort((a, b) => {
|
|
379
|
+
const oa = orderFor(a);
|
|
380
|
+
const ob = orderFor(b);
|
|
381
|
+
if (oa === NO_ORDER && ob === NO_ORDER) {
|
|
382
|
+
return 0;
|
|
383
|
+
}
|
|
384
|
+
if (oa === NO_ORDER) {
|
|
385
|
+
return -1;
|
|
386
|
+
}
|
|
387
|
+
if (ob === NO_ORDER) {
|
|
388
|
+
return 1;
|
|
389
|
+
}
|
|
390
|
+
return oa - ob;
|
|
391
|
+
});
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
el.value = flattened;
|
|
395
|
+
push(el);
|
|
396
|
+
}
|
|
397
|
+
else if (isNode(el, 'CompoundSelector')) {
|
|
398
|
+
// CRITICAL: Compound selectors can have duplicate components (e.g., .v.w.v)
|
|
399
|
+
// Process components with root=false to prevent deduplication
|
|
400
|
+
el.value = createProcessedSelector(el.value, false);
|
|
401
|
+
push(el);
|
|
402
|
+
}
|
|
403
|
+
else if (isNode(el, 'ComplexSelector')) {
|
|
404
|
+
let components = el.value;
|
|
405
|
+
let result = createProcessedSelector(components);
|
|
406
|
+
el.value = result;
|
|
407
|
+
let [first, second] = components;
|
|
408
|
+
/** Remove invisibility on combinator if it's a generated */
|
|
409
|
+
if (first?.type === 'Ampersand') {
|
|
410
|
+
/** Implicit ampersand was kept for nested output (don't resolve to parent selector here). */
|
|
411
|
+
if (first.hasFlag(F_IMPLICIT_AMPERSAND) && result[0] === first) {
|
|
412
|
+
el.value = result;
|
|
413
|
+
// Fall through; no throw, no slice
|
|
414
|
+
}
|
|
415
|
+
else if (isNode(result[0], 'Selector')) {
|
|
416
|
+
if (first.generated) {
|
|
417
|
+
result[1].removeFlag(F_VISIBLE);
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
else if (first.generated) {
|
|
421
|
+
/** Silent removal if generated and no selector was resolved */
|
|
422
|
+
if (second?.type === 'Combinator' && second.generated) {
|
|
423
|
+
el.value = result.slice(2);
|
|
424
|
+
}
|
|
425
|
+
else {
|
|
426
|
+
el.value = result.slice(1);
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
else {
|
|
430
|
+
throw new ExtendError(ExtendErrorType.AMPERSAND_BOUNDARY, 'Ampersand does not resolve to a selector');
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
// If a generated :is() ends up as the sole selector after a combinator in a complex selector,
|
|
434
|
+
// distribute it into a selector list. This avoids emitting `:is(...)` where a plain selector
|
|
435
|
+
// list is equivalent (and matches Less output expectations).
|
|
436
|
+
//
|
|
437
|
+
// Example:
|
|
438
|
+
// .attributes :is([data="test"], .attributes .attribute-test)
|
|
439
|
+
// becomes:
|
|
440
|
+
// .attributes [data="test"], .attributes .attribute-test
|
|
441
|
+
if (result.length >= 3) {
|
|
442
|
+
const maybeCombinator = result[result.length - 2];
|
|
443
|
+
const maybeIs = result[result.length - 1];
|
|
444
|
+
if (isNode(maybeCombinator, 'Combinator')
|
|
445
|
+
&& isNode(maybeIs, 'PseudoSelector')
|
|
446
|
+
&& maybeIs.value.name === ':is'
|
|
447
|
+
&& maybeIs.value.arg) {
|
|
448
|
+
// Only safe to flatten here when the combinator is the implicit (invisible) space
|
|
449
|
+
// from implicit `& ` nesting. In that case:
|
|
450
|
+
// & :is(.a, .b) === & .a, & .b
|
|
451
|
+
// and if `&` is also implicit/invisible, it further collapses naturally.
|
|
452
|
+
const prefix = result.slice(0, -2);
|
|
453
|
+
const first = prefix[0];
|
|
454
|
+
const originalFirst = components[0];
|
|
455
|
+
const originalSecond = components[1];
|
|
456
|
+
const canFlattenViaImplicitNesting =
|
|
457
|
+
// Either the processed prefix still begins with an ampersand...
|
|
458
|
+
(!!first
|
|
459
|
+
&& isNode(first, 'Ampersand')
|
|
460
|
+
&& (first.hasFlag(F_IMPLICIT_AMPERSAND) || first.generated)
|
|
461
|
+
&& !first.hasFlag(F_VISIBLE)
|
|
462
|
+
&& !maybeCombinator.hasFlag(F_VISIBLE))
|
|
463
|
+
// ...or the prefix is a generated `:is(...)` wrapper that came from implicit nesting
|
|
464
|
+
// materialization (e.g. when the parent selector is itself a selector list).
|
|
465
|
+
|| (!!first
|
|
466
|
+
&& isNode(first, 'PseudoSelector')
|
|
467
|
+
&& first.value.name === ':is'
|
|
468
|
+
&& first.generated === true
|
|
469
|
+
&& !maybeCombinator.hasFlag(F_VISIBLE))
|
|
470
|
+
// ...or we already resolved the invisible ampersand to a concrete selector in `result`,
|
|
471
|
+
// but the original components indicate this came from implicit `& ` nesting.
|
|
472
|
+
|| (!!originalFirst
|
|
473
|
+
&& isNode(originalFirst, 'Ampersand')
|
|
474
|
+
&& (originalFirst.hasFlag?.(F_IMPLICIT_AMPERSAND) || originalFirst.generated)
|
|
475
|
+
&& !originalFirst.hasFlag?.(F_VISIBLE)
|
|
476
|
+
&& !!originalSecond
|
|
477
|
+
&& originalSecond.type === 'Combinator'
|
|
478
|
+
&& !originalSecond.hasFlag?.(F_VISIBLE));
|
|
479
|
+
// Only flatten when we know this is the implicit `& ` nesting case.
|
|
480
|
+
// Do NOT flatten other combinators (e.g. `.ext6 > :is(...)`) — Less expects
|
|
481
|
+
// those to remain as :is() wrappers.
|
|
482
|
+
if (!canFlattenViaImplicitNesting) {
|
|
483
|
+
push(el);
|
|
484
|
+
continue;
|
|
485
|
+
}
|
|
486
|
+
const argSel = maybeIs.value.arg;
|
|
487
|
+
const argList = isNode(argSel, 'SelectorList') ? argSel.value : [argSel];
|
|
488
|
+
// If this came from implicit `& ` nesting (both ampersand and the space are invisible),
|
|
489
|
+
// then the prefix is already represented by the parent ruleset context and must not be
|
|
490
|
+
// duplicated in nested output. In that case we drop the prefix entirely.
|
|
491
|
+
const dropImplicitPrefix = !!originalFirst
|
|
492
|
+
&& isNode(originalFirst, 'Ampersand')
|
|
493
|
+
&& (originalFirst.hasFlag?.(F_IMPLICIT_AMPERSAND) || originalFirst.generated)
|
|
494
|
+
&& !originalFirst.hasFlag?.(F_VISIBLE)
|
|
495
|
+
&& !!originalSecond
|
|
496
|
+
&& originalSecond.type === 'Combinator'
|
|
497
|
+
&& !originalSecond.hasFlag?.(F_VISIBLE);
|
|
498
|
+
const dropImplicitPrefixViaGeneratedIs = !!first
|
|
499
|
+
&& isNode(first, 'PseudoSelector')
|
|
500
|
+
&& first.value.name === ':is'
|
|
501
|
+
&& first.generated === true
|
|
502
|
+
&& !maybeCombinator.hasFlag(F_VISIBLE);
|
|
503
|
+
const outputPrefix = (dropImplicitPrefix || dropImplicitPrefixViaGeneratedIs) ? [] : prefix;
|
|
504
|
+
// Visible vs invisible ampersand (with partial extends producing :is()):
|
|
505
|
+
// - Visible authored `&`: keep one ampersand in front of the whole list.
|
|
506
|
+
// - Invisible (implicit) `&`: copy invisible ampersand + combinator onto each selector list
|
|
507
|
+
// item so valueOf() is correct for extend matching (e.g. ".bb .bb", ".aa .dd").
|
|
508
|
+
const retainInvisibleAmpersandAndCombinator = dropImplicitPrefix && outputPrefix.length === 0 && !maybeCombinator.hasFlag(F_VISIBLE);
|
|
509
|
+
const isIndexInResult = result.length - 1;
|
|
510
|
+
const suffixAfterIs = retainInvisibleAmpersandAndCombinator
|
|
511
|
+
? components.slice(isIndexInResult + 1).map((c) => (c && typeof c.copy === 'function' ? c.copy(true) : c))
|
|
512
|
+
: [];
|
|
513
|
+
for (const inner of argList) {
|
|
514
|
+
let innerSel = inner;
|
|
515
|
+
// If the inner selector redundantly starts with the same prefix selector we already have,
|
|
516
|
+
// strip that duplicated prefix so we don't emit `.attributes .attributes ...`.
|
|
517
|
+
if (prefix.length >= 1 && isNode(innerSel, 'ComplexSelector')) {
|
|
518
|
+
const innerParts = innerSel.value;
|
|
519
|
+
const innerFirst = innerParts[0];
|
|
520
|
+
// Compare against the *resolved* prefix selector (result[0]) when present.
|
|
521
|
+
const resolvedPrefixFirst = result[0];
|
|
522
|
+
const prefixFirstValue = resolvedPrefixFirst?.valueOf?.();
|
|
523
|
+
if (innerFirst && prefixFirstValue && innerFirst.valueOf() === prefixFirstValue) {
|
|
524
|
+
// Drop the matching first selector and an optional following combinator.
|
|
525
|
+
const dropCount = innerParts[1]?.type === 'Combinator' ? 2 : 1;
|
|
526
|
+
innerSel = ComplexSelector.create(innerParts.slice(dropCount)).inherit(innerSel);
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
const omitCombinator = outputPrefix.length === 0 && !maybeCombinator.hasFlag(F_VISIBLE);
|
|
530
|
+
if (retainInvisibleAmpersandAndCombinator) {
|
|
531
|
+
// Copy invisible ampersand + combinator onto each item so selector list items have
|
|
532
|
+
// correct valueOf() for extend (e.g. .bb .bb, .aa .dd). Preserve selectorContainer when present so & stays live.
|
|
533
|
+
const origAmp = originalFirst;
|
|
534
|
+
const resolved = origAmp.getResolvedSelector();
|
|
535
|
+
const parentSel = resolved ?? undefined;
|
|
536
|
+
const origAmpValue = origAmp.value;
|
|
537
|
+
const amp = Ampersand.create(origAmpValue.selectorContainer
|
|
538
|
+
? { selectorContainer: origAmpValue.selectorContainer }
|
|
539
|
+
: parentSel ? { selectorContainer: { selector: parentSel.copy(true) } } : {});
|
|
540
|
+
amp.addFlag(F_IMPLICIT_AMPERSAND);
|
|
541
|
+
amp.removeFlag(F_VISIBLE);
|
|
542
|
+
const combCopy = maybeCombinator.copy(true);
|
|
543
|
+
combCopy.removeFlag(F_VISIBLE);
|
|
544
|
+
const parts = [amp, combCopy, innerSel.copy(), ...suffixAfterIs];
|
|
545
|
+
const next = ComplexSelector.create(parts).inherit(el);
|
|
546
|
+
push(next);
|
|
547
|
+
}
|
|
548
|
+
else if (outputPrefix.length === 0 && omitCombinator) {
|
|
549
|
+
// Prefix/combinator dropped but not implicit (e.g. first was :is()): emit inner as-is.
|
|
550
|
+
push(innerSel.copy().inherit(el));
|
|
551
|
+
}
|
|
552
|
+
else {
|
|
553
|
+
const parts = [...outputPrefix, maybeCombinator.copy(), innerSel.copy()];
|
|
554
|
+
const next = ComplexSelector.create(parts).inherit(el);
|
|
555
|
+
push(next);
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
continue;
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
push(el);
|
|
562
|
+
}
|
|
563
|
+
else if (isNode(el, 'Ampersand')) {
|
|
564
|
+
// Keep implicit ampersands as-is so nested output can omit the prefix (.dd not .aa .dd).
|
|
565
|
+
// Resolving & to the parent selector here would make the prefix visible; that should only
|
|
566
|
+
// happen when we hoist (e.g. in maybeHoistMixedNestingSelectorList).
|
|
567
|
+
if (el.hasFlag(F_IMPLICIT_AMPERSAND)) {
|
|
568
|
+
push(el);
|
|
569
|
+
}
|
|
570
|
+
else if (el.generated) {
|
|
571
|
+
const sel = el.getResolvedSelector();
|
|
572
|
+
if (sel && !isNode(sel, 'Nil')) {
|
|
573
|
+
push(createProcessedSelector(sel));
|
|
574
|
+
}
|
|
575
|
+
else {
|
|
576
|
+
push(el);
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
else {
|
|
580
|
+
push(el);
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
else {
|
|
584
|
+
push(el);
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
const result = out.length === 1 ? out[0] : out;
|
|
588
|
+
return result;
|
|
589
|
+
}
|
|
590
|
+
/**
|
|
591
|
+
* Extracts selectors from a :is() pseudo-selector, returning the argument selectors.
|
|
592
|
+
* If the selector is not a :is() selector, returns it as a single-item array.
|
|
593
|
+
*
|
|
594
|
+
* @param selector - The selector to extract from (may be :is() or any other selector)
|
|
595
|
+
* @returns Array of selectors extracted from :is() argument, or [selector] if not :is()
|
|
596
|
+
*/
|
|
597
|
+
function extractSelectorsFromIs(selector) {
|
|
598
|
+
if (isNode(selector, 'PseudoSelector') && selector.value.name === ':is') {
|
|
599
|
+
const arg = selector.value.arg;
|
|
600
|
+
if (arg && isNode(arg, 'SelectorList')) {
|
|
601
|
+
// Extract all selectors from the :is() argument
|
|
602
|
+
return arg.value;
|
|
603
|
+
}
|
|
604
|
+
else if (arg) {
|
|
605
|
+
// Single selector argument
|
|
606
|
+
return [arg];
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
// Not a :is() selector, return as-is
|
|
610
|
+
return [selector];
|
|
611
|
+
}
|
|
612
|
+
/**
|
|
613
|
+
* Helper function to create a SelectorList from an array of selectors,
|
|
614
|
+
* with deduplication and flattening of generated :is() wrappers applied.
|
|
615
|
+
* This is the standard pattern used throughout extend operations.
|
|
616
|
+
*
|
|
617
|
+
* If any selector in the array is a :is() selector, its argument selectors are extracted
|
|
618
|
+
* instead of nesting the :is() wrapper.
|
|
619
|
+
*
|
|
620
|
+
* @param selectors - Array of selectors to process
|
|
621
|
+
* @param inheritFrom - Optional selector to inherit from
|
|
622
|
+
* @returns A new SelectorList with deduplicated and flattened selectors
|
|
623
|
+
*/
|
|
624
|
+
function createExtendedSelectorList(selectors, inheritFrom) {
|
|
625
|
+
// Extract selectors from any :is() wrappers in the array
|
|
626
|
+
const extractedSelectors = [];
|
|
627
|
+
for (const selector of selectors) {
|
|
628
|
+
extractedSelectors.push(...extractSelectorsFromIs(selector));
|
|
629
|
+
}
|
|
630
|
+
if (extendOrderMap && extractedSelectors.length > 1) {
|
|
631
|
+
const orderMap = extendOrderMap;
|
|
632
|
+
const orderByValue = extendOrderByValueOf;
|
|
633
|
+
// Preserve ruleset-owner-first: when inheritFrom is the ruleset's selector (single-selector full match),
|
|
634
|
+
// keep it first so we get [.e, .d], [.z, .x, .y] etc. Otherwise extendOrderMap would sort all selectors
|
|
635
|
+
// by extend index and put .d before .e (wrong), because .e is also an extend source elsewhere.
|
|
636
|
+
const inheritVal = inheritFrom && typeof inheritFrom.valueOf === 'function' ? inheritFrom.valueOf() : undefined;
|
|
637
|
+
const ownerFirst = inheritVal !== undefined
|
|
638
|
+
&& extractedSelectors.some(s => (s.valueOf?.() ?? '') === inheritVal);
|
|
639
|
+
if (ownerFirst && inheritVal !== undefined) {
|
|
640
|
+
const first = extractedSelectors.find(s => (s.valueOf?.() ?? '') === inheritVal);
|
|
641
|
+
const rest = extractedSelectors.filter(s => (s.valueOf?.() ?? '') !== inheritVal);
|
|
642
|
+
// Wrap/append case: only preserve input order when we're truly appending one selector.
|
|
643
|
+
// When rest has 2+ items we must sort by document order (e.g. [.clearfix, .bar, .foo] → [.clearfix, .foo, .bar]).
|
|
644
|
+
const isAppendOne = rest.length === 1
|
|
645
|
+
&& selectors.length >= 2
|
|
646
|
+
&& (() => {
|
|
647
|
+
const lastInput = selectors[selectors.length - 1];
|
|
648
|
+
const fromLast = extractSelectorsFromIs(lastInput);
|
|
649
|
+
return fromLast.length === 1 && fromLast[0] === rest[rest.length - 1];
|
|
650
|
+
})();
|
|
651
|
+
let restSorted;
|
|
652
|
+
if (isAppendOne) {
|
|
653
|
+
restSorted = rest;
|
|
654
|
+
}
|
|
655
|
+
else {
|
|
656
|
+
const orderFor = (s, origIndex) => {
|
|
657
|
+
const fromMap = orderMap.get(s);
|
|
658
|
+
if (fromMap !== undefined) {
|
|
659
|
+
return fromMap;
|
|
660
|
+
}
|
|
661
|
+
const key = String(typeof s.valueOf === 'function' ? s.valueOf() : '').trim();
|
|
662
|
+
let order = orderByValue?.get(key);
|
|
663
|
+
if (order === undefined && key && orderByValue) {
|
|
664
|
+
const lastPart = key.split(/\s+/).pop();
|
|
665
|
+
if (lastPart) {
|
|
666
|
+
order = orderByValue.get(lastPart);
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
return order ?? 999999;
|
|
670
|
+
};
|
|
671
|
+
const NO_ORDER = 999999;
|
|
672
|
+
const mapped = rest.map((s, i) => ({ selector: s, order: orderFor(s, i), origIndex: i }));
|
|
673
|
+
restSorted = mapped
|
|
674
|
+
.sort((a, b) => {
|
|
675
|
+
if (a.order === NO_ORDER && b.order === NO_ORDER) {
|
|
676
|
+
return a.origIndex - b.origIndex;
|
|
677
|
+
}
|
|
678
|
+
if (a.order === NO_ORDER) {
|
|
679
|
+
return -1;
|
|
680
|
+
}
|
|
681
|
+
if (b.order === NO_ORDER) {
|
|
682
|
+
return 1;
|
|
683
|
+
}
|
|
684
|
+
return a.order - b.order || a.origIndex - b.origIndex;
|
|
685
|
+
})
|
|
686
|
+
.map(x => x.selector);
|
|
687
|
+
}
|
|
688
|
+
extractedSelectors.length = 0;
|
|
689
|
+
extractedSelectors.push(first, ...restSorted);
|
|
690
|
+
}
|
|
691
|
+
else {
|
|
692
|
+
// Only preserve input order when we're truly appending one selector (original + one new).
|
|
693
|
+
// When we have 3+ items we must sort by document order (e.g. [.clearfix, .bar, .foo] → [.clearfix, .foo, .bar]).
|
|
694
|
+
const isAppendOneElse = extractedSelectors.length === 2
|
|
695
|
+
&& selectors.length >= 2
|
|
696
|
+
&& (() => {
|
|
697
|
+
const lastInput = selectors[selectors.length - 1];
|
|
698
|
+
const fromLast = extractSelectorsFromIs(lastInput);
|
|
699
|
+
return fromLast.length === 1 && fromLast[0] === extractedSelectors[extractedSelectors.length - 1];
|
|
700
|
+
})();
|
|
701
|
+
if (isAppendOneElse) {
|
|
702
|
+
// Preserve input order (already doc order from wrap path)
|
|
703
|
+
}
|
|
704
|
+
else {
|
|
705
|
+
const withOrder = [];
|
|
706
|
+
const withoutOrder = [];
|
|
707
|
+
const orderForElse = (selector) => {
|
|
708
|
+
const fromWeak = orderMap.get(selector);
|
|
709
|
+
if (fromWeak !== undefined) {
|
|
710
|
+
return fromWeak;
|
|
711
|
+
}
|
|
712
|
+
const key = String(typeof selector.valueOf === 'function' ? selector.valueOf() : '').trim();
|
|
713
|
+
let order = orderByValue?.get(key);
|
|
714
|
+
if (order === undefined && key && orderByValue) {
|
|
715
|
+
const lastPart = key.split(/\s+/).pop();
|
|
716
|
+
if (lastPart) {
|
|
717
|
+
order = orderByValue.get(lastPart);
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
return order;
|
|
721
|
+
};
|
|
722
|
+
for (const selector of extractedSelectors) {
|
|
723
|
+
const order = orderForElse(selector);
|
|
724
|
+
if (order !== undefined) {
|
|
725
|
+
withOrder.push({ selector, order });
|
|
726
|
+
}
|
|
727
|
+
else {
|
|
728
|
+
withoutOrder.push(selector);
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
withOrder.sort((a, b) => a.order - b.order);
|
|
732
|
+
extractedSelectors.length = 0;
|
|
733
|
+
extractedSelectors.push(...withoutOrder, ...withOrder.map(item => item.selector));
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
// createProcessedSelector may return a single selector if only one item, so ensure it's an array
|
|
738
|
+
const processed = createProcessedSelector(extractedSelectors, true);
|
|
739
|
+
const processedArray = isArray(processed) ? processed : [processed];
|
|
740
|
+
// IMPORTANT: Avoid self-parenting cycles:
|
|
741
|
+
// If `inheritFrom` is also included as an item in the selector list, the constructor will adopt it,
|
|
742
|
+
// reparenting `inheritFrom` to the new SelectorList, and then `.inherit(inheritFrom)` will read
|
|
743
|
+
// `inheritFrom.parent` (now the new list) and set `result.parent` to itself.
|
|
744
|
+
// Always clone any element that is the same object as `inheritFrom`.
|
|
745
|
+
const safeArray = inheritFrom
|
|
746
|
+
? processedArray.map(s => (s === inheritFrom ? s.clone(true) : s))
|
|
747
|
+
: processedArray;
|
|
748
|
+
const result = SelectorList.create(safeArray);
|
|
749
|
+
return inheritFrom ? result.inherit(inheritFrom) : result;
|
|
750
|
+
}
|
|
751
|
+
/**
|
|
752
|
+
* Detects and handles boundary-crossing matches where a compound selector find
|
|
753
|
+
* matches across an :is() boundary in a compound selector target.
|
|
754
|
+
*
|
|
755
|
+
* Example: :is(.a, .b).c matching .b.c should flatten to .a.c, .b.c, .d.c
|
|
756
|
+
*
|
|
757
|
+
* However, if the match consumes the ENTIRE target selector (e.g., :is(.a, .b).c
|
|
758
|
+
* matching .a.c where .a matches inside :is() and .c matches after), we should
|
|
759
|
+
* NOT flatten but instead treat it as a root-level full match (selector list).
|
|
760
|
+
*
|
|
761
|
+
* @param target - The compound selector to extend
|
|
762
|
+
* @param find - The compound selector being matched (must have length > 1)
|
|
763
|
+
* @param extendWith - The selector to extend with
|
|
764
|
+
* @returns The flattened selector list if boundary-crossing detected, null otherwise
|
|
765
|
+
*/
|
|
766
|
+
function detectAndHandleBoundaryCrossing(target, find, extendWith) {
|
|
767
|
+
if (find.value.length <= 1) {
|
|
768
|
+
return null;
|
|
769
|
+
}
|
|
770
|
+
// Look for :is() components in the target
|
|
771
|
+
for (let i = 0; i < target.value.length; i++) {
|
|
772
|
+
const comp = target.value[i];
|
|
773
|
+
if (!isNode(comp, 'PseudoSelector') || comp.value.name !== ':is') {
|
|
774
|
+
continue;
|
|
775
|
+
}
|
|
776
|
+
const arg = comp.value.arg;
|
|
777
|
+
if (!arg || !arg.isSelector || !isNode(arg, 'SelectorList')) {
|
|
778
|
+
continue;
|
|
779
|
+
}
|
|
780
|
+
// Check if the first part of find matches inside the :is() and the rest matches after
|
|
781
|
+
const firstPart = find.value[0];
|
|
782
|
+
const restParts = find.value.slice(1);
|
|
783
|
+
if (!firstPart || restParts.length === 0 || i + 1 >= target.value.length) {
|
|
784
|
+
continue;
|
|
785
|
+
}
|
|
786
|
+
const firstPartComparison = selectorCompare(arg, firstPart);
|
|
787
|
+
const firstPartMatches = firstPartComparison.hasWholeMatch || firstPartComparison.hasPartialMatch;
|
|
788
|
+
if (!firstPartMatches) {
|
|
789
|
+
continue;
|
|
790
|
+
}
|
|
791
|
+
// Check if the rest matches the components after the :is()
|
|
792
|
+
const restCompound = restParts.length === 1
|
|
793
|
+
? restParts[0]
|
|
794
|
+
: CompoundSelector.create(restParts);
|
|
795
|
+
const afterIs = target.value.slice(i + 1);
|
|
796
|
+
const afterIsCompound = afterIs.length === 1
|
|
797
|
+
? afterIs[0]
|
|
798
|
+
: CompoundSelector.create(afterIs);
|
|
799
|
+
let restMatches = false;
|
|
800
|
+
const targetAfter = isNode(afterIsCompound, 'CompoundSelector') ? afterIsCompound : afterIs[0];
|
|
801
|
+
const restComparison = selectorCompare(targetAfter, restCompound);
|
|
802
|
+
restMatches = restComparison.hasWholeMatch || restComparison.hasPartialMatch;
|
|
803
|
+
if (restMatches) {
|
|
804
|
+
// We have a boundary-crossing match. Check if we've consumed the ENTIRE target selector.
|
|
805
|
+
// We've consumed the entire target if:
|
|
806
|
+
// 1. No components before :is() (we start at the beginning)
|
|
807
|
+
// 2. We matched one simple part inside :is() (one "or" option, not a compound)
|
|
808
|
+
// 3. We matched all parts after :is() (all "and" parts)
|
|
809
|
+
// 4. The total length matches (we've matched the entire structure)
|
|
810
|
+
//
|
|
811
|
+
// Note: Other options in :is() are "or" options and don't need to match.
|
|
812
|
+
// Only "and" parts (components after :is()) need to match.
|
|
813
|
+
//
|
|
814
|
+
// However, if the firstPart is a compound selector (not a simple selector), we should flatten
|
|
815
|
+
// because we can't preserve the :is() structure when matching compounds inside it.
|
|
816
|
+
const componentsBeforeIs = i; // Number of components before :is()
|
|
817
|
+
const componentsAfterIs = target.value.length - i - 1; // Number of components after :is()
|
|
818
|
+
const findPartsBeforeIs = 1; // We matched firstPart inside :is()
|
|
819
|
+
const findPartsAfterIs = restParts.length; // We matched restParts after :is()
|
|
820
|
+
// Check if firstPart is a simple selector (not a compound)
|
|
821
|
+
const firstPartIsSimple = !isNode(firstPart, 'CompoundSelector') && !isNode(firstPart, 'ComplexSelector');
|
|
822
|
+
// If we've matched exactly the structure of the target (one SIMPLE part in :is(), rest after),
|
|
823
|
+
// and the total length matches, we've consumed the entire target
|
|
824
|
+
// This means we matched all "and" parts (one SIMPLE option from :is() + all parts after)
|
|
825
|
+
if (componentsBeforeIs === 0 // No components before :is() (we start at the beginning)
|
|
826
|
+
&& findPartsBeforeIs === 1 // One part matched inside :is() (one "or" option)
|
|
827
|
+
&& firstPartIsSimple // The matched part is a simple selector (not a compound)
|
|
828
|
+
&& findPartsAfterIs === componentsAfterIs // Rest parts match components after :is() (all "and" parts)
|
|
829
|
+
&& find.value.length === target.value.length) { // Total length matches (entire structure)
|
|
830
|
+
// This is a full match of the entire target with a simple selector - don't flatten, let it be handled as root-level
|
|
831
|
+
// The result will be :is(.a, .b).c, .d (selector list) instead of .a.c, .b.c, .d.c (flattened)
|
|
832
|
+
return null;
|
|
833
|
+
}
|
|
834
|
+
// Otherwise, it's a boundary-crossing match that should be flattened
|
|
835
|
+
// This creates all combinations: each :is() option + parts after + extendWith + parts after
|
|
836
|
+
return createFlattenedBoundaryCrossingResult(arg, afterIs, extendWith, target);
|
|
837
|
+
}
|
|
838
|
+
}
|
|
839
|
+
return null;
|
|
840
|
+
}
|
|
841
|
+
/**
|
|
842
|
+
* Creates flattened selectors for a boundary-crossing match.
|
|
843
|
+
* Each alternative in the :is() is combined with components after it, plus the extension.
|
|
844
|
+
*
|
|
845
|
+
* @param isArg - The SelectorList argument of the :is() pseudo-selector
|
|
846
|
+
* @param afterIs - The components after the :is() in the compound selector
|
|
847
|
+
* @param extendWith - The selector to extend with
|
|
848
|
+
* @param inheritFrom - The selector to inherit from
|
|
849
|
+
* @returns A SelectorList with all flattened combinations
|
|
850
|
+
*/
|
|
851
|
+
function createFlattenedBoundaryCrossingResult(isArg, afterIs, extendWith, inheritFrom) {
|
|
852
|
+
const flattenedSelectors = [];
|
|
853
|
+
// For each alternative in :is(), create alt + components after :is()
|
|
854
|
+
for (const alt of isArg.value) {
|
|
855
|
+
const altWithRest = CompoundSelector.create([alt, ...afterIs]).inherit(inheritFrom);
|
|
856
|
+
flattenedSelectors.push(altWithRest);
|
|
857
|
+
}
|
|
858
|
+
// Also add extendWith + components after :is()
|
|
859
|
+
const extendWithRest = CompoundSelector.create([extendWith, ...afterIs]).inherit(inheritFrom);
|
|
860
|
+
flattenedSelectors.push(extendWithRest);
|
|
861
|
+
return createExtendedSelectorList(flattenedSelectors, inheritFrom);
|
|
862
|
+
}
|
|
863
|
+
// Removed unused functions: getIsSelectorArg, extendWithinIsArg
|
|
864
|
+
// These were only used by handleCompoundFullExtend which is also unused
|
|
865
|
+
// Removed unused functions: flattenGeneratedIs, flattenGeneratedIsInSelector
|
|
866
|
+
// All :is() flattening is now handled in createProcessedSelector in a single pass.
|
|
867
|
+
// This eliminates redundant traversals and consolidates all final processing.
|
|
868
|
+
/**
|
|
869
|
+
* Wrapper function that provides error information for extend operations.
|
|
870
|
+
* Returns a result object with the extended selector and optional error information.
|
|
871
|
+
*
|
|
872
|
+
* @param target - The selector to extend
|
|
873
|
+
* @param find - The target selector to find matches for
|
|
874
|
+
* @param extendWith - The selector to extend with
|
|
875
|
+
* @param partial - Whether to use partial matching (true) or full matching (false)
|
|
876
|
+
* @param skipAmpersandCheck - Whether to skip ampersand boundary checking (used in recursive calls)
|
|
877
|
+
* @returns ExtendResult with the extended selector and optional error information
|
|
878
|
+
*/
|
|
879
|
+
export function tryExtendSelector(target, find, extendWith, partial, skipAmpersandCheck = false) {
|
|
880
|
+
try {
|
|
881
|
+
const result = extendSelector(target, find, extendWith, partial, skipAmpersandCheck, false);
|
|
882
|
+
return createSuccessResult(result);
|
|
883
|
+
}
|
|
884
|
+
catch (error) {
|
|
885
|
+
if (error instanceof ExtendError) {
|
|
886
|
+
return createErrorResult(target, error);
|
|
887
|
+
}
|
|
888
|
+
// Re-throw unexpected errors
|
|
889
|
+
throw error;
|
|
890
|
+
}
|
|
891
|
+
}
|
|
892
|
+
/**
|
|
893
|
+
* Extends a selector by finding matches for a target selector and adding the extension.
|
|
894
|
+
* Throws ExtendError if the extension cannot be performed.
|
|
895
|
+
*
|
|
896
|
+
* @param target - The selector to extend
|
|
897
|
+
* @param find - The target selector to find matches for
|
|
898
|
+
* @param extendWith - The selector to extend with
|
|
899
|
+
* @param partial - Whether to use partial matching (true) or full matching (false)
|
|
900
|
+
* @param skipAmpersandCheck - Whether to skip ampersand boundary checking (used in recursive calls)
|
|
901
|
+
* @param hasMoreAfterIs - Internal
|
|
902
|
+
* @returns The extended selector
|
|
903
|
+
* @throws ExtendError if extension fails
|
|
904
|
+
*/
|
|
905
|
+
export function extendSelector(target, find, extendWith, partial, skipAmpersandCheck = false, hasMoreAfterIs = false) {
|
|
906
|
+
if (partial && find.valueOf() === extendWith.valueOf()) {
|
|
907
|
+
return target;
|
|
908
|
+
}
|
|
909
|
+
// Use the unified ExtendLocation API for all selector matching.
|
|
910
|
+
//
|
|
911
|
+
// IMPORTANT: normalize :is(...) equivalences for matching. In Less output we often materialize
|
|
912
|
+
// parent selector alternatives via `:is(...)`, and exact extends must match any single branch.
|
|
913
|
+
const originalTarget = target;
|
|
914
|
+
const originalFind = find;
|
|
915
|
+
let searchResult = findExtendableLocations(target, find);
|
|
916
|
+
if (!searchResult.hasMatches) {
|
|
917
|
+
const normalizedTarget = normalizeSelectorForExtend(target);
|
|
918
|
+
const normalizedFind = normalizeSelectorForExtend(find);
|
|
919
|
+
if (normalizedTarget.valueOf() !== target.valueOf() || normalizedFind.valueOf() !== find.valueOf()) {
|
|
920
|
+
const normalizedSearch = findExtendableLocations(normalizedTarget, normalizedFind);
|
|
921
|
+
if (normalizedSearch.hasMatches) {
|
|
922
|
+
target = normalizedTarget;
|
|
923
|
+
find = normalizedFind;
|
|
924
|
+
searchResult = normalizedSearch;
|
|
925
|
+
}
|
|
926
|
+
}
|
|
927
|
+
}
|
|
928
|
+
const comparison = selectorCompare(target, find, searchResult);
|
|
929
|
+
if (!searchResult.hasMatches) {
|
|
930
|
+
throw new ExtendError('NOT_FOUND', 'No match found for target selector', { target, find, extendWith });
|
|
931
|
+
}
|
|
932
|
+
// Check for ampersand boundary: "target only matches when ampersand is resolved" = match only
|
|
933
|
+
// within ampersand. One state: do not extend here; parent selector should carry the extend.
|
|
934
|
+
if (!skipAmpersandCheck) {
|
|
935
|
+
const ampersandCrossingInfo = checkAmpersandCrossingDuringExtension(originalTarget, originalFind);
|
|
936
|
+
if (ampersandCrossingInfo.crossed) {
|
|
937
|
+
const shouldSkipResolvedOnlySimpleBoundary = Boolean(!partial
|
|
938
|
+
&& ampersandCrossingInfo.reason === 'resolved-only'
|
|
939
|
+
&& isNode(originalFind, 'SimpleSelector'));
|
|
940
|
+
if (shouldSkipResolvedOnlySimpleBoundary) {
|
|
941
|
+
// Keep exact simple-selector extends on nested rules in normal flow.
|
|
942
|
+
// Forcing amp-boundary hoisting here flattens authored nesting unexpectedly.
|
|
943
|
+
}
|
|
944
|
+
else {
|
|
945
|
+
const hasWholeSelectorLocation = searchResult.locations.some((loc) => !loc?.isPartialMatch
|
|
946
|
+
&& Array.isArray(loc?.path)
|
|
947
|
+
&& loc.path.length === 0);
|
|
948
|
+
// If a partial extend only matches through a resolved ampersand boundary (no whole-selector hit),
|
|
949
|
+
// the current selector should not consume it; parent-level selector processing handles it.
|
|
950
|
+
if (partial && !hasWholeSelectorLocation) {
|
|
951
|
+
throw new ExtendError('NOT_FOUND', 'No match found for target selector', { target: originalTarget, find: originalFind, extendWith });
|
|
952
|
+
}
|
|
953
|
+
return handleAmpersandBoundaryCrossing(originalTarget, originalFind, extendWith, ampersandCrossingInfo.ampersandNode, searchResult);
|
|
954
|
+
}
|
|
955
|
+
}
|
|
956
|
+
}
|
|
957
|
+
// Special handling for SelectorList targets - extend each matching selector in the list
|
|
958
|
+
if (isNode(target, 'SelectorList')) {
|
|
959
|
+
return extendSelectorList(target, find, extendWith, partial, skipAmpersandCheck);
|
|
960
|
+
}
|
|
961
|
+
// Select the best location from search results
|
|
962
|
+
const location = selectBestLocation(searchResult, comparison, target, find, partial, hasMoreAfterIs, extendWith);
|
|
963
|
+
// If the match is entirely inside an ampersand node (e.g. `&:before` matching `.header .header-nav`),
|
|
964
|
+
// do NOT extend here. The parent selector/ruleset should carry the extension.
|
|
965
|
+
if (isNode(location.matchedNode, 'Ampersand')
|
|
966
|
+
&& location.parentNode
|
|
967
|
+
&& isNode(location.parentNode, 'CompoundSelector')
|
|
968
|
+
&& location.parentNode.value.length > 1) {
|
|
969
|
+
throw new ExtendError('NOT_FOUND', 'Match found only within ampersand; parent selector should carry the extend', { target, find, extendWith });
|
|
970
|
+
}
|
|
971
|
+
// Also handle the case where the matcher reports a partial match at the compound level:
|
|
972
|
+
// `&:before` is a compound; matching `.header .header-nav` against it should be treated as
|
|
973
|
+
// "within ampersand" rather than rewriting into a descendant combinator form.
|
|
974
|
+
if (location.isPartialMatch
|
|
975
|
+
&& isNode(location.matchedNode, 'CompoundSelector')
|
|
976
|
+
&& location.matchedNode.value.length > 1
|
|
977
|
+
&& isNode(location.matchedNode.value[0], 'Ampersand')) {
|
|
978
|
+
const firstResolved = location.matchedNode.value[0].getResolvedSelector();
|
|
979
|
+
if (firstResolved && firstResolved.valueOf() === find.valueOf()) {
|
|
980
|
+
throw new ExtendError('NOT_FOUND', 'Match found only within ampersand; parent selector should carry the extend', { target, find, extendWith });
|
|
981
|
+
}
|
|
982
|
+
}
|
|
983
|
+
// If we matched an ampersand *component* within a larger compound selector (e.g. `&:before`),
|
|
984
|
+
// do NOT extend that ampersand. The parent selector should have already been extended/hoisted.
|
|
985
|
+
if (isNode(target, 'CompoundSelector')
|
|
986
|
+
&& target.value.length > 1
|
|
987
|
+
&& location.path.length === 1
|
|
988
|
+
&& typeof location.path[0] === 'number') {
|
|
989
|
+
const idx = location.path[0];
|
|
990
|
+
const component = target.value[idx];
|
|
991
|
+
if (component && isNode(component, 'Ampersand') && component.getResolvedSelector()) {
|
|
992
|
+
throw new ExtendError('NOT_FOUND', 'Match found only within ampersand; parent selector should carry the extend', { target, find, extendWith });
|
|
993
|
+
}
|
|
994
|
+
}
|
|
995
|
+
// Handle partial vs full matching modes
|
|
996
|
+
if (partial) {
|
|
997
|
+
// PARTIAL MATCHING MODE: Create :is() wrappers for component-level matches
|
|
998
|
+
// If it's a root-level match in partial mode, handle remainders
|
|
999
|
+
if (location.path.length === 0) {
|
|
1000
|
+
// When find is a (contiguous or non-contiguous) subset of the compound, wrap matched part as :is(matched, extendWith).rest
|
|
1001
|
+
if (location.contiguousCompoundRange || (location.compoundMatchIndices?.length ?? 0) > 0) {
|
|
1002
|
+
return applyExtensionAtLocation(target, location, extendWith);
|
|
1003
|
+
}
|
|
1004
|
+
// §3a spans combinator: wrap the full matched segment as :is(segment, extendWith), keep before components
|
|
1005
|
+
if (location.complexMatchRange && isNode(target, 'ComplexSelector')) {
|
|
1006
|
+
const [start, end] = location.complexMatchRange;
|
|
1007
|
+
const segmentComponents = target.value.slice(start, end);
|
|
1008
|
+
const matchedSegment = segmentComponents.length === 1
|
|
1009
|
+
? segmentComponents[0]
|
|
1010
|
+
: ComplexSelector.create(segmentComponents).inherit(target);
|
|
1011
|
+
const wrapped = createValidatedIsWrapperWithErrors([matchedSegment, extendWith], matchedSegment, undefined, undefined);
|
|
1012
|
+
const before = target.value.slice(0, start);
|
|
1013
|
+
const newComponents = [...before, wrapped, ...target.value.slice(end)];
|
|
1014
|
+
return ComplexSelector.create(newComponents).inherit(target);
|
|
1015
|
+
}
|
|
1016
|
+
// Check if we have remainders that need to be combined with the extension
|
|
1017
|
+
if (location.isPartialMatch && location.remainders && location.remainders.length > 0) {
|
|
1018
|
+
const remainder = location.remainders[0];
|
|
1019
|
+
// Combine remainder with extension
|
|
1020
|
+
let combinedExtension;
|
|
1021
|
+
if (isNode(remainder, 'ComplexSelector') && remainder.value.length > 0) {
|
|
1022
|
+
// Remainder is complex selector - append extension
|
|
1023
|
+
const newComponents = [...remainder.value, extendWith];
|
|
1024
|
+
combinedExtension = ComplexSelector.create(newComponents).inherit(remainder);
|
|
1025
|
+
}
|
|
1026
|
+
else {
|
|
1027
|
+
// Simple remainder - create compound or complex as needed
|
|
1028
|
+
if (isNode(extendWith, 'ComplexSelector')) {
|
|
1029
|
+
const newComponents = [remainder, ...extendWith.value];
|
|
1030
|
+
combinedExtension = ComplexSelector.create(newComponents).inherit(extendWith);
|
|
1031
|
+
}
|
|
1032
|
+
else {
|
|
1033
|
+
combinedExtension = createValidatedCompoundSelectorWithErrors([remainder, extendWith], remainder, { target, find, extendWith });
|
|
1034
|
+
}
|
|
1035
|
+
}
|
|
1036
|
+
return createExtendedSelectorList([target, combinedExtension], target);
|
|
1037
|
+
}
|
|
1038
|
+
// Partial match that SPANS a combinator: per EXTEND_RULES.md §3a we should wrap the FULL segment
|
|
1039
|
+
// (first matched compound through last, including all in between). E.g. .a.b > .x in div + .a.c.b > .y.x
|
|
1040
|
+
// → div + :is(.a.c.b > .y.x, .q). The block below may implement a related case (remainder + extendWith as new list item).
|
|
1041
|
+
if (location.isPartialMatch && isNode(target, 'ComplexSelector') && isNode(find, 'ComplexSelector')) {
|
|
1042
|
+
// Try to detect if we have a case like .a>.b.c matching .a>.b
|
|
1043
|
+
const selectorComponents = target.value;
|
|
1044
|
+
const findComponents = find.value;
|
|
1045
|
+
// Check if target is a prefix of selector structure
|
|
1046
|
+
if (findComponents.length <= selectorComponents.length) {
|
|
1047
|
+
let foundCompoundRemainder = false;
|
|
1048
|
+
let compoundRemainder = null;
|
|
1049
|
+
// Check each component for partial compound matches
|
|
1050
|
+
for (let i = 0; i < findComponents.length; i++) {
|
|
1051
|
+
const sComp = selectorComponents[i];
|
|
1052
|
+
const tComp = findComponents[i];
|
|
1053
|
+
if (sComp && tComp && !isNode(sComp, 'Combinator') && !isNode(tComp, 'Combinator')) {
|
|
1054
|
+
// Check if find component partially matches selector component
|
|
1055
|
+
if (isNode(sComp, 'CompoundSelector') && isNode(tComp, 'SimpleSelector')) {
|
|
1056
|
+
const matchingElement = sComp.value.find(el => el.valueOf() === tComp.valueOf());
|
|
1057
|
+
if (matchingElement) {
|
|
1058
|
+
// Found partial match - extract remainder
|
|
1059
|
+
const remainderElements = sComp.value.filter(el => el.valueOf() !== tComp.valueOf());
|
|
1060
|
+
if (remainderElements.length > 0) {
|
|
1061
|
+
compoundRemainder = remainderElements.length === 1
|
|
1062
|
+
? remainderElements[0]
|
|
1063
|
+
: createValidatedCompoundSelectorWithErrors(remainderElements, sComp, { target, find, extendWith });
|
|
1064
|
+
foundCompoundRemainder = true;
|
|
1065
|
+
}
|
|
1066
|
+
}
|
|
1067
|
+
}
|
|
1068
|
+
}
|
|
1069
|
+
}
|
|
1070
|
+
if (foundCompoundRemainder && compoundRemainder) {
|
|
1071
|
+
// Create combined extension with remainder
|
|
1072
|
+
const combinedExtension = createValidatedCompoundSelectorWithErrors([compoundRemainder, extendWith], compoundRemainder, { target, find, extendWith });
|
|
1073
|
+
return createExtendedSelectorList([target, combinedExtension], target);
|
|
1074
|
+
}
|
|
1075
|
+
}
|
|
1076
|
+
}
|
|
1077
|
+
const rootFallback = createExtendedSelectorList([target, extendWith], target);
|
|
1078
|
+
return rootFallback;
|
|
1079
|
+
}
|
|
1080
|
+
// For deeper matches in partial mode, we need to analyze the context
|
|
1081
|
+
// If we're matching within a compound selector, create :is() wrapper
|
|
1082
|
+
if (location.path.length > 0) {
|
|
1083
|
+
// When partial: true, we may have multiple matching locations (e.g., .foo.foo has two .foo matches)
|
|
1084
|
+
// Process all matching locations, not just the first one
|
|
1085
|
+
// Handle multiple component matches in compound selectors (e.g., .foo.foo)
|
|
1086
|
+
if (isNode(target, 'CompoundSelector') && searchResult.locations.length > 1) {
|
|
1087
|
+
// Filter to only component-level matches (path length 1 with numeric index)
|
|
1088
|
+
const componentMatches = searchResult.locations.filter(loc => loc.path.length === 1
|
|
1089
|
+
&& typeof loc.path[0] === 'number');
|
|
1090
|
+
if (componentMatches.length > 1) {
|
|
1091
|
+
// Process all component matches - wrap each matching component in :is()
|
|
1092
|
+
const newComponents = [...target.value];
|
|
1093
|
+
const extendWithSelectors = extractSelectorsFromIs(extendWith);
|
|
1094
|
+
for (const matchLoc of componentMatches) {
|
|
1095
|
+
const componentIndex = matchLoc.path[0];
|
|
1096
|
+
const matchedComponent = newComponents[componentIndex];
|
|
1097
|
+
if (matchedComponent) {
|
|
1098
|
+
newComponents[componentIndex] = wrapMatchInIs(matchedComponent, matchedComponent, extendWith, target, { target, find, extendWith }, extendWithSelectors);
|
|
1099
|
+
}
|
|
1100
|
+
}
|
|
1101
|
+
return createValidatedCompoundSelectorWithErrors(newComponents, target);
|
|
1102
|
+
}
|
|
1103
|
+
}
|
|
1104
|
+
// Handle multiple component matches in complex selectors
|
|
1105
|
+
if (isNode(target, 'ComplexSelector') && searchResult.locations.length > 1) {
|
|
1106
|
+
// Only treat *component* matches as "multiple matches".
|
|
1107
|
+
// NOTE: locations inside pseudo-selector args (paths including 'arg') can include both:
|
|
1108
|
+
// - a direct match path like [i, 'arg', altIndex]
|
|
1109
|
+
// - an "append opportunity" path like [i, 'arg']
|
|
1110
|
+
// Those should NOT trigger the "multiple component matches" logic here.
|
|
1111
|
+
const componentMatches = searchResult.locations.filter((loc) => {
|
|
1112
|
+
if (loc.path.length !== 1 || typeof loc.path[0] !== 'number') {
|
|
1113
|
+
return false;
|
|
1114
|
+
}
|
|
1115
|
+
const component = target.value[loc.path[0]];
|
|
1116
|
+
return !!component && !isNode(component, 'Combinator');
|
|
1117
|
+
});
|
|
1118
|
+
const compoundInnerMatches = searchResult.locations.filter((loc) => {
|
|
1119
|
+
if (loc.path.length !== 2 || typeof loc.path[0] !== 'number' || typeof loc.path[1] !== 'number') {
|
|
1120
|
+
return false;
|
|
1121
|
+
}
|
|
1122
|
+
const component = target.value[loc.path[0]];
|
|
1123
|
+
return !!component && isNode(component, 'CompoundSelector');
|
|
1124
|
+
});
|
|
1125
|
+
// Matches inside pseudo-selector arguments (e.g., :is(...)) won't show up as
|
|
1126
|
+
// component/compoundInner matches above. In Less `all` mode we still need to
|
|
1127
|
+
// extend occurrences inside the arg (including duplicates like `.replace.replace`).
|
|
1128
|
+
const argMatches = searchResult.locations.filter((loc) => {
|
|
1129
|
+
if (!loc.path.includes('arg')) {
|
|
1130
|
+
return false;
|
|
1131
|
+
}
|
|
1132
|
+
// Ignore "append opportunity" locations which end in 'arg' (no concrete match),
|
|
1133
|
+
// and keep only actual matches within the argument.
|
|
1134
|
+
return typeof loc.path[loc.path.length - 1] === 'number';
|
|
1135
|
+
});
|
|
1136
|
+
const complexMatches = [...componentMatches, ...compoundInnerMatches];
|
|
1137
|
+
if (complexMatches.length > 1 || argMatches.length > 0) {
|
|
1138
|
+
const newComponents = [...target.value];
|
|
1139
|
+
const extendWithSelectors = extractSelectorsFromIs(extendWith);
|
|
1140
|
+
// Apply arg extensions per pseudo component (once per component index)
|
|
1141
|
+
if (argMatches.length > 0) {
|
|
1142
|
+
const indices = new Set();
|
|
1143
|
+
for (const loc of argMatches) {
|
|
1144
|
+
const argIndex = loc.path.indexOf('arg');
|
|
1145
|
+
const componentIndex = argIndex > 0 ? loc.path[argIndex - 1] : undefined;
|
|
1146
|
+
if (typeof componentIndex === 'number') {
|
|
1147
|
+
indices.add(componentIndex);
|
|
1148
|
+
}
|
|
1149
|
+
}
|
|
1150
|
+
for (const idx of indices) {
|
|
1151
|
+
const component = newComponents[idx];
|
|
1152
|
+
if (!component || !isNode(component, 'PseudoSelector')) {
|
|
1153
|
+
continue;
|
|
1154
|
+
}
|
|
1155
|
+
const arg = component.value.arg;
|
|
1156
|
+
if (!isSelectorNode(arg)) {
|
|
1157
|
+
continue;
|
|
1158
|
+
}
|
|
1159
|
+
// Extend the arg selector itself; this reuses existing SelectorList/Compound logic
|
|
1160
|
+
// (including "wrap all occurrences" for `.replace.replace`).
|
|
1161
|
+
const extendedArg = isNode(arg, 'SelectorList')
|
|
1162
|
+
? extendSelectorList(arg, find, extendWith, true, true, false)
|
|
1163
|
+
: extendSelector(arg, find, extendWith, true, true, false);
|
|
1164
|
+
if (component.generated) {
|
|
1165
|
+
component.value.arg = extendedArg;
|
|
1166
|
+
}
|
|
1167
|
+
else {
|
|
1168
|
+
newComponents[idx] = PseudoSelector.create({
|
|
1169
|
+
name: component.value.name,
|
|
1170
|
+
arg: extendedArg
|
|
1171
|
+
}).inherit(component);
|
|
1172
|
+
}
|
|
1173
|
+
}
|
|
1174
|
+
}
|
|
1175
|
+
for (const matchLoc of complexMatches) {
|
|
1176
|
+
const componentIndex = matchLoc.path[0];
|
|
1177
|
+
const component = newComponents[componentIndex];
|
|
1178
|
+
if (!component || isNode(component, 'Combinator')) {
|
|
1179
|
+
continue;
|
|
1180
|
+
}
|
|
1181
|
+
// Match is the entire complex component
|
|
1182
|
+
if (matchLoc.path.length === 1) {
|
|
1183
|
+
newComponents[componentIndex] = wrapMatchInIs(component, component, extendWith, target, { target, find, extendWith }, extendWithSelectors);
|
|
1184
|
+
continue;
|
|
1185
|
+
}
|
|
1186
|
+
// Match is inside a compound component: [componentIndex, compoundChildIndex]
|
|
1187
|
+
if (matchLoc.path.length === 2 && typeof matchLoc.path[1] === 'number' && isNode(component, 'CompoundSelector')) {
|
|
1188
|
+
const childIndex = matchLoc.path[1];
|
|
1189
|
+
const compoundComponents = [...component.value];
|
|
1190
|
+
const matchedChild = compoundComponents[childIndex];
|
|
1191
|
+
if (matchedChild) {
|
|
1192
|
+
compoundComponents[childIndex] = wrapMatchInIs(matchedChild, matchedChild, extendWith, component, { target, find, extendWith }, extendWithSelectors);
|
|
1193
|
+
newComponents[componentIndex] = createValidatedCompoundSelectorWithErrors(compoundComponents, component, { target, find, extendWith });
|
|
1194
|
+
}
|
|
1195
|
+
continue;
|
|
1196
|
+
}
|
|
1197
|
+
}
|
|
1198
|
+
return ComplexSelector.create(newComponents).inherit(target);
|
|
1199
|
+
}
|
|
1200
|
+
}
|
|
1201
|
+
const partialResult = handlePartialModeExtension(target, location, extendWith);
|
|
1202
|
+
return partialResult;
|
|
1203
|
+
}
|
|
1204
|
+
return applyExtensionAtLocation(target, location, extendWith);
|
|
1205
|
+
}
|
|
1206
|
+
else {
|
|
1207
|
+
// FULL MATCHING MODE: Create selector lists for complete matches
|
|
1208
|
+
// When partial: false, reject ALL partial matches - unified check before any special-casing.
|
|
1209
|
+
// This applies regardless of context (root, SelectorList, :is(), compound, complex, etc.)
|
|
1210
|
+
if (!partial && location.isPartialMatch) {
|
|
1211
|
+
return target;
|
|
1212
|
+
}
|
|
1213
|
+
// Less semantics: without `all`, `:extend(.x)` should only apply when `.x` is a complete selector
|
|
1214
|
+
// match (i.e. the entire selector / selector-list item), not when `.x` appears as a component
|
|
1215
|
+
// inside a larger selector like `.a .b .c`.
|
|
1216
|
+
//
|
|
1217
|
+
// Runtime evidence: in `extend-exact.less`, `.effected { &:extend(.a); ... }` should NOT affect
|
|
1218
|
+
// `.a .b .c`, but it currently does because the matcher can report a non-partial location for a
|
|
1219
|
+
// component match.
|
|
1220
|
+
if (!partial && isNode(find, 'SimpleSelector')) {
|
|
1221
|
+
const findV = find.valueOf();
|
|
1222
|
+
const wholeSelectorItemMatch = isNonAllWholeSelectorItemMatch(originalTarget, findV);
|
|
1223
|
+
if (!wholeSelectorItemMatch) {
|
|
1224
|
+
return target;
|
|
1225
|
+
}
|
|
1226
|
+
}
|
|
1227
|
+
// Check for boundary-crossing matches in compound selectors FIRST
|
|
1228
|
+
// This handles cases like :is(.a, .b).c matching .b.c where the match crosses the :is() boundary
|
|
1229
|
+
// This must be checked before handleFullExtend because it requires special flattening logic
|
|
1230
|
+
if (isNode(target, 'CompoundSelector') && isNode(find, 'CompoundSelector')) {
|
|
1231
|
+
const boundaryResult = detectAndHandleBoundaryCrossing(target, find, extendWith);
|
|
1232
|
+
if (boundaryResult) {
|
|
1233
|
+
return boundaryResult;
|
|
1234
|
+
}
|
|
1235
|
+
}
|
|
1236
|
+
// Special handling for pseudo-selector matches in full mode
|
|
1237
|
+
// All pseudo-selectors with selector arguments allow extending inside
|
|
1238
|
+
// This includes :is(), :where(), :not(), :has(), and any other pseudo-selector with selector args
|
|
1239
|
+
if (location.path.includes('arg')) {
|
|
1240
|
+
// (Partial matches are already handled by the unified check above - no need to check again)
|
|
1241
|
+
// But double-check: if the path indicates a match deep inside (e.g., ['arg', index, subIndex]),
|
|
1242
|
+
// and that match is partial, we should have already returned above. If we reach here,
|
|
1243
|
+
// it means either it's a full match OR the isPartialMatch flag wasn't set correctly.
|
|
1244
|
+
// For safety, if the path has more than just 'arg' (meaning we're matching inside a selector
|
|
1245
|
+
// within the :is() argument), check if it's a partial match by examining the matched node.
|
|
1246
|
+
// Double-check for partial matches: if path indicates component match within compound
|
|
1247
|
+
// (e.g., ['arg', index, subIndex] where both index and subIndex are numbers)
|
|
1248
|
+
if (location.path.length >= 3) {
|
|
1249
|
+
const pathLastNum = location.path[location.path.length - 1];
|
|
1250
|
+
const pathSecondLast = location.path[location.path.length - 2];
|
|
1251
|
+
// Path like ['arg', index, subIndex] indicates component match within compound selector
|
|
1252
|
+
if (typeof pathLastNum === 'number' && typeof pathSecondLast === 'number') {
|
|
1253
|
+
const matchedNode = location.matchedNode;
|
|
1254
|
+
// If matching a SimpleSelector within a compound, it's a partial match
|
|
1255
|
+
if (matchedNode && isNode(matchedNode, 'SimpleSelector') && isNode(find, 'SimpleSelector')) {
|
|
1256
|
+
if (matchedNode.valueOf() === find.valueOf()) {
|
|
1257
|
+
// Component match within compound - treat as partial
|
|
1258
|
+
return target;
|
|
1259
|
+
}
|
|
1260
|
+
}
|
|
1261
|
+
}
|
|
1262
|
+
}
|
|
1263
|
+
// Check if this is a compound target that fully matches a compound selector
|
|
1264
|
+
// In this case, create a selector list instead of extending inside the pseudo-selector
|
|
1265
|
+
if (isNode(find, 'CompoundSelector') && isNode(target, 'CompoundSelector')) {
|
|
1266
|
+
// This is a full compound match - create selector list
|
|
1267
|
+
return createExtendedSelectorList([target, extendWith], target);
|
|
1268
|
+
}
|
|
1269
|
+
// When partial: false and we're matching inside a pseudo-selector (path includes 'arg'),
|
|
1270
|
+
// check if there are ANY components outside the :is() (before or after).
|
|
1271
|
+
// If so, this is a partial match of the entire selector and should be rejected.
|
|
1272
|
+
// Examples:
|
|
1273
|
+
// - d :is(.b .c) matching .b .c with partial: false → rejected (d is before)
|
|
1274
|
+
// - :is(.i).j matching .i with partial: false → rejected (.j is after)
|
|
1275
|
+
// - :is(.i) matching .i with partial: false → allowed (no components outside)
|
|
1276
|
+
// Note: We return target unchanged (not throw) to match the behavior of other partial match rejections
|
|
1277
|
+
// The chaining logic should check if the selector changed before processing chained extends
|
|
1278
|
+
if (!partial) {
|
|
1279
|
+
const argIndex = location.path.indexOf('arg');
|
|
1280
|
+
if (argIndex > 0) {
|
|
1281
|
+
// We're matching inside a pseudo-selector - find the component index
|
|
1282
|
+
const componentIndex = location.path[argIndex - 1];
|
|
1283
|
+
if (typeof componentIndex === 'number') {
|
|
1284
|
+
// Check for components before the :is() in ComplexSelector
|
|
1285
|
+
if (isNode(target, 'ComplexSelector') && componentIndex > 0) {
|
|
1286
|
+
// There are components before the :is() - this is a partial match
|
|
1287
|
+
// Return unchanged - chaining logic should skip if selector didn't change
|
|
1288
|
+
return target;
|
|
1289
|
+
}
|
|
1290
|
+
// Check for components before or after the :is() in CompoundSelector
|
|
1291
|
+
if (isNode(target, 'CompoundSelector')) {
|
|
1292
|
+
const hasComponentsBefore = componentIndex > 0;
|
|
1293
|
+
const hasComponentsAfter = componentIndex < target.value.length - 1;
|
|
1294
|
+
if (hasComponentsBefore || hasComponentsAfter) {
|
|
1295
|
+
// There are components outside the :is() - this is a partial match
|
|
1296
|
+
// Return unchanged - chaining logic should skip if selector didn't change
|
|
1297
|
+
return target;
|
|
1298
|
+
}
|
|
1299
|
+
}
|
|
1300
|
+
}
|
|
1301
|
+
}
|
|
1302
|
+
}
|
|
1303
|
+
// This is a full match inside a pseudo-selector argument
|
|
1304
|
+
// Always extend inside pseudo-selectors with selector arguments
|
|
1305
|
+
const applied = applyExtensionAtLocation(target, location, extendWith);
|
|
1306
|
+
return applied;
|
|
1307
|
+
}
|
|
1308
|
+
// Special handling for full matches at the first component of complex selectors
|
|
1309
|
+
// Component matches in complex selectors create :is() wrappers (not selector lists)
|
|
1310
|
+
// Example: .aa .dd extended with .cc (where .cc:extend(.aa !all)) should produce :is(.aa, .cc) .dd
|
|
1311
|
+
// (Partial matches are already handled by the unified check above)
|
|
1312
|
+
if (location.path.length === 1 && isNode(target, 'ComplexSelector') && location.path[0] === 0) {
|
|
1313
|
+
// This is a component match in a complex selector - create :is() wrapper
|
|
1314
|
+
// REASON: Anything that's "part of" a selector gets wrapped in :is()
|
|
1315
|
+
const componentIndex = location.path[0];
|
|
1316
|
+
const matchedComponent = target.value[componentIndex];
|
|
1317
|
+
if (matchedComponent && !isNode(matchedComponent, 'Combinator')) {
|
|
1318
|
+
// Replace the matched component with :is(original, extension)
|
|
1319
|
+
const newComponents = [...target.value];
|
|
1320
|
+
// If extendWith is a :is() selector, extract its selectors to avoid nesting
|
|
1321
|
+
const extendWithSelectors = extractSelectorsFromIs(extendWith);
|
|
1322
|
+
const isWrapper = createValidatedIsWrapperWithErrors([matchedComponent, ...extendWithSelectors], matchedComponent, target, { target, find, extendWith });
|
|
1323
|
+
newComponents[componentIndex] = isWrapper;
|
|
1324
|
+
return ComplexSelector.create(newComponents).inherit(target);
|
|
1325
|
+
}
|
|
1326
|
+
}
|
|
1327
|
+
// For full matches within compound selectors, create :is() wrapper
|
|
1328
|
+
// (Partial matches are already handled by the unified check above)
|
|
1329
|
+
if (location.path.length === 1 && isNode(target, 'CompoundSelector')) {
|
|
1330
|
+
// Check if we have multiple matching locations (e.g., .foo.foo has two .foo matches)
|
|
1331
|
+
// Process all matching locations, not just the first one
|
|
1332
|
+
if (searchResult.locations.length > 1) {
|
|
1333
|
+
// Filter to only component-level matches (path length 1 with numeric index)
|
|
1334
|
+
const componentMatches = searchResult.locations.filter(loc => loc.path.length === 1
|
|
1335
|
+
&& typeof loc.path[0] === 'number'
|
|
1336
|
+
&& !loc.isPartialMatch);
|
|
1337
|
+
if (componentMatches.length > 1) {
|
|
1338
|
+
// Process all component matches - wrap each matching component in :is()
|
|
1339
|
+
const newComponents = [...target.value];
|
|
1340
|
+
for (const matchLoc of componentMatches) {
|
|
1341
|
+
const componentIndex = matchLoc.path[0];
|
|
1342
|
+
const matchedComponent = newComponents[componentIndex];
|
|
1343
|
+
if (matchedComponent) {
|
|
1344
|
+
// Wrap this component in :is(original, extension)
|
|
1345
|
+
newComponents[componentIndex] = createValidatedIsWrapperWithErrors([matchedComponent, extendWith], matchedComponent, target, { target, find, extendWith });
|
|
1346
|
+
}
|
|
1347
|
+
}
|
|
1348
|
+
return createValidatedCompoundSelectorWithErrors(newComponents, target, { target, find, extendWith });
|
|
1349
|
+
}
|
|
1350
|
+
}
|
|
1351
|
+
// Single match case
|
|
1352
|
+
const componentIndex = location.path[0];
|
|
1353
|
+
const matchedComponent = target.value[componentIndex];
|
|
1354
|
+
if (matchedComponent && target.value.length > 1) {
|
|
1355
|
+
// Replace the matched component with :is(original, extension)
|
|
1356
|
+
const newComponents = [...target.value];
|
|
1357
|
+
// If extendWith is a :is() selector, extract its selectors to avoid nesting
|
|
1358
|
+
const extendWithSelectors = extractSelectorsFromIs(extendWith);
|
|
1359
|
+
const isWrapper = createValidatedIsWrapperWithErrors([matchedComponent, ...extendWithSelectors], matchedComponent, target, { target, find, extendWith });
|
|
1360
|
+
newComponents[componentIndex] = isWrapper;
|
|
1361
|
+
const result = createValidatedCompoundSelectorWithErrors(newComponents, target, { target, find, extendWith });
|
|
1362
|
+
return result;
|
|
1363
|
+
}
|
|
1364
|
+
}
|
|
1365
|
+
// Use handleFullExtend for root-level matches and default cases
|
|
1366
|
+
// This consolidates logic for SelectorList, PseudoSelector, and CompoundSelector handling
|
|
1367
|
+
// and includes performance optimizations for generated selectors
|
|
1368
|
+
return handleFullExtend(target, find, extendWith, location);
|
|
1369
|
+
}
|
|
1370
|
+
}
|
|
1371
|
+
/**
|
|
1372
|
+
* Extends a SelectorList by extending each matching selector in the list
|
|
1373
|
+
* @param target - The SelectorList to extend
|
|
1374
|
+
* @param find - The selector to find
|
|
1375
|
+
* @param extendWith - The selector to extend with
|
|
1376
|
+
* @param partial - Whether to use partial matching
|
|
1377
|
+
* @param skipAmpersandCheck - Whether to skip ampersand boundary checking
|
|
1378
|
+
* @returns Extended SelectorList
|
|
1379
|
+
*/
|
|
1380
|
+
function extendSelectorList(target, find, extendWith, partial, skipAmpersandCheck, preferIsWrapperInPartialMode = false) {
|
|
1381
|
+
const markExtended = (selector) => {
|
|
1382
|
+
selector.addFlag(F_EXTENDED);
|
|
1383
|
+
return selector;
|
|
1384
|
+
};
|
|
1385
|
+
const markExtendTarget = (selector) => {
|
|
1386
|
+
if (partial && find.valueOf() !== extendWith.valueOf()) {
|
|
1387
|
+
selector.addFlag(F_EXTEND_TARGET);
|
|
1388
|
+
}
|
|
1389
|
+
return selector;
|
|
1390
|
+
};
|
|
1391
|
+
const keepOriginalInReference = (_selector) => !partial || (partial && find.valueOf() === extendWith.valueOf());
|
|
1392
|
+
const maybePrefixNewSelectorWithImplicitParent = (template, s) => {
|
|
1393
|
+
// If we're extending inside a nested selector that already starts with an implicit `&`,
|
|
1394
|
+
// ensure any newly-added selector alternatives also start with the same implicit `&`.
|
|
1395
|
+
//
|
|
1396
|
+
// Without this, we can create a "mixed" selector list under a SelectorList parent:
|
|
1397
|
+
// - `& .replace` (relative via implicit parent)
|
|
1398
|
+
// - `.rep_ace` (absolute)
|
|
1399
|
+
//
|
|
1400
|
+
// That triggers `maybeHoistMixedNestingSelectorList()` and produces the unwanted
|
|
1401
|
+
// `:is(:is(...), ...) .rep_ace` distribution.
|
|
1402
|
+
if (!partial) {
|
|
1403
|
+
return s;
|
|
1404
|
+
}
|
|
1405
|
+
if (!isNode(template, 'ComplexSelector')) {
|
|
1406
|
+
return s;
|
|
1407
|
+
}
|
|
1408
|
+
const t = template;
|
|
1409
|
+
const first = t.value[0];
|
|
1410
|
+
const second = t.value[1];
|
|
1411
|
+
if (!(first instanceof Ampersand) || !first.hasFlag(F_IMPLICIT_AMPERSAND)) {
|
|
1412
|
+
return s;
|
|
1413
|
+
}
|
|
1414
|
+
// If the selector already starts with an implicit `&`, keep it.
|
|
1415
|
+
if (isNode(s, 'ComplexSelector')) {
|
|
1416
|
+
const sf = s.value[0];
|
|
1417
|
+
if (sf instanceof Ampersand && sf.hasFlag(F_IMPLICIT_AMPERSAND)) {
|
|
1418
|
+
return s;
|
|
1419
|
+
}
|
|
1420
|
+
}
|
|
1421
|
+
// Prefix with the same implicit `&` + combinator shape from the template.
|
|
1422
|
+
const prefixed = ComplexSelector.create([
|
|
1423
|
+
first.copy(true),
|
|
1424
|
+
isNode(second, 'Combinator') ? second.copy(true) : Combinator.create(' ').inherit(second),
|
|
1425
|
+
s.copy(true)
|
|
1426
|
+
]).inherit(s);
|
|
1427
|
+
return prefixed;
|
|
1428
|
+
};
|
|
1429
|
+
// For SelectorLists, extend each selector that contains the find target.
|
|
1430
|
+
// Build list as [original selectors..., new selectors...] so .replace, .c + extend → .replace, .c, .rep_ace.
|
|
1431
|
+
const orderedSelectors = [];
|
|
1432
|
+
const orderedMatchFlags = [];
|
|
1433
|
+
const newSelectors = [];
|
|
1434
|
+
for (const selector of target.value) {
|
|
1435
|
+
const comparison = selectorCompare(selector, find);
|
|
1436
|
+
if (!comparison.locations.length || (!comparison.hasWholeMatch && !comparison.hasPartialMatch)) {
|
|
1437
|
+
orderedSelectors.push(selector);
|
|
1438
|
+
orderedMatchFlags.push(comparison.hasWholeMatch || comparison.hasPartialMatch);
|
|
1439
|
+
continue;
|
|
1440
|
+
}
|
|
1441
|
+
const extended = extendSelector(selector, find, extendWith, partial, skipAmpersandCheck, false);
|
|
1442
|
+
let appendedVariant = false;
|
|
1443
|
+
if (extended === selector) {
|
|
1444
|
+
orderedSelectors.push(keepOriginalInReference(selector)
|
|
1445
|
+
? markExtended(selector.clone(true))
|
|
1446
|
+
: markExtendTarget(selector.clone(true)));
|
|
1447
|
+
orderedMatchFlags.push(comparison.hasWholeMatch || comparison.hasPartialMatch);
|
|
1448
|
+
if (comparison.hasWholeMatch && extendWith.valueOf() !== selector.valueOf()) {
|
|
1449
|
+
newSelectors.push(markExtended(maybePrefixNewSelectorWithImplicitParent(selector, extendWith.clone(true))));
|
|
1450
|
+
}
|
|
1451
|
+
continue;
|
|
1452
|
+
}
|
|
1453
|
+
if (isNode(extended, 'SelectorList')) {
|
|
1454
|
+
if (partial
|
|
1455
|
+
&& preferIsWrapperInPartialMode
|
|
1456
|
+
&& extended.value.length === 2
|
|
1457
|
+
&& extended.value[0].valueOf() === selector.valueOf()
|
|
1458
|
+
&& extended.value[1].valueOf() === extendWith.valueOf()) {
|
|
1459
|
+
const extendWithSelectors = extractSelectorsFromIs(extendWith);
|
|
1460
|
+
const isWrapper = createValidatedIsWrapperWithErrors([selector, ...extendWithSelectors], selector, target, { target: selector, find, extendWith });
|
|
1461
|
+
isWrapper.generated = true;
|
|
1462
|
+
orderedSelectors.push(markExtended(isWrapper));
|
|
1463
|
+
orderedMatchFlags.push(comparison.hasWholeMatch || comparison.hasPartialMatch);
|
|
1464
|
+
continue;
|
|
1465
|
+
}
|
|
1466
|
+
if (extended.value.length === 0) {
|
|
1467
|
+
orderedSelectors.push(keepOriginalInReference(selector)
|
|
1468
|
+
? markExtended(selector.clone(true))
|
|
1469
|
+
: markExtendTarget(selector.clone(true)));
|
|
1470
|
+
orderedMatchFlags.push(comparison.hasWholeMatch || comparison.hasPartialMatch);
|
|
1471
|
+
}
|
|
1472
|
+
else if (extended.value.length === 1 && extended.value[0].valueOf() === extendWith.valueOf()) {
|
|
1473
|
+
orderedSelectors.push(keepOriginalInReference(selector)
|
|
1474
|
+
? markExtended(selector.clone(true))
|
|
1475
|
+
: markExtendTarget(selector.clone(true)));
|
|
1476
|
+
orderedMatchFlags.push(comparison.hasWholeMatch || comparison.hasPartialMatch);
|
|
1477
|
+
if (extendWith.valueOf() !== selector.valueOf()) {
|
|
1478
|
+
newSelectors.push(markExtended(maybePrefixNewSelectorWithImplicitParent(selector, extendWith.clone(true))));
|
|
1479
|
+
}
|
|
1480
|
+
appendedVariant = true;
|
|
1481
|
+
}
|
|
1482
|
+
else {
|
|
1483
|
+
const first = extended.value[0].clone(true);
|
|
1484
|
+
orderedSelectors.push(keepOriginalInReference(selector) ? markExtended(first) : markExtendTarget(first));
|
|
1485
|
+
orderedMatchFlags.push(comparison.hasWholeMatch || comparison.hasPartialMatch);
|
|
1486
|
+
const template = extended.value[0] ?? selector;
|
|
1487
|
+
newSelectors.push(...extended.value
|
|
1488
|
+
.slice(1)
|
|
1489
|
+
.map(s => markExtended(maybePrefixNewSelectorWithImplicitParent(template, s)))
|
|
1490
|
+
.map(s => s.clone(true)));
|
|
1491
|
+
appendedVariant = true;
|
|
1492
|
+
}
|
|
1493
|
+
}
|
|
1494
|
+
else {
|
|
1495
|
+
let fullMatchOfListItem = selector.valueOf() === find.valueOf() && extended.valueOf() === extendWith.valueOf();
|
|
1496
|
+
if (!fullMatchOfListItem && isNode(selector, 'ComplexSelector')) {
|
|
1497
|
+
const cs = selector;
|
|
1498
|
+
const val = cs.value;
|
|
1499
|
+
if (val.length >= 3 && val[0] instanceof Ampersand && val[0].hasFlag(F_IMPLICIT_AMPERSAND)) {
|
|
1500
|
+
const ownPart = val[2];
|
|
1501
|
+
const ownVal = ownPart && typeof ownPart.valueOf === 'function' ? ownPart.valueOf() : '';
|
|
1502
|
+
if (ownVal === find.valueOf()) {
|
|
1503
|
+
if (extended.valueOf() === extendWith.valueOf()) {
|
|
1504
|
+
fullMatchOfListItem = true;
|
|
1505
|
+
}
|
|
1506
|
+
else if (isNode(extended, 'PseudoSelector') && extended.value.name === ':is') {
|
|
1507
|
+
const isArgs = extractSelectorsFromIs(extended);
|
|
1508
|
+
const hasFind = isArgs.some((s) => s.valueOf() === find.valueOf());
|
|
1509
|
+
const hasExtendWith = isArgs.some((s) => s.valueOf() === extendWith.valueOf());
|
|
1510
|
+
if (hasFind && hasExtendWith) {
|
|
1511
|
+
fullMatchOfListItem = true;
|
|
1512
|
+
}
|
|
1513
|
+
}
|
|
1514
|
+
}
|
|
1515
|
+
}
|
|
1516
|
+
}
|
|
1517
|
+
if (fullMatchOfListItem) {
|
|
1518
|
+
orderedSelectors.push(keepOriginalInReference(selector)
|
|
1519
|
+
? markExtended(selector.clone(true))
|
|
1520
|
+
: markExtendTarget(selector.clone(true)));
|
|
1521
|
+
orderedMatchFlags.push(comparison.hasWholeMatch || comparison.hasPartialMatch);
|
|
1522
|
+
if (extendWith.valueOf() !== selector.valueOf()) {
|
|
1523
|
+
newSelectors.push(markExtended(maybePrefixNewSelectorWithImplicitParent(selector, extendWith.clone(true))));
|
|
1524
|
+
}
|
|
1525
|
+
appendedVariant = true;
|
|
1526
|
+
}
|
|
1527
|
+
else {
|
|
1528
|
+
orderedSelectors.push(markExtended(extended.clone(true)));
|
|
1529
|
+
orderedMatchFlags.push(comparison.hasWholeMatch || comparison.hasPartialMatch);
|
|
1530
|
+
appendedVariant = true;
|
|
1531
|
+
}
|
|
1532
|
+
}
|
|
1533
|
+
if (!appendedVariant && extended.valueOf() !== selector.valueOf()) {
|
|
1534
|
+
const variant = markExtended(maybePrefixNewSelectorWithImplicitParent(selector, extended.clone(true)));
|
|
1535
|
+
newSelectors.push(variant);
|
|
1536
|
+
}
|
|
1537
|
+
}
|
|
1538
|
+
const allSelectors = [...orderedSelectors, ...newSelectors];
|
|
1539
|
+
if (partial) {
|
|
1540
|
+
// In partial mode we intentionally keep :is() wrappers as items (Less `all` behavior),
|
|
1541
|
+
// rather than extracting them into comma-separated alternatives.
|
|
1542
|
+
const processed = createProcessedSelector(allSelectors, true);
|
|
1543
|
+
const processedArray = isArray(processed) ? processed : [processed];
|
|
1544
|
+
// See createExtendedSelectorList() for rationale: never include `target` as an adopted child
|
|
1545
|
+
// when we also inherit from it.
|
|
1546
|
+
const safeArray = processedArray.map(s => (s === target ? s.clone(true) : s));
|
|
1547
|
+
return SelectorList.create(safeArray).inherit(target);
|
|
1548
|
+
}
|
|
1549
|
+
// Exact-mode OR propagation:
|
|
1550
|
+
// If a selector-list contains authored `:is(parent)` sibling branches and only some siblings
|
|
1551
|
+
// whole-match `find`, propagate `extendWith` into the shared parent `:is(...)` argument for the
|
|
1552
|
+
// non-matching sibling branches in the same group.
|
|
1553
|
+
let fullModeSelectors = allSelectors;
|
|
1554
|
+
if (!partial && isNode(find, 'ComplexSelector') && isNode(target, 'SelectorList')) {
|
|
1555
|
+
const hasStandaloneExtendWith = fullModeSelectors.some(s => s.valueOf() === extendWith.valueOf());
|
|
1556
|
+
if (hasStandaloneExtendWith) {
|
|
1557
|
+
const candidates = [];
|
|
1558
|
+
for (let i = 0; i < orderedSelectors.length; i++) {
|
|
1559
|
+
const s = fullModeSelectors[i];
|
|
1560
|
+
if (!s || !isNode(s, 'ComplexSelector')) {
|
|
1561
|
+
continue;
|
|
1562
|
+
}
|
|
1563
|
+
const cs = s;
|
|
1564
|
+
if (cs.value.length !== 3) {
|
|
1565
|
+
continue;
|
|
1566
|
+
}
|
|
1567
|
+
const first = cs.value[0];
|
|
1568
|
+
const second = cs.value[1];
|
|
1569
|
+
if (!isNode(first, 'PseudoSelector') || first.value.name !== ':is' || first.generated) {
|
|
1570
|
+
continue;
|
|
1571
|
+
}
|
|
1572
|
+
if (!isNode(second, 'Combinator')) {
|
|
1573
|
+
continue;
|
|
1574
|
+
}
|
|
1575
|
+
const arg = first.value.arg;
|
|
1576
|
+
if (!arg || !isNode(arg, 'SelectorList')) {
|
|
1577
|
+
continue;
|
|
1578
|
+
}
|
|
1579
|
+
candidates.push({
|
|
1580
|
+
idx: i,
|
|
1581
|
+
selector: cs,
|
|
1582
|
+
parentArg: arg,
|
|
1583
|
+
hasSelectorMatch: !!orderedMatchFlags[i],
|
|
1584
|
+
groupKey: `${second.valueOf()}|${arg.valueOf()}`
|
|
1585
|
+
});
|
|
1586
|
+
}
|
|
1587
|
+
const byGroup = new Map();
|
|
1588
|
+
for (const c of candidates) {
|
|
1589
|
+
const list = byGroup.get(c.groupKey) ?? [];
|
|
1590
|
+
list.push(c);
|
|
1591
|
+
byGroup.set(c.groupKey, list);
|
|
1592
|
+
}
|
|
1593
|
+
let mutationCount = 0;
|
|
1594
|
+
const next = [...fullModeSelectors];
|
|
1595
|
+
for (const [, members] of byGroup) {
|
|
1596
|
+
if (members.length < 2) {
|
|
1597
|
+
continue;
|
|
1598
|
+
}
|
|
1599
|
+
if (!members.some(m => m.hasSelectorMatch) || !members.some(m => !m.hasSelectorMatch)) {
|
|
1600
|
+
continue;
|
|
1601
|
+
}
|
|
1602
|
+
for (const m of members) {
|
|
1603
|
+
if (m.hasSelectorMatch) {
|
|
1604
|
+
continue;
|
|
1605
|
+
}
|
|
1606
|
+
const hasExtendWith = m.parentArg.value.some(s => s.valueOf() === extendWith.valueOf());
|
|
1607
|
+
if (hasExtendWith) {
|
|
1608
|
+
continue;
|
|
1609
|
+
}
|
|
1610
|
+
const updatedArg = SelectorList.create([
|
|
1611
|
+
...m.parentArg.value.map(s => s.copy(true)),
|
|
1612
|
+
extendWith.copy(true)
|
|
1613
|
+
]).inherit(m.parentArg);
|
|
1614
|
+
const updatedSel = m.selector.copy(true);
|
|
1615
|
+
const updatedPseudo = updatedSel.value[0];
|
|
1616
|
+
updatedPseudo.value.arg = updatedArg;
|
|
1617
|
+
next[m.idx] = updatedSel;
|
|
1618
|
+
mutationCount++;
|
|
1619
|
+
}
|
|
1620
|
+
}
|
|
1621
|
+
if (mutationCount > 0) {
|
|
1622
|
+
fullModeSelectors = next;
|
|
1623
|
+
}
|
|
1624
|
+
}
|
|
1625
|
+
}
|
|
1626
|
+
// In full mode, try to factorize common `:is(parent) <child>` expansions back into
|
|
1627
|
+
// `:is(parent) :is(childA, childB, ...)` to match Less output expectations.
|
|
1628
|
+
//
|
|
1629
|
+
// This specifically targets the pattern produced by implicit parent selector alternatives.
|
|
1630
|
+
let finalSelectors = fullModeSelectors;
|
|
1631
|
+
try {
|
|
1632
|
+
const candidates = [];
|
|
1633
|
+
let sharedParent = null;
|
|
1634
|
+
let sharedCombinator = null;
|
|
1635
|
+
for (let i = 0; i < allSelectors.length; i++) {
|
|
1636
|
+
const s = allSelectors[i];
|
|
1637
|
+
if (!isNode(s, 'ComplexSelector')) {
|
|
1638
|
+
continue;
|
|
1639
|
+
}
|
|
1640
|
+
const cs = s;
|
|
1641
|
+
if (cs.value.length !== 3) {
|
|
1642
|
+
continue;
|
|
1643
|
+
}
|
|
1644
|
+
const first = cs.value[0];
|
|
1645
|
+
const second = cs.value[1];
|
|
1646
|
+
const third = cs.value[2];
|
|
1647
|
+
if (!(first instanceof Ampersand) || !first.hasFlag(F_IMPLICIT_AMPERSAND)) {
|
|
1648
|
+
continue;
|
|
1649
|
+
}
|
|
1650
|
+
const parentSel = first.getResolvedSelector();
|
|
1651
|
+
if (!parentSel || isNode(parentSel, 'Nil')) {
|
|
1652
|
+
continue;
|
|
1653
|
+
}
|
|
1654
|
+
if (!isNode(parentSel, 'PseudoSelector') || parentSel.value.name !== ':is') {
|
|
1655
|
+
continue;
|
|
1656
|
+
}
|
|
1657
|
+
if (!isNode(second, 'Combinator')) {
|
|
1658
|
+
continue;
|
|
1659
|
+
}
|
|
1660
|
+
if (!isNode(third, 'BasicSelector')) {
|
|
1661
|
+
continue;
|
|
1662
|
+
}
|
|
1663
|
+
const parentStr = parentSel.valueOf();
|
|
1664
|
+
const combStr = second.valueOf();
|
|
1665
|
+
if (sharedParent === null) {
|
|
1666
|
+
sharedParent = parentStr;
|
|
1667
|
+
}
|
|
1668
|
+
if (sharedCombinator === null) {
|
|
1669
|
+
sharedCombinator = combStr;
|
|
1670
|
+
}
|
|
1671
|
+
if (parentStr !== sharedParent || combStr !== sharedCombinator) {
|
|
1672
|
+
continue;
|
|
1673
|
+
}
|
|
1674
|
+
candidates.push({ idx: i, sel: cs });
|
|
1675
|
+
}
|
|
1676
|
+
if (candidates.length >= 2 && sharedParent && sharedCombinator) {
|
|
1677
|
+
const insertionIdx = candidates[0].idx;
|
|
1678
|
+
const template = candidates[0].sel;
|
|
1679
|
+
const first = template.value[0];
|
|
1680
|
+
const second = template.value[1];
|
|
1681
|
+
const childBasics = candidates.map(c => c.sel.value[2]).map(b => b.copy(true));
|
|
1682
|
+
const childList = SelectorList.create(childBasics).inherit(template);
|
|
1683
|
+
const childIs = new PseudoSelector({ name: ':is', arg: childList }).inherit(template);
|
|
1684
|
+
const combined = ComplexSelector.create([
|
|
1685
|
+
first.copy(true),
|
|
1686
|
+
second.copy(true),
|
|
1687
|
+
childIs
|
|
1688
|
+
]).inherit(template);
|
|
1689
|
+
const filtered = [];
|
|
1690
|
+
const removeIdx = new Set(candidates.map(c => c.idx));
|
|
1691
|
+
for (let i = 0; i < allSelectors.length; i++) {
|
|
1692
|
+
if (i === insertionIdx) {
|
|
1693
|
+
filtered.push(combined);
|
|
1694
|
+
}
|
|
1695
|
+
if (removeIdx.has(i)) {
|
|
1696
|
+
continue;
|
|
1697
|
+
}
|
|
1698
|
+
filtered.push(allSelectors[i]);
|
|
1699
|
+
}
|
|
1700
|
+
finalSelectors = filtered;
|
|
1701
|
+
}
|
|
1702
|
+
// Exact-mode de-distribution:
|
|
1703
|
+
// Collapse explicit cartesian-product expansions
|
|
1704
|
+
// p1 <c> r1, p2 <c> r1, p1 <c> r2, p2 <c> r2
|
|
1705
|
+
// into
|
|
1706
|
+
// :is(p1, p2) <c> :is(r1, r2)
|
|
1707
|
+
// when the full cross-product is present.
|
|
1708
|
+
if (!partial) {
|
|
1709
|
+
const byCombinator = new Map();
|
|
1710
|
+
for (let i = 0; i < finalSelectors.length; i++) {
|
|
1711
|
+
const s = finalSelectors[i];
|
|
1712
|
+
if (!s || !isNode(s, 'ComplexSelector')) {
|
|
1713
|
+
continue;
|
|
1714
|
+
}
|
|
1715
|
+
const cs = s;
|
|
1716
|
+
if (cs.value.length !== 3) {
|
|
1717
|
+
continue;
|
|
1718
|
+
}
|
|
1719
|
+
const first = cs.value[0];
|
|
1720
|
+
const second = cs.value[1];
|
|
1721
|
+
const third = cs.value[2];
|
|
1722
|
+
if (!isNode(second, 'Combinator')) {
|
|
1723
|
+
continue;
|
|
1724
|
+
}
|
|
1725
|
+
const groupKey = second.valueOf();
|
|
1726
|
+
const list = byCombinator.get(groupKey) ?? [];
|
|
1727
|
+
list.push({
|
|
1728
|
+
idx: i,
|
|
1729
|
+
selector: cs,
|
|
1730
|
+
left: first,
|
|
1731
|
+
right: third,
|
|
1732
|
+
combinator: second
|
|
1733
|
+
});
|
|
1734
|
+
byCombinator.set(groupKey, list);
|
|
1735
|
+
}
|
|
1736
|
+
for (const [, group] of byCombinator) {
|
|
1737
|
+
if (group.length < 4) {
|
|
1738
|
+
continue;
|
|
1739
|
+
}
|
|
1740
|
+
const leftOrder = [];
|
|
1741
|
+
const rightOrder = [];
|
|
1742
|
+
const leftMap = new Map();
|
|
1743
|
+
const rightMap = new Map();
|
|
1744
|
+
const pairSet = new Set();
|
|
1745
|
+
for (const c of group) {
|
|
1746
|
+
const lk = c.left.valueOf();
|
|
1747
|
+
const rk = c.right.valueOf();
|
|
1748
|
+
if (!leftMap.has(lk)) {
|
|
1749
|
+
leftMap.set(lk, c.left);
|
|
1750
|
+
leftOrder.push(lk);
|
|
1751
|
+
}
|
|
1752
|
+
if (!rightMap.has(rk)) {
|
|
1753
|
+
rightMap.set(rk, c.right);
|
|
1754
|
+
rightOrder.push(rk);
|
|
1755
|
+
}
|
|
1756
|
+
pairSet.add(`${lk}||${rk}`);
|
|
1757
|
+
}
|
|
1758
|
+
if (leftOrder.length < 2 || rightOrder.length < 2) {
|
|
1759
|
+
continue;
|
|
1760
|
+
}
|
|
1761
|
+
const expectedPairs = leftOrder.length * rightOrder.length;
|
|
1762
|
+
if (pairSet.size !== expectedPairs) {
|
|
1763
|
+
continue;
|
|
1764
|
+
}
|
|
1765
|
+
const groupPairCount = group.reduce((count, c) => {
|
|
1766
|
+
const lk = c.left.valueOf();
|
|
1767
|
+
const rk = c.right.valueOf();
|
|
1768
|
+
return pairSet.has(`${lk}||${rk}`) ? count + 1 : count;
|
|
1769
|
+
}, 0);
|
|
1770
|
+
if (groupPairCount !== expectedPairs) {
|
|
1771
|
+
continue;
|
|
1772
|
+
}
|
|
1773
|
+
const mkSide = (keys, map, inheritFrom) => {
|
|
1774
|
+
if (keys.length === 1) {
|
|
1775
|
+
return map.get(keys[0]).copy(true);
|
|
1776
|
+
}
|
|
1777
|
+
const list = SelectorList.create(keys.map(k => map.get(k).copy(true))).inherit(inheritFrom);
|
|
1778
|
+
const pseudo = PseudoSelector.create({ name: ':is', arg: list }).inherit(inheritFrom);
|
|
1779
|
+
pseudo.generated = false;
|
|
1780
|
+
return pseudo;
|
|
1781
|
+
};
|
|
1782
|
+
const insertIdx = Math.min(...group.map(c => c.idx));
|
|
1783
|
+
const template = group.find(c => c.idx === insertIdx) ?? group[0];
|
|
1784
|
+
const leftSide = mkSide(leftOrder, leftMap, template.left);
|
|
1785
|
+
const rightSide = mkSide(rightOrder, rightMap, template.right);
|
|
1786
|
+
const combined = ComplexSelector.create([
|
|
1787
|
+
leftSide,
|
|
1788
|
+
template.combinator.copy(true),
|
|
1789
|
+
rightSide
|
|
1790
|
+
]).inherit(template.selector);
|
|
1791
|
+
const removeSet = new Set(group.map(c => c.idx));
|
|
1792
|
+
const rebuilt = [];
|
|
1793
|
+
for (let i = 0; i < finalSelectors.length; i++) {
|
|
1794
|
+
if (i === insertIdx) {
|
|
1795
|
+
rebuilt.push(combined);
|
|
1796
|
+
}
|
|
1797
|
+
if (removeSet.has(i)) {
|
|
1798
|
+
continue;
|
|
1799
|
+
}
|
|
1800
|
+
rebuilt.push(finalSelectors[i]);
|
|
1801
|
+
}
|
|
1802
|
+
finalSelectors = rebuilt;
|
|
1803
|
+
break;
|
|
1804
|
+
}
|
|
1805
|
+
}
|
|
1806
|
+
}
|
|
1807
|
+
catch { }
|
|
1808
|
+
return createExtendedSelectorList(finalSelectors, target);
|
|
1809
|
+
}
|
|
1810
|
+
/**
|
|
1811
|
+
* Selects the best location from search results based on partial/full mode and context
|
|
1812
|
+
* @param searchResult - The search result with all matching locations
|
|
1813
|
+
* @param target - The target selector
|
|
1814
|
+
* @param find - The selector to find
|
|
1815
|
+
* @param partial - Whether to use partial matching
|
|
1816
|
+
* @param hasMoreAfterIs - Whether there are more components after :is()
|
|
1817
|
+
* @param extendWith - The selector to extend with (for error context)
|
|
1818
|
+
* @returns The selected location
|
|
1819
|
+
*/
|
|
1820
|
+
function selectBestLocation(searchResult, comparison, target, find, partial, hasMoreAfterIs, extendWith) {
|
|
1821
|
+
const getMatchScope = (loc) => {
|
|
1822
|
+
if (loc.matchScope) {
|
|
1823
|
+
return loc.matchScope;
|
|
1824
|
+
}
|
|
1825
|
+
const path = Array.isArray(loc.path) ? loc.path : [];
|
|
1826
|
+
if (path.includes('arg')) {
|
|
1827
|
+
return 'isArgument';
|
|
1828
|
+
}
|
|
1829
|
+
if (isNode(target, 'SelectorList')) {
|
|
1830
|
+
return 'selectorList';
|
|
1831
|
+
}
|
|
1832
|
+
return 'root';
|
|
1833
|
+
};
|
|
1834
|
+
// For partial extends, prefer actual matches over "append to :is() list" extension points
|
|
1835
|
+
// The "append to list" locations have paths ending in 'arg', while actual matches have
|
|
1836
|
+
// more specific paths like [index, 'arg', altIndex]
|
|
1837
|
+
// For full extends (partial: false), prefer valid full matches
|
|
1838
|
+
// Prefer an actual matched-node replacement/wrap over "append to :is() list" locations.
|
|
1839
|
+
// This matters for cases like:
|
|
1840
|
+
// target: `:is(parent) :is(.replace,.c)`
|
|
1841
|
+
// find: `.c`
|
|
1842
|
+
// where the matcher reports both:
|
|
1843
|
+
// - a real `.c` match inside the child :is() arg (replace/wrap)
|
|
1844
|
+
// - an "append" location for the parent :is() arg
|
|
1845
|
+
// In full mode we should extend the `.c` occurrence, not mutate the parent list.
|
|
1846
|
+
const originalLocations = Array.isArray(searchResult.locations)
|
|
1847
|
+
? searchResult.locations
|
|
1848
|
+
: [];
|
|
1849
|
+
const locations = originalLocations.length > 0
|
|
1850
|
+
? originalLocations
|
|
1851
|
+
: comparison.locations;
|
|
1852
|
+
if (locations.length > 0) {
|
|
1853
|
+
const typePriority = { wrap: 0, replace: 1, append: 2 };
|
|
1854
|
+
locations.sort((a, b) => {
|
|
1855
|
+
const pa = typePriority[a.extensionType] ?? 3;
|
|
1856
|
+
const pb = typePriority[b.extensionType] ?? 3;
|
|
1857
|
+
if (pa !== pb) {
|
|
1858
|
+
return pa - pb;
|
|
1859
|
+
}
|
|
1860
|
+
const pathA = Array.isArray(a.path) ? a.path.length : 0;
|
|
1861
|
+
const pathB = Array.isArray(b.path) ? b.path.length : 0;
|
|
1862
|
+
return pathA - pathB;
|
|
1863
|
+
});
|
|
1864
|
+
searchResult.locations = locations;
|
|
1865
|
+
}
|
|
1866
|
+
const findV = find.valueOf();
|
|
1867
|
+
// In partial mode, prefer wrapping a specific list item over appending to the :is() list.
|
|
1868
|
+
// e.g. .a:is(.b,.c).d + find .b → use path [1,'arg',0] (wrap .b) not [1,'arg'] (append .q to list).
|
|
1869
|
+
if (partial && locations.length > 1) {
|
|
1870
|
+
const withItemIndex = locations.filter((l) => Array.isArray(l?.path) && l.path.length >= 2
|
|
1871
|
+
&& l.path[l.path.length - 2] === 'arg'
|
|
1872
|
+
&& typeof l.path[l.path.length - 1] === 'number');
|
|
1873
|
+
if (withItemIndex.length > 0) {
|
|
1874
|
+
const appendOnly = locations.filter((l) => Array.isArray(l?.path) && l.path.length >= 1 && l.path[l.path.length - 1] === 'arg');
|
|
1875
|
+
if (appendOnly.length > 0) {
|
|
1876
|
+
// Prefer wrap/replace at the item over append at the list
|
|
1877
|
+
const wrapOrReplace = withItemIndex.filter((l) => l?.extensionType === 'wrap' || l?.extensionType === 'replace');
|
|
1878
|
+
searchResult.locations = wrapOrReplace.length > 0 ? wrapOrReplace : withItemIndex;
|
|
1879
|
+
}
|
|
1880
|
+
}
|
|
1881
|
+
}
|
|
1882
|
+
const preferNonAppend = !partial && locations.length > 0;
|
|
1883
|
+
if (preferNonAppend) {
|
|
1884
|
+
const actualMatches = locations.filter((l) => {
|
|
1885
|
+
if (l?.extensionType === 'append' && getMatchScope(l) === 'isArgument') {
|
|
1886
|
+
return true;
|
|
1887
|
+
}
|
|
1888
|
+
if (l?.extensionType !== 'append') {
|
|
1889
|
+
return true;
|
|
1890
|
+
}
|
|
1891
|
+
try {
|
|
1892
|
+
const mv = l?.matchedNode?.valueOf?.();
|
|
1893
|
+
return typeof mv === 'string' && mv === findV;
|
|
1894
|
+
}
|
|
1895
|
+
catch {
|
|
1896
|
+
return false;
|
|
1897
|
+
}
|
|
1898
|
+
});
|
|
1899
|
+
// Keep "append" locations that target the matched node itself (e.g. appending into a child :is() arg),
|
|
1900
|
+
// but drop "append" locations that mutate an enclosing SelectorList (these incorrectly add to the parent list).
|
|
1901
|
+
const filtered = actualMatches.filter((l) => {
|
|
1902
|
+
if (l?.extensionType === 'append' && getMatchScope(l) === 'isArgument') {
|
|
1903
|
+
return true;
|
|
1904
|
+
}
|
|
1905
|
+
if (l?.extensionType !== 'append') {
|
|
1906
|
+
return true;
|
|
1907
|
+
}
|
|
1908
|
+
const mt = l?.matchedNode?.type ?? null;
|
|
1909
|
+
if (mt === 'SelectorList') {
|
|
1910
|
+
return false;
|
|
1911
|
+
}
|
|
1912
|
+
// Also drop the common parent-arg append shape: [..., 'arg'] with no index following.
|
|
1913
|
+
if (Array.isArray(l?.path) && l.path.length >= 2) {
|
|
1914
|
+
const last = l.path[l.path.length - 1];
|
|
1915
|
+
const prev = l.path[l.path.length - 2];
|
|
1916
|
+
if (last === 'arg' && typeof prev === 'number') {
|
|
1917
|
+
return false;
|
|
1918
|
+
}
|
|
1919
|
+
}
|
|
1920
|
+
return true;
|
|
1921
|
+
});
|
|
1922
|
+
if (filtered.length > 0) {
|
|
1923
|
+
// Prefer appending into the exact matched node (keeps `:is(.a,.b,.effected)` shape)
|
|
1924
|
+
const appendBasic = isNode(find, 'SimpleSelector')
|
|
1925
|
+
? filtered.find((l) => l?.extensionType === 'append' && l?.matchedNode?.type === 'BasicSelector')
|
|
1926
|
+
: undefined;
|
|
1927
|
+
if (appendBasic) {
|
|
1928
|
+
searchResult.locations = [appendBasic];
|
|
1929
|
+
}
|
|
1930
|
+
else {
|
|
1931
|
+
// Otherwise prefer replace over wrap if both exist.
|
|
1932
|
+
const replace = filtered.find((l) => l?.extensionType === 'replace');
|
|
1933
|
+
const wrap = filtered.find((l) => l?.extensionType === 'wrap');
|
|
1934
|
+
searchResult.locations = replace ? [replace] : (wrap ? [wrap] : filtered);
|
|
1935
|
+
}
|
|
1936
|
+
}
|
|
1937
|
+
}
|
|
1938
|
+
// Narrow rule for complex exact extends (e.g. `.replace.replace .replace`):
|
|
1939
|
+
// if both append and non-append candidates exist, prefer concrete non-append
|
|
1940
|
+
// locations to avoid mutating the parent :is() argument.
|
|
1941
|
+
if (!partial && isNode(find, 'ComplexSelector') && searchResult.locations?.length > 1) {
|
|
1942
|
+
const nonAppend = searchResult.locations.filter((l) => l.extensionType !== 'append');
|
|
1943
|
+
if (nonAppend.length > 0) {
|
|
1944
|
+
searchResult.locations = nonAppend;
|
|
1945
|
+
}
|
|
1946
|
+
}
|
|
1947
|
+
if (searchResult.locations?.length) {
|
|
1948
|
+
const hasWrap = searchResult.locations.some((l) => l.extensionType === 'wrap');
|
|
1949
|
+
const hasAppend = searchResult.locations.some((l) => l.extensionType === 'append');
|
|
1950
|
+
if (hasWrap && hasAppend && !isNode(find, 'SimpleSelector')) {
|
|
1951
|
+
searchResult.locations = searchResult.locations.filter((l) => l.extensionType !== 'append');
|
|
1952
|
+
}
|
|
1953
|
+
}
|
|
1954
|
+
let locationLocked = false;
|
|
1955
|
+
const finalLocations = (searchResult.locations && searchResult.locations.length > 0)
|
|
1956
|
+
? searchResult.locations
|
|
1957
|
+
: locations;
|
|
1958
|
+
const matchScopePriority = {
|
|
1959
|
+
isArgument: 0,
|
|
1960
|
+
selectorList: 1,
|
|
1961
|
+
root: 2
|
|
1962
|
+
};
|
|
1963
|
+
let location = finalLocations[0];
|
|
1964
|
+
if (!partial && finalLocations.length > 1) {
|
|
1965
|
+
let best = finalLocations[0];
|
|
1966
|
+
for (const candidate of finalLocations) {
|
|
1967
|
+
const bestScope = matchScopePriority[getMatchScope(best)];
|
|
1968
|
+
const candidateScope = matchScopePriority[getMatchScope(candidate)];
|
|
1969
|
+
if (candidateScope < bestScope) {
|
|
1970
|
+
best = candidate;
|
|
1971
|
+
}
|
|
1972
|
+
}
|
|
1973
|
+
location = best;
|
|
1974
|
+
locationLocked = getMatchScope(best) === 'isArgument';
|
|
1975
|
+
}
|
|
1976
|
+
if (!locationLocked) {
|
|
1977
|
+
const appendInIsArg = finalLocations.find((loc) => loc.extensionType === 'append'
|
|
1978
|
+
&& getMatchScope(loc) === 'isArgument'
|
|
1979
|
+
&& !loc.isPartialMatch);
|
|
1980
|
+
if (appendInIsArg && !isNode(find, 'ComplexSelector')) {
|
|
1981
|
+
location = appendInIsArg;
|
|
1982
|
+
locationLocked = true;
|
|
1983
|
+
}
|
|
1984
|
+
}
|
|
1985
|
+
// Exception: When partial: false and we're inside an :is() with more components after it,
|
|
1986
|
+
// even if we've matched the entire find (full match of item in :is()), it's still a partial match
|
|
1987
|
+
// of the entire selector because there are components after the :is()
|
|
1988
|
+
// Example: :is(.i).j with find .i and partial: false
|
|
1989
|
+
// We matched .i (full match of item in :is()), but there's .j after, so this is a partial match
|
|
1990
|
+
if (!partial && hasMoreAfterIs) {
|
|
1991
|
+
// If target is a SelectorList (we're inside an :is() argument), check if we matched an entire item
|
|
1992
|
+
const isInsideSelectorList = isNode(target, 'SelectorList');
|
|
1993
|
+
if (isInsideSelectorList) {
|
|
1994
|
+
// The location path will be like [index] or ['arg', index] when matching an item in the list
|
|
1995
|
+
// Check if we matched an entire item (not a partial match within that item)
|
|
1996
|
+
const pathHasIndex = location.path.some((p, i) => typeof p === 'number' && (i === 0 || location.path[i - 1] === 'arg'));
|
|
1997
|
+
const matchedEntireItem = pathHasIndex && !location.isPartialMatch;
|
|
1998
|
+
// Also check if the matched node equals the find
|
|
1999
|
+
const matchedNode = location.matchedNode;
|
|
2000
|
+
const matchedNodeEqualsFind = matchedNode && matchedNode.valueOf() === find.valueOf();
|
|
2001
|
+
// If we matched an entire item and there are more components after, this is a partial match
|
|
2002
|
+
if (matchedEntireItem || matchedNodeEqualsFind) {
|
|
2003
|
+
throw new ExtendError(ExtendErrorType.PARTIAL_MATCH, 'Partial match found but exact match required', { target, find, extendWith });
|
|
2004
|
+
}
|
|
2005
|
+
}
|
|
2006
|
+
}
|
|
2007
|
+
// (Partial matches are now handled by the unified check in the full matching mode section)
|
|
2008
|
+
if (!locationLocked && !partial && searchResult.locations.length > 1) {
|
|
2009
|
+
// When partial: false, prefer valid full matches (root-level or first component of complex selector)
|
|
2010
|
+
// IMPORTANT: Must check !loc.isPartialMatch to avoid selecting partial matches
|
|
2011
|
+
const validFullMatch = searchResult.locations.find((loc) => {
|
|
2012
|
+
if (loc.path.length === 0 && !loc.isPartialMatch) {
|
|
2013
|
+
return true;
|
|
2014
|
+
}
|
|
2015
|
+
if (loc.path.length === 1 && isNode(target, 'ComplexSelector') && loc.path[0] === 0 && !loc.isPartialMatch) {
|
|
2016
|
+
return true;
|
|
2017
|
+
}
|
|
2018
|
+
if (loc.path.includes('arg') && !loc.isPartialMatch) {
|
|
2019
|
+
return true;
|
|
2020
|
+
}
|
|
2021
|
+
return false;
|
|
2022
|
+
});
|
|
2023
|
+
if (validFullMatch) {
|
|
2024
|
+
location = validFullMatch;
|
|
2025
|
+
}
|
|
2026
|
+
}
|
|
2027
|
+
else if (partial && searchResult.locations.length > 1) {
|
|
2028
|
+
// Find a location that's not just an "append to :is() list" opportunity
|
|
2029
|
+
// These have paths ending in 'arg' without a following index
|
|
2030
|
+
const actualMatch = searchResult.locations.find((loc) => {
|
|
2031
|
+
// If it's not an append type, it's definitely an actual match
|
|
2032
|
+
if (loc.extensionType !== 'append') {
|
|
2033
|
+
return true;
|
|
2034
|
+
}
|
|
2035
|
+
// For append types, check if this is an actual match inside :is() vs just an append opportunity
|
|
2036
|
+
// Actual matches have paths like [0, 'arg', 0] (ending in a number after 'arg')
|
|
2037
|
+
// Append opportunities have paths like [0, 'arg'] (ending in 'arg')
|
|
2038
|
+
const lastPathElement = loc.path[loc.path.length - 1];
|
|
2039
|
+
return typeof lastPathElement === 'number';
|
|
2040
|
+
});
|
|
2041
|
+
if (actualMatch) {
|
|
2042
|
+
location = actualMatch;
|
|
2043
|
+
}
|
|
2044
|
+
}
|
|
2045
|
+
return location;
|
|
2046
|
+
}
|
|
2047
|
+
function isNonAllWholeSelectorItemMatch(target, findValue) {
|
|
2048
|
+
// Exact whole-selector match (single selector item).
|
|
2049
|
+
if (target.valueOf() === findValue) {
|
|
2050
|
+
return true;
|
|
2051
|
+
}
|
|
2052
|
+
// SelectorList item match.
|
|
2053
|
+
if (isNode(target, 'SelectorList')) {
|
|
2054
|
+
return target.value.some((s) => {
|
|
2055
|
+
try {
|
|
2056
|
+
return s?.valueOf?.() === findValue;
|
|
2057
|
+
}
|
|
2058
|
+
catch {
|
|
2059
|
+
return false;
|
|
2060
|
+
}
|
|
2061
|
+
});
|
|
2062
|
+
}
|
|
2063
|
+
// OR-path match: if the *entire selector item* is a selector-arg pseudo like :is(...)
|
|
2064
|
+
// and one alternative equals the find selector, that's a valid whole-item match.
|
|
2065
|
+
if (isNode(target, 'PseudoSelector')) {
|
|
2066
|
+
const arg = target.value?.arg;
|
|
2067
|
+
if (arg && isNode(arg, 'SelectorList')) {
|
|
2068
|
+
return arg.value.some((s) => {
|
|
2069
|
+
try {
|
|
2070
|
+
return s?.valueOf?.() === findValue;
|
|
2071
|
+
}
|
|
2072
|
+
catch {
|
|
2073
|
+
return false;
|
|
2074
|
+
}
|
|
2075
|
+
});
|
|
2076
|
+
}
|
|
2077
|
+
if (arg && typeof arg === 'object' && typeof arg.valueOf === 'function') {
|
|
2078
|
+
try {
|
|
2079
|
+
if (arg.valueOf() === findValue) {
|
|
2080
|
+
return true;
|
|
2081
|
+
}
|
|
2082
|
+
// Nested :is() e.g. :is(:is(.foo)) - recurse into single arg
|
|
2083
|
+
if (isNode(arg, 'Selector')) {
|
|
2084
|
+
return isNonAllWholeSelectorItemMatch(arg, findValue);
|
|
2085
|
+
}
|
|
2086
|
+
}
|
|
2087
|
+
catch {
|
|
2088
|
+
return false;
|
|
2089
|
+
}
|
|
2090
|
+
}
|
|
2091
|
+
}
|
|
2092
|
+
return false;
|
|
2093
|
+
}
|
|
2094
|
+
/**
|
|
2095
|
+
* Handles extension in partial matching mode - creates :is() wrappers for component-level matches.
|
|
2096
|
+
*
|
|
2097
|
+
* What gets wrapped: within-one-compound → wrap only matched part; spans-combinator → wrap full segment.
|
|
2098
|
+
* See EXTEND_RULES.md §3a.
|
|
2099
|
+
*
|
|
2100
|
+
* IMPLEMENTATION WARNING: Do NOT decide wrap scope by target type or path length. Target can be
|
|
2101
|
+
* :is() containing complex, SelectorList, compound with :is() inside, etc. Use keySet + equivalency
|
|
2102
|
+
* and "what does the match PRODUCE" (e.g. does it include combinators?) to decide. The branches
|
|
2103
|
+
* below that check path.length and isNode(target, ...) are narrow and fail for nested targets;
|
|
2104
|
+
* they should be replaced by match-result-based logic.
|
|
2105
|
+
*/
|
|
2106
|
+
function handlePartialModeExtension(target, location, extendWith) {
|
|
2107
|
+
// Unified path: use path + match result only. For partial mode, component-level matches get :is(matched, extendWith).
|
|
2108
|
+
// Force extensionType to 'wrap' when path points to a component (path.length >= 1) so applyExtensionAtPath
|
|
2109
|
+
// wraps the node at path instead of replacing. Works for any target shape (SelectorList, :is(complex), etc.).
|
|
2110
|
+
const extensionType = location.path && location.path.length >= 1 ? 'wrap' : (location.extensionType ?? 'replace');
|
|
2111
|
+
const wrapLocation = { ...location, extensionType };
|
|
2112
|
+
const result = applyExtensionAtLocation(target, wrapLocation, extendWith);
|
|
2113
|
+
return result;
|
|
2114
|
+
}
|
|
2115
|
+
/**
|
|
2116
|
+
* Handles full match extension - adds the extension as a new alternative
|
|
2117
|
+
* @param target - The selector to extend (what we're searching within)
|
|
2118
|
+
* @param find - The selector that was matched (what we were searching for)
|
|
2119
|
+
* @param extendWith - The selector to add as an alternative
|
|
2120
|
+
* @param matchResult - The result from the selector matching operation
|
|
2121
|
+
* @returns Extended selector with the new alternative
|
|
2122
|
+
*/
|
|
2123
|
+
function handleFullExtend(target, find, extendWith, _matchResult) {
|
|
2124
|
+
// For full matches, we add the extension as a new selector in a list
|
|
2125
|
+
// If target is already a selector list, add to it
|
|
2126
|
+
if (isNode(target, 'SelectorList')) {
|
|
2127
|
+
// Use clone to preserve comments
|
|
2128
|
+
const copyForInheritance = target.clone();
|
|
2129
|
+
return createExtendedSelectorList([...target.value, extendWith], copyForInheritance);
|
|
2130
|
+
}
|
|
2131
|
+
// If target is a pseudo-selector with selector arguments, check if we should extend arguments or create selector list
|
|
2132
|
+
if (isNode(target, 'PseudoSelector')) {
|
|
2133
|
+
const arg = target.value.arg;
|
|
2134
|
+
// Only extend arguments for :is() pseudo-selectors or when the find is NOT the complete pseudo-selector
|
|
2135
|
+
// For other pseudo-selectors like :where(), when the entire pseudo-selector is matched, create a selector list
|
|
2136
|
+
if (arg && arg.isSelector && target.value.name === ':is') {
|
|
2137
|
+
if (isNode(arg, 'SelectorList')) {
|
|
2138
|
+
// Add to existing selector list
|
|
2139
|
+
const newArg = createExtendedSelectorList([...arg.value, extendWith], arg);
|
|
2140
|
+
// If the original selector was generated, we can mutate it in place for performance
|
|
2141
|
+
if (target.generated) {
|
|
2142
|
+
target.value.arg = newArg;
|
|
2143
|
+
return target;
|
|
2144
|
+
}
|
|
2145
|
+
else {
|
|
2146
|
+
// For authored selectors, create a new one to preserve the original
|
|
2147
|
+
return PseudoSelector.create({
|
|
2148
|
+
name: target.value.name,
|
|
2149
|
+
arg: newArg
|
|
2150
|
+
}).inherit(target);
|
|
2151
|
+
}
|
|
2152
|
+
}
|
|
2153
|
+
else {
|
|
2154
|
+
// Convert single selector to list and add extension
|
|
2155
|
+
const newArg = createExtendedSelectorList([arg, extendWith], arg);
|
|
2156
|
+
// If the original selector was generated, we can mutate it in place for performance
|
|
2157
|
+
if (target.generated) {
|
|
2158
|
+
target.value.arg = newArg;
|
|
2159
|
+
return target;
|
|
2160
|
+
}
|
|
2161
|
+
else {
|
|
2162
|
+
// For authored selectors, create a new one to preserve the original
|
|
2163
|
+
return PseudoSelector.create({
|
|
2164
|
+
name: target.value.name,
|
|
2165
|
+
arg: newArg
|
|
2166
|
+
}).inherit(target);
|
|
2167
|
+
}
|
|
2168
|
+
}
|
|
2169
|
+
}
|
|
2170
|
+
// For non-:is() pseudo-selectors or when find matches the entire pseudo-selector,
|
|
2171
|
+
// fall through to create a selector list
|
|
2172
|
+
}
|
|
2173
|
+
// For compound selectors in full extend mode, just create a selector list
|
|
2174
|
+
// (Component-level matches are handled earlier in extendSelector, not here)
|
|
2175
|
+
// handleCompoundFullExtend is only for special cases like extending within :is() pseudo-selectors
|
|
2176
|
+
if (isNode(target, 'CompoundSelector')) {
|
|
2177
|
+
// Order: target (ruleset owner) first, then extendWith. Same as SelectorList append and circular ref.
|
|
2178
|
+
const copyForInheritance = target.clone();
|
|
2179
|
+
return createExtendedSelectorList([target, extendWith], copyForInheritance);
|
|
2180
|
+
}
|
|
2181
|
+
// Order: target (ruleset owner) first, then extendWith. So .e gets [.e, .d], .z gets [.z, .x], and
|
|
2182
|
+
// when we later append (e.g. .y to [.z, .x]) we get [.z, .x, .y] — one consistent path.
|
|
2183
|
+
const copyForInheritance = target.clone();
|
|
2184
|
+
return createExtendedSelectorList([target, extendWith], copyForInheritance);
|
|
2185
|
+
}
|
|
2186
|
+
// Removed unused function: handleCompoundFullExtend
|
|
2187
|
+
// This function was never called. The logic it contained is now handled inline
|
|
2188
|
+
// in extendSelector (lines 1160-1203) for full mode compound selector handling.
|
|
2189
|
+
/**
|
|
2190
|
+
* Creates an :is() wrapper around the given selectors
|
|
2191
|
+
* Preserves comments on original selectors, strips them from inheritance chain
|
|
2192
|
+
*/
|
|
2193
|
+
function createIsWrapper(selectors, inheritFrom) {
|
|
2194
|
+
// Strip comments only from the inheritance chain to avoid duplication on the wrapper
|
|
2195
|
+
const copyForInheritance = inheritFrom.copy();
|
|
2196
|
+
// Create selectorList with original selectors (preserving their comments)
|
|
2197
|
+
// Basic deduplication here to avoid obvious duplicates
|
|
2198
|
+
// Full normalization (flattening) will be handled by createProcessedSelector
|
|
2199
|
+
// when the result is processed through createExtendedSelectorList
|
|
2200
|
+
const deduplicated = deduplicateSelectors(selectors);
|
|
2201
|
+
const selectorList = SelectorList.create(deduplicated);
|
|
2202
|
+
// Create PseudoSelector using the create factory method - same signature as constructor but marks as generated
|
|
2203
|
+
const pseudoSelector = PseudoSelector.create({
|
|
2204
|
+
name: ':is',
|
|
2205
|
+
arg: selectorList
|
|
2206
|
+
}).inherit(copyForInheritance);
|
|
2207
|
+
// Ensure downstream normalization can unwrap/merge this wrapper when appropriate.
|
|
2208
|
+
pseudoSelector.generated = true;
|
|
2209
|
+
return pseudoSelector;
|
|
2210
|
+
}
|
|
2211
|
+
// Removed unused function: createValidatedIsWrapper
|
|
2212
|
+
// Only createValidatedIsWrapperWithErrors (which throws) is used throughout the codebase.
|
|
2213
|
+
// Fallback behavior is not needed.
|
|
2214
|
+
/**
|
|
2215
|
+
* Creates an :is() wrapper with validation that throws errors on conflicts
|
|
2216
|
+
* @param selectors - Array of selectors to wrap in :is()
|
|
2217
|
+
* @param inheritFrom - Selector to inherit properties from
|
|
2218
|
+
* @param contextSelector - Optional context selector to check for conflicts
|
|
2219
|
+
* @param context - Context information for error reporting
|
|
2220
|
+
* @returns Valid :is() pseudo-selector
|
|
2221
|
+
* @throws ExtendError if validation fails
|
|
2222
|
+
*/
|
|
2223
|
+
function createValidatedIsWrapperWithErrors(selectors, inheritFrom, contextSelector, context) {
|
|
2224
|
+
const decoratedSelectors = selectors.map(selector => selector.copy(true));
|
|
2225
|
+
if (context?.find && context?.extendWith && context.find.valueOf() !== context.extendWith.valueOf()) {
|
|
2226
|
+
const first = decoratedSelectors[0];
|
|
2227
|
+
if (first) {
|
|
2228
|
+
first.addFlag(F_EXTEND_TARGET);
|
|
2229
|
+
}
|
|
2230
|
+
for (let i = 1; i < decoratedSelectors.length; i++) {
|
|
2231
|
+
decoratedSelectors[i].addFlag(F_EXTENDED);
|
|
2232
|
+
}
|
|
2233
|
+
}
|
|
2234
|
+
const validation = validateIsWrapper(decoratedSelectors, contextSelector);
|
|
2235
|
+
if (!validation.isValid) {
|
|
2236
|
+
throw new ExtendError(validation.errorType, validation.errorMessage, context);
|
|
2237
|
+
}
|
|
2238
|
+
const wrapper = createIsWrapper(decoratedSelectors, inheritFrom);
|
|
2239
|
+
// Mark generated so downstream normalization and valueOf can flatten when appropriate.
|
|
2240
|
+
wrapper.generated = true;
|
|
2241
|
+
return wrapper;
|
|
2242
|
+
}
|
|
2243
|
+
/**
|
|
2244
|
+
* Enhanced validation for :is() wrappers that returns detailed error information
|
|
2245
|
+
*/
|
|
2246
|
+
function validateIsWrapper(selectors, contextSelector) {
|
|
2247
|
+
// If we have a context selector (the compound this :is() will be placed in),
|
|
2248
|
+
// check if the :is() contents would conflict with the context
|
|
2249
|
+
if (contextSelector && isNode(contextSelector, 'CompoundSelector')) {
|
|
2250
|
+
// Collect all elements and IDs from context
|
|
2251
|
+
const contextElementTypes = new Set();
|
|
2252
|
+
const contextIdValues = new Set();
|
|
2253
|
+
for (const child of contextSelector.value) {
|
|
2254
|
+
if (isNode(child, 'BasicSelector')) {
|
|
2255
|
+
if (child.isTag) {
|
|
2256
|
+
contextElementTypes.add(child.value.toLowerCase());
|
|
2257
|
+
}
|
|
2258
|
+
if (child.isId) {
|
|
2259
|
+
contextIdValues.add(child.value);
|
|
2260
|
+
}
|
|
2261
|
+
}
|
|
2262
|
+
}
|
|
2263
|
+
// Collect all elements and IDs from all selectors in the :is()
|
|
2264
|
+
const allElementTypes = new Set(contextElementTypes);
|
|
2265
|
+
const allIdValues = new Set(contextIdValues);
|
|
2266
|
+
for (const selector of selectors) {
|
|
2267
|
+
if (isNode(selector, 'BasicSelector')) {
|
|
2268
|
+
if (selector.isTag) {
|
|
2269
|
+
allElementTypes.add(selector.value.toLowerCase());
|
|
2270
|
+
}
|
|
2271
|
+
if (selector.isId) {
|
|
2272
|
+
allIdValues.add(selector.value);
|
|
2273
|
+
}
|
|
2274
|
+
}
|
|
2275
|
+
else if (isNode(selector, 'CompoundSelector')) {
|
|
2276
|
+
for (const child of selector.value) {
|
|
2277
|
+
if (isNode(child, 'BasicSelector')) {
|
|
2278
|
+
if (child.isTag) {
|
|
2279
|
+
allElementTypes.add(child.value.toLowerCase());
|
|
2280
|
+
}
|
|
2281
|
+
if (child.isId) {
|
|
2282
|
+
allIdValues.add(child.value);
|
|
2283
|
+
}
|
|
2284
|
+
}
|
|
2285
|
+
}
|
|
2286
|
+
}
|
|
2287
|
+
}
|
|
2288
|
+
// Check for conflicts: multiple different element types or multiple different IDs
|
|
2289
|
+
if (allElementTypes.size > 1) {
|
|
2290
|
+
const elementList = Array.from(allElementTypes);
|
|
2291
|
+
return {
|
|
2292
|
+
isValid: false,
|
|
2293
|
+
errorType: 'ELEMENT_CONFLICT',
|
|
2294
|
+
errorMessage: `Cannot combine different element types in compound selector: ${elementList.join(', ')}`,
|
|
2295
|
+
conflictingSelectors: [] // We could collect the actual selector objects if needed
|
|
2296
|
+
};
|
|
2297
|
+
}
|
|
2298
|
+
if (allIdValues.size > 1) {
|
|
2299
|
+
const idList = Array.from(allIdValues);
|
|
2300
|
+
return {
|
|
2301
|
+
isValid: false,
|
|
2302
|
+
errorType: 'ID_CONFLICT',
|
|
2303
|
+
errorMessage: `Cannot combine different ID selectors in compound selector: ${idList.join(', ')}`,
|
|
2304
|
+
conflictingSelectors: [] // We could collect the actual selector objects if needed
|
|
2305
|
+
};
|
|
2306
|
+
}
|
|
2307
|
+
}
|
|
2308
|
+
else {
|
|
2309
|
+
// Original validation for standalone :is() without context
|
|
2310
|
+
const elementTypes = new Set();
|
|
2311
|
+
const idValues = new Set();
|
|
2312
|
+
for (const selector of selectors) {
|
|
2313
|
+
if (isNode(selector, 'BasicSelector')) {
|
|
2314
|
+
if (selector.isTag) {
|
|
2315
|
+
elementTypes.add(selector.value.toLowerCase());
|
|
2316
|
+
}
|
|
2317
|
+
if (selector.isId) {
|
|
2318
|
+
idValues.add(selector.value);
|
|
2319
|
+
}
|
|
2320
|
+
}
|
|
2321
|
+
else if (isNode(selector, 'CompoundSelector')) {
|
|
2322
|
+
for (const child of selector.value) {
|
|
2323
|
+
if (isNode(child, 'BasicSelector')) {
|
|
2324
|
+
if (child.isTag) {
|
|
2325
|
+
elementTypes.add(child.value.toLowerCase());
|
|
2326
|
+
}
|
|
2327
|
+
if (child.isId) {
|
|
2328
|
+
idValues.add(child.value);
|
|
2329
|
+
}
|
|
2330
|
+
}
|
|
2331
|
+
}
|
|
2332
|
+
}
|
|
2333
|
+
}
|
|
2334
|
+
// If we'd have multiple different element types or IDs, fail validation
|
|
2335
|
+
if (elementTypes.size > 1) {
|
|
2336
|
+
const elementList = Array.from(elementTypes);
|
|
2337
|
+
return {
|
|
2338
|
+
isValid: false,
|
|
2339
|
+
errorType: 'ELEMENT_CONFLICT',
|
|
2340
|
+
errorMessage: `Cannot combine different element types in :is(): ${elementList.join(', ')}`,
|
|
2341
|
+
conflictingSelectors: [] // We could collect the actual selectors if needed
|
|
2342
|
+
};
|
|
2343
|
+
}
|
|
2344
|
+
if (idValues.size > 1) {
|
|
2345
|
+
const idList = Array.from(idValues);
|
|
2346
|
+
return {
|
|
2347
|
+
isValid: false,
|
|
2348
|
+
errorType: 'ID_CONFLICT',
|
|
2349
|
+
errorMessage: `Cannot combine different ID selectors in :is(): ${idList.join(', ')}`,
|
|
2350
|
+
conflictingSelectors: [] // We could collect the actual selectors if needed
|
|
2351
|
+
};
|
|
2352
|
+
}
|
|
2353
|
+
}
|
|
2354
|
+
return { isValid: true };
|
|
2355
|
+
}
|
|
2356
|
+
/**
|
|
2357
|
+
* Checks if extending the target would cross an ampersand boundary
|
|
2358
|
+
* This is simpler than the old analyzeAmpersandBoundary - we just check if:
|
|
2359
|
+
* 1. Selector contains ampersands with resolved values
|
|
2360
|
+
* 2. Target would match the resolved form of those ampersands
|
|
2361
|
+
* @param selector - The selector containing potential ampersands
|
|
2362
|
+
* @param target - The target selector being extended
|
|
2363
|
+
* @returns Information about ampersand boundary crossing
|
|
2364
|
+
*/
|
|
2365
|
+
/**
|
|
2366
|
+
* True when the selector is entirely "implicit & + rest" (every list item is a complex selector
|
|
2367
|
+
* that starts with implicit ampersand + combinator), or a single ComplexSelector that starts
|
|
2368
|
+
* that way. In that case, any match of the find in the resolved form is "only within ampersand".
|
|
2369
|
+
*/
|
|
2370
|
+
function selectorIsEntirelyImplicitAmpersandLeading(selector) {
|
|
2371
|
+
const checkItem = (item) => {
|
|
2372
|
+
if (!isNode(item, 'ComplexSelector') || item.value.length < 2) {
|
|
2373
|
+
return false;
|
|
2374
|
+
}
|
|
2375
|
+
const [first, second] = item.value;
|
|
2376
|
+
return (isNode(first, 'Ampersand')
|
|
2377
|
+
&& first.hasFlag(F_IMPLICIT_AMPERSAND)
|
|
2378
|
+
&& isNode(second, 'Combinator'));
|
|
2379
|
+
};
|
|
2380
|
+
if (isNode(selector, 'SelectorList')) {
|
|
2381
|
+
const list = selector.value;
|
|
2382
|
+
if (!Array.isArray(list) || list.length === 0) {
|
|
2383
|
+
return false;
|
|
2384
|
+
}
|
|
2385
|
+
return list.every(item => checkItem(item));
|
|
2386
|
+
}
|
|
2387
|
+
return checkItem(selector);
|
|
2388
|
+
}
|
|
2389
|
+
function checkAmpersandCrossingDuringExtension(selector, target) {
|
|
2390
|
+
// When the selector is entirely "implicit & + rest" *and* it's a SelectorList with more than
|
|
2391
|
+
// one item (e.g. "& .b, & .a" or "& .a, & .c"), any match in the resolved form is "only within
|
|
2392
|
+
// ampersand" — the parent should carry the extend. Single-item "& .a" is handled by the loop
|
|
2393
|
+
// below (replaceAmpersandWithEmpty leaves ".a" which matches, so we don't return crossed).
|
|
2394
|
+
if (isNode(selector, 'SelectorList')
|
|
2395
|
+
&& selector.value.length > 1
|
|
2396
|
+
&& selectorIsEntirelyImplicitAmpersandLeading(selector)) {
|
|
2397
|
+
const list = selector.value;
|
|
2398
|
+
const firstItem = list[0];
|
|
2399
|
+
if (firstItem && isNode(firstItem, 'ComplexSelector') && firstItem.value.length > 0) {
|
|
2400
|
+
const firstComp = firstItem.value[0];
|
|
2401
|
+
if (isNode(firstComp, 'Ampersand')) {
|
|
2402
|
+
const amp = firstComp;
|
|
2403
|
+
const resolved = amp.getResolvedSelector();
|
|
2404
|
+
if (resolved && !isNode(resolved, 'Nil')) {
|
|
2405
|
+
const resolvedSelector = replaceAmpersandWithItsValue(selector, amp);
|
|
2406
|
+
const resolvedComparison = selectorCompare(resolvedSelector, target);
|
|
2407
|
+
const selectorWithoutAmpersand = replaceAmpersandWithEmpty(selector, amp);
|
|
2408
|
+
const nonAmpersandComparison = selectorCompare(selectorWithoutAmpersand, target);
|
|
2409
|
+
if (resolvedComparison.locations.length > 0 && nonAmpersandComparison.locations.length === 0) {
|
|
2410
|
+
return {
|
|
2411
|
+
crossed: true,
|
|
2412
|
+
ampersandNode: amp,
|
|
2413
|
+
reason: 'selectorlist-implicit-leading',
|
|
2414
|
+
resolvedMatches: resolvedComparison.locations.length,
|
|
2415
|
+
nonAmpMatches: nonAmpersandComparison.locations.length
|
|
2416
|
+
};
|
|
2417
|
+
}
|
|
2418
|
+
}
|
|
2419
|
+
}
|
|
2420
|
+
}
|
|
2421
|
+
}
|
|
2422
|
+
// Find ampersands in the selector (reaches into compound/complex; SelectorList handled above)
|
|
2423
|
+
const ampersandNodes = findAmpersandsInSelector(selector);
|
|
2424
|
+
for (const { ampersand } of ampersandNodes) {
|
|
2425
|
+
const resolved = ampersand.getResolvedSelector();
|
|
2426
|
+
// Skip ampersands without resolved selectors
|
|
2427
|
+
if (!resolved || isNode(resolved, 'Nil')) {
|
|
2428
|
+
continue;
|
|
2429
|
+
}
|
|
2430
|
+
// Create resolved version by replacing ampersand with its resolved selector
|
|
2431
|
+
const resolvedSelector = replaceAmpersandWithItsValue(selector, ampersand);
|
|
2432
|
+
const resolvedComparison = selectorCompare(resolvedSelector, target);
|
|
2433
|
+
// Also check if target matches the selector without this ampersand
|
|
2434
|
+
const selectorWithoutAmpersand = replaceAmpersandWithEmpty(selector, ampersand);
|
|
2435
|
+
const nonAmpersandComparison = selectorCompare(selectorWithoutAmpersand, target);
|
|
2436
|
+
if (resolvedComparison.locations.length > 0 && nonAmpersandComparison.locations.length === 0) {
|
|
2437
|
+
// Target only matches when ampersand is resolved = boundary crossing
|
|
2438
|
+
return {
|
|
2439
|
+
crossed: true,
|
|
2440
|
+
ampersandNode: ampersand,
|
|
2441
|
+
reason: 'resolved-only',
|
|
2442
|
+
resolvedMatches: resolvedComparison.locations.length,
|
|
2443
|
+
nonAmpMatches: nonAmpersandComparison.locations.length
|
|
2444
|
+
};
|
|
2445
|
+
}
|
|
2446
|
+
}
|
|
2447
|
+
return { crossed: false };
|
|
2448
|
+
}
|
|
2449
|
+
/**
|
|
2450
|
+
* Finds all ampersand nodes in a selector
|
|
2451
|
+
* @param selector - The selector to search
|
|
2452
|
+
* @returns Array of ampersand nodes
|
|
2453
|
+
*/
|
|
2454
|
+
function findAmpersandsInSelector(selector) {
|
|
2455
|
+
const results = [];
|
|
2456
|
+
// Use the nodes() iterator to traverse all nodes recursively
|
|
2457
|
+
for (const node of selector.nodes()) {
|
|
2458
|
+
if (isNode(node, 'Ampersand')) {
|
|
2459
|
+
results.push({ ampersand: node });
|
|
2460
|
+
}
|
|
2461
|
+
}
|
|
2462
|
+
return results;
|
|
2463
|
+
}
|
|
2464
|
+
/**
|
|
2465
|
+
* Creates a version of the selector with the specified ampersand replaced by its resolved value
|
|
2466
|
+
* @param selector - The selector containing the ampersand
|
|
2467
|
+
* @param ampersand - The ampersand node to replace
|
|
2468
|
+
* @returns Selector with ampersand replaced by its resolved selector
|
|
2469
|
+
*/
|
|
2470
|
+
function replaceAmpersandWithItsValue(selector, ampersand) {
|
|
2471
|
+
const resolved = ampersand.getResolvedSelector();
|
|
2472
|
+
if (!resolved || isNode(resolved, 'Nil')) {
|
|
2473
|
+
return selector;
|
|
2474
|
+
}
|
|
2475
|
+
// Create a copy of the selector
|
|
2476
|
+
const selectorCopy = selector.copy();
|
|
2477
|
+
let resolvedSelector = resolved.copy();
|
|
2478
|
+
// If the resolved selector is a SelectorList, wrap it in :is() so it can be used as a single
|
|
2479
|
+
// selector component. This prevents invalid structures and matches Less output expectations.
|
|
2480
|
+
// Example: & .replace, & .c with parent .a, .b becomes :is(.a, .b) :is(.replace, .c)
|
|
2481
|
+
if (isNode(resolvedSelector, 'SelectorList')) {
|
|
2482
|
+
const isWrapper = isSelectorPseudo(resolvedSelector);
|
|
2483
|
+
isWrapper.generated = true; // Mark as generated so it can be optimized later if needed
|
|
2484
|
+
resolvedSelector = isWrapper;
|
|
2485
|
+
}
|
|
2486
|
+
// Find and replace ALL matching ampersand nodes (not just the first)
|
|
2487
|
+
// This is important for SelectorList targets like & .replace, & .c
|
|
2488
|
+
const nodesToReplace = [];
|
|
2489
|
+
const ampersandResolvedValue = ampersand.getResolvedSelector()?.valueOf();
|
|
2490
|
+
for (const node of selectorCopy.nodes()) {
|
|
2491
|
+
if (isNode(node, 'Ampersand') && node.getResolvedSelector()?.valueOf() === ampersandResolvedValue) {
|
|
2492
|
+
const parent = findParentOfNode(selectorCopy, node);
|
|
2493
|
+
if (parent) {
|
|
2494
|
+
nodesToReplace.push({ node: node, parent });
|
|
2495
|
+
}
|
|
2496
|
+
}
|
|
2497
|
+
}
|
|
2498
|
+
// Replace all matching ampersands
|
|
2499
|
+
for (const { node, parent } of nodesToReplace) {
|
|
2500
|
+
replaceNodeInParent(parent, node, resolvedSelector.copy());
|
|
2501
|
+
}
|
|
2502
|
+
return selectorCopy;
|
|
2503
|
+
}
|
|
2504
|
+
/**
|
|
2505
|
+
* Creates a version of the selector with the ampersand removed (for boundary analysis)
|
|
2506
|
+
* @param selector - The selector containing the ampersand
|
|
2507
|
+
* @param ampersand - The ampersand node to remove
|
|
2508
|
+
* @returns Selector with ampersand removed
|
|
2509
|
+
*/
|
|
2510
|
+
function replaceAmpersandWithEmpty(selector, ampersand) {
|
|
2511
|
+
// Create a copy of the selector
|
|
2512
|
+
const selectorCopy = selector.copy();
|
|
2513
|
+
const ampersandResolvedValue = ampersand.getResolvedSelector()?.valueOf();
|
|
2514
|
+
// Find and remove the ampersand node
|
|
2515
|
+
for (const node of selectorCopy.nodes()) {
|
|
2516
|
+
if (node === ampersand || (isNode(node, 'Ampersand')
|
|
2517
|
+
&& node.getResolvedSelector()?.valueOf() === ampersandResolvedValue)) {
|
|
2518
|
+
// We need to find the parent container and remove the ampersand
|
|
2519
|
+
const parent = findParentOfNode(selectorCopy, node);
|
|
2520
|
+
if (parent && (isNode(parent, 'CompoundSelector') || isNode(parent, 'ComplexSelector'))) {
|
|
2521
|
+
// Remove from compound/complex selector
|
|
2522
|
+
const idx = parent.value.indexOf(node);
|
|
2523
|
+
if (idx >= 0) {
|
|
2524
|
+
parent.value.splice(idx, 1);
|
|
2525
|
+
// If we removed a leading ampersand in a complex selector, also remove a following combinator
|
|
2526
|
+
// (implicit nesting uses `&` + generated whitespace combinator).
|
|
2527
|
+
const next = parent.value[idx];
|
|
2528
|
+
if (isNode(next, 'Combinator') && next.value === ' ') {
|
|
2529
|
+
parent.value.splice(idx, 1);
|
|
2530
|
+
}
|
|
2531
|
+
}
|
|
2532
|
+
}
|
|
2533
|
+
break;
|
|
2534
|
+
}
|
|
2535
|
+
}
|
|
2536
|
+
return selectorCopy;
|
|
2537
|
+
}
|
|
2538
|
+
/**
|
|
2539
|
+
* Handles extension when it crosses an ampersand boundary
|
|
2540
|
+
* @param selector - The original selector
|
|
2541
|
+
* @param target - The target being extended
|
|
2542
|
+
* @param extendWith - The selector to extend with
|
|
2543
|
+
* @param ampersandNode - The ampersand node that was crossed
|
|
2544
|
+
* @param matchResult - The match result
|
|
2545
|
+
* @returns Extended selector with ampersand resolved and hoisted to root
|
|
2546
|
+
*/
|
|
2547
|
+
function handleAmpersandBoundaryCrossing(selector, target, extendWith, ampersandNode, _matchResult) {
|
|
2548
|
+
const parentSelectorResolved = ampersandNode.getResolvedSelector();
|
|
2549
|
+
if (!parentSelectorResolved || isNode(parentSelectorResolved, 'Nil')) {
|
|
2550
|
+
throw new Error('Ampersand boundary crossing detected but ampersand has no resolved selector');
|
|
2551
|
+
}
|
|
2552
|
+
// Special handling for SelectorList: when crossing ampersand boundary, we need to replace
|
|
2553
|
+
// all ampersands in the list and wrap the inner SelectorList in :is() instead of distributing.
|
|
2554
|
+
// Example: & .replace, & .c with parent .a, .b should become :is(.a, .b) :is(.replace, .c)
|
|
2555
|
+
// not :is(.a, .b) .replace, :is(.a, .b) .c
|
|
2556
|
+
if (isNode(selector, 'SelectorList')) {
|
|
2557
|
+
const parentSelector = parentSelectorResolved;
|
|
2558
|
+
let parentWrapped = parentSelector.copy();
|
|
2559
|
+
if (isNode(parentWrapped, 'SelectorList')) {
|
|
2560
|
+
parentWrapped = isSelectorPseudo(parentWrapped);
|
|
2561
|
+
parentWrapped.generated = true;
|
|
2562
|
+
}
|
|
2563
|
+
// Extract nested selectors directly from each selector-list item:
|
|
2564
|
+
// "& .replace, & .c" -> ".replace, .c"
|
|
2565
|
+
const extractNestedFromItem = (item) => {
|
|
2566
|
+
if (!isNode(item, 'ComplexSelector')) {
|
|
2567
|
+
return item.copy();
|
|
2568
|
+
}
|
|
2569
|
+
const parts = item.value;
|
|
2570
|
+
if (parts.length === 0 || !isNode(parts[0], 'Ampersand')) {
|
|
2571
|
+
return item.copy();
|
|
2572
|
+
}
|
|
2573
|
+
let start = 1;
|
|
2574
|
+
if (parts[start] && isNode(parts[start], 'Combinator')) {
|
|
2575
|
+
start += 1;
|
|
2576
|
+
}
|
|
2577
|
+
const tail = parts.slice(start).filter(p => isNode(p, 'Selector') || isNode(p, 'Combinator'));
|
|
2578
|
+
if (tail.length === 0) {
|
|
2579
|
+
return null;
|
|
2580
|
+
}
|
|
2581
|
+
if (tail.length === 1 && isNode(tail[0], 'Selector')) {
|
|
2582
|
+
return tail[0].copy();
|
|
2583
|
+
}
|
|
2584
|
+
return ComplexSelector.create(tail).inherit(item);
|
|
2585
|
+
};
|
|
2586
|
+
let nestedItems = selector.value
|
|
2587
|
+
.map(extractNestedFromItem)
|
|
2588
|
+
.filter((s) => !!s);
|
|
2589
|
+
// Ensure we have at least one nested item
|
|
2590
|
+
if (nestedItems.length === 0) {
|
|
2591
|
+
nestedItems = selector.value.map(item => item.copy());
|
|
2592
|
+
}
|
|
2593
|
+
// Wrap the inner SelectorList in :is() to match Less expectations
|
|
2594
|
+
const innerList = SelectorList.create(nestedItems);
|
|
2595
|
+
const innerWrapped = isSelectorPseudo(innerList);
|
|
2596
|
+
innerWrapped.generated = true;
|
|
2597
|
+
// Create the combined selector: :is(parent) :is(inner)
|
|
2598
|
+
const combined = ComplexSelector.create([
|
|
2599
|
+
parentWrapped,
|
|
2600
|
+
Combinator.create(' '),
|
|
2601
|
+
innerWrapped
|
|
2602
|
+
]).inherit(selector);
|
|
2603
|
+
// Step 2: Extend the combined selector (skip ampersand check to prevent recursion)
|
|
2604
|
+
const extendedSelector = extendSelector(combined, target, extendWith, false, true, false);
|
|
2605
|
+
// Step 3: Mark for hoisting to root
|
|
2606
|
+
const hoisted = markSelectorForHoisting(extendedSelector);
|
|
2607
|
+
const hoistedList = SelectorList.create([hoisted, extendWith.copy(true)]).inherit(hoisted);
|
|
2608
|
+
hoistedList.hoistToRoot = true;
|
|
2609
|
+
return hoistedList;
|
|
2610
|
+
}
|
|
2611
|
+
// Step 1: Replace the ampersand with its resolved selector
|
|
2612
|
+
const resolvedSelector = replaceAmpersandWithItsValue(selector, ampersandNode);
|
|
2613
|
+
// Step 2: Extend the resolved selector (skip ampersand check to prevent recursion)
|
|
2614
|
+
const extendedSelector = extendSelector(resolvedSelector, target, extendWith, false, true, false);
|
|
2615
|
+
// Step 3: Mark for hoisting to root
|
|
2616
|
+
return markSelectorForHoisting(extendedSelector);
|
|
2617
|
+
}
|
|
2618
|
+
/**
|
|
2619
|
+
* Finds the parent container of a specific node
|
|
2620
|
+
* @param root - The root selector to search in
|
|
2621
|
+
* @param targetNode - The node to find the parent of
|
|
2622
|
+
* @returns The parent container or null if not found
|
|
2623
|
+
*/
|
|
2624
|
+
function findParentOfNode(root, targetNode) {
|
|
2625
|
+
for (const node of root.nodes()) {
|
|
2626
|
+
if (isNode(node, 'CompoundSelector') || isNode(node, 'ComplexSelector') || isNode(node, 'SelectorList')) {
|
|
2627
|
+
for (let i = 0; i < node.value.length; i++) {
|
|
2628
|
+
if (node.value[i] === targetNode) {
|
|
2629
|
+
return node;
|
|
2630
|
+
}
|
|
2631
|
+
}
|
|
2632
|
+
}
|
|
2633
|
+
else if (isNode(node, 'PseudoSelector') && node.value.arg === targetNode) {
|
|
2634
|
+
return node;
|
|
2635
|
+
}
|
|
2636
|
+
}
|
|
2637
|
+
return null;
|
|
2638
|
+
}
|
|
2639
|
+
/**
|
|
2640
|
+
* Replaces a node within its parent container
|
|
2641
|
+
* @param parent - The parent container
|
|
2642
|
+
* @param oldNode - The node to replace
|
|
2643
|
+
* @param newNode - The replacement node
|
|
2644
|
+
*/
|
|
2645
|
+
function replaceNodeInParent(parent, oldNode, newNode) {
|
|
2646
|
+
if (isNode(parent, 'CompoundSelector') || isNode(parent, 'ComplexSelector') || isNode(parent, 'SelectorList')) {
|
|
2647
|
+
for (let i = 0; i < parent.value.length; i++) {
|
|
2648
|
+
if (parent.value[i] === oldNode) {
|
|
2649
|
+
parent.value[i] = newNode;
|
|
2650
|
+
break;
|
|
2651
|
+
}
|
|
2652
|
+
}
|
|
2653
|
+
}
|
|
2654
|
+
else if (isNode(parent, 'PseudoSelector') && parent.value.arg === oldNode) {
|
|
2655
|
+
parent.value.arg = newNode;
|
|
2656
|
+
}
|
|
2657
|
+
}
|
|
2658
|
+
/**
|
|
2659
|
+
* Marks a selector for hoisting to root by setting hoistToRoot option
|
|
2660
|
+
* @param selector - The selector to mark for hoisting
|
|
2661
|
+
* @returns Selector marked for hoisting
|
|
2662
|
+
*/
|
|
2663
|
+
function markSelectorForHoisting(selector) {
|
|
2664
|
+
// Clone the selector and set hoistToRoot option
|
|
2665
|
+
const hoistedSelector = selector.copy();
|
|
2666
|
+
hoistedSelector.hoistToRoot = true;
|
|
2667
|
+
return hoistedSelector;
|
|
2668
|
+
}
|
|
2669
|
+
/**
|
|
2670
|
+
* Optimizes unnecessary standalone :is() wrappers that contain a single selector.
|
|
2671
|
+
* Removes :is() when it wraps only one selector and was generated during compilation.
|
|
2672
|
+
* Example: :is(.a) → .a (when generated)
|
|
2673
|
+
* Does NOT optimize :is(.a, .b) (multiple selectors) or :is() in compound selectors.
|
|
2674
|
+
* @param selector - The selector to check for optimization
|
|
2675
|
+
* @returns Optimized selector or original if no optimization needed
|
|
2676
|
+
*/
|
|
2677
|
+
// Removed unused function: optimizeUnnecessaryIsWrapper
|
|
2678
|
+
// This was only used by flattenGeneratedIsInSelector, which has been removed.
|
|
2679
|
+
// All :is() optimization and flattening is now handled in createProcessedSelector.
|
|
2680
|
+
// Removed unused functions: isValidCompoundSelector, createValidatedCompoundSelector
|
|
2681
|
+
// isValidCompoundSelector was never called - validateCompoundSelector has its own implementation
|
|
2682
|
+
// createValidatedCompoundSelector was never called - only createValidatedCompoundSelectorWithErrors (which throws) is used
|
|
2683
|
+
/**
|
|
2684
|
+
* Creates a compound selector with validation that throws errors on conflicts
|
|
2685
|
+
* @param components - Array of selectors to combine
|
|
2686
|
+
* @param inheritFrom - Selector to inherit properties from
|
|
2687
|
+
* @param context - Context information for error reporting
|
|
2688
|
+
* @returns Valid compound selector
|
|
2689
|
+
* @throws ExtendError if validation fails
|
|
2690
|
+
*/
|
|
2691
|
+
function createValidatedCompoundSelectorWithErrors(components, inheritFrom, context) {
|
|
2692
|
+
const validation = validateCompoundSelector(components);
|
|
2693
|
+
if (!validation.isValid) {
|
|
2694
|
+
throw new ExtendError(validation.errorType, validation.errorMessage, context);
|
|
2695
|
+
}
|
|
2696
|
+
const compound = CompoundSelector.create(components);
|
|
2697
|
+
return compound.inherit(inheritFrom);
|
|
2698
|
+
}
|
|
2699
|
+
/**
|
|
2700
|
+
* Enhanced validation that returns detailed error information
|
|
2701
|
+
*/
|
|
2702
|
+
function validateCompoundSelector(components) {
|
|
2703
|
+
const elementTypes = new Set();
|
|
2704
|
+
const idValues = new Set();
|
|
2705
|
+
for (const component of components) {
|
|
2706
|
+
if (isNode(component, 'BasicSelector')) {
|
|
2707
|
+
if (component.isTag) {
|
|
2708
|
+
elementTypes.add(component.value.toLowerCase());
|
|
2709
|
+
}
|
|
2710
|
+
if (component.isId) {
|
|
2711
|
+
idValues.add(component.value);
|
|
2712
|
+
}
|
|
2713
|
+
// Invalid if we have more than one different element type or ID
|
|
2714
|
+
if (elementTypes.size > 1) {
|
|
2715
|
+
const elementList = Array.from(elementTypes);
|
|
2716
|
+
return {
|
|
2717
|
+
isValid: false,
|
|
2718
|
+
errorType: 'ELEMENT_CONFLICT',
|
|
2719
|
+
errorMessage: `Cannot combine different element types: ${elementList.join(', ')}`,
|
|
2720
|
+
conflictingSelectors: [] // We could collect the actual selectors if needed
|
|
2721
|
+
};
|
|
2722
|
+
}
|
|
2723
|
+
if (idValues.size > 1) {
|
|
2724
|
+
const idList = Array.from(idValues);
|
|
2725
|
+
return {
|
|
2726
|
+
isValid: false,
|
|
2727
|
+
errorType: 'ID_CONFLICT',
|
|
2728
|
+
errorMessage: `Cannot combine different ID selectors: ${idList.join(', ')}`,
|
|
2729
|
+
conflictingSelectors: [] // We could collect the actual selectors if needed
|
|
2730
|
+
};
|
|
2731
|
+
}
|
|
2732
|
+
}
|
|
2733
|
+
else if (isNode(component, 'CompoundSelector')) {
|
|
2734
|
+
// Recursively check nested compounds
|
|
2735
|
+
const nestedValidation = validateCompoundSelector(component.value);
|
|
2736
|
+
if (!nestedValidation.isValid) {
|
|
2737
|
+
return nestedValidation;
|
|
2738
|
+
}
|
|
2739
|
+
}
|
|
2740
|
+
}
|
|
2741
|
+
return { isValid: true };
|
|
2742
|
+
}
|
|
2743
|
+
/**
|
|
2744
|
+
* Finds extends that should be processed next on a newly transformed selector.
|
|
2745
|
+
* This is part of the iterative extend process: when a selector is transformed
|
|
2746
|
+
* (e.g., .foo -> .foo, .ext3), we check if any selector in the result matches
|
|
2747
|
+
* other extend targets. If so, those extends should be processed on the new
|
|
2748
|
+
* selector, and we continue iterating until no more transforms occur or all
|
|
2749
|
+
* extends are exhausted.
|
|
2750
|
+
*
|
|
2751
|
+
* Example: .ext3 extends .foo -> .foo, .ext3. We then check if .foo (in the
|
|
2752
|
+
* result) matches .ext4:extend(.foo), and if so, process that extend on
|
|
2753
|
+
* .foo, .ext3 to get .foo, .ext3, .ext4. This continues until exhausted.
|
|
2754
|
+
*
|
|
2755
|
+
* @param extendedSelector - The selector after transformation (e.g., .foo, .ext3)
|
|
2756
|
+
* @param allExtends - Array of all extends: [target, selectorWithExtend, partial, extendRoot, extendNode]
|
|
2757
|
+
* @param currentTarget - The target of the extend that just completed
|
|
2758
|
+
* @param currentSelectorWithExtend - The selector that just extended
|
|
2759
|
+
* @returns Array of extends to process next: [target, selectorWithExtend, partial, extendRoot, extendNode]
|
|
2760
|
+
* where target is the extendedSelector (the newly transformed selector to continue extending)
|
|
2761
|
+
*/
|
|
2762
|
+
export function findChainedExtends(extendedSelector, allExtends, currentTarget, currentSelectorWithExtend, originalSelector) {
|
|
2763
|
+
const chained = [];
|
|
2764
|
+
// (debug log removed)
|
|
2765
|
+
// Only check SelectorList results (when we get .foo, .ext3 from extending .foo with .ext3)
|
|
2766
|
+
if (!isNode(extendedSelector, 'SelectorList')) {
|
|
2767
|
+
return chained;
|
|
2768
|
+
}
|
|
2769
|
+
// Check each selector in the list against all other extends
|
|
2770
|
+
// Only chain extends that target selectors that were in the original ruleset selector
|
|
2771
|
+
const originalSelectors = isNode(originalSelector, 'SelectorList')
|
|
2772
|
+
? originalSelector.value
|
|
2773
|
+
: [originalSelector];
|
|
2774
|
+
const originalSelectorValues = new Set(originalSelectors.map(s => s.valueOf()));
|
|
2775
|
+
for (const selectorInList of extendedSelector.value) {
|
|
2776
|
+
// Chain based on NEW selectors produced by the extend.
|
|
2777
|
+
//
|
|
2778
|
+
// If we chain on selectors that were already present in the original selector,
|
|
2779
|
+
// we can reorder independent extends that share the same target (e.g. `.foo:extend(.clearfix all)`
|
|
2780
|
+
// and `.bar:extend(.clearfix all)`), causing `.bar` to be applied during `.foo` processing.
|
|
2781
|
+
//
|
|
2782
|
+
// We only want chaining for "extend-of-an-extension" cases (targets that match newly-added selectors).
|
|
2783
|
+
if (originalSelectorValues.has(selectorInList.valueOf())) {
|
|
2784
|
+
continue;
|
|
2785
|
+
}
|
|
2786
|
+
for (const [otherTarget, otherSelectorWithExtend, otherPartial, otherExtendRoot, otherExtendNode] of allExtends) {
|
|
2787
|
+
// Skip if this is the same extend we just processed
|
|
2788
|
+
if (otherTarget.valueOf() === currentTarget.valueOf()
|
|
2789
|
+
&& otherSelectorWithExtend.valueOf() === currentSelectorWithExtend.valueOf()) {
|
|
2790
|
+
continue;
|
|
2791
|
+
}
|
|
2792
|
+
// Check if otherTarget matches selectorInList
|
|
2793
|
+
const otherTargetSelectors = isNode(otherTarget, 'SelectorList')
|
|
2794
|
+
? otherTarget.value
|
|
2795
|
+
: [otherTarget];
|
|
2796
|
+
for (const otherSingleTarget of otherTargetSelectors) {
|
|
2797
|
+
// Check if selectorInList equals otherSingleTarget (the target of another extend)
|
|
2798
|
+
// Combinators must match exactly (space vs + vs > etc.)
|
|
2799
|
+
if (selectorInList.valueOf() === otherSingleTarget.valueOf()) {
|
|
2800
|
+
// CRITICAL: Pass the individual selector that matched, not the entire extendedSelector
|
|
2801
|
+
// This ensures processExtend extracts the correct target (the one that matched)
|
|
2802
|
+
chained.push([selectorInList, otherSelectorWithExtend, otherPartial, otherExtendRoot, otherExtendNode]);
|
|
2803
|
+
// (debug log removed)
|
|
2804
|
+
break; // Only add once per otherTarget
|
|
2805
|
+
}
|
|
2806
|
+
}
|
|
2807
|
+
}
|
|
2808
|
+
}
|
|
2809
|
+
return chained;
|
|
2810
|
+
}
|
|
2811
|
+
/**
|
|
2812
|
+
* Applies an extension at a specific location within a selector tree
|
|
2813
|
+
* @param selector - The original selector
|
|
2814
|
+
* @param location - The location where to apply the extension
|
|
2815
|
+
* @param extendWith - The selector to extend with
|
|
2816
|
+
* @returns The modified selector with extension applied
|
|
2817
|
+
*/
|
|
2818
|
+
export function applyExtensionAtLocation(selector, location, extendWith) {
|
|
2819
|
+
const result = applyExtensionAtPath(selector, location.path, location.matchedNode, extendWith, location.extensionType, location, undefined);
|
|
2820
|
+
return result;
|
|
2821
|
+
}
|
|
2822
|
+
/**
|
|
2823
|
+
* Recursively applies an extension at a specific path.
|
|
2824
|
+
* @param contextSelector - When wrapping inside a compound, the compound that will contain the :is(); used for element/ID conflict validation.
|
|
2825
|
+
*/
|
|
2826
|
+
function applyExtensionAtPath(current, path, matchedNode, extendWith, extensionType, location, contextSelector) {
|
|
2827
|
+
const isArgMatch = path.includes('arg');
|
|
2828
|
+
// When at root compound with a contiguous slice to wrap, replace that slice with :is(matched, extendWith)
|
|
2829
|
+
if (path.length === 0 && isNode(current, 'CompoundSelector') && location?.contiguousCompoundRange) {
|
|
2830
|
+
const [start, end] = location.contiguousCompoundRange;
|
|
2831
|
+
const wrapped = createValidatedIsWrapperWithErrors([matchedNode, extendWith], matchedNode, undefined, undefined);
|
|
2832
|
+
const newValue = [
|
|
2833
|
+
...current.value.slice(0, start),
|
|
2834
|
+
wrapped,
|
|
2835
|
+
...current.value.slice(end)
|
|
2836
|
+
];
|
|
2837
|
+
return CompoundSelector.create(newValue).inherit(current);
|
|
2838
|
+
}
|
|
2839
|
+
// When at root compound with non-contiguous match indices, replace those indices with :is(matched, extendWith)
|
|
2840
|
+
if (path.length === 0 && isNode(current, 'CompoundSelector') && location?.compoundMatchIndices?.length) {
|
|
2841
|
+
const indicesSet = new Set(location.compoundMatchIndices);
|
|
2842
|
+
const wrapped = createValidatedIsWrapperWithErrors([matchedNode, extendWith], matchedNode, undefined, undefined);
|
|
2843
|
+
const newValue = [];
|
|
2844
|
+
let wrappedAdded = false;
|
|
2845
|
+
for (let i = 0; i < current.value.length; i++) {
|
|
2846
|
+
if (indicesSet.has(i)) {
|
|
2847
|
+
if (!wrappedAdded) {
|
|
2848
|
+
newValue.push(wrapped);
|
|
2849
|
+
wrappedAdded = true;
|
|
2850
|
+
}
|
|
2851
|
+
}
|
|
2852
|
+
else {
|
|
2853
|
+
newValue.push(current.value[i]);
|
|
2854
|
+
}
|
|
2855
|
+
}
|
|
2856
|
+
return CompoundSelector.create(newValue).inherit(current);
|
|
2857
|
+
}
|
|
2858
|
+
if (path.length === 0) {
|
|
2859
|
+
// We've reached the target location
|
|
2860
|
+
return applyExtension(current, matchedNode, extendWith, extensionType, contextSelector);
|
|
2861
|
+
}
|
|
2862
|
+
const [nextSegment, ...remainingPath] = path;
|
|
2863
|
+
if (isNode(current, 'SelectorList')) {
|
|
2864
|
+
// For selector lists, we need special handling
|
|
2865
|
+
if (remainingPath.length === 0) {
|
|
2866
|
+
// We're targeting a specific item in the list
|
|
2867
|
+
const index = nextSegment;
|
|
2868
|
+
const item = current.value[index];
|
|
2869
|
+
// Less parity: for targets like `:is(.a,.b):after` extending `.a`,
|
|
2870
|
+
// append to the `:is()` argument list (`:is(.a,.b,.x):after`) instead
|
|
2871
|
+
// of wrapping the single matched item (`:is(.a,.x,.b):after`).
|
|
2872
|
+
// Keep this extremely narrow: only when the :is() pseudo has trailing
|
|
2873
|
+
// components in its parent compound selector.
|
|
2874
|
+
if (extensionType === 'wrap'
|
|
2875
|
+
&& item
|
|
2876
|
+
&& isNode(item, 'SimpleSelector')
|
|
2877
|
+
&& isNode(matchedNode, 'SimpleSelector')
|
|
2878
|
+
&& isNode(current.parent, 'PseudoSelector')
|
|
2879
|
+
&& current.parent.value.name === ':is'
|
|
2880
|
+
&& isNode(current.parent.parent, 'CompoundSelector')) {
|
|
2881
|
+
const parentCompound = current.parent.parent;
|
|
2882
|
+
const pseudoIndex = parentCompound.value.findIndex(n => n === current.parent);
|
|
2883
|
+
const trailing = pseudoIndex >= 0 ? parentCompound.value.slice(pseudoIndex + 1) : [];
|
|
2884
|
+
// Only force append-to-:is() for pseudo tails like `:is(.a,.b):after`.
|
|
2885
|
+
// For structural tails like `.a:is(.b,.c).d`, preserve positional wrap semantics.
|
|
2886
|
+
const hasPseudoOnlyTail = trailing.length > 0 && trailing.every(n => isNode(n, 'PseudoSelector'));
|
|
2887
|
+
if (hasPseudoOnlyTail) {
|
|
2888
|
+
const additions = (isNode(extendWith, 'PseudoSelector') && extendWith.value.name === ':is')
|
|
2889
|
+
? extractSelectorsFromIs(extendWith)
|
|
2890
|
+
: [extendWith];
|
|
2891
|
+
const newValue = [...current.value];
|
|
2892
|
+
let changed = false;
|
|
2893
|
+
for (const add of additions) {
|
|
2894
|
+
if (!newValue.some(s => s.valueOf() === add.valueOf())) {
|
|
2895
|
+
newValue.push(add);
|
|
2896
|
+
changed = true;
|
|
2897
|
+
}
|
|
2898
|
+
}
|
|
2899
|
+
return changed ? SelectorList.create(newValue).inherit(current) : current;
|
|
2900
|
+
}
|
|
2901
|
+
}
|
|
2902
|
+
// For wrap, wrap the matched list item in :is(matched, extendWith) rather than replacing with extendWith
|
|
2903
|
+
if (extensionType === 'wrap' && item) {
|
|
2904
|
+
const newValue = [...current.value];
|
|
2905
|
+
const wrapped = applyExtension(item, matchedNode, extendWith, 'wrap', undefined);
|
|
2906
|
+
newValue[index] = wrapped;
|
|
2907
|
+
return SelectorList.create(newValue).inherit(current);
|
|
2908
|
+
}
|
|
2909
|
+
// For extend operations (replace/append), add to the list rather than replace the matched item
|
|
2910
|
+
if (extensionType === 'wrap') {
|
|
2911
|
+
const newValue = [...current.value];
|
|
2912
|
+
newValue[index] = extendWith;
|
|
2913
|
+
return SelectorList.create(newValue).inherit(current);
|
|
2914
|
+
}
|
|
2915
|
+
else {
|
|
2916
|
+
// For extend operations (both 'replace' and 'append'), add to the list
|
|
2917
|
+
// If extendWith is a :is(), append its argument selectors instead of nesting.
|
|
2918
|
+
const additions = (isNode(extendWith, 'PseudoSelector') && extendWith.value.name === ':is')
|
|
2919
|
+
? extractSelectorsFromIs(extendWith)
|
|
2920
|
+
: [extendWith];
|
|
2921
|
+
const newValue = [...current.value];
|
|
2922
|
+
let changed = false;
|
|
2923
|
+
for (const add of additions) {
|
|
2924
|
+
const extensionExists = newValue.some(item => item.valueOf() === add.valueOf());
|
|
2925
|
+
if (!extensionExists) {
|
|
2926
|
+
newValue.push(add);
|
|
2927
|
+
changed = true;
|
|
2928
|
+
}
|
|
2929
|
+
}
|
|
2930
|
+
const result = changed ? SelectorList.create(newValue).inherit(current) : current;
|
|
2931
|
+
return result;
|
|
2932
|
+
}
|
|
2933
|
+
}
|
|
2934
|
+
else {
|
|
2935
|
+
// Navigate deeper into the list
|
|
2936
|
+
const index = nextSegment;
|
|
2937
|
+
const newValue = [...current.value];
|
|
2938
|
+
newValue[index] = applyExtensionAtPath(newValue[index], remainingPath, matchedNode, extendWith, extensionType, undefined, undefined);
|
|
2939
|
+
return SelectorList.create(newValue).inherit(current);
|
|
2940
|
+
}
|
|
2941
|
+
}
|
|
2942
|
+
if (isNode(current, 'CompoundSelector')) {
|
|
2943
|
+
const index = nextSegment;
|
|
2944
|
+
const newValue = [...current.value];
|
|
2945
|
+
// When we recurse into a component that will be wrapped, pass this compound as context for element/ID validation.
|
|
2946
|
+
const childContext = remainingPath.length === 0 && extensionType === 'wrap' ? current : undefined;
|
|
2947
|
+
newValue[index] = applyExtensionAtPath(newValue[index], remainingPath, matchedNode, extendWith, extensionType, undefined, childContext);
|
|
2948
|
+
return CompoundSelector.create(newValue).inherit(current);
|
|
2949
|
+
}
|
|
2950
|
+
if (isNode(current, 'ComplexSelector')) {
|
|
2951
|
+
const index = nextSegment;
|
|
2952
|
+
const newValue = [...current.value];
|
|
2953
|
+
newValue[index] = applyExtensionAtPath(newValue[index], remainingPath, matchedNode, extendWith, extensionType, undefined, undefined);
|
|
2954
|
+
return ComplexSelector.create(newValue).inherit(current);
|
|
2955
|
+
}
|
|
2956
|
+
if (isNode(current, 'PseudoSelector') && nextSegment === 'arg') {
|
|
2957
|
+
const arg = current.value.arg;
|
|
2958
|
+
// Special handling for pseudo-selector arguments
|
|
2959
|
+
if (remainingPath.length === 0) {
|
|
2960
|
+
// Direct match in the argument - create a list or extend existing list
|
|
2961
|
+
let newArg;
|
|
2962
|
+
if (isNode(arg, 'SelectorList')) {
|
|
2963
|
+
const newSelectors = [...arg.value, extendWith];
|
|
2964
|
+
newArg = SelectorList.create(newSelectors).inherit(arg);
|
|
2965
|
+
}
|
|
2966
|
+
else {
|
|
2967
|
+
newArg = SelectorList.create([arg, extendWith]);
|
|
2968
|
+
}
|
|
2969
|
+
const processedArg = createProcessedSelector(newArg, true);
|
|
2970
|
+
const normalizedArg = isArray(processedArg) ? SelectorList.create(processedArg) : processedArg;
|
|
2971
|
+
const result = PseudoSelector.create({
|
|
2972
|
+
name: current.value.name,
|
|
2973
|
+
arg: normalizedArg
|
|
2974
|
+
}).inherit(current);
|
|
2975
|
+
return result;
|
|
2976
|
+
}
|
|
2977
|
+
else {
|
|
2978
|
+
// Navigate deeper into the argument
|
|
2979
|
+
const newArg = applyExtensionAtPath(arg, remainingPath, matchedNode, extendWith, extensionType, undefined, undefined);
|
|
2980
|
+
const processedArg = createProcessedSelector(newArg, true);
|
|
2981
|
+
const normalizedArg = isArray(processedArg) ? SelectorList.create(processedArg) : processedArg;
|
|
2982
|
+
const nestedResult = PseudoSelector.create({
|
|
2983
|
+
name: current.value.name,
|
|
2984
|
+
arg: normalizedArg
|
|
2985
|
+
}).inherit(current);
|
|
2986
|
+
return nestedResult;
|
|
2987
|
+
}
|
|
2988
|
+
}
|
|
2989
|
+
throw new Error(`Unable to apply extension at path: ${path.join('.')}`);
|
|
2990
|
+
}
|
|
2991
|
+
/**
|
|
2992
|
+
* Applies the actual extension based on the extension type.
|
|
2993
|
+
* @param contextSelector - When wrapping inside a compound, the compound that will contain the :is(); used for element/ID conflict validation.
|
|
2994
|
+
*/
|
|
2995
|
+
function applyExtension(current, matchedNode, extendWith, extensionType, contextSelector) {
|
|
2996
|
+
switch (extensionType) {
|
|
2997
|
+
case 'replace':
|
|
2998
|
+
return extendWith;
|
|
2999
|
+
case 'append':
|
|
3000
|
+
// For append within a selector list context, we add to the current list
|
|
3001
|
+
if (isNode(current, 'SelectorList')) {
|
|
3002
|
+
const newSelectors = [...current.value, extendWith];
|
|
3003
|
+
return SelectorList.create(newSelectors).inherit(current);
|
|
3004
|
+
}
|
|
3005
|
+
else {
|
|
3006
|
+
// For append at the selector level, create a list with the current and extension
|
|
3007
|
+
return SelectorList.create([current, extendWith]);
|
|
3008
|
+
}
|
|
3009
|
+
case 'wrap':
|
|
3010
|
+
if (isNode(current, 'PseudoSelector') && current.value.name === ':is' && current.value.arg) {
|
|
3011
|
+
const existing = extractSelectorsFromIs(current);
|
|
3012
|
+
const additions = extractSelectorsFromIs(extendWith);
|
|
3013
|
+
const merged = [...existing];
|
|
3014
|
+
for (const add of additions) {
|
|
3015
|
+
if (!merged.some(s => s.valueOf() === add.valueOf())) {
|
|
3016
|
+
merged.push(add);
|
|
3017
|
+
}
|
|
3018
|
+
}
|
|
3019
|
+
return createValidatedIsWrapperWithErrors(merged, current, contextSelector, undefined);
|
|
3020
|
+
}
|
|
3021
|
+
// Same rule as everywhere: extend = append extendWith at end of list. Reuse createExtendedSelectorList
|
|
3022
|
+
// so order (extendOrderMap) and flattening apply; then wrap that list in :is().
|
|
3023
|
+
// Works for both single selector (current → [current, extendWith]) and already-extended :is()
|
|
3024
|
+
// (e.g. :is(.clearfix, .foo) + .bar → :is(.clearfix, .foo, .bar)) without branching on :is().
|
|
3025
|
+
const wrapExisting = extractSelectorsFromIs(current);
|
|
3026
|
+
const wrapOrdered = createExtendedSelectorList([...wrapExisting, extendWith], current);
|
|
3027
|
+
const wrapSelectors = wrapOrdered.value;
|
|
3028
|
+
return createValidatedIsWrapperWithErrors(wrapSelectors, current, contextSelector, undefined);
|
|
3029
|
+
default:
|
|
3030
|
+
throw new Error(`Unknown extension type: ${extensionType}`);
|
|
3031
|
+
}
|
|
3032
|
+
}
|
|
3033
|
+
//# sourceMappingURL=extend.js.map
|