index.js 9.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241
  1. import { parse, SelectorType, isTraversal } from "css-what";
  2. import { _compileToken as compileToken, prepareContext, } from "css-select";
  3. import * as DomUtils from "domutils";
  4. import * as boolbase from "boolbase";
  5. import { getDocumentRoot, groupSelectors } from "./helpers.js";
  6. import { isFilter, getLimit, } from "./positionals.js";
  7. // Re-export pseudo extension points
  8. export { filters, pseudos, aliases } from "css-select";
  9. const UNIVERSAL_SELECTOR = {
  10. type: SelectorType.Universal,
  11. namespace: null,
  12. };
  13. const SCOPE_PSEUDO = {
  14. type: SelectorType.Pseudo,
  15. name: "scope",
  16. data: null,
  17. };
  18. export function is(element, selector, options = {}) {
  19. return some([element], selector, options);
  20. }
  21. export function some(elements, selector, options = {}) {
  22. if (typeof selector === "function")
  23. return elements.some(selector);
  24. const [plain, filtered] = groupSelectors(parse(selector));
  25. return ((plain.length > 0 && elements.some(compileToken(plain, options))) ||
  26. filtered.some((sel) => filterBySelector(sel, elements, options).length > 0));
  27. }
  28. function filterByPosition(filter, elems, data, options) {
  29. const num = typeof data === "string" ? parseInt(data, 10) : NaN;
  30. switch (filter) {
  31. case "first":
  32. case "lt":
  33. // Already done in `getLimit`
  34. return elems;
  35. case "last":
  36. return elems.length > 0 ? [elems[elems.length - 1]] : elems;
  37. case "nth":
  38. case "eq":
  39. return isFinite(num) && Math.abs(num) < elems.length
  40. ? [num < 0 ? elems[elems.length + num] : elems[num]]
  41. : [];
  42. case "gt":
  43. return isFinite(num) ? elems.slice(num + 1) : [];
  44. case "even":
  45. return elems.filter((_, i) => i % 2 === 0);
  46. case "odd":
  47. return elems.filter((_, i) => i % 2 === 1);
  48. case "not": {
  49. const filtered = new Set(filterParsed(data, elems, options));
  50. return elems.filter((e) => !filtered.has(e));
  51. }
  52. }
  53. }
  54. export function filter(selector, elements, options = {}) {
  55. return filterParsed(parse(selector), elements, options);
  56. }
  57. /**
  58. * Filter a set of elements by a selector.
  59. *
  60. * Will return elements in the original order.
  61. *
  62. * @param selector Selector to filter by.
  63. * @param elements Elements to filter.
  64. * @param options Options for selector.
  65. */
  66. function filterParsed(selector, elements, options) {
  67. if (elements.length === 0)
  68. return [];
  69. const [plainSelectors, filteredSelectors] = groupSelectors(selector);
  70. let found;
  71. if (plainSelectors.length) {
  72. const filtered = filterElements(elements, plainSelectors, options);
  73. // If there are no filters, just return
  74. if (filteredSelectors.length === 0) {
  75. return filtered;
  76. }
  77. // Otherwise, we have to do some filtering
  78. if (filtered.length) {
  79. found = new Set(filtered);
  80. }
  81. }
  82. for (let i = 0; i < filteredSelectors.length && (found === null || found === void 0 ? void 0 : found.size) !== elements.length; i++) {
  83. const filteredSelector = filteredSelectors[i];
  84. const missing = found
  85. ? elements.filter((e) => DomUtils.isTag(e) && !found.has(e))
  86. : elements;
  87. if (missing.length === 0)
  88. break;
  89. const filtered = filterBySelector(filteredSelector, elements, options);
  90. if (filtered.length) {
  91. if (!found) {
  92. /*
  93. * If we haven't found anything before the last selector,
  94. * just return what we found now.
  95. */
  96. if (i === filteredSelectors.length - 1) {
  97. return filtered;
  98. }
  99. found = new Set(filtered);
  100. }
  101. else {
  102. filtered.forEach((el) => found.add(el));
  103. }
  104. }
  105. }
  106. return typeof found !== "undefined"
  107. ? (found.size === elements.length
  108. ? elements
  109. : // Filter elements to preserve order
  110. elements.filter((el) => found.has(el)))
  111. : [];
  112. }
  113. function filterBySelector(selector, elements, options) {
  114. var _a;
  115. if (selector.some(isTraversal)) {
  116. /*
  117. * Get root node, run selector with the scope
  118. * set to all of our nodes.
  119. */
  120. const root = (_a = options.root) !== null && _a !== void 0 ? _a : getDocumentRoot(elements[0]);
  121. const opts = { ...options, context: elements, relativeSelector: false };
  122. selector.push(SCOPE_PSEUDO);
  123. return findFilterElements(root, selector, opts, true, elements.length);
  124. }
  125. // Performance optimization: If we don't have to traverse, just filter set.
  126. return findFilterElements(elements, selector, options, false, elements.length);
  127. }
  128. export function select(selector, root, options = {}, limit = Infinity) {
  129. if (typeof selector === "function") {
  130. return find(root, selector);
  131. }
  132. const [plain, filtered] = groupSelectors(parse(selector));
  133. const results = filtered.map((sel) => findFilterElements(root, sel, options, true, limit));
  134. // Plain selectors can be queried in a single go
  135. if (plain.length) {
  136. results.push(findElements(root, plain, options, limit));
  137. }
  138. if (results.length === 0) {
  139. return [];
  140. }
  141. // If there was only a single selector, just return the result
  142. if (results.length === 1) {
  143. return results[0];
  144. }
  145. // Sort results, filtering for duplicates
  146. return DomUtils.uniqueSort(results.reduce((a, b) => [...a, ...b]));
  147. }
  148. /**
  149. *
  150. * @param root Element(s) to search from.
  151. * @param selector Selector to look for.
  152. * @param options Options for querying.
  153. * @param queryForSelector Query multiple levels deep for the initial selector, even if it doesn't contain a traversal.
  154. */
  155. function findFilterElements(root, selector, options, queryForSelector, totalLimit) {
  156. const filterIndex = selector.findIndex(isFilter);
  157. const sub = selector.slice(0, filterIndex);
  158. const filter = selector[filterIndex];
  159. // If we are at the end of the selector, we can limit the number of elements to retrieve.
  160. const partLimit = selector.length - 1 === filterIndex ? totalLimit : Infinity;
  161. /*
  162. * Set the number of elements to retrieve.
  163. * Eg. for :first, we only have to get a single element.
  164. */
  165. const limit = getLimit(filter.name, filter.data, partLimit);
  166. if (limit === 0)
  167. return [];
  168. /*
  169. * Skip `findElements` call if our selector starts with a positional
  170. * pseudo.
  171. */
  172. const elemsNoLimit = sub.length === 0 && !Array.isArray(root)
  173. ? DomUtils.getChildren(root).filter(DomUtils.isTag)
  174. : sub.length === 0
  175. ? (Array.isArray(root) ? root : [root]).filter(DomUtils.isTag)
  176. : queryForSelector || sub.some(isTraversal)
  177. ? findElements(root, [sub], options, limit)
  178. : filterElements(root, [sub], options);
  179. const elems = elemsNoLimit.slice(0, limit);
  180. let result = filterByPosition(filter.name, elems, filter.data, options);
  181. if (result.length === 0 || selector.length === filterIndex + 1) {
  182. return result;
  183. }
  184. const remainingSelector = selector.slice(filterIndex + 1);
  185. const remainingHasTraversal = remainingSelector.some(isTraversal);
  186. if (remainingHasTraversal) {
  187. if (isTraversal(remainingSelector[0])) {
  188. const { type } = remainingSelector[0];
  189. if (type === SelectorType.Sibling ||
  190. type === SelectorType.Adjacent) {
  191. // If we have a sibling traversal, we need to also look at the siblings.
  192. result = prepareContext(result, DomUtils, true);
  193. }
  194. // Avoid a traversal-first selector error.
  195. remainingSelector.unshift(UNIVERSAL_SELECTOR);
  196. }
  197. options = {
  198. ...options,
  199. // Avoid absolutizing the selector
  200. relativeSelector: false,
  201. /*
  202. * Add a custom root func, to make sure traversals don't match elements
  203. * that aren't a part of the considered tree.
  204. */
  205. rootFunc: (el) => result.includes(el),
  206. };
  207. }
  208. else if (options.rootFunc && options.rootFunc !== boolbase.trueFunc) {
  209. options = { ...options, rootFunc: boolbase.trueFunc };
  210. }
  211. /*
  212. * If we have another filter, recursively call `findFilterElements`,
  213. * with the `recursive` flag disabled. We only have to look for more
  214. * elements when we see a traversal.
  215. *
  216. * Otherwise,
  217. */
  218. return remainingSelector.some(isFilter)
  219. ? findFilterElements(result, remainingSelector, options, false, totalLimit)
  220. : remainingHasTraversal
  221. ? // Query existing elements to resolve traversal.
  222. findElements(result, [remainingSelector], options, totalLimit)
  223. : // If we don't have any more traversals, simply filter elements.
  224. filterElements(result, [remainingSelector], options);
  225. }
  226. function findElements(root, sel, options, limit) {
  227. const query = compileToken(sel, options, root);
  228. return find(root, query, limit);
  229. }
  230. function find(root, query, limit = Infinity) {
  231. const elems = prepareContext(root, DomUtils, query.shouldTestNextSiblings);
  232. return DomUtils.find((node) => DomUtils.isTag(node) && query(node), elems, true, limit);
  233. }
  234. function filterElements(elements, sel, options) {
  235. const els = (Array.isArray(elements) ? elements : [elements]).filter(DomUtils.isTag);
  236. if (els.length === 0)
  237. return els;
  238. const query = compileToken(sel, options);
  239. return query === boolbase.trueFunc ? els : els.filter(query);
  240. }
  241. //# sourceMappingURL=index.js.map