UNPKG

r2-navigator-js

Version:

Readium 2 'navigator' for NodeJS (TypeScript)

275 lines 7.92 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.uniqueCssSelector = uniqueCssSelector; const CSSEscape = require("css.escape"); let config; let rootDocument; function uniqueCssSelector(input, doc, options) { if (input.nodeType !== Node.ELEMENT_NODE) { throw new Error("Can't generate CSS selector for non-element node type."); } if (input.__r2CssSelector) { return input.__r2CssSelector; } if ("html" === input.tagName.toLowerCase()) { input.__r2CssSelector = "html"; return "html"; } const defaults = { root: doc.body, idName: (_name) => true, className: (_name) => true, tagName: (_name) => true, attr: (_name, _value) => false, seedMinLength: 1, optimizedMinLength: 2, threshold: 1000, maxNumberOfTries: 10000, }; config = Object.assign(Object.assign({}, defaults), options); rootDocument = findRootDocument(config.root, defaults); let path = bottomUpSearch(input, "all", () => bottomUpSearch(input, "two", () => bottomUpSearch(input, "one", () => bottomUpSearch(input, "none")))); if (path) { const optimized = sort(optimize(path, input)); if (optimized.length > 0) { path = optimized[0]; } input.__r2CssSelector = selector(path); return input.__r2CssSelector; } else { throw new Error("Selector was not found."); } } function findRootDocument(rootNode, defaults) { if (rootNode.nodeType === Node.DOCUMENT_NODE) { return rootNode; } if (rootNode === defaults.root) { return rootNode.ownerDocument; } return rootNode; } function bottomUpSearch(input, limit, fallback) { let path = null; const stack = []; let current = input; let i = 0; while (current) { let level = maybe(id(current)) || maybe(...attr(current)) || maybe(...classNames(current)) || maybe(tagName(current)) || [any()]; const nth = index(current); if (limit == "all") { if (nth) { level = level.concat(level.filter(dispensableNth).map((node) => nthChild(node, nth))); } } else if (limit == "two") { level = level.slice(0, 1); if (nth) { level = level.concat(level.filter(dispensableNth).map((node) => nthChild(node, nth))); } } else if (limit == "one") { const [node] = (level = level.slice(0, 1)); if (nth && dispensableNth(node)) { level = [nthChild(node, nth)]; } } else if (limit == "none") { level = [any()]; if (nth) { level = [nthChild(level[0], nth)]; } } for (const node of level) { node.level = i; } stack.push(level); if (stack.length >= config.seedMinLength) { path = findUniquePath(stack, fallback); if (path) { break; } } current = current.parentElement; if (current && !current.parentElement) { break; } i++; } if (!path) { path = findUniquePath(stack, fallback); } if (!path && fallback) { return fallback(); } return path; } function findUniquePath(stack, fallback) { const paths = sort(combinations(stack)); if (paths.length > config.threshold) { return fallback ? fallback() : null; } for (const candidate of paths) { if (unique(candidate)) { return candidate; } } return null; } function selector(path) { let node = path[0]; let query = node.name; for (let i = 1; i < path.length; i++) { const level = path[i].level || 0; if (node.level === level - 1) { query = `${path[i].name} > ${query}`; } else { query = `${path[i].name} ${query}`; } node = path[i]; } return query; } function penalty(path) { return path.map((node) => node.penalty).reduce((acc, i) => acc + i, 0); } function unique(path) { const css = selector(path); switch (rootDocument.querySelectorAll(css).length) { case 0: throw new Error(`Can't select any node with this selector: ${css}`); case 1: return true; default: return false; } } function id(input) { const elementId = input.getAttribute("id"); if (elementId && config.idName(elementId)) { return { name: "#" + CSSEscape(elementId), penalty: 0, }; } return null; } function attr(input) { const attrs = Array.from(input.attributes).filter((attr) => config.attr(attr.name, attr.value)); return attrs.map((attr) => ({ name: `[${CSSEscape(attr.name)}="${CSSEscape(attr.value)}"]`, penalty: 0.5, })); } function classNames(input) { const names = Array.from(input.classList).filter(config.className); return names.map((name) => ({ name: "." + CSSEscape(name), penalty: 1, })); } const ELEMENT_NAMESPACE_PREFIX = /^(.+:)(.+)$/; const ELEMENT_NAMESPACE_PREFIX_ = /^\*\|(a|script|style)$/; function tagName(input) { const name = input.tagName === "foreignObject" ? input.tagName : input.tagName.toLowerCase(); if (config.tagName(name)) { const n = name.replace(ELEMENT_NAMESPACE_PREFIX, "*|$2").replace(ELEMENT_NAMESPACE_PREFIX_, "*|$1:not(|$1)"); return { name: n, penalty: 2, }; } return null; } function any() { return { name: "*", penalty: 3, }; } function index(input) { const parent = input.parentNode; if (!parent) { return null; } let child = parent.firstChild; if (!child) { return null; } let i = 0; while (child) { if (child.nodeType === Node.ELEMENT_NODE) { i++; } if (child === input) { break; } child = child.nextSibling; } return i; } function nthChild(node, i) { return { name: node.name + `:nth-child(${i})`, penalty: node.penalty + 1, }; } function dispensableNth(node) { return node.name !== "html" && !node.name.startsWith("#"); } function maybe(...level) { const list = level.filter(notEmpty); if (list.length > 0) { return list; } return null; } function notEmpty(value) { return value !== null && value !== undefined; } function* combinations(stack, path = []) { if (stack.length > 0) { for (const node of stack[0]) { yield* combinations(stack.slice(1, stack.length), path.concat(node)); } } else { yield path; } } function sort(paths) { return [...paths].sort((a, b) => penalty(a) - penalty(b)); } function* optimize(path, input, scope = { counter: 0, visited: new Map(), }) { if (path.length > 2 && path.length > config.optimizedMinLength) { for (let i = 1; i < path.length - 1; i++) { if (scope.counter > config.maxNumberOfTries) { return; } scope.counter += 1; const newPath = [...path]; newPath.splice(i, 1); const newPathKey = selector(newPath); if (scope.visited.has(newPathKey)) { return; } if (unique(newPath) && same(newPath, input)) { yield newPath; scope.visited.set(newPathKey, true); yield* optimize(newPath, input, scope); } } } } function same(path, input) { return rootDocument.querySelector(selector(path)) === input; } //# sourceMappingURL=cssselector3.js.map