UNPKG

ultrahtml

Version:

A 1.75kB library for enhancing `html`. `ultrahtml` has zero dependencies and is compatible with any JavaScript runtime.

426 lines (423 loc) 14.7 kB
// src/selector.ts import { ELEMENT_NODE, TEXT_NODE, walkSync } from "./index.js"; // node_modules/.pnpm/parsel-js@1.1.2/node_modules/parsel-js/dist/parsel.min.js var e = { attribute: /\[\s*(?:(?<namespace>\*|[-\w\P{ASCII}]*)\|)?(?<name>[-\w\P{ASCII}]+)\s*(?:(?<operator>\W?=)\s*(?<value>.+?)\s*(\s(?<caseSensitive>[iIsS]))?\s*)?\]/gu, id: /#(?<name>[-\w\P{ASCII}]+)/gu, class: /\.(?<name>[-\w\P{ASCII}]+)/gu, comma: /\s*,\s*/g, combinator: /\s*[\s>+~]\s*/g, "pseudo-element": /::(?<name>[-\w\P{ASCII}]+)(?:\((?<argument>¶*)\))?/gu, "pseudo-class": /:(?<name>[-\w\P{ASCII}]+)(?:\((?<argument>¶*)\))?/gu, universal: /(?:(?<namespace>\*|[-\w\P{ASCII}]*)\|)?\*/gu, type: /(?:(?<namespace>\*|[-\w\P{ASCII}]*)\|)?(?<name>[-\w\P{ASCII}]+)/gu }; var t = /* @__PURE__ */ new Set(["combinator", "comma"]); var n = /* @__PURE__ */ new Set(["not", "is", "where", "has", "matches", "-moz-any", "-webkit-any", "nth-child", "nth-last-child"]); var s = /(?<index>[\dn+-]+)\s+of\s+(?<subtree>.+)/; var o = { "nth-child": s, "nth-last-child": s }; var r = (t2) => { switch (t2) { case "pseudo-element": case "pseudo-class": return new RegExp(e[t2].source.replace("(?<argument>\xB6*)", "(?<argument>.*)"), "gu"); default: return e[t2]; } }; function c(e2, t2) { let n2 = 0, s2 = ""; for (; t2 < e2.length; t2++) { const o2 = e2[t2]; switch (o2) { case "(": ++n2; break; case ")": --n2; } if (s2 += o2, 0 === n2) return s2; } return s2; } function i(n2, s2 = e) { if (!n2) return []; const o2 = [n2]; for (const [e2, t2] of Object.entries(s2)) for (let n3 = 0; n3 < o2.length; n3++) { const s3 = o2[n3]; if ("string" != typeof s3) continue; t2.lastIndex = 0; const r3 = t2.exec(s3); if (!r3) continue; const c2 = r3.index - 1, i2 = [], a2 = r3[0], l2 = s3.slice(0, c2 + 1); l2 && i2.push(l2), i2.push({ ...r3.groups, type: e2, content: a2 }); const u2 = s3.slice(c2 + a2.length + 1); u2 && i2.push(u2), o2.splice(n3, 1, ...i2); } let r2 = 0; for (const e2 of o2) switch (typeof e2) { case "string": throw new Error(`Unexpected sequence ${e2} found at index ${r2}`); case "object": r2 += e2.content.length, e2.pos = [r2 - e2.content.length, r2], t.has(e2.type) && (e2.content = e2.content.trim() || " "); } return o2; } var a = /(['"])([^\\\n]+?)\1/g; var l = /\\./g; function u(t2, n2 = e) { if ("" === (t2 = t2.trim())) return []; const s2 = []; t2 = (t2 = t2.replace(l, (e2, t3) => (s2.push({ value: e2, offset: t3 }), "\uE000".repeat(e2.length)))).replace(a, (e2, t3, n3, o3) => (s2.push({ value: e2, offset: o3 }), `${t3}${"\uE001".repeat(n3.length)}${t3}`)); { let e2, n3 = 0; for (; (e2 = t2.indexOf("(", n3)) > -1; ) { const o3 = c(t2, e2); s2.push({ value: o3, offset: e2 }), t2 = `${t2.substring(0, e2)}(${"\xB6".repeat(o3.length - 2)})${t2.substring(e2 + o3.length)}`, n3 = e2 + o3.length; } } const o2 = i(t2, n2), u2 = /* @__PURE__ */ new Set(); for (const e2 of s2.reverse()) for (const t3 of o2) { const { offset: n3, value: s3 } = e2; if (!(t3.pos[0] <= n3 && n3 + s3.length <= t3.pos[1])) continue; const { content: o3 } = t3, r2 = n3 - t3.pos[0]; t3.content = o3.slice(0, r2) + s3 + o3.slice(r2 + s3.length), t3.content !== o3 && u2.add(t3); } for (const e2 of u2) { const t3 = r(e2.type); if (!t3) throw new Error(`Unknown token type: ${e2.type}`); t3.lastIndex = 0; const n3 = t3.exec(e2.content); if (!n3) throw new Error(`Unable to parse content for ${e2.type}: ${e2.content}`); Object.assign(e2, n3.groups); } return o2; } function f(e2, { list: t2 = true } = {}) { if (t2 && e2.find((e3) => "comma" === e3.type)) { const t3 = [], n2 = []; for (let s2 = 0; s2 < e2.length; s2++) if ("comma" === e2[s2].type) { if (0 === n2.length) throw new Error("Incorrect comma at " + s2); t3.push(f(n2, { list: false })), n2.length = 0; } else n2.push(e2[s2]); if (0 === n2.length) throw new Error("Trailing comma"); return t3.push(f(n2, { list: false })), { type: "list", list: t3 }; } for (let t3 = e2.length - 1; t3 >= 0; t3--) { let n2 = e2[t3]; if ("combinator" === n2.type) { let s2 = e2.slice(0, t3), o2 = e2.slice(t3 + 1); return { type: "complex", combinator: n2.content, left: f(s2), right: f(o2) }; } } switch (e2.length) { case 0: throw new Error("Could not build AST."); case 1: return e2[0]; default: return { type: "compound", list: [...e2] }; } } function* p(e2, t2) { switch (e2.type) { case "list": for (let t3 of e2.list) yield* p(t3, e2); break; case "complex": yield* p(e2.left, e2), yield* p(e2.right, e2); break; case "compound": yield* e2.list.map((t3) => [t3, e2]); break; default: yield [e2, t2]; } } function m(e2, { recursive: t2 = true, list: s2 = true } = {}) { const r2 = u(e2); if (!r2) return; const c2 = f(r2, { list: s2 }); if (!t2) return c2; for (const [e3] of p(c2)) { if ("pseudo-class" !== e3.type || !e3.argument) continue; if (!n.has(e3.name)) continue; let t3 = e3.argument; const s3 = o[e3.name]; if (s3) { const n2 = s3.exec(t3); if (!n2) continue; Object.assign(e3, n2.groups), t3 = n2.groups.subtree; } t3 && Object.assign(e3, { subtree: m(t3, { recursive: true, list: true }) }); } return c2; } function d(e2, t2) { return t2 = t2 || Math.max(...e2) + 1, e2[0] * (t2 << 1) + e2[1] * t2 + e2[2]; } function w(e2) { let t2 = e2; if ("string" == typeof t2 && (t2 = m(t2, { recursive: true })), !t2) return []; if ("list" === t2.type && "list" in t2) { let e3 = 10; const n2 = t2.list.map((t3) => { const n3 = w(t3); return e3 = Math.max(e3, ...w(t3)), n3; }), s3 = n2.map((t3) => d(t3, e3)); return n2[s3.indexOf(Math.max(...s3))]; } const s2 = [0, 0, 0]; for (const [e3] of p(t2)) switch (e3.type) { case "id": s2[0]++; break; case "class": case "attribute": s2[1]++; break; case "pseudo-element": case "type": s2[2]++; break; case "pseudo-class": if ("where" === e3.name) break; if (!n.has(e3.name) || !e3.subtree) { s2[1]++; break; } w(e3.subtree).forEach((e4, t3) => s2[t3] += e4), "nth-child" !== e3.name && "nth-last-child" !== e3.name || s2[1]++; } return s2; } // src/selector.ts function specificity(selector) { return d(w(selector), 10); } function matches(node, selector) { const match = selectorToMatch(selector); return match(node, node.parent, nthChildIndex(node, node.parent)); } function querySelector(node, selector) { const match = selectorToMatch(selector); try { return select( node, (n2, parent, index) => { let m2 = match(n2, parent, index); if (!m2) return false; return m2; }, { single: true } )[0]; } catch (e2) { if (e2 instanceof Error) { throw e2; } return e2; } } function querySelectorAll(node, selector) { const match = selectorToMatch(selector); return select(node, (n2, parent, index) => { let m2 = match(n2, parent, index); if (!m2) return false; return m2; }); } var selector_default = querySelectorAll; function select(node, match, opts = { single: false }) { let nodes = []; walkSync(node, (n2, parent, index) => { if (n2 && n2.type !== ELEMENT_NODE) return; if (match(n2, parent, index)) { if (opts.single) throw n2; nodes.push(n2); } }); return nodes; } var getAttributeMatch = (selector) => { const { operator = "=" } = selector; switch (operator) { case "=": return (a2, b) => a2 === b; case "~=": return (a2, b) => a2.split(/\s+/g).includes(b); case "|=": return (a2, b) => a2.startsWith(b + "-"); case "*=": return (a2, b) => a2.indexOf(b) > -1; case "$=": return (a2, b) => a2.endsWith(b); case "^=": return (a2, b) => a2.startsWith(b); } return (a2, b) => false; }; var nthChildIndex = (node, parent) => parent?.children.filter((n2) => n2.type === ELEMENT_NODE).findIndex((n2) => n2 === node); var nthChild = (formula) => { let [_, A = "1", B = "0"] = /^\s*(?:(-?(?:\d+)?)n)?\s*\+?\s*(\d+)?\s*$/gm.exec(formula) ?? []; if (A.length === 0) A = "1"; const a2 = Number.parseInt(A === "-" ? "-1" : A); const b = Number.parseInt(B); return (n2) => a2 * n2 + b; }; var lastChild = (node, parent) => parent?.children.filter((n2) => n2.type === ELEMENT_NODE).pop() === node; var firstChild = (node, parent) => parent?.children.filter((n2) => n2.type === ELEMENT_NODE).shift() === node; var onlyChild = (node, parent) => parent?.children.filter((n2) => n2.type === ELEMENT_NODE).length === 1; var createMatch = (selector) => { switch (selector.type) { case "type": return (node) => { if (selector.content === "*") return true; return node.name === selector.name; }; case "class": return (node) => node.attributes?.class?.split(/\s+/g).includes(selector.name); case "id": return (node) => node.attributes?.id === selector.name; case "pseudo-class": { switch (selector.name) { case "global": return (...args) => selectorToMatch(m(selector.argument))(...args); case "not": return (...args) => !createMatch(selector.subtree)(...args); case "is": return (...args) => selectorToMatch(selector.subtree)(...args); case "where": return (...args) => selectorToMatch(selector.subtree)(...args); case "root": return (node, parent) => node.type === ELEMENT_NODE && node.name === "html"; case "empty": return (node) => node.type === ELEMENT_NODE && (node.children.length === 0 || node.children.every( (n2) => n2.type === TEXT_NODE && n2.value.trim() === "" )); case "first-child": return (node, parent) => firstChild(node, parent); case "last-child": return (node, parent) => lastChild(node, parent); case "only-child": return (node, parent) => onlyChild(node, parent); case "nth-child": return (node, parent) => { const target = nthChildIndex(node, parent) + 1; if (Number.isNaN(Number(selector.argument))) { switch (selector.argument) { case "odd": return Math.abs(target % 2) == 1; case "even": return target % 2 === 0; default: { if (!selector.argument) { throw new Error(`Unsupported empty nth-child selector!`); } const nth = nthChild(selector.argument); const elements = parent?.children.filter( (n2) => n2.type === ELEMENT_NODE ); const childIndex = nthChildIndex(node, parent) + 1; for (let i2 = 0; i2 < elements.length; i2++) { let n2 = nth(i2); if (n2 > elements.length) return false; if (n2 === childIndex) return true; } return false; } } } return target === Number(selector.argument); }; default: throw new Error(`Unhandled pseudo-class: ${selector.name}!`); } } case "attribute": return (node) => { let { caseSensitive, name, value } = selector; if (!node.attributes) return false; const attrs = Object.entries(node.attributes); for (let [attr, attrVal] of attrs) { if (caseSensitive === "i") { value = name.toLowerCase(); attrVal = attr.toLowerCase(); } if (attr !== name) continue; if (!value) return true; if ((value[0] === '"' || value[0] === "'") && value[0] === value[value.length - 1]) { value = JSON.parse(value); } if (value) { return getAttributeMatch(selector)(attrVal, value); } } return false; }; case "universal": return (_) => { return true; }; default: { throw new Error(`Unhandled selector: ${selector.type}`); } } }; var selectorToMatch = (sel) => { let selector = typeof sel === "string" ? m(sel) : sel; switch (selector?.type) { case "list": { const matchers = selector.list.map((s2) => createMatch(s2)); return (node, parent, index) => { for (const match of matchers) { if (match(node, parent)) return true; } return false; }; } case "compound": { const matchers = selector.list.map((s2) => createMatch(s2)); return (node, parent, index) => { for (const match of matchers) { if (!match(node, parent)) return false; } return true; }; } case "complex": { const { left, right, combinator } = selector; const matchLeft = selectorToMatch(left); const matchRight = selectorToMatch(right); let leftMatches = /* @__PURE__ */ new WeakSet(); return (node, parent, i2 = 0) => { if (matchLeft(node)) { leftMatches.add(node); } else if (parent && leftMatches.has(parent) && combinator === " ") { leftMatches.add(node); } if (!matchRight(node)) return false; switch (combinator) { case " ": // fall-through case ">": return parent ? leftMatches.has(parent) : false; case "~": { if (!parent) return false; for (let sibling of parent.children.slice(0, i2)) { if (leftMatches.has(sibling)) return true; } return false; } case "+": { if (!parent) return false; let prevSiblings = parent.children.slice(0, i2).filter((el) => el.type === ELEMENT_NODE); if (prevSiblings.length === 0) return false; const prev = prevSiblings[prevSiblings.length - 1]; if (!prev) return false; if (leftMatches.has(prev)) return true; } default: return false; } }; } default: return createMatch(selector); } }; export { selector_default as default, matches, querySelector, querySelectorAll, specificity };