@fastly/esi
Version:
ESI implementation for JavaScript, using the modern fetch and streaming APIs.
277 lines (276 loc) • 9.53 kB
JavaScript
/*
* Copyright Fastly, Inc.
* Licensed under the MIT license. See LICENSE file for details.
*/
// A lightweight representation of XML Elements.
// Not as heavy as @xmldom/xmldom.
import { xmlEncode } from "./xmlUtils.js";
export class XmlDocument {
constructor(namespaceDefs, allowUnknownNamespacePrefixes) {
this.namespaceDefs = namespaceDefs ?? {};
this.allowUnknownNamespacePrefixes = allowUnknownNamespacePrefixes ?? false;
}
namespaceDefs;
allowUnknownNamespacePrefixes;
}
export class XmlElement {
constructor(document, name, attrs = null, children = null) {
this.document = document;
this.parent = null;
this.namespaceDefs = {};
this.attrs = {};
if (attrs != null) {
for (const [key, value] of Object.entries(attrs)) {
if (key === 'xmlns') {
this.namespaceDefs[''] = value;
}
else if (key.startsWith('xmlns:')) {
this.namespaceDefs[key.slice(6)] = value;
}
else {
this.addAttr(key, value, false);
}
}
}
[this.localNamespacePrefix, this.localName] = this.parseName(name);
this.children = [];
if (children != null) {
for (const child of children) {
this.children.push(child);
if (child instanceof XmlElement) {
child.parent = this;
}
}
}
}
document;
parent;
localName;
localNamespacePrefix; // null for default namespace
get localFullname() {
return (this.localNamespacePrefix != null ? this.localNamespacePrefix + ':' : '') +
this.localName;
}
namespace;
namespaceDefs;
attrs;
children;
applyNamespaces() {
for (const child of this.children) {
if (child instanceof XmlElement) {
child.applyNamespaces();
}
}
if (this.namespace === undefined) {
this.namespace = this.lookupNamespace(this.localNamespacePrefix);
}
for (const [key, xmlAttr] of Object.entries(this.attrs)) {
if (xmlAttr.namespace === undefined) {
delete this.attrs[key];
xmlAttr.namespace = xmlAttr.localNamespacePrefix != null ? this.lookupNamespace(xmlAttr.localNamespacePrefix) : null;
this.attrs[(xmlAttr.namespace ?? '') + '|' + xmlAttr.localName] = xmlAttr;
}
}
}
lookupNamespace(prefix) {
const namespace = this.namespaceDefs[prefix ?? ''];
if (namespace != null) {
return namespace;
}
if (this.parent != null) {
return this.parent.lookupNamespace(prefix);
}
const documentNamespace = this.document.namespaceDefs[prefix ?? ''];
if (documentNamespace == null) {
if (this.document.allowUnknownNamespacePrefixes) {
return '';
}
throw new Error(`Unknown namespace prefix '${prefix ?? ''}'`);
}
return documentNamespace;
}
parseName(name) {
let prefix;
let localName;
[prefix, localName] = name.split(':');
if (localName == null) {
localName = prefix;
prefix = null;
}
return [prefix, localName];
}
addAttr(key, value, resolveNamespace) {
const [localNamespacePrefix, localName] = this.parseName(key);
let namespace;
if (localNamespacePrefix == null) {
namespace = null;
}
else if (resolveNamespace) {
namespace = this.lookupNamespace(localNamespacePrefix);
}
const xmlAttr = {
localName,
localNamespacePrefix,
namespace,
value,
};
if (namespace === null) {
this.attrs[localName] = xmlAttr;
}
else if (resolveNamespace) {
this.attrs[namespace + '|' + localName] = xmlAttr;
}
else {
this.attrs[localNamespacePrefix + ':' + localName] = xmlAttr;
}
}
get tagOpen() {
return '<' +
this.localFullname +
Object.values(this.attrs)
.map(xmlAttr => {
return ' ' +
(xmlAttr.localNamespacePrefix != null ? xmlAttr.localNamespacePrefix + ':' : '') +
xmlAttr.localName +
'="' + xmlEncode(xmlAttr.value) + '"';
})
.join('') +
Object.entries(this.namespaceDefs)
.map(([prefix, value]) => {
return ' xmlns' +
(prefix != '' ? ':' + prefix : '') +
'="' + xmlEncode(value) + '"';
})
.join('') +
(this.children.length === 0 ? ' /' : '') +
'>';
}
get tagClose() {
if (this.children.length === 0) {
return null;
}
return '</' + this.localFullname + '>';
}
serialize() {
const results = [];
results.push(this.tagOpen);
for (const child of this.children) {
if (typeof child === 'string') {
results.push(child);
break;
}
results.push(child.serialize());
}
results.push(this.tagClose ?? '');
return results.join('');
}
static serialize(el) {
if (el == null) {
return '';
}
if (el instanceof XmlElement) {
if (el.localName === '_replace') {
return el.children
.map(x => XmlElement.serialize(x))
.join('');
}
return el.serialize();
}
return el;
}
}
export const WalkXmlStop = Symbol();
export const WalkXmlStopRecursion = Symbol();
export async function walkXmlElements(xmlElement, beforeFunc = null, afterFunc = null, context = undefined, collectResults = false) {
async function walkXmlElementWorker(stack, node, parent, index) {
if (node instanceof XmlElement && stack.includes(node)) {
throw new Error('A cycle was detected at ' + JSON.stringify(node));
}
if (beforeFunc != null) {
const result = await beforeFunc.call(context, node, parent, index);
if (result === WalkXmlStop) {
return WalkXmlStop;
}
if (result === WalkXmlStopRecursion) {
return undefined;
}
}
let subResults;
// Collect results from subtrees
if (node instanceof XmlElement) {
if (collectResults) {
subResults = [];
}
for (const [index, child] of node.children.entries()) {
const result = await walkXmlElementWorker([...stack, node], child, node, index);
if (result === WalkXmlStop) {
return WalkXmlStop;
}
if (subResults != null) {
subResults.push(result);
}
}
}
if (afterFunc != null) {
return afterFunc.call(context, node, parent, index, subResults);
}
}
return walkXmlElementWorker([], xmlElement, null, -1);
}
export function buildTransform(document, fn) {
async function applyTransform(el) {
if (!(el instanceof XmlElement)) {
return el;
}
const prevParent = el.parent;
try {
// Wrap in a temporary parent to simplify processing
const root = new XmlElement(document, '_root', null, [
el
]);
await walkXmlElements(root, async (el, parent, index) => {
if (parent == null) {
return;
}
const result = await fn(el, parent);
if (result !== undefined) {
if (result === null) {
parent.children[index] = new XmlElement(document, '_replace', null, []);
}
else if (Array.isArray(result)) {
parent.children[index] = new XmlElement(document, '_replace', null, result);
}
else {
parent.children[index] = result;
}
return WalkXmlStopRecursion;
}
}, (el) => {
if (el instanceof XmlElement) {
el.children = el.children
.reduce((acc, el) => {
if (el instanceof XmlElement && el.localName === '_replace') {
acc.push(...el.children);
}
else {
acc.push(el);
}
return acc;
}, []);
}
});
if (root.children.length > 1) {
return new XmlElement(document, '_replace', null, root.children);
}
if (root.children.length === 1) {
return root.children[0];
}
return null;
}
finally {
// Detach from temporary parent
el.parent = prevParent;
}
}
return applyTransform;
}