235 lines
6.9 KiB
JavaScript
235 lines
6.9 KiB
JavaScript
|
|
export 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] ?? "";
|
||
|
|
});
|
||
|
|
}
|
||
|
|
}
|