Comparing version 3.0.0 to 3.1.0
@@ -0,1 +1,22 @@ | ||
<a name="3.1.0"></a> | ||
# [3.1.0](https://github.com/lddubeau/saxes/compare/v3.0.0...v3.1.0) (2018-08-28) | ||
### Bug Fixes | ||
* correct typo ([97bc5da](https://github.com/lddubeau/saxes/commit/97bc5da)) | ||
### Performance Improvements | ||
* add emitNodes to skip checking text buffer more than needed ([9d5e357](https://github.com/lddubeau/saxes/commit/9d5e357)) | ||
* capture names in the ``name`` field ([c7dffd5](https://github.com/lddubeau/saxes/commit/c7dffd5)) | ||
* introduce a specialized version of captureWhile ([04855d6](https://github.com/lddubeau/saxes/commit/04855d6)) | ||
* introduce captureTo and captureToChar ([76eb95a](https://github.com/lddubeau/saxes/commit/76eb95a)) | ||
* remove skipWhitespace ([c8b7ae2](https://github.com/lddubeau/saxes/commit/c8b7ae2)) | ||
* remove some redundant buffer resets ([5ded326](https://github.com/lddubeau/saxes/commit/5ded326)) | ||
* use charCodeAt and handle surrogates ourselves ([b8ec232](https://github.com/lddubeau/saxes/commit/b8ec232)) | ||
<a name="3.0.0"></a> | ||
@@ -2,0 +23,0 @@ # [3.0.0](https://github.com/lddubeau/saxes/compare/v2.2.1...v3.0.0) (2018-08-21) |
737
lib/saxes.js
@@ -85,8 +85,2 @@ "use strict"; | ||
const buffers = [ | ||
"comment", "openWakaBang", "textNode", "tagName", "doctype", "piTarget", | ||
"piBody", "entity", "attribName", "attribValue", "cdata", "xmlDeclName", | ||
"xmlDeclValue", | ||
]; | ||
const NL = 0xA; | ||
@@ -114,2 +108,11 @@ const SPACE = 0x20; | ||
const QUOTES = [DQUOTE, SQUOTE]; | ||
const S = [SPACE, NL, 0xD, 9]; | ||
const TEXT_TERMINATOR = [LESS, AMP]; | ||
const DOCTYPE_TERMINATOR = [...QUOTES, OPEN_BRACKET, GREATER]; | ||
const DOCTYPE_DTD_TERMINATOR = [...QUOTES, CLOSE_BRACKET]; | ||
const XML_DECL_NAME_TERMINATOR = [EQUAL, QUESTION, ...S]; | ||
const ATTRIB_VALUE_UNQUOTED_TERMINATOR = [...S, GREATER, AMP, LESS]; | ||
function isEntityStartChar(c) { | ||
@@ -204,15 +207,2 @@ return isNameStartChar(c) || c === HASH; | ||
/** | ||
* @typedef ChunkState | ||
* | ||
* @private | ||
* | ||
* @property {string} chunk The chunk being read. This is readonly. | ||
* | ||
* @property {number} limit The size of the chunk. This is readonly. | ||
* | ||
* @property {number} i The offset into the chunk at which we are to read the | ||
* next character. | ||
*/ | ||
/** | ||
* @typedef XMLDecl | ||
@@ -275,5 +265,14 @@ * | ||
_init(opt) { | ||
for (const buffer of buffers) { | ||
this[buffer] = ""; | ||
} | ||
this.comment = ""; | ||
this.openWakaBang = ""; | ||
this.textNode = ""; | ||
this.name = ""; | ||
this.doctype = ""; | ||
this.piTarget = ""; | ||
this.piBody = ""; | ||
this.entity = ""; | ||
this.attribValue = ""; | ||
this.cdata = ""; | ||
this.xmlDeclName = ""; | ||
this.xmlDeclValue = ""; | ||
@@ -310,2 +309,5 @@ /** | ||
this.tag = null; | ||
this.chunk = ""; | ||
this.chunkPosition = 0; | ||
this.i = 0; | ||
/** | ||
@@ -322,9 +324,10 @@ * A map of entity name to expansion. | ||
this.state = this.opt.fragment ? S_TEXT : S_BEGIN_WHITESPACE; | ||
const fragmentOpt = this.fragmentOpt = !!this.opt.fragment; | ||
this.state = fragmentOpt ? S_TEXT : S_BEGIN_WHITESPACE; | ||
// We want these to be all true if we are dealing with a fragment. | ||
this.reportedTextBeforeRoot = this.reportedTextAfterRoot = | ||
this.closedRoot = this.sawRoot = this.inRoot = this.opt.fragment; | ||
this.closedRoot = this.sawRoot = this.inRoot = fragmentOpt; | ||
// An XML declaration is intially possible only when parsing whole | ||
// documents. | ||
this.xmlDeclPossible = !this.opt.fragment; | ||
this.xmlDeclPossible = !fragmentOpt; | ||
@@ -341,4 +344,5 @@ this.piIsXMLDecl = false; | ||
this.textNodeCheckedBefore = 0; | ||
this.xmlnsOpt = !!this.opt.xmlns; | ||
if (this.opt.xmlns) { | ||
if (this.xmlnsOpt) { | ||
this.ns = Object.assign({ __proto__: null }, rootNS); | ||
@@ -353,17 +357,17 @@ const additional = this.opt.additionalNamespaces; | ||
this.trackPosition = this.opt.position !== false; | ||
if (this.trackPosition) { | ||
/** The line number the parser is currently looking at. */ | ||
this.line = 1; | ||
/** The line number the parser is currently looking at. */ | ||
this.line = 1; | ||
/** The stream position the parser is currently looking at. */ | ||
this.position = 0; | ||
/** The column the parser is currently looking at. */ | ||
this.column = 0; | ||
/** The column the parser is currently looking at. */ | ||
this.column = 0; | ||
this.fileName = this.opt.fileName; | ||
} | ||
this.fileName = this.opt.fileName; | ||
this.onready(); | ||
} | ||
/** The stream position the parser is currently looking at. */ | ||
get position() { | ||
return this.chunkPosition + this.i; | ||
} | ||
/* eslint-disable class-methods-use-this */ | ||
@@ -498,11 +502,9 @@ /** | ||
// ``Array.from`` but don't want to be dependent on Node.) | ||
const limit = chunk.length; | ||
const chunkState = { | ||
chunk, | ||
limit, | ||
i: 0, | ||
}; | ||
while (chunkState.i < limit) { | ||
this[this.state].call(this, chunkState); | ||
const limit = this.limit = chunk.length; | ||
this.chunk = chunk; | ||
this.i = 0; | ||
while (this.i < limit) { | ||
this[this.state](); | ||
} | ||
this.chunkPosition += limit; | ||
@@ -528,9 +530,19 @@ return this; | ||
* | ||
* @param {ChunkState} chunkState The chunk state. | ||
* | ||
* @returns {number} The character read. | ||
*/ | ||
getCode(chunkState) { | ||
const code = chunkState.chunk.codePointAt(chunkState.i); | ||
getCode() { | ||
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 >= 0xD800 && code <= 0xDBFF) { | ||
skip = 2; | ||
code = 0x10000 + ((code - 0xD800) * 0x400) + | ||
(chunk.charCodeAt(i + 1) - 0xDC00); | ||
} | ||
this.i = i + skip; | ||
if (!isChar(code)) { | ||
@@ -540,15 +552,9 @@ this.fail("disallowed character."); | ||
const skip = code <= 0xFFFF ? 1 : 2; | ||
chunkState.i += skip; | ||
if (code && this.trackPosition) { | ||
this.position += skip; | ||
if (code === NL) { | ||
this.line++; | ||
this.column = 0; | ||
} | ||
else { | ||
this.column += skip; | ||
} | ||
if (code === NL) { | ||
this.line++; | ||
this.column = 0; | ||
} | ||
else { | ||
this.column += skip; | ||
} | ||
@@ -570,13 +576,6 @@ return code; | ||
/** | ||
* Capture characters into a buffer while a condition is true. A sequence of | ||
* ``write`` calls may require the capture of text into a buffer as multiple | ||
* "fragments". For instance, given ``write("<x>Multiple")`` and | ||
* ``write("parts</x>")``, the text which is part of the ``x`` element will be | ||
* recorded in two steps: one recording ``"Multiple"`` and one recording | ||
* ``"parts"``. These are two fragments. | ||
* Capture characters into a buffer while a condition is true. | ||
* | ||
* @private | ||
* | ||
* @param {ChunkState} chunkState The current chunk state. | ||
* | ||
* @param {CharacterTest} test A test to perform on each character. The | ||
@@ -590,10 +589,10 @@ * capture ends when the test returns false. | ||
*/ | ||
captureWhile(chunkState, test, buffer) { | ||
const { limit, chunk, i: start } = chunkState; | ||
while (chunkState.i < limit) { | ||
const c = this.getCode(chunkState); | ||
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, | ||
chunkState.i - (c <= 0xFFFF ? 1 : 2)); | ||
this.i - (c <= 0xFFFF ? 1 : 2)); | ||
return c; | ||
@@ -609,19 +608,77 @@ } | ||
/** | ||
* Skip characters while a condition is true. | ||
* Capture characters into a buffer until encountering one of a set of | ||
* characters. | ||
* | ||
* @private | ||
* | ||
* @param {ChunkState} chunkState Chunk information | ||
* @param {number[]} chars An array of codepoints. Encountering a character in | ||
* the array ends the capture. | ||
* | ||
* @param {CharacterTest} test A test to perform on each character. The skip | ||
* 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 capture end, or | ||
* ``undefined`` if we hit the end of the chunk. | ||
*/ | ||
captureTo(chars, buffer) { | ||
const { chunk, limit, i: start } = this; | ||
while (this.i < limit) { | ||
const c = this.getCode(); | ||
if (chars.includes(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 a character. | ||
* | ||
* @private | ||
* | ||
* @param {number} char The codepoint that ends the capture. | ||
* | ||
* @param {string} buffer The name of the buffer to save into. | ||
* | ||
* @return {boolean} ``true`` if we ran into the character. Otherwise, we ran | ||
* into the end of the current chunk. | ||
*/ | ||
captureToChar(char, buffer) { | ||
const { chunk, limit, i: start } = this; | ||
while (this.i < limit) { | ||
const c = this.getCode(); | ||
if (c === char) { | ||
// This is faster than adding codepoints one by one. | ||
this[buffer] += chunk.substring(start, | ||
this.i - (c <= 0xFFFF ? 1 : 2)); | ||
return true; | ||
} | ||
} | ||
// This is faster than adding codepoints one by one. | ||
this[buffer] += chunk.substring(start); | ||
return false; | ||
} | ||
/** | ||
* Capture characters that satisfy ``isNameChar`` into a buffer. | ||
* | ||
* @private | ||
* | ||
* @return {string|undefined} The character that made the test fail, or | ||
* ``undefined`` if we hit the end of the chunk. | ||
*/ | ||
skipWhile(chunkState, test) { | ||
const { limit } = chunkState; | ||
while (chunkState.i < limit) { | ||
const c = this.getCode(chunkState); | ||
if (!test(c)) { | ||
captureName() { | ||
const { chunk, limit, i: start } = this; | ||
while (this.i < limit) { | ||
const c = this.getCode(); | ||
if (!isNameChar(c)) { | ||
// This is faster than adding codepoints one by one. | ||
this.name += chunk.substring(start, | ||
this.i - (c <= 0xFFFF ? 1 : 2)); | ||
return c; | ||
@@ -631,2 +688,4 @@ } | ||
// This is faster than adding codepoints one by one. | ||
this.name += chunk.substring(start); | ||
return undefined; | ||
@@ -636,7 +695,8 @@ } | ||
/** | ||
* Skip whitespace characters. | ||
* Skip characters while a condition is true. | ||
* | ||
* @private | ||
* | ||
* @param {ChunkState} chunkState The current chunk state. | ||
* @param {CharacterTest} test A test to perform on each character. The skip | ||
* ends when the test returns false. | ||
* | ||
@@ -646,7 +706,7 @@ * @return {string|undefined} The character that made the test fail, or | ||
*/ | ||
skipWhitespace(chunkState) { | ||
const { limit } = chunkState; | ||
while (chunkState.i < limit) { | ||
const c = this.getCode(chunkState); | ||
if (!isS(c)) { | ||
skipWhile(test) { | ||
const { limit } = this; | ||
while (this.i < limit) { | ||
const c = this.getCode(); | ||
if (!test(c)) { | ||
return c; | ||
@@ -659,15 +719,14 @@ } | ||
// STATE HANDLERS | ||
/** @private */ | ||
sBeginWhitespace(chunkState) { | ||
const { limit } = chunkState; | ||
let c = this.getCode(chunkState); | ||
sBeginWhitespace() { | ||
const { limit } = this; | ||
let c = this.getCode(); | ||
if (this.initial && c === 0xFEFF) { | ||
this.initial = false; | ||
if (chunkState.i >= limit) { | ||
if (this.i >= limit) { | ||
return; | ||
} | ||
c = this.getCode(chunkState); | ||
c = this.getCode(); | ||
} | ||
@@ -679,4 +738,4 @@ else { | ||
// read character first. | ||
while (chunkState.i < limit && isS(c)) { | ||
c = this.getCode(chunkState); | ||
while (this.i < limit && isS(c)) { | ||
c = this.getCode(); | ||
this.xmlDeclPossible = false; | ||
@@ -702,6 +761,4 @@ } | ||
/** @private */ | ||
sText(chunkState) { | ||
const c = this.captureWhile(chunkState, | ||
cx => cx !== LESS && cx !== AMP, | ||
"textNode"); | ||
sText() { | ||
const c = this.captureTo(TEXT_TERMINATOR, "textNode"); | ||
@@ -746,8 +803,8 @@ if (!this.inRoot && (/\S/.test(this.textNode) || c === AMP)) { | ||
/** @private */ | ||
sOpenWaka(chunkState) { | ||
const c = this.getCode(chunkState); | ||
sOpenWaka() { | ||
const c = this.getCode(); | ||
// either a /, ?, !, or text is coming next. | ||
if (isNameStartChar(c)) { | ||
this.state = S_OPEN_TAG; | ||
this.tagName = String.fromCodePoint(c); | ||
this.name = String.fromCodePoint(c); | ||
this.xmlDeclPossible = false; | ||
@@ -759,3 +816,2 @@ } | ||
this.state = S_CLOSE_TAG; | ||
this.tagName = ""; | ||
this.xmlDeclPossible = false; | ||
@@ -770,3 +826,2 @@ break; | ||
this.state = S_PI; | ||
this.piTarget = this.piBody = ""; | ||
break; | ||
@@ -782,4 +837,4 @@ default: | ||
/** @private */ | ||
sOpenWakaBang(chunkState) { | ||
const c = String.fromCodePoint(this.getCode(chunkState)); | ||
sOpenWakaBang() { | ||
const c = String.fromCodePoint(this.getCode()); | ||
this.openWakaBang += c; | ||
@@ -799,7 +854,5 @@ switch (this.openWakaBang) { | ||
this.openWakaBang = ""; | ||
this.cdata = ""; | ||
break; | ||
case "--": | ||
this.state = S_COMMENT; | ||
this.comment = ""; | ||
this.openWakaBang = ""; | ||
@@ -812,3 +865,2 @@ break; | ||
} | ||
this.doctype = ""; | ||
this.openWakaBang = ""; | ||
@@ -826,7 +878,4 @@ break; | ||
/** @private */ | ||
sDoctype(chunkState) { | ||
const c = this.captureWhile(chunkState, | ||
cx => cx !== OPEN_BRACKET && !isQuote(cx) && | ||
cx !== GREATER, | ||
"doctype"); | ||
sDoctype() { | ||
const c = this.captureTo(DOCTYPE_TERMINATOR, "doctype"); | ||
if (c === GREATER) { | ||
@@ -850,19 +899,14 @@ this.state = S_TEXT; | ||
/** @private */ | ||
sDoctypeQuoted(chunkState) { | ||
sDoctypeQuoted() { | ||
const { q } = this; | ||
const c = this.captureWhile(chunkState, cx => cx !== q, "doctype"); | ||
if (!c || c !== q) { | ||
return; | ||
if (this.captureToChar(q, "doctype")) { | ||
this.doctype += String.fromCodePoint(q); | ||
this.q = null; | ||
this.state = S_DOCTYPE; | ||
} | ||
this.doctype += String.fromCodePoint(c); | ||
this.q = null; | ||
this.state = S_DOCTYPE; | ||
} | ||
/** @private */ | ||
sDoctypeDTD(chunkState) { | ||
const c = this.captureWhile(chunkState, | ||
cx => cx !== CLOSE_BRACKET && !isQuote(cx), | ||
"doctype"); | ||
sDoctypeDTD() { | ||
const c = this.captureTo(DOCTYPE_DTD_TERMINATOR, "doctype"); | ||
if (!c) { | ||
@@ -883,11 +927,6 @@ return; | ||
/** @private */ | ||
sDoctypeDTDQuoted(chunkState) { | ||
sDoctypeDTDQuoted() { | ||
const { q } = this; | ||
const c = this.captureWhile(chunkState, cx => cx !== q, "doctype"); | ||
if (!c) { | ||
return; | ||
} | ||
this.doctype += String.fromCodePoint(c); | ||
if (c === q) { | ||
if (this.captureToChar(q, "doctype")) { | ||
this.doctype += String.fromCodePoint(q); | ||
this.state = S_DOCTYPE_DTD; | ||
@@ -899,15 +938,11 @@ this.q = null; | ||
/** @private */ | ||
sComment(chunkState) { | ||
const c = this.captureWhile(chunkState, cx => cx !== MINUS, "comment"); | ||
if (c === MINUS) { | ||
sComment() { | ||
if (this.captureToChar(MINUS, "comment")) { | ||
this.state = S_COMMENT_ENDING; | ||
} | ||
else if (c) { | ||
this.comment += String.fromCodePoint(c); | ||
} | ||
} | ||
/** @private */ | ||
sCommentEnding(chunkState) { | ||
const c = this.getCode(chunkState); | ||
sCommentEnding() { | ||
const c = this.getCode(); | ||
if (c === MINUS) { | ||
@@ -925,4 +960,4 @@ this.state = S_COMMENT_ENDED; | ||
/** @private */ | ||
sCommentEnded(chunkState) { | ||
const c = this.getCode(chunkState); | ||
sCommentEnded() { | ||
const c = this.getCode(); | ||
if (c !== GREATER) { | ||
@@ -940,19 +975,11 @@ this.fail("malformed comment."); | ||
sCData(chunkState) { | ||
const c = this.captureWhile(chunkState, cx => cx !== CLOSE_BRACKET, "cdata"); | ||
if (!c) { | ||
return; | ||
} | ||
if (c === CLOSE_BRACKET) { | ||
sCData() { | ||
if (this.captureToChar(CLOSE_BRACKET, "cdata")) { | ||
this.state = S_CDATA_ENDING; | ||
} | ||
else { | ||
this.cdata += String.fromCodePoint(c); | ||
} | ||
} | ||
/** @private */ | ||
sCDataEnding(chunkState) { | ||
const c = this.getCode(chunkState); | ||
sCDataEnding() { | ||
const c = this.getCode(); | ||
if (c === CLOSE_BRACKET) { | ||
@@ -968,4 +995,4 @@ this.state = S_CDATA_ENDING_2; | ||
/** @private */ | ||
sCDataEnding2(chunkState) { | ||
const c = this.getCode(chunkState); | ||
sCDataEnding2() { | ||
const c = this.getCode(); | ||
switch (c) { | ||
@@ -987,3 +1014,3 @@ case GREATER: | ||
/** @private */ | ||
sPI(chunkState) { | ||
sPI() { | ||
// We have to perform the isNameStartChar check here because we do not feed | ||
@@ -993,11 +1020,10 @@ // the first character in piTarget elsehwere. | ||
const c = this.captureWhile( | ||
chunkState, | ||
// When namespaces are used, colons are not allowed in pi targets | ||
// names. | ||
// https://www.w3.org/XML/xml-names-19990114-errata.html | ||
// NE08 | ||
this.xmlnsOpt ? | ||
(cx) => { | ||
if (cx !== QUESTION && !isS(cx)) { | ||
if (!(check(cx) && | ||
// When namespaces are used, colons are not allowed in entity | ||
// names. | ||
// https://www.w3.org/XML/xml-names-19990114-errata.html | ||
// NE08 | ||
(!this.opt.xmlns || cx !== COLON))) { | ||
if (!(check(cx) && cx !== COLON)) { | ||
this.fail("disallowed characer in processing instruction name."); | ||
@@ -1011,2 +1037,14 @@ } | ||
return false; | ||
} : | ||
(cx) => { | ||
if (cx !== QUESTION && !isS(cx)) { | ||
if (!check(cx)) { | ||
this.fail("disallowed characer in processing instruction name."); | ||
} | ||
check = isNameChar; | ||
return true; | ||
} | ||
return false; | ||
}, | ||
@@ -1027,3 +1065,3 @@ "piTarget"); | ||
/** @private */ | ||
sPIBody(chunkState) { | ||
sPIBody() { | ||
let c; | ||
@@ -1033,3 +1071,3 @@ if (this.piIsXMLDecl) { | ||
case S_XML_DECL_NAME_START: | ||
c = this.skipWhile(chunkState, (cx) => { | ||
c = this.skipWhile((cx) => { | ||
if (isS(cx)) { | ||
@@ -1063,5 +1101,3 @@ this.requiredSeparator = undefined; | ||
case S_XML_DECL_NAME: | ||
c = this.captureWhile(chunkState, | ||
cx => cx !== QUESTION && !isS(cx) && cx !== EQUAL, | ||
"xmlDeclName"); | ||
c = this.captureTo(XML_DECL_NAME_TERMINATOR, "xmlDeclName"); | ||
// The question mark character is not valid inside any of the XML | ||
@@ -1092,3 +1128,3 @@ // declaration name/value pairs. | ||
case S_XML_DECL_EQ: | ||
c = this.skipWhitespace(chunkState); | ||
c = this.getCode(); | ||
// The question mark character is not valid inside any of the XML | ||
@@ -1101,3 +1137,3 @@ // declaration name/value pairs. | ||
if (c) { | ||
if (c && !isS(c)) { | ||
if (c !== EQUAL) { | ||
@@ -1110,3 +1146,3 @@ this.fail("value required."); | ||
case S_XML_DECL_VALUE_START: | ||
c = this.skipWhitespace(chunkState); | ||
c = this.getCode(); | ||
// The question mark character is not valid inside any of the XML | ||
@@ -1119,3 +1155,3 @@ // declaration name/value pairs. | ||
if (c) { | ||
if (c && !isS(c)) { | ||
if (!isQuote(c)) { | ||
@@ -1129,9 +1165,6 @@ this.fail("value must be quoted."); | ||
this.xmlDeclState = S_XML_DECL_VALUE; | ||
this.xmlDeclValue = ""; | ||
} | ||
break; | ||
case S_XML_DECL_VALUE: | ||
c = this.captureWhile(chunkState, | ||
cx => cx !== QUESTION && cx !== this.q, | ||
"xmlDeclValue"); | ||
c = this.captureTo([this.q, QUESTION], "xmlDeclValue"); | ||
@@ -1184,17 +1217,14 @@ // The question mark character is not valid inside any of the XML | ||
else if (this.piBody.length === 0) { | ||
c = this.skipWhitespace(chunkState); | ||
c = this.getCode(); | ||
if (c === QUESTION) { | ||
this.state = S_PI_ENDING; | ||
} | ||
else if (c) { | ||
else if (c && !isS(c)) { | ||
this.piBody = String.fromCodePoint(c); | ||
} | ||
} | ||
else { | ||
c = this.captureWhile(chunkState, cx => cx !== QUESTION, "piBody"); | ||
// The question mark character is not valid inside any of the XML | ||
// declaration name/value pairs. | ||
if (c === QUESTION) { | ||
this.state = S_PI_ENDING; | ||
} | ||
// The question mark character is not valid inside any of the XML | ||
// declaration name/value pairs. | ||
else if (this.captureToChar(QUESTION, "piBody")) { | ||
this.state = S_PI_ENDING; | ||
} | ||
@@ -1204,4 +1234,4 @@ } | ||
/** @private */ | ||
sPIEnding(chunkState) { | ||
const c = this.getCode(chunkState); | ||
sPIEnding() { | ||
const c = this.getCode(); | ||
if (this.piIsXMLDecl) { | ||
@@ -1220,2 +1250,3 @@ if (c === GREATER) { | ||
this.requiredSeparator = undefined; | ||
this.piTarget = this.piBody = ""; | ||
this.state = S_TEXT; | ||
@@ -1260,19 +1291,4 @@ } | ||
/** @private */ | ||
sOpenTag(chunkState) { | ||
// We don't need to check with isNameStartChar here because the first | ||
// character of tagName is fed elsewhere, and the check is done there. | ||
const c = this.captureWhile( | ||
chunkState, | ||
(cx) => { | ||
if (cx !== GREATER && !isS(cx) && cx !== FORWARD_SLASH) { | ||
if (!isNameChar(cx)) { | ||
this.fail("disallowed characer in tag name."); | ||
} | ||
return true; | ||
} | ||
return false; | ||
}, | ||
"tagName"); | ||
sOpenTag() { | ||
const c = this.captureName(); | ||
if (!c) { | ||
@@ -1283,11 +1299,10 @@ return; | ||
const tag = this.tag = { | ||
name: this.tagName, | ||
name: this.name, | ||
attributes: Object.create(null), | ||
}; | ||
if (this.opt.xmlns) { | ||
if (this.xmlnsOpt) { | ||
tag.ns = Object.create(null); | ||
} | ||
this.attribList = []; | ||
this.emitNode("onopentagstart", tag); | ||
@@ -1311,7 +1326,6 @@ | ||
/** @private */ | ||
sOpenTagSlash(chunkState) { | ||
const c = this.getCode(chunkState); | ||
sOpenTagSlash() { | ||
const c = this.getCode(); | ||
if (c === GREATER) { | ||
this.openTag(true); | ||
this.closeTag(); | ||
} | ||
@@ -1325,9 +1339,9 @@ else { | ||
/** @private */ | ||
sAttrib(chunkState) { | ||
const c = this.skipWhitespace(chunkState); | ||
if (!c) { | ||
sAttrib() { | ||
const c = this.getCode(); | ||
if (!c || isS(c)) { | ||
return; | ||
} | ||
if (isNameStartChar(c)) { | ||
this.attribName = String.fromCodePoint(c); | ||
this.name = String.fromCodePoint(c); | ||
this.attribValue = ""; | ||
@@ -1348,19 +1362,6 @@ this.state = S_ATTRIB_NAME; | ||
/** @private */ | ||
sAttribName(chunkState) { | ||
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.captureWhile( | ||
chunkState, | ||
(cx) => { | ||
if (cx !== EQUAL && !isS(cx) && cx !== GREATER) { | ||
if (!isNameChar(cx)) { | ||
this.fail("disallowed characer in attribute name."); | ||
} | ||
return true; | ||
} | ||
return false; | ||
}, | ||
"attribName"); | ||
const c = this.captureName(); | ||
if (c === EQUAL) { | ||
@@ -1374,4 +1375,4 @@ this.state = S_ATTRIB_VALUE; | ||
this.fail("attribute without value."); | ||
this.attribList.push([this.attribName, this.attribName]); | ||
this.attribName = this.attribValue = ""; | ||
this.attribList.push({ name: this.name, value: this.name }); | ||
this.name = this.attribValue = ""; | ||
this.openTag(); | ||
@@ -1385,4 +1386,8 @@ } | ||
/** @private */ | ||
sAttribNameSawWhite(chunkState) { | ||
const c = this.skipWhitespace(chunkState); | ||
sAttribNameSawWhite() { | ||
const c = this.getCode(); | ||
if (isS(c)) { | ||
return; | ||
} | ||
if (c === EQUAL) { | ||
@@ -1393,5 +1398,5 @@ this.state = S_ATTRIB_VALUE; | ||
this.fail("attribute without value."); | ||
this.tag.attributes[this.attribName] = ""; | ||
this.tag.attributes[this.name] = ""; | ||
this.attribValue = ""; | ||
this.attribName = ""; | ||
this.name = ""; | ||
if (c === GREATER) { | ||
@@ -1401,3 +1406,3 @@ this.openTag(); | ||
else if (isNameStartChar(c)) { | ||
this.attribName = String.fromCodePoint(c); | ||
this.name = String.fromCodePoint(c); | ||
this.state = S_ATTRIB_NAME; | ||
@@ -1413,4 +1418,4 @@ } | ||
/** @private */ | ||
sAttribValue(chunkState) { | ||
const c = this.skipWhitespace(chunkState); | ||
sAttribValue() { | ||
const c = this.getCode(); | ||
if (isQuote(c)) { | ||
@@ -1420,3 +1425,3 @@ this.q = c; | ||
} | ||
else if (c) { | ||
else if (c && !isS(c)) { | ||
this.fail("unquoted attribute value."); | ||
@@ -1429,13 +1434,4 @@ this.state = S_ATTRIB_VALUE_UNQUOTED; | ||
/** @private */ | ||
sAttribValueQuoted(chunkState) { | ||
const { q } = this; | ||
const c = this.captureWhile( | ||
chunkState, | ||
(cx) => { | ||
if (cx === LESS) { | ||
this.fail("disallowed character."); | ||
} | ||
return cx !== q && cx !== AMP; | ||
}, | ||
"attribValue"); | ||
sAttribValueQuoted() { | ||
const c = this.captureTo([this.q, AMP, LESS], "attribValue"); | ||
if (c === AMP) { | ||
@@ -1446,2 +1442,5 @@ this.state = S_ENTITY; | ||
} | ||
else if (c === LESS) { | ||
this.fail("disallowed character."); | ||
} | ||
else if (c) { | ||
@@ -1451,4 +1450,4 @@ if (this.attribValue.includes("]]>")) { | ||
} | ||
this.attribList.push([this.attribName, this.attribValue]); | ||
this.attribName = this.attribValue = ""; | ||
this.attribList.push({ name: this.name, value: this.attribValue }); | ||
this.name = this.attribValue = ""; | ||
this.q = null; | ||
@@ -1460,4 +1459,4 @@ this.state = S_ATTRIB_VALUE_CLOSED; | ||
/** @private */ | ||
sAttribValueClosed(chunkState) { | ||
const c = this.getCode(chunkState); | ||
sAttribValueClosed() { | ||
const c = this.getCode(); | ||
if (isS(c)) { | ||
@@ -1468,3 +1467,3 @@ this.state = S_ATTRIB; | ||
this.fail("no whitespace between attributes."); | ||
this.attribName = String.fromCodePoint(c); | ||
this.name = String.fromCodePoint(c); | ||
this.attribValue = ""; | ||
@@ -1485,12 +1484,5 @@ this.state = S_ATTRIB_NAME; | ||
/** @private */ | ||
sAttribValueUnquoted(chunkState) { | ||
const c = this.captureWhile( | ||
chunkState, | ||
(cx) => { | ||
if (cx === LESS) { | ||
this.fail("disallowed character."); | ||
} | ||
return cx !== GREATER && cx !== AMP && !isS(cx); | ||
}, | ||
"attribValue"); | ||
sAttribValueUnquoted() { | ||
const c = this.captureTo(ATTRIB_VALUE_UNQUOTED_TERMINATOR, | ||
"attribValue"); | ||
if (c === AMP) { | ||
@@ -1501,2 +1493,5 @@ this.state = S_ENTITY; | ||
} | ||
else if (c === LESS) { | ||
this.fail("disallowed character."); | ||
} | ||
else if (c) { | ||
@@ -1506,4 +1501,4 @@ if (this.attribValue.includes("]]>")) { | ||
} | ||
this.attribList.push([this.attribName, this.attribValue]); | ||
this.attribName = this.attribValue = ""; | ||
this.attribList.push({ name: this.name, value: this.attribValue }); | ||
this.name = this.attribValue = ""; | ||
if (c === GREATER) { | ||
@@ -1519,6 +1514,4 @@ this.openTag(); | ||
/** @private */ | ||
sCloseTag(chunkState) { | ||
const c = this.captureWhile(chunkState, | ||
cx => cx !== GREATER && !isS(cx), | ||
"tagName"); | ||
sCloseTag() { | ||
const c = this.captureName(); | ||
if (c === GREATER) { | ||
@@ -1530,11 +1523,14 @@ this.closeTag(); | ||
} | ||
else if (c) { | ||
this.fail("disallowed character in closing tag."); | ||
} | ||
} | ||
/** @private */ | ||
sCloseTagSawWhite(chunkState) { | ||
const c = this.skipWhitespace(chunkState); | ||
sCloseTagSawWhite() { | ||
const c = this.getCode(); | ||
if (c === GREATER) { | ||
this.closeTag(); | ||
} | ||
else if (c) { | ||
else if (c && !isS(c)) { | ||
this.fail("disallowed character in closing tag."); | ||
@@ -1545,12 +1541,12 @@ } | ||
/** @private */ | ||
sEntity(chunkState) { | ||
sEntity() { | ||
let check = this.entity.length === 0 ? isEntityStartChar : isNameChar; | ||
const c = this.captureWhile(chunkState, | ||
const c = this.captureWhile( | ||
// When namespaces are used, colons are | ||
// not valid in entity names. | ||
// https://www.w3.org/XML/xml-names-19990114-errata.html | ||
// NE08 | ||
this.xmlnsOpt ? | ||
(cx) => { | ||
if (check(cx) && | ||
// When namespaces are used, colons are | ||
// not valid in entity names. | ||
// https://www.w3.org/XML/xml-names-19990114-errata.html | ||
// NE08 | ||
(!this.opt.xmlns || cx !== COLON)) { | ||
if (check(cx) && cx !== COLON) { | ||
check = isNameChar; | ||
@@ -1561,2 +1557,9 @@ return true; | ||
return false; | ||
} : (cx) => { | ||
if (check(cx)) { | ||
check = isNameChar; | ||
return true; | ||
} | ||
return false; | ||
}, | ||
@@ -1566,3 +1569,3 @@ "entity"); | ||
if (c === SEMICOLON) { | ||
this[this.entityBufferName] += this.parseEntity(); | ||
this[this.entityBufferName] += this.parseEntity(this.entity); | ||
if (this.entityBufferName === "textNode") { | ||
@@ -1640,2 +1643,31 @@ this.textNodeCheckedBefore = this.textNode.length; | ||
/** | ||
* 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.textNode) { | ||
this.closeText(); | ||
} | ||
this[nodeTypeA](data); | ||
this[nodeTypeB](data); | ||
} | ||
/** | ||
* Resolve a namespace prefix. | ||
* | ||
* @param {string} prefix The prefix to resolve. | ||
* | ||
* @returns {string|undefined} The namespace URI or ``undefined`` if the | ||
* prefix is not defined. | ||
* | ||
* @private | ||
*/ | ||
resolve(prefix) { | ||
@@ -1675,18 +1707,20 @@ let uri = this.tag.ns[prefix]; | ||
const colon = name.indexOf(":"); | ||
let prefix; | ||
let local; | ||
if (colon < 0) { | ||
prefix = ""; | ||
local = name; | ||
return { prefix: "", local: name }; | ||
} | ||
else { | ||
// A colon at the start of the name is illegal. | ||
if (colon === 0) { | ||
this.fail(`malformed name: ${name}.`); | ||
} | ||
prefix = name.substr(0, colon); | ||
local = name.substr(colon + 1); | ||
// A colon at the start of the name is illegal. | ||
if (colon === 0) { | ||
this.fail(`malformed name: ${name}.`); | ||
} | ||
return { prefix, local }; | ||
const local = name.substring(colon + 1); | ||
if (local.indexOf(":") !== -1) { | ||
this.fail(`malformed name: ${name}.`); | ||
} | ||
return { | ||
prefix: name.substring(0, colon), | ||
local, | ||
}; | ||
} | ||
@@ -1705,12 +1739,13 @@ | ||
const { tag, attribList } = this; | ||
if (this.opt.xmlns) { | ||
const { name: tagName, attributes } = tag; | ||
if (this.xmlnsOpt) { | ||
// emit namespace binding events | ||
const { ns, attributes } = tag; | ||
for (const [name, uri] of attribList) { | ||
const { ns } = tag; | ||
for (const { name, value } of attribList) { | ||
const { prefix, local } = this.qname(name); | ||
if (prefix === "xmlns") { | ||
ns[local] = uri.trim(); | ||
ns[local] = value.trim(); | ||
} | ||
else if (name === "xmlns") { | ||
ns[""] = uri.trim(); | ||
ns[""] = value.trim(); | ||
} | ||
@@ -1723,3 +1758,3 @@ } | ||
// add namespace info to tag | ||
const { prefix, local } = this.qname(this.tagName); | ||
const { prefix, local } = this.qname(tagName); | ||
tag.prefix = prefix; | ||
@@ -1744,12 +1779,27 @@ tag.local = local; | ||
// http://www.w3.org/TR/REC-xml-names/#defaulting | ||
for (const [name, value] of attribList) { | ||
const { prefix, local } = this.qname(name, true); | ||
for (const { name, value } of attribList) { | ||
const { prefix, local } = this.qname(name); | ||
let uri; | ||
let eqname; | ||
if (prefix === "") { | ||
uri = (name === "xmlns") ? XMLNS_NAMESPACE : ""; | ||
eqname = name; | ||
} | ||
else { | ||
uri = this.resolve(prefix) || ""; | ||
uri = this.resolve(prefix); | ||
// if there's any attributes with an undefined namespace, | ||
// then fail on them now. | ||
if (!uri) { | ||
this.fail(`unbound namespace prefix: ${JSON.stringify(prefix)}.`); | ||
uri = prefix; | ||
} | ||
eqname = `{${uri}}${local}`; | ||
} | ||
const a = { | ||
if (seen.has(eqname)) { | ||
this.fail(`duplicate attribute: ${eqname}.`); | ||
} | ||
seen.add(eqname); | ||
attributes[name] = { | ||
name, | ||
@@ -1761,21 +1811,6 @@ value, | ||
}; | ||
const eqname = `{${uri}}${local}`; | ||
if (seen.has(eqname)) { | ||
this.fail(`duplicate attribute: ${eqname}.`); | ||
} | ||
seen.add(eqname); | ||
// if there's any attributes with an undefined namespace, | ||
// then fail on them now. | ||
if (prefix && !uri) { | ||
this.fail(`unbound namespace prefix: ${JSON.stringify(prefix)}.`); | ||
a.uri = prefix; | ||
} | ||
attributes[name] = a; | ||
} | ||
} | ||
else { | ||
const { attributes } = this.tag; | ||
for (const [name, value] of attribList) { | ||
for (const { name, value } of attribList) { | ||
if (attributes[name]) { | ||
@@ -1789,20 +1824,25 @@ this.fail(`duplicate attribute: ${name}.`); | ||
tag.isSelfClosing = !!selfClosing; | ||
selfClosing = !!selfClosing; | ||
tag.isSelfClosing = selfClosing; | ||
// process the tag | ||
if (!this.opt.fragment && this.closedRoot) { | ||
if (!this.fragmentOpt && this.closedRoot) { | ||
this.fail("documents may contain only one root."); | ||
} | ||
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.sawRoot = true; | ||
this.tags.push(tag); | ||
this.emitNode("onopentag", tag); | ||
if (!selfClosing) { | ||
this.state = S_TEXT; | ||
this.tag = null; | ||
this.tagName = ""; | ||
} | ||
this.attribName = this.attribValue = ""; | ||
this.state = S_TEXT; | ||
this.name = ""; | ||
} | ||
@@ -1818,3 +1858,3 @@ | ||
closeTag() { | ||
const { tags, tagName } = this; | ||
const { tags, name } = this; | ||
@@ -1824,5 +1864,5 @@ // Our state after this will be S_TEXT, no matter what, and we can clear | ||
this.state = S_TEXT; | ||
this.tagName = ""; | ||
this.name = ""; | ||
if (!tagName) { | ||
if (!name) { | ||
this.fail("weird empty close tag."); | ||
@@ -1837,3 +1877,3 @@ this.textNode += "</>"; | ||
this.emitNode("onclosetag", tag); | ||
if (tag.name !== tagName) { | ||
if (tag.name !== name) { | ||
this.fail("unexpected close tag."); | ||
@@ -1851,4 +1891,4 @@ } | ||
else if (l < 0) { | ||
this.fail(`unmatched closing tag: ${tagName}.`); | ||
this.textNode += `</${tagName}>`; | ||
this.fail(`unmatched closing tag: ${name}.`); | ||
this.textNode += `</${name}>`; | ||
} | ||
@@ -1858,12 +1898,11 @@ } | ||
/** | ||
* Resolves an entity stored in the ``entity`` buffer. Makes any necessary | ||
* well-formedness checks. | ||
* Resolves an entity. Makes any necessary well-formedness checks. | ||
* | ||
* @private | ||
* | ||
* @param {string} entity The entity to resolve. | ||
* | ||
* @returns {string} The parsed entity. | ||
*/ | ||
parseEntity() { | ||
const { entity } = this; | ||
parseEntity(entity) { | ||
const defined = this.ENTITIES[entity]; | ||
@@ -1870,0 +1909,0 @@ if (defined) { |
@@ -5,3 +5,3 @@ { | ||
"author": "Louis-Dominique Dubeau <ldd@lddubeau.com>", | ||
"version": "3.0.0", | ||
"version": "3.1.0", | ||
"main": "lib/saxes.js", | ||
@@ -8,0 +8,0 @@ "types": "lib/saxes.d.ts", |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
78942
1740