mirror of https://github.com/ghostfolio/ghostfolio
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
413 lines
11 KiB
413 lines
11 KiB
(function () {
|
|
var sax;
|
|
|
|
if (
|
|
typeof module !== "undefined" &&
|
|
module.exports &&
|
|
!global.xmldocAssumeBrowser
|
|
) {
|
|
// We're being used in a Node-like environment
|
|
sax = require("sax");
|
|
} else {
|
|
// assume it's attached to the Window object in a browser
|
|
sax = this.sax;
|
|
|
|
if (!sax) {
|
|
// no sax for you!
|
|
throw new Error(
|
|
"Expected sax to be defined. Make sure you're including sax.js before this file.",
|
|
);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* XmlElement is our basic building block. Everything is an XmlElement; even XmlDocument
|
|
* behaves like an XmlElement by inheriting its attributes and functions.
|
|
*/
|
|
function XmlElement(tag, parser) {
|
|
// If you didn't hand us a parser (common case) see if we can grab one
|
|
// from the current execution stack.
|
|
if (!parser) {
|
|
var delegate = delegates[delegates.length - 1];
|
|
|
|
if (delegate.parser) {
|
|
parser = delegate.parser;
|
|
}
|
|
}
|
|
|
|
this.name = tag.name;
|
|
this.attr = tag.attributes;
|
|
this.val = "";
|
|
this.children = [];
|
|
this.firstChild = null;
|
|
this.lastChild = null;
|
|
|
|
// Assign parse information
|
|
this.line = parser ? parser.line : null;
|
|
this.column = parser ? parser.column : null;
|
|
this.position = parser ? parser.position : null;
|
|
this.startTagPosition = parser ? parser.startTagPosition : null;
|
|
}
|
|
|
|
// Private methods
|
|
|
|
XmlElement.prototype._addChild = function (child) {
|
|
// add to our children array
|
|
this.children.push(child);
|
|
|
|
// update first/last pointers
|
|
if (!this.firstChild) this.firstChild = child;
|
|
this.lastChild = child;
|
|
};
|
|
|
|
// SaxParser handlers
|
|
|
|
XmlElement.prototype._opentag = function (tag) {
|
|
var child = new XmlElement(tag);
|
|
|
|
this._addChild(child);
|
|
|
|
delegates.unshift(child);
|
|
};
|
|
|
|
XmlElement.prototype._closetag = function () {
|
|
delegates.shift();
|
|
};
|
|
|
|
XmlElement.prototype._text = function (text) {
|
|
if (typeof this.children === "undefined") return;
|
|
|
|
this.val += text;
|
|
|
|
this._addChild(new XmlTextNode(text));
|
|
};
|
|
|
|
XmlElement.prototype._cdata = function (cdata) {
|
|
this.val += cdata;
|
|
|
|
this._addChild(new XmlCDataNode(cdata));
|
|
};
|
|
|
|
XmlElement.prototype._comment = function (comment) {
|
|
if (typeof this.children === "undefined") return;
|
|
|
|
this._addChild(new XmlCommentNode(comment));
|
|
};
|
|
|
|
XmlElement.prototype._error = function (err) {
|
|
throw err;
|
|
};
|
|
|
|
// Useful functions
|
|
|
|
XmlElement.prototype.eachChild = function (iterator, context) {
|
|
for (var i = 0, l = this.children.length; i < l; i++)
|
|
if (this.children[i].type === "element")
|
|
if (
|
|
iterator.call(context, this.children[i], i, this.children) === false
|
|
)
|
|
return;
|
|
};
|
|
|
|
XmlElement.prototype.childNamed = function (name) {
|
|
for (var i = 0, l = this.children.length; i < l; i++) {
|
|
var child = this.children[i];
|
|
if (child.name === name) return child;
|
|
}
|
|
return undefined;
|
|
};
|
|
|
|
XmlElement.prototype.childrenNamed = function (name) {
|
|
var matches = [];
|
|
|
|
for (var i = 0, l = this.children.length; i < l; i++)
|
|
if (this.children[i].name === name) matches.push(this.children[i]);
|
|
|
|
return matches;
|
|
};
|
|
|
|
XmlElement.prototype.childWithAttribute = function (name, value) {
|
|
for (var i = 0, l = this.children.length; i < l; i++) {
|
|
var child = this.children[i];
|
|
if (
|
|
child.type === "element" &&
|
|
((value && child.attr[name] === value) || (!value && child.attr[name]))
|
|
)
|
|
return child;
|
|
}
|
|
return undefined;
|
|
};
|
|
|
|
XmlElement.prototype.descendantsNamed = function (name) {
|
|
var matches = [];
|
|
|
|
for (var i = 0, l = this.children.length; i < l; i++) {
|
|
var child = this.children[i];
|
|
if (child.type === "element") {
|
|
if (child.name === name) matches.push(child);
|
|
matches = matches.concat(child.descendantsNamed(name));
|
|
}
|
|
}
|
|
|
|
return matches;
|
|
};
|
|
|
|
XmlElement.prototype.descendantWithPath = function (path) {
|
|
var descendant = this;
|
|
var components = path.split(".");
|
|
|
|
for (var i = 0, l = components.length; i < l; i++)
|
|
if (descendant && descendant.type === "element")
|
|
descendant = descendant.childNamed(components[i]);
|
|
else return undefined;
|
|
|
|
return descendant;
|
|
};
|
|
|
|
XmlElement.prototype.valueWithPath = function (path) {
|
|
var components = path.split("@");
|
|
var descendant = this.descendantWithPath(components[0]);
|
|
if (descendant)
|
|
return components.length > 1
|
|
? descendant.attr[components[1]]
|
|
: descendant.val;
|
|
else return undefined;
|
|
};
|
|
|
|
// String formatting (for debugging)
|
|
|
|
XmlElement.prototype.toString = function (options) {
|
|
return this.toStringWithIndent("", options);
|
|
};
|
|
|
|
XmlElement.prototype.toStringWithIndent = function (indent, options) {
|
|
var s = indent + "<" + this.name;
|
|
var linebreak = options && options.compressed ? "" : "\n";
|
|
var preserveWhitespace = options && options.preserveWhitespace;
|
|
|
|
for (var name in this.attr)
|
|
if (Object.prototype.hasOwnProperty.call(this.attr, name))
|
|
s += " " + name + '="' + escapeXML(this.attr[name]) + '"';
|
|
|
|
if (this.children.length === 1 && this.children[0].type !== "element") {
|
|
s += ">" + this.children[0].toString(options) + "</" + this.name + ">";
|
|
} else if (this.children.length) {
|
|
s += ">" + linebreak;
|
|
|
|
var childIndent = indent + (options && options.compressed ? "" : " ");
|
|
|
|
for (var i = 0, l = this.children.length; i < l; i++) {
|
|
s +=
|
|
this.children[i].toStringWithIndent(childIndent, options) + linebreak;
|
|
}
|
|
|
|
s += indent + "</" + this.name + ">";
|
|
} else if (options && options.html) {
|
|
var whiteList = [
|
|
"area",
|
|
"base",
|
|
"br",
|
|
"col",
|
|
"embed",
|
|
"frame",
|
|
"hr",
|
|
"img",
|
|
"input",
|
|
"keygen",
|
|
"link",
|
|
"menuitem",
|
|
"meta",
|
|
"param",
|
|
"source",
|
|
"track",
|
|
"wbr",
|
|
];
|
|
if (whiteList.indexOf(this.name) !== -1) s += "/>";
|
|
else s += "></" + this.name + ">";
|
|
} else {
|
|
s += "/>";
|
|
}
|
|
|
|
return s;
|
|
};
|
|
|
|
// Alternative XML nodes
|
|
|
|
function XmlTextNode(text) {
|
|
this.text = text;
|
|
}
|
|
|
|
XmlTextNode.prototype.toString = function (options) {
|
|
return formatText(escapeXML(this.text), options);
|
|
};
|
|
|
|
XmlTextNode.prototype.toStringWithIndent = function (indent, options) {
|
|
return indent + this.toString(options);
|
|
};
|
|
|
|
function XmlCDataNode(cdata) {
|
|
this.cdata = cdata;
|
|
}
|
|
|
|
XmlCDataNode.prototype.toString = function (options) {
|
|
return "<![CDATA[" + formatText(this.cdata, options) + "]]>";
|
|
};
|
|
|
|
XmlCDataNode.prototype.toStringWithIndent = function (indent, options) {
|
|
return indent + this.toString(options);
|
|
};
|
|
|
|
function XmlCommentNode(comment) {
|
|
this.comment = comment;
|
|
}
|
|
|
|
XmlCommentNode.prototype.toString = function (options) {
|
|
return "<!--" + formatText(escapeXML(this.comment), options) + "-->";
|
|
};
|
|
|
|
XmlCommentNode.prototype.toStringWithIndent = function (indent, options) {
|
|
return indent + this.toString(options);
|
|
};
|
|
|
|
// Node type tag
|
|
|
|
XmlElement.prototype.type = "element";
|
|
XmlTextNode.prototype.type = "text";
|
|
XmlCDataNode.prototype.type = "cdata";
|
|
XmlCommentNode.prototype.type = "comment";
|
|
|
|
/**
|
|
* XmlDocument is the class we expose to the user; it uses the sax parser to create a hierarchy
|
|
* of XmlElements.
|
|
*/
|
|
function XmlDocument(xml) {
|
|
xml && (xml = xml.toString().trim());
|
|
|
|
if (!xml) throw new Error("No XML to parse!");
|
|
|
|
// Stores doctype (if defined)
|
|
this.doctype = "";
|
|
|
|
// Expose the parser to the other delegates while the parser is running
|
|
this.parser = sax.parser(true); // strict
|
|
addParserEvents(this.parser);
|
|
|
|
// We'll use the file-scoped "delegates" var to remember what elements we're currently
|
|
// parsing; they will push and pop off the stack as we get deeper into the XML hierarchy.
|
|
// It's safe to use a global because JS is single-threaded.
|
|
delegates = [this];
|
|
|
|
try {
|
|
this.parser.write(xml);
|
|
} finally {
|
|
// Remove the parser as it is no longer needed and should not be exposed to clients
|
|
delete this.parser;
|
|
}
|
|
}
|
|
|
|
// make XmlDocument inherit XmlElement's methods
|
|
extend(XmlDocument.prototype, XmlElement.prototype);
|
|
|
|
XmlDocument.prototype._opentag = function (tag) {
|
|
if (typeof this.children === "undefined")
|
|
// the first tag we encounter should be the root - we'll "become" the root XmlElement
|
|
XmlElement.call(this, tag);
|
|
// all other tags will be the root element's children
|
|
else XmlElement.prototype._opentag.apply(this, arguments);
|
|
};
|
|
|
|
XmlDocument.prototype._doctype = function (doctype) {
|
|
this.doctype += doctype;
|
|
};
|
|
|
|
// file-scoped global stack of delegates
|
|
var delegates = null;
|
|
|
|
/*
|
|
* Helper functions
|
|
*/
|
|
|
|
function addParserEvents(parser) {
|
|
parser.onopentag = parser_opentag;
|
|
parser.onclosetag = parser_closetag;
|
|
parser.ontext = parser_text;
|
|
parser.oncdata = parser_cdata;
|
|
parser.oncomment = parser_comment;
|
|
parser.ondoctype = parser_doctype;
|
|
parser.onerror = parser_error;
|
|
}
|
|
|
|
// create these closures and cache them by keeping them file-scoped
|
|
function parser_opentag() {
|
|
delegates[0] && delegates[0]._opentag.apply(delegates[0], arguments);
|
|
}
|
|
function parser_closetag() {
|
|
delegates[0] && delegates[0]._closetag.apply(delegates[0], arguments);
|
|
}
|
|
function parser_text() {
|
|
delegates[0] && delegates[0]._text.apply(delegates[0], arguments);
|
|
}
|
|
function parser_cdata() {
|
|
delegates[0] && delegates[0]._cdata.apply(delegates[0], arguments);
|
|
}
|
|
function parser_comment() {
|
|
delegates[0] && delegates[0]._comment.apply(delegates[0], arguments);
|
|
}
|
|
function parser_doctype() {
|
|
delegates[0] && delegates[0]._doctype.apply(delegates[0], arguments);
|
|
}
|
|
function parser_error() {
|
|
delegates[0] && delegates[0]._error.apply(delegates[0], arguments);
|
|
}
|
|
|
|
// a relatively standard extend method
|
|
function extend(destination, source) {
|
|
for (var prop in source)
|
|
if (source.hasOwnProperty(prop)) destination[prop] = source[prop];
|
|
}
|
|
|
|
// escapes XML entities like "<", "&", etc.
|
|
function escapeXML(value) {
|
|
return value
|
|
.toString()
|
|
.replace(/&/g, "&")
|
|
.replace(/</g, "<")
|
|
.replace(/>/g, ">")
|
|
.replace(/'/g, "'")
|
|
.replace(/"/g, """);
|
|
}
|
|
|
|
// formats some text for debugging given a few options
|
|
function formatText(text, options) {
|
|
var finalText = text;
|
|
|
|
if (options && options.trimmed && text.length > 25) {
|
|
finalText = finalText.substring(0, 25).trim() + "…";
|
|
}
|
|
|
|
if (!(options && options.preserveWhitespace)) {
|
|
finalText = finalText.trim();
|
|
}
|
|
|
|
return finalText;
|
|
}
|
|
|
|
// Are we being used in a Node-like environment?
|
|
if (
|
|
typeof module !== "undefined" &&
|
|
module.exports &&
|
|
!global.xmldocAssumeBrowser
|
|
) {
|
|
module.exports.XmlDocument = XmlDocument;
|
|
module.exports.XmlElement = XmlElement;
|
|
module.exports.XmlTextNode = XmlTextNode;
|
|
module.exports.XmlCDataNode = XmlCDataNode;
|
|
module.exports.XmlCommentNode = XmlCommentNode;
|
|
} else {
|
|
this.XmlDocument = XmlDocument;
|
|
this.XmlElement = XmlElement;
|
|
this.XmlTextNode = XmlTextNode;
|
|
this.XmlCDataNode = XmlCDataNode;
|
|
this.XmlCommentNode = XmlCommentNode;
|
|
}
|
|
})();
|
|
|