| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
|
|
|
|
| import { parse, serialize } from "parse5";
|
| import type { DefaultTreeAdapterMap } from "parse5";
|
|
|
| type Document = DefaultTreeAdapterMap["document"];
|
| type Element = DefaultTreeAdapterMap["element"];
|
| type Node = DefaultTreeAdapterMap["node"];
|
| type Attr = { name: string; value: string; namespace?: string; prefix?: string };
|
|
|
| // Matches /foo but not //foo, http://, https://, mailto:, javascript:, #, etc.
|
| const ABS_PATH_RE = /^\/(?!\/)/;
|
|
|
| // Schemes that should NOT be rewritten β leave them as-is.
|
| const SKIP_SCHEMES = ["http:", "https:", "mailto:", "javascript:", "data:", "blob:", "ftp:"];
|
|
|
| const REWRITABLE_ATTRS: Record<string, string[]> = {
|
| a: ["href"],
|
| link: ["href"],
|
| script: ["src"],
|
| img: ["src", "srcset"],
|
| form: ["action"],
|
| iframe: ["src"],
|
| source: ["src", "srcset"],
|
| };
|
|
|
| |
| |
| |
| |
| |
| |
| |
|
|
| export function rewriteHtml(html: string, publicPrefix: string): string {
|
| const prefix = publicPrefix.endsWith("/") ? publicPrefix.slice(0, -1) : publicPrefix;
|
|
|
| const doc = parse(html) as Document;
|
|
|
| const headEl = findOrCreateHead(doc);
|
| injectBase(headEl, `${prefix}/`);
|
| visitNode(doc, prefix);
|
|
|
| return serialize(doc);
|
| }
|
|
|
|
|
|
|
| |
| |
| |
| |
|
|
| function findOrCreateHead(doc: Document): Element {
|
| for (const child of doc.childNodes) {
|
| if (isElement(child) && child.tagName === "html") {
|
| for (const htmlChild of child.childNodes) {
|
| if (isElement(htmlChild) && htmlChild.tagName === "head") {
|
| return htmlChild;
|
| }
|
| }
|
|
|
| const head = makeElement("head");
|
| child.childNodes.unshift(head);
|
| (head as Element & { parentNode: Node }).parentNode = child;
|
| return head;
|
| }
|
| }
|
|
|
|
|
|
|
| const htmlEl = makeElement("html");
|
| const head = makeElement("head");
|
| head.childNodes = [];
|
| (head as Element & { parentNode: Node }).parentNode = htmlEl;
|
| htmlEl.childNodes = [head as Node];
|
| (htmlEl as Element & { parentNode: Node }).parentNode = doc as unknown as Node;
|
| doc.childNodes.push(htmlEl as unknown as Node);
|
| return head;
|
| }
|
|
|
| |
| |
| |
|
|
| function injectBase(head: Element, baseHref: string): void {
|
|
|
| for (const child of head.childNodes) {
|
| if (isElement(child) && child.tagName === "base") {
|
| const hrefAttr = child.attrs.find((a) => a.name === "href");
|
| if (hrefAttr) {
|
| hrefAttr.value = baseHref;
|
| } else {
|
| child.attrs.push({ name: "href", value: baseHref });
|
| }
|
| return;
|
| }
|
| }
|
|
|
|
|
| const baseEl = makeElement("base");
|
| baseEl.attrs = [{ name: "href", value: baseHref }];
|
| baseEl.childNodes = [];
|
| (baseEl as Element & { parentNode: Node }).parentNode = head;
|
| head.childNodes.unshift(baseEl as unknown as Node);
|
| }
|
|
|
|
|
| function visitNode(node: Node, prefix: string): void {
|
| if (isElement(node)) {
|
| const tag = node.tagName.toLowerCase();
|
| const rewritable = REWRITABLE_ATTRS[tag];
|
| if (rewritable) {
|
| for (const attr of node.attrs as Attr[]) {
|
| if (rewritable.includes(attr.name)) {
|
| attr.value = rewriteUrl(attr.value, prefix);
|
| }
|
| }
|
| }
|
| }
|
|
|
| const children = (node as { childNodes?: Node[] }).childNodes;
|
| if (children) {
|
| for (const child of children) {
|
| visitNode(child, prefix);
|
| }
|
| }
|
| }
|
|
|
| |
| |
| |
| |
|
|
| function rewriteUrl(value: string, prefix: string): string {
|
| const trimmed = value.trim();
|
| if (!trimmed) return value;
|
|
|
|
|
| if (trimmed.includes(",")) return value;
|
|
|
|
|
| for (const scheme of SKIP_SCHEMES) {
|
| if (trimmed.toLowerCase().startsWith(scheme)) return value;
|
| }
|
|
|
|
|
| if (ABS_PATH_RE.test(trimmed)) {
|
| return `${prefix}${trimmed}`;
|
| }
|
|
|
| return value;
|
| }
|
|
|
| function isElement(node: Node): node is Element {
|
| return (node as Element).attrs !== undefined && (node as Element).tagName !== undefined;
|
| }
|
|
|
| function makeElement(tag: string): Element {
|
| return {
|
| nodeName: tag,
|
| tagName: tag,
|
| attrs: [],
|
| namespaceURI: "http://www.w3.org/1999/xhtml",
|
| childNodes: [],
|
| parentNode: null as unknown as Node,
|
| } as unknown as Element;
|
| }
|
|
|