@jesscss/core 2.0.0-alpha.1

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