4020 lines
93 KiB
JavaScript
4020 lines
93 KiB
JavaScript
(function (global, factory) {
|
|
typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) :
|
|
typeof define === 'function' && define.amd ? define(['exports'], factory) :
|
|
(global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.ModdleXML = {}));
|
|
})(this, (function (exports) { 'use strict';
|
|
|
|
/**
|
|
* Flatten array, one level deep.
|
|
*
|
|
* @param {Array<?>} arr
|
|
*
|
|
* @return {Array<?>}
|
|
*/
|
|
|
|
const nativeToString = Object.prototype.toString;
|
|
const nativeHasOwnProperty = Object.prototype.hasOwnProperty;
|
|
|
|
function isUndefined$1(obj) {
|
|
return obj === undefined;
|
|
}
|
|
|
|
function isArray(obj) {
|
|
return nativeToString.call(obj) === '[object Array]';
|
|
}
|
|
|
|
function isObject(obj) {
|
|
return nativeToString.call(obj) === '[object Object]';
|
|
}
|
|
|
|
function isFunction(obj) {
|
|
const tag = nativeToString.call(obj);
|
|
|
|
return (
|
|
tag === '[object Function]' ||
|
|
tag === '[object AsyncFunction]' ||
|
|
tag === '[object GeneratorFunction]' ||
|
|
tag === '[object AsyncGeneratorFunction]' ||
|
|
tag === '[object Proxy]'
|
|
);
|
|
}
|
|
|
|
function isString(obj) {
|
|
return nativeToString.call(obj) === '[object String]';
|
|
}
|
|
|
|
/**
|
|
* Return true, if target owns a property with the given key.
|
|
*
|
|
* @param {Object} target
|
|
* @param {String} key
|
|
*
|
|
* @return {Boolean}
|
|
*/
|
|
function has(target, key) {
|
|
return nativeHasOwnProperty.call(target, key);
|
|
}
|
|
|
|
/**
|
|
* Find element in collection.
|
|
*
|
|
* @param {Array|Object} collection
|
|
* @param {Function|Object} matcher
|
|
*
|
|
* @return {Object}
|
|
*/
|
|
function find(collection, matcher) {
|
|
|
|
matcher = toMatcher(matcher);
|
|
|
|
let match;
|
|
|
|
forEach(collection, function(val, key) {
|
|
if (matcher(val, key)) {
|
|
match = val;
|
|
|
|
return false;
|
|
}
|
|
});
|
|
|
|
return match;
|
|
|
|
}
|
|
|
|
|
|
/**
|
|
* Find element index in collection.
|
|
*
|
|
* @param {Array|Object} collection
|
|
* @param {Function} matcher
|
|
*
|
|
* @return {Object}
|
|
*/
|
|
function findIndex(collection, matcher) {
|
|
|
|
matcher = toMatcher(matcher);
|
|
|
|
let idx = isArray(collection) ? -1 : undefined;
|
|
|
|
forEach(collection, function(val, key) {
|
|
if (matcher(val, key)) {
|
|
idx = key;
|
|
|
|
return false;
|
|
}
|
|
});
|
|
|
|
return idx;
|
|
}
|
|
|
|
|
|
/**
|
|
* Find element in collection.
|
|
*
|
|
* @param {Array|Object} collection
|
|
* @param {Function} matcher
|
|
*
|
|
* @return {Array} result
|
|
*/
|
|
function filter(collection, matcher) {
|
|
|
|
let result = [];
|
|
|
|
forEach(collection, function(val, key) {
|
|
if (matcher(val, key)) {
|
|
result.push(val);
|
|
}
|
|
});
|
|
|
|
return result;
|
|
}
|
|
|
|
|
|
/**
|
|
* Iterate over collection; returning something
|
|
* (non-undefined) will stop iteration.
|
|
*
|
|
* @param {Array|Object} collection
|
|
* @param {Function} iterator
|
|
*
|
|
* @return {Object} return result that stopped the iteration
|
|
*/
|
|
function forEach(collection, iterator) {
|
|
|
|
let val,
|
|
result;
|
|
|
|
if (isUndefined$1(collection)) {
|
|
return;
|
|
}
|
|
|
|
const convertKey = isArray(collection) ? toNum : identity;
|
|
|
|
for (let key in collection) {
|
|
|
|
if (has(collection, key)) {
|
|
val = collection[key];
|
|
|
|
result = iterator(val, convertKey(key));
|
|
|
|
if (result === false) {
|
|
return val;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
function toMatcher(matcher) {
|
|
return isFunction(matcher) ? matcher : (e) => {
|
|
return e === matcher;
|
|
};
|
|
}
|
|
|
|
|
|
function identity(arg) {
|
|
return arg;
|
|
}
|
|
|
|
function toNum(arg) {
|
|
return Number(arg);
|
|
}
|
|
|
|
/**
|
|
* Bind function against target <this>.
|
|
*
|
|
* @param {Function} fn
|
|
* @param {Object} target
|
|
*
|
|
* @return {Function} bound function
|
|
*/
|
|
function bind(fn, target) {
|
|
return fn.bind(target);
|
|
}
|
|
|
|
/**
|
|
* Convenience wrapper for `Object.assign`.
|
|
*
|
|
* @param {Object} target
|
|
* @param {...Object} others
|
|
*
|
|
* @return {Object} the target
|
|
*/
|
|
function assign(target, ...others) {
|
|
return Object.assign(target, ...others);
|
|
}
|
|
|
|
/**
|
|
* Pick given properties from the target object.
|
|
*
|
|
* @param {Object} target
|
|
* @param {Array} properties
|
|
*
|
|
* @return {Object} target
|
|
*/
|
|
function pick(target, properties) {
|
|
|
|
let result = {};
|
|
|
|
let obj = Object(target);
|
|
|
|
forEach(properties, function(prop) {
|
|
|
|
if (prop in obj) {
|
|
result[prop] = target[prop];
|
|
}
|
|
});
|
|
|
|
return result;
|
|
}
|
|
|
|
var fromCharCode = String.fromCharCode;
|
|
|
|
var hasOwnProperty = Object.prototype.hasOwnProperty;
|
|
|
|
var ENTITY_PATTERN = /&#(\d+);|&#x([0-9a-f]+);|&(\w+);/ig;
|
|
|
|
var ENTITY_MAPPING = {
|
|
'amp': '&',
|
|
'apos': '\'',
|
|
'gt': '>',
|
|
'lt': '<',
|
|
'quot': '"'
|
|
};
|
|
|
|
// map UPPERCASE variants of supported special chars
|
|
Object.keys(ENTITY_MAPPING).forEach(function(k) {
|
|
ENTITY_MAPPING[k.toUpperCase()] = ENTITY_MAPPING[k];
|
|
});
|
|
|
|
|
|
function replaceEntities(_, d, x, z) {
|
|
|
|
// reserved names, i.e.
|
|
if (z) {
|
|
if (hasOwnProperty.call(ENTITY_MAPPING, z)) {
|
|
return ENTITY_MAPPING[z];
|
|
} else {
|
|
|
|
// fall back to original value
|
|
return '&' + z + ';';
|
|
}
|
|
}
|
|
|
|
// decimal encoded char
|
|
if (d) {
|
|
return fromCharCode(d);
|
|
}
|
|
|
|
// hex encoded char
|
|
return fromCharCode(parseInt(x, 16));
|
|
}
|
|
|
|
|
|
/**
|
|
* A basic entity decoder that can decode a minimal
|
|
* sub-set of reserved names (&) as well as
|
|
* hex (ય) and decimal (ӏ) encoded characters.
|
|
*
|
|
* @param {string} str
|
|
*
|
|
* @return {string} decoded string
|
|
*/
|
|
function decodeEntities(s) {
|
|
if (s.length > 3 && s.indexOf('&') !== -1) {
|
|
return s.replace(ENTITY_PATTERN, replaceEntities);
|
|
}
|
|
|
|
return s;
|
|
}
|
|
|
|
var XSI_URI = 'http://www.w3.org/2001/XMLSchema-instance';
|
|
var XSI_PREFIX = 'xsi';
|
|
var XSI_TYPE$1 = 'xsi:type';
|
|
|
|
var NON_WHITESPACE_OUTSIDE_ROOT_NODE = 'non-whitespace outside of root node';
|
|
|
|
function error$1(msg) {
|
|
return new Error(msg);
|
|
}
|
|
|
|
function missingNamespaceForPrefix(prefix) {
|
|
return 'missing namespace for prefix <' + prefix + '>';
|
|
}
|
|
|
|
function getter(getFn) {
|
|
return {
|
|
'get': getFn,
|
|
'enumerable': true
|
|
};
|
|
}
|
|
|
|
function cloneNsMatrix(nsMatrix) {
|
|
var clone = {}, key;
|
|
for (key in nsMatrix) {
|
|
clone[key] = nsMatrix[key];
|
|
}
|
|
return clone;
|
|
}
|
|
|
|
function uriPrefix(prefix) {
|
|
return prefix + '$uri';
|
|
}
|
|
|
|
function buildNsMatrix(nsUriToPrefix) {
|
|
var nsMatrix = {},
|
|
uri,
|
|
prefix;
|
|
|
|
for (uri in nsUriToPrefix) {
|
|
prefix = nsUriToPrefix[uri];
|
|
nsMatrix[prefix] = prefix;
|
|
nsMatrix[uriPrefix(prefix)] = uri;
|
|
}
|
|
|
|
return nsMatrix;
|
|
}
|
|
|
|
function noopGetContext() {
|
|
return { 'line': 0, 'column': 0 };
|
|
}
|
|
|
|
function throwFunc(err) {
|
|
throw err;
|
|
}
|
|
|
|
/**
|
|
* Creates a new parser with the given options.
|
|
*
|
|
* @constructor
|
|
*
|
|
* @param {!Object<string, ?>=} options
|
|
*/
|
|
function Parser(options) {
|
|
|
|
if (!this) {
|
|
return new Parser(options);
|
|
}
|
|
|
|
var proxy = options && options['proxy'];
|
|
|
|
var onText,
|
|
onOpenTag,
|
|
onCloseTag,
|
|
onCDATA,
|
|
onError = throwFunc,
|
|
onWarning,
|
|
onComment,
|
|
onQuestion,
|
|
onAttention;
|
|
|
|
var getContext = noopGetContext;
|
|
|
|
/**
|
|
* Do we need to parse the current elements attributes for namespaces?
|
|
*
|
|
* @type {boolean}
|
|
*/
|
|
var maybeNS = false;
|
|
|
|
/**
|
|
* Do we process namespaces at all?
|
|
*
|
|
* @type {boolean}
|
|
*/
|
|
var isNamespace = false;
|
|
|
|
/**
|
|
* The caught error returned on parse end
|
|
*
|
|
* @type {Error}
|
|
*/
|
|
var returnError = null;
|
|
|
|
/**
|
|
* Should we stop parsing?
|
|
*
|
|
* @type {boolean}
|
|
*/
|
|
var parseStop = false;
|
|
|
|
/**
|
|
* A map of { uri: prefix } used by the parser.
|
|
*
|
|
* This map will ensure we can normalize prefixes during processing;
|
|
* for each uri, only one prefix will be exposed to the handlers.
|
|
*
|
|
* @type {!Object<string, string>}}
|
|
*/
|
|
var nsUriToPrefix;
|
|
|
|
/**
|
|
* Handle parse error.
|
|
*
|
|
* @param {string|Error} err
|
|
*/
|
|
function handleError(err) {
|
|
if (!(err instanceof Error)) {
|
|
err = error$1(err);
|
|
}
|
|
|
|
returnError = err;
|
|
|
|
onError(err, getContext);
|
|
}
|
|
|
|
/**
|
|
* Handle parse error.
|
|
*
|
|
* @param {string|Error} err
|
|
*/
|
|
function handleWarning(err) {
|
|
|
|
if (!onWarning) {
|
|
return;
|
|
}
|
|
|
|
if (!(err instanceof Error)) {
|
|
err = error$1(err);
|
|
}
|
|
|
|
onWarning(err, getContext);
|
|
}
|
|
|
|
/**
|
|
* Register parse listener.
|
|
*
|
|
* @param {string} name
|
|
* @param {Function} cb
|
|
*
|
|
* @return {Parser}
|
|
*/
|
|
this['on'] = function(name, cb) {
|
|
|
|
if (typeof cb !== 'function') {
|
|
throw error$1('required args <name, cb>');
|
|
}
|
|
|
|
switch (name) {
|
|
case 'openTag': onOpenTag = cb; break;
|
|
case 'text': onText = cb; break;
|
|
case 'closeTag': onCloseTag = cb; break;
|
|
case 'error': onError = cb; break;
|
|
case 'warn': onWarning = cb; break;
|
|
case 'cdata': onCDATA = cb; break;
|
|
case 'attention': onAttention = cb; break; // <!XXXXX zzzz="eeee">
|
|
case 'question': onQuestion = cb; break; // <? .... ?>
|
|
case 'comment': onComment = cb; break;
|
|
default:
|
|
throw error$1('unsupported event: ' + name);
|
|
}
|
|
|
|
return this;
|
|
};
|
|
|
|
/**
|
|
* Set the namespace to prefix mapping.
|
|
*
|
|
* @example
|
|
*
|
|
* parser.ns({
|
|
* 'http://foo': 'foo',
|
|
* 'http://bar': 'bar'
|
|
* });
|
|
*
|
|
* @param {!Object<string, string>} nsMap
|
|
*
|
|
* @return {Parser}
|
|
*/
|
|
this['ns'] = function(nsMap) {
|
|
|
|
if (typeof nsMap === 'undefined') {
|
|
nsMap = {};
|
|
}
|
|
|
|
if (typeof nsMap !== 'object') {
|
|
throw error$1('required args <nsMap={}>');
|
|
}
|
|
|
|
var _nsUriToPrefix = {}, k;
|
|
|
|
for (k in nsMap) {
|
|
_nsUriToPrefix[k] = nsMap[k];
|
|
}
|
|
|
|
// FORCE default mapping for schema instance
|
|
_nsUriToPrefix[XSI_URI] = XSI_PREFIX;
|
|
|
|
isNamespace = true;
|
|
nsUriToPrefix = _nsUriToPrefix;
|
|
|
|
return this;
|
|
};
|
|
|
|
/**
|
|
* Parse xml string.
|
|
*
|
|
* @param {string} xml
|
|
*
|
|
* @return {Error} returnError, if not thrown
|
|
*/
|
|
this['parse'] = function(xml) {
|
|
if (typeof xml !== 'string') {
|
|
throw error$1('required args <xml=string>');
|
|
}
|
|
|
|
returnError = null;
|
|
|
|
parse(xml);
|
|
|
|
getContext = noopGetContext;
|
|
parseStop = false;
|
|
|
|
return returnError;
|
|
};
|
|
|
|
/**
|
|
* Stop parsing.
|
|
*/
|
|
this['stop'] = function() {
|
|
parseStop = true;
|
|
};
|
|
|
|
/**
|
|
* Parse string, invoking configured listeners on element.
|
|
*
|
|
* @param {string} xml
|
|
*/
|
|
function parse(xml) {
|
|
var nsMatrixStack = isNamespace ? [] : null,
|
|
nsMatrix = isNamespace ? buildNsMatrix(nsUriToPrefix) : null,
|
|
_nsMatrix,
|
|
nodeStack = [],
|
|
anonymousNsCount = 0,
|
|
tagStart = false,
|
|
tagEnd = false,
|
|
i = 0, j = 0,
|
|
x, y, q, w, v,
|
|
xmlns,
|
|
elementName,
|
|
_elementName,
|
|
elementProxy
|
|
;
|
|
|
|
var attrsString = '',
|
|
attrsStart = 0,
|
|
cachedAttrs // false = parsed with errors, null = needs parsing
|
|
;
|
|
|
|
/**
|
|
* Parse attributes on demand and returns the parsed attributes.
|
|
*
|
|
* Return semantics: (1) `false` on attribute parse error,
|
|
* (2) object hash on extracted attrs.
|
|
*
|
|
* @return {boolean|Object}
|
|
*/
|
|
function getAttrs() {
|
|
if (cachedAttrs !== null) {
|
|
return cachedAttrs;
|
|
}
|
|
|
|
var nsUri,
|
|
nsUriPrefix,
|
|
nsName,
|
|
defaultAlias = isNamespace && nsMatrix['xmlns'],
|
|
attrList = isNamespace && maybeNS ? [] : null,
|
|
i = attrsStart,
|
|
s = attrsString,
|
|
l = s.length,
|
|
hasNewMatrix,
|
|
newalias,
|
|
value,
|
|
alias,
|
|
name,
|
|
attrs = {},
|
|
seenAttrs = {},
|
|
skipAttr,
|
|
w,
|
|
j;
|
|
|
|
parseAttr:
|
|
for (; i < l; i++) {
|
|
skipAttr = false;
|
|
w = s.charCodeAt(i);
|
|
|
|
if (w === 32 || (w < 14 && w > 8)) { // WHITESPACE={ \f\n\r\t\v}
|
|
continue;
|
|
}
|
|
|
|
// wait for non whitespace character
|
|
if (w < 65 || w > 122 || (w > 90 && w < 97)) {
|
|
if (w !== 95 && w !== 58) { // char 95"_" 58":"
|
|
handleWarning('illegal first char attribute name');
|
|
skipAttr = true;
|
|
}
|
|
}
|
|
|
|
// parse attribute name
|
|
for (j = i + 1; j < l; j++) {
|
|
w = s.charCodeAt(j);
|
|
|
|
if (
|
|
w > 96 && w < 123 ||
|
|
w > 64 && w < 91 ||
|
|
w > 47 && w < 59 ||
|
|
w === 46 || // '.'
|
|
w === 45 || // '-'
|
|
w === 95 // '_'
|
|
) {
|
|
continue;
|
|
}
|
|
|
|
// unexpected whitespace
|
|
if (w === 32 || (w < 14 && w > 8)) { // WHITESPACE
|
|
handleWarning('missing attribute value');
|
|
i = j;
|
|
|
|
continue parseAttr;
|
|
}
|
|
|
|
// expected "="
|
|
if (w === 61) { // "=" == 61
|
|
break;
|
|
}
|
|
|
|
handleWarning('illegal attribute name char');
|
|
skipAttr = true;
|
|
}
|
|
|
|
name = s.substring(i, j);
|
|
|
|
if (name === 'xmlns:xmlns') {
|
|
handleWarning('illegal declaration of xmlns');
|
|
skipAttr = true;
|
|
}
|
|
|
|
w = s.charCodeAt(j + 1);
|
|
|
|
if (w === 34) { // '"'
|
|
j = s.indexOf('"', i = j + 2);
|
|
|
|
if (j === -1) {
|
|
j = s.indexOf('\'', i);
|
|
|
|
if (j !== -1) {
|
|
handleWarning('attribute value quote missmatch');
|
|
skipAttr = true;
|
|
}
|
|
}
|
|
|
|
} else if (w === 39) { // "'"
|
|
j = s.indexOf('\'', i = j + 2);
|
|
|
|
if (j === -1) {
|
|
j = s.indexOf('"', i);
|
|
|
|
if (j !== -1) {
|
|
handleWarning('attribute value quote missmatch');
|
|
skipAttr = true;
|
|
}
|
|
}
|
|
|
|
} else {
|
|
handleWarning('missing attribute value quotes');
|
|
skipAttr = true;
|
|
|
|
// skip to next space
|
|
for (j = j + 1; j < l; j++) {
|
|
w = s.charCodeAt(j + 1);
|
|
|
|
if (w === 32 || (w < 14 && w > 8)) { // WHITESPACE
|
|
break;
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
if (j === -1) {
|
|
handleWarning('missing closing quotes');
|
|
|
|
j = l;
|
|
skipAttr = true;
|
|
}
|
|
|
|
if (!skipAttr) {
|
|
value = s.substring(i, j);
|
|
}
|
|
|
|
i = j;
|
|
|
|
// ensure SPACE follows attribute
|
|
// skip illegal content otherwise
|
|
// example a="b"c
|
|
for (; j + 1 < l; j++) {
|
|
w = s.charCodeAt(j + 1);
|
|
|
|
if (w === 32 || (w < 14 && w > 8)) { // WHITESPACE
|
|
break;
|
|
}
|
|
|
|
// FIRST ILLEGAL CHAR
|
|
if (i === j) {
|
|
handleWarning('illegal character after attribute end');
|
|
skipAttr = true;
|
|
}
|
|
}
|
|
|
|
// advance cursor to next attribute
|
|
i = j + 1;
|
|
|
|
if (skipAttr) {
|
|
continue parseAttr;
|
|
}
|
|
|
|
// check attribute re-declaration
|
|
if (name in seenAttrs) {
|
|
handleWarning('attribute <' + name + '> already defined');
|
|
continue;
|
|
}
|
|
|
|
seenAttrs[name] = true;
|
|
|
|
if (!isNamespace) {
|
|
attrs[name] = value;
|
|
continue;
|
|
}
|
|
|
|
// try to extract namespace information
|
|
if (maybeNS) {
|
|
newalias = (
|
|
name === 'xmlns'
|
|
? 'xmlns'
|
|
: (name.charCodeAt(0) === 120 && name.substr(0, 6) === 'xmlns:')
|
|
? name.substr(6)
|
|
: null
|
|
);
|
|
|
|
// handle xmlns(:alias) assignment
|
|
if (newalias !== null) {
|
|
nsUri = decodeEntities(value);
|
|
nsUriPrefix = uriPrefix(newalias);
|
|
|
|
alias = nsUriToPrefix[nsUri];
|
|
|
|
if (!alias) {
|
|
|
|
// no prefix defined or prefix collision
|
|
if (
|
|
(newalias === 'xmlns') ||
|
|
(nsUriPrefix in nsMatrix && nsMatrix[nsUriPrefix] !== nsUri)
|
|
) {
|
|
|
|
// alocate free ns prefix
|
|
do {
|
|
alias = 'ns' + (anonymousNsCount++);
|
|
} while (typeof nsMatrix[alias] !== 'undefined');
|
|
} else {
|
|
alias = newalias;
|
|
}
|
|
|
|
nsUriToPrefix[nsUri] = alias;
|
|
}
|
|
|
|
if (nsMatrix[newalias] !== alias) {
|
|
if (!hasNewMatrix) {
|
|
nsMatrix = cloneNsMatrix(nsMatrix);
|
|
hasNewMatrix = true;
|
|
}
|
|
|
|
nsMatrix[newalias] = alias;
|
|
if (newalias === 'xmlns') {
|
|
nsMatrix[uriPrefix(alias)] = nsUri;
|
|
defaultAlias = alias;
|
|
}
|
|
|
|
nsMatrix[nsUriPrefix] = nsUri;
|
|
}
|
|
|
|
// expose xmlns(:asd)="..." in attributes
|
|
attrs[name] = value;
|
|
continue;
|
|
}
|
|
|
|
// collect attributes until all namespace
|
|
// declarations are processed
|
|
attrList.push(name, value);
|
|
continue;
|
|
|
|
} /** end if (maybeNs) */
|
|
|
|
// handle attributes on element without
|
|
// namespace declarations
|
|
w = name.indexOf(':');
|
|
if (w === -1) {
|
|
attrs[name] = value;
|
|
continue;
|
|
}
|
|
|
|
// normalize ns attribute name
|
|
if (!(nsName = nsMatrix[name.substring(0, w)])) {
|
|
handleWarning(missingNamespaceForPrefix(name.substring(0, w)));
|
|
continue;
|
|
}
|
|
|
|
name = defaultAlias === nsName
|
|
? name.substr(w + 1)
|
|
: nsName + name.substr(w);
|
|
|
|
// end: normalize ns attribute name
|
|
|
|
// normalize xsi:type ns attribute value
|
|
if (name === XSI_TYPE$1) {
|
|
w = value.indexOf(':');
|
|
|
|
if (w !== -1) {
|
|
nsName = value.substring(0, w);
|
|
|
|
// handle default prefixes, i.e. xs:String gracefully
|
|
nsName = nsMatrix[nsName] || nsName;
|
|
value = nsName + value.substring(w);
|
|
} else {
|
|
value = defaultAlias + ':' + value;
|
|
}
|
|
}
|
|
|
|
// end: normalize xsi:type ns attribute value
|
|
|
|
attrs[name] = value;
|
|
}
|
|
|
|
|
|
// handle deferred, possibly namespaced attributes
|
|
if (maybeNS) {
|
|
|
|
// normalize captured attributes
|
|
for (i = 0, l = attrList.length; i < l; i++) {
|
|
|
|
name = attrList[i++];
|
|
value = attrList[i];
|
|
|
|
w = name.indexOf(':');
|
|
|
|
if (w !== -1) {
|
|
|
|
// normalize ns attribute name
|
|
if (!(nsName = nsMatrix[name.substring(0, w)])) {
|
|
handleWarning(missingNamespaceForPrefix(name.substring(0, w)));
|
|
continue;
|
|
}
|
|
|
|
name = defaultAlias === nsName
|
|
? name.substr(w + 1)
|
|
: nsName + name.substr(w);
|
|
|
|
// end: normalize ns attribute name
|
|
|
|
// normalize xsi:type ns attribute value
|
|
if (name === XSI_TYPE$1) {
|
|
w = value.indexOf(':');
|
|
|
|
if (w !== -1) {
|
|
nsName = value.substring(0, w);
|
|
|
|
// handle default prefixes, i.e. xs:String gracefully
|
|
nsName = nsMatrix[nsName] || nsName;
|
|
value = nsName + value.substring(w);
|
|
} else {
|
|
value = defaultAlias + ':' + value;
|
|
}
|
|
}
|
|
|
|
// end: normalize xsi:type ns attribute value
|
|
}
|
|
|
|
attrs[name] = value;
|
|
}
|
|
|
|
// end: normalize captured attributes
|
|
}
|
|
|
|
return cachedAttrs = attrs;
|
|
}
|
|
|
|
/**
|
|
* Extract the parse context { line, column, part }
|
|
* from the current parser position.
|
|
*
|
|
* @return {Object} parse context
|
|
*/
|
|
function getParseContext() {
|
|
var splitsRe = /(\r\n|\r|\n)/g;
|
|
|
|
var line = 0;
|
|
var column = 0;
|
|
var startOfLine = 0;
|
|
var endOfLine = j;
|
|
var match;
|
|
var data;
|
|
|
|
while (i >= startOfLine) {
|
|
|
|
match = splitsRe.exec(xml);
|
|
|
|
if (!match) {
|
|
break;
|
|
}
|
|
|
|
// end of line = (break idx + break chars)
|
|
endOfLine = match[0].length + match.index;
|
|
|
|
if (endOfLine > i) {
|
|
break;
|
|
}
|
|
|
|
// advance to next line
|
|
line += 1;
|
|
|
|
startOfLine = endOfLine;
|
|
}
|
|
|
|
// EOF errors
|
|
if (i == -1) {
|
|
column = endOfLine;
|
|
data = xml.substring(j);
|
|
} else
|
|
|
|
// start errors
|
|
if (j === 0) {
|
|
data = xml.substring(j, i);
|
|
}
|
|
|
|
// other errors
|
|
else {
|
|
column = i - startOfLine;
|
|
data = (j == -1 ? xml.substring(i) : xml.substring(i, j + 1));
|
|
}
|
|
|
|
return {
|
|
'data': data,
|
|
'line': line,
|
|
'column': column
|
|
};
|
|
}
|
|
|
|
getContext = getParseContext;
|
|
|
|
|
|
if (proxy) {
|
|
elementProxy = Object.create({}, {
|
|
'name': getter(function() {
|
|
return elementName;
|
|
}),
|
|
'originalName': getter(function() {
|
|
return _elementName;
|
|
}),
|
|
'attrs': getter(getAttrs),
|
|
'ns': getter(function() {
|
|
return nsMatrix;
|
|
})
|
|
});
|
|
}
|
|
|
|
// actual parse logic
|
|
while (j !== -1) {
|
|
|
|
if (xml.charCodeAt(j) === 60) { // "<"
|
|
i = j;
|
|
} else {
|
|
i = xml.indexOf('<', j);
|
|
}
|
|
|
|
// parse end
|
|
if (i === -1) {
|
|
if (nodeStack.length) {
|
|
return handleError('unexpected end of file');
|
|
}
|
|
|
|
if (j === 0) {
|
|
return handleError('missing start tag');
|
|
}
|
|
|
|
if (j < xml.length) {
|
|
if (xml.substring(j).trim()) {
|
|
handleWarning(NON_WHITESPACE_OUTSIDE_ROOT_NODE);
|
|
}
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
// parse text
|
|
if (j !== i) {
|
|
|
|
if (nodeStack.length) {
|
|
if (onText) {
|
|
onText(xml.substring(j, i), decodeEntities, getContext);
|
|
|
|
if (parseStop) {
|
|
return;
|
|
}
|
|
}
|
|
} else {
|
|
if (xml.substring(j, i).trim()) {
|
|
handleWarning(NON_WHITESPACE_OUTSIDE_ROOT_NODE);
|
|
|
|
if (parseStop) {
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
w = xml.charCodeAt(i+1);
|
|
|
|
// parse comments + CDATA
|
|
if (w === 33) { // "!"
|
|
q = xml.charCodeAt(i+2);
|
|
|
|
// CDATA section
|
|
if (q === 91 && xml.substr(i + 3, 6) === 'CDATA[') { // 91 == "["
|
|
j = xml.indexOf(']]>', i);
|
|
if (j === -1) {
|
|
return handleError('unclosed cdata');
|
|
}
|
|
|
|
if (onCDATA) {
|
|
onCDATA(xml.substring(i + 9, j), getContext);
|
|
if (parseStop) {
|
|
return;
|
|
}
|
|
}
|
|
|
|
j += 3;
|
|
continue;
|
|
}
|
|
|
|
// comment
|
|
if (q === 45 && xml.charCodeAt(i + 3) === 45) { // 45 == "-"
|
|
j = xml.indexOf('-->', i);
|
|
if (j === -1) {
|
|
return handleError('unclosed comment');
|
|
}
|
|
|
|
|
|
if (onComment) {
|
|
onComment(xml.substring(i + 4, j), decodeEntities, getContext);
|
|
if (parseStop) {
|
|
return;
|
|
}
|
|
}
|
|
|
|
j += 3;
|
|
continue;
|
|
}
|
|
}
|
|
|
|
// parse question <? ... ?>
|
|
if (w === 63) { // "?"
|
|
j = xml.indexOf('?>', i);
|
|
if (j === -1) {
|
|
return handleError('unclosed question');
|
|
}
|
|
|
|
if (onQuestion) {
|
|
onQuestion(xml.substring(i, j + 2), getContext);
|
|
if (parseStop) {
|
|
return;
|
|
}
|
|
}
|
|
|
|
j += 2;
|
|
continue;
|
|
}
|
|
|
|
// find matching closing tag for attention or standard tags
|
|
// for that we must skip through attribute values
|
|
// (enclosed in single or double quotes)
|
|
for (x = i + 1; ; x++) {
|
|
v = xml.charCodeAt(x);
|
|
if (isNaN(v)) {
|
|
j = -1;
|
|
return handleError('unclosed tag');
|
|
}
|
|
|
|
// [10] AttValue ::= '"' ([^<&"] | Reference)* '"' | "'" ([^<&'] | Reference)* "'"
|
|
// skips the quoted string
|
|
// (double quotes) does not appear in a literal enclosed by (double quotes)
|
|
// (single quote) does not appear in a literal enclosed by (single quote)
|
|
if (v === 34) { // '"'
|
|
q = xml.indexOf('"', x + 1);
|
|
x = q !== -1 ? q : x;
|
|
} else if (v === 39) { // "'"
|
|
q = xml.indexOf("'", x + 1);
|
|
x = q !== -1 ? q : x;
|
|
} else if (v === 62) { // '>'
|
|
j = x;
|
|
break;
|
|
}
|
|
}
|
|
|
|
|
|
// parse attention <! ...>
|
|
// previously comment and CDATA have already been parsed
|
|
if (w === 33) { // "!"
|
|
|
|
if (onAttention) {
|
|
onAttention(xml.substring(i, j + 1), decodeEntities, getContext);
|
|
if (parseStop) {
|
|
return;
|
|
}
|
|
}
|
|
|
|
j += 1;
|
|
continue;
|
|
}
|
|
|
|
// don't process attributes;
|
|
// there are none
|
|
cachedAttrs = {};
|
|
|
|
// if (xml.charCodeAt(i+1) === 47) { // </...
|
|
if (w === 47) { // </...
|
|
tagStart = false;
|
|
tagEnd = true;
|
|
|
|
if (!nodeStack.length) {
|
|
return handleError('missing open tag');
|
|
}
|
|
|
|
// verify open <-> close tag match
|
|
x = elementName = nodeStack.pop();
|
|
q = i + 2 + x.length;
|
|
|
|
if (xml.substring(i + 2, q) !== x) {
|
|
return handleError('closing tag mismatch');
|
|
}
|
|
|
|
// verify chars in close tag
|
|
for (; q < j; q++) {
|
|
w = xml.charCodeAt(q);
|
|
|
|
if (w === 32 || (w > 8 && w < 14)) { // \f\n\r\t\v space
|
|
continue;
|
|
}
|
|
|
|
return handleError('close tag');
|
|
}
|
|
|
|
} else {
|
|
if (xml.charCodeAt(j - 1) === 47) { // .../>
|
|
x = elementName = xml.substring(i + 1, j - 1);
|
|
|
|
tagStart = true;
|
|
tagEnd = true;
|
|
|
|
} else {
|
|
x = elementName = xml.substring(i + 1, j);
|
|
|
|
tagStart = true;
|
|
tagEnd = false;
|
|
}
|
|
|
|
if (!(w > 96 && w < 123 || w > 64 && w < 91 || w === 95 || w === 58)) { // char 95"_" 58":"
|
|
return handleError('illegal first char nodeName');
|
|
}
|
|
|
|
for (q = 1, y = x.length; q < y; q++) {
|
|
w = x.charCodeAt(q);
|
|
|
|
if (w > 96 && w < 123 || w > 64 && w < 91 || w > 47 && w < 59 || w === 45 || w === 95 || w == 46) {
|
|
continue;
|
|
}
|
|
|
|
if (w === 32 || (w < 14 && w > 8)) { // \f\n\r\t\v space
|
|
elementName = x.substring(0, q);
|
|
|
|
// maybe there are attributes
|
|
cachedAttrs = null;
|
|
break;
|
|
}
|
|
|
|
return handleError('invalid nodeName');
|
|
}
|
|
|
|
if (!tagEnd) {
|
|
nodeStack.push(elementName);
|
|
}
|
|
}
|
|
|
|
if (isNamespace) {
|
|
|
|
_nsMatrix = nsMatrix;
|
|
|
|
if (tagStart) {
|
|
|
|
// remember old namespace
|
|
// unless we're self-closing
|
|
if (!tagEnd) {
|
|
nsMatrixStack.push(_nsMatrix);
|
|
}
|
|
|
|
if (cachedAttrs === null) {
|
|
|
|
// quick check, whether there may be namespace
|
|
// declarations on the node; if that is the case
|
|
// we need to eagerly parse the node attributes
|
|
if ((maybeNS = x.indexOf('xmlns', q) !== -1)) {
|
|
attrsStart = q;
|
|
attrsString = x;
|
|
|
|
getAttrs();
|
|
|
|
maybeNS = false;
|
|
}
|
|
}
|
|
}
|
|
|
|
_elementName = elementName;
|
|
|
|
w = elementName.indexOf(':');
|
|
if (w !== -1) {
|
|
xmlns = nsMatrix[elementName.substring(0, w)];
|
|
|
|
// prefix given; namespace must exist
|
|
if (!xmlns) {
|
|
return handleError('missing namespace on <' + _elementName + '>');
|
|
}
|
|
|
|
elementName = elementName.substr(w + 1);
|
|
} else {
|
|
xmlns = nsMatrix['xmlns'];
|
|
|
|
// if no default namespace is defined,
|
|
// we'll import the element as anonymous.
|
|
//
|
|
// it is up to users to correct that to the document defined
|
|
// targetNamespace, or whatever their undersanding of the
|
|
// XML spec mandates.
|
|
}
|
|
|
|
// adjust namespace prefixs as configured
|
|
if (xmlns) {
|
|
elementName = xmlns + ':' + elementName;
|
|
}
|
|
|
|
}
|
|
|
|
if (tagStart) {
|
|
attrsStart = q;
|
|
attrsString = x;
|
|
|
|
if (onOpenTag) {
|
|
if (proxy) {
|
|
onOpenTag(elementProxy, decodeEntities, tagEnd, getContext);
|
|
} else {
|
|
onOpenTag(elementName, getAttrs, decodeEntities, tagEnd, getContext);
|
|
}
|
|
|
|
if (parseStop) {
|
|
return;
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
if (tagEnd) {
|
|
|
|
if (onCloseTag) {
|
|
onCloseTag(proxy ? elementProxy : elementName, decodeEntities, tagStart, getContext);
|
|
|
|
if (parseStop) {
|
|
return;
|
|
}
|
|
}
|
|
|
|
// restore old namespace
|
|
if (isNamespace) {
|
|
if (!tagStart) {
|
|
nsMatrix = nsMatrixStack.pop();
|
|
} else {
|
|
nsMatrix = _nsMatrix;
|
|
}
|
|
}
|
|
}
|
|
|
|
j += 1;
|
|
}
|
|
} /** end parse */
|
|
|
|
}
|
|
|
|
/**
|
|
* Moddle base element.
|
|
*/
|
|
function Base() { }
|
|
|
|
Base.prototype.get = function(name) {
|
|
return this.$model.properties.get(this, name);
|
|
};
|
|
|
|
Base.prototype.set = function(name, value) {
|
|
this.$model.properties.set(this, name, value);
|
|
};
|
|
|
|
/**
|
|
* A model element factory.
|
|
*
|
|
* @param {Moddle} model
|
|
* @param {Properties} properties
|
|
*/
|
|
function Factory(model, properties) {
|
|
this.model = model;
|
|
this.properties = properties;
|
|
}
|
|
|
|
|
|
Factory.prototype.createType = function(descriptor) {
|
|
|
|
var model = this.model;
|
|
|
|
var props = this.properties,
|
|
prototype = Object.create(Base.prototype);
|
|
|
|
// initialize default values
|
|
forEach(descriptor.properties, function(p) {
|
|
if (!p.isMany && p.default !== undefined) {
|
|
prototype[p.name] = p.default;
|
|
}
|
|
});
|
|
|
|
props.defineModel(prototype, model);
|
|
props.defineDescriptor(prototype, descriptor);
|
|
|
|
var name = descriptor.ns.name;
|
|
|
|
/**
|
|
* The new type constructor
|
|
*/
|
|
function ModdleElement(attrs) {
|
|
props.define(this, '$type', { value: name, enumerable: true });
|
|
props.define(this, '$attrs', { value: {} });
|
|
props.define(this, '$parent', { writable: true });
|
|
|
|
forEach(attrs, bind(function(val, key) {
|
|
this.set(key, val);
|
|
}, this));
|
|
}
|
|
|
|
ModdleElement.prototype = prototype;
|
|
|
|
ModdleElement.hasType = prototype.$instanceOf = this.model.hasType;
|
|
|
|
// static links
|
|
props.defineModel(ModdleElement, model);
|
|
props.defineDescriptor(ModdleElement, descriptor);
|
|
|
|
return ModdleElement;
|
|
};
|
|
|
|
/**
|
|
* Built-in moddle types
|
|
*/
|
|
var BUILTINS = {
|
|
String: true,
|
|
Boolean: true,
|
|
Integer: true,
|
|
Real: true,
|
|
Element: true
|
|
};
|
|
|
|
/**
|
|
* Converters for built in types from string representations
|
|
*/
|
|
var TYPE_CONVERTERS = {
|
|
String: function(s) { return s; },
|
|
Boolean: function(s) { return s === 'true'; },
|
|
Integer: function(s) { return parseInt(s, 10); },
|
|
Real: function(s) { return parseFloat(s); }
|
|
};
|
|
|
|
/**
|
|
* Convert a type to its real representation
|
|
*/
|
|
function coerceType(type, value) {
|
|
|
|
var converter = TYPE_CONVERTERS[type];
|
|
|
|
if (converter) {
|
|
return converter(value);
|
|
} else {
|
|
return value;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Return whether the given type is built-in
|
|
*/
|
|
function isBuiltIn(type) {
|
|
return !!BUILTINS[type];
|
|
}
|
|
|
|
/**
|
|
* Return whether the given type is simple
|
|
*/
|
|
function isSimple(type) {
|
|
return !!TYPE_CONVERTERS[type];
|
|
}
|
|
|
|
/**
|
|
* Parses a namespaced attribute name of the form (ns:)localName to an object,
|
|
* given a default prefix to assume in case no explicit namespace is given.
|
|
*
|
|
* @param {String} name
|
|
* @param {String} [defaultPrefix] the default prefix to take, if none is present.
|
|
*
|
|
* @return {Object} the parsed name
|
|
*/
|
|
function parseName(name, defaultPrefix) {
|
|
var parts = name.split(/:/),
|
|
localName, prefix;
|
|
|
|
// no prefix (i.e. only local name)
|
|
if (parts.length === 1) {
|
|
localName = name;
|
|
prefix = defaultPrefix;
|
|
} else
|
|
|
|
// prefix + local name
|
|
if (parts.length === 2) {
|
|
localName = parts[1];
|
|
prefix = parts[0];
|
|
} else {
|
|
throw new Error('expected <prefix:localName> or <localName>, got ' + name);
|
|
}
|
|
|
|
name = (prefix ? prefix + ':' : '') + localName;
|
|
|
|
return {
|
|
name: name,
|
|
prefix: prefix,
|
|
localName: localName
|
|
};
|
|
}
|
|
|
|
/**
|
|
* A utility to build element descriptors.
|
|
*/
|
|
function DescriptorBuilder(nameNs) {
|
|
this.ns = nameNs;
|
|
this.name = nameNs.name;
|
|
this.allTypes = [];
|
|
this.allTypesByName = {};
|
|
this.properties = [];
|
|
this.propertiesByName = {};
|
|
}
|
|
|
|
|
|
DescriptorBuilder.prototype.build = function() {
|
|
return pick(this, [
|
|
'ns',
|
|
'name',
|
|
'allTypes',
|
|
'allTypesByName',
|
|
'properties',
|
|
'propertiesByName',
|
|
'bodyProperty',
|
|
'idProperty'
|
|
]);
|
|
};
|
|
|
|
/**
|
|
* Add property at given index.
|
|
*
|
|
* @param {Object} p
|
|
* @param {Number} [idx]
|
|
* @param {Boolean} [validate=true]
|
|
*/
|
|
DescriptorBuilder.prototype.addProperty = function(p, idx, validate) {
|
|
|
|
if (typeof idx === 'boolean') {
|
|
validate = idx;
|
|
idx = undefined;
|
|
}
|
|
|
|
this.addNamedProperty(p, validate !== false);
|
|
|
|
var properties = this.properties;
|
|
|
|
if (idx !== undefined) {
|
|
properties.splice(idx, 0, p);
|
|
} else {
|
|
properties.push(p);
|
|
}
|
|
};
|
|
|
|
|
|
DescriptorBuilder.prototype.replaceProperty = function(oldProperty, newProperty, replace) {
|
|
var oldNameNs = oldProperty.ns;
|
|
|
|
var props = this.properties,
|
|
propertiesByName = this.propertiesByName,
|
|
rename = oldProperty.name !== newProperty.name;
|
|
|
|
if (oldProperty.isId) {
|
|
if (!newProperty.isId) {
|
|
throw new Error(
|
|
'property <' + newProperty.ns.name + '> must be id property ' +
|
|
'to refine <' + oldProperty.ns.name + '>');
|
|
}
|
|
|
|
this.setIdProperty(newProperty, false);
|
|
}
|
|
|
|
if (oldProperty.isBody) {
|
|
|
|
if (!newProperty.isBody) {
|
|
throw new Error(
|
|
'property <' + newProperty.ns.name + '> must be body property ' +
|
|
'to refine <' + oldProperty.ns.name + '>');
|
|
}
|
|
|
|
// TODO: Check compatibility
|
|
this.setBodyProperty(newProperty, false);
|
|
}
|
|
|
|
// validate existence and get location of old property
|
|
var idx = props.indexOf(oldProperty);
|
|
if (idx === -1) {
|
|
throw new Error('property <' + oldNameNs.name + '> not found in property list');
|
|
}
|
|
|
|
// remove old property
|
|
props.splice(idx, 1);
|
|
|
|
// replacing the named property is intentional
|
|
//
|
|
// * validate only if this is a "rename" operation
|
|
// * add at specific index unless we "replace"
|
|
//
|
|
this.addProperty(newProperty, replace ? undefined : idx, rename);
|
|
|
|
// make new property available under old name
|
|
propertiesByName[oldNameNs.name] = propertiesByName[oldNameNs.localName] = newProperty;
|
|
};
|
|
|
|
|
|
DescriptorBuilder.prototype.redefineProperty = function(p, targetPropertyName, replace) {
|
|
|
|
var nsPrefix = p.ns.prefix;
|
|
var parts = targetPropertyName.split('#');
|
|
|
|
var name = parseName(parts[0], nsPrefix);
|
|
var attrName = parseName(parts[1], name.prefix).name;
|
|
|
|
var redefinedProperty = this.propertiesByName[attrName];
|
|
if (!redefinedProperty) {
|
|
throw new Error('refined property <' + attrName + '> not found');
|
|
} else {
|
|
this.replaceProperty(redefinedProperty, p, replace);
|
|
}
|
|
|
|
delete p.redefines;
|
|
};
|
|
|
|
DescriptorBuilder.prototype.addNamedProperty = function(p, validate) {
|
|
var ns = p.ns,
|
|
propsByName = this.propertiesByName;
|
|
|
|
if (validate) {
|
|
this.assertNotDefined(p, ns.name);
|
|
this.assertNotDefined(p, ns.localName);
|
|
}
|
|
|
|
propsByName[ns.name] = propsByName[ns.localName] = p;
|
|
};
|
|
|
|
DescriptorBuilder.prototype.removeNamedProperty = function(p) {
|
|
var ns = p.ns,
|
|
propsByName = this.propertiesByName;
|
|
|
|
delete propsByName[ns.name];
|
|
delete propsByName[ns.localName];
|
|
};
|
|
|
|
DescriptorBuilder.prototype.setBodyProperty = function(p, validate) {
|
|
|
|
if (validate && this.bodyProperty) {
|
|
throw new Error(
|
|
'body property defined multiple times ' +
|
|
'(<' + this.bodyProperty.ns.name + '>, <' + p.ns.name + '>)');
|
|
}
|
|
|
|
this.bodyProperty = p;
|
|
};
|
|
|
|
DescriptorBuilder.prototype.setIdProperty = function(p, validate) {
|
|
|
|
if (validate && this.idProperty) {
|
|
throw new Error(
|
|
'id property defined multiple times ' +
|
|
'(<' + this.idProperty.ns.name + '>, <' + p.ns.name + '>)');
|
|
}
|
|
|
|
this.idProperty = p;
|
|
};
|
|
|
|
DescriptorBuilder.prototype.assertNotDefined = function(p, name) {
|
|
var propertyName = p.name,
|
|
definedProperty = this.propertiesByName[propertyName];
|
|
|
|
if (definedProperty) {
|
|
throw new Error(
|
|
'property <' + propertyName + '> already defined; ' +
|
|
'override of <' + definedProperty.definedBy.ns.name + '#' + definedProperty.ns.name + '> by ' +
|
|
'<' + p.definedBy.ns.name + '#' + p.ns.name + '> not allowed without redefines');
|
|
}
|
|
};
|
|
|
|
DescriptorBuilder.prototype.hasProperty = function(name) {
|
|
return this.propertiesByName[name];
|
|
};
|
|
|
|
DescriptorBuilder.prototype.addTrait = function(t, inherited) {
|
|
|
|
var typesByName = this.allTypesByName,
|
|
types = this.allTypes;
|
|
|
|
var typeName = t.name;
|
|
|
|
if (typeName in typesByName) {
|
|
return;
|
|
}
|
|
|
|
forEach(t.properties, bind(function(p) {
|
|
|
|
// clone property to allow extensions
|
|
p = assign({}, p, {
|
|
name: p.ns.localName,
|
|
inherited: inherited
|
|
});
|
|
|
|
Object.defineProperty(p, 'definedBy', {
|
|
value: t
|
|
});
|
|
|
|
var replaces = p.replaces,
|
|
redefines = p.redefines;
|
|
|
|
// add replace/redefine support
|
|
if (replaces || redefines) {
|
|
this.redefineProperty(p, replaces || redefines, replaces);
|
|
} else {
|
|
if (p.isBody) {
|
|
this.setBodyProperty(p);
|
|
}
|
|
if (p.isId) {
|
|
this.setIdProperty(p);
|
|
}
|
|
this.addProperty(p);
|
|
}
|
|
}, this));
|
|
|
|
types.push(t);
|
|
typesByName[typeName] = t;
|
|
};
|
|
|
|
/**
|
|
* A registry of Moddle packages.
|
|
*
|
|
* @param {Array<Package>} packages
|
|
* @param {Properties} properties
|
|
*/
|
|
function Registry(packages, properties) {
|
|
this.packageMap = {};
|
|
this.typeMap = {};
|
|
|
|
this.packages = [];
|
|
|
|
this.properties = properties;
|
|
|
|
forEach(packages, bind(this.registerPackage, this));
|
|
}
|
|
|
|
|
|
Registry.prototype.getPackage = function(uriOrPrefix) {
|
|
return this.packageMap[uriOrPrefix];
|
|
};
|
|
|
|
Registry.prototype.getPackages = function() {
|
|
return this.packages;
|
|
};
|
|
|
|
|
|
Registry.prototype.registerPackage = function(pkg) {
|
|
|
|
// copy package
|
|
pkg = assign({}, pkg);
|
|
|
|
var pkgMap = this.packageMap;
|
|
|
|
ensureAvailable(pkgMap, pkg, 'prefix');
|
|
ensureAvailable(pkgMap, pkg, 'uri');
|
|
|
|
// register types
|
|
forEach(pkg.types, bind(function(descriptor) {
|
|
this.registerType(descriptor, pkg);
|
|
}, this));
|
|
|
|
pkgMap[pkg.uri] = pkgMap[pkg.prefix] = pkg;
|
|
this.packages.push(pkg);
|
|
};
|
|
|
|
|
|
/**
|
|
* Register a type from a specific package with us
|
|
*/
|
|
Registry.prototype.registerType = function(type, pkg) {
|
|
|
|
type = assign({}, type, {
|
|
superClass: (type.superClass || []).slice(),
|
|
extends: (type.extends || []).slice(),
|
|
properties: (type.properties || []).slice(),
|
|
meta: assign((type.meta || {}))
|
|
});
|
|
|
|
var ns = parseName(type.name, pkg.prefix),
|
|
name = ns.name,
|
|
propertiesByName = {};
|
|
|
|
// parse properties
|
|
forEach(type.properties, bind(function(p) {
|
|
|
|
// namespace property names
|
|
var propertyNs = parseName(p.name, ns.prefix),
|
|
propertyName = propertyNs.name;
|
|
|
|
// namespace property types
|
|
if (!isBuiltIn(p.type)) {
|
|
p.type = parseName(p.type, propertyNs.prefix).name;
|
|
}
|
|
|
|
assign(p, {
|
|
ns: propertyNs,
|
|
name: propertyName
|
|
});
|
|
|
|
propertiesByName[propertyName] = p;
|
|
}, this));
|
|
|
|
// update ns + name
|
|
assign(type, {
|
|
ns: ns,
|
|
name: name,
|
|
propertiesByName: propertiesByName
|
|
});
|
|
|
|
forEach(type.extends, bind(function(extendsName) {
|
|
var extended = this.typeMap[extendsName];
|
|
|
|
extended.traits = extended.traits || [];
|
|
extended.traits.push(name);
|
|
}, this));
|
|
|
|
// link to package
|
|
this.definePackage(type, pkg);
|
|
|
|
// register
|
|
this.typeMap[name] = type;
|
|
};
|
|
|
|
|
|
/**
|
|
* Traverse the type hierarchy from bottom to top,
|
|
* calling iterator with (type, inherited) for all elements in
|
|
* the inheritance chain.
|
|
*
|
|
* @param {Object} nsName
|
|
* @param {Function} iterator
|
|
* @param {Boolean} [trait=false]
|
|
*/
|
|
Registry.prototype.mapTypes = function(nsName, iterator, trait) {
|
|
|
|
var type = isBuiltIn(nsName.name) ? { name: nsName.name } : this.typeMap[nsName.name];
|
|
|
|
var self = this;
|
|
|
|
/**
|
|
* Traverse the selected trait.
|
|
*
|
|
* @param {String} cls
|
|
*/
|
|
function traverseTrait(cls) {
|
|
return traverseSuper(cls, true);
|
|
}
|
|
|
|
/**
|
|
* Traverse the selected super type or trait
|
|
*
|
|
* @param {String} cls
|
|
* @param {Boolean} [trait=false]
|
|
*/
|
|
function traverseSuper(cls, trait) {
|
|
var parentNs = parseName(cls, isBuiltIn(cls) ? '' : nsName.prefix);
|
|
self.mapTypes(parentNs, iterator, trait);
|
|
}
|
|
|
|
if (!type) {
|
|
throw new Error('unknown type <' + nsName.name + '>');
|
|
}
|
|
|
|
forEach(type.superClass, trait ? traverseTrait : traverseSuper);
|
|
|
|
// call iterator with (type, inherited=!trait)
|
|
iterator(type, !trait);
|
|
|
|
forEach(type.traits, traverseTrait);
|
|
};
|
|
|
|
|
|
/**
|
|
* Returns the effective descriptor for a type.
|
|
*
|
|
* @param {String} type the namespaced name (ns:localName) of the type
|
|
*
|
|
* @return {Descriptor} the resulting effective descriptor
|
|
*/
|
|
Registry.prototype.getEffectiveDescriptor = function(name) {
|
|
|
|
var nsName = parseName(name);
|
|
|
|
var builder = new DescriptorBuilder(nsName);
|
|
|
|
this.mapTypes(nsName, function(type, inherited) {
|
|
builder.addTrait(type, inherited);
|
|
});
|
|
|
|
var descriptor = builder.build();
|
|
|
|
// define package link
|
|
this.definePackage(descriptor, descriptor.allTypes[descriptor.allTypes.length - 1].$pkg);
|
|
|
|
return descriptor;
|
|
};
|
|
|
|
|
|
Registry.prototype.definePackage = function(target, pkg) {
|
|
this.properties.define(target, '$pkg', { value: pkg });
|
|
};
|
|
|
|
|
|
|
|
// helpers ////////////////////////////
|
|
|
|
function ensureAvailable(packageMap, pkg, identifierKey) {
|
|
|
|
var value = pkg[identifierKey];
|
|
|
|
if (value in packageMap) {
|
|
throw new Error('package with ' + identifierKey + ' <' + value + '> already defined');
|
|
}
|
|
}
|
|
|
|
/**
|
|
* A utility that gets and sets properties of model elements.
|
|
*
|
|
* @param {Model} model
|
|
*/
|
|
function Properties(model) {
|
|
this.model = model;
|
|
}
|
|
|
|
|
|
/**
|
|
* Sets a named property on the target element.
|
|
* If the value is undefined, the property gets deleted.
|
|
*
|
|
* @param {Object} target
|
|
* @param {String} name
|
|
* @param {Object} value
|
|
*/
|
|
Properties.prototype.set = function(target, name, value) {
|
|
|
|
if (!isString(name) || !name.length) {
|
|
throw new TypeError('property name must be a non-empty string');
|
|
}
|
|
|
|
var property = this.model.getPropertyDescriptor(target, name);
|
|
|
|
var propertyName = property && property.name;
|
|
|
|
if (isUndefined(value)) {
|
|
|
|
// unset the property, if the specified value is undefined;
|
|
// delete from $attrs (for extensions) or the target itself
|
|
if (property) {
|
|
delete target[propertyName];
|
|
} else {
|
|
delete target.$attrs[name];
|
|
}
|
|
} else {
|
|
|
|
// set the property, defining well defined properties on the fly
|
|
// or simply updating them in target.$attrs (for extensions)
|
|
if (property) {
|
|
if (propertyName in target) {
|
|
target[propertyName] = value;
|
|
} else {
|
|
defineProperty(target, property, value);
|
|
}
|
|
} else {
|
|
target.$attrs[name] = value;
|
|
}
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Returns the named property of the given element
|
|
*
|
|
* @param {Object} target
|
|
* @param {String} name
|
|
*
|
|
* @return {Object}
|
|
*/
|
|
Properties.prototype.get = function(target, name) {
|
|
|
|
var property = this.model.getPropertyDescriptor(target, name);
|
|
|
|
if (!property) {
|
|
return target.$attrs[name];
|
|
}
|
|
|
|
var propertyName = property.name;
|
|
|
|
// check if access to collection property and lazily initialize it
|
|
if (!target[propertyName] && property.isMany) {
|
|
defineProperty(target, property, []);
|
|
}
|
|
|
|
return target[propertyName];
|
|
};
|
|
|
|
|
|
/**
|
|
* Define a property on the target element
|
|
*
|
|
* @param {Object} target
|
|
* @param {String} name
|
|
* @param {Object} options
|
|
*/
|
|
Properties.prototype.define = function(target, name, options) {
|
|
|
|
if (!options.writable) {
|
|
|
|
var value = options.value;
|
|
|
|
// use getters for read-only variables to support ES6 proxies
|
|
// cf. https://github.com/bpmn-io/internal-docs/issues/386
|
|
options = assign({}, options, {
|
|
get: function() { return value; }
|
|
});
|
|
|
|
delete options.value;
|
|
}
|
|
|
|
Object.defineProperty(target, name, options);
|
|
};
|
|
|
|
|
|
/**
|
|
* Define the descriptor for an element
|
|
*/
|
|
Properties.prototype.defineDescriptor = function(target, descriptor) {
|
|
this.define(target, '$descriptor', { value: descriptor });
|
|
};
|
|
|
|
/**
|
|
* Define the model for an element
|
|
*/
|
|
Properties.prototype.defineModel = function(target, model) {
|
|
this.define(target, '$model', { value: model });
|
|
};
|
|
|
|
|
|
function isUndefined(val) {
|
|
return typeof val === 'undefined';
|
|
}
|
|
|
|
function defineProperty(target, property, value) {
|
|
Object.defineProperty(target, property.name, {
|
|
enumerable: !property.isReference,
|
|
writable: true,
|
|
value: value,
|
|
configurable: true
|
|
});
|
|
}
|
|
|
|
// Moddle implementation /////////////////////////////////////////////////
|
|
|
|
/**
|
|
* @class Moddle
|
|
*
|
|
* A model that can be used to create elements of a specific type.
|
|
*
|
|
* @example
|
|
*
|
|
* var Moddle = require('moddle');
|
|
*
|
|
* var pkg = {
|
|
* name: 'mypackage',
|
|
* prefix: 'my',
|
|
* types: [
|
|
* { name: 'Root' }
|
|
* ]
|
|
* };
|
|
*
|
|
* var moddle = new Moddle([pkg]);
|
|
*
|
|
* @param {Array<Package>} packages the packages to contain
|
|
*/
|
|
function Moddle(packages) {
|
|
|
|
this.properties = new Properties(this);
|
|
|
|
this.factory = new Factory(this, this.properties);
|
|
this.registry = new Registry(packages, this.properties);
|
|
|
|
this.typeCache = {};
|
|
}
|
|
|
|
|
|
/**
|
|
* Create an instance of the specified type.
|
|
*
|
|
* @method Moddle#create
|
|
*
|
|
* @example
|
|
*
|
|
* var foo = moddle.create('my:Foo');
|
|
* var bar = moddle.create('my:Bar', { id: 'BAR_1' });
|
|
*
|
|
* @param {String|Object} descriptor the type descriptor or name know to the model
|
|
* @param {Object} attrs a number of attributes to initialize the model instance with
|
|
* @return {Object} model instance
|
|
*/
|
|
Moddle.prototype.create = function(descriptor, attrs) {
|
|
var Type = this.getType(descriptor);
|
|
|
|
if (!Type) {
|
|
throw new Error('unknown type <' + descriptor + '>');
|
|
}
|
|
|
|
return new Type(attrs);
|
|
};
|
|
|
|
|
|
/**
|
|
* Returns the type representing a given descriptor
|
|
*
|
|
* @method Moddle#getType
|
|
*
|
|
* @example
|
|
*
|
|
* var Foo = moddle.getType('my:Foo');
|
|
* var foo = new Foo({ 'id' : 'FOO_1' });
|
|
*
|
|
* @param {String|Object} descriptor the type descriptor or name know to the model
|
|
* @return {Object} the type representing the descriptor
|
|
*/
|
|
Moddle.prototype.getType = function(descriptor) {
|
|
|
|
var cache = this.typeCache;
|
|
|
|
var name = isString(descriptor) ? descriptor : descriptor.ns.name;
|
|
|
|
var type = cache[name];
|
|
|
|
if (!type) {
|
|
descriptor = this.registry.getEffectiveDescriptor(name);
|
|
type = cache[name] = this.factory.createType(descriptor);
|
|
}
|
|
|
|
return type;
|
|
};
|
|
|
|
|
|
/**
|
|
* Creates an any-element type to be used within model instances.
|
|
*
|
|
* This can be used to create custom elements that lie outside the meta-model.
|
|
* The created element contains all the meta-data required to serialize it
|
|
* as part of meta-model elements.
|
|
*
|
|
* @method Moddle#createAny
|
|
*
|
|
* @example
|
|
*
|
|
* var foo = moddle.createAny('vendor:Foo', 'http://vendor', {
|
|
* value: 'bar'
|
|
* });
|
|
*
|
|
* var container = moddle.create('my:Container', 'http://my', {
|
|
* any: [ foo ]
|
|
* });
|
|
*
|
|
* // go ahead and serialize the stuff
|
|
*
|
|
*
|
|
* @param {String} name the name of the element
|
|
* @param {String} nsUri the namespace uri of the element
|
|
* @param {Object} [properties] a map of properties to initialize the instance with
|
|
* @return {Object} the any type instance
|
|
*/
|
|
Moddle.prototype.createAny = function(name, nsUri, properties) {
|
|
|
|
var nameNs = parseName(name);
|
|
|
|
var element = {
|
|
$type: name,
|
|
$instanceOf: function(type) {
|
|
return type === this.$type;
|
|
}
|
|
};
|
|
|
|
var descriptor = {
|
|
name: name,
|
|
isGeneric: true,
|
|
ns: {
|
|
prefix: nameNs.prefix,
|
|
localName: nameNs.localName,
|
|
uri: nsUri
|
|
}
|
|
};
|
|
|
|
this.properties.defineDescriptor(element, descriptor);
|
|
this.properties.defineModel(element, this);
|
|
this.properties.define(element, '$parent', { enumerable: false, writable: true });
|
|
this.properties.define(element, '$instanceOf', { enumerable: false, writable: true });
|
|
|
|
forEach(properties, function(a, key) {
|
|
if (isObject(a) && a.value !== undefined) {
|
|
element[a.name] = a.value;
|
|
} else {
|
|
element[key] = a;
|
|
}
|
|
});
|
|
|
|
return element;
|
|
};
|
|
|
|
/**
|
|
* Returns a registered package by uri or prefix
|
|
*
|
|
* @return {Object} the package
|
|
*/
|
|
Moddle.prototype.getPackage = function(uriOrPrefix) {
|
|
return this.registry.getPackage(uriOrPrefix);
|
|
};
|
|
|
|
/**
|
|
* Returns a snapshot of all known packages
|
|
*
|
|
* @return {Object} the package
|
|
*/
|
|
Moddle.prototype.getPackages = function() {
|
|
return this.registry.getPackages();
|
|
};
|
|
|
|
/**
|
|
* Returns the descriptor for an element
|
|
*/
|
|
Moddle.prototype.getElementDescriptor = function(element) {
|
|
return element.$descriptor;
|
|
};
|
|
|
|
/**
|
|
* Returns true if the given descriptor or instance
|
|
* represents the given type.
|
|
*
|
|
* May be applied to this, if element is omitted.
|
|
*/
|
|
Moddle.prototype.hasType = function(element, type) {
|
|
if (type === undefined) {
|
|
type = element;
|
|
element = this;
|
|
}
|
|
|
|
var descriptor = element.$model.getElementDescriptor(element);
|
|
|
|
return (type in descriptor.allTypesByName);
|
|
};
|
|
|
|
/**
|
|
* Returns the descriptor of an elements named property
|
|
*/
|
|
Moddle.prototype.getPropertyDescriptor = function(element, property) {
|
|
return this.getElementDescriptor(element).propertiesByName[property];
|
|
};
|
|
|
|
/**
|
|
* Returns a mapped type's descriptor
|
|
*/
|
|
Moddle.prototype.getTypeDescriptor = function(type) {
|
|
return this.registry.typeMap[type];
|
|
};
|
|
|
|
function hasLowerCaseAlias(pkg) {
|
|
return pkg.xml && pkg.xml.tagAlias === 'lowerCase';
|
|
}
|
|
|
|
var DEFAULT_NS_MAP = {
|
|
'xsi': 'http://www.w3.org/2001/XMLSchema-instance',
|
|
'xml': 'http://www.w3.org/XML/1998/namespace'
|
|
};
|
|
|
|
var XSI_TYPE = 'xsi:type';
|
|
|
|
function serializeFormat(element) {
|
|
return element.xml && element.xml.serialize;
|
|
}
|
|
|
|
function serializeAsType(element) {
|
|
return serializeFormat(element) === XSI_TYPE;
|
|
}
|
|
|
|
function serializeAsProperty(element) {
|
|
return serializeFormat(element) === 'property';
|
|
}
|
|
|
|
function capitalize(str) {
|
|
return str.charAt(0).toUpperCase() + str.slice(1);
|
|
}
|
|
|
|
function aliasToName(aliasNs, pkg) {
|
|
|
|
if (!hasLowerCaseAlias(pkg)) {
|
|
return aliasNs.name;
|
|
}
|
|
|
|
return aliasNs.prefix + ':' + capitalize(aliasNs.localName);
|
|
}
|
|
|
|
function prefixedToName(nameNs, pkg) {
|
|
|
|
var name = nameNs.name,
|
|
localName = nameNs.localName;
|
|
|
|
var typePrefix = pkg.xml && pkg.xml.typePrefix;
|
|
|
|
if (typePrefix && localName.indexOf(typePrefix) === 0) {
|
|
return nameNs.prefix + ':' + localName.slice(typePrefix.length);
|
|
} else {
|
|
return name;
|
|
}
|
|
}
|
|
|
|
function normalizeXsiTypeName(name, model) {
|
|
|
|
var nameNs = parseName(name);
|
|
var pkg = model.getPackage(nameNs.prefix);
|
|
|
|
return prefixedToName(nameNs, pkg);
|
|
}
|
|
|
|
function error(message) {
|
|
return new Error(message);
|
|
}
|
|
|
|
/**
|
|
* Get the moddle descriptor for a given instance or type.
|
|
*
|
|
* @param {ModdleElement|Function} element
|
|
*
|
|
* @return {Object} the moddle descriptor
|
|
*/
|
|
function getModdleDescriptor(element) {
|
|
return element.$descriptor;
|
|
}
|
|
|
|
|
|
/**
|
|
* A parse context.
|
|
*
|
|
* @class
|
|
*
|
|
* @param {Object} options
|
|
* @param {ElementHandler} options.rootHandler the root handler for parsing a document
|
|
* @param {boolean} [options.lax=false] whether or not to ignore invalid elements
|
|
*/
|
|
function Context(options) {
|
|
|
|
/**
|
|
* @property {ElementHandler} rootHandler
|
|
*/
|
|
|
|
/**
|
|
* @property {Boolean} lax
|
|
*/
|
|
|
|
assign(this, options);
|
|
|
|
this.elementsById = {};
|
|
this.references = [];
|
|
this.warnings = [];
|
|
|
|
/**
|
|
* Add an unresolved reference.
|
|
*
|
|
* @param {Object} reference
|
|
*/
|
|
this.addReference = function(reference) {
|
|
this.references.push(reference);
|
|
};
|
|
|
|
/**
|
|
* Add a processed element.
|
|
*
|
|
* @param {ModdleElement} element
|
|
*/
|
|
this.addElement = function(element) {
|
|
|
|
if (!element) {
|
|
throw error('expected element');
|
|
}
|
|
|
|
var elementsById = this.elementsById;
|
|
|
|
var descriptor = getModdleDescriptor(element);
|
|
|
|
var idProperty = descriptor.idProperty,
|
|
id;
|
|
|
|
if (idProperty) {
|
|
id = element.get(idProperty.name);
|
|
|
|
if (id) {
|
|
|
|
// for QName validation as per http://www.w3.org/TR/REC-xml/#NT-NameChar
|
|
if (!/^([a-z][\w-.]*:)?[a-z_][\w-.]*$/i.test(id)) {
|
|
throw new Error('illegal ID <' + id + '>');
|
|
}
|
|
|
|
if (elementsById[id]) {
|
|
throw error('duplicate ID <' + id + '>');
|
|
}
|
|
|
|
elementsById[id] = element;
|
|
}
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Add an import warning.
|
|
*
|
|
* @param {Object} warning
|
|
* @param {String} warning.message
|
|
* @param {Error} [warning.error]
|
|
*/
|
|
this.addWarning = function(warning) {
|
|
this.warnings.push(warning);
|
|
};
|
|
}
|
|
|
|
function BaseHandler() {}
|
|
|
|
BaseHandler.prototype.handleEnd = function() {};
|
|
BaseHandler.prototype.handleText = function() {};
|
|
BaseHandler.prototype.handleNode = function() {};
|
|
|
|
|
|
/**
|
|
* A simple pass through handler that does nothing except for
|
|
* ignoring all input it receives.
|
|
*
|
|
* This is used to ignore unknown elements and
|
|
* attributes.
|
|
*/
|
|
function NoopHandler() { }
|
|
|
|
NoopHandler.prototype = Object.create(BaseHandler.prototype);
|
|
|
|
NoopHandler.prototype.handleNode = function() {
|
|
return this;
|
|
};
|
|
|
|
function BodyHandler() {}
|
|
|
|
BodyHandler.prototype = Object.create(BaseHandler.prototype);
|
|
|
|
BodyHandler.prototype.handleText = function(text) {
|
|
this.body = (this.body || '') + text;
|
|
};
|
|
|
|
function ReferenceHandler(property, context) {
|
|
this.property = property;
|
|
this.context = context;
|
|
}
|
|
|
|
ReferenceHandler.prototype = Object.create(BodyHandler.prototype);
|
|
|
|
ReferenceHandler.prototype.handleNode = function(node) {
|
|
|
|
if (this.element) {
|
|
throw error('expected no sub nodes');
|
|
} else {
|
|
this.element = this.createReference(node);
|
|
}
|
|
|
|
return this;
|
|
};
|
|
|
|
ReferenceHandler.prototype.handleEnd = function() {
|
|
this.element.id = this.body;
|
|
};
|
|
|
|
ReferenceHandler.prototype.createReference = function(node) {
|
|
return {
|
|
property: this.property.ns.name,
|
|
id: ''
|
|
};
|
|
};
|
|
|
|
function ValueHandler(propertyDesc, element) {
|
|
this.element = element;
|
|
this.propertyDesc = propertyDesc;
|
|
}
|
|
|
|
ValueHandler.prototype = Object.create(BodyHandler.prototype);
|
|
|
|
ValueHandler.prototype.handleEnd = function() {
|
|
|
|
var value = this.body || '',
|
|
element = this.element,
|
|
propertyDesc = this.propertyDesc;
|
|
|
|
value = coerceType(propertyDesc.type, value);
|
|
|
|
if (propertyDesc.isMany) {
|
|
element.get(propertyDesc.name).push(value);
|
|
} else {
|
|
element.set(propertyDesc.name, value);
|
|
}
|
|
};
|
|
|
|
|
|
function BaseElementHandler() {}
|
|
|
|
BaseElementHandler.prototype = Object.create(BodyHandler.prototype);
|
|
|
|
BaseElementHandler.prototype.handleNode = function(node) {
|
|
var parser = this,
|
|
element = this.element;
|
|
|
|
if (!element) {
|
|
element = this.element = this.createElement(node);
|
|
|
|
this.context.addElement(element);
|
|
} else {
|
|
parser = this.handleChild(node);
|
|
}
|
|
|
|
return parser;
|
|
};
|
|
|
|
/**
|
|
* @class Reader.ElementHandler
|
|
*
|
|
*/
|
|
function ElementHandler(model, typeName, context) {
|
|
this.model = model;
|
|
this.type = model.getType(typeName);
|
|
this.context = context;
|
|
}
|
|
|
|
ElementHandler.prototype = Object.create(BaseElementHandler.prototype);
|
|
|
|
ElementHandler.prototype.addReference = function(reference) {
|
|
this.context.addReference(reference);
|
|
};
|
|
|
|
ElementHandler.prototype.handleText = function(text) {
|
|
|
|
var element = this.element,
|
|
descriptor = getModdleDescriptor(element),
|
|
bodyProperty = descriptor.bodyProperty;
|
|
|
|
if (!bodyProperty) {
|
|
throw error('unexpected body text <' + text + '>');
|
|
}
|
|
|
|
BodyHandler.prototype.handleText.call(this, text);
|
|
};
|
|
|
|
ElementHandler.prototype.handleEnd = function() {
|
|
|
|
var value = this.body,
|
|
element = this.element,
|
|
descriptor = getModdleDescriptor(element),
|
|
bodyProperty = descriptor.bodyProperty;
|
|
|
|
if (bodyProperty && value !== undefined) {
|
|
value = coerceType(bodyProperty.type, value);
|
|
element.set(bodyProperty.name, value);
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Create an instance of the model from the given node.
|
|
*
|
|
* @param {Element} node the xml node
|
|
*/
|
|
ElementHandler.prototype.createElement = function(node) {
|
|
var attributes = node.attributes,
|
|
Type = this.type,
|
|
descriptor = getModdleDescriptor(Type),
|
|
context = this.context,
|
|
instance = new Type({}),
|
|
model = this.model,
|
|
propNameNs;
|
|
|
|
forEach(attributes, function(value, name) {
|
|
|
|
var prop = descriptor.propertiesByName[name],
|
|
values;
|
|
|
|
if (prop && prop.isReference) {
|
|
|
|
if (!prop.isMany) {
|
|
context.addReference({
|
|
element: instance,
|
|
property: prop.ns.name,
|
|
id: value
|
|
});
|
|
} else {
|
|
|
|
// IDREFS: parse references as whitespace-separated list
|
|
values = value.split(' ');
|
|
|
|
forEach(values, function(v) {
|
|
context.addReference({
|
|
element: instance,
|
|
property: prop.ns.name,
|
|
id: v
|
|
});
|
|
});
|
|
}
|
|
|
|
} else {
|
|
if (prop) {
|
|
value = coerceType(prop.type, value);
|
|
} else
|
|
if (name !== 'xmlns') {
|
|
propNameNs = parseName(name, descriptor.ns.prefix);
|
|
|
|
// check whether attribute is defined in a well-known namespace
|
|
// if that is the case we emit a warning to indicate potential misuse
|
|
if (model.getPackage(propNameNs.prefix)) {
|
|
|
|
context.addWarning({
|
|
message: 'unknown attribute <' + name + '>',
|
|
element: instance,
|
|
property: name,
|
|
value: value
|
|
});
|
|
}
|
|
}
|
|
|
|
instance.set(name, value);
|
|
}
|
|
});
|
|
|
|
return instance;
|
|
};
|
|
|
|
ElementHandler.prototype.getPropertyForNode = function(node) {
|
|
|
|
var name = node.name;
|
|
var nameNs = parseName(name);
|
|
|
|
var type = this.type,
|
|
model = this.model,
|
|
descriptor = getModdleDescriptor(type);
|
|
|
|
var propertyName = nameNs.name,
|
|
property = descriptor.propertiesByName[propertyName],
|
|
elementTypeName,
|
|
elementType;
|
|
|
|
// search for properties by name first
|
|
|
|
if (property && !property.isAttr) {
|
|
|
|
if (serializeAsType(property)) {
|
|
elementTypeName = node.attributes[XSI_TYPE];
|
|
|
|
// xsi type is optional, if it does not exists the
|
|
// default type is assumed
|
|
if (elementTypeName) {
|
|
|
|
// take possible type prefixes from XML
|
|
// into account, i.e.: xsi:type="t{ActualType}"
|
|
elementTypeName = normalizeXsiTypeName(elementTypeName, model);
|
|
|
|
elementType = model.getType(elementTypeName);
|
|
|
|
return assign({}, property, {
|
|
effectiveType: getModdleDescriptor(elementType).name
|
|
});
|
|
}
|
|
}
|
|
|
|
// search for properties by name first
|
|
return property;
|
|
}
|
|
|
|
var pkg = model.getPackage(nameNs.prefix);
|
|
|
|
if (pkg) {
|
|
elementTypeName = aliasToName(nameNs, pkg);
|
|
elementType = model.getType(elementTypeName);
|
|
|
|
// search for collection members later
|
|
property = find(descriptor.properties, function(p) {
|
|
return !p.isVirtual && !p.isReference && !p.isAttribute && elementType.hasType(p.type);
|
|
});
|
|
|
|
if (property) {
|
|
return assign({}, property, {
|
|
effectiveType: getModdleDescriptor(elementType).name
|
|
});
|
|
}
|
|
} else {
|
|
|
|
// parse unknown element (maybe extension)
|
|
property = find(descriptor.properties, function(p) {
|
|
return !p.isReference && !p.isAttribute && p.type === 'Element';
|
|
});
|
|
|
|
if (property) {
|
|
return property;
|
|
}
|
|
}
|
|
|
|
throw error('unrecognized element <' + nameNs.name + '>');
|
|
};
|
|
|
|
ElementHandler.prototype.toString = function() {
|
|
return 'ElementDescriptor[' + getModdleDescriptor(this.type).name + ']';
|
|
};
|
|
|
|
ElementHandler.prototype.valueHandler = function(propertyDesc, element) {
|
|
return new ValueHandler(propertyDesc, element);
|
|
};
|
|
|
|
ElementHandler.prototype.referenceHandler = function(propertyDesc) {
|
|
return new ReferenceHandler(propertyDesc, this.context);
|
|
};
|
|
|
|
ElementHandler.prototype.handler = function(type) {
|
|
if (type === 'Element') {
|
|
return new GenericElementHandler(this.model, type, this.context);
|
|
} else {
|
|
return new ElementHandler(this.model, type, this.context);
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Handle the child element parsing
|
|
*
|
|
* @param {Element} node the xml node
|
|
*/
|
|
ElementHandler.prototype.handleChild = function(node) {
|
|
var propertyDesc, type, element, childHandler;
|
|
|
|
propertyDesc = this.getPropertyForNode(node);
|
|
element = this.element;
|
|
|
|
type = propertyDesc.effectiveType || propertyDesc.type;
|
|
|
|
if (isSimple(type)) {
|
|
return this.valueHandler(propertyDesc, element);
|
|
}
|
|
|
|
if (propertyDesc.isReference) {
|
|
childHandler = this.referenceHandler(propertyDesc).handleNode(node);
|
|
} else {
|
|
childHandler = this.handler(type).handleNode(node);
|
|
}
|
|
|
|
var newElement = childHandler.element;
|
|
|
|
// child handles may decide to skip elements
|
|
// by not returning anything
|
|
if (newElement !== undefined) {
|
|
|
|
if (propertyDesc.isMany) {
|
|
element.get(propertyDesc.name).push(newElement);
|
|
} else {
|
|
element.set(propertyDesc.name, newElement);
|
|
}
|
|
|
|
if (propertyDesc.isReference) {
|
|
assign(newElement, {
|
|
element: element
|
|
});
|
|
|
|
this.context.addReference(newElement);
|
|
} else {
|
|
|
|
// establish child -> parent relationship
|
|
newElement.$parent = element;
|
|
}
|
|
}
|
|
|
|
return childHandler;
|
|
};
|
|
|
|
/**
|
|
* An element handler that performs special validation
|
|
* to ensure the node it gets initialized with matches
|
|
* the handlers type (namespace wise).
|
|
*
|
|
* @param {Moddle} model
|
|
* @param {String} typeName
|
|
* @param {Context} context
|
|
*/
|
|
function RootElementHandler(model, typeName, context) {
|
|
ElementHandler.call(this, model, typeName, context);
|
|
}
|
|
|
|
RootElementHandler.prototype = Object.create(ElementHandler.prototype);
|
|
|
|
RootElementHandler.prototype.createElement = function(node) {
|
|
|
|
var name = node.name,
|
|
nameNs = parseName(name),
|
|
model = this.model,
|
|
type = this.type,
|
|
pkg = model.getPackage(nameNs.prefix),
|
|
typeName = pkg && aliasToName(nameNs, pkg) || name;
|
|
|
|
// verify the correct namespace if we parse
|
|
// the first element in the handler tree
|
|
//
|
|
// this ensures we don't mistakenly import wrong namespace elements
|
|
if (!type.hasType(typeName)) {
|
|
throw error('unexpected element <' + node.originalName + '>');
|
|
}
|
|
|
|
return ElementHandler.prototype.createElement.call(this, node);
|
|
};
|
|
|
|
|
|
function GenericElementHandler(model, typeName, context) {
|
|
this.model = model;
|
|
this.context = context;
|
|
}
|
|
|
|
GenericElementHandler.prototype = Object.create(BaseElementHandler.prototype);
|
|
|
|
GenericElementHandler.prototype.createElement = function(node) {
|
|
|
|
var name = node.name,
|
|
ns = parseName(name),
|
|
prefix = ns.prefix,
|
|
uri = node.ns[prefix + '$uri'],
|
|
attributes = node.attributes;
|
|
|
|
return this.model.createAny(name, uri, attributes);
|
|
};
|
|
|
|
GenericElementHandler.prototype.handleChild = function(node) {
|
|
|
|
var handler = new GenericElementHandler(this.model, 'Element', this.context).handleNode(node),
|
|
element = this.element;
|
|
|
|
var newElement = handler.element,
|
|
children;
|
|
|
|
if (newElement !== undefined) {
|
|
children = element.$children = element.$children || [];
|
|
children.push(newElement);
|
|
|
|
// establish child -> parent relationship
|
|
newElement.$parent = element;
|
|
}
|
|
|
|
return handler;
|
|
};
|
|
|
|
GenericElementHandler.prototype.handleEnd = function() {
|
|
if (this.body) {
|
|
this.element.$body = this.body;
|
|
}
|
|
};
|
|
|
|
/**
|
|
* A reader for a meta-model
|
|
*
|
|
* @param {Object} options
|
|
* @param {Model} options.model used to read xml files
|
|
* @param {Boolean} options.lax whether to make parse errors warnings
|
|
*/
|
|
function Reader(options) {
|
|
|
|
if (options instanceof Moddle) {
|
|
options = {
|
|
model: options
|
|
};
|
|
}
|
|
|
|
assign(this, { lax: false }, options);
|
|
}
|
|
|
|
/**
|
|
* The fromXML result.
|
|
*
|
|
* @typedef {Object} ParseResult
|
|
*
|
|
* @property {ModdleElement} rootElement
|
|
* @property {Array<Object>} references
|
|
* @property {Array<Error>} warnings
|
|
* @property {Object} elementsById - a mapping containing each ID -> ModdleElement
|
|
*/
|
|
|
|
/**
|
|
* The fromXML result.
|
|
*
|
|
* @typedef {Error} ParseError
|
|
*
|
|
* @property {Array<Error>} warnings
|
|
*/
|
|
|
|
/**
|
|
* Parse the given XML into a moddle document tree.
|
|
*
|
|
* @param {String} xml
|
|
* @param {ElementHandler|Object} options or rootHandler
|
|
*
|
|
* @returns {Promise<ParseResult, ParseError>}
|
|
*/
|
|
Reader.prototype.fromXML = function(xml, options, done) {
|
|
|
|
var rootHandler = options.rootHandler;
|
|
|
|
if (options instanceof ElementHandler) {
|
|
|
|
// root handler passed via (xml, { rootHandler: ElementHandler }, ...)
|
|
rootHandler = options;
|
|
options = {};
|
|
} else {
|
|
if (typeof options === 'string') {
|
|
|
|
// rootHandler passed via (xml, 'someString', ...)
|
|
rootHandler = this.handler(options);
|
|
options = {};
|
|
} else if (typeof rootHandler === 'string') {
|
|
|
|
// rootHandler passed via (xml, { rootHandler: 'someString' }, ...)
|
|
rootHandler = this.handler(rootHandler);
|
|
}
|
|
}
|
|
|
|
var model = this.model,
|
|
lax = this.lax;
|
|
|
|
var context = new Context(assign({}, options, { rootHandler: rootHandler })),
|
|
parser = new Parser({ proxy: true }),
|
|
stack = createStack();
|
|
|
|
rootHandler.context = context;
|
|
|
|
// push root handler
|
|
stack.push(rootHandler);
|
|
|
|
|
|
/**
|
|
* Handle error.
|
|
*
|
|
* @param {Error} err
|
|
* @param {Function} getContext
|
|
* @param {boolean} lax
|
|
*
|
|
* @return {boolean} true if handled
|
|
*/
|
|
function handleError(err, getContext, lax) {
|
|
|
|
var ctx = getContext();
|
|
|
|
var line = ctx.line,
|
|
column = ctx.column,
|
|
data = ctx.data;
|
|
|
|
// we receive the full context data here,
|
|
// for elements trim down the information
|
|
// to the tag name, only
|
|
if (data.charAt(0) === '<' && data.indexOf(' ') !== -1) {
|
|
data = data.slice(0, data.indexOf(' ')) + '>';
|
|
}
|
|
|
|
var message =
|
|
'unparsable content ' + (data ? data + ' ' : '') + 'detected\n\t' +
|
|
'line: ' + line + '\n\t' +
|
|
'column: ' + column + '\n\t' +
|
|
'nested error: ' + err.message;
|
|
|
|
if (lax) {
|
|
context.addWarning({
|
|
message: message,
|
|
error: err
|
|
});
|
|
|
|
return true;
|
|
} else {
|
|
throw error(message);
|
|
}
|
|
}
|
|
|
|
function handleWarning(err, getContext) {
|
|
|
|
// just like handling errors in <lax=true> mode
|
|
return handleError(err, getContext, true);
|
|
}
|
|
|
|
/**
|
|
* Resolve collected references on parse end.
|
|
*/
|
|
function resolveReferences() {
|
|
|
|
var elementsById = context.elementsById;
|
|
var references = context.references;
|
|
|
|
var i, r;
|
|
|
|
for (i = 0; (r = references[i]); i++) {
|
|
var element = r.element;
|
|
var reference = elementsById[r.id];
|
|
var property = getModdleDescriptor(element).propertiesByName[r.property];
|
|
|
|
if (!reference) {
|
|
context.addWarning({
|
|
message: 'unresolved reference <' + r.id + '>',
|
|
element: r.element,
|
|
property: r.property,
|
|
value: r.id
|
|
});
|
|
}
|
|
|
|
if (property.isMany) {
|
|
var collection = element.get(property.name),
|
|
idx = collection.indexOf(r);
|
|
|
|
// we replace an existing place holder (idx != -1) or
|
|
// append to the collection instead
|
|
if (idx === -1) {
|
|
idx = collection.length;
|
|
}
|
|
|
|
if (!reference) {
|
|
|
|
// remove unresolvable reference
|
|
collection.splice(idx, 1);
|
|
} else {
|
|
|
|
// add or update reference in collection
|
|
collection[idx] = reference;
|
|
}
|
|
} else {
|
|
element.set(property.name, reference);
|
|
}
|
|
}
|
|
}
|
|
|
|
function handleClose() {
|
|
stack.pop().handleEnd();
|
|
}
|
|
|
|
var PREAMBLE_START_PATTERN = /^<\?xml /i;
|
|
|
|
var ENCODING_PATTERN = / encoding="([^"]+)"/i;
|
|
|
|
var UTF_8_PATTERN = /^utf-8$/i;
|
|
|
|
function handleQuestion(question) {
|
|
|
|
if (!PREAMBLE_START_PATTERN.test(question)) {
|
|
return;
|
|
}
|
|
|
|
var match = ENCODING_PATTERN.exec(question);
|
|
var encoding = match && match[1];
|
|
|
|
if (!encoding || UTF_8_PATTERN.test(encoding)) {
|
|
return;
|
|
}
|
|
|
|
context.addWarning({
|
|
message:
|
|
'unsupported document encoding <' + encoding + '>, ' +
|
|
'falling back to UTF-8'
|
|
});
|
|
}
|
|
|
|
function handleOpen(node, getContext) {
|
|
var handler = stack.peek();
|
|
|
|
try {
|
|
stack.push(handler.handleNode(node));
|
|
} catch (err) {
|
|
|
|
if (handleError(err, getContext, lax)) {
|
|
stack.push(new NoopHandler());
|
|
}
|
|
}
|
|
}
|
|
|
|
function handleCData(text, getContext) {
|
|
|
|
try {
|
|
stack.peek().handleText(text);
|
|
} catch (err) {
|
|
handleWarning(err, getContext);
|
|
}
|
|
}
|
|
|
|
function handleText(text, getContext) {
|
|
|
|
// strip whitespace only nodes, i.e. before
|
|
// <!CDATA[ ... ]> sections and in between tags
|
|
|
|
if (!text.trim()) {
|
|
return;
|
|
}
|
|
|
|
handleCData(text, getContext);
|
|
}
|
|
|
|
var uriMap = model.getPackages().reduce(function(uriMap, p) {
|
|
uriMap[p.uri] = p.prefix;
|
|
|
|
return uriMap;
|
|
}, {
|
|
'http://www.w3.org/XML/1998/namespace': 'xml' // add default xml ns
|
|
});
|
|
parser
|
|
.ns(uriMap)
|
|
.on('openTag', function(obj, decodeStr, selfClosing, getContext) {
|
|
|
|
// gracefully handle unparsable attributes (attrs=false)
|
|
var attrs = obj.attrs || {};
|
|
|
|
var decodedAttrs = Object.keys(attrs).reduce(function(d, key) {
|
|
var value = decodeStr(attrs[key]);
|
|
|
|
d[key] = value;
|
|
|
|
return d;
|
|
}, {});
|
|
|
|
var node = {
|
|
name: obj.name,
|
|
originalName: obj.originalName,
|
|
attributes: decodedAttrs,
|
|
ns: obj.ns
|
|
};
|
|
|
|
handleOpen(node, getContext);
|
|
})
|
|
.on('question', handleQuestion)
|
|
.on('closeTag', handleClose)
|
|
.on('cdata', handleCData)
|
|
.on('text', function(text, decodeEntities, getContext) {
|
|
handleText(decodeEntities(text), getContext);
|
|
})
|
|
.on('error', handleError)
|
|
.on('warn', handleWarning);
|
|
|
|
// async XML parsing to make sure the execution environment
|
|
// (node or brower) is kept responsive and that certain optimization
|
|
// strategies can kick in.
|
|
return new Promise(function(resolve, reject) {
|
|
|
|
var err;
|
|
|
|
try {
|
|
parser.parse(xml);
|
|
|
|
resolveReferences();
|
|
} catch (e) {
|
|
err = e;
|
|
}
|
|
|
|
var rootElement = rootHandler.element;
|
|
|
|
if (!err && !rootElement) {
|
|
err = error('failed to parse document as <' + rootHandler.type.$descriptor.name + '>');
|
|
}
|
|
|
|
var warnings = context.warnings;
|
|
var references = context.references;
|
|
var elementsById = context.elementsById;
|
|
|
|
if (err) {
|
|
err.warnings = warnings;
|
|
|
|
return reject(err);
|
|
} else {
|
|
return resolve({
|
|
rootElement: rootElement,
|
|
elementsById: elementsById,
|
|
references: references,
|
|
warnings: warnings
|
|
});
|
|
}
|
|
});
|
|
};
|
|
|
|
Reader.prototype.handler = function(name) {
|
|
return new RootElementHandler(this.model, name);
|
|
};
|
|
|
|
|
|
// helpers //////////////////////////
|
|
|
|
function createStack() {
|
|
var stack = [];
|
|
|
|
Object.defineProperty(stack, 'peek', {
|
|
value: function() {
|
|
return this[this.length - 1];
|
|
}
|
|
});
|
|
|
|
return stack;
|
|
}
|
|
|
|
var XML_PREAMBLE = '<?xml version="1.0" encoding="UTF-8"?>\n';
|
|
|
|
var ESCAPE_ATTR_CHARS = /<|>|'|"|&|\n\r|\n/g;
|
|
var ESCAPE_CHARS = /<|>|&/g;
|
|
|
|
|
|
function Namespaces(parent) {
|
|
|
|
var prefixMap = {};
|
|
var uriMap = {};
|
|
var used = {};
|
|
|
|
var wellknown = [];
|
|
var custom = [];
|
|
|
|
// API
|
|
|
|
this.byUri = function(uri) {
|
|
return uriMap[uri] || (
|
|
parent && parent.byUri(uri)
|
|
);
|
|
};
|
|
|
|
this.add = function(ns, isWellknown) {
|
|
|
|
uriMap[ns.uri] = ns;
|
|
|
|
if (isWellknown) {
|
|
wellknown.push(ns);
|
|
} else {
|
|
custom.push(ns);
|
|
}
|
|
|
|
this.mapPrefix(ns.prefix, ns.uri);
|
|
};
|
|
|
|
this.uriByPrefix = function(prefix) {
|
|
return prefixMap[prefix || 'xmlns'];
|
|
};
|
|
|
|
this.mapPrefix = function(prefix, uri) {
|
|
prefixMap[prefix || 'xmlns'] = uri;
|
|
};
|
|
|
|
this.getNSKey = function(ns) {
|
|
return (ns.prefix !== undefined) ? (ns.uri + '|' + ns.prefix) : ns.uri;
|
|
};
|
|
|
|
this.logUsed = function(ns) {
|
|
|
|
var uri = ns.uri;
|
|
var nsKey = this.getNSKey(ns);
|
|
|
|
used[nsKey] = this.byUri(uri);
|
|
|
|
// Inform parent recursively about the usage of this NS
|
|
if (parent) {
|
|
parent.logUsed(ns);
|
|
}
|
|
};
|
|
|
|
this.getUsed = function(ns) {
|
|
|
|
function isUsed(ns) {
|
|
var nsKey = self.getNSKey(ns);
|
|
|
|
return used[nsKey];
|
|
}
|
|
|
|
var self = this;
|
|
|
|
var allNs = [].concat(wellknown, custom);
|
|
|
|
return allNs.filter(isUsed);
|
|
};
|
|
|
|
}
|
|
|
|
function lower(string) {
|
|
return string.charAt(0).toLowerCase() + string.slice(1);
|
|
}
|
|
|
|
function nameToAlias(name, pkg) {
|
|
if (hasLowerCaseAlias(pkg)) {
|
|
return lower(name);
|
|
} else {
|
|
return name;
|
|
}
|
|
}
|
|
|
|
function inherits(ctor, superCtor) {
|
|
ctor.super_ = superCtor;
|
|
ctor.prototype = Object.create(superCtor.prototype, {
|
|
constructor: {
|
|
value: ctor,
|
|
enumerable: false,
|
|
writable: true,
|
|
configurable: true
|
|
}
|
|
});
|
|
}
|
|
|
|
function nsName(ns) {
|
|
if (isString(ns)) {
|
|
return ns;
|
|
} else {
|
|
return (ns.prefix ? ns.prefix + ':' : '') + ns.localName;
|
|
}
|
|
}
|
|
|
|
function getNsAttrs(namespaces) {
|
|
|
|
return namespaces.getUsed().filter(function(ns) {
|
|
|
|
// do not serialize built in <xml> namespace
|
|
return ns.prefix !== 'xml';
|
|
}).map(function(ns) {
|
|
var name = 'xmlns' + (ns.prefix ? ':' + ns.prefix : '');
|
|
return { name: name, value: ns.uri };
|
|
});
|
|
|
|
}
|
|
|
|
function getElementNs(ns, descriptor) {
|
|
if (descriptor.isGeneric) {
|
|
return assign({ localName: descriptor.ns.localName }, ns);
|
|
} else {
|
|
return assign({ localName: nameToAlias(descriptor.ns.localName, descriptor.$pkg) }, ns);
|
|
}
|
|
}
|
|
|
|
function getPropertyNs(ns, descriptor) {
|
|
return assign({ localName: descriptor.ns.localName }, ns);
|
|
}
|
|
|
|
function getSerializableProperties(element) {
|
|
var descriptor = element.$descriptor;
|
|
|
|
return filter(descriptor.properties, function(p) {
|
|
var name = p.name;
|
|
|
|
if (p.isVirtual) {
|
|
return false;
|
|
}
|
|
|
|
// do not serialize defaults
|
|
if (!has(element, name)) {
|
|
return false;
|
|
}
|
|
|
|
var value = element[name];
|
|
|
|
// do not serialize default equals
|
|
if (value === p.default) {
|
|
return false;
|
|
}
|
|
|
|
// do not serialize null properties
|
|
if (value === null) {
|
|
return false;
|
|
}
|
|
|
|
return p.isMany ? value.length : true;
|
|
});
|
|
}
|
|
|
|
var ESCAPE_ATTR_MAP = {
|
|
'\n': '#10',
|
|
'\n\r': '#10',
|
|
'"': '#34',
|
|
'\'': '#39',
|
|
'<': '#60',
|
|
'>': '#62',
|
|
'&': '#38'
|
|
};
|
|
|
|
var ESCAPE_MAP = {
|
|
'<': 'lt',
|
|
'>': 'gt',
|
|
'&': 'amp'
|
|
};
|
|
|
|
function escape(str, charPattern, replaceMap) {
|
|
|
|
// ensure we are handling strings here
|
|
str = isString(str) ? str : '' + str;
|
|
|
|
return str.replace(charPattern, function(s) {
|
|
return '&' + replaceMap[s] + ';';
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Escape a string attribute to not contain any bad values (line breaks, '"', ...)
|
|
*
|
|
* @param {String} str the string to escape
|
|
* @return {String} the escaped string
|
|
*/
|
|
function escapeAttr(str) {
|
|
return escape(str, ESCAPE_ATTR_CHARS, ESCAPE_ATTR_MAP);
|
|
}
|
|
|
|
function escapeBody(str) {
|
|
return escape(str, ESCAPE_CHARS, ESCAPE_MAP);
|
|
}
|
|
|
|
function filterAttributes(props) {
|
|
return filter(props, function(p) { return p.isAttr; });
|
|
}
|
|
|
|
function filterContained(props) {
|
|
return filter(props, function(p) { return !p.isAttr; });
|
|
}
|
|
|
|
|
|
function ReferenceSerializer(tagName) {
|
|
this.tagName = tagName;
|
|
}
|
|
|
|
ReferenceSerializer.prototype.build = function(element) {
|
|
this.element = element;
|
|
return this;
|
|
};
|
|
|
|
ReferenceSerializer.prototype.serializeTo = function(writer) {
|
|
writer
|
|
.appendIndent()
|
|
.append('<' + this.tagName + '>' + this.element.id + '</' + this.tagName + '>')
|
|
.appendNewLine();
|
|
};
|
|
|
|
function BodySerializer() {}
|
|
|
|
BodySerializer.prototype.serializeValue =
|
|
BodySerializer.prototype.serializeTo = function(writer) {
|
|
writer.append(
|
|
this.escape
|
|
? escapeBody(this.value)
|
|
: this.value
|
|
);
|
|
};
|
|
|
|
BodySerializer.prototype.build = function(prop, value) {
|
|
this.value = value;
|
|
|
|
if (prop.type === 'String' && value.search(ESCAPE_CHARS) !== -1) {
|
|
this.escape = true;
|
|
}
|
|
|
|
return this;
|
|
};
|
|
|
|
function ValueSerializer(tagName) {
|
|
this.tagName = tagName;
|
|
}
|
|
|
|
inherits(ValueSerializer, BodySerializer);
|
|
|
|
ValueSerializer.prototype.serializeTo = function(writer) {
|
|
|
|
writer
|
|
.appendIndent()
|
|
.append('<' + this.tagName + '>');
|
|
|
|
this.serializeValue(writer);
|
|
|
|
writer
|
|
.append('</' + this.tagName + '>')
|
|
.appendNewLine();
|
|
};
|
|
|
|
function ElementSerializer(parent, propertyDescriptor) {
|
|
this.body = [];
|
|
this.attrs = [];
|
|
|
|
this.parent = parent;
|
|
this.propertyDescriptor = propertyDescriptor;
|
|
}
|
|
|
|
ElementSerializer.prototype.build = function(element) {
|
|
this.element = element;
|
|
|
|
var elementDescriptor = element.$descriptor,
|
|
propertyDescriptor = this.propertyDescriptor;
|
|
|
|
var otherAttrs,
|
|
properties;
|
|
|
|
var isGeneric = elementDescriptor.isGeneric;
|
|
|
|
if (isGeneric) {
|
|
otherAttrs = this.parseGeneric(element);
|
|
} else {
|
|
otherAttrs = this.parseNsAttributes(element);
|
|
}
|
|
|
|
if (propertyDescriptor) {
|
|
this.ns = this.nsPropertyTagName(propertyDescriptor);
|
|
} else {
|
|
this.ns = this.nsTagName(elementDescriptor);
|
|
}
|
|
|
|
// compute tag name
|
|
this.tagName = this.addTagName(this.ns);
|
|
|
|
if (!isGeneric) {
|
|
properties = getSerializableProperties(element);
|
|
|
|
this.parseAttributes(filterAttributes(properties));
|
|
this.parseContainments(filterContained(properties));
|
|
}
|
|
|
|
this.parseGenericAttributes(element, otherAttrs);
|
|
|
|
return this;
|
|
};
|
|
|
|
ElementSerializer.prototype.nsTagName = function(descriptor) {
|
|
var effectiveNs = this.logNamespaceUsed(descriptor.ns);
|
|
return getElementNs(effectiveNs, descriptor);
|
|
};
|
|
|
|
ElementSerializer.prototype.nsPropertyTagName = function(descriptor) {
|
|
var effectiveNs = this.logNamespaceUsed(descriptor.ns);
|
|
return getPropertyNs(effectiveNs, descriptor);
|
|
};
|
|
|
|
ElementSerializer.prototype.isLocalNs = function(ns) {
|
|
return ns.uri === this.ns.uri;
|
|
};
|
|
|
|
/**
|
|
* Get the actual ns attribute name for the given element.
|
|
*
|
|
* @param {Object} element
|
|
* @param {Boolean} [element.inherited=false]
|
|
*
|
|
* @return {Object} nsName
|
|
*/
|
|
ElementSerializer.prototype.nsAttributeName = function(element) {
|
|
|
|
var ns;
|
|
|
|
if (isString(element)) {
|
|
ns = parseName(element);
|
|
} else {
|
|
ns = element.ns;
|
|
}
|
|
|
|
// return just local name for inherited attributes
|
|
if (element.inherited) {
|
|
return { localName: ns.localName };
|
|
}
|
|
|
|
// parse + log effective ns
|
|
var effectiveNs = this.logNamespaceUsed(ns);
|
|
|
|
// LOG ACTUAL namespace use
|
|
this.getNamespaces().logUsed(effectiveNs);
|
|
|
|
// strip prefix if same namespace like parent
|
|
if (this.isLocalNs(effectiveNs)) {
|
|
return { localName: ns.localName };
|
|
} else {
|
|
return assign({ localName: ns.localName }, effectiveNs);
|
|
}
|
|
};
|
|
|
|
ElementSerializer.prototype.parseGeneric = function(element) {
|
|
|
|
var self = this,
|
|
body = this.body;
|
|
|
|
var attributes = [];
|
|
|
|
forEach(element, function(val, key) {
|
|
|
|
var nonNsAttr;
|
|
|
|
if (key === '$body') {
|
|
body.push(new BodySerializer().build({ type: 'String' }, val));
|
|
} else
|
|
if (key === '$children') {
|
|
forEach(val, function(child) {
|
|
body.push(new ElementSerializer(self).build(child));
|
|
});
|
|
} else
|
|
if (key.indexOf('$') !== 0) {
|
|
nonNsAttr = self.parseNsAttribute(element, key, val);
|
|
|
|
if (nonNsAttr) {
|
|
attributes.push({ name: key, value: val });
|
|
}
|
|
}
|
|
});
|
|
|
|
return attributes;
|
|
};
|
|
|
|
ElementSerializer.prototype.parseNsAttribute = function(element, name, value) {
|
|
var model = element.$model;
|
|
|
|
var nameNs = parseName(name);
|
|
|
|
var ns;
|
|
|
|
// parse xmlns:foo="http://foo.bar"
|
|
if (nameNs.prefix === 'xmlns') {
|
|
ns = { prefix: nameNs.localName, uri: value };
|
|
}
|
|
|
|
// parse xmlns="http://foo.bar"
|
|
if (!nameNs.prefix && nameNs.localName === 'xmlns') {
|
|
ns = { uri: value };
|
|
}
|
|
|
|
if (!ns) {
|
|
return {
|
|
name: name,
|
|
value: value
|
|
};
|
|
}
|
|
|
|
if (model && model.getPackage(value)) {
|
|
|
|
// register well known namespace
|
|
this.logNamespace(ns, true, true);
|
|
} else {
|
|
|
|
// log custom namespace directly as used
|
|
var actualNs = this.logNamespaceUsed(ns, true);
|
|
|
|
this.getNamespaces().logUsed(actualNs);
|
|
}
|
|
};
|
|
|
|
|
|
/**
|
|
* Parse namespaces and return a list of left over generic attributes
|
|
*
|
|
* @param {Object} element
|
|
* @return {Array<Object>}
|
|
*/
|
|
ElementSerializer.prototype.parseNsAttributes = function(element, attrs) {
|
|
var self = this;
|
|
|
|
var genericAttrs = element.$attrs;
|
|
|
|
var attributes = [];
|
|
|
|
// parse namespace attributes first
|
|
// and log them. push non namespace attributes to a list
|
|
// and process them later
|
|
forEach(genericAttrs, function(value, name) {
|
|
|
|
var nonNsAttr = self.parseNsAttribute(element, name, value);
|
|
|
|
if (nonNsAttr) {
|
|
attributes.push(nonNsAttr);
|
|
}
|
|
});
|
|
|
|
return attributes;
|
|
};
|
|
|
|
ElementSerializer.prototype.parseGenericAttributes = function(element, attributes) {
|
|
|
|
var self = this;
|
|
|
|
forEach(attributes, function(attr) {
|
|
|
|
// do not serialize xsi:type attribute
|
|
// it is set manually based on the actual implementation type
|
|
if (attr.name === XSI_TYPE) {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
self.addAttribute(self.nsAttributeName(attr.name), attr.value);
|
|
} catch (e) {
|
|
/* global console */
|
|
|
|
console.warn(
|
|
'missing namespace information for ',
|
|
attr.name, '=', attr.value, 'on', element,
|
|
e);
|
|
}
|
|
});
|
|
};
|
|
|
|
ElementSerializer.prototype.parseContainments = function(properties) {
|
|
|
|
var self = this,
|
|
body = this.body,
|
|
element = this.element;
|
|
|
|
forEach(properties, function(p) {
|
|
var value = element.get(p.name),
|
|
isReference = p.isReference,
|
|
isMany = p.isMany;
|
|
|
|
if (!isMany) {
|
|
value = [ value ];
|
|
}
|
|
|
|
if (p.isBody) {
|
|
body.push(new BodySerializer().build(p, value[0]));
|
|
} else
|
|
if (isSimple(p.type)) {
|
|
forEach(value, function(v) {
|
|
body.push(new ValueSerializer(self.addTagName(self.nsPropertyTagName(p))).build(p, v));
|
|
});
|
|
} else
|
|
if (isReference) {
|
|
forEach(value, function(v) {
|
|
body.push(new ReferenceSerializer(self.addTagName(self.nsPropertyTagName(p))).build(v));
|
|
});
|
|
} else {
|
|
|
|
// allow serialization via type
|
|
// rather than element name
|
|
var asType = serializeAsType(p),
|
|
asProperty = serializeAsProperty(p);
|
|
|
|
forEach(value, function(v) {
|
|
var serializer;
|
|
|
|
if (asType) {
|
|
serializer = new TypeSerializer(self, p);
|
|
} else
|
|
if (asProperty) {
|
|
serializer = new ElementSerializer(self, p);
|
|
} else {
|
|
serializer = new ElementSerializer(self);
|
|
}
|
|
|
|
body.push(serializer.build(v));
|
|
});
|
|
}
|
|
});
|
|
};
|
|
|
|
ElementSerializer.prototype.getNamespaces = function(local) {
|
|
|
|
var namespaces = this.namespaces,
|
|
parent = this.parent,
|
|
parentNamespaces;
|
|
|
|
if (!namespaces) {
|
|
parentNamespaces = parent && parent.getNamespaces();
|
|
|
|
if (local || !parentNamespaces) {
|
|
this.namespaces = namespaces = new Namespaces(parentNamespaces);
|
|
} else {
|
|
namespaces = parentNamespaces;
|
|
}
|
|
}
|
|
|
|
return namespaces;
|
|
};
|
|
|
|
ElementSerializer.prototype.logNamespace = function(ns, wellknown, local) {
|
|
var namespaces = this.getNamespaces(local);
|
|
|
|
var nsUri = ns.uri,
|
|
nsPrefix = ns.prefix;
|
|
|
|
var existing = namespaces.byUri(nsUri);
|
|
|
|
if (!existing || local) {
|
|
namespaces.add(ns, wellknown);
|
|
}
|
|
|
|
namespaces.mapPrefix(nsPrefix, nsUri);
|
|
|
|
return ns;
|
|
};
|
|
|
|
ElementSerializer.prototype.logNamespaceUsed = function(ns, local) {
|
|
var element = this.element,
|
|
model = element.$model,
|
|
namespaces = this.getNamespaces(local);
|
|
|
|
// ns may be
|
|
//
|
|
// * prefix only
|
|
// * prefix:uri
|
|
// * localName only
|
|
|
|
var prefix = ns.prefix,
|
|
uri = ns.uri,
|
|
newPrefix, idx,
|
|
wellknownUri;
|
|
|
|
// handle anonymous namespaces (elementForm=unqualified), cf. #23
|
|
if (!prefix && !uri) {
|
|
return { localName: ns.localName };
|
|
}
|
|
|
|
wellknownUri = DEFAULT_NS_MAP[prefix] || model && (model.getPackage(prefix) || {}).uri;
|
|
|
|
uri = uri || wellknownUri || namespaces.uriByPrefix(prefix);
|
|
|
|
if (!uri) {
|
|
throw new Error('no namespace uri given for prefix <' + prefix + '>');
|
|
}
|
|
|
|
ns = namespaces.byUri(uri);
|
|
|
|
if (!ns) {
|
|
newPrefix = prefix;
|
|
idx = 1;
|
|
|
|
// find a prefix that is not mapped yet
|
|
while (namespaces.uriByPrefix(newPrefix)) {
|
|
newPrefix = prefix + '_' + idx++;
|
|
}
|
|
|
|
ns = this.logNamespace({ prefix: newPrefix, uri: uri }, wellknownUri === uri);
|
|
}
|
|
|
|
if (prefix) {
|
|
namespaces.mapPrefix(prefix, uri);
|
|
}
|
|
|
|
return ns;
|
|
};
|
|
|
|
ElementSerializer.prototype.parseAttributes = function(properties) {
|
|
var self = this,
|
|
element = this.element;
|
|
|
|
forEach(properties, function(p) {
|
|
|
|
var value = element.get(p.name);
|
|
|
|
if (p.isReference) {
|
|
|
|
if (!p.isMany) {
|
|
value = value.id;
|
|
}
|
|
else {
|
|
var values = [];
|
|
forEach(value, function(v) {
|
|
values.push(v.id);
|
|
});
|
|
|
|
// IDREFS is a whitespace-separated list of references.
|
|
value = values.join(' ');
|
|
}
|
|
|
|
}
|
|
|
|
self.addAttribute(self.nsAttributeName(p), value);
|
|
});
|
|
};
|
|
|
|
ElementSerializer.prototype.addTagName = function(nsTagName) {
|
|
var actualNs = this.logNamespaceUsed(nsTagName);
|
|
|
|
this.getNamespaces().logUsed(actualNs);
|
|
|
|
return nsName(nsTagName);
|
|
};
|
|
|
|
ElementSerializer.prototype.addAttribute = function(name, value) {
|
|
var attrs = this.attrs;
|
|
|
|
if (isString(value)) {
|
|
value = escapeAttr(value);
|
|
}
|
|
|
|
// de-duplicate attributes
|
|
// https://github.com/bpmn-io/moddle-xml/issues/66
|
|
var idx = findIndex(attrs, function(element) {
|
|
return (
|
|
element.name.localName === name.localName &&
|
|
element.name.uri === name.uri &&
|
|
element.name.prefix === name.prefix
|
|
);
|
|
});
|
|
|
|
var attr = { name: name, value: value };
|
|
|
|
if (idx !== -1) {
|
|
attrs.splice(idx, 1, attr);
|
|
} else {
|
|
attrs.push(attr);
|
|
}
|
|
};
|
|
|
|
ElementSerializer.prototype.serializeAttributes = function(writer) {
|
|
var attrs = this.attrs,
|
|
namespaces = this.namespaces;
|
|
|
|
if (namespaces) {
|
|
attrs = getNsAttrs(namespaces).concat(attrs);
|
|
}
|
|
|
|
forEach(attrs, function(a) {
|
|
writer
|
|
.append(' ')
|
|
.append(nsName(a.name)).append('="').append(a.value).append('"');
|
|
});
|
|
};
|
|
|
|
ElementSerializer.prototype.serializeTo = function(writer) {
|
|
var firstBody = this.body[0],
|
|
indent = firstBody && firstBody.constructor !== BodySerializer;
|
|
|
|
writer
|
|
.appendIndent()
|
|
.append('<' + this.tagName);
|
|
|
|
this.serializeAttributes(writer);
|
|
|
|
writer.append(firstBody ? '>' : ' />');
|
|
|
|
if (firstBody) {
|
|
|
|
if (indent) {
|
|
writer
|
|
.appendNewLine()
|
|
.indent();
|
|
}
|
|
|
|
forEach(this.body, function(b) {
|
|
b.serializeTo(writer);
|
|
});
|
|
|
|
if (indent) {
|
|
writer
|
|
.unindent()
|
|
.appendIndent();
|
|
}
|
|
|
|
writer.append('</' + this.tagName + '>');
|
|
}
|
|
|
|
writer.appendNewLine();
|
|
};
|
|
|
|
/**
|
|
* A serializer for types that handles serialization of data types
|
|
*/
|
|
function TypeSerializer(parent, propertyDescriptor) {
|
|
ElementSerializer.call(this, parent, propertyDescriptor);
|
|
}
|
|
|
|
inherits(TypeSerializer, ElementSerializer);
|
|
|
|
TypeSerializer.prototype.parseNsAttributes = function(element) {
|
|
|
|
// extracted attributes
|
|
var attributes = ElementSerializer.prototype.parseNsAttributes.call(this, element);
|
|
|
|
var descriptor = element.$descriptor;
|
|
|
|
// only serialize xsi:type if necessary
|
|
if (descriptor.name === this.propertyDescriptor.type) {
|
|
return attributes;
|
|
}
|
|
|
|
var typeNs = this.typeNs = this.nsTagName(descriptor);
|
|
this.getNamespaces().logUsed(this.typeNs);
|
|
|
|
// add xsi:type attribute to represent the elements
|
|
// actual type
|
|
|
|
var pkg = element.$model.getPackage(typeNs.uri),
|
|
typePrefix = (pkg.xml && pkg.xml.typePrefix) || '';
|
|
|
|
this.addAttribute(
|
|
this.nsAttributeName(XSI_TYPE),
|
|
(typeNs.prefix ? typeNs.prefix + ':' : '') + typePrefix + descriptor.ns.localName
|
|
);
|
|
|
|
return attributes;
|
|
};
|
|
|
|
TypeSerializer.prototype.isLocalNs = function(ns) {
|
|
return ns.uri === (this.typeNs || this.ns).uri;
|
|
};
|
|
|
|
function SavingWriter() {
|
|
this.value = '';
|
|
|
|
this.write = function(str) {
|
|
this.value += str;
|
|
};
|
|
}
|
|
|
|
function FormatingWriter(out, format) {
|
|
|
|
var indent = [ '' ];
|
|
|
|
this.append = function(str) {
|
|
out.write(str);
|
|
|
|
return this;
|
|
};
|
|
|
|
this.appendNewLine = function() {
|
|
if (format) {
|
|
out.write('\n');
|
|
}
|
|
|
|
return this;
|
|
};
|
|
|
|
this.appendIndent = function() {
|
|
if (format) {
|
|
out.write(indent.join(' '));
|
|
}
|
|
|
|
return this;
|
|
};
|
|
|
|
this.indent = function() {
|
|
indent.push('');
|
|
return this;
|
|
};
|
|
|
|
this.unindent = function() {
|
|
indent.pop();
|
|
return this;
|
|
};
|
|
}
|
|
|
|
/**
|
|
* A writer for meta-model backed document trees
|
|
*
|
|
* @param {Object} options output options to pass into the writer
|
|
*/
|
|
function Writer(options) {
|
|
|
|
options = assign({ format: false, preamble: true }, options || {});
|
|
|
|
function toXML(tree, writer) {
|
|
var internalWriter = writer || new SavingWriter();
|
|
var formatingWriter = new FormatingWriter(internalWriter, options.format);
|
|
|
|
if (options.preamble) {
|
|
formatingWriter.append(XML_PREAMBLE);
|
|
}
|
|
|
|
new ElementSerializer().build(tree).serializeTo(formatingWriter);
|
|
|
|
if (!writer) {
|
|
return internalWriter.value;
|
|
}
|
|
}
|
|
|
|
return {
|
|
toXML: toXML
|
|
};
|
|
}
|
|
|
|
exports.Reader = Reader;
|
|
exports.Writer = Writer;
|
|
|
|
Object.defineProperty(exports, '__esModule', { value: true });
|
|
|
|
}));
|