@jesscss/core 2.0.0-alpha.4 → 2.0.0-alpha.6

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