Skip to content

Commit

Permalink
feat: use alternative selector engine Sizzle.js to avoid code generat…
Browse files Browse the repository at this point in the history
…ion (#90) (fixes #75)

feat: use alternative selector engine Sizzle.js to avoid code generation
  • Loading branch information
b-fuze authored Apr 2, 2022
2 parents 62715db + ccf34cb commit 29c9e1f
Show file tree
Hide file tree
Showing 13 changed files with 2,792 additions and 12 deletions.
3 changes: 2 additions & 1 deletion deno.jsonc
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@
"exclude": [
"./wpt",
"./bench/node_modules",
"./src/dom/nwsapi.js",
"./src/dom/selectors/nwsapi.js",
"./src/dom/selectors/sizzle.js",
"./target",
"./html-parser",
"./.vim"
Expand Down
1 change: 1 addition & 0 deletions src/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ export * from "./dom/element.ts";
export * from "./dom/document.ts";
export * from "./dom/document-fragment.ts";
export * from "./dom/dom-parser.ts";
export { disableCodeGeneration as denoDomDisableQuerySelectorCodeGeneration } from "./dom/selectors/selectors.ts";

// Re-export private constructors without constructor signature
import {
Expand Down
56 changes: 56 additions & 0 deletions src/dom/document-fragment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,11 @@ import { HTMLCollection } from "./html-collection.ts";
import { NodeList, nodeListMutatorSym } from "./node-list.ts";
import { Node, nodesAndTextNodes, NodeType } from "./node.ts";
import { Element } from "./element.ts";
import {
customByClassNameSym,
customByTagNameSym,
} from "./selectors/custom-api.ts";
import { getElementsByClassName } from "./utils.ts";
import UtilTypes from "./utils-types.ts";

export class DocumentFragment extends Node {
Expand Down Expand Up @@ -98,3 +103,54 @@ export class DocumentFragment extends Node {
}

UtilTypes.DocumentFragment = DocumentFragment;

// Add required methods just for Sizzle.js selector to work on
// DocumentFragment's
function documentFragmentGetElementsByTagName(
this: DocumentFragment,
tagName: string,
): Node[] {
const search: Node[] = [];

if (tagName === "*") {
return documentFragmentGetElementsByTagNameWildcard(this, search);
}

for (const child of this.childNodes) {
if (child.nodeType === NodeType.ELEMENT_NODE) {
if ((<Element> child).tagName === tagName) {
search.push(child);
}

(<Element> child)._getElementsByTagName(tagName, search);
}
}

return search;
}

function documentFragmentGetElementsByClassName(
this: DocumentFragment,
className: string,
) {
return getElementsByClassName(this, className, []);
}

function documentFragmentGetElementsByTagNameWildcard(
fragment: DocumentFragment,
search: Node[],
): Node[] {
for (const child of fragment.childNodes) {
if (child.nodeType === NodeType.ELEMENT_NODE) {
search.push(child);
(<Element> child)._getElementsByTagNameWildcard(search);
}
}

return search;
}

(DocumentFragment as any).prototype[customByTagNameSym] =
documentFragmentGetElementsByTagName;
(DocumentFragment as any).prototype[customByClassNameSym] =
documentFragmentGetElementsByClassName;
10 changes: 5 additions & 5 deletions src/dom/document.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { Comment, Node, NodeType, Text } from "./node.ts";
import { NodeList, nodeListMutatorSym } from "./node-list.ts";
import { Element } from "./element.ts";
import { DocumentFragment } from "./document-fragment.ts";
import { DOM as NWAPI } from "./nwsapi-types.ts";
import { getSelectorEngine, SelectorApi } from "./selectors/selectors.ts";
import { getElementsByClassName } from "./utils.ts";
import UtilTypes from "./utils-types.ts";

Expand Down Expand Up @@ -121,7 +121,7 @@ export class Document extends Node {
#lockState = false;
#documentURI = "about:blank"; // TODO
#title = "";
#nwapi = NWAPI(this);
#nwapi: SelectorApi | null = null;

constructor() {
super(
Expand All @@ -141,7 +141,7 @@ export class Document extends Node {
// Expose the document's NWAPI for Element's access to
// querySelector/querySelectorAll
get _nwapi() {
return this.#nwapi;
return this.#nwapi || (this.#nwapi = getSelectorEngine()(this));
}

get documentURI() {
Expand Down Expand Up @@ -260,13 +260,13 @@ export class Document extends Node {
}

querySelector(selectors: string): Element | null {
return this.#nwapi.first(selectors, this);
return this._nwapi.first(selectors, this);
}

querySelectorAll(selectors: string): NodeList {
const nodeList = new NodeList();
const mutator = nodeList[nodeListMutatorSym]();
mutator.push(...this.#nwapi.select(selectors, this));
mutator.push(...this._nwapi.select(selectors, this));

return nodeList;
}
Expand Down
4 changes: 2 additions & 2 deletions src/dom/element.ts
Original file line number Diff line number Diff line change
Expand Up @@ -476,7 +476,7 @@ export class Element extends Node {
}
}

private _getElementsByTagNameWildcard(search: Node[]): Node[] {
_getElementsByTagNameWildcard(search: Node[]): Node[] {
for (const child of this.childNodes) {
if (child.nodeType === NodeType.ELEMENT_NODE) {
search.push(child);
Expand All @@ -487,7 +487,7 @@ export class Element extends Node {
return search;
}

private _getElementsByTagName(tagName: string, search: Node[]): Node[] {
_getElementsByTagName(tagName: string, search: Node[]): Node[] {
for (const child of this.childNodes) {
if (child.nodeType === NodeType.ELEMENT_NODE) {
if ((<Element> child).tagName === tagName) {
Expand Down
6 changes: 6 additions & 0 deletions src/dom/selectors/custom-api.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
/**
* Symbols for using getElementsByTagName/ClassName on document-fragment
*/

export const customByTagNameSym = Symbol();
export const customByClassNameSym = Symbol();
6 changes: 3 additions & 3 deletions src/dom/nwsapi-types.ts → src/dom/selectors/nwsapi-types.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import NWDom from "./nwsapi.js";
import type { Element } from "./element.ts";
import type { Document } from "./document.ts";
import type { Element } from "../element.ts";
import type { Document } from "../document.ts";

export const DOM: (doc: Document) => {
ancestor(
Expand All @@ -26,4 +26,4 @@ export const DOM: (doc: Document) => {
byId(id: string, from: Element | Document): Element[];
byTag(tag: string, from: Element | Document): Element[];
byClass(tag: string, from: Element | Document): Element[];
} = <any> NWDom;
} = NWDom as any;
File renamed without changes.
51 changes: 51 additions & 0 deletions src/dom/selectors/selectors.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { DOM as NWAPI } from "./nwsapi-types.ts";
import { DOM as Sizzle } from "./sizzle-types.ts";
import type { Element } from "../element.ts";
import type { Document } from "../document.ts";

export type Selector = (doc: Document) => {
first(
selector: string,
context: Element | Document,
callback?: (element: Element) => void,
): Element | null;
match(
selector: string,
context: Element | Document,
callback?: (element: Element) => void,
): boolean;
select(
selector: string,
context: Element | Document,
callback?: (element: Element) => void,
): Element[];
};
export type SelectorApi = ReturnType<Selector>;

let codeGenerationAllowed: boolean | null = null;

export function getSelectorEngine(): Selector {
if (codeGenerationAllowed === null) {
try {
new Function("");
codeGenerationAllowed = true;
} catch (e) {
codeGenerationAllowed = false;
}
}

if (codeGenerationAllowed) {
return NWAPI;
} else {
return Sizzle;
}
}

/**
* Explicitly disable querySelector/All code generation with the `Function`
* constructor forcing the Sizzle engine. Enables those APIs on platforms
* like Deno Deploy that don't allow code generation.
*/
export function disableCodeGeneration() {
codeGenerationAllowed = false;
}
18 changes: 18 additions & 0 deletions src/dom/selectors/sizzle-types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import Sizzle from "./sizzle.js";
import type { Element } from "../element.ts";
import type { Document } from "../document.ts";

export const DOM: (doc: Document) => {
first(
selector: string,
context: Element | Document,
): Element | null;
match(
selector: string,
context: Element | Document,
): boolean;
select(
selector: string,
context: Element | Document,
): Element[];
} = Sizzle as any;
Loading

0 comments on commit 29c9e1f

Please sign in to comment.