@ara-web/sds
Version:
Write code faster without worry about future debts or scalability in the future
391 lines (350 loc) • 14.3 kB
text/typescript
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 || "";
}
}