Comparing version 0.4.3 to 0.5.0
'use strict'; | ||
require('babel-register'); | ||
// benchmarking libraries | ||
@@ -6,0 +4,0 @@ const Benchmark = require('benchmark'); |
@@ -34,4 +34,5 @@ 'use strict'; | ||
console.error( | ||
'/!\\ To run benchmarks, please install the following ' + | ||
'missing modules:\n npm install ' + notFound.join(' ') | ||
'/!\\ To run benchmarks, please install the following ' | ||
+ 'missing modules:\n npm install --no-save ' | ||
+ notFound.join(' ') | ||
); | ||
@@ -38,0 +39,0 @@ process.exit(); |
@@ -0,3 +1,10 @@ | ||
<!-- vim: set spelllang=en : --> | ||
# Changelog | ||
## v0.5.0 | ||
### New features | ||
* Report an error when opening and closing tags are mismatched or omitted. | ||
## v0.4.3 | ||
@@ -4,0 +11,0 @@ |
@@ -0,1 +1,2 @@ | ||
<!-- vim: set spelllang=en : --> | ||
# Contributing | ||
@@ -2,0 +3,0 @@ |
const {Writable} = require('readable-stream'); | ||
const {StringDecoder} = require('string_decoder'); | ||
const TOKENS = { | ||
TEXT: 'text', | ||
CDATA: 'cdata', | ||
COMMENT: 'comment', | ||
PROCESSINGINSTRUCTION: 'processinginstruction', | ||
TAGOPEN: 'tagopen', | ||
TAGCLOSE: 'tagclose' | ||
/** | ||
* Information about a text node. | ||
* | ||
* @typedef TextNode | ||
* @type {object} | ||
* @prop {string} contents The text value. | ||
*/ | ||
/** | ||
* Emitted whenever a text node is encountered. | ||
* | ||
* @event Saxophone#text | ||
* @type {TextNode} | ||
*/ | ||
/** | ||
* Information about a CDATA node | ||
* (<![CDATA[ ... ]]>). | ||
* | ||
* @typedef CDATANode | ||
* @type {object} | ||
* @prop {string} contents The CDATA contents. | ||
*/ | ||
/** | ||
* Emitted whenever a CDATA node is encountered. | ||
* | ||
* @event Saxophone#cdata | ||
* @type {CDATANode} | ||
*/ | ||
/** | ||
* Information about a comment node | ||
* (<!-- ... -->). | ||
* | ||
* @typedef CommentNode | ||
* @type {object} | ||
* @prop {string} contents The comment contents | ||
*/ | ||
/** | ||
* Emitted whenever a comment node is encountered. | ||
* | ||
* @event Saxophone#comment | ||
* @type {CommentNode} | ||
*/ | ||
/** | ||
* Information about a processing instruction node | ||
* (<? ... ?>). | ||
* | ||
* @typedef ProcessingInstructionNode | ||
* @type {object} | ||
* @prop {string} contents The instruction contents | ||
*/ | ||
/** | ||
* Emitted whenever a processing instruction node is encountered. | ||
* | ||
* @event Saxophone#processinginstruction | ||
* @type {ProcessingInstructionNode} | ||
*/ | ||
/** | ||
* Information about an opened tag | ||
* (<tag attr="value">). | ||
* | ||
* @typedef TagOpenNode | ||
* @type {object} | ||
* @prop {string} name Name of the tag that was opened. | ||
* @prop {string} attrs Attributes passed to the tag, in a string representation | ||
* (use Saxophone.parseAttributes to get an attribute-value mapping). | ||
* @prop {bool} isSelfClosing Whether the tag self-closes (tags of the form | ||
* `<tag />`). Such tags will not be followed by a closing tag. | ||
*/ | ||
/** | ||
* Emitted whenever an opening tag node is encountered. | ||
* | ||
* @event Saxophone#tagopen | ||
* @type {TagOpen} | ||
*/ | ||
/** | ||
* Information about a closed tag | ||
* (</tag>). | ||
* | ||
* @typedef TagCloseNode | ||
* @type {object} | ||
* @prop {string} name The tag name | ||
*/ | ||
/** | ||
* Emitted whenever a closing tag node is encountered. | ||
* | ||
* @event Saxophone#tagclose | ||
* @type {TagCloseNode} | ||
*/ | ||
/** | ||
* Nodes that can be found inside an XML stream. | ||
* @private | ||
*/ | ||
const Node = { | ||
text: 'text', | ||
cdata: 'cdata', | ||
comment: 'comment', | ||
processingInstruction: 'processinginstruction', | ||
tagOpen: 'tagopen', | ||
tagClose: 'tagclose', | ||
}; | ||
@@ -40,2 +143,5 @@ | ||
// Stack of tags that were opened up until the current cursor position | ||
this._tagStack = []; | ||
// Not stalled initially | ||
@@ -70,2 +176,18 @@ this._stall(null); | ||
/** | ||
* Handle the opening of a tag in the text stream. | ||
* | ||
* Push the tag into the opened tag stack and emit the | ||
* corresponding event on the event emitter. | ||
* | ||
* @param {TagOpen} node Information about the opened tag. | ||
*/ | ||
_handleTagOpening(node) { | ||
if (!node.isSelfClosing) { | ||
this._tagStack.push(node.name); | ||
} | ||
this.emit(Node.tagOpen, node); | ||
} | ||
/** | ||
* Parse a XML chunk. | ||
@@ -93,3 +215,3 @@ * | ||
this._stall( | ||
TOKENS.TEXT, | ||
Node.text, | ||
input.slice(chunkPos) | ||
@@ -102,12 +224,4 @@ ); | ||
// we have all the data needed for the TEXT node | ||
/** | ||
* Text token event | ||
* | ||
* @event Saxophone#text | ||
* @type {object} | ||
* @prop {string} contents The text value | ||
*/ | ||
this.emit( | ||
TOKENS.TEXT, | ||
Node.text, | ||
{contents: input.slice(chunkPos, nextTag)} | ||
@@ -140,3 +254,3 @@ ); | ||
this._stall( | ||
TOKENS.CDATA, | ||
Node.cdata, | ||
input.slice(chunkPos - 9) | ||
@@ -147,12 +261,4 @@ ); | ||
/** | ||
* CDATA token event | ||
* (<![CDATA[ ... ]]>) | ||
* | ||
* @event Saxophone#cdata | ||
* @type {object} | ||
* @prop {string} contents The CDATA contents | ||
*/ | ||
this.emit( | ||
TOKENS.CDATA, | ||
Node.cdata, | ||
{contents: input.slice(chunkPos, cdataClose)} | ||
@@ -173,3 +279,3 @@ ); | ||
this._stall( | ||
TOKENS.COMMENT, | ||
Node.comment, | ||
input.slice(chunkPos - 4) | ||
@@ -185,12 +291,4 @@ ); | ||
/** | ||
* Comment token event | ||
* (<!-- ... -->) | ||
* | ||
* @event Saxophone#comment | ||
* @type {object} | ||
* @prop {string} contents The comment contents | ||
*/ | ||
this.emit( | ||
TOKENS.COMMENT, | ||
Node.comment, | ||
{contents: input.slice(chunkPos, commentClose)} | ||
@@ -216,3 +314,3 @@ ); | ||
this._stall( | ||
TOKENS.PROCESSINGINSTRUCTION, | ||
Node.processingInstruction, | ||
input.slice(chunkPos - 2) | ||
@@ -223,12 +321,4 @@ ); | ||
/** | ||
* Processing instruction token event | ||
* (<? ... ?>) | ||
* | ||
* @event Saxophone#processinginstruction | ||
* @type {object} | ||
* @prop {string} contents The instruction contents | ||
*/ | ||
this.emit( | ||
TOKENS.PROCESSINGINSTRUCTION, | ||
Node.processingInstruction, | ||
{contents: input.slice(chunkPos, piClose)} | ||
@@ -246,3 +336,3 @@ ); | ||
this._stall( | ||
TOKENS.TAGOPEN, | ||
Node.tagOpen, | ||
input.slice(chunkPos - 1) | ||
@@ -255,13 +345,14 @@ ); | ||
if (input[chunkPos] === '/') { | ||
/** | ||
* Closing tag token event | ||
* (</tag>) | ||
* | ||
* @event Saxophone#tagclose | ||
* @type {object} | ||
* @prop {string} name The tag name | ||
*/ | ||
const tagName = input.slice(chunkPos + 1, tagClose); | ||
const stackedTagName = this._tagStack.pop(); | ||
if (stackedTagName !== tagName) { | ||
callback(new Error(`Unclosed tag: ${stackedTagName}`)); | ||
this._tagStack.length = 0; | ||
return; | ||
} | ||
this.emit( | ||
TOKENS.TAGCLOSE, | ||
{name: input.slice(chunkPos + 1, tagClose)} | ||
Node.tagClose, | ||
{name: tagName} | ||
); | ||
@@ -282,16 +373,3 @@ | ||
// Tag without any attribute | ||
/** | ||
* Opening tag token event | ||
* (<tag attr="value">) | ||
* | ||
* @event Saxophone#tagopen | ||
* @type {object} | ||
* @prop {string} name The tag name | ||
* @prop {string} attrs The tag attributes (use | ||
* Saxophone.parseAttributes) to parse the string to a hash | ||
* @prop {bool} isSelfClosing Whether the tag is self-closing | ||
* (<tag />) | ||
*/ | ||
this.emit(TOKENS.TAGOPEN, { | ||
this._handleTagOpening({ | ||
name: input.slice(chunkPos, realTagClose), | ||
@@ -306,3 +384,3 @@ attrs: '', | ||
// Tag with attributes | ||
this.emit(TOKENS.TAGOPEN, { | ||
this._handleTagOpening({ | ||
name: input.slice(chunkPos, chunkPos + whitespace), | ||
@@ -353,3 +431,3 @@ attrs: input.slice(chunkPos + whitespace, realTagClose), | ||
switch (this._stalled) { | ||
case TOKENS.TEXT: | ||
case Node.text: | ||
// Text nodes are implicitly closed | ||
@@ -361,13 +439,13 @@ this.emit( | ||
break; | ||
case TOKENS.CDATA: | ||
case Node.cdata: | ||
callback(new Error('Unclosed CDATA section')); | ||
return; | ||
case TOKENS.COMMENT: | ||
case Node.comment: | ||
callback(new Error('Unclosed comment')); | ||
return; | ||
case TOKENS.PROCESSINGINSTRUCTION: | ||
case Node.processingInstruction: | ||
callback(new Error('Unclosed processing instruction')); | ||
return; | ||
case TOKENS.TAGOPEN: | ||
case TOKENS.TAGCLOSE: | ||
case Node.tagOpen: | ||
case Node.tagClose: | ||
// We do not distinguish between unclosed opening | ||
@@ -381,2 +459,7 @@ // or unclosed closing tags | ||
if (this._tagStack.length !== 0) { | ||
callback(new Error(`Unclosed tags: ${this._tagStack.join(',')}`)); | ||
return; | ||
} | ||
callback(); | ||
@@ -383,0 +466,0 @@ }); |
@@ -101,4 +101,7 @@ const {Readable} = require('readable-stream'); | ||
expectEvents(assert, | ||
'<tag>', | ||
[['tagopen', {name: 'tag', attrs: '', isSelfClosing: false}]] | ||
'<tag></tag>', | ||
[ | ||
['tagopen', {name: 'tag', attrs: '', isSelfClosing: false}], | ||
['tagclose', {name: 'tag'}] | ||
] | ||
); | ||
@@ -114,2 +117,20 @@ }); | ||
test('should not parse unclosed tags 2', assert => { | ||
expectEvents(assert, | ||
'<tag>', | ||
[['error', new Error('Unclosed tags: tag')]] | ||
); | ||
}); | ||
test('should not parse unclosed tags 3', assert => { | ||
expectEvents(assert, | ||
'<closed><unclosed></closed>', | ||
[ | ||
['tagopen', {name: 'closed', attrs: '', isSelfClosing: false}], | ||
['tagopen', {name: 'unclosed', attrs: '', isSelfClosing: false}], | ||
['error', new Error('Unclosed tag: unclosed')], | ||
] | ||
); | ||
}); | ||
test('should not parse DOCTYPEs', assert => { | ||
@@ -138,4 +159,7 @@ expectEvents(assert, | ||
expectEvents(assert, | ||
'</closed>', | ||
[['tagclose', {name: 'closed'}]] | ||
'<closed></closed>', | ||
[ | ||
['tagopen', {name: 'closed', attrs: '', isSelfClosing: false}], | ||
['tagclose', {name: 'closed'}] | ||
] | ||
); | ||
@@ -142,0 +166,0 @@ }); |
{ | ||
"name": "saxophone", | ||
"description": "Fast and lightweight event-driven XML parser in pure JavaScript", | ||
"version": "0.4.3", | ||
"version": "0.5.0", | ||
"license": "MIT", | ||
@@ -6,0 +6,0 @@ "main": "lib/index.js", |
@@ -0,1 +1,2 @@ | ||
<!-- vim: set spelllang=en : --> | ||
# Saxophone 🎷 | ||
@@ -19,3 +20,3 @@ | ||
This library works both in Node.JS ≥4.0 and recent browsers. | ||
This library works both in Node.JS ≥6.0 and recent browsers. | ||
To install with `npm`: | ||
@@ -31,9 +32,9 @@ | ||
| Library | Operations per second (higher is better) | | ||
|--------------------|-----------------------------------------:| | ||
| **Saxophone** | **3,717 ops/sec ±1.41%** | | ||
| **EasySax** | **4,346 ops/sec ±2.56%** | | ||
| node-expat | 1,161 ops/sec ±1.94% | | ||
| libxmljs.SaxParser | 1,040 ops/sec ±1.53% | | ||
| sax-js | 760 ops/sec ±2.30% | | ||
Library | Version | Operations per second (higher is better) | ||
-------------------|--------:|----------------------------------------: | ||
**Saxophone** | 0.5.0 | **6,840 ±1.48%** | ||
**EasySax** | 0.3.2 | **7,354 ±1.16%** | ||
node-expat | 2.3.17 | 1,251 ±0.60% | ||
libxmljs.SaxParser | 0.19.5 | 1,007 ±0.81% | ||
sax-js | 1.2.4 | 982 ±1.50% | ||
@@ -46,3 +47,3 @@ To run the benchmark by yourself, use the following commands: | ||
$ npm install | ||
$ npm install easysax node-expat libxmljs sax | ||
$ npm install --no-save easysax node-expat libxmljs sax | ||
$ npm run benchmark | ||
@@ -217,4 +218,10 @@ ``` | ||
Emitted when a parsing error is encountered while reading the XML stream such that the rest of the XML cannot be correctly interpreted. | ||
Emitted when a parsing error is encountered while reading the XML stream such that the rest of the XML cannot be correctly interpreted: | ||
* when a DOCTYPE node is found (not supported yet); | ||
* when a comment node contains the `--` sequence; | ||
* when opening and closing tags are mismatched or missing; | ||
* when a tag name starts with white space; | ||
* when nodes are unclosed (missing their final `>`). | ||
Because this library's goal is not to provide accurate error reports, the passed error will only contain a short description of the syntax error (without giving the position, for example). | ||
@@ -230,3 +237,3 @@ | ||
Thanks to [Norman Rzepka](https://github.com/normanrz) for implementing the streaming API. | ||
Thanks to [Norman Rzepka](https://github.com/normanrz) for implementing the streaming API and the check for opening and closing tags mismatch. | ||
@@ -233,0 +240,0 @@ ## License |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
71172
955
238
0