@safeaccess/inline 0.1.1 → 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/.gitattributes +1 -1
- package/CHANGELOG.md +23 -5
- package/LICENSE +1 -1
- package/README.md +79 -21
- package/dist/accessors/abstract-accessor.d.ts +24 -10
- package/dist/accessors/abstract-accessor.js +21 -8
- package/dist/accessors/abstract-integration-accessor.d.ts +22 -0
- package/dist/accessors/abstract-integration-accessor.js +23 -0
- package/dist/accessors/formats/any-accessor.d.ts +10 -8
- package/dist/accessors/formats/any-accessor.js +9 -8
- package/dist/accessors/formats/array-accessor.d.ts +2 -0
- package/dist/accessors/formats/array-accessor.js +2 -0
- package/dist/accessors/formats/env-accessor.d.ts +2 -0
- package/dist/accessors/formats/env-accessor.js +2 -0
- package/dist/accessors/formats/ini-accessor.d.ts +2 -0
- package/dist/accessors/formats/ini-accessor.js +2 -0
- package/dist/accessors/formats/json-accessor.d.ts +2 -0
- package/dist/accessors/formats/json-accessor.js +2 -0
- package/dist/accessors/formats/ndjson-accessor.d.ts +2 -0
- package/dist/accessors/formats/ndjson-accessor.js +2 -0
- package/dist/accessors/formats/object-accessor.d.ts +2 -0
- package/dist/accessors/formats/object-accessor.js +2 -0
- package/dist/accessors/formats/xml-accessor.d.ts +2 -0
- package/dist/accessors/formats/xml-accessor.js +2 -0
- package/dist/accessors/formats/yaml-accessor.d.ts +3 -1
- package/dist/accessors/formats/yaml-accessor.js +4 -2
- package/dist/cache/simple-path-cache.d.ts +51 -0
- package/dist/cache/simple-path-cache.js +72 -0
- package/dist/contracts/accessors-interface.d.ts +2 -0
- package/dist/contracts/factory-accessors-interface.d.ts +2 -0
- package/dist/contracts/filter-evaluator-interface.d.ts +28 -0
- package/dist/contracts/filter-evaluator-interface.js +1 -0
- package/dist/contracts/parse-integration-interface.d.ts +2 -0
- package/dist/contracts/parser-interface.d.ts +92 -0
- package/dist/contracts/parser-interface.js +1 -0
- package/dist/contracts/path-cache-interface.d.ts +7 -6
- package/dist/contracts/readable-accessors-interface.d.ts +11 -6
- package/dist/contracts/security-guard-interface.d.ts +2 -0
- package/dist/contracts/security-parser-interface.d.ts +2 -0
- package/dist/contracts/validatable-parser-interface.d.ts +59 -0
- package/dist/contracts/validatable-parser-interface.js +1 -0
- package/dist/contracts/writable-accessors-interface.d.ts +5 -0
- package/dist/core/accessor-factory.d.ts +124 -0
- package/dist/core/accessor-factory.js +157 -0
- package/dist/core/dot-notation-parser.d.ts +34 -5
- package/dist/core/dot-notation-parser.js +51 -10
- package/dist/core/inline-builder-accessor.d.ts +82 -0
- package/dist/core/inline-builder-accessor.js +107 -0
- package/dist/exceptions/accessor-exception.d.ts +9 -0
- package/dist/exceptions/accessor-exception.js +9 -0
- package/dist/exceptions/invalid-format-exception.d.ts +5 -0
- package/dist/exceptions/invalid-format-exception.js +5 -0
- package/dist/exceptions/parser-exception.d.ts +4 -0
- package/dist/exceptions/parser-exception.js +4 -0
- package/dist/exceptions/path-not-found-exception.d.ts +4 -0
- package/dist/exceptions/path-not-found-exception.js +4 -0
- package/dist/exceptions/readonly-violation-exception.d.ts +4 -0
- package/dist/exceptions/readonly-violation-exception.js +4 -0
- package/dist/exceptions/security-exception.d.ts +6 -0
- package/dist/exceptions/security-exception.js +6 -0
- package/dist/exceptions/unsupported-type-exception.d.ts +4 -0
- package/dist/exceptions/unsupported-type-exception.js +4 -0
- package/dist/exceptions/yaml-parse-exception.d.ts +4 -0
- package/dist/exceptions/yaml-parse-exception.js +4 -0
- package/dist/index.js +2 -1
- package/dist/inline.d.ts +26 -56
- package/dist/inline.js +43 -111
- package/dist/parser/xml-parser.js +23 -10
- package/dist/parser/yaml-parser.d.ts +54 -7
- package/dist/parser/yaml-parser.js +268 -51
- package/dist/path-query/segment-filter-parser.d.ts +142 -0
- package/dist/path-query/segment-filter-parser.js +384 -0
- package/dist/path-query/segment-parser.d.ts +98 -0
- package/dist/path-query/segment-parser.js +283 -0
- package/dist/path-query/segment-path-resolver.d.ts +149 -0
- package/dist/path-query/segment-path-resolver.js +351 -0
- package/dist/path-query/segment-type.d.ts +85 -0
- package/dist/path-query/segment-type.js +35 -0
- package/dist/security/forbidden-keys.d.ts +2 -2
- package/dist/security/forbidden-keys.js +5 -5
- package/dist/security/security-guard.d.ts +4 -1
- package/dist/security/security-guard.js +7 -2
- package/dist/security/security-parser.d.ts +10 -1
- package/dist/security/security-parser.js +10 -1
- package/dist/type-format.d.ts +2 -0
- package/dist/type-format.js +2 -0
- package/package.json +11 -3
- package/src/accessors/abstract-accessor.ts +25 -19
- package/src/accessors/abstract-integration-accessor.ts +27 -0
- package/src/accessors/formats/any-accessor.ts +11 -11
- package/src/accessors/formats/array-accessor.ts +2 -0
- package/src/accessors/formats/env-accessor.ts +2 -0
- package/src/accessors/formats/ini-accessor.ts +2 -0
- package/src/accessors/formats/json-accessor.ts +2 -0
- package/src/accessors/formats/ndjson-accessor.ts +2 -0
- package/src/accessors/formats/object-accessor.ts +2 -0
- package/src/accessors/formats/xml-accessor.ts +2 -0
- package/src/accessors/formats/yaml-accessor.ts +4 -2
- package/src/cache/simple-path-cache.ts +77 -0
- package/src/contracts/accessors-interface.ts +2 -0
- package/src/contracts/factory-accessors-interface.ts +2 -0
- package/src/contracts/filter-evaluator-interface.ts +30 -0
- package/src/contracts/parse-integration-interface.ts +2 -0
- package/src/contracts/parser-interface.ts +114 -0
- package/src/contracts/path-cache-interface.ts +8 -6
- package/src/contracts/readable-accessors-interface.ts +11 -6
- package/src/contracts/security-guard-interface.ts +2 -0
- package/src/contracts/security-parser-interface.ts +2 -0
- package/src/contracts/validatable-parser-interface.ts +64 -0
- package/src/contracts/writable-accessors-interface.ts +5 -0
- package/src/core/accessor-factory.ts +173 -0
- package/src/core/dot-notation-parser.ts +74 -11
- package/src/core/inline-builder-accessor.ts +163 -0
- package/src/exceptions/accessor-exception.ts +9 -0
- package/src/exceptions/invalid-format-exception.ts +5 -0
- package/src/exceptions/parser-exception.ts +4 -0
- package/src/exceptions/path-not-found-exception.ts +4 -0
- package/src/exceptions/readonly-violation-exception.ts +4 -0
- package/src/exceptions/security-exception.ts +6 -0
- package/src/exceptions/unsupported-type-exception.ts +4 -0
- package/src/exceptions/yaml-parse-exception.ts +4 -0
- package/src/index.ts +3 -1
- package/src/inline.ts +46 -120
- package/src/parser/xml-parser.ts +31 -10
- package/src/parser/yaml-parser.ts +310 -45
- package/src/path-query/segment-filter-parser.ts +444 -0
- package/src/path-query/segment-parser.ts +321 -0
- package/src/path-query/segment-path-resolver.ts +521 -0
- package/src/path-query/segment-type.ts +82 -0
- package/src/security/forbidden-keys.ts +5 -5
- package/src/security/security-guard.ts +10 -2
- package/src/security/security-parser.ts +18 -3
- package/src/type-format.ts +2 -0
- package/stryker.config.json +8 -10
- package/tests/accessors/abstract-accessor.test.ts +217 -0
- package/tests/accessors/abstract-integration-accessor.test.ts +37 -0
- package/tests/accessors/formats/any-accessor.test.ts +57 -0
- package/tests/accessors/formats/array-accessor.test.ts +42 -0
- package/tests/accessors/formats/env-accessor.test.ts +103 -0
- package/tests/accessors/formats/ini-accessor.test.ts +186 -0
- package/tests/accessors/{json-accessor.test.ts → formats/json-accessor.test.ts} +6 -6
- package/tests/accessors/formats/ndjson-accessor.test.ts +49 -0
- package/tests/accessors/formats/object-accessor.test.ts +172 -0
- package/tests/accessors/formats/xml-accessor.test.ts +162 -0
- package/tests/accessors/formats/yaml-accessor.test.ts +36 -0
- package/tests/cache/simple-path-cache.test.ts +168 -0
- package/tests/core/accessor-factory.test.ts +157 -0
- package/tests/core/dot-notation-parser-edge-cases.test.ts +415 -0
- package/tests/core/dot-notation-parser.test.ts +0 -288
- package/tests/core/inline-builder-accessor.test.ts +114 -0
- package/tests/exceptions/accessor-exception.test.ts +28 -0
- package/tests/exceptions/invalid-format-exception.test.ts +31 -0
- package/tests/exceptions/path-not-found-exception.test.ts +33 -0
- package/tests/exceptions/readonly-violation-exception.test.ts +35 -0
- package/tests/exceptions/security-exception.test.ts +33 -0
- package/tests/exceptions/unsupported-type-exception.test.ts +33 -0
- package/tests/exceptions/yaml-parse-exception.test.ts +38 -0
- package/tests/mocks/fake-path-cache.ts +4 -3
- package/tests/parity-from.test.ts +118 -0
- package/tests/parity.test.ts +227 -10
- package/tests/parser/xml-parser-mutations.test.ts +579 -0
- package/tests/parser/xml-parser-scanner.test.ts +379 -0
- package/tests/parser/xml-parser.test.ts +17 -330
- package/tests/parser/yaml-parser-mutations.test.ts +750 -0
- package/tests/parser/yaml-parser.test.ts +844 -18
- package/tests/path-query/segment-filter-parser-mutations.test.ts +735 -0
- package/tests/path-query/segment-filter-parser.test.ts +1091 -0
- package/tests/path-query/segment-parser-mutations.test.ts +539 -0
- package/tests/path-query/segment-parser.test.ts +606 -0
- package/tests/path-query/segment-path-resolver-mutations.test.ts +626 -0
- package/tests/path-query/segment-path-resolver.test.ts +1009 -0
- package/tests/security/security-guard-advanced.test.ts +413 -0
- package/tests/security/security-guard-forbidden-keys.test.ts +87 -0
- package/tests/security/security-guard.test.ts +8 -479
- package/tests/security/security-parser.test.ts +18 -14
- package/vitest.config.ts +3 -3
- package/benchmarks/get.bench.ts +0 -26
- package/benchmarks/parse.bench.ts +0 -41
- package/tests/accessors/accessors.test.ts +0 -1017
|
@@ -0,0 +1,379 @@
|
|
|
1
|
+
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import { XmlParser } from '../../src/parser/xml-parser.js';
|
|
3
|
+
import { InvalidFormatException } from '../../src/exceptions/invalid-format-exception.js';
|
|
4
|
+
import { SecurityException } from '../../src/exceptions/security-exception.js';
|
|
5
|
+
|
|
6
|
+
function makeParser(maxDepth = 10): XmlParser {
|
|
7
|
+
return new XmlParser(maxDepth);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
type FakeAttr = { name: string; value: string };
|
|
11
|
+
type FakeNode = {
|
|
12
|
+
nodeType: number;
|
|
13
|
+
textContent?: string;
|
|
14
|
+
nodeName?: string;
|
|
15
|
+
attributes?: FakeAttrs;
|
|
16
|
+
childNodes?: FakeChildNodes;
|
|
17
|
+
};
|
|
18
|
+
type FakeAttrs = { length: number; [index: number]: FakeAttr | undefined };
|
|
19
|
+
type FakeChildNodes = { length: number; [index: number]: FakeNode | undefined };
|
|
20
|
+
|
|
21
|
+
function makeTextNode(text: string): FakeNode {
|
|
22
|
+
return { nodeType: 3, textContent: text };
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function makeElement(name: string, children: FakeNode[] = [], attrs: FakeAttr[] = []): FakeNode {
|
|
26
|
+
const attributes: FakeAttrs = { length: attrs.length };
|
|
27
|
+
attrs.forEach((a, i) => {
|
|
28
|
+
attributes[i] = a;
|
|
29
|
+
});
|
|
30
|
+
const childNodes: FakeChildNodes = { length: children.length };
|
|
31
|
+
children.forEach((c, i) => {
|
|
32
|
+
childNodes[i] = c;
|
|
33
|
+
});
|
|
34
|
+
return { nodeType: 1, nodeName: name, attributes, childNodes };
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function stubDomParser(root: FakeNode | null, hasParserError = false): void {
|
|
38
|
+
const parserErrorEl = hasParserError ? { textContent: 'parse failed' } : null;
|
|
39
|
+
vi.stubGlobal(
|
|
40
|
+
'DOMParser',
|
|
41
|
+
class {
|
|
42
|
+
parseFromString(): unknown {
|
|
43
|
+
return {
|
|
44
|
+
querySelector: (sel: string) => (sel === 'parsererror' ? parserErrorEl : null),
|
|
45
|
+
documentElement: root,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
},
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
describe(`${XmlParser.name} > browser DOMParser path`, () => {
|
|
53
|
+
afterEach(() => {
|
|
54
|
+
vi.unstubAllGlobals();
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it('delegates to DOMParser when available', () => {
|
|
58
|
+
const root = makeElement('root', [makeElement('name', [makeTextNode('Alice')])]);
|
|
59
|
+
stubDomParser(root);
|
|
60
|
+
expect(makeParser().parse('<root><name>Alice</name></root>')).toEqual({
|
|
61
|
+
name: { '#text': 'Alice' },
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it('passes application/xml as MIME type to DOMParser', () => {
|
|
66
|
+
let capturedType = '';
|
|
67
|
+
vi.stubGlobal(
|
|
68
|
+
'DOMParser',
|
|
69
|
+
class {
|
|
70
|
+
parseFromString(_xml: string, type: string): unknown {
|
|
71
|
+
capturedType = type;
|
|
72
|
+
return {
|
|
73
|
+
querySelector: () => null,
|
|
74
|
+
documentElement: makeElement('root', []),
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
},
|
|
78
|
+
);
|
|
79
|
+
makeParser().parse('<root/>');
|
|
80
|
+
expect(capturedType).toBe('application/xml');
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it('throws InvalidFormatException when DOMParser reports parsererror', () => {
|
|
84
|
+
stubDomParser(null, true);
|
|
85
|
+
expect(() => makeParser().parse('<root/>')).toThrow(InvalidFormatException);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it('includes the parseerror detail in the exception message', () => {
|
|
89
|
+
stubDomParser(null, true);
|
|
90
|
+
expect(() => makeParser().parse('<root/>')).toThrow(/parse failed/);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it('uses Unknown error when parseerror textContent is null', () => {
|
|
94
|
+
vi.stubGlobal(
|
|
95
|
+
'DOMParser',
|
|
96
|
+
class {
|
|
97
|
+
parseFromString(): unknown {
|
|
98
|
+
return {
|
|
99
|
+
querySelector: (sel: string) =>
|
|
100
|
+
sel === 'parsererror' ? { textContent: null } : null,
|
|
101
|
+
documentElement: null,
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
},
|
|
105
|
+
);
|
|
106
|
+
expect(() => makeParser().parse('<root/>')).toThrow(/Unknown error/);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it('returns empty object when documentElement is null', () => {
|
|
110
|
+
stubDomParser(null, false);
|
|
111
|
+
expect(makeParser().parse('<root/>')).toEqual({});
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it('maps element attributes with @ prefix', () => {
|
|
115
|
+
const root = makeElement('root', [], [{ name: 'id', value: '42' }]);
|
|
116
|
+
stubDomParser(root);
|
|
117
|
+
const result = makeParser().parse('<root id="42"/>');
|
|
118
|
+
expect(result['@id']).toBe('42');
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it('ignores undefined attribute slots', () => {
|
|
122
|
+
const attrs: FakeAttrs = { length: 2, 0: { name: 'a', value: '1' }, 1: undefined };
|
|
123
|
+
const root: FakeNode = {
|
|
124
|
+
nodeType: 1,
|
|
125
|
+
nodeName: 'root',
|
|
126
|
+
attributes: attrs,
|
|
127
|
+
childNodes: { length: 0 },
|
|
128
|
+
};
|
|
129
|
+
stubDomParser(root);
|
|
130
|
+
expect(makeParser().parse('<root a="1"/>')).toEqual({ '@a': '1' });
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it('captures non-empty text nodes as #text', () => {
|
|
134
|
+
const root = makeElement('root', [makeTextNode(' hello ')]);
|
|
135
|
+
stubDomParser(root);
|
|
136
|
+
const result = makeParser().parse('<root>hello</root>');
|
|
137
|
+
expect(result['#text']).toBe('hello');
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it('ignores whitespace-only text nodes', () => {
|
|
141
|
+
const root = makeElement('root', [makeTextNode(' ')]);
|
|
142
|
+
stubDomParser(root);
|
|
143
|
+
expect(makeParser().parse('<root> </root>')).toEqual({});
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
it('handles text node with null textContent gracefully', () => {
|
|
147
|
+
const nullTextNode: FakeNode = { nodeType: 3, textContent: null as unknown as string };
|
|
148
|
+
const root = makeElement('root', [nullTextNode]);
|
|
149
|
+
stubDomParser(root);
|
|
150
|
+
expect(makeParser().parse('<root/>')).toEqual({});
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
it('ignores undefined child node slots', () => {
|
|
154
|
+
const undef: FakeNode = undefined as unknown as FakeNode;
|
|
155
|
+
const childNodes: FakeChildNodes = { length: 2, 0: makeTextNode('hello'), 1: undef };
|
|
156
|
+
const root: FakeNode = {
|
|
157
|
+
nodeType: 1,
|
|
158
|
+
nodeName: 'root',
|
|
159
|
+
attributes: { length: 0 },
|
|
160
|
+
childNodes,
|
|
161
|
+
};
|
|
162
|
+
stubDomParser(root);
|
|
163
|
+
const result = makeParser().parse('<root/>');
|
|
164
|
+
expect(result['#text']).toBe('hello');
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
it('merges duplicate child elements into an array via DOMParser', () => {
|
|
168
|
+
const root = makeElement('root', [
|
|
169
|
+
makeElement('item', [makeTextNode('a')]),
|
|
170
|
+
makeElement('item', [makeTextNode('b')]),
|
|
171
|
+
]);
|
|
172
|
+
stubDomParser(root);
|
|
173
|
+
const result = makeParser().parse('<root><item>a</item><item>b</item></root>');
|
|
174
|
+
expect(result['item']).toEqual([{ '#text': 'a' }, { '#text': 'b' }]);
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
it('pushes into existing array when third duplicate appears', () => {
|
|
178
|
+
const root = makeElement('root', [
|
|
179
|
+
makeElement('item', [makeTextNode('a')]),
|
|
180
|
+
makeElement('item', [makeTextNode('b')]),
|
|
181
|
+
makeElement('item', [makeTextNode('c')]),
|
|
182
|
+
]);
|
|
183
|
+
stubDomParser(root);
|
|
184
|
+
const result = makeParser().parse(
|
|
185
|
+
'<root><item>a</item><item>b</item><item>c</item></root>',
|
|
186
|
+
);
|
|
187
|
+
expect(Array.isArray(result['item'])).toBe(true);
|
|
188
|
+
expect((result['item'] as unknown[]).length).toBe(3);
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
it('returns element with only #text key as-is (not flattened)', () => {
|
|
192
|
+
const root = makeElement('root', [makeTextNode('only-text')]);
|
|
193
|
+
stubDomParser(root);
|
|
194
|
+
const result = makeParser().parse('<root>only-text</root>');
|
|
195
|
+
expect(result['#text']).toBe('only-text');
|
|
196
|
+
expect(Object.keys(result).length).toBe(1);
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
it('does not throw when DOM element depth exactly equals maxDepth', () => {
|
|
200
|
+
const child = makeElement('child', [makeTextNode('v')]);
|
|
201
|
+
const root = makeElement('root', [child]);
|
|
202
|
+
stubDomParser(root);
|
|
203
|
+
expect(() => new XmlParser(1).parse('<root/>')).not.toThrow();
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
it('throws SecurityException when DOM element depth exceeds maxDepth', () => {
|
|
207
|
+
const deep = makeElement('c', [makeTextNode('v')]);
|
|
208
|
+
const mid = makeElement('b', [deep]);
|
|
209
|
+
const root = makeElement('root', [makeElement('a', [mid])]);
|
|
210
|
+
stubDomParser(root);
|
|
211
|
+
expect(() => new XmlParser(1).parse('<root/>')).toThrow(SecurityException);
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
it('includes depth information in the SecurityException message', () => {
|
|
215
|
+
const deep = makeElement('c', [makeTextNode('v')]);
|
|
216
|
+
const mid = makeElement('b', [deep]);
|
|
217
|
+
const root = makeElement('root', [makeElement('a', [mid])]);
|
|
218
|
+
stubDomParser(root);
|
|
219
|
+
expect(() => new XmlParser(1).parse('<root/>')).toThrow(/exceed/i);
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
it('ignores child nodes with nodeType other than 1 or 3', () => {
|
|
223
|
+
const unknownNode: FakeNode = { nodeType: 8 };
|
|
224
|
+
const root = makeElement('root', [unknownNode, makeElement('name', [makeTextNode('Bob')])]);
|
|
225
|
+
stubDomParser(root);
|
|
226
|
+
const result = makeParser().parse('<root/>');
|
|
227
|
+
expect(result['name']).toEqual({ '#text': 'Bob' });
|
|
228
|
+
});
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
describe(`${XmlParser.name} > linear scanner - nesting counter`, () => {
|
|
232
|
+
it('extracts outer element when same-name elements nest (kills nestDepth++ mutant)', () => {
|
|
233
|
+
// nestDepth must be incremented at inner <a> so the first </a> does not
|
|
234
|
+
// prematurely close the outer element
|
|
235
|
+
const result = makeParser().parse('<root><a><a>inner</a>rest</a></root>');
|
|
236
|
+
const a = result['a'] as Record<string, unknown>;
|
|
237
|
+
expect(a['a']).toBe('inner');
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
it('resolves 3-deep same-name nesting (kills off-by-one in nestDepth-- condition)', () => {
|
|
241
|
+
// nestDepth starts at 1, increments twice, decrements 3× - only
|
|
242
|
+
// when it hits exactly 0 should inner content be collected
|
|
243
|
+
const result = makeParser().parse('<root><a><a><a>deep</a></a></a></root>');
|
|
244
|
+
const a1 = result['a'] as Record<string, unknown>;
|
|
245
|
+
const a2 = a1['a'] as Record<string, unknown>;
|
|
246
|
+
expect(a2['a']).toBe('deep');
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
it('does not count a self-closing same-name tag as open nestDepth (kills self-closing increment mutant)', () => {
|
|
250
|
+
// <a/> inside <a>…</a> must NOT increment nestDepth; if it did, the first
|
|
251
|
+
// </a> would only bring nestDepth to 1 and the parser would scan past it
|
|
252
|
+
const result = makeParser().parse('<root><a><a/>text</a></root>');
|
|
253
|
+
const a = result['a'] as Record<string, unknown>;
|
|
254
|
+
expect(a['a']).toBe('');
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
it('increments nestDepth for same-name opening tag with tab after name', () => {
|
|
258
|
+
const result = makeParser().parse('<root><a><a\t>inner</a></a></root>');
|
|
259
|
+
const a = result['a'] as Record<string, unknown>;
|
|
260
|
+
expect(a).toEqual({ a: 'inner' });
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
it('increments nestDepth for same-name opening tag with newline after name', () => {
|
|
264
|
+
const result = makeParser().parse('<root><a><a\n>inner</a></a></root>');
|
|
265
|
+
const a = result['a'] as Record<string, unknown>;
|
|
266
|
+
expect(a).toEqual({ a: 'inner' });
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
it('increments nestDepth for same-name opening tag with carriage return after name', () => {
|
|
270
|
+
const result = makeParser().parse('<root><a><a\r>inner</a></a></root>');
|
|
271
|
+
const a = result['a'] as Record<string, unknown>;
|
|
272
|
+
expect(a).toEqual({ a: 'inner' });
|
|
273
|
+
});
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
describe(`${XmlParser.name} > linear scanner - self-closing detection`, () => {
|
|
277
|
+
it('treats <tag /> (spaces before />) as self-closing (kills trimEnd mutant)', () => {
|
|
278
|
+
const result = makeParser().parse('<root><empty /></root>');
|
|
279
|
+
expect(result['empty']).toBe('');
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
it('treats <tag attr="v" /> as self-closing (attribute + space + /)', () => {
|
|
283
|
+
const result = makeParser().parse('<root><flag enabled="true" /></root>');
|
|
284
|
+
expect(result['flag']).toBe('');
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
it('treats <tag / > (space after slash before >) as self-closing', () => {
|
|
288
|
+
const result = makeParser().parse('<root><empty / ></root>');
|
|
289
|
+
expect(result['empty']).toBe('');
|
|
290
|
+
});
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
describe(`${XmlParser.name} > linear scanner - skip non-element tokens`, () => {
|
|
294
|
+
it('skips XML comment nodes inside children (kills nextChar === "!" mutant)', () => {
|
|
295
|
+
const result = makeParser().parse('<root><!-- comment --><name>Alice</name></root>');
|
|
296
|
+
expect(result['name']).toBe('Alice');
|
|
297
|
+
expect(Object.keys(result)).toEqual(['name']);
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
it('skips processing instructions inside children (kills nextChar === "?" mutant)', () => {
|
|
301
|
+
const result = makeParser().parse('<root><?pi data?><name>Bob</name></root>');
|
|
302
|
+
expect(result['name']).toBe('Bob');
|
|
303
|
+
expect(Object.keys(result)).toEqual(['name']);
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
it('skips stray closing tags inside children (kills nextChar === "/" mutant)', () => {
|
|
307
|
+
const result = makeParser().parse('<root></stray><name>Charlie</name></root>');
|
|
308
|
+
expect(result['name']).toBe('Charlie');
|
|
309
|
+
expect(Object.keys(result)).toEqual(['name']);
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
it('handles comment-like token with no closing > (gt === -1 ternary branch)', () => {
|
|
313
|
+
// '<!no close tag' in inner content - no '>' found, so i is set to
|
|
314
|
+
// content.length terminating the loop; content falls through as #text
|
|
315
|
+
const result = makeParser().parse('<root><!no close tag</root>');
|
|
316
|
+
expect(result['#text']).toBe('<!no close tag');
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
it('skips child tag whose name starts with a digit (kills !\\[a-zA-Z_\\] continue branch)', () => {
|
|
320
|
+
// <1tag> - nextChar is '1', fails [a-zA-Z_] test; loop advances past it
|
|
321
|
+
// and the content falls through as #text
|
|
322
|
+
const result = makeParser().parse('<root><1tag>value</root>');
|
|
323
|
+
expect(result['#text']).toBe('<1tag>value');
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
it('does not parse element-like tokens inside XML comments', () => {
|
|
327
|
+
const result = makeParser().parse('<root><!-- <fake>x</fake> --><name>Alice</name></root>');
|
|
328
|
+
expect(result['name']).toBe('Alice');
|
|
329
|
+
expect(Object.keys(result)).toEqual(['name']);
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
it('does not parse element-like tokens inside processing instructions', () => {
|
|
333
|
+
const result = makeParser().parse('<root><?pi <data>x</data> ?><name>Bob</name></root>');
|
|
334
|
+
expect(result['name']).toBe('Bob');
|
|
335
|
+
expect(Object.keys(result)).toEqual(['name']);
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
it('ignores digit-started tag even when it has a matching close tag', () => {
|
|
339
|
+
const result = makeParser().parse('<root><1>v</1><name>w</name></root>');
|
|
340
|
+
expect(result['name']).toBe('w');
|
|
341
|
+
expect(Object.keys(result)).toEqual(['name']);
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
it('does not parse elements embedded inside a stray closing tag prefix', () => {
|
|
345
|
+
const result = makeParser().parse('<root></a<b>text</b><name>v</name></root>');
|
|
346
|
+
expect(result['name']).toBe('v');
|
|
347
|
+
expect(Object.keys(result)).toEqual(['name']);
|
|
348
|
+
});
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
describe(`${XmlParser.name} > linear scanner - unclosed and malformed tags`, () => {
|
|
352
|
+
it('skips an unclosed child and continues parsing siblings (kills innerEnd === -1 check mutant)', () => {
|
|
353
|
+
// <unclosed> has no </unclosed> - parser must skip it and continue
|
|
354
|
+
const result = makeParser().parse('<root><unclosed><name>Bob</name></root>');
|
|
355
|
+
expect(result['name']).toBe('Bob');
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
it('accepts closing tag at end of string with no trailing > (c === undefined branch)', () => {
|
|
359
|
+
// </a is the last token with no > - charAfter is undefined; the undefined
|
|
360
|
+
// branch must accept this as the close tag or the inner value is lost
|
|
361
|
+
const result = makeParser().parse('<root><a>1</a</root>');
|
|
362
|
+
expect(result['a']).toBe('1');
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
it('skips close-tag prefix that matches a longer tag name', () => {
|
|
366
|
+
// </a matches the prefix of </ab> - the char after </a is 'b', not
|
|
367
|
+
// a delimiter, so the scanner must skip it and keep looking for </a>
|
|
368
|
+
const result = makeParser().parse('<root><a><ab>inner</ab>rest</a></root>');
|
|
369
|
+
const a = result['a'] as Record<string, unknown>;
|
|
370
|
+
expect(a['ab']).toBe('inner');
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
it('handles a trailing bare < in inner content gracefully (nextChar === undefined break)', () => {
|
|
374
|
+
// Bare < at the very end of inner content - nextChar is undefined, the
|
|
375
|
+
// outer loop must terminate without crashing
|
|
376
|
+
const result = makeParser().parse('<root><name>Alice</name><</root>');
|
|
377
|
+
expect(result['name']).toBe('Alice');
|
|
378
|
+
});
|
|
379
|
+
});
|