saxes
Advanced tools
Comparing version 3.1.5 to 3.1.6
@@ -0,1 +1,11 @@ | ||
<a name="3.1.6"></a> | ||
## [3.1.6](https://github.com/lddubeau/saxes/compare/v3.1.5...v3.1.6) (2019-01-17) | ||
### Bug Fixes | ||
* detect unclosed tags in fragments ([5642f36](https://github.com/lddubeau/saxes/commit/5642f36)) | ||
<a name="3.1.5"></a> | ||
@@ -2,0 +12,0 @@ ## [3.1.5](https://github.com/lddubeau/saxes/compare/v3.1.4...v3.1.5) (2019-01-08) |
376
lib/saxes.js
@@ -113,38 +113,47 @@ "use strict"; | ||
function nsMappingCheck(parser, mapping) { | ||
const { xml, xmlns } = mapping; | ||
if (xml && xml !== XML_NAMESPACE) { | ||
parser.fail(`xml prefix must be bound to ${XML_NAMESPACE}.`); | ||
function nsPairCheck(parser, prefix, uri) { | ||
switch (prefix) { | ||
case "xml": | ||
if (uri !== XML_NAMESPACE) { | ||
parser.fail(`xml prefix must be bound to ${XML_NAMESPACE}.`); | ||
} | ||
break; | ||
case "xmlns": | ||
if (uri !== XMLNS_NAMESPACE) { | ||
parser.fail(`xmlns prefix must be bound to ${XMLNS_NAMESPACE}.`); | ||
} | ||
break; | ||
default: | ||
} | ||
if (xmlns && xmlns !== XMLNS_NAMESPACE) { | ||
parser.fail(`xmlns prefix must be bound to ${XMLNS_NAMESPACE}.`); | ||
} | ||
for (const local of Object.keys(mapping)) { | ||
const uri = mapping[local]; | ||
switch (uri) { | ||
case XMLNS_NAMESPACE: | ||
parser.fail(local === "" ? | ||
`the default namespace may not be set to ${uri}.` : | ||
`may not assign a prefix (even "xmlns") to the URI \ | ||
switch (uri) { | ||
case XMLNS_NAMESPACE: | ||
parser.fail(prefix === "" ? | ||
`the default namespace may not be set to ${uri}.` : | ||
`may not assign a prefix (even "xmlns") to the URI \ | ||
${XMLNS_NAMESPACE}.`); | ||
break; | ||
case XML_NAMESPACE: | ||
switch (prefix) { | ||
case "xml": | ||
// Assinging the XML namespace to "xml" is fine. | ||
break; | ||
case XML_NAMESPACE: | ||
switch (local) { | ||
case "xml": | ||
// Assinging the XML namespace to "xml" is fine. | ||
break; | ||
case "": | ||
parser.fail(`the default namespace may not be set to ${uri}.`); | ||
break; | ||
default: | ||
parser.fail("may not assign the xml namespace to another prefix."); | ||
} | ||
case "": | ||
parser.fail(`the default namespace may not be set to ${uri}.`); | ||
break; | ||
default: | ||
parser.fail("may not assign the xml namespace to another prefix."); | ||
} | ||
break; | ||
default: | ||
} | ||
} | ||
function nsMappingCheck(parser, mapping) { | ||
for (const local of Object.keys(mapping)) { | ||
nsPairCheck(parser, local, mapping[local]); | ||
} | ||
} | ||
/** | ||
@@ -316,4 +325,4 @@ * Data structure for an XML tag. | ||
// We want these to be all true if we are dealing with a fragment. | ||
this.reportedTextBeforeRoot = this.reportedTextAfterRoot = | ||
this.closedRoot = this.sawRoot = this.inRoot = fragmentOpt; | ||
this.reportedTextBeforeRoot = this.reportedTextAfterRoot = this.closedRoot = | ||
this.sawRoot = fragmentOpt; | ||
// An XML declaration is intially possible only when parsing whole | ||
@@ -345,3 +354,4 @@ // documents. | ||
this.nameCheck = isNCNameChar; | ||
this.processAttributes = this.processAttributesNS; | ||
this.processAttribs = this.processAttribsNS; | ||
this.pushAttrib = this.pushAttribNS; | ||
@@ -358,3 +368,4 @@ this.ns = Object.assign({ __proto__: null }, rootNS); | ||
this.nameCheck = isNameChar; | ||
this.processAttributes = this.processAttributesPlain; | ||
this.processAttribs = this.processAttribsPlain; | ||
this.pushAttrib = this.pushAttribPlain; | ||
} | ||
@@ -557,17 +568,16 @@ | ||
getCode() { | ||
const { chunk } = this; | ||
let { i } = this; | ||
const { chunk, i } = this; | ||
// Using charCodeAt and handling the surrogates ourselves is faster | ||
// than using codePointAt. | ||
let code = chunk.charCodeAt(i); | ||
let skip = 1; | ||
if (code === CR) { | ||
switch (code) { | ||
case CR: | ||
// We may get undefined if we read past the end of the chunk, which is | ||
// fine. | ||
const next = chunk.charCodeAt(i + 1); | ||
if (next === NL) { | ||
if (chunk.charCodeAt(i + 1) === NL) { | ||
// A \r\n sequence is converted to \n so we have to skip over the next | ||
// character. We already know it has a size of 1 so ++ is fine here. | ||
i++; | ||
skip++; | ||
} | ||
@@ -579,17 +589,16 @@ // Otherwise, a \r is just converted to \n, so we don't have to skip | ||
code = NL; | ||
} | ||
if (code === NL) { | ||
/* yes, fall through */ | ||
case NL: | ||
this.line++; | ||
this.column = 0; | ||
} | ||
else { | ||
break; | ||
default: | ||
this.column++; | ||
if (code >= 0xD800 && code <= 0xDBFF) { | ||
skip = 2; | ||
code = 0x10000 + ((code - 0xD800) * 0x400) + | ||
(chunk.charCodeAt(i + 1) - 0xDC00); | ||
this.column++; | ||
skip++; | ||
} | ||
this.column += skip; | ||
if (!isChar(code)) { | ||
@@ -600,3 +609,3 @@ this.fail("disallowed character."); | ||
this.i = i + skip; | ||
this.i += skip; | ||
@@ -618,32 +627,2 @@ return code; | ||
/** | ||
* Capture characters into a buffer while a condition is true. | ||
* | ||
* @private | ||
* | ||
* @param {CharacterTest} test A test to perform on each character. The | ||
* capture ends when the test returns false. | ||
* | ||
* @param {string} buffer The name of the buffer to save into. | ||
* | ||
* @return {string|undefined} The character that made the test fail, or | ||
* ``undefined`` if we hit the end of the chunk. | ||
*/ | ||
captureWhile(test, buffer) { | ||
const { chunk, limit, i: start } = this; | ||
while (this.i < limit) { | ||
const c = this.getCode(); | ||
if (!test(c)) { | ||
// This is faster than adding codepoints one by one. | ||
this[buffer] += chunk.substring(start, | ||
this.i - (c <= 0xFFFF ? 1 : 2)); | ||
return c; | ||
} | ||
} | ||
// This is faster than adding codepoints one by one. | ||
this[buffer] += chunk.substring(start); | ||
return undefined; | ||
} | ||
/** | ||
* Capture characters into a buffer until encountering one of a set of | ||
@@ -709,3 +688,4 @@ * characters. | ||
/** | ||
* Capture characters that satisfy ``isNameChar`` into a buffer. | ||
* Capture characters that satisfy ``isNameChar`` into the ``name`` field of | ||
* this parser. | ||
* | ||
@@ -717,3 +697,3 @@ * @private | ||
*/ | ||
captureName() { | ||
captureNameChars() { | ||
const { chunk, limit, i: start } = this; | ||
@@ -736,2 +716,30 @@ while (this.i < limit) { | ||
/** | ||
* Capture characters into a buffer while ``this.nameCheck`` run on the | ||
* character read returns true. | ||
* | ||
* @private | ||
* | ||
* @param {string} buffer The name of the buffer to save into. | ||
* | ||
* @return {string|undefined} The character that made the test fail, or | ||
* ``undefined`` if we hit the end of the chunk. | ||
*/ | ||
captureWhileNameCheck(buffer) { | ||
const { chunk, limit, i: start } = this; | ||
while (this.i < limit) { | ||
const c = this.getCode(); | ||
if (!this.nameCheck(c)) { | ||
// This is faster than adding codepoints one by one. | ||
this[buffer] += chunk.substring(start, | ||
this.i - (c <= 0xFFFF ? 1 : 2)); | ||
return c; | ||
} | ||
} | ||
// This is faster than adding codepoints one by one. | ||
this[buffer] += chunk.substring(start); | ||
return undefined; | ||
} | ||
/** | ||
* Skip characters while a condition is true. | ||
@@ -802,3 +810,4 @@ * | ||
if (!this.inRoot && (/\S/.test(this.text) || c === AMP)) { | ||
if ((!this.sawRoot || this.closedRoot) && | ||
(/\S/.test(this.text) || c === AMP)) { | ||
// We use the reportedTextBeforeRoot and reportedTextAfterRoot flags | ||
@@ -913,3 +922,6 @@ // to avoid reporting errors for every single character that is out of | ||
this.state = S_TEXT; | ||
this.emitNode("ondoctype", this.doctype); | ||
if (this.text.length !== 0) { | ||
this.closeText(); | ||
} | ||
this.ondoctype(this.doctype); | ||
this.doctype = true; // just remember that we saw it. | ||
@@ -978,3 +990,6 @@ } | ||
this.state = S_COMMENT_ENDED; | ||
this.emitNode("oncomment", this.comment); | ||
if (this.text.length !== 0) { | ||
this.closeText(); | ||
} | ||
this.oncomment(this.comment); | ||
this.comment = ""; | ||
@@ -1026,3 +1041,6 @@ } | ||
case GREATER: | ||
this.emitNode("oncdata", this.cdata); | ||
if (this.text.length !== 0) { | ||
this.closeText(); | ||
} | ||
this.oncdata(this.cdata); | ||
this.cdata = ""; | ||
@@ -1060,3 +1078,3 @@ this.state = S_TEXT; | ||
sPIRest() { | ||
const c = this.captureWhile(this.nameCheck, "piTarget"); | ||
const c = this.captureWhileNameCheck("piTarget"); | ||
if ((c === QUESTION || isS(c))) { | ||
@@ -1266,3 +1284,6 @@ this.piIsXMLDecl = this.piTarget === "xml"; | ||
} | ||
this.emitNode("onprocessinginstruction", { | ||
if (this.text.length !== 0) { | ||
this.closeText(); | ||
} | ||
this.onprocessinginstruction({ | ||
target: this.piTarget, | ||
@@ -1290,3 +1311,3 @@ body: this.piBody, | ||
sOpenTag() { | ||
const c = this.captureName(); | ||
const c = this.captureNameChars(); | ||
if (!c) { | ||
@@ -1305,3 +1326,10 @@ return; | ||
this.emitNode("onopentagstart", tag); | ||
if (this.text.length !== 0) { | ||
this.closeText(); | ||
} | ||
this.onopentagstart(tag); | ||
this.sawRoot = true; | ||
if (!this.fragmentOpt && this.closedRoot) { | ||
this.fail("documents may contain only one root."); | ||
} | ||
@@ -1327,3 +1355,3 @@ switch (c) { | ||
if (c === GREATER) { | ||
this.openTag(true); | ||
this.openSelfClosingTag(); | ||
} | ||
@@ -1358,6 +1386,25 @@ else { | ||
/** @private */ | ||
pushAttribNS(name, value) { | ||
const { prefix, local } = this.qname(name); | ||
this.attribList.push({ name, prefix, local, value, uri: undefined }); | ||
if (prefix === "xmlns") { | ||
const trimmed = value.trim(); | ||
this.tag.ns[local] = trimmed; | ||
nsPairCheck(this, local, trimmed); | ||
} | ||
else if (name === "xmlns") { | ||
const trimmed = value.trim(); | ||
this.tag.ns[""] = trimmed; | ||
nsPairCheck(this, "", trimmed); | ||
} | ||
} | ||
/** @private */ | ||
pushAttribPlain(name, value) { | ||
this.attribList.push({ name, value }); | ||
} | ||
/** @private */ | ||
sAttribName() { | ||
// We don't need to check with isNameStartChar here because the first | ||
// character of attribute is fed elsewhere, and the check is done there. | ||
const c = this.captureName(); | ||
const c = this.captureNameChars(); | ||
if (c === EQUAL) { | ||
@@ -1371,3 +1418,3 @@ this.state = S_ATTRIB_VALUE; | ||
this.fail("attribute without value."); | ||
this.attribList.push({ name: this.name, value: this.name }); | ||
this.pushAttrib(this.name, this.name); | ||
this.name = this.text = ""; | ||
@@ -1435,3 +1482,3 @@ this.openTag(); | ||
else if (c) { | ||
this.attribList.push({ name: this.name, value: this.text }); | ||
this.pushAttrib(this.name, this.text); | ||
this.name = this.text = ""; | ||
@@ -1479,3 +1526,3 @@ this.q = null; | ||
} | ||
this.attribList.push({ name: this.name, value: this.text }); | ||
this.pushAttrib(this.name, this.text); | ||
this.name = this.text = ""; | ||
@@ -1493,3 +1540,3 @@ if (c === GREATER) { | ||
sCloseTag() { | ||
const c = this.captureName(); | ||
const c = this.captureNameChars(); | ||
if (c === GREATER) { | ||
@@ -1539,3 +1586,3 @@ this.closeTag(); | ||
sEntityRest() { | ||
const c = this.captureWhile(this.nameCheck, "entity"); | ||
const c = this.captureWhileNameCheck("entity"); | ||
@@ -1579,4 +1626,6 @@ if (c === SEMICOLON) { | ||
} | ||
if (this.sawRoot && !this.closedRoot) { | ||
this.fail("unclosed root tag."); | ||
const { tags } = this; | ||
while (tags.length > 0) { | ||
const tag = tags.pop(); | ||
this.fail(`unclosed tag: ${tag.name}`); | ||
} | ||
@@ -1587,3 +1636,3 @@ if ((this.state !== S_BEGIN_WHITESPACE) && | ||
} | ||
if (this.text) { | ||
if (this.text.length !== 0) { | ||
this.closeText(); | ||
@@ -1609,37 +1658,2 @@ } | ||
/** | ||
* Emit any buffered text. Then emit the specified node type. | ||
* | ||
* @param {string} nodeType The node type to emit. | ||
* | ||
* @param {string} data The data associated with the node type. | ||
* | ||
* @private | ||
*/ | ||
emitNode(nodeType, data) { | ||
if (this.text) { | ||
this.closeText(); | ||
} | ||
this[nodeType](data); | ||
} | ||
/** | ||
* Emit any buffered text. Then emit the specified node types. | ||
* | ||
* @param {string} nodeTypeA The node type to emit. | ||
* | ||
* @param {string} nodeTypeB The node type to emit. | ||
* | ||
* @param {string} data The data associated with the node type. | ||
* | ||
* @private | ||
*/ | ||
emitNodes(nodeTypeA, nodeTypeB, data) { | ||
if (this.text) { | ||
this.closeText(); | ||
} | ||
this[nodeTypeA](data); | ||
this[nodeTypeB](data); | ||
} | ||
/** | ||
* Resolve a namespace prefix. | ||
@@ -1692,3 +1706,3 @@ * | ||
const prefix = name.substring(0, colon); | ||
if (prefix === "" || local === "" || local.indexOf(":") !== -1) { | ||
if (prefix === "" || local === "" || local.includes(":")) { | ||
this.fail(`malformed name: ${name}.`); | ||
@@ -1701,18 +1715,6 @@ } | ||
/** @private */ | ||
processAttributesNS() { | ||
processAttribsNS() { | ||
const { tag, attribList } = this; | ||
// emit namespace binding events | ||
const { name: tagName, attributes, ns } = tag; | ||
for (const { name, value } of attribList) { | ||
const { prefix, local } = this.qname(name); | ||
if (prefix === "xmlns") { | ||
ns[local] = value.trim(); | ||
} | ||
else if (name === "xmlns") { | ||
ns[""] = value.trim(); | ||
} | ||
} | ||
const { name: tagName, attributes } = tag; | ||
nsMappingCheck(this, ns); | ||
{ | ||
@@ -1740,4 +1742,4 @@ // add namespace info to tag | ||
// http://www.w3.org/TR/REC-xml-names/#defaulting | ||
for (const { name, value } of attribList) { | ||
const { prefix, local } = this.qname(name); | ||
for (const attr of attribList) { | ||
const { name, prefix, local } = attr; | ||
let uri; | ||
@@ -1765,9 +1767,4 @@ let eqname; | ||
attributes[name] = { | ||
name, | ||
value, | ||
prefix, | ||
local, | ||
uri, | ||
}; | ||
attr.uri = uri; | ||
attributes[name] = attr; | ||
} | ||
@@ -1779,3 +1776,3 @@ | ||
/** @private */ | ||
processAttributesPlain() { | ||
processAttribsPlain() { | ||
const { attribList, tag: { attributes } } = this; | ||
@@ -1797,30 +1794,39 @@ for (const { name, value } of attribList) { | ||
* | ||
* @param {boolean} [selfClosing=false] Whether the tag is self-closing. | ||
* @private | ||
*/ | ||
openTag() { | ||
this.processAttribs(); | ||
const { tag, tags } = this; | ||
tag.isSelfClosing = false; | ||
// There cannot be any pending text here due to the onopentagstart that was | ||
// necessarily emitted before we get here. So we do not check text. | ||
this.onopentag(tag); | ||
tags.push(tag); | ||
this.state = S_TEXT; | ||
this.name = ""; | ||
} | ||
/** | ||
* Handle a complete self-closing tag. This parser code calls this once it has | ||
* seen the whole tag. This method checks for well-formeness and then emits | ||
* ``onopentag`` and ``onclosetag``. | ||
* | ||
* @private | ||
*/ | ||
openTag(selfClosing) { | ||
this.processAttributes(); | ||
openSelfClosingTag() { | ||
this.processAttribs(); | ||
const { tag } = this; | ||
selfClosing = !!selfClosing; | ||
tag.isSelfClosing = selfClosing; | ||
const { tag, tags } = this; | ||
tag.isSelfClosing = true; | ||
if (!this.fragmentOpt && this.closedRoot) { | ||
this.fail("documents may contain only one root."); | ||
// There cannot be any pending text here due to the onopentagstart that was | ||
// necessarily emitted before we get here. So we do not check text. | ||
this.onopentag(tag); | ||
this.onclosetag(tag); | ||
const top = this.tag = tags[tags.length - 1]; | ||
if (!top) { | ||
this.closedRoot = true; | ||
} | ||
this.sawRoot = true; | ||
const { tags } = this; | ||
if (selfClosing) { | ||
this.emitNodes("onopentag", "onclosetag", tag); | ||
const top = this.tag = tags[tags.length - 1]; | ||
if (!top) { | ||
this.closedRoot = true; | ||
} | ||
} | ||
else { | ||
this.emitNode("onopentag", tag); | ||
this.inRoot = true; | ||
tags.push(tag); | ||
} | ||
this.state = S_TEXT; | ||
@@ -1854,13 +1860,13 @@ this.name = ""; | ||
const tag = this.tag = tags.pop(); | ||
this.emitNode("onclosetag", tag); | ||
if (tag.name !== name) { | ||
this.fail("unexpected close tag."); | ||
if (this.text.length !== 0) { | ||
this.closeText(); | ||
} | ||
else { | ||
this.onclosetag(tag); | ||
if (tag.name === name) { | ||
break; | ||
} | ||
this.fail("unexpected close tag."); | ||
} | ||
if (l === 0) { | ||
this.inRoot = false; | ||
this.closedRoot = true; | ||
@@ -1867,0 +1873,0 @@ } |
@@ -5,3 +5,3 @@ { | ||
"author": "Louis-Dominique Dubeau <ldd@lddubeau.com>", | ||
"version": "3.1.5", | ||
"version": "3.1.6", | ||
"main": "lib/saxes.js", | ||
@@ -8,0 +8,0 @@ "types": "lib/saxes.d.ts", |
@@ -37,3 +37,3 @@ # saxes | ||
handler which throws. You can replace it with your own handler if you want. If | ||
your handler does nothing. There is no `resume` method to call. | ||
your handler does nothing, there is no `resume` method to call. | ||
@@ -40,0 +40,0 @@ * There's no `Stream` API. A revamped API may be introduced later. (It is still |
81684
1766