xml-stream
Advanced tools
Comparing version 0.2.0 to 0.3.0
@@ -10,3 +10,4 @@ module.exports = FiniteAutomata; | ||
leave: {}, | ||
state: {} | ||
state: {}, | ||
flag: {} | ||
}; | ||
@@ -23,3 +24,3 @@ this._stack = []; | ||
function run(args, type) { | ||
function run(type, args) { | ||
var cbs = this._callbacks[type]; | ||
@@ -39,3 +40,3 @@ for (var cb in this._state) if (this._state.hasOwnProperty(cb)) { | ||
return this._deterministic; | ||
} | ||
}; | ||
@@ -52,10 +53,10 @@ FiniteAutomata.prototype.on = function(type, state, cb) { | ||
return this; | ||
} | ||
}; | ||
FiniteAutomata.prototype.setState = function(state, args) { | ||
this._state = state; | ||
run.call(this, args, 'enter'); | ||
run.call(this, args, 'state'); | ||
run.call(this, 'enter', args); | ||
run.call(this, 'state', args); | ||
return this; | ||
} | ||
}; | ||
@@ -76,3 +77,3 @@ FiniteAutomata.prototype.nextState = function(symbol) { | ||
return newState; | ||
} | ||
}; | ||
@@ -87,3 +88,3 @@ FiniteAutomata.prototype.go = function(symbol, args) { | ||
this._stack[this._stackPtr] = undefined; | ||
run.call(this, args, 'leave'); | ||
run.call(this, 'leave', args); | ||
this._state = this._stack[--this._stackPtr]; | ||
@@ -100,9 +101,10 @@ return this; | ||
this._state = next; | ||
run.call(this, args, 'enter'); | ||
run.call(this, 'flag'); | ||
run.call(this, 'enter', args); | ||
return this; | ||
}; | ||
FiniteAutomata.prototype.run = function(args) { | ||
run.call(this, args, 'state'); | ||
} | ||
FiniteAutomata.prototype.run = function(state, args) { | ||
run.call(this, state, args); | ||
}; | ||
@@ -109,0 +111,0 @@ FiniteAutomata.prototype.transition = function(stateFrom, symbol, stateTo) { |
@@ -6,3 +6,3 @@ var events = require('events') | ||
// Test if object is empty (has no own properties). | ||
// Tests if object is empty (has no own properties). | ||
function isEmpty(obj) { | ||
@@ -52,2 +52,4 @@ for (var key in obj) if (obj.hasOwnProperty(key)) { | ||
this._emitData = false; | ||
this._bufferLevel = 0; | ||
this._collect = false; | ||
parse.call(this); | ||
@@ -68,10 +70,13 @@ } | ||
// Supported events: | ||
// | ||
// * `data` on outgoing data chunk, | ||
// * `end` when parsing has ended, | ||
// * `startElement[: selector]` on opening tag for selector match, | ||
// * `updateElement[: selector]` on finished node for selector match | ||
// with its contents buffered, | ||
// * `endElement[: selector]` on closing tag for selector match, | ||
// * `text[: selector]` on tag text for selector match. | ||
// | ||
// When adding listeners for `startElement` and `text`, the callback | ||
// can modify the provided node, before it is sent to the consumer. | ||
// When adding listeners for `startElement`, `updateElement`, and `text` the | ||
// callback can modify the provided node, before it is sent to the consumer. | ||
// | ||
@@ -82,43 +87,151 @@ // Selector syntax is CSS-like and currently supports: | ||
// * `parent > child` | ||
XmlStream.prototype.on = function(event, listener) { | ||
XmlStream.super_.prototype.on.call(this, event, listener); | ||
if (event === 'data') { | ||
XmlStream.prototype.on = function(eventName, listener) { | ||
XmlStream.super_.prototype.on.call(this, eventName, listener); | ||
if (eventName === 'data') { | ||
this._emitData = true; | ||
} | ||
var eventParts = event.match(/^(startElement|endElement|text):?(.*)/); | ||
if (eventParts !== null) { | ||
var event = parseEvent(eventName); | ||
if (event !== null) { | ||
// If we're dealing with a selector event, | ||
// continue with selector-specific processing logic. | ||
var evtType = eventParts[1]; | ||
var selector = eventParts[2]; | ||
var parts = selector.match(/[a-z0-9\:]+|>/ig); | ||
selector = parts.join(' '); | ||
event = evtType + ': ' + selector; | ||
var finalState; | ||
if (this._finalStates.hasOwnProperty(selector)) { | ||
finalState = this._finalStates[selector]; | ||
var finalState = getFinalState.call(this, event.selector); | ||
var self = this; | ||
if (event.type === 'updateElement') { | ||
this._fa.on('enter', finalState, function() { | ||
self._bufferLevel++; | ||
}); | ||
this._fa.on('leave', finalState, function(element, context, trace) { | ||
self.emit(event.name, element, context, trace); | ||
if (!--self._bufferLevel && self._emitData) { | ||
emitElement.call(self, element, element.$name, true); | ||
} | ||
}); | ||
} else { | ||
var n = parts.length; | ||
var immediate = false; | ||
this._startState[this._lastState] = true; | ||
for (var i = 0; i < n; i++) { | ||
if (parts[i] === '>') { | ||
immediate = true; | ||
var fn = function(element, context, trace) { | ||
self.emit(event.name, element, context, trace); | ||
}; | ||
this._fa.on(faModes[event.type], finalState, fn); | ||
} | ||
} | ||
}; | ||
// | ||
XmlStream.prototype.collect = function(selector) { | ||
selector = normalizeSelector(selector); | ||
var finalState = getFinalState.call(this, selector); | ||
var self = this; | ||
this._fa.on('flag', finalState, function() { | ||
self._collect = true; | ||
}); | ||
}; | ||
// Normalizes the selector and returns the new version and its parts. | ||
function normalizeSelector(selector) { | ||
var parts = selector.match(/[a-z0-9\:]+|>/ig); | ||
selector = parts.join(' '); | ||
return { | ||
normalized: parts.join(' '), | ||
parts: parts | ||
}; | ||
} | ||
// Parses the selector event string and returns event information. | ||
function parseEvent(event) { | ||
var eventParts = event.match(/^((?:start|end|update)Element|text):?(.*)/); | ||
if (eventParts === null) { | ||
return null; | ||
} | ||
var eventType = eventParts[1]; | ||
var selector = normalizeSelector(eventParts[2]); | ||
return { | ||
selector: selector, | ||
type: eventType, | ||
name: eventType + ': ' + selector.normalized | ||
}; | ||
} | ||
// Compiles a given selector object to a finite automata | ||
// and returns its last state. | ||
function getFinalState(selector) { | ||
if (this._finalStates.hasOwnProperty(selector.normalized)) { | ||
finalState = this._finalStates[selector.normalized]; | ||
} else { | ||
var n = selector.parts.length; | ||
var immediate = false; | ||
this._startState[this._lastState] = true; | ||
for (var i = 0; i < n; i++) { | ||
var part = selector.parts[i]; | ||
if (part === '>') { | ||
immediate = true; | ||
} else { | ||
if (!immediate) { | ||
this._fa.transition(this._lastState, '', this._lastState); | ||
} | ||
this._fa.transition(this._lastState, part, ++this._lastState); | ||
immediate = false; | ||
} | ||
} | ||
finalState = this._lastState++; | ||
this._finalStates[selector.normalized] = finalState; | ||
} | ||
return finalState; | ||
} | ||
// Emits XML for element opening tag. | ||
function emitStart(name, attrs) { | ||
this.emit('data', '<' + name); | ||
for (var attr in attrs) if (attrs.hasOwnProperty(attr)) { | ||
this.emit('data', ' ' + attr + '="' + escape(attrs[attr]) + '"'); | ||
} | ||
this.emit('data', '>'); | ||
} | ||
// Emits XML for element closing tag. | ||
function emitEnd(name) { | ||
this.emit('data', '</' + name + '>'); | ||
} | ||
// Emits XML for element text. | ||
function emitText(text) { | ||
this.emit('data', escape(text)); | ||
} | ||
// Emits a single element and its descendants, or an array of elements. | ||
function emitElement(element, name, onLeave) { | ||
if (Array.isArray(element)) { | ||
var i; | ||
for (i = 0; i < element.length - 1; i++) { | ||
emitOneElement.call(this, element[i], name); | ||
} | ||
emitOneElement.call(this, element[i], name, onLeave); | ||
} else { | ||
emitOneElement.call(this, element, name, onLeave); | ||
} | ||
} | ||
// Recursively emits a given element and its descendants. | ||
function emitOneElement(element, name, onLeave) { | ||
if (typeof element === 'object') { | ||
emitStart.call(this, name, element.$); | ||
var hasText = false; | ||
for (var child in element) { | ||
if (element.hasOwnProperty(child) && child !== '$' && child != '$name') { | ||
if (child === '$text') { | ||
hasText = true; | ||
} else { | ||
if (!immediate) { | ||
this._fa.transition(this._lastState, '', this._lastState); | ||
} | ||
this._fa.transition(this._lastState, parts[i], ++this._lastState); | ||
immediate = false; | ||
emitElement.call(this, element[child], child); | ||
} | ||
} | ||
finalState = this._lastState++; | ||
this._finalStates[selector] = finalState; | ||
} | ||
var self = this; | ||
this._fa.on(faModes[evtType], finalState, function(item, context, trace) { | ||
self.emit(event, item, context, trace); | ||
}); | ||
if (hasText) { | ||
emitText.call(this, element.$text); | ||
} | ||
} else { | ||
emitStart.call(this, name, element.$); | ||
emitText.call(this, element); | ||
} | ||
}; | ||
if(!onLeave) { | ||
emitEnd.call(this, name); | ||
} | ||
} | ||
@@ -133,8 +246,6 @@ // Starts parsing the source stream and emitting various events. | ||
var curr = { | ||
node: { | ||
element: {}, | ||
text: '', | ||
name: 'root' | ||
}, | ||
element: {}, | ||
collect: this._collect, | ||
fullText: '', | ||
space: 0, | ||
path: '', | ||
@@ -151,28 +262,36 @@ context: {} | ||
stack.push(curr); | ||
trace[curr.path] = curr.node.element; | ||
trace[curr.path] = curr.element; | ||
var context = Object.create(curr.context); | ||
var element = {$: attr}; | ||
curr.node.element[name] = element; | ||
var element = { | ||
$: attr, | ||
$name: name, | ||
$text: '' | ||
}; | ||
var parent = curr.element; | ||
curr = { | ||
node: { | ||
element: element, | ||
text: '', | ||
name: name | ||
}, | ||
element: element, | ||
collect: false, | ||
fullText: '', | ||
path: curr.path + '/' + name | ||
space: 0, | ||
path: curr.path + '/' + name, | ||
context: context | ||
}; | ||
context[name] = curr.node.element; | ||
curr.context = context; | ||
fa.enter(name, [curr.node, curr.context, trace]); | ||
if (self._emitData) { | ||
self.emit('data', '<' + curr.node.name); | ||
var attrs = curr.node.element.$; | ||
for (var attr in attrs) if (attrs.hasOwnProperty(attr)) { | ||
self.emit('data', ' ' + attr + '="' + escape(attrs[attr]) + '"'); | ||
fa.enter(name, [element, context, trace]); | ||
name = element.$name; | ||
curr.collect = self._collect; | ||
if (curr.collect) { | ||
var container; | ||
if (Object.hasOwnProperty.call(parent, name)) { | ||
container = parent[name]; | ||
container.push(element); | ||
} else { | ||
container = [element]; | ||
parent[name] = container; | ||
} | ||
self.emit('data', '>'); | ||
} else { | ||
parent[name] = element; | ||
context[name] = element; | ||
} | ||
if (isEmpty(curr.node.element.$)) { | ||
delete curr.node.element.$; | ||
if (self._bufferLevel === 0 && self._emitData) { | ||
emitStart.call(self, element.$name, element.$); | ||
} | ||
@@ -187,22 +306,37 @@ }); | ||
var prev = stack.pop(); | ||
var node = curr.node; | ||
node.text = curr.fullText; | ||
var val; | ||
if (isEmpty(node.element)) { | ||
val = node.text; | ||
} else if (node.text !== '') { | ||
val = new String(node.text); | ||
for(var prop in node.element) if (node.element.hasOwnProperty(prop)) { | ||
val[prop] = node.element[prop]; | ||
var element = curr.element; | ||
var text = curr.fullText; | ||
var attr = element.$; | ||
if (typeof attr !== 'object') { | ||
attr = {}; | ||
} | ||
var name = element.$name; | ||
delete element.$; | ||
delete element.$text; | ||
delete element.$name; | ||
var val = element; | ||
if (isEmpty(element) && isEmpty(attr)) { | ||
val = text; | ||
} else { | ||
if (text !== '') { | ||
element.$text = text; | ||
} | ||
} else { | ||
val = node.element; | ||
if (attr !== null) { | ||
element.$ = attr; | ||
} | ||
} | ||
element.$name = name; | ||
curr.context[name] = val; | ||
prev.node.element[node.name] = val; | ||
fa.leave([node, curr.context, trace]); | ||
if (self._emitData) { | ||
self.emit('data', '</' + node.name + '>'); | ||
if (this._collect) { | ||
var container = prev.element[name]; | ||
container[container.length - 1] = val; | ||
} else { | ||
prev.element[name] = val; | ||
} | ||
fa.leave([element, curr.context, trace]); | ||
if (self._bufferLevel === 0 && self._emitData) { | ||
emitEnd.call(self, name); | ||
} | ||
curr = prev; | ||
this._collect = curr.collect; | ||
}); | ||
@@ -213,8 +347,23 @@ | ||
xml.on('text', function(text) { | ||
curr.node.text = text; | ||
fa.run([curr.node, curr.context, trace]); | ||
if (self._emitData) { | ||
self.emit('data', escape(curr.node.text)); | ||
curr.element.$text = text; | ||
fa.run('state', [curr.element, curr.context, trace]); | ||
if (self._bufferLevel === 0 && self._emitData) { | ||
emitText.call(self, text); | ||
} | ||
curr.fullText += curr.node.text.trim(); | ||
var trimmed = curr.element.$text.trim(); | ||
if (curr.space == 0) { | ||
if (trimmed != '') { | ||
curr.space = 1; | ||
} | ||
} else if (curr.space == 1) { | ||
if (trimmed == '') { | ||
curr.space = 2; | ||
} | ||
} else if (curr.space == 2) { | ||
if (trimmed != '') { | ||
curr.space = 1; | ||
curr.fullText += ' '; | ||
} | ||
} | ||
curr.fullText += trimmed; | ||
}); | ||
@@ -232,2 +381,2 @@ | ||
}); | ||
}; | ||
} |
@@ -9,3 +9,3 @@ { | ||
], | ||
"version": "0.2.0", | ||
"version": "0.3.0", | ||
"author": "AssistUnion <info@assistunion.com>", | ||
@@ -23,5 +23,2 @@ "maintainers": [ | ||
}, | ||
"devDependencies": { | ||
"vows": "~0.5.8" | ||
}, | ||
"engines": { | ||
@@ -34,2 +31,2 @@ "node": ">=0.4.7" | ||
"main": "index" | ||
} | ||
} |
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
No README
QualityPackage does not have a README. This may indicate a failed publish or a low quality package.
Found 1 instance in 1 package
Filesystem access
Supply chain riskAccesses the file system, and could potentially read sensitive data.
Found 1 instance in 1 package
0
500
0
39
0
17910
8