xml-to-html-converter 0.2.0 → 0.3.1
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.
- package/README.md +19 -3
- package/dist/index.cjs +265 -0
- package/dist/index.d.cts +28 -0
- package/dist/index.d.ts +3 -1
- package/dist/index.js +6 -0
- package/package.json +5 -3
package/README.md
CHANGED
|
@@ -10,9 +10,9 @@ A zero-dependency Node.js package for converting XML to HTML. Currently in pre-1
|
|
|
10
10
|
|
|
11
11
|
---
|
|
12
12
|
|
|
13
|
-
##
|
|
13
|
+
## XML Node Extraction & Scaffolding
|
|
14
14
|
|
|
15
|
-
|
|
15
|
+
The `scaffold` function walks an XML string and produces an array of `XmlNode` objects, each carrying its role, its raw source text, and its position in the document, both globally across the full document and locally within its parent.
|
|
16
16
|
|
|
17
17
|
```ts
|
|
18
18
|
interface XmlAttribute {
|
|
@@ -50,6 +50,7 @@ This scaffold is the foundation everything else will be built on. No transformat
|
|
|
50
50
|
>
|
|
51
51
|
> `v0.x` is building the scaffold and the first render pass.
|
|
52
52
|
>
|
|
53
|
+
> - **`minify(xml)`** strips inter-tag whitespace from prettified XML before parsing — text content is left untouched
|
|
53
54
|
> - **`scaffold(xml)`** reads any XML string and returns a nested node tree
|
|
54
55
|
> - Every node knows its `role`, its `raw` source string, its `globalIndex` in the document, and its `localIndex` within its parent
|
|
55
56
|
> - Tag nodes (`openTag`, `selfTag`) also carry `xmlTag`, `xmlInner`, and `xmlAttributes` — the parsed tag name, raw attribute string, and structured attribute array
|
|
@@ -164,6 +165,20 @@ Processing instructions and doctypes are dropped. Comments are passed through un
|
|
|
164
165
|
|
|
165
166
|
---
|
|
166
167
|
|
|
168
|
+
### Minifying prettified XML
|
|
169
|
+
|
|
170
|
+
When your XML comes from a file or an API it is usually indented and line-broken. `minify` strips the whitespace between tags before parsing, leaving text content completely untouched.
|
|
171
|
+
|
|
172
|
+
```js
|
|
173
|
+
import { minify, scaffold, render } from "xml-to-html-converter";
|
|
174
|
+
|
|
175
|
+
const html = render(scaffold(minify(xml)));
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
`minify` is opt-in. Skip it if whitespace inside your content is meaningful.
|
|
179
|
+
|
|
180
|
+
---
|
|
181
|
+
|
|
167
182
|
## Node Shape
|
|
168
183
|
|
|
169
184
|
Every node in the tree has the following fields:
|
|
@@ -253,7 +268,7 @@ const tree = scaffold("<root><unclosed><valid>text</valid></root>");
|
|
|
253
268
|
## Exports
|
|
254
269
|
|
|
255
270
|
```ts
|
|
256
|
-
import { scaffold, render, isMalformed } from "xml-to-html-converter";
|
|
271
|
+
import { scaffold, render, minify, isMalformed } from "xml-to-html-converter";
|
|
257
272
|
import type {
|
|
258
273
|
XmlNode,
|
|
259
274
|
XmlNodeRole,
|
|
@@ -264,6 +279,7 @@ import type {
|
|
|
264
279
|
|
|
265
280
|
| Export | Kind | Description |
|
|
266
281
|
| ------------------ | -------- | --------------------------------------------------- |
|
|
282
|
+
| `minify` | function | Strips inter-tag whitespace from an XML string |
|
|
267
283
|
| `scaffold` | function | Parses an XML string and returns a node tree |
|
|
268
284
|
| `render` | function | Converts a node tree to an HTML string |
|
|
269
285
|
| `isMalformed` | function | Type guard, narrows `XmlNode` to `MalformedXmlNode` |
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/index.ts
|
|
21
|
+
var src_exports = {};
|
|
22
|
+
__export(src_exports, {
|
|
23
|
+
isMalformed: () => isMalformed,
|
|
24
|
+
minify: () => minify,
|
|
25
|
+
render: () => render,
|
|
26
|
+
scaffold: () => scaffold
|
|
27
|
+
});
|
|
28
|
+
module.exports = __toCommonJS(src_exports);
|
|
29
|
+
|
|
30
|
+
// src/modules/minify/minify.ts
|
|
31
|
+
function minify(xml) {
|
|
32
|
+
return xml.replace(/>(\s+)</g, (_, gap) => gap.trim() === "" ? "><" : `>${gap}<`).trim();
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// src/modules/render/render.ts
|
|
36
|
+
function render(nodes) {
|
|
37
|
+
return nodes.map(renderNode).join("");
|
|
38
|
+
}
|
|
39
|
+
function renderNode(node) {
|
|
40
|
+
if (node.role === "textLeaf") return node.raw;
|
|
41
|
+
if (node.role === "comment") return node.raw;
|
|
42
|
+
if (node.role === "processingInstruction") return "";
|
|
43
|
+
if (node.role === "doctype") return "";
|
|
44
|
+
if (node.role === "closeTag") return "";
|
|
45
|
+
const tag = node.xmlTag ?? "";
|
|
46
|
+
const attrs = buildDataAttrs(node);
|
|
47
|
+
const attrsHtml = attrs ? ` ${attrs}` : "";
|
|
48
|
+
if (node.role === "selfTag") {
|
|
49
|
+
return `<div data-tag="${tag}"${attrsHtml}></div>`;
|
|
50
|
+
}
|
|
51
|
+
const children = render(node.children ?? []);
|
|
52
|
+
return `<div data-tag="${tag}"${attrsHtml}>${children}</div>`;
|
|
53
|
+
}
|
|
54
|
+
function buildDataAttrs(node) {
|
|
55
|
+
if (!node.xmlAttributes || node.xmlAttributes.length === 0) return "";
|
|
56
|
+
return node.xmlAttributes.map(({ name, value }) => `data-attrs-${name}="${value}"`).join(" ");
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// src/modules/scaffold/scaffold.ts
|
|
60
|
+
function parseXmlAttributes(xmlInner) {
|
|
61
|
+
const attributes = [];
|
|
62
|
+
let i = 0;
|
|
63
|
+
const s = xmlInner.trim();
|
|
64
|
+
while (i < s.length) {
|
|
65
|
+
while (i < s.length && /\s/.test(s[i])) i++;
|
|
66
|
+
if (i >= s.length) break;
|
|
67
|
+
const nameStart = i;
|
|
68
|
+
while (i < s.length && s[i] !== "=" && !/\s/.test(s[i])) i++;
|
|
69
|
+
const name = s.slice(nameStart, i).trim();
|
|
70
|
+
if (!name) break;
|
|
71
|
+
while (i < s.length && /\s/.test(s[i])) i++;
|
|
72
|
+
if (s[i] !== "=") break;
|
|
73
|
+
i++;
|
|
74
|
+
while (i < s.length && /\s/.test(s[i])) i++;
|
|
75
|
+
const quote = s[i];
|
|
76
|
+
if (quote !== '"' && quote !== "'") break;
|
|
77
|
+
i++;
|
|
78
|
+
const valueStart = i;
|
|
79
|
+
while (i < s.length && s[i] !== quote) i++;
|
|
80
|
+
const value = s.slice(valueStart, i);
|
|
81
|
+
i++;
|
|
82
|
+
attributes.push({ name, value });
|
|
83
|
+
}
|
|
84
|
+
return attributes.length > 0 ? attributes : void 0;
|
|
85
|
+
}
|
|
86
|
+
var MAX_DEPTH = 500;
|
|
87
|
+
function scaffold(xml) {
|
|
88
|
+
const counter = { value: 0 };
|
|
89
|
+
const { xmlNodes } = collectXmlNodes(xml, 0, null, counter, 0);
|
|
90
|
+
return xmlNodes;
|
|
91
|
+
}
|
|
92
|
+
function collectXmlNodes(xml, position, parentTag, counter, depth) {
|
|
93
|
+
if (depth > MAX_DEPTH) return { xmlNodes: [], position, closed: false };
|
|
94
|
+
const xmlNodes = [];
|
|
95
|
+
while (position < xml.length) {
|
|
96
|
+
const xmlNodeData = extractXmlNodes(xml, position);
|
|
97
|
+
if (xmlNodeData.role === "textLeaf" && xmlNodeData.raw.trim() === "") {
|
|
98
|
+
position = xmlNodeData.end;
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
if (xmlNodeData.role === "closeTag") {
|
|
102
|
+
if (xmlNodeData.tag === parentTag)
|
|
103
|
+
return { xmlNodes, position: xmlNodeData.end, closed: true };
|
|
104
|
+
xmlNodes.push({
|
|
105
|
+
role: "closeTag",
|
|
106
|
+
raw: xmlNodeData.raw,
|
|
107
|
+
xmlTag: xmlNodeData.tag || void 0,
|
|
108
|
+
xmlInner: xmlNodeData.xmlInner,
|
|
109
|
+
globalIndex: counter.value++,
|
|
110
|
+
localIndex: xmlNodes.length,
|
|
111
|
+
malformed: true
|
|
112
|
+
});
|
|
113
|
+
position = xmlNodeData.end;
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
if (xmlNodeData.role === "openTag" && !xmlNodeData.malformed) {
|
|
117
|
+
const globalIndex = counter.value++;
|
|
118
|
+
const localIndex = xmlNodes.length;
|
|
119
|
+
const nested = collectXmlNodes(
|
|
120
|
+
xml,
|
|
121
|
+
xmlNodeData.end,
|
|
122
|
+
xmlNodeData.tag,
|
|
123
|
+
counter,
|
|
124
|
+
depth + 1
|
|
125
|
+
);
|
|
126
|
+
const xmlNode2 = {
|
|
127
|
+
role: "openTag",
|
|
128
|
+
raw: xmlNodeData.raw,
|
|
129
|
+
xmlTag: xmlNodeData.tag || void 0,
|
|
130
|
+
xmlInner: xmlNodeData.xmlInner,
|
|
131
|
+
xmlAttributes: xmlNodeData.xmlAttributes,
|
|
132
|
+
globalIndex,
|
|
133
|
+
localIndex,
|
|
134
|
+
children: nested.xmlNodes
|
|
135
|
+
};
|
|
136
|
+
if (!nested.closed) xmlNode2.malformed = true;
|
|
137
|
+
xmlNodes.push(xmlNode2);
|
|
138
|
+
position = nested.position;
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
const xmlNode = {
|
|
142
|
+
role: xmlNodeData.role,
|
|
143
|
+
raw: xmlNodeData.raw,
|
|
144
|
+
xmlTag: xmlNodeData.tag || void 0,
|
|
145
|
+
xmlInner: xmlNodeData.xmlInner,
|
|
146
|
+
xmlAttributes: xmlNodeData.xmlAttributes,
|
|
147
|
+
globalIndex: counter.value++,
|
|
148
|
+
localIndex: xmlNodes.length
|
|
149
|
+
};
|
|
150
|
+
if (xmlNodeData.malformed) xmlNode.malformed = true;
|
|
151
|
+
if (xmlNodeData.role === "openTag") xmlNode.children = [];
|
|
152
|
+
xmlNodes.push(xmlNode);
|
|
153
|
+
position = xmlNodeData.end;
|
|
154
|
+
}
|
|
155
|
+
return { xmlNodes, position, closed: parentTag === null };
|
|
156
|
+
}
|
|
157
|
+
function findTagClose(xml, position) {
|
|
158
|
+
let i = position;
|
|
159
|
+
while (i < xml.length) {
|
|
160
|
+
const ch = xml[i];
|
|
161
|
+
if (ch === '"' || ch === "'") {
|
|
162
|
+
const closeQuote = xml.indexOf(ch, i + 1);
|
|
163
|
+
i = closeQuote === -1 ? xml.length : closeQuote + 1;
|
|
164
|
+
continue;
|
|
165
|
+
}
|
|
166
|
+
if (ch === ">") return i;
|
|
167
|
+
i++;
|
|
168
|
+
}
|
|
169
|
+
return -1;
|
|
170
|
+
}
|
|
171
|
+
function extractXmlNodes(xml, position) {
|
|
172
|
+
if (xml[position] !== "<") {
|
|
173
|
+
const end2 = xml.indexOf("<", position);
|
|
174
|
+
return {
|
|
175
|
+
raw: xml.slice(position, end2 === -1 ? xml.length : end2),
|
|
176
|
+
role: "textLeaf",
|
|
177
|
+
tag: "",
|
|
178
|
+
end: end2 === -1 ? xml.length : end2
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
if (xml[position + 1] === "?") {
|
|
182
|
+
const end2 = xml.indexOf("?>", position + 2);
|
|
183
|
+
return end2 === -1 ? {
|
|
184
|
+
raw: xml.slice(position),
|
|
185
|
+
role: "processingInstruction",
|
|
186
|
+
tag: "",
|
|
187
|
+
end: xml.length
|
|
188
|
+
} : {
|
|
189
|
+
raw: xml.slice(position, end2 + 2),
|
|
190
|
+
role: "processingInstruction",
|
|
191
|
+
tag: "",
|
|
192
|
+
end: end2 + 2
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
if (xml[position + 1] === "!" && xml[position + 2] === "[") {
|
|
196
|
+
const end2 = xml.indexOf("]]>", position + 3);
|
|
197
|
+
return end2 === -1 ? { raw: xml.slice(position), role: "textLeaf", tag: "", end: xml.length } : {
|
|
198
|
+
raw: xml.slice(position, end2 + 3),
|
|
199
|
+
role: "textLeaf",
|
|
200
|
+
tag: "",
|
|
201
|
+
end: end2 + 3
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
if (xml[position + 1] === "!" && xml[position + 2] === "-" && xml[position + 3] === "-") {
|
|
205
|
+
const end2 = xml.indexOf("-->", position + 4);
|
|
206
|
+
return end2 === -1 ? { raw: xml.slice(position), role: "comment", tag: "", end: xml.length } : {
|
|
207
|
+
raw: xml.slice(position, end2 + 3),
|
|
208
|
+
role: "comment",
|
|
209
|
+
tag: "",
|
|
210
|
+
end: end2 + 3
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
if (xml.startsWith("<!DOCTYPE", position)) {
|
|
214
|
+
const bracketOpen = xml.indexOf("[", position);
|
|
215
|
+
const firstClose = xml.indexOf(">", position);
|
|
216
|
+
const hasBracket = bracketOpen !== -1 && bracketOpen < firstClose;
|
|
217
|
+
if (hasBracket) {
|
|
218
|
+
const bracketClose = xml.indexOf("]>", bracketOpen);
|
|
219
|
+
const end3 = bracketClose === -1 ? xml.length : bracketClose + 2;
|
|
220
|
+
return { raw: xml.slice(position, end3), role: "doctype", tag: "", end: end3 };
|
|
221
|
+
}
|
|
222
|
+
const end2 = firstClose === -1 ? xml.length : firstClose + 1;
|
|
223
|
+
return { raw: xml.slice(position, end2), role: "doctype", tag: "", end: end2 };
|
|
224
|
+
}
|
|
225
|
+
const closeAt = findTagClose(xml, position + 1);
|
|
226
|
+
if (closeAt === -1)
|
|
227
|
+
return {
|
|
228
|
+
raw: xml.slice(position),
|
|
229
|
+
role: "openTag",
|
|
230
|
+
tag: "",
|
|
231
|
+
end: xml.length,
|
|
232
|
+
malformed: true
|
|
233
|
+
};
|
|
234
|
+
const raw = xml.slice(position, closeAt + 1);
|
|
235
|
+
const end = closeAt + 1;
|
|
236
|
+
const inner = xml.slice(position + 1, closeAt).trim();
|
|
237
|
+
if (inner.startsWith("/")) {
|
|
238
|
+
const tag2 = inner.slice(1).trim().split(/\s/)[0] ?? "";
|
|
239
|
+
const xmlInner2 = inner.slice(1).trim().slice(tag2.length).trim() || void 0;
|
|
240
|
+
return { raw, role: "closeTag", tag: tag2, xmlInner: xmlInner2, end };
|
|
241
|
+
}
|
|
242
|
+
if (inner.endsWith("/")) {
|
|
243
|
+
const trimmed = inner.slice(0, -1).trim();
|
|
244
|
+
const tag2 = trimmed.split(/\s/)[0] ?? "";
|
|
245
|
+
const xmlInner2 = trimmed.slice(tag2.length).trim() || void 0;
|
|
246
|
+
const xmlAttributes2 = xmlInner2 ? parseXmlAttributes(xmlInner2) : void 0;
|
|
247
|
+
return { raw, role: "selfTag", tag: tag2, xmlInner: xmlInner2, xmlAttributes: xmlAttributes2, end };
|
|
248
|
+
}
|
|
249
|
+
const tag = inner.split(/\s/)[0] ?? "";
|
|
250
|
+
const xmlInner = inner.slice(tag.length).trim() || void 0;
|
|
251
|
+
const xmlAttributes = xmlInner ? parseXmlAttributes(xmlInner) : void 0;
|
|
252
|
+
return { raw, role: "openTag", tag, xmlInner, xmlAttributes, end };
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// src/modules/scaffold/types.ts
|
|
256
|
+
function isMalformed(node) {
|
|
257
|
+
return node.malformed === true;
|
|
258
|
+
}
|
|
259
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
260
|
+
0 && (module.exports = {
|
|
261
|
+
isMalformed,
|
|
262
|
+
minify,
|
|
263
|
+
render,
|
|
264
|
+
scaffold
|
|
265
|
+
});
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
declare function minify(xml: string): string;
|
|
2
|
+
|
|
3
|
+
interface XmlAttribute {
|
|
4
|
+
name: string;
|
|
5
|
+
value: string;
|
|
6
|
+
}
|
|
7
|
+
type XmlNodeRole = "closeTag" | "comment" | "doctype" | "openTag" | "processingInstruction" | "selfTag" | "textLeaf";
|
|
8
|
+
interface XmlNode {
|
|
9
|
+
role: XmlNodeRole;
|
|
10
|
+
raw: string;
|
|
11
|
+
xmlTag?: string;
|
|
12
|
+
xmlInner?: string;
|
|
13
|
+
xmlAttributes?: XmlAttribute[];
|
|
14
|
+
globalIndex: number;
|
|
15
|
+
localIndex: number;
|
|
16
|
+
children?: XmlNode[];
|
|
17
|
+
malformed?: true;
|
|
18
|
+
}
|
|
19
|
+
type MalformedXmlNode = XmlNode & {
|
|
20
|
+
malformed: true;
|
|
21
|
+
};
|
|
22
|
+
declare function isMalformed(node: XmlNode): node is MalformedXmlNode;
|
|
23
|
+
|
|
24
|
+
declare function render(nodes: XmlNode[]): string;
|
|
25
|
+
|
|
26
|
+
declare function scaffold(xml: string): XmlNode[];
|
|
27
|
+
|
|
28
|
+
export { type MalformedXmlNode, type XmlAttribute, type XmlNode, type XmlNodeRole, isMalformed, minify, render, scaffold };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
declare function minify(xml: string): string;
|
|
2
|
+
|
|
1
3
|
interface XmlAttribute {
|
|
2
4
|
name: string;
|
|
3
5
|
value: string;
|
|
@@ -23,4 +25,4 @@ declare function render(nodes: XmlNode[]): string;
|
|
|
23
25
|
|
|
24
26
|
declare function scaffold(xml: string): XmlNode[];
|
|
25
27
|
|
|
26
|
-
export { type MalformedXmlNode, type XmlAttribute, type XmlNode, type XmlNodeRole, isMalformed, render, scaffold };
|
|
28
|
+
export { type MalformedXmlNode, type XmlAttribute, type XmlNode, type XmlNodeRole, isMalformed, minify, render, scaffold };
|
package/dist/index.js
CHANGED
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
// src/modules/minify/minify.ts
|
|
2
|
+
function minify(xml) {
|
|
3
|
+
return xml.replace(/>(\s+)</g, (_, gap) => gap.trim() === "" ? "><" : `>${gap}<`).trim();
|
|
4
|
+
}
|
|
5
|
+
|
|
1
6
|
// src/modules/render/render.ts
|
|
2
7
|
function render(nodes) {
|
|
3
8
|
return nodes.map(renderNode).join("");
|
|
@@ -224,6 +229,7 @@ function isMalformed(node) {
|
|
|
224
229
|
}
|
|
225
230
|
export {
|
|
226
231
|
isMalformed,
|
|
232
|
+
minify,
|
|
227
233
|
render,
|
|
228
234
|
scaffold
|
|
229
235
|
};
|
package/package.json
CHANGED
|
@@ -1,14 +1,16 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "xml-to-html-converter",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.1",
|
|
4
4
|
"description": "Zero dependency XML to HTML converter for Node environments",
|
|
5
5
|
"type": "module",
|
|
6
|
-
"main": "./dist/index.
|
|
6
|
+
"main": "./dist/index.cjs",
|
|
7
|
+
"module": "./dist/index.js",
|
|
7
8
|
"types": "./dist/index.d.ts",
|
|
8
9
|
"exports": {
|
|
9
10
|
".": {
|
|
10
11
|
"types": "./dist/index.d.ts",
|
|
11
|
-
"import": "./dist/index.js"
|
|
12
|
+
"import": "./dist/index.js",
|
|
13
|
+
"require": "./dist/index.cjs"
|
|
12
14
|
}
|
|
13
15
|
},
|
|
14
16
|
"files": [
|