xml-to-html-converter 0.1.2 → 0.1.3

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
@@ -1,6 +1,6 @@
1
1
  # xml-to-html-converter
2
2
 
3
- ![version](https://img.shields.io/badge/version-0.1.1-blue)
3
+ ![version](https://img.shields.io/npm/v/xml-to-html-converter)
4
4
  ![node version](https://img.shields.io/node/v/xml-to-html-converter)
5
5
  ![zero dependencies](https://img.shields.io/badge/dependencies-0-brightgreen)
6
6
  ![XML](https://img.shields.io/badge/input-XML-orange)
@@ -158,4 +158,4 @@ Node.js `>=18.0.0`
158
158
 
159
159
  ---
160
160
 
161
- ![license](https://img.shields.io/badge/license-ISC-green)
161
+ ![license](https://img.shields.io/npm/l/xml-to-html-converter)
@@ -0,0 +1,85 @@
1
+ interface DocumentNode {
2
+ type: 'document';
3
+ children: Node[];
4
+ }
5
+ interface ElementNode {
6
+ type: 'element';
7
+ tag: string;
8
+ attributes: Record<string, string>;
9
+ children: Node[];
10
+ malformed?: true;
11
+ }
12
+ interface TextNode {
13
+ type: 'text';
14
+ value: string;
15
+ }
16
+ interface CommentNode {
17
+ type: 'comment';
18
+ value: string;
19
+ }
20
+ interface CDataNode {
21
+ type: 'cdata';
22
+ value: string;
23
+ }
24
+ interface ProcessingInstructionNode {
25
+ type: 'processing-instruction';
26
+ target: string;
27
+ attributes: Record<string, string>;
28
+ }
29
+ interface MalformedNode {
30
+ type: 'malformed';
31
+ raw: string;
32
+ malformed: true;
33
+ }
34
+ type Node = ElementNode | TextNode | CommentNode | CDataNode | ProcessingInstructionNode | MalformedNode;
35
+ declare function parse(xml: string): DocumentNode;
36
+
37
+ declare const TokenType: {
38
+ readonly PROCESSING_INSTRUCTION: "processing-instruction";
39
+ readonly ELEMENT_OPEN: "element-open";
40
+ readonly ELEMENT_CLOSE: "element-close";
41
+ readonly SELF_CLOSING: "self-closing";
42
+ readonly TEXT: "text";
43
+ readonly COMMENT: "comment";
44
+ readonly CDATA: "cdata";
45
+ readonly MALFORMED: "malformed";
46
+ };
47
+ type TokenTypeValue = typeof TokenType[keyof typeof TokenType];
48
+ interface ProcessingInstructionToken {
49
+ type: typeof TokenType.PROCESSING_INSTRUCTION;
50
+ target: string;
51
+ attributes: Record<string, string>;
52
+ }
53
+ interface ElementOpenToken {
54
+ type: typeof TokenType.ELEMENT_OPEN;
55
+ tag: string;
56
+ attributes: Record<string, string>;
57
+ }
58
+ interface ElementCloseToken {
59
+ type: typeof TokenType.ELEMENT_CLOSE;
60
+ tag: string;
61
+ }
62
+ interface SelfClosingToken {
63
+ type: typeof TokenType.SELF_CLOSING;
64
+ tag: string;
65
+ attributes: Record<string, string>;
66
+ }
67
+ interface TextToken {
68
+ type: typeof TokenType.TEXT;
69
+ value: string;
70
+ }
71
+ interface CommentToken {
72
+ type: typeof TokenType.COMMENT;
73
+ value: string;
74
+ }
75
+ interface CDataToken {
76
+ type: typeof TokenType.CDATA;
77
+ value: string;
78
+ }
79
+ interface MalformedToken {
80
+ type: typeof TokenType.MALFORMED;
81
+ raw: string;
82
+ }
83
+ type Token = ProcessingInstructionToken | ElementOpenToken | ElementCloseToken | SelfClosingToken | TextToken | CommentToken | CDataToken | MalformedToken;
84
+
85
+ export { type CDataNode, type CommentNode, type DocumentNode, type ElementNode, type MalformedNode, type Node, type ProcessingInstructionNode, type TextNode, type Token, type TokenTypeValue, parse };
package/dist/index.js ADDED
@@ -0,0 +1,113 @@
1
+ // src/tokenizer.ts
2
+ var TokenType = {
3
+ PROCESSING_INSTRUCTION: "processing-instruction",
4
+ ELEMENT_OPEN: "element-open",
5
+ ELEMENT_CLOSE: "element-close",
6
+ SELF_CLOSING: "self-closing",
7
+ TEXT: "text",
8
+ COMMENT: "comment",
9
+ CDATA: "cdata",
10
+ MALFORMED: "malformed"
11
+ };
12
+ var WHITESPACE = /\s/;
13
+ function parseAttributes(raw) {
14
+ const attributes = {};
15
+ const pattern = /(\S+?)\s*=\s*["']([^"']*)["']/g;
16
+ let match;
17
+ while ((match = pattern.exec(raw)) !== null) {
18
+ attributes[match[1]] = match[2];
19
+ }
20
+ return attributes;
21
+ }
22
+ function nextToken(xml, position) {
23
+ if (xml[position] !== "<") {
24
+ const end2 = xml.indexOf("<", position);
25
+ const value = xml.slice(position, end2 === -1 ? xml.length : end2);
26
+ return { token: value.trim() ? { type: TokenType.TEXT, value } : null, end: end2 === -1 ? xml.length : end2 };
27
+ }
28
+ const next = xml[position + 1];
29
+ if (next === "?") {
30
+ const closeIndex = xml.indexOf("?>", position);
31
+ if (closeIndex === -1) return { token: { type: TokenType.MALFORMED, raw: xml.slice(position) }, end: xml.length };
32
+ const end2 = closeIndex + 2;
33
+ const inner2 = xml.slice(position + 2, end2 - 2).trim();
34
+ const space2 = inner2.search(WHITESPACE);
35
+ return { token: { type: TokenType.PROCESSING_INSTRUCTION, target: space2 === -1 ? inner2 : inner2.slice(0, space2), attributes: parseAttributes(inner2) }, end: end2 };
36
+ }
37
+ if (next === "!" && xml[position + 2] === "-") {
38
+ const closeIndex = xml.indexOf("-->", position);
39
+ if (closeIndex === -1) return { token: { type: TokenType.MALFORMED, raw: xml.slice(position) }, end: xml.length };
40
+ const end2 = closeIndex + 3;
41
+ return { token: { type: TokenType.COMMENT, value: xml.slice(position + 4, end2 - 3) }, end: end2 };
42
+ }
43
+ if (next === "!" && xml[position + 2] === "[") {
44
+ const closeIndex = xml.indexOf("]]>", position);
45
+ if (closeIndex === -1) return { token: { type: TokenType.MALFORMED, raw: xml.slice(position) }, end: xml.length };
46
+ const end2 = closeIndex + 3;
47
+ return { token: { type: TokenType.CDATA, value: xml.slice(position + 9, end2 - 3) }, end: end2 };
48
+ }
49
+ const end = xml.indexOf(">", position) + 1;
50
+ if (!end) return { token: { type: TokenType.MALFORMED, raw: xml.slice(position) }, end: xml.length };
51
+ const raw = xml.slice(position + 1, end - 1).trim();
52
+ if (raw[0] === "/") return { token: { type: TokenType.ELEMENT_CLOSE, tag: raw.slice(1).trim() }, end };
53
+ const selfClosing = raw[raw.length - 1] === "/";
54
+ const inner = selfClosing ? raw.slice(0, -1).trim() : raw;
55
+ const space = inner.search(WHITESPACE);
56
+ const tag = space === -1 ? inner : inner.slice(0, space);
57
+ const type = selfClosing ? TokenType.SELF_CLOSING : TokenType.ELEMENT_OPEN;
58
+ return { token: { type, tag, attributes: parseAttributes(inner) }, end };
59
+ }
60
+ function tokenize(xml) {
61
+ const tokens = [];
62
+ let position = 0;
63
+ while (position < xml.length) {
64
+ const { token, end } = nextToken(xml, position);
65
+ if (token) tokens.push(token);
66
+ position = end;
67
+ }
68
+ return tokens;
69
+ }
70
+
71
+ // src/parser.ts
72
+ function parse(xml) {
73
+ const tokens = tokenize(xml);
74
+ const cursor = { position: 0 };
75
+ const children = collectChildren(tokens, cursor, null);
76
+ return { type: "document", children };
77
+ }
78
+ function collectChildren(tokens, cursor, parentTag) {
79
+ const children = [];
80
+ while (cursor.position < tokens.length) {
81
+ const token = tokens[cursor.position];
82
+ cursor.position++;
83
+ if (token.type === TokenType.ELEMENT_CLOSE) {
84
+ if (token.tag === parentTag) return children;
85
+ children.push({ type: "malformed", raw: `</${token.tag}>`, malformed: true });
86
+ continue;
87
+ }
88
+ if (token.type === TokenType.ELEMENT_OPEN) {
89
+ const node = { type: "element", tag: token.tag, attributes: token.attributes, children: collectChildren(tokens, cursor, token.tag) };
90
+ children.push(node);
91
+ continue;
92
+ }
93
+ children.push(tokenToNode(token));
94
+ }
95
+ if (parentTag !== null) {
96
+ children.forEach((child) => {
97
+ child.malformed = true;
98
+ });
99
+ }
100
+ return children;
101
+ }
102
+ function tokenToNode(token) {
103
+ if (token.type === TokenType.TEXT) return { type: "text", value: token.value };
104
+ if (token.type === TokenType.COMMENT) return { type: "comment", value: token.value };
105
+ if (token.type === TokenType.CDATA) return { type: "cdata", value: token.value };
106
+ if (token.type === TokenType.SELF_CLOSING) return { type: "element", tag: token.tag, attributes: token.attributes, children: [] };
107
+ if (token.type === TokenType.PROCESSING_INSTRUCTION) return { type: "processing-instruction", target: token.target, attributes: token.attributes };
108
+ if (token.type === TokenType.MALFORMED) return { type: "malformed", raw: token.raw, malformed: true };
109
+ return { type: "malformed", raw: `<${token.tag}>`, malformed: true };
110
+ }
111
+ export {
112
+ parse
113
+ };
package/package.json CHANGED
@@ -1,21 +1,26 @@
1
1
  {
2
2
  "name": "xml-to-html-converter",
3
- "version": "0.1.2",
3
+ "version": "0.1.3",
4
4
  "description": "Zero dependency XML to HTML converter for Node environments",
5
5
  "type": "module",
6
- "main": "src/index.js",
6
+ "main": "./dist/index.js",
7
+ "types": "./dist/index.d.ts",
7
8
  "exports": {
8
- ".": "./src/index.js"
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "import": "./dist/index.js"
12
+ }
9
13
  },
10
14
  "files": [
11
- "src/"
15
+ "dist/"
12
16
  ],
13
17
  "engines": {
14
- "node": ">=18.0.0"
18
+ "node": ">=20.0.0"
15
19
  },
16
20
  "scripts": {
17
- "test": "node --test tests/tokenizer.test.js tests/parser.test.js",
18
- "coverage": "node --experimental-test-coverage --test tests/tokenizer.test.js tests/parser.test.js"
21
+ "build": "tsup",
22
+ "test": "vitest run",
23
+ "coverage": "vitest run --coverage"
19
24
  },
20
25
  "keywords": [
21
26
  "xml",
@@ -24,8 +29,8 @@
24
29
  "parser",
25
30
  "tree"
26
31
  ],
27
- "author": "",
28
- "license": "ISC",
32
+ "author": "Jeffery W. Patterson",
33
+ "license": "MIT",
29
34
  "repository": {
30
35
  "type": "git",
31
36
  "url": "git+https://github.com/jpatterson933/xml-to-html-converter.git"
@@ -33,5 +38,12 @@
33
38
  "bugs": {
34
39
  "url": "https://github.com/jpatterson933/xml-to-html-converter/issues"
35
40
  },
36
- "homepage": "https://github.com/jpatterson933/xml-to-html-converter#readme"
41
+ "homepage": "https://github.com/jpatterson933/xml-to-html-converter#readme",
42
+ "devDependencies": {
43
+ "@types/node": "^25.3.2",
44
+ "@vitest/coverage-v8": "^4.0.18",
45
+ "tsup": "^8.5.1",
46
+ "typescript": "^5.9.3",
47
+ "vitest": "^4.0.18"
48
+ }
37
49
  }
package/src/index.js DELETED
@@ -1,3 +0,0 @@
1
- import { parse } from './parser.js';
2
-
3
- export { parse };
package/src/parser.js DELETED
@@ -1,48 +0,0 @@
1
- import { tokenize, TokenType } from './tokenizer.js';
2
-
3
- function parse(xml) {
4
- const tokens = tokenize(xml);
5
- const cursor = { position: 0 };
6
- const children = collectChildren(tokens, cursor, null);
7
- return { type: 'document', children };
8
- }
9
-
10
- function collectChildren(tokens, cursor, parentTag) {
11
- const children = [];
12
-
13
- while (cursor.position < tokens.length) {
14
- const token = tokens[cursor.position];
15
- cursor.position++;
16
-
17
- if (token.type === TokenType.ELEMENT_CLOSE) {
18
- if (token.tag === parentTag) return children;
19
- children.push({ type: 'malformed', raw: `</${token.tag}>`, malformed: true });
20
- continue;
21
- }
22
-
23
- if (token.type === TokenType.ELEMENT_OPEN) {
24
- const node = { type: 'element', tag: token.tag, attributes: token.attributes, children: collectChildren(tokens, cursor, token.tag) };
25
- children.push(node);
26
- continue;
27
- }
28
-
29
- children.push(tokenToNode(token));
30
- }
31
-
32
- if (parentTag !== null) {
33
- children.forEach(child => { child.malformed = true; });
34
- }
35
-
36
- return children;
37
- }
38
-
39
- function tokenToNode(token) {
40
- if (token.type === TokenType.TEXT) return { type: 'text', value: token.value };
41
- if (token.type === TokenType.COMMENT) return { type: 'comment', value: token.value };
42
- if (token.type === TokenType.CDATA) return { type: 'cdata', value: token.value };
43
- if (token.type === TokenType.SELF_CLOSING) return { type: 'element', tag: token.tag, attributes: token.attributes, children: [] };
44
- if (token.type === TokenType.PROCESSING_INSTRUCTION) return { type: 'processing-instruction', target: token.target, attributes: token.attributes };
45
- return { type: 'malformed', raw: token.raw, malformed: true };
46
- }
47
-
48
- export { parse };
package/src/tokenizer.js DELETED
@@ -1,85 +0,0 @@
1
- const TokenType = {
2
- PROCESSING_INSTRUCTION: 'processing-instruction',
3
- ELEMENT_OPEN: 'element-open',
4
- ELEMENT_CLOSE: 'element-close',
5
- SELF_CLOSING: 'self-closing',
6
- TEXT: 'text',
7
- COMMENT: 'comment',
8
- CDATA: 'cdata',
9
- MALFORMED: 'malformed',
10
- };
11
-
12
- const WHITESPACE = /\s/;
13
- const ATTRIBUTE_PATTERN = /(\S+?)\s*=\s*["']([^"']*)["']/g;
14
-
15
- function parseAttributes(raw) {
16
- const attributes = {};
17
- ATTRIBUTE_PATTERN.lastIndex = 0;
18
- let match;
19
- while ((match = ATTRIBUTE_PATTERN.exec(raw)) !== null) {
20
- attributes[match[1]] = match[2];
21
- }
22
- return attributes;
23
- }
24
-
25
- function nextToken(xml, position) {
26
- if (xml[position] !== '<') {
27
- const end = xml.indexOf('<', position);
28
- const value = xml.slice(position, end === -1 ? xml.length : end);
29
- return { token: value.trim() ? { type: TokenType.TEXT, value } : null, end: end === -1 ? xml.length : end };
30
- }
31
-
32
- const next = xml[position + 1];
33
-
34
- if (next === '?') {
35
- const closeIndex = xml.indexOf('?>', position);
36
- if (closeIndex === -1) return { token: { type: TokenType.MALFORMED, raw: xml.slice(position) }, end: xml.length };
37
- const end = closeIndex + 2;
38
- const inner = xml.slice(position + 2, end - 2).trim();
39
- const space = inner.search(WHITESPACE);
40
- return { token: { type: TokenType.PROCESSING_INSTRUCTION, target: space === -1 ? inner : inner.slice(0, space), attributes: parseAttributes(inner) }, end };
41
- }
42
-
43
- if (next === '!' && xml[position + 2] === '-') {
44
- const closeIndex = xml.indexOf('-->', position);
45
- if (closeIndex === -1) return { token: { type: TokenType.MALFORMED, raw: xml.slice(position) }, end: xml.length };
46
- const end = closeIndex + 3;
47
- return { token: { type: TokenType.COMMENT, value: xml.slice(position + 4, end - 3) }, end };
48
- }
49
-
50
- if (next === '!' && xml[position + 2] === '[') {
51
- const closeIndex = xml.indexOf(']]>', position);
52
- if (closeIndex === -1) return { token: { type: TokenType.MALFORMED, raw: xml.slice(position) }, end: xml.length };
53
- const end = closeIndex + 3;
54
- return { token: { type: TokenType.CDATA, value: xml.slice(position + 9, end - 3) }, end };
55
- }
56
-
57
- const end = xml.indexOf('>', position) + 1;
58
- if (!end) return { token: { type: TokenType.MALFORMED, raw: xml.slice(position) }, end: xml.length };
59
-
60
- const raw = xml.slice(position + 1, end - 1).trim();
61
- if (raw[0] === '/') return { token: { type: TokenType.ELEMENT_CLOSE, tag: raw.slice(1).trim() }, end };
62
-
63
- const selfClosing = raw[raw.length - 1] === '/';
64
- const inner = selfClosing ? raw.slice(0, -1).trim() : raw;
65
- const space = inner.search(WHITESPACE);
66
- const tag = space === -1 ? inner : inner.slice(0, space);
67
- const type = selfClosing ? TokenType.SELF_CLOSING : TokenType.ELEMENT_OPEN;
68
-
69
- return { token: { type, tag, attributes: parseAttributes(inner) }, end };
70
- }
71
-
72
- function tokenize(xml) {
73
- const tokens = [];
74
- let position = 0;
75
-
76
- while (position < xml.length) {
77
- const { token, end } = nextToken(xml, position);
78
- if (token) tokens.push(token);
79
- position = end;
80
- }
81
-
82
- return tokens;
83
- }
84
-
85
- export { tokenize, TokenType };