fast-xml-validator 1.3.0 → 1.4.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.
@@ -0,0 +1,106 @@
1
+ 'use strict';
2
+
3
+ import ControlCharValidator from './ControlCharValidator.js';
4
+
5
+ /**
6
+ * CommentCdataReader — single responsibility: recognize and consume
7
+ * `<!-- comment -->`, `<![CDATA[ ... ]]>` and `<!DOCTYPE ...>` constructs
8
+ * starting at a given index (which points at the `!`), returning the index
9
+ * of the last character consumed (the closing `>`).
10
+ *
11
+ * DOCTYPE handling is fully delegated to a DocTypeValidator instance passed
12
+ * to `read()` by the caller (SyntaxValidator), since DOCTYPE parsing is a
13
+ * large, separate concern in its own right.
14
+ *
15
+ * Opt-in checks (via `invalidCharSequence`):
16
+ * - `comment: true` -> literal `--` inside a comment's body is rejected
17
+ * (besides the sequence immediately before the closing `-->`).
18
+ */
19
+ export default class CommentCdataReader {
20
+ constructor(options = {}) {
21
+ this.checkCommentDashes = options.invalidCharSequence?.comment === true;
22
+ }
23
+
24
+ /**
25
+ * @param {string} xmlData
26
+ * @param {number} i - index of the '!' character
27
+ * @param {import('./DocTypeValidator.js').default} docTypeValidator
28
+ * @returns {number} index of the last character consumed
29
+ */
30
+ read(xmlData, i, docTypeValidator) {
31
+ if (xmlData.length > i + 2 && xmlData[i + 1] === '-' && xmlData[i + 2] === '-') {
32
+ return this._readComment(xmlData, i);
33
+ } else if (
34
+ xmlData.length > i + 7 &&
35
+ xmlData[i + 1] === 'D' &&
36
+ xmlData[i + 2] === 'O' &&
37
+ xmlData[i + 3] === 'C' &&
38
+ xmlData[i + 4] === 'T' &&
39
+ xmlData[i + 5] === 'Y' &&
40
+ xmlData[i + 6] === 'P' &&
41
+ xmlData[i + 7] === 'E'
42
+ ) {
43
+ return docTypeValidator.validateDocType(xmlData, i - 1);
44
+ } else if (
45
+ xmlData.length > i + 8 &&
46
+ xmlData[i + 1] === '[' &&
47
+ xmlData[i + 2] === 'C' &&
48
+ xmlData[i + 3] === 'D' &&
49
+ xmlData[i + 4] === 'A' &&
50
+ xmlData[i + 5] === 'T' &&
51
+ xmlData[i + 6] === 'A' &&
52
+ xmlData[i + 7] === '['
53
+ ) {
54
+ return this._readCdata(xmlData, i);
55
+ } else {
56
+ throw new Error("Invalid construct starting with '<!'.");
57
+ }
58
+ }
59
+
60
+ _readComment(xmlData, i) {
61
+ const bodyStart = i + 3;
62
+ for (i = bodyStart; i < xmlData.length; i++) {
63
+ if (xmlData[i] === '-' && xmlData[i + 1] === '-' && xmlData[i + 2] === '>') {
64
+ i += 2;
65
+ break;
66
+ }
67
+ }
68
+ if (xmlData[i] !== '>') {
69
+ throw new Error('Comment is not closed with "-->".');
70
+ }
71
+
72
+ const body = xmlData.substring(bodyStart, i - 2);
73
+ const illegalIdx = ControlCharValidator.findIllegalIndex(body);
74
+ if (illegalIdx !== -1) {
75
+ const code = body.charCodeAt(illegalIdx);
76
+ throw new Error(`Illegal control character ${ControlCharValidator.describe(code)} in comment.`);
77
+ }
78
+ if (this.checkCommentDashes && body.indexOf('--') !== -1) {
79
+ throw new Error(`Comment must not contain '--'.`);
80
+ }
81
+
82
+ return i;
83
+ }
84
+
85
+ _readCdata(xmlData, i) {
86
+ const bodyStart = i + 8;
87
+ for (i = bodyStart; i < xmlData.length; i++) {
88
+ if (xmlData[i] === ']' && xmlData[i + 1] === ']' && xmlData[i + 2] === '>') {
89
+ i += 2;
90
+ break;
91
+ }
92
+ }
93
+ if (xmlData[i] !== '>') {
94
+ throw new Error('CDATA section is not closed with "]]>".');
95
+ }
96
+
97
+ const body = xmlData.substring(bodyStart, i - 2);
98
+ const illegalIdx = ControlCharValidator.findIllegalIndex(body);
99
+ if (illegalIdx !== -1) {
100
+ const code = body.charCodeAt(illegalIdx);
101
+ throw new Error(`Illegal control character ${ControlCharValidator.describe(code)} in CDATA section.`);
102
+ }
103
+
104
+ return i;
105
+ }
106
+ }
@@ -0,0 +1,44 @@
1
+ 'use strict';
2
+
3
+ import { isIllegalControlCode } from '../util.js';
4
+
5
+ /**
6
+ * ControlCharValidator — single responsibility: detect illegal control
7
+ * characters (0x00-0x08, 0x0B, 0x0C, 0x0E-0x1F) anywhere they appear as
8
+ * literal characters in the document (element text, attribute values,
9
+ * comments, CDATA, PI content, etc.).
10
+ *
11
+ * This check is always on — it is not gated by any option — since illegal
12
+ * control characters make a document non-well-formed per the XML spec.
13
+ */
14
+ export default class ControlCharValidator {
15
+ /**
16
+ * Scan `str` for the first illegal control character.
17
+ * @param {string} str
18
+ * @returns {number} local index (within `str`) of the first offending
19
+ * character, or -1 if none found.
20
+ */
21
+ static findIllegalIndex(str) {
22
+ const len = str.length;
23
+ for (let i = 0; i < len; i++) {
24
+ if (isIllegalControlCode(str.charCodeAt(i))) return i;
25
+ }
26
+ return -1;
27
+ }
28
+
29
+ /**
30
+ * @param {string} str
31
+ * @returns {boolean}
32
+ */
33
+ static hasIllegalChar(str) {
34
+ return this.findIllegalIndex(str) !== -1;
35
+ }
36
+
37
+ /**
38
+ * Convenience formatter for error messages.
39
+ * @param {number} code - char code
40
+ */
41
+ static describe(code) {
42
+ return `0x${code.toString(16).padStart(2, '0')}`;
43
+ }
44
+ }
@@ -0,0 +1,280 @@
1
+ import { name as isXmlName } from 'xml-naming';
2
+
3
+ const defaultOptions = {
4
+ maxEntityCount: Infinity,
5
+ maxEntitySize: Infinity,
6
+ };
7
+
8
+ export default class DocTypeValidator {
9
+ constructor(options, xmlVersion = '1.0') {
10
+ this.options = Object.assign({}, defaultOptions, options);
11
+ this.suppressValidationErr = !options;
12
+ this.xmlVersion = xmlVersion;
13
+ }
14
+
15
+ validateDocType(xmlData, i) {
16
+ if (
17
+ xmlData[i + 3] !== 'O' ||
18
+ xmlData[i + 4] !== 'C' ||
19
+ xmlData[i + 5] !== 'T' ||
20
+ xmlData[i + 6] !== 'Y' ||
21
+ xmlData[i + 7] !== 'P' ||
22
+ xmlData[i + 8] !== 'E'
23
+ ) {
24
+ throw new Error(`Invalid Tag instead of DOCTYPE`);
25
+ }
26
+
27
+ i += 9;
28
+ let angleBracketsCount = 1;
29
+ let hasBody = false;
30
+ let comment = false;
31
+ let entityCount = 0;
32
+
33
+ for (; i < xmlData.length; i++) {
34
+ if (xmlData[i] === '<' && !comment) {
35
+ if (hasBody && hasSeq(xmlData, '!ENTITY', i)) {
36
+ i += 7;
37
+ [entityCount, i] = this._validateEntityExp(xmlData, i + 1, entityCount);
38
+ // continue;
39
+ } else if (hasBody && hasSeq(xmlData, '!ELEMENT', i)) {
40
+ i += 8;
41
+ i = this._validateElementExp(xmlData, i + 1);
42
+ // continue;
43
+ } else if (hasBody && hasSeq(xmlData, '!ATTLIST', i)) {
44
+ i += 8;
45
+ i = this._validateAttlistExp(xmlData, i + 1);
46
+ continue;
47
+ } else if (hasBody && hasSeq(xmlData, '!NOTATION', i)) {
48
+ i += 9;
49
+ i = this._validateNotationExp(xmlData, i + 1);
50
+ // continue;
51
+ } else if (hasSeq(xmlData, '!--', i)) {
52
+ comment = true;
53
+ } else {
54
+ throw new Error(`Invalid DOCTYPE`);
55
+ }
56
+ angleBracketsCount++;
57
+ } else if (xmlData[i] === '>') {
58
+ if (comment) {
59
+ if (xmlData[i - 1] === '-' && xmlData[i - 2] === '-') {
60
+ comment = false;
61
+ angleBracketsCount--;
62
+ }
63
+ } else {
64
+ angleBracketsCount--;
65
+ }
66
+ if (angleBracketsCount === 0) break;
67
+ } else if (xmlData[i] === '[') {
68
+ hasBody = true;
69
+ }
70
+ }
71
+
72
+ if (angleBracketsCount !== 0) {
73
+ throw new Error(`Unclosed DOCTYPE`);
74
+ }
75
+
76
+ return i;
77
+ }
78
+
79
+ _validateEntityExp(xmlData, i, entityCount) {
80
+ i = skipWhitespace(xmlData, i);
81
+
82
+ // Read and validate entity name
83
+ const nameStart = i;
84
+ while (i < xmlData.length && !/\s/.test(xmlData[i]) && xmlData[i] !== '"' && xmlData[i] !== "'") {
85
+ i++;
86
+ }
87
+ const entityName = xmlData.substring(nameStart, i);
88
+ validateName(entityName, this.xmlVersion);
89
+
90
+ i = skipWhitespace(xmlData, i);
91
+
92
+ // Check for unsupported constructs
93
+ if (!this.suppressValidationErr) {
94
+ if (xmlData.substring(i, i + 6).toUpperCase() === 'SYSTEM') {
95
+ throw new Error('External entities are not supported');
96
+ } else if (xmlData[i] === '%') {
97
+ throw new Error('Parameter entities are not supported');
98
+ }
99
+ }
100
+
101
+ // Read and validate the entity value (validates quoting + size limit)
102
+ let entityValue;
103
+ [i, entityValue] = readQuotedValue(xmlData, i, 'entity');
104
+
105
+ const { maxEntitySize, maxEntityCount } = this.options;
106
+
107
+ if (entityValue.length > maxEntitySize) {
108
+ throw new Error(
109
+ `Entity "${entityName}" size (${entityValue.length}) exceeds maximum allowed size (${maxEntitySize})`
110
+ );
111
+ }
112
+
113
+ // Only count non-parameter entities (parameter entities contain '&')
114
+ if (entityValue.indexOf('&') === -1) {
115
+ entityCount++;
116
+ if (entityCount > maxEntityCount) {
117
+ throw new Error(
118
+ `Entity count (${entityCount}) exceeds maximum allowed (${maxEntityCount})`
119
+ );
120
+ }
121
+ }
122
+
123
+ return [entityCount, i - 1];
124
+ }
125
+
126
+ _validateNotationExp(xmlData, i) {
127
+ i = skipWhitespace(xmlData, i);
128
+
129
+ // Read and validate notation name
130
+ const nameStart = i;
131
+ while (i < xmlData.length && !/\s/.test(xmlData[i])) i++;
132
+ const notationName = xmlData.substring(nameStart, i);
133
+ !this.suppressValidationErr && validateName(notationName, this.xmlVersion);
134
+
135
+ i = skipWhitespace(xmlData, i);
136
+
137
+ const identifierType = xmlData.substring(i, i + 6).toUpperCase();
138
+ if (!this.suppressValidationErr && identifierType !== 'SYSTEM' && identifierType !== 'PUBLIC') {
139
+ throw new Error(`Expected SYSTEM or PUBLIC, found "${identifierType}"`);
140
+ }
141
+ i += identifierType.length;
142
+ i = skipWhitespace(xmlData, i);
143
+
144
+ if (identifierType === 'PUBLIC') {
145
+ [i] = readQuotedValue(xmlData, i, 'publicIdentifier');
146
+ i = skipWhitespace(xmlData, i);
147
+ if (xmlData[i] === '"' || xmlData[i] === "'") {
148
+ [i] = readQuotedValue(xmlData, i, 'systemIdentifier');
149
+ }
150
+ } else if (identifierType === 'SYSTEM') {
151
+ [i] = readQuotedValue(xmlData, i, 'systemIdentifier');
152
+ }
153
+
154
+ return --i;
155
+ }
156
+
157
+ _validateElementExp(xmlData, i) {
158
+ i = skipWhitespace(xmlData, i);
159
+
160
+ // Read and validate element name
161
+ const nameStart = i;
162
+ while (i < xmlData.length && !/\s/.test(xmlData[i])) i++;
163
+ const elementName = xmlData.substring(nameStart, i);
164
+ if (!this.suppressValidationErr && !isXmlName(elementName, { xmlVersion: this.xmlVersion })) {
165
+ throw new Error(`Invalid element name: "${elementName}"`);
166
+ }
167
+
168
+ i = skipWhitespace(xmlData, i);
169
+
170
+ // Validate content model keyword or group
171
+ if (xmlData[i] === 'E' && hasSeq(xmlData, 'MPTY', i)) {
172
+ i += 4;
173
+ } else if (xmlData[i] === 'A' && hasSeq(xmlData, 'NY', i)) {
174
+ i += 2;
175
+ } else if (xmlData[i] === '(') {
176
+ i++;
177
+ while (i < xmlData.length && xmlData[i] !== ')') i++;
178
+ if (xmlData[i] !== ')') throw new Error('Unterminated content model');
179
+ } else if (!this.suppressValidationErr) {
180
+ throw new Error(`Invalid Element Expression, found "${xmlData[i]}"`);
181
+ }
182
+
183
+ return i;
184
+ }
185
+
186
+ _validateAttlistExp(xmlData, i) {
187
+ i = skipWhitespace(xmlData, i);
188
+
189
+ // Read element name
190
+ const elemNameStart = i;
191
+ while (i < xmlData.length && !/\s/.test(xmlData[i])) i++;
192
+ const elementName = xmlData.substring(elemNameStart, i);
193
+ validateName(elementName, this.xmlVersion);
194
+
195
+ i = skipWhitespace(xmlData, i);
196
+
197
+ // Read attribute name
198
+ const attrNameStart = i;
199
+ while (i < xmlData.length && !/\s/.test(xmlData[i])) i++;
200
+ const attributeName = xmlData.substring(attrNameStart, i);
201
+ validateName(attributeName, this.xmlVersion);
202
+
203
+ i = skipWhitespace(xmlData, i);
204
+
205
+ // Read attribute type
206
+ if (xmlData.substring(i, i + 8).toUpperCase() === 'NOTATION') {
207
+ i += 8;
208
+ i = skipWhitespace(xmlData, i);
209
+ if (xmlData[i] !== '(') throw new Error(`Expected '(', found "${xmlData[i]}"`);
210
+ i++;
211
+ while (i < xmlData.length && xmlData[i] !== ')') {
212
+ const notationStart = i;
213
+ while (i < xmlData.length && xmlData[i] !== '|' && xmlData[i] !== ')') i++;
214
+ const notation = xmlData.substring(notationStart, i).trim();
215
+ validateName(notation, this.xmlVersion);
216
+ if (xmlData[i] === '|') {
217
+ i++;
218
+ i = skipWhitespace(xmlData, i);
219
+ }
220
+ }
221
+ if (xmlData[i] !== ')') throw new Error('Unterminated list of notations');
222
+ i++;
223
+ } else {
224
+ const typeStart = i;
225
+ while (i < xmlData.length && !/\s/.test(xmlData[i])) i++;
226
+ const attributeType = xmlData.substring(typeStart, i);
227
+ const validTypes = ['CDATA', 'ID', 'IDREF', 'IDREFS', 'ENTITY', 'ENTITIES', 'NMTOKEN', 'NMTOKENS'];
228
+ if (!this.suppressValidationErr && !validTypes.includes(attributeType.toUpperCase())) {
229
+ throw new Error(`Invalid attribute type: "${attributeType}"`);
230
+ }
231
+ }
232
+
233
+ i = skipWhitespace(xmlData, i);
234
+
235
+ // Read default value declaration
236
+ if (xmlData.substring(i, i + 9).toUpperCase() === '#REQUIRED') {
237
+ i += 9;
238
+ } else if (xmlData.substring(i, i + 8).toUpperCase() === '#IMPLIED') {
239
+ i += 8;
240
+ } else {
241
+ [i] = readQuotedValue(xmlData, i, 'ATTLIST default value');
242
+ }
243
+
244
+ return i;
245
+ }
246
+ }
247
+
248
+ // ─── Helpers ────────────────────────────────────────────────────────────────
249
+
250
+ function skipWhitespace(data, index) {
251
+ while (index < data.length && /\s/.test(data[index])) index++;
252
+ return index;
253
+ }
254
+
255
+ function hasSeq(data, seq, i) {
256
+ for (let j = 0; j < seq.length; j++) {
257
+ if (seq[j] !== data[i + j + 1]) return false;
258
+ }
259
+ return true;
260
+ }
261
+
262
+ function validateName(name, xmlVersion = '1.0') {
263
+ if (!isXmlName(name, { xmlVersion })) throw new Error(`Invalid entity name "${name}"`);
264
+ }
265
+
266
+ /**
267
+ * Read a single- or double-quoted string and return [nextIndex, value].
268
+ */
269
+ function readQuotedValue(xmlData, i, type) {
270
+ const startChar = xmlData[i];
271
+ if (startChar !== '"' && startChar !== "'") {
272
+ throw new Error(`Expected quoted string for ${type}, found "${startChar}"`);
273
+ }
274
+ i++;
275
+ const start = i;
276
+ while (i < xmlData.length && xmlData[i] !== startChar) i++;
277
+ if (xmlData[i] !== startChar) throw new Error(`Unterminated ${type} value`);
278
+ const value = xmlData.substring(start, i);
279
+ return [++i, value];
280
+ }
@@ -0,0 +1,47 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * RawSubtreeReader — single responsibility: given the index right after a
5
+ * skipped tag's opening `>`, consume raw characters until the literal
6
+ * closing tag `</tagName>` (optional whitespace before `>`) is found.
7
+ *
8
+ * Deliberately does NOT try to parse anything in between: skipped content is
9
+ * assumed to be non-XML or otherwise not meant to be validated (script/style
10
+ * bodies, vendor blobs, etc.), so it may contain unmatched `<`, quotes, or
11
+ * anything else. This mirrors how CDATA content is treated — opaque text,
12
+ * found by looking for its one true terminator.
13
+ *
14
+ * Same-name tags never "nest" under this strategy — the first `</tagName>`
15
+ * found ends the skip, matching the common script/style use case. Use a more
16
+ * specific skip expression (e.g. targeting only the outermost occurrence) if
17
+ * your skipped content can itself contain literal `</tagName>` text.
18
+ */
19
+ export default class RawSubtreeReader {
20
+ /**
21
+ * @param {string} xmlData
22
+ * @param {string} tagName
23
+ * @param {number} i - index right after the opening tag's '>'
24
+ * @returns {number} index of the matching closing tag's own '>'
25
+ */
26
+ static consume(xmlData, tagName, i) {
27
+ const needle = '</' + tagName;
28
+ let searchFrom = i;
29
+
30
+ while (true) {
31
+ const idx = xmlData.indexOf(needle, searchFrom);
32
+ if (idx === -1) {
33
+ throw new Error(`Unclosed skipped tag '${tagName}'.`);
34
+ }
35
+
36
+ let j = idx + needle.length;
37
+ while (j < xmlData.length && /\s/.test(xmlData[j])) j++;
38
+
39
+ if (xmlData[j] === '>') {
40
+ return j;
41
+ }
42
+ // Not actually a match (e.g. tagName is a prefix of a longer name) —
43
+ // keep looking further in the string.
44
+ searchFrom = idx + needle.length;
45
+ }
46
+ }
47
+ }
@@ -0,0 +1,54 @@
1
+ 'use strict';
2
+
3
+ import { Expression, Matcher } from 'path-expression-matcher';
4
+
5
+ /**
6
+ * SkipTagMatcher — single responsibility: decide whether a tag currently
7
+ * being opened should be excluded from validation, based on a list of path
8
+ * expressions supplied via `options.skipTags` (same expression syntax as
9
+ * `path-expression-matcher`, e.g. `"..script"`, `"root.data.raw"`,
10
+ * `"row[type=raw]"`).
11
+ *
12
+ * Skip decisions are only ever made for tags reached through normal
13
+ * (non-skipped) parsing — once a match is found, the caller
14
+ * (`SyntaxValidator`) consumes the whole matched subtree as opaque raw text
15
+ * (mirroring how CDATA is treated) and calls `pop()` once for that tag, so
16
+ * this matcher's path stack never needs to represent the skipped subtree's
17
+ * internal structure.
18
+ */
19
+ export default class SkipTagMatcher {
20
+ /**
21
+ * @param {Array<string>} [skipTags]
22
+ */
23
+ constructor(skipTags = []) {
24
+ this._expressions = skipTags.map((expr) => new Expression(expr));
25
+ this._matcher = new Matcher();
26
+ this._enabled = this._expressions.length > 0;
27
+ }
28
+
29
+ get enabled() {
30
+ return this._enabled;
31
+ }
32
+
33
+ /**
34
+ * Push the tag currently being opened onto the path and report whether it
35
+ * (and its whole subtree) should be skipped.
36
+ * @param {string} tagName
37
+ * @param {Record<string,string>} [rawAttributes] - for `[attr=value]` conditions
38
+ * @returns {boolean}
39
+ */
40
+ push(tagName, rawAttributes) {
41
+ if (!this._enabled) return false;
42
+ this._matcher.push(tagName, rawAttributes || null);
43
+ for (let i = 0; i < this._expressions.length; i++) {
44
+ if (this._matcher.matches(this._expressions[i])) return true;
45
+ }
46
+ return false;
47
+ }
48
+
49
+ /** Pop the most recently pushed tag off the path (tag fully closed). */
50
+ pop() {
51
+ if (!this._enabled) return;
52
+ this._matcher.pop();
53
+ }
54
+ }
@@ -0,0 +1,103 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * XmlDeclarationValidator — single responsibility: parse and validate the
5
+ * `<?xml version="..." encoding="..." standalone="..."?>` prolog.
6
+ *
7
+ * Behaviour is controlled by the `xmlDeclaraion` config block:
8
+ *
9
+ * xmlDeclaraion: {
10
+ * optional: true, // when false, the document MUST start with a decl
11
+ * argPosition: true, // when true, version/encoding/standalone must
12
+ * // appear in that fixed order (spec-mandated);
13
+ * // set false to allow any order.
14
+ * }
15
+ *
16
+ * (`argPostion` — the spelling used in the original design note — is also
17
+ * accepted as an alias for `argPosition`.)
18
+ */
19
+ export default class XmlDeclarationValidator {
20
+ constructor(options = {}, throwError) {
21
+ this.optional = options.optional !== undefined ? options.optional : true;
22
+ this.argPosition = options.argPosition !== undefined
23
+ ? options.argPosition
24
+ : (options.argPostion !== undefined ? options.argPostion : true);
25
+ this._throwError = throwError;
26
+ }
27
+
28
+ /**
29
+ * @param {string} xmlData - full document (BOM already stripped)
30
+ * @returns {{ version: string, end: number }} version defaults to '1.0'
31
+ * and end is 0 when there is no declaration present.
32
+ */
33
+ validate(xmlData) {
34
+ const hasDecl = xmlData.startsWith('<?xml');
35
+
36
+ if (!hasDecl) {
37
+ if (!this.optional) {
38
+ this._throwError('InvalidXml', 'XML declaration is required but missing.', { line: 1, col: 1 });
39
+ }
40
+ return { version: '1.0', end: 0 };
41
+ }
42
+
43
+ const piEnd = xmlData.indexOf('?>');
44
+ if (piEnd === -1) {
45
+ this._throwError('InvalidXml', 'XML declaration is not closed with "?>".', { line: 1, col: 1 });
46
+ }
47
+
48
+ const declBody = xmlData.substring(5, piEnd);
49
+ const end = piEnd + 2;
50
+
51
+ const ALLOWED = ['version', 'encoding', 'standalone'];
52
+ const attrRe = /\s+([\w]+)\s*=\s*(?:"([^"]*)"|'([^']*)')/g;
53
+
54
+ let version = '1.0';
55
+ let lastAllowedIdx = -1;
56
+ let match;
57
+
58
+ while ((match = attrRe.exec(declBody)) !== null) {
59
+ const attrName = match[1];
60
+ const attrValue = match[2] !== undefined ? match[2] : match[3];
61
+ const attrPos = match.index;
62
+
63
+ const allowedIdx = ALLOWED.indexOf(attrName);
64
+ if (allowedIdx === -1) {
65
+ this._throwError('InvalidXml',
66
+ 'XML declaration contains unknown attribute "' + attrName + '". Allowed: version, encoding, standalone.',
67
+ { line: 1, col: 6 + attrPos });
68
+ }
69
+
70
+ if (this.argPosition && allowedIdx <= lastAllowedIdx) {
71
+ this._throwError('InvalidXml',
72
+ 'XML declaration attribute "' + attrName + '" is out of order. Required order: version, encoding, standalone.',
73
+ { line: 1, col: 6 + attrPos });
74
+ }
75
+ lastAllowedIdx = allowedIdx;
76
+
77
+ if (attrName === 'version') {
78
+ if (attrValue !== '1.0' && attrValue !== '1.1') {
79
+ this._throwError('InvalidXml',
80
+ 'XML declaration version "' + attrValue + '" is not supported. Must be "1.0" or "1.1".',
81
+ { line: 1, col: 6 + attrPos });
82
+ }
83
+ version = attrValue;
84
+ } else if (attrName === 'standalone') {
85
+ if (attrValue !== 'yes' && attrValue !== 'no') {
86
+ this._throwError('InvalidXml',
87
+ 'XML declaration standalone "' + attrValue + '" is invalid. Must be "yes" or "no".',
88
+ { line: 1, col: 6 + attrPos });
89
+ }
90
+ }
91
+ // encoding: value format validation is out of scope
92
+ }
93
+
94
+ const leftover = declBody.replace(/\s+[\w]+\s*=\s*(?:"[^"]*"|'[^']*')/g, '').trim();
95
+ if (leftover.length > 0) {
96
+ this._throwError('InvalidXml',
97
+ 'XML declaration contains invalid content: "' + leftover + '".',
98
+ { line: 1, col: 1 });
99
+ }
100
+
101
+ return { version, end };
102
+ }
103
+ }
@@ -0,0 +1,24 @@
1
+ 'use strict';
2
+
3
+ import { createValidator } from 'xml-naming';
4
+
5
+ /**
6
+ * XmlNameValidator — single responsibility: expose XML Name / QName
7
+ * validators for a given xmlVersion, memoized per instance since element
8
+ * and attribute names repeat heavily across a document.
9
+ */
10
+ export default class XmlNameValidator {
11
+ constructor(xmlVersion = '1.0') {
12
+ this.xmlVersion = xmlVersion;
13
+ this.qName = createValidator('qName', { xmlVersion });
14
+ this.name = createValidator('name', { xmlVersion });
15
+ }
16
+
17
+ isValidQName(value) {
18
+ return this.qName(value);
19
+ }
20
+
21
+ isValidName(value) {
22
+ return this.name(value);
23
+ }
24
+ }