create-monolayer-app 0.2.2 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,325 @@
1
+ /**
2
+ * Zero-dependency XML parser, DOM builder, and serializer for S3 XML payloads.
3
+ */
4
+
5
+ // oxlint-disable-next-line max-classes-per-file
6
+ export const S3_NS = "http://s3.amazonaws.com/doc/2006-03-01/";
7
+ export const XML_DECL = '<?xml version="1.0" encoding="UTF-8"?>';
8
+
9
+ export class ParseError extends Error {
10
+ constructor(message: string) {
11
+ super(message);
12
+ this.name = "ParseError";
13
+ }
14
+ }
15
+
16
+ export function escapeXml(
17
+ str: string | number | boolean | null | undefined
18
+ ): string {
19
+ if (str === null || str === undefined) return "";
20
+ return String(str)
21
+ .replaceAll("&", "&amp;")
22
+ .replaceAll("<", "&lt;")
23
+ .replaceAll(">", "&gt;")
24
+ .replaceAll('"', "&quot;")
25
+ .replaceAll("'", "&apos;");
26
+ }
27
+
28
+ export function unescapeXml(str: string): string {
29
+ return str
30
+ .replaceAll("&quot;", '"')
31
+ .replaceAll("&apos;", "'")
32
+ .replaceAll("&lt;", "<")
33
+ .replaceAll("&gt;", ">")
34
+ .replaceAll("&amp;", "&")
35
+ .replaceAll(/&#(\d+);/gu, (_, dec) =>
36
+ String.fromCodePoint(Math.trunc(Number(dec)))
37
+ )
38
+ .replaceAll(/&#x([0-9a-fA-F]+);/gu, (_, hex) =>
39
+ String.fromCodePoint(Number.parseInt(hex, 16))
40
+ );
41
+ }
42
+
43
+ /**
44
+ * Strips namespace prefix or Clark notation from a tag name.
45
+ * e.g. "{http://...}Rule" -> "Rule", "s3:Rule" -> "Rule", "Rule" -> "Rule"
46
+ */
47
+ export function localTagName(tag: string): string {
48
+ if (tag.startsWith("{")) {
49
+ const idx = tag.indexOf("}");
50
+ if (idx !== -1) return tag.slice(idx + 1);
51
+ }
52
+ const colonIdx = tag.indexOf(":");
53
+ if (colonIdx !== -1) {
54
+ return tag.slice(colonIdx + 1);
55
+ }
56
+ return tag;
57
+ }
58
+
59
+ function isString(
60
+ value: string | Record<string, string> | undefined
61
+ ): value is string {
62
+ return typeof value === "string";
63
+ }
64
+
65
+ export class XmlElement {
66
+ public tag: string;
67
+ public attrs: Record<string, string>;
68
+ public text?: string;
69
+ public children: XmlElement[];
70
+
71
+ constructor(tag: string, attrs: Record<string, string> = {}, text?: string) {
72
+ this.tag = tag;
73
+ this.attrs = { ...attrs };
74
+ this.text = text;
75
+ this.children = [];
76
+ }
77
+
78
+ public localName(): string {
79
+ return localTagName(this.tag);
80
+ }
81
+
82
+ public subElement(
83
+ tag: string,
84
+ textOrAttrs?: string | Record<string, string>,
85
+ text?: string
86
+ ): XmlElement {
87
+ const el = isString(textOrAttrs)
88
+ ? new XmlElement(tag, {}, textOrAttrs)
89
+ : new XmlElement(tag, textOrAttrs || {}, text);
90
+
91
+ this.children.push(el);
92
+ return el;
93
+ }
94
+
95
+ public find(tag: string): XmlElement | undefined {
96
+ const targetLocal = localTagName(tag);
97
+ return this.children.find(
98
+ (c) => c.tag === tag || c.localName() === targetLocal
99
+ );
100
+ }
101
+
102
+ public findAll(tag: string): XmlElement[] {
103
+ const targetLocal = localTagName(tag);
104
+ return this.children.filter(
105
+ (c) => c.tag === tag || c.localName() === targetLocal
106
+ );
107
+ }
108
+
109
+ public findText(tag: string): string | undefined {
110
+ const el = this.find(tag);
111
+ return el?.text;
112
+ }
113
+
114
+ public findDescendant(tag: string): XmlElement | undefined {
115
+ const targetLocal = localTagName(tag);
116
+ for (const c of this.children) {
117
+ if (c.tag === tag || c.localName() === targetLocal) {
118
+ return c;
119
+ }
120
+ const deep = c.findDescendant(tag);
121
+ if (deep) return deep;
122
+ }
123
+ return undefined;
124
+ }
125
+
126
+ public findAllDescendants(tag: string): XmlElement[] {
127
+ const targetLocal = localTagName(tag);
128
+ const result: XmlElement[] = [];
129
+ for (const c of this.children) {
130
+ if (c.tag === tag || c.localName() === targetLocal) {
131
+ result.push(c);
132
+ }
133
+ result.push(...c.findAllDescendants(tag));
134
+ }
135
+ return result;
136
+ }
137
+
138
+ public iter(): XmlElement[] {
139
+ const result: XmlElement[] = [this];
140
+ for (const child of this.children) {
141
+ result.push(...child.iter());
142
+ }
143
+ return result;
144
+ }
145
+
146
+ public toXml(includeDeclaration = false): string {
147
+ let s = includeDeclaration ? `${XML_DECL}\n` : "";
148
+ s += `<${this.tag}`;
149
+ for (const [k, v] of Object.entries(this.attrs)) {
150
+ s += ` ${k}="${escapeXml(v)}"`;
151
+ }
152
+ if (this.children.length === 0 && this.text === undefined) {
153
+ s += "/>";
154
+ return s;
155
+ }
156
+ s += ">";
157
+ if (this.text !== undefined) {
158
+ s += escapeXml(this.text);
159
+ }
160
+ for (const child of this.children) {
161
+ s += child.toXml(false);
162
+ }
163
+ s += `</${this.tag}>`;
164
+ return s;
165
+ }
166
+ }
167
+
168
+ export function subElement(
169
+ parent: XmlElement,
170
+ tag: string,
171
+ textOrAttrs?: string | Record<string, string>,
172
+ text?: string
173
+ ): XmlElement {
174
+ return parent.subElement(tag, textOrAttrs, text);
175
+ }
176
+
177
+ // oxlint-disable-next-line complexity
178
+ export function parseXml(xml: string | Buffer): XmlElement {
179
+ // String inputs are also used by browser upload handles.
180
+ // oxlint-disable-next-line anti-slop/no-runtime-typeof
181
+ let str = (typeof xml === "string" ? xml : xml.toString("utf-8")).trim();
182
+ if (!str) {
183
+ throw new ParseError("Empty XML document");
184
+ }
185
+
186
+ // Strip XML declaration if present
187
+ if (str.startsWith("<?xml")) {
188
+ const end = str.indexOf("?>");
189
+ if (end !== -1) {
190
+ str = str.slice(end + 2).trim();
191
+ }
192
+ }
193
+
194
+ let pos = 0;
195
+ const len = str.length;
196
+ let root: XmlElement | null = null;
197
+ const stack: XmlElement[] = [];
198
+
199
+ while (pos < len) {
200
+ const lt = str.indexOf("<", pos);
201
+ if (lt === -1) {
202
+ if (stack.length > 0) {
203
+ const text = unescapeXml(str.slice(pos).trim());
204
+ const top = stack.at(-1);
205
+ if (text && top) {
206
+ top.text = (top.text || "") + text;
207
+ }
208
+ }
209
+ break;
210
+ }
211
+
212
+ if (lt > pos && stack.length > 0) {
213
+ const text = unescapeXml(str.slice(pos, lt).trim());
214
+ const top = stack.at(-1);
215
+ if (text && top) {
216
+ top.text = (top.text || "") + text;
217
+ }
218
+ }
219
+
220
+ pos = lt + 1;
221
+
222
+ // Comment
223
+ if (str.startsWith("!--", pos)) {
224
+ const end = str.indexOf("-->", pos + 3);
225
+ pos = end === -1 ? len : end + 3;
226
+ continue;
227
+ }
228
+
229
+ // CDATA
230
+ if (str.startsWith("![CDATA[", pos)) {
231
+ const end = str.indexOf("]]>", pos + 8);
232
+ const cdata = end === -1 ? str.slice(pos + 8) : str.slice(pos + 8, end);
233
+ const top = stack.at(-1);
234
+ if (top) {
235
+ top.text = (top.text || "") + cdata;
236
+ }
237
+ pos = end === -1 ? len : end + 3;
238
+ continue;
239
+ }
240
+
241
+ // Processing instructions or DOCTYPE
242
+ if (str.startsWith("?", pos) || str.startsWith("!", pos)) {
243
+ const gt = str.indexOf(">", pos);
244
+ pos = gt === -1 ? len : gt + 1;
245
+ continue;
246
+ }
247
+
248
+ // Closing tag: </tag>
249
+ if (str[pos] === "/") {
250
+ const gt = str.indexOf(">", pos);
251
+ if (gt === -1) throw new ParseError("Unclosed closing tag");
252
+ const closeTag = str.slice(pos + 1, gt).trim();
253
+ const open = stack.pop();
254
+ if (!open) {
255
+ throw new ParseError(`Unexpected closing tag: </${closeTag}>`);
256
+ }
257
+ if (
258
+ localTagName(open.tag) !== localTagName(closeTag) &&
259
+ open.tag !== closeTag
260
+ ) {
261
+ throw new ParseError(
262
+ `Mismatched closing tag: expected </${open.tag}>, got </${closeTag}>`
263
+ );
264
+ }
265
+ pos = gt + 1;
266
+ continue;
267
+ }
268
+
269
+ // Opening tag: <tag ...>
270
+ const gt = str.indexOf(">", pos);
271
+ if (gt === -1) {
272
+ throw new ParseError("Unclosed opening tag");
273
+ }
274
+
275
+ let tagBody = str.slice(pos, gt).trim();
276
+ const selfClosing = tagBody.endsWith("/");
277
+ if (selfClosing) {
278
+ tagBody = tagBody.slice(0, -1).trim();
279
+ }
280
+
281
+ const spaceIdx = tagBody.search(/\s/u);
282
+ const tagName = spaceIdx === -1 ? tagBody : tagBody.slice(0, spaceIdx);
283
+ const attrs: Record<string, string> = {};
284
+
285
+ if (spaceIdx !== -1) {
286
+ const attrStr = tagBody.slice(spaceIdx).trim();
287
+ const attrRegex =
288
+ /([a-zA-Z0-9_:.-]+)\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s>]+))/gu;
289
+ let m: RegExpExecArray | null;
290
+ while ((m = attrRegex.exec(attrStr)) !== null) {
291
+ attrs[m[1]] = unescapeXml(m[2] ?? m[3] ?? m[4] ?? "");
292
+ }
293
+ }
294
+
295
+ const el = new XmlElement(tagName, attrs);
296
+ const top = stack.at(-1);
297
+ if (top) {
298
+ top.children.push(el);
299
+ } else {
300
+ if (root) {
301
+ throw new ParseError("Multiple root elements found in XML document");
302
+ }
303
+ root = el;
304
+ }
305
+
306
+ if (!selfClosing) {
307
+ stack.push(el);
308
+ }
309
+ pos = gt + 1;
310
+ }
311
+
312
+ const lastUnclosed = stack.at(-1);
313
+ if (lastUnclosed) {
314
+ throw new ParseError(`Unclosed element: <${lastUnclosed.tag}>`);
315
+ }
316
+ if (!root) {
317
+ throw new ParseError("Empty XML document");
318
+ }
319
+
320
+ return root;
321
+ }
322
+
323
+ export function xmlBody(root: XmlElement): Buffer {
324
+ return Buffer.from(`${XML_DECL}\n${root.toXml(false)}`, "utf-8");
325
+ }