101 lines
3.4 KiB
TypeScript
101 lines
3.4 KiB
TypeScript
/// <reference lib="dom" />
|
|
import { templateNoop, HTML_ESCAPES, xss } from "./string.ts"
|
|
|
|
export { HTML_ESCAPES }
|
|
|
|
/**
|
|
* Parses HTML, kinda smart.
|
|
*
|
|
* As a function, it just parses the string as HTML. When used as a tagged template, expressions are XSS escaped using {@link xss}, except Nodes, which are added to the tree.
|
|
*
|
|
* ```js
|
|
* html("<p>Hello world!</p>")
|
|
* // DocumentFragment
|
|
* // └─ p
|
|
* // └─ "Hello world!"
|
|
* html`<strong>
|
|
* ${html`<em>Look ma, nested HTML!</em>`}
|
|
* ${`<img src="/bogus/path" onerror="alert('xss')">`}
|
|
* </strong>`
|
|
*
|
|
* // DocumentFragment
|
|
* // └─ strong
|
|
* // ├─ em
|
|
* // │ └─ "Look ma, nested HTML!"
|
|
* // └─ "<img src="/bogus/path" onerror="alert('xss')">"
|
|
* ```
|
|
* @author MindfulMinun
|
|
* @since 2020-06-23
|
|
*/
|
|
export function html(content: string): DocumentFragment
|
|
export function html(strings: TemplateStringsArray, ...exprs: unknown[]): DocumentFragment
|
|
export function html(strings: TemplateStringsArray | string, ...exprs: unknown[]): DocumentFragment {
|
|
const temp = document.createElement("template")
|
|
let out = ""
|
|
|
|
if (typeof strings === "string") {
|
|
temp.innerHTML = strings
|
|
return temp.content.cloneNode(true) as DocumentFragment
|
|
}
|
|
|
|
// Join the strings together
|
|
for (let i = 0; i < strings.length; i++) {
|
|
// If it's another Node, put a template in its place so we can replace it later
|
|
if (exprs[i] instanceof Node) {
|
|
out += strings[i] + `<template data-replaceindex="${i}"></template>`
|
|
continue
|
|
}
|
|
// Otherwise, just join the expressions together, escaping dynamic content
|
|
out += strings[i] + (typeof exprs[i] != 'undefined' ? xss("" + exprs[i]) : "")
|
|
}
|
|
temp.innerHTML = out
|
|
const clone = temp.content.cloneNode(true) as DocumentFragment
|
|
|
|
// Replace all templates with the real nodes
|
|
clone.querySelectorAll("template[data-replaceindex]").forEach(node => {
|
|
if (!(node instanceof HTMLElement)) return
|
|
if (!node.parentNode) return
|
|
const index = parseInt(node.getAttribute("data-replaceindex") || "", 10)
|
|
node.parentNode.replaceChild(exprs[index] as Node, node);
|
|
})
|
|
|
|
return clone
|
|
}
|
|
|
|
/**
|
|
* Creates a text node for use in the DOM. The text content is NOT escaped.
|
|
* @author MindfulMinun
|
|
* @since 2022-06-04
|
|
*/
|
|
export function textNode(templ: string): Text
|
|
export function textNode(templ: TemplateStringsArray, ...values: unknown[]): Text
|
|
export function textNode(templ: string | TemplateStringsArray, ...values: unknown[]): Text {
|
|
const string = templateNoop(templ, ...values)
|
|
return document.createTextNode(string)
|
|
}
|
|
|
|
/**
|
|
* Walks the DOM recursively, calling the callback for each node.
|
|
* @deprecated
|
|
* @param root - The root of the DOM walk
|
|
* @param callback - If this callback returns true, stop tree traversal.
|
|
* @param filters - A bitmask of what nodes to show. Defaults to `NodeFilter.SHOW_ELEMENT`.
|
|
* @author MindfulMinun
|
|
* @since 2020-06-29
|
|
*/
|
|
export function domWalker(
|
|
root: Node,
|
|
callback: (node: Node, root: Node, walker: TreeWalker) => boolean,
|
|
filters: number = NodeFilter.SHOW_ELEMENT
|
|
) {
|
|
const walker = document.createTreeWalker(root, filters);
|
|
let guard = false;
|
|
let current: Node | null = walker.currentNode
|
|
while (true) {
|
|
if (!current) break
|
|
if (guard) break
|
|
guard = callback.call(null, current, root, walker);
|
|
current = walker.nextNode()
|
|
}
|
|
}
|