import { Character } from "../../types/data"; import { Constant, Selector, SelectorInput } from "../../types/layout"; import { ExcludeClassNamesList } from "./constants"; const excludeClassNames = ExcludeClassNamesList; let selectorMap: { [selector: string]: number[] } = {}; export function reset(): void { selectorMap = {}; } export function get(input: SelectorInput, type: Selector): string { let a = input.attributes; let prefix = input.prefix ? input.prefix[type] : null; let suffix = type === Selector.Alpha ? `${Constant.Tilde}${input.position-1}` : `:nth-of-type(${input.position})`; switch (input.tag) { case "STYLE": case "TITLE": case "LINK": case "META": case Constant.TextTag: case Constant.DocumentTag: return Constant.Empty; case "HTML": return Constant.HTML; default: if (prefix === null) { return Constant.Empty; } prefix = `${prefix}${Constant.Separator}`; input.tag = input.tag.indexOf(Constant.SvgPrefix) === 0 ? input.tag.substr(Constant.SvgPrefix.length) : input.tag; let selector = `${prefix}${input.tag}${suffix}`; let id = Constant.Id in a && a[Constant.Id].length > 0 ? a[Constant.Id] : null; let classes = input.tag !== Constant.BodyTag && Constant.Class in a && a[Constant.Class].length > 0 ? a[Constant.Class].trim().split(/\s+/).filter(c => filter(c)).join(Constant.Period) : null; if (classes && classes.length > 0) { if (type === Selector.Alpha) { // In Alpha mode, update selector to use class names, with relative positioning within the parent id container. // If the node has valid class name(s) then drop relative positioning within the parent path to keep things simple. let key = `${getDomPath(prefix)}${input.tag}${Constant.Dot}${classes}`; if (!(key in selectorMap)) { selectorMap[key] = []; } if (selectorMap[key].indexOf(input.id) < 0) { selectorMap[key].push(input.id); } selector = `${key}${Constant.Tilde}${selectorMap[key].indexOf(input.id)}`; } else { // In Beta mode, we continue to look at query selectors in context of the full page selector = `${prefix}${input.tag}.${classes}${suffix}` } } // Update selector to use "id" field when available. There are two exceptions: // (1) if "id" appears to be an auto generated string token, e.g. guid or a random id containing digits // (2) if "id" appears inside a shadow DOM, in which case we continue to prefix up to shadow DOM to prevent conflicts selector = id && filter(id) ? `${getDomPrefix(prefix)}${Constant.Hash}${id}` : selector; return selector; } } function getDomPrefix(prefix: string): string { const shadowDomStart = prefix.lastIndexOf(Constant.ShadowDomTag); const iframeDomStart = prefix.lastIndexOf(`${Constant.IFramePrefix}${Constant.HTML}`); const domStart = Math.max(shadowDomStart, iframeDomStart); if (domStart < 0) { return Constant.Empty; } return prefix.substring(0, prefix.indexOf(Constant.Separator, domStart) + 1); } function getDomPath(input: string): string { let parts = input.split(Constant.Separator); for (let i = 0; i < parts.length; i++) { let tIndex = parts[i].indexOf(Constant.Tilde); let dIndex = parts[i].indexOf(Constant.Dot); parts[i] = parts[i].substring(0, dIndex > 0 ? dIndex : (tIndex > 0 ? tIndex : parts[i].length)); } return parts.join(Constant.Separator); } // Check if the given input string has digits or excluded class names function filter(value: string): boolean { if (!value) { return false; } // Do not process empty strings if (excludeClassNames.some(x => value.toLowerCase().indexOf(x) >= 0)) { return false; } for (let i = 0; i < value.length; i++) { let c = value.charCodeAt(i); if (c >= Character.Zero && c <= Character.Nine) { return false }; } return true; }