selector.js 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635
  1. 'use strict'
  2. const parser = require('postcss-selector-parser')
  3. const { default: nthCheck } = require('nth-check')
  4. const { getAttribute, isVElement } = require('.')
  5. /**
  6. * @typedef {object} VElementSelector
  7. * @property {(element: VElement)=>boolean} test
  8. */
  9. module.exports = {
  10. parseSelector
  11. }
  12. /**
  13. * Parses CSS selectors and returns an object with a function that tests VElement.
  14. * @param {string} selector CSS selector
  15. * @param {RuleContext} context - The rule context.
  16. * @returns {VElementSelector}
  17. */
  18. function parseSelector(selector, context) {
  19. let astSelector
  20. try {
  21. astSelector = parser().astSync(selector)
  22. } catch (error) {
  23. context.report({
  24. loc: { line: 0, column: 0 },
  25. message: `Cannot parse selector: ${selector}.`
  26. })
  27. return {
  28. test: () => false
  29. }
  30. }
  31. try {
  32. const test = selectorsToVElementMatcher(astSelector.nodes)
  33. return {
  34. test(element) {
  35. return test(element, null)
  36. }
  37. }
  38. } catch (error) {
  39. if (error instanceof SelectorError) {
  40. context.report({
  41. loc: { line: 0, column: 0 },
  42. message: error.message
  43. })
  44. return {
  45. test: () => false
  46. }
  47. }
  48. throw error
  49. }
  50. }
  51. class SelectorError extends Error {}
  52. /**
  53. * @typedef {(element: VElement, subject: VElement | null )=>boolean} VElementMatcher
  54. * @typedef {Exclude<parser.Selector['nodes'][number], {type:'comment'|'root'}>} ChildNode
  55. */
  56. /**
  57. * Convert nodes to VElementMatcher
  58. * @param {parser.Selector[]} selectorNodes
  59. * @returns {VElementMatcher}
  60. */
  61. function selectorsToVElementMatcher(selectorNodes) {
  62. const selectors = selectorNodes.map((n) =>
  63. selectorToVElementMatcher(cleanSelectorChildren(n))
  64. )
  65. return (element, subject) => selectors.some((sel) => sel(element, subject))
  66. }
  67. /**
  68. * @param {parser.Node|null} node
  69. * @returns {node is parser.Combinator}
  70. */
  71. function isDescendantCombinator(node) {
  72. return Boolean(node && node.type === 'combinator' && !node.value.trim())
  73. }
  74. /**
  75. * Clean and get the selector child nodes.
  76. * @param {parser.Selector} selector
  77. * @returns {ChildNode[]}
  78. */
  79. function cleanSelectorChildren(selector) {
  80. /** @type {ChildNode[]} */
  81. const nodes = []
  82. /** @type {ChildNode|null} */
  83. let last = null
  84. for (const node of selector.nodes) {
  85. if (node.type === 'root') {
  86. throw new SelectorError('Unexpected state type=root')
  87. }
  88. if (node.type === 'comment') {
  89. continue
  90. }
  91. if (
  92. (last == null || last.type === 'combinator') &&
  93. isDescendantCombinator(node)
  94. ) {
  95. // Ignore descendant combinator
  96. continue
  97. }
  98. if (isDescendantCombinator(last) && node.type === 'combinator') {
  99. // Replace combinator
  100. nodes.pop()
  101. }
  102. nodes.push(node)
  103. last = node
  104. }
  105. if (isDescendantCombinator(last)) {
  106. nodes.pop()
  107. }
  108. return nodes
  109. }
  110. /**
  111. * Convert Selector child nodes to VElementMatcher
  112. * @param {ChildNode[]} selectorChildren
  113. * @returns {VElementMatcher}
  114. */
  115. function selectorToVElementMatcher(selectorChildren) {
  116. const nodes = [...selectorChildren]
  117. let node = nodes.shift()
  118. /**
  119. * @type {VElementMatcher | null}
  120. */
  121. let result = null
  122. while (node) {
  123. if (node.type === 'combinator') {
  124. const combinator = node.value
  125. node = nodes.shift()
  126. if (!node) {
  127. throw new SelectorError(`Expected selector after '${combinator}'.`)
  128. }
  129. if (node.type === 'combinator') {
  130. throw new SelectorError(`Unexpected combinator '${node.value}'.`)
  131. }
  132. const right = nodeToVElementMatcher(node)
  133. result = combination(
  134. result ||
  135. // for :has()
  136. ((element, subject) => element === subject),
  137. combinator,
  138. right
  139. )
  140. } else {
  141. const sel = nodeToVElementMatcher(node)
  142. result = result ? compound(result, sel) : sel
  143. }
  144. node = nodes.shift()
  145. }
  146. if (!result) {
  147. throw new SelectorError(`Unexpected empty selector.`)
  148. }
  149. return result
  150. }
  151. /**
  152. * @param {VElementMatcher} left
  153. * @param {string} combinator
  154. * @param {VElementMatcher} right
  155. * @returns {VElementMatcher}
  156. */
  157. function combination(left, combinator, right) {
  158. switch (combinator.trim()) {
  159. case '': {
  160. // descendant
  161. return (element, subject) => {
  162. if (right(element, null)) {
  163. let parent = element.parent
  164. while (parent.type === 'VElement') {
  165. if (left(parent, subject)) {
  166. return true
  167. }
  168. parent = parent.parent
  169. }
  170. }
  171. return false
  172. }
  173. }
  174. case '>': {
  175. // child
  176. return (element, subject) => {
  177. if (right(element, null)) {
  178. const parent = element.parent
  179. if (parent.type === 'VElement') {
  180. return left(parent, subject)
  181. }
  182. }
  183. return false
  184. }
  185. }
  186. case '+': {
  187. // adjacent
  188. return (element, subject) => {
  189. if (right(element, null)) {
  190. const before = getBeforeElement(element)
  191. if (before) {
  192. return left(before, subject)
  193. }
  194. }
  195. return false
  196. }
  197. }
  198. case '~': {
  199. // sibling
  200. return (element, subject) => {
  201. if (right(element, null)) {
  202. for (const before of getBeforeElements(element)) {
  203. if (left(before, subject)) {
  204. return true
  205. }
  206. }
  207. }
  208. return false
  209. }
  210. }
  211. default: {
  212. throw new SelectorError(`Unknown combinator: ${combinator}.`)
  213. }
  214. }
  215. }
  216. /**
  217. * Convert node to VElementMatcher
  218. * @param {Exclude<parser.Node, {type:'combinator'|'comment'|'root'|'selector'}>} selector
  219. * @returns {VElementMatcher}
  220. */
  221. function nodeToVElementMatcher(selector) {
  222. switch (selector.type) {
  223. case 'attribute': {
  224. return attributeNodeToVElementMatcher(selector)
  225. }
  226. case 'class': {
  227. return classNameNodeToVElementMatcher(selector)
  228. }
  229. case 'id': {
  230. return identifierNodeToVElementMatcher(selector)
  231. }
  232. case 'tag': {
  233. return tagNodeToVElementMatcher(selector)
  234. }
  235. case 'universal': {
  236. return universalNodeToVElementMatcher(selector)
  237. }
  238. case 'pseudo': {
  239. return pseudoNodeToVElementMatcher(selector)
  240. }
  241. case 'nesting': {
  242. throw new SelectorError('Unsupported nesting selector.')
  243. }
  244. case 'string': {
  245. throw new SelectorError(`Unknown selector: ${selector.value}.`)
  246. }
  247. default: {
  248. throw new SelectorError(
  249. `Unknown selector: ${/** @type {any}*/ (selector).value}.`
  250. )
  251. }
  252. }
  253. }
  254. /**
  255. * Convert Attribute node to VElementMatcher
  256. * @param {parser.Attribute} selector
  257. * @returns {VElementMatcher}
  258. */
  259. function attributeNodeToVElementMatcher(selector) {
  260. const key = selector.attribute
  261. if (!selector.operator) {
  262. return (element) => getAttributeValue(element, key) != null
  263. }
  264. const value = selector.value || ''
  265. switch (selector.operator) {
  266. case '=': {
  267. return buildVElementMatcher(value, (attr, val) => attr === val)
  268. }
  269. case '~=': {
  270. // words
  271. return buildVElementMatcher(value, (attr, val) =>
  272. attr.split(/\s+/gu).includes(val)
  273. )
  274. }
  275. case '|=': {
  276. // immediately followed by hyphen
  277. return buildVElementMatcher(
  278. value,
  279. (attr, val) => attr === val || attr.startsWith(`${val}-`)
  280. )
  281. }
  282. case '^=': {
  283. // prefixed
  284. return buildVElementMatcher(value, (attr, val) => attr.startsWith(val))
  285. }
  286. case '$=': {
  287. // suffixed
  288. return buildVElementMatcher(value, (attr, val) => attr.endsWith(val))
  289. }
  290. case '*=': {
  291. // contains
  292. return buildVElementMatcher(value, (attr, val) => attr.includes(val))
  293. }
  294. default: {
  295. throw new SelectorError(`Unsupported operator: ${selector.operator}.`)
  296. }
  297. }
  298. /**
  299. * @param {string} selectorValue
  300. * @param {(attrValue:string, selectorValue: string)=>boolean} test
  301. * @returns {VElementMatcher}
  302. */
  303. function buildVElementMatcher(selectorValue, test) {
  304. const val = selector.insensitive
  305. ? selectorValue.toLowerCase()
  306. : selectorValue
  307. return (element) => {
  308. const attrValue = getAttributeValue(element, key)
  309. if (attrValue == null) {
  310. return false
  311. }
  312. return test(
  313. selector.insensitive ? attrValue.toLowerCase() : attrValue,
  314. val
  315. )
  316. }
  317. }
  318. }
  319. /**
  320. * Convert ClassName node to VElementMatcher
  321. * @param {parser.ClassName} selector
  322. * @returns {VElementMatcher}
  323. */
  324. function classNameNodeToVElementMatcher(selector) {
  325. const className = selector.value
  326. return (element) => {
  327. const attrValue = getAttributeValue(element, 'class')
  328. if (attrValue == null) {
  329. return false
  330. }
  331. return attrValue.split(/\s+/gu).includes(className)
  332. }
  333. }
  334. /**
  335. * Convert Identifier node to VElementMatcher
  336. * @param {parser.Identifier} selector
  337. * @returns {VElementMatcher}
  338. */
  339. function identifierNodeToVElementMatcher(selector) {
  340. const id = selector.value
  341. return (element) => {
  342. const attrValue = getAttributeValue(element, 'id')
  343. if (attrValue == null) {
  344. return false
  345. }
  346. return attrValue === id
  347. }
  348. }
  349. /**
  350. * Convert Tag node to VElementMatcher
  351. * @param {parser.Tag} selector
  352. * @returns {VElementMatcher}
  353. */
  354. function tagNodeToVElementMatcher(selector) {
  355. const name = selector.value
  356. return (element) => element.rawName === name
  357. }
  358. /**
  359. * Convert Universal node to VElementMatcher
  360. * @param {parser.Universal} _selector
  361. * @returns {VElementMatcher}
  362. */
  363. function universalNodeToVElementMatcher(_selector) {
  364. return () => true
  365. }
  366. /**
  367. * Convert Pseudo node to VElementMatcher
  368. * @param {parser.Pseudo} selector
  369. * @returns {VElementMatcher}
  370. */
  371. function pseudoNodeToVElementMatcher(selector) {
  372. const pseudo = selector.value
  373. switch (pseudo) {
  374. case ':not': {
  375. // https://developer.mozilla.org/en-US/docs/Web/CSS/:not
  376. const selectors = selectorsToVElementMatcher(selector.nodes)
  377. return (element, subject) => !selectors(element, subject)
  378. }
  379. case ':is':
  380. case ':where': {
  381. // https://developer.mozilla.org/en-US/docs/Web/CSS/:is
  382. // https://developer.mozilla.org/en-US/docs/Web/CSS/:where
  383. return selectorsToVElementMatcher(selector.nodes)
  384. }
  385. case ':has': {
  386. // https://developer.mozilla.org/en-US/docs/Web/CSS/:has
  387. return pseudoHasSelectorsToVElementMatcher(selector.nodes)
  388. }
  389. case ':empty': {
  390. // https://developer.mozilla.org/en-US/docs/Web/CSS/:empty
  391. return (element) =>
  392. element.children.every(
  393. (child) => child.type === 'VText' && !child.value.trim()
  394. )
  395. }
  396. case ':nth-child': {
  397. // https://developer.mozilla.org/en-US/docs/Web/CSS/:nth-child
  398. const nth = parseNth(selector)
  399. return buildPseudoNthVElementMatcher(nth)
  400. }
  401. case ':nth-last-child': {
  402. // https://developer.mozilla.org/en-US/docs/Web/CSS/:nth-last-child
  403. const nth = parseNth(selector)
  404. return buildPseudoNthVElementMatcher((index, length) =>
  405. nth(length - index - 1)
  406. )
  407. }
  408. case ':first-child': {
  409. // https://developer.mozilla.org/en-US/docs/Web/CSS/:first-child
  410. return buildPseudoNthVElementMatcher((index) => index === 0)
  411. }
  412. case ':last-child': {
  413. // https://developer.mozilla.org/en-US/docs/Web/CSS/:last-child
  414. return buildPseudoNthVElementMatcher(
  415. (index, length) => index === length - 1
  416. )
  417. }
  418. case ':only-child': {
  419. // https://developer.mozilla.org/en-US/docs/Web/CSS/:only-child
  420. return buildPseudoNthVElementMatcher(
  421. (index, length) => index === 0 && length === 1
  422. )
  423. }
  424. case ':nth-of-type': {
  425. // https://developer.mozilla.org/en-US/docs/Web/CSS/:nth-of-type
  426. const nth = parseNth(selector)
  427. return buildPseudoNthOfTypeVElementMatcher(nth)
  428. }
  429. case ':nth-last-of-type': {
  430. // https://developer.mozilla.org/en-US/docs/Web/CSS/:nth-last-of-type
  431. const nth = parseNth(selector)
  432. return buildPseudoNthOfTypeVElementMatcher((index, length) =>
  433. nth(length - index - 1)
  434. )
  435. }
  436. case ':first-of-type': {
  437. // https://developer.mozilla.org/en-US/docs/Web/CSS/:first-of-type
  438. return buildPseudoNthOfTypeVElementMatcher((index) => index === 0)
  439. }
  440. case ':last-of-type': {
  441. // https://developer.mozilla.org/en-US/docs/Web/CSS/:last-of-type
  442. return buildPseudoNthOfTypeVElementMatcher(
  443. (index, length) => index === length - 1
  444. )
  445. }
  446. case ':only-of-type': {
  447. // https://developer.mozilla.org/en-US/docs/Web/CSS/:only-of-type
  448. return buildPseudoNthOfTypeVElementMatcher(
  449. (index, length) => index === 0 && length === 1
  450. )
  451. }
  452. default: {
  453. throw new SelectorError(`Unsupported pseudo selector: ${pseudo}.`)
  454. }
  455. }
  456. }
  457. /**
  458. * Convert :has() selector nodes to VElementMatcher
  459. * @param {parser.Selector[]} selectorNodes
  460. * @returns {VElementMatcher}
  461. */
  462. function pseudoHasSelectorsToVElementMatcher(selectorNodes) {
  463. const selectors = selectorNodes.map((n) =>
  464. pseudoHasSelectorToVElementMatcher(n)
  465. )
  466. return (element, subject) => selectors.some((sel) => sel(element, subject))
  467. }
  468. /**
  469. * Convert :has() selector node to VElementMatcher
  470. * @param {parser.Selector} selector
  471. * @returns {VElementMatcher}
  472. */
  473. function pseudoHasSelectorToVElementMatcher(selector) {
  474. const nodes = cleanSelectorChildren(selector)
  475. const selectors = selectorToVElementMatcher(nodes)
  476. const firstNode = nodes[0]
  477. if (
  478. firstNode.type === 'combinator' &&
  479. (firstNode.value === '+' || firstNode.value === '~')
  480. ) {
  481. // adjacent or sibling
  482. return buildVElementMatcher(selectors, (element) =>
  483. getAfterElements(element)
  484. )
  485. }
  486. // descendant or child
  487. return buildVElementMatcher(selectors, (element) =>
  488. element.children.filter(isVElement)
  489. )
  490. }
  491. /**
  492. * @param {VElementMatcher} selectors
  493. * @param {(element: VElement) => VElement[]} getStartElements
  494. * @returns {VElementMatcher}
  495. */
  496. function buildVElementMatcher(selectors, getStartElements) {
  497. return (element) => {
  498. const elements = [...getStartElements(element)]
  499. /** @type {VElement|undefined} */
  500. let curr
  501. while ((curr = elements.shift())) {
  502. const el = curr
  503. if (selectors(el, element)) {
  504. return true
  505. }
  506. elements.push(...el.children.filter(isVElement))
  507. }
  508. return false
  509. }
  510. }
  511. /**
  512. * Parse <nth>
  513. * @param {parser.Pseudo} pseudoNode
  514. * @returns {(index: number)=>boolean}
  515. */
  516. function parseNth(pseudoNode) {
  517. const argumentsText = pseudoNode
  518. .toString()
  519. .slice(pseudoNode.value.length)
  520. .toLowerCase()
  521. const openParenIndex = argumentsText.indexOf('(')
  522. const closeParenIndex = argumentsText.lastIndexOf(')')
  523. if (openParenIndex === -1 || closeParenIndex === -1) {
  524. throw new SelectorError(
  525. `Cannot parse An+B micro syntax (:nth-xxx() argument): ${argumentsText}.`
  526. )
  527. }
  528. const argument = argumentsText
  529. .slice(openParenIndex + 1, closeParenIndex)
  530. .trim()
  531. try {
  532. return nthCheck(argument)
  533. } catch (error) {
  534. throw new SelectorError(
  535. `Cannot parse An+B micro syntax (:nth-xxx() argument): '${argument}'.`
  536. )
  537. }
  538. }
  539. /**
  540. * Build VElementMatcher for :nth-xxx()
  541. * @param {(index: number, length: number)=>boolean} testIndex
  542. * @returns {VElementMatcher}
  543. */
  544. function buildPseudoNthVElementMatcher(testIndex) {
  545. return (element) => {
  546. const elements = element.parent.children.filter(isVElement)
  547. return testIndex(elements.indexOf(element), elements.length)
  548. }
  549. }
  550. /**
  551. * Build VElementMatcher for :nth-xxx-of-type()
  552. * @param {(index: number, length: number)=>boolean} testIndex
  553. * @returns {VElementMatcher}
  554. */
  555. function buildPseudoNthOfTypeVElementMatcher(testIndex) {
  556. return (element) => {
  557. const elements = element.parent.children.filter(
  558. /** @returns {e is VElement} */
  559. (e) => isVElement(e) && e.rawName === element.rawName
  560. )
  561. return testIndex(elements.indexOf(element), elements.length)
  562. }
  563. }
  564. /**
  565. * @param {VElement} element
  566. */
  567. function getBeforeElement(element) {
  568. return getBeforeElements(element).pop() || null
  569. }
  570. /**
  571. * @param {VElement} element
  572. */
  573. function getBeforeElements(element) {
  574. const parent = element.parent
  575. const index = parent.children.indexOf(element)
  576. return parent.children.slice(0, index).filter(isVElement)
  577. }
  578. /**
  579. * @param {VElement} element
  580. */
  581. function getAfterElements(element) {
  582. const parent = element.parent
  583. const index = parent.children.indexOf(element)
  584. return parent.children.slice(index + 1).filter(isVElement)
  585. }
  586. /**
  587. * @param {VElementMatcher} a
  588. * @param {VElementMatcher} b
  589. * @returns {VElementMatcher}
  590. */
  591. function compound(a, b) {
  592. return (element, subject) => a(element, subject) && b(element, subject)
  593. }
  594. /**
  595. * Get attribute value from given element.
  596. * @param {VElement} element The element node.
  597. * @param {string} attribute The attribute name.
  598. */
  599. function getAttributeValue(element, attribute) {
  600. const attr = getAttribute(element, attribute)
  601. if (attr) {
  602. return (attr.value && attr.value.value) || ''
  603. }
  604. return null
  605. }