Skip to content

Commit d31bedf

Browse files
committed
Add XML fragment parsing and enhance XmlDocument functionality; update TypeScript configuration and add new tests
1 parent 5c7289c commit d31bedf

16 files changed

+371
-139
lines changed

.npmignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,5 @@
33

44
*.spec.ts
55
*.spec.js
6+
7+
*.tgz

README.md

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,37 @@ If you have an XML file that you want to read, modify, and write back, this libr
1414
- Retains XML processing instructions (including the XML declaration)
1515
- Retains CDATA sections
1616

17+
## Example
18+
19+
The following XML:
20+
21+
```xml
22+
<?xml version="1.0" encoding="UTF-8"?>
23+
<root>
24+
<!-- my comment -->
25+
<element attribute="value" />
26+
<element attribute='value'></element>
27+
</root>
28+
```
29+
30+
Will get parsed by `XmlParser.parse(..)` to:
31+
32+
```ts
33+
new XmlDocument([
34+
new XmlProcessing('xml',' ', 'version="1.0" encoding="UTF-8"'),
35+
new XmlText('\n'),
36+
new XmlElement('root', [], [
37+
new XmlText('\n '),
38+
new XmlComment(' my comment '),
39+
new XmlText('\n '),
40+
new XmlElement('element', [new XmlAttribute('attribute', 'value')], [], ' ', true),
41+
new XmlText('\n '),
42+
new XmlElement('element', [new XmlAttribute('attribute', 'value', ' ', '', '', "'")], [], '', false),
43+
new XmlText('\n')
44+
])
45+
]);
46+
```
47+
1748
## Development
1849

1950
This project is written in Typescript and uses Bun.

model/xmlCData.spec.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import { describe, expect, it } from 'bun:test';
2+
import { XmlCData } from './xmlCData';
3+
4+
describe('XmlCData', () => {
5+
it('CDATA section', () => {
6+
const cdata = '<tag>Some data</tag>';
7+
const node = new XmlCData(cdata);
8+
expect(node.toString()).toBe(`<![CDATA[${cdata}]]>`);
9+
});
10+
});

model/xmlComment.spec.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import { describe, expect, it } from 'bun:test';
2+
import { XmlComment } from './xmlComment';
3+
4+
describe('XmlComment', () => {
5+
it('Comment node roundtrip', () => {
6+
const comment = 'This is a comment';
7+
const node = new XmlComment(comment);
8+
expect(node.toString()).toBe(`<!--${comment}-->`);
9+
});
10+
});

model/xmlDoctype.spec.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import { describe, expect, it } from 'bun:test';
2+
import { XmlDoctype } from './xmlDoctype';
3+
4+
describe('XmlDoctype', () => {
5+
6+
it('DOCTYPE declaration', () => {
7+
const content = 'note SYSTEM "Note.dtd"';
8+
const node = new XmlDoctype(content);
9+
expect(node.toString()).toBe(`<!DOCTYPE ${content}>`);
10+
});
11+
});

model/xmlDocument.spec.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import { describe, expect, it } from 'bun:test';
2+
import { XmlDocument } from './xmlDocument';
3+
import { XmlElement } from './xmlElement';
4+
5+
describe('XmlDocument', () => {
6+
describe('getRootElement', () => {
7+
it('should return the root element', () => {
8+
const root = new XmlElement('root');
9+
const document = new XmlDocument([root]);
10+
expect(document.getRootElement()).toBe(root);
11+
});
12+
13+
it('should throw if no root element found', () => {
14+
const document = new XmlDocument([]);
15+
expect(() => document.getRootElement()).toThrowError('No root element found');
16+
});
17+
})
18+
});

model/xmlDocument.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,20 @@
11
import { XmlNode } from './xmlNode';
2+
import { XmlElement } from './xmlElement';
23

34
/// The document node is simply a container for a sequence of XML nodes.
45
export class XmlDocument extends XmlNode {
56
constructor(public children: XmlNode[]) {
67
super();
78
}
89

10+
getRootElement(): XmlElement {
11+
const root = this.children.find((child) => child instanceof XmlElement);
12+
if (!root) {
13+
throw new Error('No root element found');
14+
}
15+
return root;
16+
}
17+
918
toString(): string {
1019
return this.children.map((child) => child.toString()).join('');
1120
}

model/xmlElement.spec.ts

Lines changed: 115 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,49 @@ import { XmlText } from './xmlText';
44
import { XmlAttribute } from './xmlAttribute';
55

66
describe('XmlElement', () => {
7+
describe('toString', () => {
8+
it('Element with attributes and text', () => {
9+
const attrDate = new XmlAttribute('date', '2025-02-22', ' ', '', ' ', '"');
10+
const attrAuthor = new XmlAttribute(
11+
'author',
12+
'John Doe',
13+
' ',
14+
' ',
15+
'',
16+
'\''
17+
);
18+
const elem = new XmlElement(
19+
'note',
20+
[attrDate, attrAuthor],
21+
[new XmlText('Some content')],
22+
' ',
23+
false,
24+
''
25+
);
26+
const expected = `<note date= "2025-02-22" author ='John Doe' >Some content</note>`;
27+
expect(elem.toString()).toBe(expected);
28+
});
29+
30+
it('Self-closing element', () => {
31+
const elem = new XmlElement('br', [], [], ' ', true, '');
32+
const expected = `<br />`;
33+
expect(elem.toString()).toBe(expected);
34+
});
35+
36+
it('Nested elements', () => {
37+
const child = new XmlElement(
38+
'child',
39+
[],
40+
[new XmlText('Text')],
41+
' ',
42+
false,
43+
''
44+
);
45+
const root = new XmlElement('root', [], [child], '', false, '');
46+
const expected = `<root><child >Text</child></root>`;
47+
expect(root.toString()).toBe(expected);
48+
});
49+
});
750
describe('addElement', () => {
851
it('should add a child element', () => {
952
const existingChild = new XmlElement('child1');
@@ -19,7 +62,7 @@ describe('XmlElement', () => {
1962
const element = new XmlElement(
2063
'test',
2164
[],
22-
[existingChild1, existingChild2],
65+
[existingChild1, existingChild2]
2366
);
2467
const child = new XmlElement('child');
2568
element.addElement(child, { after: existingChild1 });
@@ -32,7 +75,7 @@ describe('XmlElement', () => {
3275
const element = new XmlElement(
3376
'test',
3477
[],
35-
[existingChild1, existingChild2],
78+
[existingChild1, existingChild2]
3679
);
3780
const child = new XmlElement('child');
3881
element.addElement(child, { before: existingChild2 });
@@ -52,33 +95,46 @@ describe('XmlElement', () => {
5295
const element = new XmlElement(
5396
'test',
5497
[],
55-
[new XmlText('\n '), existingChild, new XmlText('\n')],
98+
[new XmlText('\n '), existingChild, new XmlText('\n')]
5699
);
57100
const child = new XmlElement('new-child');
58101
element.addElement(child, {
59-
after: existingChild,
60-
guessFormatting: true,
102+
after: existingChild
61103
});
62104
expect(element.toString()).toEqual(
63-
`<test>\n <child></child>\n <new-child></new-child>\n</test>`,
105+
`<test>\n <child></child>\n <new-child></new-child>\n</test>`
64106
);
65107
});
66108
it('should add whitespace according previous node with before', () => {
67109
const existingChild = new XmlElement('child');
68110
const element = new XmlElement(
69111
'test',
70112
[],
71-
[new XmlText('\n '), existingChild, new XmlText('\n')],
113+
[new XmlText('\n '), existingChild, new XmlText('\n')]
72114
);
73115
const child = new XmlElement('new-child');
74116
element.addElement(child, {
75-
before: existingChild,
76-
guessFormatting: true,
117+
before: existingChild
77118
});
78119
expect(element.toString()).toEqual(
79-
`<test>\n <new-child></new-child>\n <child></child>\n</test>`,
120+
`<test>\n <new-child></new-child>\n <child></child>\n</test>`
80121
);
81122
});
123+
it('should handle trailing whitespace', () => {
124+
const element = new XmlElement('root', [], [
125+
new XmlText('\n '),
126+
new XmlElement('child'),
127+
new XmlText('\n')
128+
]);
129+
element.addElement(new XmlElement('new-child'));
130+
expect(element).toEqual(new XmlElement('root', [], [
131+
new XmlText('\n '),
132+
new XmlElement('child'),
133+
new XmlText('\n '),
134+
new XmlElement('new-child'),
135+
new XmlText('\n')
136+
]));
137+
});
82138
});
83139
});
84140
describe('getAttributeValue', () => {
@@ -92,4 +148,53 @@ describe('XmlElement', () => {
92148
expect(element.getAttributeValue('key')).toBeUndefined();
93149
});
94150
});
151+
describe('setAttributeValue', () => {
152+
it('should set the value of the attribute', () => {
153+
const element = new XmlElement('test', [new XmlAttribute('key', 'value')]);
154+
element.setAttributeValue('key', 'new & value');
155+
expect(element.getAttributeValue('key')).toBe('new & value');
156+
expect(element.attributes).toEqual([new XmlAttribute('key', 'new &amp; value')]);
157+
});
158+
159+
it('should add the attribute if it does not exist', () => {
160+
const element = new XmlElement('test');
161+
element.setAttributeValue('key', 'value');
162+
expect(element.getAttributeValue('key')).toBe('value');
163+
});
164+
165+
it('should add the attribute with analogous formatting', () => {
166+
const element = new XmlElement('test', [new XmlAttribute('key', 'value', '\n ', ' ', ' ', '\'')]);
167+
element.setAttributeValue('another-key', 'value');
168+
expect(element.toString()).toBe(`<test
169+
key = 'value'
170+
another-key = 'value'></test>`);
171+
});
172+
});
173+
describe('getFirstElementByName', () => {
174+
it('should return the first element with the given name', () => {
175+
const child1 = new XmlElement('child');
176+
const child2 = new XmlElement('child');
177+
const element = new XmlElement('test', [], [child1, child2]);
178+
expect(element.getFirstElementByName('child')).toBe(child1);
179+
});
180+
181+
it('should return undefined if no element with the given name exists', () => {
182+
const element = new XmlElement('test');
183+
expect(element.getFirstElementByName('child')).toBeUndefined();
184+
});
185+
});
186+
187+
describe('getElementsByName', () => {
188+
it('should return all elements with the given name', () => {
189+
const child1 = new XmlElement('child');
190+
const child2 = new XmlElement('child');
191+
const element = new XmlElement('test', [], [child1, child2]);
192+
expect(element.getElementsByName('child')).toEqual([child1, child2]);
193+
});
194+
195+
it('should return an empty array if no element with the given name exists', () => {
196+
const element = new XmlElement('test');
197+
expect(element.getElementsByName('child')).toEqual([]);
198+
});
199+
});
95200
});

0 commit comments

Comments
 (0)