fast-xml-validator 1.0.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/LICENSE +21 -0
- package/README.md +47 -0
- package/lib/fxv.cjs +1 -0
- package/lib/fxv.d.cts +129 -0
- package/lib/fxv.min.js +2 -0
- package/lib/fxv.min.js.map +1 -0
- package/package.json +70 -0
- package/src/DocTypeValidator.js +279 -0
- package/src/ValidationError.js +10 -0
- package/src/fxv.d.ts +115 -0
- package/src/fxv.js +12 -0
- package/src/util.js +61 -0
- package/src/validator.js +472 -0
package/src/validator.js
ADDED
|
@@ -0,0 +1,472 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
import { getAllMatches, isName } from './util.js';
|
|
4
|
+
import DocTypeValidator from './DocTypeValidator.js';
|
|
5
|
+
import ValidationError from './ValidationError.js';
|
|
6
|
+
|
|
7
|
+
const defaultOptions = {
|
|
8
|
+
allowBooleanAttributes: false,
|
|
9
|
+
unpairedTags: [],
|
|
10
|
+
// DOCTYPE entity limits — Infinity means no limit by default
|
|
11
|
+
docType: {
|
|
12
|
+
maxEntityCount: Infinity,
|
|
13
|
+
maxEntitySize: Infinity,
|
|
14
|
+
},
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export function validate(xmlData, options) {
|
|
18
|
+
options = Object.assign({}, defaultOptions, options);
|
|
19
|
+
|
|
20
|
+
const docTypeValidator = new DocTypeValidator(options.docType);
|
|
21
|
+
|
|
22
|
+
const tags = [];
|
|
23
|
+
let tagFound = false;
|
|
24
|
+
|
|
25
|
+
//indicates that the root tag has been closed (aka. depth 0 has been reached)
|
|
26
|
+
let reachedRoot = false;
|
|
27
|
+
|
|
28
|
+
if (xmlData[0] === '\ufeff') {
|
|
29
|
+
// check for byte order mark (BOM)
|
|
30
|
+
xmlData = xmlData.substr(1);
|
|
31
|
+
}
|
|
32
|
+
|
|
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);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
for (let i = 0; i < xmlData.length; i++) {
|
|
43
|
+
if (xmlData[i] === '<' && xmlData[i + 1] === '?') {
|
|
44
|
+
i += 2;
|
|
45
|
+
i = readPI(xmlData, i);
|
|
46
|
+
if (i.err) return i;
|
|
47
|
+
} else if (xmlData[i] === '<') {
|
|
48
|
+
//starting of tag
|
|
49
|
+
//read until you reach to '>' avoiding any '>' in attribute value
|
|
50
|
+
let tagStartPos = i;
|
|
51
|
+
i++;
|
|
52
|
+
|
|
53
|
+
if (xmlData[i] === '!') {
|
|
54
|
+
try {
|
|
55
|
+
i = readCommentAndCDATA(xmlData, i, docTypeValidator);
|
|
56
|
+
} catch (err) {
|
|
57
|
+
return throwError("InvalidTag", err.message, getLineNumberForPosition(xmlData, i - 1));
|
|
58
|
+
}
|
|
59
|
+
continue;
|
|
60
|
+
} else {
|
|
61
|
+
let closingTag = false;
|
|
62
|
+
if (xmlData[i] === '/') {
|
|
63
|
+
closingTag = true;
|
|
64
|
+
i++;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
let tagName = '';
|
|
68
|
+
for (;
|
|
69
|
+
i < xmlData.length &&
|
|
70
|
+
xmlData[i] !== '>' &&
|
|
71
|
+
xmlData[i] !== ' ' &&
|
|
72
|
+
xmlData[i] !== '\t' &&
|
|
73
|
+
xmlData[i] !== '\n' &&
|
|
74
|
+
xmlData[i] !== '\r';
|
|
75
|
+
i++
|
|
76
|
+
) {
|
|
77
|
+
tagName += xmlData[i];
|
|
78
|
+
}
|
|
79
|
+
tagName = tagName.trim();
|
|
80
|
+
|
|
81
|
+
if (tagName[tagName.length - 1] === '/') {
|
|
82
|
+
//self closing tag without attributes
|
|
83
|
+
tagName = tagName.substring(0, tagName.length - 1);
|
|
84
|
+
i--;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// use validateQName so namespaced tags (e.g. <ns:tag>) are
|
|
88
|
+
// accepted and malformed ones (e.g. <:tag>, <ns:>, <a:b:c>) are rejected.
|
|
89
|
+
if (!validateQName(tagName)) {
|
|
90
|
+
let msg;
|
|
91
|
+
if (tagName.trim().length === 0) {
|
|
92
|
+
msg = "Invalid space after '<'.";
|
|
93
|
+
} else {
|
|
94
|
+
msg = "Tag '" + tagName + "' is an invalid name.";
|
|
95
|
+
}
|
|
96
|
+
return throwError('InvalidTag', msg, getLineNumberForPosition(xmlData, tagStartPos));
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const result = readAttributeStr(xmlData, i);
|
|
100
|
+
if (result === false) {
|
|
101
|
+
return throwError('InvalidAttr', "Attributes for '" + tagName + "' have open quote.", getLineNumberForPosition(xmlData, i));
|
|
102
|
+
}
|
|
103
|
+
let attrStr = result.value;
|
|
104
|
+
i = result.index;
|
|
105
|
+
|
|
106
|
+
if (attrStr[attrStr.length - 1] === '/') {
|
|
107
|
+
//self closing tag
|
|
108
|
+
const attrStrStart = i - attrStr.length;
|
|
109
|
+
attrStr = attrStr.substring(0, attrStr.length - 1);
|
|
110
|
+
const isValid = validateAttributeString(attrStr, options);
|
|
111
|
+
if (isValid === true) {
|
|
112
|
+
if (reachedRoot === true) {
|
|
113
|
+
return throwError('InvalidXml', 'Multiple possible root nodes found.', getLineNumberForPosition(xmlData, tagStartPos));
|
|
114
|
+
}
|
|
115
|
+
tagFound = true;
|
|
116
|
+
//if no open tags remain on the stack, this self-closing tag is the root
|
|
117
|
+
if (tags.length === 0) {
|
|
118
|
+
reachedRoot = true;
|
|
119
|
+
}
|
|
120
|
+
} else {
|
|
121
|
+
//the result from the nested function returns the position of the error within the attribute
|
|
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
|
+
//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));
|
|
125
|
+
}
|
|
126
|
+
} else if (closingTag) {
|
|
127
|
+
if (!result.tagClosed) {
|
|
128
|
+
return throwError('InvalidTag', "Closing tag '" + tagName + "' doesn't have proper closing.", getLineNumberForPosition(xmlData, i));
|
|
129
|
+
} else if (attrStr.trim().length > 0) {
|
|
130
|
+
return throwError('InvalidTag', "Closing tag '" + tagName + "' can't have attributes or invalid starting.", getLineNumberForPosition(xmlData, tagStartPos));
|
|
131
|
+
} else if (tags.length === 0) {
|
|
132
|
+
return throwError('InvalidTag', "Closing tag '" + tagName + "' has not been opened.", getLineNumberForPosition(xmlData, tagStartPos));
|
|
133
|
+
} else {
|
|
134
|
+
const otg = tags.pop();
|
|
135
|
+
if (tagName !== otg.tagName) {
|
|
136
|
+
let openPos = getLineNumberForPosition(xmlData, otg.tagStartPos);
|
|
137
|
+
return throwError(
|
|
138
|
+
'InvalidTag',
|
|
139
|
+
"Expected closing tag '" + otg.tagName + "' (opened in line " + openPos.line + ", col " + openPos.col + ") instead of closing tag '" + tagName + "'.",
|
|
140
|
+
getLineNumberForPosition(xmlData, tagStartPos)
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
//when there are no more tags, we reached the root level.
|
|
145
|
+
if (tags.length === 0) {
|
|
146
|
+
reachedRoot = true;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
} else {
|
|
150
|
+
const isValid = validateAttributeString(attrStr, options);
|
|
151
|
+
if (isValid !== true) {
|
|
152
|
+
return throwError(isValid.err.code, isValid.err.msg, getLineNumberForPosition(xmlData, i - attrStr.length + isValid.err.line));
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
if (reachedRoot === true) {
|
|
156
|
+
return throwError('InvalidXml', 'Multiple possible root nodes found.', getLineNumberForPosition(xmlData, i));
|
|
157
|
+
} else if (options.unpairedTags.indexOf(tagName) !== -1) {
|
|
158
|
+
// don't push into stack
|
|
159
|
+
} else {
|
|
160
|
+
tags.push({ tagName, tagStartPos });
|
|
161
|
+
}
|
|
162
|
+
tagFound = true;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
//skip tag text value
|
|
166
|
+
//It may include comments and CDATA value
|
|
167
|
+
for (i++; i < xmlData.length; i++) {
|
|
168
|
+
if (xmlData[i] === '<') {
|
|
169
|
+
if (xmlData[i + 1] === '!') {
|
|
170
|
+
//comment or CADATA
|
|
171
|
+
i++;
|
|
172
|
+
try {
|
|
173
|
+
i = readCommentAndCDATA(xmlData, i, docTypeValidator);
|
|
174
|
+
} catch (err) {
|
|
175
|
+
return throwError("InvalidDocType", err.message, getLineNumberForPosition(xmlData, i - 1));
|
|
176
|
+
}
|
|
177
|
+
continue;
|
|
178
|
+
} else if (xmlData[i + 1] === '?') {
|
|
179
|
+
i += 2;
|
|
180
|
+
i = readPI(xmlData, i);
|
|
181
|
+
if (i.err) return i;
|
|
182
|
+
} else {
|
|
183
|
+
break;
|
|
184
|
+
}
|
|
185
|
+
} else if (xmlData[i] === '&') {
|
|
186
|
+
const afterAmp = validateAmpersand(xmlData, i);
|
|
187
|
+
if (afterAmp === -1)
|
|
188
|
+
return throwError('InvalidChar', "char '&' is not expected.", getLineNumberForPosition(xmlData, i));
|
|
189
|
+
i = afterAmp;
|
|
190
|
+
} else {
|
|
191
|
+
if (reachedRoot === true && !isWhiteSpace(xmlData[i])) {
|
|
192
|
+
return throwError('InvalidXml', 'Extra text at the end', getLineNumberForPosition(xmlData, i));
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
} //end of reading tag text value
|
|
196
|
+
if (xmlData[i] === '<') {
|
|
197
|
+
i--;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
} else {
|
|
201
|
+
if (isWhiteSpace(xmlData[i])) {
|
|
202
|
+
continue;
|
|
203
|
+
}
|
|
204
|
+
if (reachedRoot) {
|
|
205
|
+
return throwError('InvalidXml', "Extra text at the end: '" + xmlData[i] + "' is not expected.", getLineNumberForPosition(xmlData, i));
|
|
206
|
+
}
|
|
207
|
+
return throwError('InvalidChar', "char '" + xmlData[i] + "' is not expected.", getLineNumberForPosition(xmlData, i));
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
if (!tagFound) {
|
|
212
|
+
return throwError('InvalidXml', 'Start tag expected.', { line: 1, col: 1 });
|
|
213
|
+
} else if (tags.length === 1) {
|
|
214
|
+
return throwError('InvalidTag', "Unclosed tag '" + tags[0].tagName + "'.", getLineNumberForPosition(xmlData, tags[0].tagStartPos));
|
|
215
|
+
} else if (tags.length > 0) {
|
|
216
|
+
return throwError('InvalidXml', "Invalid '" +
|
|
217
|
+
JSON.stringify(tags.map(t => t.tagName)).replace(/\r?\n/g, '') +
|
|
218
|
+
"' found.", { line: 1, col: 1 });
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
return true;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function isWhiteSpace(char) {
|
|
225
|
+
return char === ' ' || char === '\t' || char === '\n' || char === '\r';
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function readPI(xmlData, i) {
|
|
229
|
+
const piStart = i; // points just after '<?'
|
|
230
|
+
const nameStart = i;
|
|
231
|
+
|
|
232
|
+
for (; i < xmlData.length; i++) {
|
|
233
|
+
const ch = xmlData[i];
|
|
234
|
+
if (ch === ' ' || ch === '?') { //end of name
|
|
235
|
+
const piName = xmlData.substr(nameStart, i - nameStart);
|
|
236
|
+
|
|
237
|
+
if (piName.toLowerCase() === 'xml') {
|
|
238
|
+
return throwError(
|
|
239
|
+
'InvalidXml',
|
|
240
|
+
'XML declaration allowed only at the start of the document.',
|
|
241
|
+
getLineNumberForPosition(xmlData, piStart - 2) // point at '<'
|
|
242
|
+
);
|
|
243
|
+
} else if (ch === '?' && xmlData[i + 1] === '>') {
|
|
244
|
+
i++;
|
|
245
|
+
return i;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
continue;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// reached EOF without closing '?>'
|
|
252
|
+
return throwError(
|
|
253
|
+
'InvalidXml',
|
|
254
|
+
'Processing instruction is not closed with "?>".',
|
|
255
|
+
getLineNumberForPosition(xmlData, piStart - 2) // point at '<'
|
|
256
|
+
);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* Handles comments, CDATA sections, and DOCTYPE declarations.
|
|
261
|
+
* DOCTYPE validation is delegated to DocTypeValidator.
|
|
262
|
+
* @param {string} xmlData
|
|
263
|
+
* @param {number} i — points at the '!' character
|
|
264
|
+
* @param {DocTypeValidator} docTypeValidator
|
|
265
|
+
*/
|
|
266
|
+
function readCommentAndCDATA(xmlData, i, docTypeValidator) {
|
|
267
|
+
if (xmlData.length > i + 2 && xmlData[i + 1] === '-' && xmlData[i + 2] === '-') {
|
|
268
|
+
// Comment: <!-- ... -->
|
|
269
|
+
for (i += 3; i < xmlData.length; i++) {
|
|
270
|
+
if (xmlData[i] === '-' && xmlData[i + 1] === '-' && xmlData[i + 2] === '>') {
|
|
271
|
+
i += 2;
|
|
272
|
+
break;
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
if (xmlData[i] !== '>') {
|
|
276
|
+
throw new Error('Comment is not closed with "-->".');
|
|
277
|
+
}
|
|
278
|
+
} else if (
|
|
279
|
+
xmlData.length > i + 7 &&
|
|
280
|
+
xmlData[i + 1] === 'D' &&
|
|
281
|
+
xmlData[i + 2] === 'O' &&
|
|
282
|
+
xmlData[i + 3] === 'C' &&
|
|
283
|
+
xmlData[i + 4] === 'T' &&
|
|
284
|
+
xmlData[i + 5] === 'Y' &&
|
|
285
|
+
xmlData[i + 6] === 'P' &&
|
|
286
|
+
xmlData[i + 7] === 'E'
|
|
287
|
+
) {
|
|
288
|
+
// DOCTYPE: delegate fully to DocTypeValidator
|
|
289
|
+
// i currently points at '!', so the '<' is at i - 1
|
|
290
|
+
i = docTypeValidator.validateDocType(xmlData, i - 1);
|
|
291
|
+
} else if (
|
|
292
|
+
xmlData.length > i + 8 &&
|
|
293
|
+
xmlData[i + 1] === '[' &&
|
|
294
|
+
xmlData[i + 2] === 'C' &&
|
|
295
|
+
xmlData[i + 3] === 'D' &&
|
|
296
|
+
xmlData[i + 4] === 'A' &&
|
|
297
|
+
xmlData[i + 5] === 'T' &&
|
|
298
|
+
xmlData[i + 6] === 'A' &&
|
|
299
|
+
xmlData[i + 7] === '['
|
|
300
|
+
) {
|
|
301
|
+
// CDATA: <![CDATA[ ... ]]>
|
|
302
|
+
for (i += 8; i < xmlData.length; i++) {
|
|
303
|
+
if (xmlData[i] === ']' && xmlData[i + 1] === ']' && xmlData[i + 2] === '>') {
|
|
304
|
+
i += 2;
|
|
305
|
+
break;
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
if (xmlData[i] !== '>') {
|
|
309
|
+
throw new Error('CDATA section is not closed with "]]>".');
|
|
310
|
+
}
|
|
311
|
+
} else {
|
|
312
|
+
//Reject unrecognised <! constructs (e.g. <!ELEMENT, <!ATTLIST
|
|
313
|
+
// appearing outside a DOCTYPE, or simply malformed "<!xyz").
|
|
314
|
+
throw new Error("Invalid construct starting with '<!'.");
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
return i;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
const doubleQuote = '"';
|
|
321
|
+
const singleQuote = "'";
|
|
322
|
+
|
|
323
|
+
/**
|
|
324
|
+
* Keep reading xmlData until '<' is found outside the attribute value.
|
|
325
|
+
* @param {string} xmlData
|
|
326
|
+
* @param {number} i
|
|
327
|
+
*/
|
|
328
|
+
function readAttributeStr(xmlData, i) {
|
|
329
|
+
let attrStr = '';
|
|
330
|
+
let startChar = '';
|
|
331
|
+
let tagClosed = false;
|
|
332
|
+
for (; i < xmlData.length; i++) {
|
|
333
|
+
if (xmlData[i] === doubleQuote || xmlData[i] === singleQuote) {
|
|
334
|
+
if (startChar === '') {
|
|
335
|
+
startChar = xmlData[i];
|
|
336
|
+
} else if (startChar !== xmlData[i]) {
|
|
337
|
+
// opposite quote inside value — allowed
|
|
338
|
+
} else {
|
|
339
|
+
startChar = '';
|
|
340
|
+
}
|
|
341
|
+
} else if (xmlData[i] === '>') {
|
|
342
|
+
if (startChar === '') {
|
|
343
|
+
tagClosed = true;
|
|
344
|
+
break;
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
attrStr += xmlData[i];
|
|
348
|
+
}
|
|
349
|
+
if (startChar !== '') {
|
|
350
|
+
return false;
|
|
351
|
+
}
|
|
352
|
+
return { value: attrStr, index: i, tagClosed };
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
const validAttrStrRegxp = new RegExp('(\\s*)([^\\s=]+)(\\s*=)?(\\s*([\'"])(([\\s\\S])*?)\\5)?', 'g');
|
|
356
|
+
|
|
357
|
+
function validateAttributeString(attrStr, options) {
|
|
358
|
+
const matches = getAllMatches(attrStr, validAttrStrRegxp);
|
|
359
|
+
const attrNames = {};
|
|
360
|
+
|
|
361
|
+
for (let i = 0; i < matches.length; i++) {
|
|
362
|
+
if (matches[i][1].length === 0) {
|
|
363
|
+
return { err: { code: 'InvalidAttr', msg: "Attribute '" + matches[i][2] + "' has no space in starting.", line: getPositionFromMatch(matches[i]) } };
|
|
364
|
+
} else if (matches[i][3] !== undefined && matches[i][4] === undefined) {
|
|
365
|
+
return { err: { code: 'InvalidAttr', msg: "Attribute '" + matches[i][2] + "' is without value.", line: getPositionFromMatch(matches[i]) } };
|
|
366
|
+
} else if (matches[i][3] === undefined && !options.allowBooleanAttributes) {
|
|
367
|
+
return { err: { code: 'InvalidAttr', msg: "boolean attribute '" + matches[i][2] + "' is not allowed.", line: getPositionFromMatch(matches[i]) } };
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
const attrName = matches[i][2];
|
|
371
|
+
|
|
372
|
+
//validate attribute names as QNames so namespaced attributes
|
|
373
|
+
// (e.g. xml:lang, xmlns:xsi) are accepted and malformed ones are rejected.
|
|
374
|
+
if (!validateQName(attrName)) {
|
|
375
|
+
return { err: { code: 'InvalidAttr', msg: "Attribute '" + attrName + "' is an invalid name.", line: getPositionFromMatch(matches[i]) } };
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
// xmlns="" (undeclaring the default namespace) is valid in XML 1.1
|
|
379
|
+
// only. Emit a clear error rather than silently accepting it.
|
|
380
|
+
if (attrName === 'xmlns' && matches[i][6] === '') {
|
|
381
|
+
return { err: { code: 'InvalidAttr', msg: 'Undeclaring the default namespace with xmlns="" is only permitted in XML 1.1 documents.', line: getPositionFromMatch(matches[i]) } };
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
if (!Object.prototype.hasOwnProperty.call(attrNames, attrName)) {
|
|
385
|
+
attrNames[attrName] = 1;
|
|
386
|
+
} else {
|
|
387
|
+
return { err: { code: 'InvalidAttr', msg: "Attribute '" + attrName + "' is repeated.", line: getPositionFromMatch(matches[i]) } };
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
return true;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
function validateNumberAmpersand(xmlData, i) {
|
|
395
|
+
let re = /\d/;
|
|
396
|
+
if (xmlData[i] === 'x') {
|
|
397
|
+
i++;
|
|
398
|
+
re = /[\da-fA-F]/;
|
|
399
|
+
}
|
|
400
|
+
for (; i < xmlData.length; i++) {
|
|
401
|
+
if (xmlData[i] === ';') return i;
|
|
402
|
+
if (!xmlData[i].match(re)) break;
|
|
403
|
+
}
|
|
404
|
+
return -1;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
function validateAmpersand(xmlData, i) {
|
|
408
|
+
i++;
|
|
409
|
+
if (xmlData[i] === ';') return -1;
|
|
410
|
+
if (xmlData[i] === '#') {
|
|
411
|
+
i++;
|
|
412
|
+
return validateNumberAmpersand(xmlData, i);
|
|
413
|
+
}
|
|
414
|
+
let count = 0;
|
|
415
|
+
for (; i < xmlData.length; i++, count++) {
|
|
416
|
+
if (xmlData[i].match(/\w/) && count < 20) continue;
|
|
417
|
+
if (xmlData[i] === ';') break;
|
|
418
|
+
return -1;
|
|
419
|
+
}
|
|
420
|
+
return i;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
function throwError(code, message, lineNumber) {
|
|
424
|
+
throw new ValidationError(message, code, lineNumber.line || lineNumber, lineNumber.col);
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
/**
|
|
428
|
+
* 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().
|
|
440
|
+
*/
|
|
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);
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
function getLineNumberForPosition(xmlData, index) {
|
|
463
|
+
const lines = xmlData.substring(0, index).split(/\r?\n/);
|
|
464
|
+
return {
|
|
465
|
+
line: lines.length,
|
|
466
|
+
col: lines[lines.length - 1].length + 1,
|
|
467
|
+
};
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
function getPositionFromMatch(match) {
|
|
471
|
+
return match.startIndex + match[1].length;
|
|
472
|
+
}
|