Skip to content
Permalink
Browse files

New: Add a snapshot utility

  • Loading branch information...
antross committed Apr 9, 2019
1 parent 95afd62 commit 0edaba4b16425d9ac1051e673c8ed9e7abfbcc02
@@ -0,0 +1,49 @@
import { ElementLocation } from 'parse5';

export type ChildData = CommentData | ElementData | TextData;
export type DocumentChildData = ChildData | DoctypeData;
export type NodeData = DocumentData | ChildData;

type BaseData = {
id?: string;
next: ChildData | null;
parent: ElementData | null;
prev: ChildData | null;
};

export type CommentData = BaseData & {
data: string;
type: 'comment';
};

export type DoctypeData = BaseData & {
data: string;
name: '!doctype';
nodeName?: string;
publicId?: string;
systemId?: string;
type: 'directive';
};

export type DocumentData = {
children: DocumentChildData[];
name: 'root';
type: 'root';
'x-mode': 'no-quirks' | 'quirks' | 'limited-quirks';
};

export type ElementData = BaseData & {
attribs: { [name: string]: string };
children: ChildData[];
name: string;
namespace?: string;
sourceCodeLocation?: ElementLocation;
type: 'script' | 'style' | 'tag';
'x-attribsNamespace': { [name: string]: string };
'x-attribsPrefix': { [name: string]: string };
};

export type TextData = BaseData & {
data: string;
type: 'text';
};
@@ -0,0 +1,123 @@
import { NodeData } from '../types/snapshot';

export const inject = () => {
let nextId = 1;
const nodeIds = new WeakMap<Node, number>();

const getId = (node: Node): number => {
if (nodeIds.has(node)) {
return nodeIds.get(node)!;
}

const id = nextId++;

nodeIds.set(node, id);

return id;
};

const findNode = (id: number, list: Iterable<Node>): Node | null => {
for (const node of list) {
if (nodeIds.get(node) === id) {
return node;
} else if (node.childNodes) {
const match = findNode(id, node.childNodes);

if (match) {
return match;
}
}
}

return null;
};

const snapshotAttr = (obj, attr: Attr) => {
obj.attribs[attr.name] = attr.value;
obj['x-attribsNamespace'][attr.name] = attr.namespaceURI;
obj['x-attribsPrefix'][attr.name] = attr.prefix;

return obj;
};

const snapshot = (node: Node): NodeData => {
const id = getId(node);

if (node instanceof Comment) {
return {
data: node.nodeValue,
id,
next: null,
parent: null,
prev: null,
type: 'comment'
};
} else if (node instanceof Document) {
return {
children: Array.from(node.childNodes).map(snapshot),
name: 'root',
type: 'root',
'x-mode': document.compatMode === 'BackCompat' ? 'quirks' : 'no-quirks'
};
} else if (node instanceof DocumentType) {
return {
data: node.nodeValue,
id,
name: '!doctype',
nodeName: node.name,
publicId: node.publicId,
systemId: node.systemId,
type: 'directive'
};
} else if (node instanceof Element) {
const name = node.nodeName.toLowerCase();
const attrs = Array.from(node.attributes).reduce(snapshotAttr, {
attribs: {},
'x-attribsNamespace': {},
'x-attribsPrefix': {}
});

return {
attribs: attrs.attribs,
children: Array.from(node.childNodes).map(snapshot),
id,
name,
namespace: node.namespaceURI,
next: null,
parent: null,
prev: null,
type: name === 'script' || name === 'style' ? name : 'tag',
'x-attribsNamespace': attrs['x-attribsNamespace'],
'x-attribsPrefix': attrs['x-attribsPrefix']
};
} else if (node instanceof Text) {
return {
data: node.nodeValue,
id,
next: null,
parent: null,
prev: null,
type: 'text'
};
}

throw new Error(`Unexpected node type: ${node.nodeType}`);
};

const win = window as any;

win.__webhint__ = win.__webhint__ || {};
win.__webhint__.findNode = findNode;
};

export const setReferences = (node: NodeData, index: number, arr: NodeData[], parent?: NodeData) => {
node.next = arr[index + 1] || null;
node.parent = parent;
node.prev = arr[index - 1] || null;

if (node.children) {
node.children.forEach((n: NodeData, i: number, a: NodeData[]) => {
setReferences(n, i, a, node);
});
}
};
@@ -6,6 +6,8 @@ import { ProblemLocation } from '../types/problem-location';
import { findOriginalElement } from './find-original-element';
import { INamedNodeMap } from '../types/html';

import { DocumentData, ElementData } from './snapshot';

type Attrib = {
[key: string]: string;
};
@@ -24,9 +26,9 @@ type ParsedHTMLElement = {
export class HTMLElement {
public ownerDocument?: HTMLDocument;

private _element: ParsedHTMLElement;
private _element: ElementData;

public constructor(element: ParsedHTMLElement | HTMLElement, ownerDocument?: HTMLDocument) {
public constructor(element: ElementData | HTMLElement, ownerDocument?: HTMLDocument) {
this._element = element instanceof HTMLElement ? element._element : element;
this.ownerDocument = ownerDocument;
}
@@ -46,16 +48,16 @@ export class HTMLElement {
const result: HTMLElement[] = [];

for (const child of this._element.children) {
if (child.nodeType === 1) {
result.push(new HTMLElement(child as ParsedHTMLElement, this.ownerDocument));
if (child.type === 'tag' || child.type === 'script' || child.type === 'style') {
result.push(new HTMLElement(child, this.ownerDocument));
}
}

return result;
}

public get nodeName(): string {
return this._element.tagName;
return this._element.name;
}

public getAttribute(attribute: string): string | null {
@@ -183,21 +185,21 @@ export class HTMLElement {
}

export class HTMLDocument {
private _document: any;
private _document: DocumentData;
private _pageHTML = '';

public originalDocument?: HTMLDocument;

public constructor(document: parse5.Document, originalDocument?: HTMLDocument) {
public constructor(document: DocumentData, originalDocument?: HTMLDocument) {
this._document = document;
this.originalDocument = originalDocument;
this._pageHTML = parse5.serialize(document, { treeAdapter: htmlparser2Adapter });
}

public get documentElement(): HTMLElement {
const htmlNode = this._document.children.find((node: any) => {
const htmlNode = this._document.children.find((node) => {
return node.type === 'tag' && node.name === 'html';
});
}) as ElementData;

return new HTMLElement(htmlNode, this);
}

0 comments on commit 0edaba4

Please sign in to comment.
You can’t perform that action at this time.