UNPKG

@worker-tools/parsed-html-rewriter

Version:

A DOM-based implementation of Cloudflare Worker's HTMLRewriter.

201 lines (166 loc) 5.47 kB
export type Awaitable<T> = T | Promise<T>; export function append<K, V>(m: Map<K, V[]>, k: K, v: V) { const vs = m.get(k) ?? []; vs.push(v); return m.set(k, vs); } export async function* promiseToAsyncIterable<T>(promise: Promise<T>): AsyncIterableIterator<T> { yield await promise; } export function* treeWalkerToIter(walker: TreeWalker): IterableIterator<Node> { let node = walker.nextNode(); while (node) { yield node; node = walker.nextNode(); } } type Content = string; /** Fragment form string function that works with linkedom. */ function fragmentFromString(document: Document, html: string) { const temp = document.createElement('template'); temp.innerHTML = html; return temp.content; } function replace(document: Document, node: Element | Text | Comment | null, content: string, opts?: ContentOptions) { node?.replaceWith(...opts?.html ? fragmentFromString(document, content).childNodes // depends on DOM.Iterable : [content]); } export class ParsedHTMLRewriterNode { #node: Element | Text | Comment | null; #doc: Document; constructor(node: Element | Text | Comment | null, document: Document) { this.#node = node; this.#doc = document; } get removed() { return !this.#doc.contains(this.#node) } before(content: Content, opts?: ContentOptions): this { const before = this.#doc.createComment(''); this.#node?.parentElement?.insertBefore(before, this.#node) replace(this.#doc, before, content, opts); return this; } after(content: Content, opts?: ContentOptions): this { const after = this.#doc.createComment(''); this.#node?.parentElement?.insertBefore(after, this.#node.nextSibling) replace(this.#doc, after, content, opts); return this; } replace(content: Content, opts?: ContentOptions): this { replace(this.#doc, this.#node, content, opts); return this; } remove(): this { this.#node?.remove() return this; } } export class ParsedHTMLRewriterElement extends ParsedHTMLRewriterNode { #node: Element; constructor(node: Element, document: Document) { super(node, document) this.#node = node; } get tagName() { return this.#node.tagName.toLowerCase() } get attributes(): Iterable<[string, string]> { return [...this.#node.attributes].map(attr => [attr.name, attr.value]); } get namespaceURI() { return this.#node.namespaceURI } getAttribute(name: string) { return this.#node.getAttribute(name); } hasAttribute(name: string) { return this.#node.hasAttribute(name); } setAttribute(name: string, value: string): this { this.#node.setAttribute(name, value); return this; } removeAttribute(name: string): this { this.#node.removeAttribute(name); return this; } prepend(content: Content, opts?: ContentOptions): this { return this.before(content, opts); } append(content: Content, opts?: ContentOptions): this { return this.after(content, opts); } setInnerContent(content: Content, opts?: ContentOptions): this { this.#node[opts?.html ? 'innerHTML' : 'textContent'] = content; return this; } removeAndKeepContent(): this { this.#node?.replaceWith(...this.#node.childNodes); return this; } } export class ParsedHTMLRewriterText extends ParsedHTMLRewriterNode { #text: Text | null; #done: boolean; constructor(text: Text | null, document: Document) { super(text, document); this.#text = text; this.#done = text === null; } get text() { return this.#text?.textContent ?? '' } get lastInTextNode() { return this.#done } } export class ParsedHTMLRewriterComment extends ParsedHTMLRewriterNode { #comm: Comment; constructor(comm: Comment, document: Document) { super(comm, document); this.#comm = comm; } get text() { return this.#comm.textContent ?? '' } set text(value: string) { this.#comm.textContent = value } } export class ParsedHTMLRewriterDocumentType { #doctype: DocumentType; constructor(doctype: DocumentType) { this.#doctype = doctype; } get name() { return this.#doctype.name } get publicId() { return this.#doctype.publicId } get systemId() { return this.#doctype.systemId } } export class ParsedHTMLRewriterEnd { #doc: Document; constructor(document: Document) { this.#doc = document; } append(content: Content, opts?: ContentOptions): this { const after = this.#doc.createComment(''); this.#doc.insertBefore(after, null); replace(this.#doc, after, content, opts); return this; } } // function* ancestors(el: Node) { // while (el.parentElement) { // yield el.parentElement // el = el.parentElement // } // } // function root(el: Node): globalThis.HTMLElement | undefined { // const ancs = [...ancestors(el)] // return ancs[ancs.length - 1]; // } // function* zip<X, Y>(xs: Iterable<X>, ys: Iterable<Y>): IterableIterator<[X, Y]> { // const xit = xs[Symbol.iterator](); // const yit = ys[Symbol.iterator](); // while (true) { // const [xr, yr] = [xit.next(), yit.next()]; // if (xr.done || yr.done) break; // yield [xr.value, yr.value]; // } // } // /* Checks if this element or any of its parents matches a given `selector`. */ // function matchesAncestors(el: Element | null, selector: string): Element | null { // let curr = el; // while (curr != null) { // if (curr.matches(selector)) return curr; // curr = curr.parentElement; // } // return null; // }