turtlestitch/xml.js

442 wiersze
11 KiB
JavaScript

/*
xml.js
a simple XML DOM, encoder and parser for morphic.js
written by Jens Mönig
jens@moenig.org
Copyright (C) 2014 by Jens Mönig
This file is part of Snap!.
Snap! is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
published by the Free Software Foundation, either version 3 of
the License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
prerequisites:
--------------
needs morphic.js
hierarchy
---------
the following tree lists all constructors hierarchically,
indentation indicating inheritance. Refer to this list to get a
contextual overview:
Node*
XML_Element
ReadStream
* defined in morphic.js
toc
---
the following list shows the order in which all constructors are
defined. Use this list to locate code in this document:
ReadStream
XML_Element
credits
-------
Nathan Dinsmore contributed to the design and implemented a first
working version of a complete XMLSerializer. I have taken much of the
overall design and many of the functions and methods in this file from
Nathan's fine original prototype.
*/
/*global modules, isString, detect, Node, isNil*/
// Global stuff ////////////////////////////////////////////////////////
modules.xml = '2014-January-09';
// Declarations
var ReadStream;
var XML_Element;
// ReadStream ////////////////////////////////////////////////////////////
// I am a sequential reading interface to an Array or String
// ReadStream instance creation:
function ReadStream(arrayOrString) {
this.contents = arrayOrString || '';
this.index = 0;
}
// ReadStream constants:
ReadStream.prototype.space = /[\s]/;
// ReadStream accessing:
ReadStream.prototype.next = function (count) {
var element, start;
if (count === undefined) {
element = this.contents[this.index];
this.index += 1;
return element;
}
start = this.index;
this.index += count;
return this.contents.slice(start, this.index);
};
ReadStream.prototype.peek = function () {
return this.contents[this.index];
};
ReadStream.prototype.skip = function (count) {
this.index += count || 1;
};
ReadStream.prototype.atEnd = function () {
return this.index > (this.contents.length - 1);
};
// ReadStream accessing String contents:
ReadStream.prototype.upTo = function (regex) {
var i, start;
if (!isString(this.contents)) {return ''; }
i = this.contents.substr(this.index).search(regex);
if (i === -1) {
return '';
}
start = this.index;
this.index += i;
return this.contents.substring(start, this.index);
};
ReadStream.prototype.peekUpTo = function (regex) {
if (!isString(this.contents)) {return ''; }
var i = this.contents.substr(this.index).search(regex);
if (i === -1) {
return '';
}
return this.contents.substring(this.index, this.index + i);
};
ReadStream.prototype.skipSpace = function () {
if (!isString(this.contents)) {return ''; }
var ch = this.peek();
while (this.space.test(ch) && ch !== '') {
this.skip();
ch = this.peek();
}
};
ReadStream.prototype.word = function () {
var i, start;
if (!isString(this.contents)) {return ''; }
i = this.contents.substr(this.index).search(/[\s\>\/\=]|$/);
if (i === -1) {
return '';
}
start = this.index;
this.index += i;
return this.contents.substring(start, this.index);
};
// XML_Element ///////////////////////////////////////////////////////////
/*
I am a DOM-Node which can encode itself to as well as parse itself
from a well-formed XML string. Note that there is no separate parser
object, all the parsing can be done in a single object.
*/
// XML_Element inherits from Node:
XML_Element.prototype = new Node();
XML_Element.prototype.constructor = XML_Element;
XML_Element.uber = Node.prototype;
// XML_Element preferences settings:
XML_Element.prototype.indentation = ' ';
// XML_Element instance creation:
function XML_Element(tag, contents, parent) {
this.init(tag, contents, parent);
}
XML_Element.prototype.init = function (tag, contents, parent) {
// additional properties:
this.tag = tag || 'unnamed';
this.attributes = {};
this.contents = contents || '';
// initialize inherited properties:
XML_Element.uber.init.call(this);
// override inherited properties
if (parent instanceof XML_Element) {
parent.addChild(this);
}
};
// XML_Element DOM navigation: (aside from what's inherited from Node)
XML_Element.prototype.require = function (tagName) {
// answer the first direct child with the specified tagName, or throw
// an error if it doesn't exist
var child = this.childNamed(tagName);
if (!child) {
throw new Error('Missing required element <' + tagName + '>!');
}
return child;
};
XML_Element.prototype.childNamed = function (tagName) {
// answer the first direct child with the specified tagName, or null
return detect(
this.children,
function (child) {return child.tag === tagName; }
);
};
XML_Element.prototype.childrenNamed = function (tagName) {
// answer all direct children with the specified tagName
return this.children.filter(
function (child) {return child.tag === tagName; }
);
};
XML_Element.prototype.parentNamed = function (tagName) {
// including myself
if (this.tag === tagName) {
return this;
}
if (!this.parent) {
return null;
}
return this.parent.parentNamed(tagName);
};
// XML_Element output:
XML_Element.prototype.toString = function (isFormatted, indentationLevel) {
var result = '',
indent = '',
level = indentationLevel || 0,
key,
i;
// spaces for indentation, if any
if (isFormatted) {
for (i = 0; i < level; i += 1) {
indent += this.indentation;
}
result += indent;
}
// opening tag
result += ('<' + this.tag);
// attributes, if any
for (key in this.attributes) {
if (Object.prototype.hasOwnProperty.call(this.attributes, key)
&& this.attributes[key]) {
result += ' ' + key + '="' + this.attributes[key] + '"';
}
}
// contents, subnodes, and closing tag
if (!this.contents.length && !this.children.length) {
result += '/>';
} else {
result += '>';
result += this.contents;
this.children.forEach(function (element) {
if (isFormatted) {
result += '\n';
}
result += element.toString(isFormatted, level + 1);
});
if (isFormatted && this.children.length) {
result += ('\n' + indent);
}
result += '</' + this.tag + '>';
}
return result;
};
XML_Element.prototype.escape = function (string, ignoreQuotes) {
var src = isNil(string) ? '' : string.toString(),
result = '',
i,
ch;
for (i = 0; i < src.length; i += 1) {
ch = src[i];
switch (ch) {
case '\'':
result += '&apos;';
break;
case '\"':
result += ignoreQuotes ? ch : '&quot;';
break;
case '<':
result += '&lt;';
break;
case '>':
result += '&gt;';
break;
case '&':
result += '&amp;';
break;
case '\n': // escape CR b/c of export to URL feature
result += '&#xD;';
break;
case '~': // escape tilde b/c it's overloaded in serializer.store()
result += '&#126;';
break;
default:
result += ch;
}
}
return result;
};
XML_Element.prototype.unescape = function (string) {
var stream = new ReadStream(string),
result = '',
ch,
esc;
function nextPut(str) {
result += str;
stream.upTo(';');
stream.skip();
}
while (!stream.atEnd()) {
ch = stream.next();
if (ch === '&') {
esc = stream.peekUpTo(';');
switch (esc) {
case 'apos':
nextPut('\'');
break;
case 'quot':
nextPut('\"');
break;
case 'lt':
nextPut('<');
break;
case 'gt':
nextPut('>');
break;
case 'amp':
nextPut('&');
break;
case '#xD':
nextPut('\n');
break;
case '#126':
nextPut('~');
break;
default:
result += ch;
}
} else {
result += ch;
}
}
return result;
};
// XML_Element parsing:
XML_Element.prototype.parseString = function (string) {
var stream = new ReadStream(string);
stream.upTo('<');
stream.skip();
this.parseStream(stream);
};
XML_Element.prototype.parseStream = function (stream) {
var key,
value,
ch,
child;
// tag:
this.tag = stream.word();
stream.skipSpace();
// attributes:
ch = stream.peek();
while (ch !== '>' && ch !== '/') {
key = stream.word();
stream.skipSpace();
if (stream.next() !== '=') {
throw new Error('Expected "=" after attribute name');
}
stream.skipSpace();
ch = stream.next();
if (ch !== '"' && ch !== "'") {
throw new Error(
'Expected single- or double-quoted attribute value'
);
}
value = stream.upTo(ch);
stream.skip(1);
stream.skipSpace();
this.attributes[key] = this.unescape(value);
ch = stream.peek();
}
// empty tag:
if (stream.peek() === '/') {
stream.skip();
if (stream.next() !== '>') {
throw new Error('Expected ">" after "/" in empty tag');
}
return;
}
if (stream.next() !== '>') {
throw new Error('Expected ">" after tag name and attributes');
}
// contents and children
while (!stream.atEnd()) {
ch = stream.next();
if (ch === '<') {
if (stream.peek() === '/') { // closing tag
stream.skip();
if (stream.word() !== this.tag) {
throw new Error('Expected to close ' + this.tag);
}
stream.upTo('>');
stream.skip();
this.contents = this.unescape(this.contents);
return;
}
child = new XML_Element(null, null, this);
child.parseStream(stream);
} else {
this.contents += ch;
}
}
};