363 lines
10 KiB
JavaScript
363 lines
10 KiB
JavaScript
|
|
const ATTR_ESCAPE_RE = /[&<>"]/g;
|
||
|
|
const ATTR_ESCAPE_MAP = {
|
||
|
|
"&": "&",
|
||
|
|
"<": "<",
|
||
|
|
">": ">",
|
||
|
|
'"': """,
|
||
|
|
};
|
||
|
|
function escapeAttribute(value) {
|
||
|
|
return value.replace(ATTR_ESCAPE_RE, (ch) => ATTR_ESCAPE_MAP[ch]);
|
||
|
|
}
|
||
|
|
|
||
|
|
const ELEMENT_ESCAPE_RE = /[&"'<>\r\n\u0085\u2028]/g;
|
||
|
|
const ELEMENT_ESCAPE_MAP = {
|
||
|
|
"&": "&",
|
||
|
|
'"': """,
|
||
|
|
"'": "'",
|
||
|
|
"<": "<",
|
||
|
|
">": ">",
|
||
|
|
"\r": "
",
|
||
|
|
"\n": "
",
|
||
|
|
"\u0085": "…",
|
||
|
|
"\u2028": "
",
|
||
|
|
};
|
||
|
|
function escapeElement(value) {
|
||
|
|
return value.replace(ELEMENT_ESCAPE_RE, (ch) => ELEMENT_ESCAPE_MAP[ch]);
|
||
|
|
}
|
||
|
|
|
||
|
|
class XmlText {
|
||
|
|
value;
|
||
|
|
constructor(value) {
|
||
|
|
this.value = value;
|
||
|
|
}
|
||
|
|
toString() {
|
||
|
|
return escapeElement("" + this.value);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
class XmlNode {
|
||
|
|
name;
|
||
|
|
children;
|
||
|
|
attributes = {};
|
||
|
|
static of(name, childText, withName) {
|
||
|
|
const node = new XmlNode(name);
|
||
|
|
if (childText !== undefined) {
|
||
|
|
node.addChildNode(new XmlText(childText));
|
||
|
|
}
|
||
|
|
if (withName !== undefined) {
|
||
|
|
node.withName(withName);
|
||
|
|
}
|
||
|
|
return node;
|
||
|
|
}
|
||
|
|
constructor(name, children = []) {
|
||
|
|
this.name = name;
|
||
|
|
this.children = children;
|
||
|
|
}
|
||
|
|
withName(name) {
|
||
|
|
this.name = name;
|
||
|
|
return this;
|
||
|
|
}
|
||
|
|
addAttribute(name, value) {
|
||
|
|
this.attributes[name] = value;
|
||
|
|
return this;
|
||
|
|
}
|
||
|
|
addChildNode(child) {
|
||
|
|
this.children.push(child);
|
||
|
|
return this;
|
||
|
|
}
|
||
|
|
removeAttribute(name) {
|
||
|
|
delete this.attributes[name];
|
||
|
|
return this;
|
||
|
|
}
|
||
|
|
n(name) {
|
||
|
|
this.name = name;
|
||
|
|
return this;
|
||
|
|
}
|
||
|
|
c(child) {
|
||
|
|
this.children.push(child);
|
||
|
|
return this;
|
||
|
|
}
|
||
|
|
a(name, value) {
|
||
|
|
if (value != null) {
|
||
|
|
this.attributes[name] = value;
|
||
|
|
}
|
||
|
|
return this;
|
||
|
|
}
|
||
|
|
cc(input, field, withName = field) {
|
||
|
|
if (input[field] != null) {
|
||
|
|
const node = XmlNode.of(field, input[field]).withName(withName);
|
||
|
|
this.c(node);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
l(input, listName, memberName, valueProvider) {
|
||
|
|
if (input[listName] != null) {
|
||
|
|
const nodes = valueProvider();
|
||
|
|
nodes.map((node) => {
|
||
|
|
node.withName(memberName);
|
||
|
|
this.c(node);
|
||
|
|
});
|
||
|
|
}
|
||
|
|
}
|
||
|
|
lc(input, listName, memberName, valueProvider) {
|
||
|
|
if (input[listName] != null) {
|
||
|
|
const nodes = valueProvider();
|
||
|
|
const containerNode = new XmlNode(memberName);
|
||
|
|
nodes.map((node) => {
|
||
|
|
containerNode.c(node);
|
||
|
|
});
|
||
|
|
this.c(containerNode);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
toString() {
|
||
|
|
const hasChildren = Boolean(this.children.length);
|
||
|
|
let xmlText = `<${this.name}`;
|
||
|
|
const attributes = this.attributes;
|
||
|
|
for (const attributeName of Object.keys(attributes)) {
|
||
|
|
const attribute = attributes[attributeName];
|
||
|
|
if (attribute != null) {
|
||
|
|
xmlText += ` ${attributeName}="${escapeAttribute("" + attribute)}"`;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return (xmlText += !hasChildren ? "/>" : `>${this.children.map((c) => c.toString()).join("")}</${this.name}>`);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
function parseXML(xml) {
|
||
|
|
const state = new AwsXmlParser(xml);
|
||
|
|
return state.parse();
|
||
|
|
}
|
||
|
|
class AwsXmlParser {
|
||
|
|
x;
|
||
|
|
i = 0;
|
||
|
|
z;
|
||
|
|
constructor(x) {
|
||
|
|
this.x = x;
|
||
|
|
this.x = x.replace(/\r\n?/g, "\n");
|
||
|
|
this.z = this.x.length;
|
||
|
|
}
|
||
|
|
parse() {
|
||
|
|
const p = this;
|
||
|
|
const { z } = p;
|
||
|
|
while (p.i < z) {
|
||
|
|
p.trim();
|
||
|
|
if (p.i >= z) {
|
||
|
|
break;
|
||
|
|
}
|
||
|
|
if (p.isNext("<?")) {
|
||
|
|
p.readTo("?>");
|
||
|
|
p.trim();
|
||
|
|
}
|
||
|
|
else if (p.isNext("<!--")) {
|
||
|
|
p.readTo("-->");
|
||
|
|
p.trim();
|
||
|
|
}
|
||
|
|
else if (p.isNext("<!DOCTYPE", false)) {
|
||
|
|
p.skipDoctype();
|
||
|
|
p.trim();
|
||
|
|
}
|
||
|
|
else if (p.x[p.i] === "<") {
|
||
|
|
const root = p.parseTag();
|
||
|
|
return { [root.tag]: root.value };
|
||
|
|
}
|
||
|
|
else {
|
||
|
|
throw new Error("@aws-sdk XML parse error: unexpected content.");
|
||
|
|
}
|
||
|
|
}
|
||
|
|
throw new Error("@aws-sdk XML parse error: no root element.");
|
||
|
|
}
|
||
|
|
isNext(s, caseSensitive = true) {
|
||
|
|
const p = this;
|
||
|
|
if (caseSensitive) {
|
||
|
|
return p.x.startsWith(s, p.i);
|
||
|
|
}
|
||
|
|
return p.x.toLowerCase().startsWith(s.toLowerCase(), p.i);
|
||
|
|
}
|
||
|
|
readTo(stop) {
|
||
|
|
const p = this;
|
||
|
|
const _i = p.x.indexOf(stop, p.i);
|
||
|
|
if (_i === -1) {
|
||
|
|
throw new Error(`@aws-sdk XML parse error: expected "${stop}" not found.`);
|
||
|
|
}
|
||
|
|
const result = p.x.slice(p.i, _i);
|
||
|
|
p.i = _i + stop.length;
|
||
|
|
return result;
|
||
|
|
}
|
||
|
|
trim() {
|
||
|
|
const p = this;
|
||
|
|
while (p.i < p.z && " \t\r\n".includes(p.x[p.i])) {
|
||
|
|
++p.i;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
readAttrValue() {
|
||
|
|
const p = this;
|
||
|
|
const quote = p.x[p.i];
|
||
|
|
++p.i;
|
||
|
|
let value = "";
|
||
|
|
while (p.i < p.z && p.x[p.i] !== quote) {
|
||
|
|
value += p.x[p.i++];
|
||
|
|
}
|
||
|
|
++p.i;
|
||
|
|
return p.decodeEntities(value);
|
||
|
|
}
|
||
|
|
parseTag() {
|
||
|
|
const p = this;
|
||
|
|
++p.i;
|
||
|
|
let tag = "";
|
||
|
|
while (p.i < p.z && !" \t\r\n>/".includes(p.x[p.i])) {
|
||
|
|
tag += p.x[p.i++];
|
||
|
|
}
|
||
|
|
let hasAttrs = false;
|
||
|
|
const attrs = Object.create(null);
|
||
|
|
while (p.i < p.z) {
|
||
|
|
p.trim();
|
||
|
|
if (">/".includes(p.x[p.i])) {
|
||
|
|
break;
|
||
|
|
}
|
||
|
|
let name = "";
|
||
|
|
while (p.i < p.z && !"= \t\r\n>/?".includes(p.x[p.i])) {
|
||
|
|
name += p.x[p.i++];
|
||
|
|
}
|
||
|
|
p.trim();
|
||
|
|
if (p.x[p.i] !== "=") {
|
||
|
|
break;
|
||
|
|
}
|
||
|
|
++p.i;
|
||
|
|
p.trim();
|
||
|
|
attrs[name] = p.readAttrValue();
|
||
|
|
hasAttrs = true;
|
||
|
|
}
|
||
|
|
if (p.i >= p.z) {
|
||
|
|
throw new Error("@aws-sdk XML parse error: unexpected end of input.");
|
||
|
|
}
|
||
|
|
if (p.x[p.i] === "/") {
|
||
|
|
++p.i;
|
||
|
|
if (p.i >= p.z || p.x[p.i] !== ">") {
|
||
|
|
throw new Error("@aws-sdk XML parse error: expected > at the end of self-closing tag.");
|
||
|
|
}
|
||
|
|
++p.i;
|
||
|
|
Object.setPrototypeOf(attrs, Object.prototype);
|
||
|
|
return { tag, value: hasAttrs ? attrs : "" };
|
||
|
|
}
|
||
|
|
if (p.x[p.i] !== ">") {
|
||
|
|
throw new Error("@aws-sdk XML parse error: expected > at the end of opening tag.");
|
||
|
|
}
|
||
|
|
++p.i;
|
||
|
|
const textParts = [];
|
||
|
|
const childTags = [];
|
||
|
|
let hasElementChild = false;
|
||
|
|
while (p.i < p.z) {
|
||
|
|
if (p.isNext("</")) {
|
||
|
|
break;
|
||
|
|
}
|
||
|
|
if (p.x[p.i] === "<") {
|
||
|
|
if (p.isNext("<!--")) {
|
||
|
|
p.readTo("-->");
|
||
|
|
}
|
||
|
|
else if (p.isNext("<![CDATA[")) {
|
||
|
|
p.i += 9;
|
||
|
|
textParts.push(p.readTo("]]>"));
|
||
|
|
}
|
||
|
|
else if (p.isNext("<?")) {
|
||
|
|
p.readTo("?>");
|
||
|
|
}
|
||
|
|
else {
|
||
|
|
hasElementChild = true;
|
||
|
|
childTags.push(p.parseTag());
|
||
|
|
}
|
||
|
|
}
|
||
|
|
else {
|
||
|
|
let text = "";
|
||
|
|
while (p.i < p.z && p.x[p.i] !== "<") {
|
||
|
|
text += p.x[p.i++];
|
||
|
|
}
|
||
|
|
textParts.push(p.decodeEntities(text));
|
||
|
|
}
|
||
|
|
}
|
||
|
|
if (!p.isNext("</")) {
|
||
|
|
throw new Error(`@aws-sdk XML parse error: missing closing tag </${tag}>.`);
|
||
|
|
}
|
||
|
|
p.i += 2;
|
||
|
|
const closeTag = p.readTo(">").trim();
|
||
|
|
if (closeTag !== tag) {
|
||
|
|
throw new Error(`@aws-sdk XML parse error: mismatched tags <${tag}> and </${closeTag}>.`);
|
||
|
|
}
|
||
|
|
if (!hasAttrs && textParts.length === 0 && !hasElementChild) {
|
||
|
|
return { tag, value: "" };
|
||
|
|
}
|
||
|
|
if (!hasAttrs && !hasElementChild) {
|
||
|
|
const text = textParts.length === 1 ? textParts[0] : textParts.join("");
|
||
|
|
if (text.trim() === "" && text.includes("\n")) {
|
||
|
|
return { tag, value: "" };
|
||
|
|
}
|
||
|
|
return { tag, value: text };
|
||
|
|
}
|
||
|
|
const obj = Object.create(null);
|
||
|
|
for (const text of textParts) {
|
||
|
|
if (text.trim() === "" && text.includes("\n")) {
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
obj["#text"] = "#text" in obj ? obj["#text"] + text : text;
|
||
|
|
}
|
||
|
|
for (const child of childTags) {
|
||
|
|
if (child.tag in obj) {
|
||
|
|
if (Array.isArray(obj[child.tag])) {
|
||
|
|
obj[child.tag].push(child.value);
|
||
|
|
}
|
||
|
|
else {
|
||
|
|
obj[child.tag] = [obj[child.tag], child.value];
|
||
|
|
}
|
||
|
|
}
|
||
|
|
else {
|
||
|
|
obj[child.tag] = child.value;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
for (const [k, v] of Object.entries(attrs)) {
|
||
|
|
obj[k] = v;
|
||
|
|
}
|
||
|
|
Object.setPrototypeOf(obj, Object.prototype);
|
||
|
|
return { tag, value: obj };
|
||
|
|
}
|
||
|
|
static ENTITIES = {
|
||
|
|
amp: "&",
|
||
|
|
lt: "<",
|
||
|
|
gt: ">",
|
||
|
|
quot: '"',
|
||
|
|
apos: "'",
|
||
|
|
};
|
||
|
|
skipDoctype() {
|
||
|
|
const p = this;
|
||
|
|
p.i += 9;
|
||
|
|
let depth = 0;
|
||
|
|
while (p.i < p.z) {
|
||
|
|
const c = p.x[p.i];
|
||
|
|
if (c === "[") {
|
||
|
|
++depth;
|
||
|
|
}
|
||
|
|
else if (c === "]") {
|
||
|
|
--depth;
|
||
|
|
}
|
||
|
|
else if (c === ">" && depth === 0) {
|
||
|
|
++p.i;
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
++p.i;
|
||
|
|
}
|
||
|
|
throw new Error("@aws-sdk XML parse error: unclosed DOCTYPE.");
|
||
|
|
}
|
||
|
|
decodeEntities(s) {
|
||
|
|
return s.replace(/&(?:#x([0-9a-fA-F]{1,6})|#(\d{1,7})|([a-zA-Z][a-zA-Z0-9]{0,30}));/g, (_, hex, dec, named) => {
|
||
|
|
if (hex) {
|
||
|
|
return String.fromCharCode(parseInt(hex, 16));
|
||
|
|
}
|
||
|
|
if (dec) {
|
||
|
|
return String.fromCharCode(parseInt(dec, 10));
|
||
|
|
}
|
||
|
|
return AwsXmlParser.ENTITIES[named] ?? "";
|
||
|
|
});
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
exports.XmlNode = XmlNode;
|
||
|
|
exports.XmlText = XmlText;
|
||
|
|
exports.parseXML = parseXML;
|