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/lib/fxv.cjs +1 -1
- package/lib/fxv.min.js +1 -1
- package/lib/fxv.min.js.map +1 -1
- package/package.json +3 -2
- package/src/DocTypeValidator.js +11 -10
- package/src/util.js +0 -10
- package/src/validator.js +141 -73
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "fast-xml-validator",
|
|
3
|
-
"version": "1.
|
|
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
|
}
|
package/src/DocTypeValidator.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
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 && !
|
|
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 (!
|
|
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
|
|
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
|
|
35
|
-
if (
|
|
36
|
-
|
|
37
|
-
|
|
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
|
-
|
|
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
|
-
|
|
96
|
+
throwError('InvalidTag', msg, getLineNumberForPosition(xmlData, tagStartPos));
|
|
97
97
|
}
|
|
98
98
|
|
|
99
99
|
const result = readAttributeStr(xmlData, i);
|
|
100
100
|
if (result === false) {
|
|
101
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
128
|
+
throwError('InvalidTag', "Closing tag '" + tagName + "' doesn't have proper closing.", getLineNumberForPosition(xmlData, i));
|
|
129
129
|
} else if (attrStr.trim().length > 0) {
|
|
130
|
-
|
|
130
|
+
throwError('InvalidTag', "Closing tag '" + tagName + "' can't have attributes or invalid starting.", getLineNumberForPosition(xmlData, tagStartPos));
|
|
131
131
|
} else if (tags.length === 0) {
|
|
132
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
205
|
+
throwError('InvalidXml', "Extra text at the end: '" + xmlData[i] + "' is not expected.", getLineNumberForPosition(xmlData, i));
|
|
206
206
|
}
|
|
207
|
-
|
|
207
|
+
throwError('InvalidChar', "char '" + xmlData[i] + "' is not expected.", getLineNumberForPosition(xmlData, i));
|
|
208
208
|
}
|
|
209
209
|
}
|
|
210
210
|
|
|
211
211
|
if (!tagFound) {
|
|
212
|
-
|
|
212
|
+
throwError('InvalidXml', 'Start tag expected.', { line: 1, col: 1 });
|
|
213
213
|
} else if (tags.length === 1) {
|
|
214
|
-
|
|
214
|
+
throwError('InvalidTag', "Unclosed tag '" + tags[0].tagName + "'.", getLineNumberForPosition(xmlData, tags[0].tagStartPos));
|
|
215
215
|
} else if (tags.length > 0) {
|
|
216
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 (
|
|
244
|
-
|
|
245
|
-
|
|
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
|
-
|
|
256
|
+
|
|
257
|
+
if (ch === '?' && xmlData[i + 1] === '>') {
|
|
258
|
+
i++;
|
|
259
|
+
return i;
|
|
260
|
+
}
|
|
249
261
|
}
|
|
250
262
|
|
|
251
263
|
// reached EOF without closing '?>'
|
|
252
|
-
|
|
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
|
-
*
|
|
431
|
-
*
|
|
432
|
-
*
|
|
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(
|
|
442
|
-
|
|
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
|
+
}
|