-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathparse-xml.js
159 lines (132 loc) · 3.24 KB
/
parse-xml.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
/**
* @file Scriptable XML Tree Parser
* @author colin273
* @license MIT
* @version 1.0.0
*/
"use strict";
function setIdx(arr, i, val) {
if (i < 0)
i += arr.length;
arr[i] = val;
}
function* iterateTree(tree) {
yield tree;
if (tree.childNodes)
for (const c of tree.childNodes)
yield* iterateTree(c);
}
/**
* Element in an XML tree.
*/
class Element {
/**
* @param {string} name Tag name
* @param {?Object.<string, string>} attrs Attributes
*/
constructor(name, attrs) {
/**
* Tag name
* @type {string}
*/
this.name = name;
// Using an object as a hash map is unsafe.
// If an attribute is called "__proto__", then it will be
// lost by XMLParser.
// Maps do not have this problem.
/**
* Attributes
* @type {Map<string, string>}
*/
this.attributes = new Map(attrs && Object.entries(attrs));
/**
* Child nodes, including elements and text nodes (strings)
* @type {(Element|string)[]}
*/
this.childNodes = [];
}
/**
* @type {string}
*/
get innerText() {
return this.childNodes.map(c => (typeof c === "string") ? c : c.innerText).join("");
}
/**
* Child elements. Text nodes are excluded.
* @type {Element[]}
*/
get children() {
return this.childNodes.filter(c => c instanceof Element);
}
}
/**
* @callback Filter
* @param {Element|string} node Node in the tree
* @returns {boolean} Whether the node satisfies the filter
*/
/**
* Finds the first node in a tree that satisfies a filter.
* @param {Element|string} tree Parent node of the subtree to search.
* @param {Filter} filter
* @returns {?(Element|string)} First node in the tree, going from top to bottom, that satisfies the filter
*/
function findInTree(tree, filter) {
for (const t of iterateTree(tree))
if (filter(t))
return t;
}
/**
* Finds all nodes in a tree that satisfy a filter.
* @param {Element|string} tree Parent node of the subtree to search.
* @param {Filter} filter
* @returns {(Element|string)[]} All nodes in the tree, going from top to bottom, that satisfy the filter
*/
function findAllInTree(tree, filter) {
const items = [];
for (const t of iterateTree(tree))
if (filter(t))
items.push(t);
return items;
}
/**
* Parses an XML string into a tree.
* @param {string} str XML document as a string
* @returns {Element} Root element of the tree
* @throws {Error} Will throw if a syntax error is detected in the XML
*/
function parseXML(str) {
let root;
let err;
const x = new XMLParser(str);
const path = [];
x.didStartElement = (name, attrs) => {
const e = new Element(name, attrs);
if (root)
path.at(-1).childNodes.push(e);
else
root = e;
path.push(e);
};
x.didEndElement = () => void path.pop();
x.foundCharacters = chars => {
const parent = path.at(-1);
const lastChild = parent.childNodes.at(-1);
if (typeof lastChild === "string")
setIdx(parent.childNodes, -1, lastChild + chars);
else
parent.childNodes.push(chars);
}
x.parseErrorOccurred = e => {
err = new Error(e)
};
if (x.parse())
return root;
else
throw err;
}
module.exports = {
parseXML,
Element,
findInTree,
findAllInTree
};