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 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
- ## v0.x.x: XML Node Extraction & Scaffolding
13
+ ## XML Node Extraction & Scaffolding
14
14
 
15
- Version `0.2.x` is focused entirely on parsing raw XML into a structured tree of nodes. 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.
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
+ });
@@ -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.2.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.js",
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": [