Skip to content
Permalink
Browse files

Chore: Reference elements from snapshots in reports

  • Loading branch information...
antross committed Apr 9, 2019
1 parent 0edaba4 commit aae4a30c02a0635328d98589a6020ec0a27f4579

This file was deleted.

Oops, something went wrong.
@@ -1,7 +1,9 @@
import { HTMLDocument } from './html';
import * as parse5 from 'parse5';
import * as htmlparser2Adapter from 'parse5-htmlparser2-tree-adapter';

import { DocumentData } from '../types/snapshot';
import { HTMLDocument } from './html';

/**
* Create an HTMLDocument object from an string.
* @param {string} html - html to create the object HTMLDocument
@@ -11,7 +13,7 @@ export const createHTMLDocument = (html: string, originalDocument?: HTMLDocument
const dom = parse5.parse(html, {
sourceCodeLocationInfo: !originalDocument,
treeAdapter: htmlparser2Adapter
});
}) as DocumentData;

return new HTMLDocument(dom, originalDocument);
};
@@ -6,22 +6,7 @@ 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;
};

type ParsedHTMLElement = {
attribs: Attrib;
children: ParsedHTMLElement[];
next: ParsedHTMLElement | null;
nodeType: number;
parent: ParsedHTMLElement | null;
prev: ParsedHTMLElement | null;
sourceCodeLocation: parse5.ElementLocation;
tagName: string;
}
import { DocumentData, ElementData } from '../types/snapshot';

export class HTMLElement {
public ownerDocument?: HTMLDocument;
@@ -86,7 +71,7 @@ export class HTMLElement {
const match = findOriginalElement(this.ownerDocument.originalDocument, this);

if (match) {
return match._element.sourceCodeLocation;
return match._element.sourceCodeLocation || null;
}
}

@@ -107,6 +92,7 @@ export class HTMLElement {
// Column is zero-based, but pointing to the tag name, not the character <
return {
column: location.startCol,
elementId: this._element.id,
line: location.startLine - 1
};
}
@@ -0,0 +1,206 @@
import { ElementLocation } from 'parse5';

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

/**
* Inject and invoke within the context of a page to generate global
* `webhint` helpers for creating DOM snapshots and resolving
* unique IDs to `Node`s. Exposes:
* * `__webhint.snapshotDocument(doc?: Document): DocumentData`
* * `__webhint.findNode(id: number): Node`
*
* ```js
* browser.devtools.inspectedWindow.eval(`(${createHelpers})()`);
*
* const snapshot = browser.devtools.inspectedWindow.eval('__webhint.snapshotDocument()');
* ```
*/
/* istanbul ignore next */
export const createHelpers = () => {
let nextId = 1;
const idSymbol = Symbol('webhint-node-id');

/**
* Retrieve the unique ID assigned to a `Node`,
* creating a new one if no ID has been assigned yet.
*/
const getId = (node: Node & { [idSymbol]?: number }): number => {
if (!node[idSymbol]) {
node[idSymbol] = nextId++;
}

return node[idSymbol]!;
};

/**
* Retrieve the source code location for the provided node (if available).
*/
const getLocation = (node: Node): ElementLocation | null => {
const __webhint = (window as any).__webhint;

if (__webhint && __webhint.nodeLocation) {
return __webhint.nodeLocation(node);
}

return null;
};

/**
* Find a node based on a previously assigned unique ID.
*/
const findNode = (id: number, list = document.childNodes): Node | null => {
for (const node of list) {
if (getId(node) === id) {
return node;
} else if (node.childNodes) {
const match = findNode(id, node.childNodes);

if (match) {
return match;
}
}
}

return null;
};

type AttrData = {
attribs: { [name: string]: string };
'x-attribsNamespace': { [name: string]: string | null };
'x-attribsPrefix': { [name: string]: string | null };
};

/**
* Snapshot attribute data in the modified `htmlparser2` format
* used by `parse5-htmlparser2-tree-adapter` (which accounts for
* namespaces).
*/
const snapshotAttr = (data: AttrData, attr: Attr): AttrData => {
data.attribs[attr.name] = attr.value;
data['x-attribsNamespace'][attr.name] = attr.namespaceURI;
data['x-attribsPrefix'][attr.name] = attr.prefix;

return data;
};

/**
* Recursively snapshot the data for the provided `Node` in the
* modified `htmlparser2` format used by
* `parse5-htmlparser2-tree-adapter`.
*/
const snapshot = (node: Node): ChildData => {
const id = getId(node);
const sourceCodeLocation = getLocation(node);

if (node instanceof Comment) {
return {
data: node.nodeValue || '',
id,
next: null,
parent: null,
prev: null,
sourceCodeLocation,
type: 'comment'
};
} else if (node instanceof DocumentType) {
return {
data: node.nodeValue || '',
id,
name: '!doctype',
next: null,
nodeName: node.name,
parent: null,
prev: null,
publicId: node.publicId,
sourceCodeLocation,
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,
sourceCodeLocation,
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,
sourceCodeLocation,
type: 'text'
};
}

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

/**
* Recursively snapshot the DOM data for the provided `Document`
* in the modified `htmlparser2` format used by
* `parse5-htmlparser2-tree-adapter`.
*/
const snapshotDocument = (doc = document): DocumentData => {
return {
children: Array.from(doc.childNodes).map(snapshot),
name: 'root',
type: 'root',
'x-mode': document.compatMode === 'BackCompat' ? 'quirks' : 'no-quirks'
};
};

// Export helpers for later use from external script.
(window as any).__webhint = {
...(window as any).__webhint,
findNode,
snapshotDocument
};
};

/**
* Recursively rebuild parent and sibling references in a DOM snapshot.
*/
const restoreChildReferences = (node: ChildData, index: number, arr: ChildData[], parent: DocumentData | ElementData) => {
node.next = arr[index + 1] || null;
node.parent = parent;
node.prev = arr[index - 1] || null;

if ('children' in node) {
node.children.forEach((n: ChildData, i: number, a: ChildData[]) => {
restoreChildReferences(n, i, a, node);
});
}
};

/**
* Rebuild parent and sibling references in a DOM snapshot.
*
* These are initially omitted from snapshots so the data can be
* passed across contexts that require serialization to JSON.
*
* Once re-parsed these references must be set in order for helper
* libraries like `css-select` to work.
*/
export const restoreReferences = (doc: DocumentData) => {
doc.children.forEach((n, i, a) => {
restoreChildReferences(n, i, a, doc);
});
};
Oops, something went wrong.

0 comments on commit aae4a30

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