mirror of
https://github.com/sstent/node.git
synced 2026-01-27 07:33:13 +00:00
174 lines
5.0 KiB
CoffeeScript
174 lines
5.0 KiB
CoffeeScript
sax = require 'sax'
|
|
events = require 'events'
|
|
|
|
# Underscore has a nice function for this, but we try to go without dependencies
|
|
isEmpty = (thing) ->
|
|
return typeof thing is "object" && thing? && Object.keys(thing).length is 0
|
|
|
|
exports.defaults =
|
|
"0.1":
|
|
explicitCharkey: false
|
|
trim: true
|
|
# normalize implicates trimming, just so you know
|
|
normalize: true
|
|
# set default attribute object key
|
|
attrkey: "@"
|
|
# set default char object key
|
|
charkey: "#"
|
|
# always put child nodes in an array
|
|
explicitArray: false
|
|
# ignore all attributes regardless
|
|
ignoreAttrs: false
|
|
# merge attributes and child elements onto parent object. this may
|
|
# cause collisions.
|
|
mergeAttrs: false
|
|
explicitRoot: false
|
|
validator: null
|
|
"0.2":
|
|
explicitCharkey: false
|
|
trim: false
|
|
normalize: false
|
|
attrkey: "$"
|
|
charkey: "_"
|
|
explicitArray: true
|
|
ignoreAttrs: false
|
|
mergeAttrs: false
|
|
explicitRoot: true
|
|
validator: null
|
|
|
|
class exports.ValidationError extends Error
|
|
constructor: (message) ->
|
|
@message = message
|
|
|
|
class exports.Parser extends events.EventEmitter
|
|
constructor: (opts) ->
|
|
# copy this versions default options
|
|
@options = {}
|
|
@options[key] = value for own key, value of exports.defaults["0.1"]
|
|
# overwrite them with the specified options, if any
|
|
@options[key] = value for own key, value of opts
|
|
|
|
@reset()
|
|
|
|
reset: =>
|
|
# remove all previous listeners for events, to prevent event listener
|
|
# accumulation
|
|
@removeAllListeners()
|
|
# make the SAX parser. tried trim and normalize, but they are not
|
|
# very helpful
|
|
@saxParser = sax.parser true, {
|
|
trim: false,
|
|
normalize: false
|
|
}
|
|
|
|
# emit one error event if the sax parser fails. this is mostly a hack, but
|
|
# the sax parser isn't state of the art either.
|
|
err = false
|
|
@saxParser.onerror = (error) =>
|
|
if ! err
|
|
err = true
|
|
@emit "error", error
|
|
|
|
# always use the '#' key, even if there are no subkeys
|
|
# setting this property by and is deprecated, yet still supported.
|
|
# better pass it as explicitCharkey option to the constructor
|
|
@EXPLICIT_CHARKEY = @options.explicitCharkey
|
|
@resultObject = null
|
|
stack = []
|
|
# aliases, so we don't have to type so much
|
|
attrkey = @options.attrkey
|
|
charkey = @options.charkey
|
|
|
|
@saxParser.onopentag = (node) =>
|
|
obj = {}
|
|
obj[charkey] = ""
|
|
unless @options.ignoreAttrs
|
|
for own key of node.attributes
|
|
if attrkey not of obj and not @options.mergeAttrs
|
|
obj[attrkey] = {}
|
|
if @options.mergeAttrs
|
|
obj[key] = node.attributes[key]
|
|
else
|
|
obj[attrkey][key] = node.attributes[key]
|
|
|
|
# need a place to store the node name
|
|
obj["#name"] = node.name
|
|
stack.push obj
|
|
|
|
@saxParser.onclosetag = =>
|
|
obj = stack.pop()
|
|
nodeName = obj["#name"]
|
|
delete obj["#name"]
|
|
|
|
s = stack[stack.length - 1]
|
|
# remove the '#' key altogether if it's blank
|
|
if obj[charkey].match(/^\s*$/)
|
|
delete obj[charkey]
|
|
else
|
|
obj[charkey] = obj[charkey].trim() if @options.trim
|
|
obj[charkey] = obj[charkey].replace(/\s{2,}/g, " ").trim() if @options.normalize
|
|
# also do away with '#' key altogether, if there's no subkeys
|
|
# unless EXPLICIT_CHARKEY is set
|
|
if Object.keys(obj).length == 1 and charkey of obj and not @EXPLICIT_CHARKEY
|
|
obj = obj[charkey]
|
|
|
|
if @options.emptyTag != undefined && isEmpty obj
|
|
obj = @options.emptyTag
|
|
|
|
if @options.validator?
|
|
xpath = "/" + (node["#name"] for node in stack).concat(nodeName).join("/")
|
|
obj = @options.validator(xpath, s and s[nodeName], obj)
|
|
|
|
# check whether we closed all the open tags
|
|
if stack.length > 0
|
|
if not @options.explicitArray
|
|
if nodeName not of s
|
|
s[nodeName] = obj
|
|
else if s[nodeName] instanceof Array
|
|
s[nodeName].push obj
|
|
else
|
|
old = s[nodeName]
|
|
s[nodeName] = [old]
|
|
s[nodeName].push obj
|
|
else
|
|
if not (s[nodeName] instanceof Array)
|
|
s[nodeName] = []
|
|
s[nodeName].push obj
|
|
else
|
|
# if explicitRoot was specified, wrap stuff in the root tag name
|
|
if @options.explicitRoot
|
|
# avoid circular references
|
|
old = obj
|
|
obj = {}
|
|
obj[nodeName] = old
|
|
|
|
@resultObject = obj
|
|
@emit "end", @resultObject
|
|
|
|
@saxParser.ontext = @saxParser.oncdata = (text) =>
|
|
s = stack[stack.length - 1]
|
|
if s
|
|
s[charkey] += text
|
|
|
|
parseString: (str, cb) =>
|
|
if cb? and typeof cb is "function"
|
|
@on "end", (result) ->
|
|
@reset()
|
|
cb null, result
|
|
@on "error", (err) ->
|
|
@reset()
|
|
cb err
|
|
|
|
if str.toString().trim() is ''
|
|
@emit "end", null
|
|
return true
|
|
|
|
try
|
|
@saxParser.write str.toString()
|
|
catch ex
|
|
if ex instanceof exports.ValidationError
|
|
@emit("error", ex.message)
|
|
else
|
|
throw ex
|
|
|