UNPKG

@ara-web/sds

Version:

Write code faster without worry about future debts or scalability in the future

391 lines (350 loc) 14.3 kB
import { OkResult, ObjectTraits } from "@ara-web/p-hintjens"; export type Predicate<Value> = (v: Value) => boolean; export const DOCUMENT_SELECTOR = "#document"; export type DataToObjectNode<T> = (obj?: T, parent?: ObjectNode<T>) => ObjectNode<T>; export interface CustomSelectorNode extends Node { selector: string; isTag: boolean; // The component html name or component name. getAttribute(attr: string): string | undefined; children: CustomSelectorNode[]; parent: CustomSelectorNode | null; name: string; siblings: CustomSelectorNode[]; data: unknown | null; deleteChildren(): void; toString(): string; isAttributeExist(name: string): boolean; isEqualTo(node: CustomSelectorNode | null | undefined): boolean; setChildren(children: CustomSelectorNode[]): void; setParent(parent: CustomSelectorNode): void; setAttribute<AttributeValue>(name: string, value: AttributeValue): OkResult; appendChild<T extends Node>(node: T): T; } /** * @link https://github.com/fb55/css-select/blob/1aa44bdd64aaf2ebdfd7f338e2e76bed36521957/src/types.ts#L6-L96 */ export interface Adapter<ObjectNode, BranchedModuleObject extends ObjectNode> { isTag: (node: ObjectNode) => node is BranchedModuleObject; existsOne: (test: Predicate<BranchedModuleObject>, elems: ObjectNode[]) => boolean; getAttributeValue: (elem: BranchedModuleObject, name: string) => string | undefined; getChildren: (node: ObjectNode) => ObjectNode[]; getName: (elem: BranchedModuleObject) => string; getParent: (node: BranchedModuleObject) => BranchedModuleObject | null; getSiblings: (node: ObjectNode) => ObjectNode[]; getText: (node: ObjectNode) => string; hasAttrib: (elem: BranchedModuleObject, name: string) => boolean; removeSubsets: (nodes: ObjectNode[]) => ObjectNode[]; findAll: (test: Predicate<BranchedModuleObject>, nodes: ObjectNode[]) => BranchedModuleObject[]; findOne: (test: Predicate<BranchedModuleObject>, elems: ObjectNode[]) => BranchedModuleObject | null; equals?: (a: ObjectNode, b: ObjectNode) => boolean; isHovered?: (elem: BranchedModuleObject) => boolean; isVisited?: (elem: BranchedModuleObject) => boolean; isActive?: (elem: BranchedModuleObject) => boolean; } export type DataOperations<T> = { getChildren(obj?: T): T[]; getAttribute(obj: T|undefined, attrName: string): string | undefined; getName(obj?: T): string; setAttribute<AttrValue>(obj: T, attrName: string, attrValue: AttrValue): OkResult } /** * ObjectNode is a representation of the page object model. * It converts the Page slots into a tree structure. * The root node is the page itself, and the children are the slots. * * Using with the `CSSObjectAdapter` and `pageToObjectNodes` function, * it can be used to walk through the page using CSS selectors. */ export class ObjectNode<DataType> implements CustomSelectorNode { public isTag: boolean = true; // legacy, this is needed by CSS-Select package. private _data?: DataType; // Only component like data private _children: ObjectNode<DataType>[] = []; private _parent?: CustomSelectorNode; private _dataOps: DataOperations<DataType>; constructor( dataOps: DataOperations<DataType>, dataToObjectNode: DataToObjectNode<DataType>, data?: DataType, parent?: CustomSelectorNode, ) { this._dataOps = dataOps; this._children = []; this._parent = parent; if (data !== undefined) { this._data = data; const children = dataOps.getChildren(data); const parentElement: ObjectNode<DataType> = this; // new ObjectNode() lints this object to it's children const childNodes = children.map((child) => dataToObjectNode(child, parentElement)) // lint child to this node. this.setChildren(childNodes); } } baseURI!: string; childNodes!: NodeListOf<ChildNode>; firstChild!: ChildNode | null; isConnected!: boolean; lastChild!: ChildNode | null; nextSibling!: ChildNode | null; nodeName!: string; nodeType!: number; nodeValue!: string | null; ownerDocument!: Document | null; parentElement!: HTMLElement | null; parentNode!: ParentNode | null; previousSibling!: ChildNode | null; textContent!: string | null; public get selector(): string { if (this._data === undefined) { return DOCUMENT_SELECTOR; } let mySelectorId = this.getAttribute("id") || ""; if (mySelectorId.length > 0) { mySelectorId = `#${mySelectorId}`; } let mySelectorClasses = this.getAttribute("class") || ""; if (mySelectorClasses.length > 0) { mySelectorClasses = mySelectorClasses.split(" ").map(c => (`.${c.trim()}`)).join("") } const mySelector = `${this.name}${mySelectorClasses}${mySelectorId}` return this._parent ? `${this._parent.selector} > ${mySelector}` : mySelector; } appendChild<T extends Node>(node: T): T { this._children.push(node as unknown as ObjectNode<DataType>); (node as unknown as ObjectNode<DataType>).setParent(this); return node; } // eslint-disable-next-line @typescript-eslint/no-unused-vars cloneNode(deep?: boolean): Node { return ObjectTraits.deepCopy(this) as Node; } // eslint-disable-next-line @typescript-eslint/no-unused-vars compareDocumentPosition(other: Node): number { throw new Error("Method not implemented."); } // eslint-disable-next-line @typescript-eslint/no-unused-vars contains(other: Node | null): boolean { throw new Error("Method not implemented."); } // eslint-disable-next-line @typescript-eslint/no-unused-vars getRootNode(options?: GetRootNodeOptions): Node { throw new Error("Method not implemented."); } hasChildNodes(): boolean { throw new Error("Method not implemented."); } // eslint-disable-next-line @typescript-eslint/no-unused-vars insertBefore<T extends Node>(node: T, child: Node | null): T { throw new Error("Method not implemented."); } // eslint-disable-next-line @typescript-eslint/no-unused-vars isDefaultNamespace(namespace: string | null): boolean { throw new Error("Method not implemented."); } // eslint-disable-next-line @typescript-eslint/no-unused-vars isEqualNode(otherNode: Node | null): boolean { throw new Error("Method not implemented."); } // eslint-disable-next-line @typescript-eslint/no-unused-vars isSameNode(otherNode: Node | null): boolean { throw new Error("Method not implemented."); } // eslint-disable-next-line @typescript-eslint/no-unused-vars lookupNamespaceURI(prefix: string | null): string | null { throw new Error("Method not implemented."); } // eslint-disable-next-line @typescript-eslint/no-unused-vars lookupPrefix(namespace: string | null): string | null { throw new Error("Method not implemented."); } normalize(): void { throw new Error("Method not implemented."); } // eslint-disable-next-line @typescript-eslint/no-unused-vars removeChild<T extends Node>(child: T): T { this._children = this._children.filter(node => (node._data as unknown as T) !== child); return child; } // eslint-disable-next-line @typescript-eslint/no-unused-vars replaceChild<T extends Node>(node: Node, child: T): T { throw new Error("Method not implemented."); } ELEMENT_NODE!: 1; ATTRIBUTE_NODE!: 2; TEXT_NODE!: 3; CDATA_SECTION_NODE!: 4; ENTITY_REFERENCE_NODE!: 5; ENTITY_NODE!: 6; PROCESSING_INSTRUCTION_NODE!: 7; COMMENT_NODE!: 8; DOCUMENT_NODE!: 9; DOCUMENT_TYPE_NODE!: 10; DOCUMENT_FRAGMENT_NODE!: 11; NOTATION_NODE!: 12; DOCUMENT_POSITION_DISCONNECTED!: 1; DOCUMENT_POSITION_PRECEDING!: 2; DOCUMENT_POSITION_FOLLOWING!: 4; DOCUMENT_POSITION_CONTAINS!: 8; DOCUMENT_POSITION_CONTAINED_BY!: 16; DOCUMENT_POSITION_IMPLEMENTATION_SPECIFIC!: 32; // eslint-disable-next-line @typescript-eslint/no-unused-vars addEventListener(type: string, callback: EventListenerOrEventListenerObject | null, options?: AddEventListenerOptions | boolean): void { throw new Error("Method not implemented."); } // eslint-disable-next-line @typescript-eslint/no-unused-vars dispatchEvent(event: Event): boolean { throw new Error("Method not implemented."); } // eslint-disable-next-line @typescript-eslint/no-unused-vars removeEventListener(type: string, callback: EventListenerOrEventListenerObject | null, options?: EventListenerOptions | boolean): void { throw new Error("Method not implemented."); } isEqualTo(node: CustomSelectorNode | null | undefined): boolean { if (!node) { return false; } return this === node; } public get data(): DataType|null { return this._data === undefined ? null : this._data; } public set data(data: DataType) { this._data = data; } /** * For Pages, it returns empty string. */ public get name(): string { return this._dataOps.getName(this._data); } public get parent(): CustomSelectorNode | null { return this._parent === undefined ? null : this._parent; } getAttribute(attrName: string): string | undefined { return this._dataOps.getAttribute(this._data, attrName); } setAttribute<AttributeValue>(name: string, value: AttributeValue): OkResult { if (this._data === undefined) { return OkResult.fail(`No internal element`, `Are you sure it can set an attribute?`) } return this._dataOps.setAttribute<AttributeValue>(this._data, name, value); } public get children(): CustomSelectorNode[] { return this._children; } public get siblings(): CustomSelectorNode[] { return this.parent === null ? [this] : this.parent.children; } deleteChildren(): void { this._children = []; } toString(): string { throw new Error("Method not implemented."); } isAttributeExist(attrName: string): boolean { return this._dataOps.getAttribute(this._data, attrName) !== undefined; } public setChildren(children: CustomSelectorNode[]) { this._children = children as ObjectNode<DataType>[]; } public setParent(parent: CustomSelectorNode) { this._parent = parent; } } export class ObjectNodeAdapter<ElementType> implements Adapter<CustomSelectorNode, ObjectNode<ElementType>>{ public isTag(elem: CustomSelectorNode): elem is ObjectNode<ElementType> { return elem.isTag; } public getChildren(elem: CustomSelectorNode): CustomSelectorNode[] { return elem.children ? Array.prototype.slice.call(elem.children, 0) : []; } public getParent(elem: ObjectNode<ElementType>): ObjectNode<ElementType>|null { return elem.parent as unknown as ObjectNode<ElementType>; } public removeSubsets(nodes: CustomSelectorNode[]): CustomSelectorNode[] { let idx = nodes.length; let node: CustomSelectorNode; let ancestor: CustomSelectorNode | null; let replace: boolean; // Check if each node (or one of its ancestors) is already contained in the // array. while(--idx > -1) { node = ancestor = nodes[idx]; // Temporarily remove the node under consideration delete nodes[idx]; replace = true; while(ancestor) { if(nodes.indexOf(ancestor) > -1) { replace = false; nodes.splice(idx, 1); break; } ancestor = this.getParent(ancestor as unknown as ObjectNode<ElementType>) } // If the node has been found to be unique, re-insert it. if(replace) { nodes[idx] = node; } } return nodes; } public existsOne(test: Predicate<ObjectNode<ElementType>>, elems: CustomSelectorNode[]): boolean { return elems.some((elem) => { return this.isTag(elem) ? test(elem) || this.existsOne(test, this.getChildren(elem)) : false; }); } public getSiblings(elem: CustomSelectorNode): CustomSelectorNode[] { const parent = this.getParent(elem as unknown as ObjectNode<ElementType>); return parent ? this.getChildren(parent) : [elem]; } public getAttributeValue(elem: ObjectNode<ElementType>, name: string): string | undefined { const attr = elem.getAttribute(name); return attr; } public hasAttrib(elem: ObjectNode<ElementType>, name: string): boolean { return (elem.getAttribute(name) !== undefined); } public getName(elem: ObjectNode<ElementType>): string { return elem.name.toLocaleLowerCase(); } public findOne( test: Predicate<ObjectNode<ElementType>>, arr: CustomSelectorNode[] ): ObjectNode<ElementType> | null { let elem: ObjectNode<ElementType> | null = null; for(let i = 0, l = arr.length; i < l && !elem; i++){ if(test(arr[i] as unknown as ObjectNode<ElementType>)){ elem = arr[i] as unknown as ObjectNode<ElementType>; } else { const childs = this.getChildren(arr[i]); if(childs && childs.length > 0){ elem = this.findOne(test, childs); } } } return elem; } public findAll(test: Predicate<ObjectNode<ElementType>>, elems: CustomSelectorNode[]): ObjectNode<ElementType>[] { let result: ObjectNode<ElementType>[] = []; for(let i = 0, j = elems.length; i < j; i++){ if(!this.isTag(elems[i])) continue; if(test(elems[i] as unknown as ObjectNode<ElementType>)) { result.push(elems[i] as unknown as ObjectNode<ElementType>); } const childs = this.getChildren(elems[i]); if(childs) { const foundChildren = this.findAll(test, childs); result = result.concat(foundChildren); } } return result; } public getText(elem: CustomSelectorNode): string { if (elem.isTag) { return this.getChildren(elem).map(this.getText).join(""); } return elem.nodeValue || ""; } }