fast-xml-validator 1.0.0 → 1.1.0

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fast-xml-validator",
3
- "version": "1.0.0",
3
+ "version": "1.1.0",
4
4
  "description": "Validate XML for Syntax and Business rules without C/C++ based libraries",
5
5
  "main": "./lib/fxv.cjs",
6
6
  "type": "module",
@@ -65,6 +65,7 @@
65
65
  }
66
66
  ],
67
67
  "dependencies": {
68
- "detailed-xml-validator": "^2.1.0"
68
+ "detailed-xml-validator": "^2.1.0",
69
+ "xml-naming": "^0.1.0"
69
70
  }
70
71
  }
@@ -1,4 +1,4 @@
1
- import { isName } from './util.js';
1
+ import { name as isXmlName } from 'xml-naming';
2
2
 
3
3
  const defaultOptions = {
4
4
  maxEntityCount: Infinity,
@@ -6,9 +6,10 @@ const defaultOptions = {
6
6
  };
7
7
 
8
8
  export default class DocTypeValidator {
9
- constructor(options) {
9
+ constructor(options, xmlVersion = '1.0') {
10
10
  this.options = Object.assign({}, defaultOptions, options);
11
11
  this.suppressValidationErr = !options;
12
+ this.xmlVersion = xmlVersion;
12
13
  }
13
14
 
14
15
  validateDocType(xmlData, i) {
@@ -84,7 +85,7 @@ export default class DocTypeValidator {
84
85
  i++;
85
86
  }
86
87
  const entityName = xmlData.substring(nameStart, i);
87
- validateName(entityName);
88
+ validateName(entityName, this.xmlVersion);
88
89
 
89
90
  i = skipWhitespace(xmlData, i);
90
91
 
@@ -129,7 +130,7 @@ export default class DocTypeValidator {
129
130
  const nameStart = i;
130
131
  while (i < xmlData.length && !/\s/.test(xmlData[i])) i++;
131
132
  const notationName = xmlData.substring(nameStart, i);
132
- !this.suppressValidationErr && validateName(notationName);
133
+ !this.suppressValidationErr && validateName(notationName, this.xmlVersion);
133
134
 
134
135
  i = skipWhitespace(xmlData, i);
135
136
 
@@ -160,7 +161,7 @@ export default class DocTypeValidator {
160
161
  const nameStart = i;
161
162
  while (i < xmlData.length && !/\s/.test(xmlData[i])) i++;
162
163
  const elementName = xmlData.substring(nameStart, i);
163
- if (!this.suppressValidationErr && !isName(elementName)) {
164
+ if (!this.suppressValidationErr && !isXmlName(elementName, { xmlVersion: this.xmlVersion })) {
164
165
  throw new Error(`Invalid element name: "${elementName}"`);
165
166
  }
166
167
 
@@ -189,7 +190,7 @@ export default class DocTypeValidator {
189
190
  const elemNameStart = i;
190
191
  while (i < xmlData.length && !/\s/.test(xmlData[i])) i++;
191
192
  const elementName = xmlData.substring(elemNameStart, i);
192
- validateName(elementName);
193
+ validateName(elementName, this.xmlVersion);
193
194
 
194
195
  i = skipWhitespace(xmlData, i);
195
196
 
@@ -197,7 +198,7 @@ export default class DocTypeValidator {
197
198
  const attrNameStart = i;
198
199
  while (i < xmlData.length && !/\s/.test(xmlData[i])) i++;
199
200
  const attributeName = xmlData.substring(attrNameStart, i);
200
- validateName(attributeName);
201
+ validateName(attributeName, this.xmlVersion);
201
202
 
202
203
  i = skipWhitespace(xmlData, i);
203
204
 
@@ -211,7 +212,7 @@ export default class DocTypeValidator {
211
212
  const notationStart = i;
212
213
  while (i < xmlData.length && xmlData[i] !== '|' && xmlData[i] !== ')') i++;
213
214
  const notation = xmlData.substring(notationStart, i).trim();
214
- validateName(notation);
215
+ validateName(notation, this.xmlVersion);
215
216
  if (xmlData[i] === '|') {
216
217
  i++;
217
218
  i = skipWhitespace(xmlData, i);
@@ -258,8 +259,8 @@ function hasSeq(data, seq, i) {
258
259
  return true;
259
260
  }
260
261
 
261
- function validateName(name) {
262
- if (!isName(name)) throw new Error(`Invalid entity name "${name}"`);
262
+ function validateName(name, xmlVersion = '1.0') {
263
+ if (!isXmlName(name, { xmlVersion })) throw new Error(`Invalid entity name "${name}"`);
263
264
  }
264
265
 
265
266
  /**
package/src/util.js CHANGED
@@ -1,10 +1,5 @@
1
1
  'use strict';
2
2
 
3
- const nameStartChar = ':A-Za-z_\\u00C0-\\u00D6\\u00D8-\\u00F6\\u00F8-\\u02FF\\u0370-\\u037D\\u037F-\\u1FFF\\u200C-\\u200D\\u2070-\\u218F\\u2C00-\\u2FEF\\u3001-\\uD7FF\\uF900-\\uFDCF\\uFDF0-\\uFFFD';
4
- const nameChar = nameStartChar + '\\-.\\d\\u00B7\\u0300-\\u036F\\u203F-\\u2040';
5
- export const nameRegexp = '[' + nameStartChar + '][' + nameChar + ']*';
6
- const regexName = new RegExp('^' + nameRegexp + '$');
7
-
8
3
  export function getAllMatches(string, regex) {
9
4
  const matches = [];
10
5
  let match = regex.exec(string);
@@ -21,11 +16,6 @@ export function getAllMatches(string, regex) {
21
16
  return matches;
22
17
  }
23
18
 
24
- export const isName = function (string) {
25
- const match = regexName.exec(string);
26
- return !(match === null || typeof match === 'undefined');
27
- }
28
-
29
19
  export function isExist(v) {
30
20
  return typeof v !== 'undefined';
31
21
  }
package/src/validator.js CHANGED
@@ -1,6 +1,7 @@
1
1
  'use strict';
2
2
 
3
- import { getAllMatches, isName } from './util.js';
3
+ import { getAllMatches } from './util.js';
4
+ import { name as xmlName, qName } from 'xml-naming';
4
5
  import DocTypeValidator from './DocTypeValidator.js';
5
6
  import ValidationError from './ValidationError.js';
6
7
 
@@ -17,8 +18,6 @@ const defaultOptions = {
17
18
  export function validate(xmlData, options) {
18
19
  options = Object.assign({}, defaultOptions, options);
19
20
 
20
- const docTypeValidator = new DocTypeValidator(options.docType);
21
-
22
21
  const tags = [];
23
22
  let tagFound = false;
24
23
 
@@ -30,19 +29,20 @@ export function validate(xmlData, options) {
30
29
  xmlData = xmlData.substr(1);
31
30
  }
32
31
 
32
+ let xmlVersion = '1.0'; // default per spec
33
33
  if (xmlData.startsWith('<?xml')) {
34
- const piEnd = xmlData.indexOf('?>');
35
- if (piEnd === -1) {
36
- return throwError('InvalidXml', 'Processing instruction is not closed with "?>".',
37
- { line: 1, col: 1 });
38
- }
39
- xmlData = xmlData.substring(piEnd + 2);
34
+ const declResult = parseXmlDeclaration(xmlData);
35
+ if (declResult.err) return declResult;
36
+ xmlVersion = declResult.version;
37
+ xmlData = xmlData.substring(declResult.end);
40
38
  }
41
39
 
40
+ const docTypeValidator = new DocTypeValidator(options.docType, xmlVersion);
41
+
42
42
  for (let i = 0; i < xmlData.length; i++) {
43
43
  if (xmlData[i] === '<' && xmlData[i + 1] === '?') {
44
44
  i += 2;
45
- i = readPI(xmlData, i);
45
+ i = readPI(xmlData, i, xmlVersion);
46
46
  if (i.err) return i;
47
47
  } else if (xmlData[i] === '<') {
48
48
  //starting of tag
@@ -54,7 +54,7 @@ export function validate(xmlData, options) {
54
54
  try {
55
55
  i = readCommentAndCDATA(xmlData, i, docTypeValidator);
56
56
  } catch (err) {
57
- return throwError("InvalidTag", err.message, getLineNumberForPosition(xmlData, i - 1));
57
+ throwError("InvalidTag", err.message, getLineNumberForPosition(xmlData, i - 1));
58
58
  }
59
59
  continue;
60
60
  } else {
@@ -93,12 +93,12 @@ export function validate(xmlData, options) {
93
93
  } else {
94
94
  msg = "Tag '" + tagName + "' is an invalid name.";
95
95
  }
96
- return throwError('InvalidTag', msg, getLineNumberForPosition(xmlData, tagStartPos));
96
+ throwError('InvalidTag', msg, getLineNumberForPosition(xmlData, tagStartPos));
97
97
  }
98
98
 
99
99
  const result = readAttributeStr(xmlData, i);
100
100
  if (result === false) {
101
- return throwError('InvalidAttr', "Attributes for '" + tagName + "' have open quote.", getLineNumberForPosition(xmlData, i));
101
+ throwError('InvalidAttr', "Attributes for '" + tagName + "' have open quote.", getLineNumberForPosition(xmlData, i));
102
102
  }
103
103
  let attrStr = result.value;
104
104
  i = result.index;
@@ -107,10 +107,10 @@ export function validate(xmlData, options) {
107
107
  //self closing tag
108
108
  const attrStrStart = i - attrStr.length;
109
109
  attrStr = attrStr.substring(0, attrStr.length - 1);
110
- const isValid = validateAttributeString(attrStr, options);
110
+ const isValid = validateAttributeString(attrStr, options, xmlVersion);
111
111
  if (isValid === true) {
112
112
  if (reachedRoot === true) {
113
- return throwError('InvalidXml', 'Multiple possible root nodes found.', getLineNumberForPosition(xmlData, tagStartPos));
113
+ throwError('InvalidXml', 'Multiple possible root nodes found.', getLineNumberForPosition(xmlData, tagStartPos));
114
114
  }
115
115
  tagFound = true;
116
116
  //if no open tags remain on the stack, this self-closing tag is the root
@@ -121,20 +121,20 @@ export function validate(xmlData, options) {
121
121
  //the result from the nested function returns the position of the error within the attribute
122
122
  //in order to get the 'true' error line, we need to calculate the position where the attribute begins (i - attrStr.length) and then add the position within the attribute
123
123
  //this gives us the absolute index in the entire xml, which we can use to find the line at last
124
- return throwError(isValid.err.code, isValid.err.msg, getLineNumberForPosition(xmlData, attrStrStart + isValid.err.line));
124
+ throwError(isValid.err.code, isValid.err.msg, getLineNumberForPosition(xmlData, attrStrStart + isValid.err.line));
125
125
  }
126
126
  } else if (closingTag) {
127
127
  if (!result.tagClosed) {
128
- return throwError('InvalidTag', "Closing tag '" + tagName + "' doesn't have proper closing.", getLineNumberForPosition(xmlData, i));
128
+ throwError('InvalidTag', "Closing tag '" + tagName + "' doesn't have proper closing.", getLineNumberForPosition(xmlData, i));
129
129
  } else if (attrStr.trim().length > 0) {
130
- return throwError('InvalidTag', "Closing tag '" + tagName + "' can't have attributes or invalid starting.", getLineNumberForPosition(xmlData, tagStartPos));
130
+ throwError('InvalidTag', "Closing tag '" + tagName + "' can't have attributes or invalid starting.", getLineNumberForPosition(xmlData, tagStartPos));
131
131
  } else if (tags.length === 0) {
132
- return throwError('InvalidTag', "Closing tag '" + tagName + "' has not been opened.", getLineNumberForPosition(xmlData, tagStartPos));
132
+ throwError('InvalidTag', "Closing tag '" + tagName + "' has not been opened.", getLineNumberForPosition(xmlData, tagStartPos));
133
133
  } else {
134
134
  const otg = tags.pop();
135
135
  if (tagName !== otg.tagName) {
136
136
  let openPos = getLineNumberForPosition(xmlData, otg.tagStartPos);
137
- return throwError(
137
+ throwError(
138
138
  'InvalidTag',
139
139
  "Expected closing tag '" + otg.tagName + "' (opened in line " + openPos.line + ", col " + openPos.col + ") instead of closing tag '" + tagName + "'.",
140
140
  getLineNumberForPosition(xmlData, tagStartPos)
@@ -147,13 +147,13 @@ export function validate(xmlData, options) {
147
147
  }
148
148
  }
149
149
  } else {
150
- const isValid = validateAttributeString(attrStr, options);
150
+ const isValid = validateAttributeString(attrStr, options, xmlVersion);
151
151
  if (isValid !== true) {
152
- return throwError(isValid.err.code, isValid.err.msg, getLineNumberForPosition(xmlData, i - attrStr.length + isValid.err.line));
152
+ throwError(isValid.err.code, isValid.err.msg, getLineNumberForPosition(xmlData, i - attrStr.length + isValid.err.line));
153
153
  }
154
154
 
155
155
  if (reachedRoot === true) {
156
- return throwError('InvalidXml', 'Multiple possible root nodes found.', getLineNumberForPosition(xmlData, i));
156
+ throwError('InvalidXml', 'Multiple possible root nodes found.', getLineNumberForPosition(xmlData, i));
157
157
  } else if (options.unpairedTags.indexOf(tagName) !== -1) {
158
158
  // don't push into stack
159
159
  } else {
@@ -172,12 +172,12 @@ export function validate(xmlData, options) {
172
172
  try {
173
173
  i = readCommentAndCDATA(xmlData, i, docTypeValidator);
174
174
  } catch (err) {
175
- return throwError("InvalidDocType", err.message, getLineNumberForPosition(xmlData, i - 1));
175
+ throwError("InvalidDocType", err.message, getLineNumberForPosition(xmlData, i - 1));
176
176
  }
177
177
  continue;
178
178
  } else if (xmlData[i + 1] === '?') {
179
179
  i += 2;
180
- i = readPI(xmlData, i);
180
+ i = readPI(xmlData, i, xmlVersion);
181
181
  if (i.err) return i;
182
182
  } else {
183
183
  break;
@@ -185,11 +185,11 @@ export function validate(xmlData, options) {
185
185
  } else if (xmlData[i] === '&') {
186
186
  const afterAmp = validateAmpersand(xmlData, i);
187
187
  if (afterAmp === -1)
188
- return throwError('InvalidChar', "char '&' is not expected.", getLineNumberForPosition(xmlData, i));
188
+ throwError('InvalidChar', "char '&' is not expected.", getLineNumberForPosition(xmlData, i));
189
189
  i = afterAmp;
190
190
  } else {
191
191
  if (reachedRoot === true && !isWhiteSpace(xmlData[i])) {
192
- return throwError('InvalidXml', 'Extra text at the end', getLineNumberForPosition(xmlData, i));
192
+ throwError('InvalidXml', 'Extra text at the end', getLineNumberForPosition(xmlData, i));
193
193
  }
194
194
  }
195
195
  } //end of reading tag text value
@@ -202,18 +202,18 @@ export function validate(xmlData, options) {
202
202
  continue;
203
203
  }
204
204
  if (reachedRoot) {
205
- return throwError('InvalidXml', "Extra text at the end: '" + xmlData[i] + "' is not expected.", getLineNumberForPosition(xmlData, i));
205
+ throwError('InvalidXml', "Extra text at the end: '" + xmlData[i] + "' is not expected.", getLineNumberForPosition(xmlData, i));
206
206
  }
207
- return throwError('InvalidChar', "char '" + xmlData[i] + "' is not expected.", getLineNumberForPosition(xmlData, i));
207
+ throwError('InvalidChar', "char '" + xmlData[i] + "' is not expected.", getLineNumberForPosition(xmlData, i));
208
208
  }
209
209
  }
210
210
 
211
211
  if (!tagFound) {
212
- return throwError('InvalidXml', 'Start tag expected.', { line: 1, col: 1 });
212
+ throwError('InvalidXml', 'Start tag expected.', { line: 1, col: 1 });
213
213
  } else if (tags.length === 1) {
214
- return throwError('InvalidTag', "Unclosed tag '" + tags[0].tagName + "'.", getLineNumberForPosition(xmlData, tags[0].tagStartPos));
214
+ throwError('InvalidTag', "Unclosed tag '" + tags[0].tagName + "'.", getLineNumberForPosition(xmlData, tags[0].tagStartPos));
215
215
  } else if (tags.length > 0) {
216
- return throwError('InvalidXml', "Invalid '" +
216
+ throwError('InvalidXml', "Invalid '" +
217
217
  JSON.stringify(tags.map(t => t.tagName)).replace(/\r?\n/g, '') +
218
218
  "' found.", { line: 1, col: 1 });
219
219
  }
@@ -225,31 +225,43 @@ function isWhiteSpace(char) {
225
225
  return char === ' ' || char === '\t' || char === '\n' || char === '\r';
226
226
  }
227
227
 
228
- function readPI(xmlData, i) {
228
+ function readPI(xmlData, i, xmlVersion = '1.0') {
229
229
  const piStart = i; // points just after '<?'
230
230
  const nameStart = i;
231
+ let nameValidated = false;
231
232
 
232
233
  for (; i < xmlData.length; i++) {
233
234
  const ch = xmlData[i];
234
- if (ch === ' ' || ch === '?') { //end of name
235
+
236
+ if (!nameValidated && (ch === ' ' || ch === '?')) {
237
+ // First delimiter — extract and validate the PI target name once
235
238
  const piName = xmlData.substr(nameStart, i - nameStart);
239
+ nameValidated = true;
236
240
 
237
241
  if (piName.toLowerCase() === 'xml') {
238
- return throwError(
242
+ throwError(
239
243
  'InvalidXml',
240
244
  'XML declaration allowed only at the start of the document.',
241
245
  getLineNumberForPosition(xmlData, piStart - 2) // point at '<'
242
246
  );
243
- } else if (ch === '?' && xmlData[i + 1] === '>') {
244
- i++;
245
- return i;
247
+ } else if (!xmlName(piName, { xmlVersion })) {
248
+ // PI target must be a valid XML Name (XML 1.0 §2.6)
249
+ throwError(
250
+ 'InvalidXml',
251
+ `Processing instruction target "${piName}" is not a valid XML Name.`,
252
+ getLineNumberForPosition(xmlData, piStart - 2)
253
+ );
246
254
  }
247
255
  }
248
- continue;
256
+
257
+ if (ch === '?' && xmlData[i + 1] === '>') {
258
+ i++;
259
+ return i;
260
+ }
249
261
  }
250
262
 
251
263
  // reached EOF without closing '?>'
252
- return throwError(
264
+ throwError(
253
265
  'InvalidXml',
254
266
  'Processing instruction is not closed with "?>".',
255
267
  getLineNumberForPosition(xmlData, piStart - 2) // point at '<'
@@ -354,7 +366,7 @@ function readAttributeStr(xmlData, i) {
354
366
 
355
367
  const validAttrStrRegxp = new RegExp('(\\s*)([^\\s=]+)(\\s*=)?(\\s*([\'"])(([\\s\\S])*?)\\5)?', 'g');
356
368
 
357
- function validateAttributeString(attrStr, options) {
369
+ function validateAttributeString(attrStr, options, xmlVersion) {
358
370
  const matches = getAllMatches(attrStr, validAttrStrRegxp);
359
371
  const attrNames = {};
360
372
 
@@ -371,7 +383,7 @@ function validateAttributeString(attrStr, options) {
371
383
 
372
384
  //validate attribute names as QNames so namespaced attributes
373
385
  // (e.g. xml:lang, xmlns:xsi) are accepted and malformed ones are rejected.
374
- if (!validateQName(attrName)) {
386
+ if (!validateQName(attrName, xmlVersion)) {
375
387
  return { err: { code: 'InvalidAttr', msg: "Attribute '" + attrName + "' is an invalid name.", line: getPositionFromMatch(matches[i]) } };
376
388
  }
377
389
 
@@ -426,37 +438,14 @@ function throwError(code, message, lineNumber) {
426
438
 
427
439
  /**
428
440
  * Validate an XML QName (Namespaces in XML 1.0 §2.3).
429
- *
430
- * A QName is either:
431
- * • a plain NCName (no colon) — e.g. "foo"
432
- * • a prefixed name NCName ':' NCName — e.g. "ns:foo"
433
- *
434
- * Rules enforced:
435
- * - The name must not be empty.
436
- * - At most one colon is allowed.
437
- * - Neither the prefix nor the local part may be empty
438
- * (so ":foo", "ns:", and ":" are all rejected).
439
- * - Both parts must individually satisfy isName().
441
+ * Delegates to xml-naming's qName production, which enforces:
442
+ * - Non-empty input.
443
+ * - At most one colon (used as prefix separator).
444
+ * - Neither prefix nor local part may be empty (:foo, ns:, : all rejected).
445
+ * - Both parts must satisfy NCName character rules.
440
446
  */
441
- function validateQName(name) {
442
- if (!name || name.length === 0) return false;
443
-
444
- const colonIndex = name.indexOf(':');
445
-
446
- if (colonIndex === -1) {
447
- // Plain name — no namespace prefix.
448
- return isName(name);
449
- }
450
-
451
- // Reject multiple colons (e.g. "a:b:c").
452
- if (name.indexOf(':', colonIndex + 1) !== -1) return false;
453
-
454
- const prefix = name.substring(0, colonIndex);
455
- const local = name.substring(colonIndex + 1);
456
-
457
- // Both parts must be non-empty valid Names.
458
- if (prefix.length === 0 || local.length === 0) return false;
459
- return isName(prefix) && isName(local);
447
+ function validateQName(str, xmlVersion = '1.0') {
448
+ return qName(str, { xmlVersion });
460
449
  }
461
450
 
462
451
  function getLineNumberForPosition(xmlData, index) {
@@ -469,4 +458,83 @@ function getLineNumberForPosition(xmlData, index) {
469
458
 
470
459
  function getPositionFromMatch(match) {
471
460
  return match.startIndex + match[1].length;
472
- }
461
+ }
462
+
463
+ /**
464
+ * Parses and validates the XML declaration: <?xml version="..." encoding="..." standalone="..."?>
465
+ *
466
+ * Rules (XML 1.0 §2.8 / XML 1.1 §2.8):
467
+ * - Attributes must appear in fixed order: version, encoding, standalone
468
+ * - No other attributes are allowed
469
+ * - version is optional (defaults to '1.0'); if present must be '1.0' or '1.1'
470
+ * - encoding and standalone are optional
471
+ * - All values must be quoted (single or double)
472
+ *
473
+ * @param {string} xmlData — full XML string starting with '<?xml'
474
+ * @returns {{ version: string, end: number } | { err: true }}
475
+ */
476
+ function parseXmlDeclaration(xmlData) {
477
+ const piEnd = xmlData.indexOf('?>');
478
+ if (piEnd === -1) {
479
+ throwError('InvalidXml', 'XML declaration is not closed with "?>".', { line: 1, col: 1 });
480
+ }
481
+
482
+ const declBody = xmlData.substring(5, piEnd); // content between '<?xml' and '?>' — keep leading whitespace for regex
483
+ const end = piEnd + 2;
484
+
485
+ // Allowed attributes in required order
486
+ const ALLOWED = ['version', 'encoding', 'standalone'];
487
+ const attrRe = /\s+([\w]+)\s*=\s*(?:"([^"]*)"|'([^']*)')/g;
488
+
489
+ let version = '1.0';
490
+ let lastAllowedIdx = -1;
491
+ let match;
492
+
493
+ while ((match = attrRe.exec(declBody)) !== null) {
494
+ const attrName = match[1];
495
+ const attrValue = match[2] !== undefined ? match[2] : match[3];
496
+ const attrPos = match.index;
497
+
498
+ // Must be a known attribute
499
+ const allowedIdx = ALLOWED.indexOf(attrName);
500
+ if (allowedIdx === -1) {
501
+ throwError('InvalidXml',
502
+ 'XML declaration contains unknown attribute "' + attrName + '". Allowed: version, encoding, standalone.',
503
+ { line: 1, col: 6 + attrPos });
504
+ }
505
+
506
+ // Must appear in fixed order: version, encoding, standalone
507
+ if (allowedIdx <= lastAllowedIdx) {
508
+ throwError('InvalidXml',
509
+ 'XML declaration attribute "' + attrName + '" is out of order. Required order: version, encoding, standalone.',
510
+ { line: 1, col: 6 + attrPos });
511
+ }
512
+ lastAllowedIdx = allowedIdx;
513
+
514
+ if (attrName === 'version') {
515
+ if (attrValue !== '1.0' && attrValue !== '1.1') {
516
+ throwError('InvalidXml',
517
+ 'XML declaration version "' + attrValue + '" is not supported. Must be "1.0" or "1.1".',
518
+ { line: 1, col: 6 + attrPos });
519
+ }
520
+ version = attrValue;
521
+ } else if (attrName === 'standalone') {
522
+ if (attrValue !== 'yes' && attrValue !== 'no') {
523
+ throwError('InvalidXml',
524
+ 'XML declaration standalone "' + attrValue + '" is invalid. Must be "yes" or "no".',
525
+ { line: 1, col: 6 + attrPos });
526
+ }
527
+ }
528
+ // encoding: value format validation is out of scope
529
+ }
530
+
531
+ // Check for leftover unrecognised content (e.g. unquoted tokens or junk)
532
+ const leftover = declBody.replace(/\s+[\w]+\s*=\s*(?:"[^"]*"|'[^']*')/g, '').trim();
533
+ if (leftover.length > 0) {
534
+ throwError('InvalidXml',
535
+ 'XML declaration contains invalid content: "' + leftover + '".',
536
+ { line: 1, col: 1 });
537
+ }
538
+
539
+ return { version, end };
540
+ }