musicjson
Version:
MusicXML to MusicJSON bi-directional converter
242 lines (196 loc) • 5.84 kB
JavaScript
/*
* MusicJSON
* - A bi-directional converter between MusicXML and MusicJSON
* https://github.com/saebekassebil/musicjson
*
* Copyright (c) 2013 Saebekassebil
* Licensed under the MIT license.
*/
var builder = require('xmlbuilder')
, sax = require('sax')
, events = require('events')
, util = require('util');
var partwise = {
id: '-//Recordare//DTD MusicXML 2.0 Partwise//EN',
url: 'http://www.musicxml.org/dtds/partwise.dtd',
type: 'score-partwise'
};
var doctype = 'PUBLIC "' + partwise.id + '" "' + partwise.url + '"';
var attrkey = '$', charkey = '_',
orderkey = '%', orderNumberKey = '&', namekey = '#name',
specialKeys = [attrkey, charkey, orderkey, orderNumberKey, namekey];
function assignOrderNumber(obj, name, parent) {
if (name in parent) {
if (!(orderNumberKey in parent)) {
parent[orderNumberKey] = 0;
for (var child in parent) {
if (specialKeys.indexOf(child) !== -1) {
continue;
}
parent[orderNumberKey]++;
parent[child][orderkey] = parent[orderNumberKey];
}
}
parent[orderNumberKey]++;
obj[orderkey] = parent[orderNumberKey];
} else {
if (orderNumberKey in parent) {
parent[orderNumberKey]++;
obj[orderkey] = parent[orderNumberKey];
}
}
}
// XML Parser for parsing MusicXML documents
function Parser(settings) {
events.EventEmitter.call(this);
// Initialize sax parser
this.sax = sax.parser(true, {
trim: false,
normalize: false,
xmlns: false
});
// Stack to hold the tags when encountered
this.stack = [];
this.settings = settings || {};
// Initialize listeners
this.sax.onerror = this.error.bind(this);
this.sax.onopentag = this.open.bind(this);
this.sax.onclosetag = this.close.bind(this);
this.sax.ontext = this.sax.oncdata = this.text.bind(this);
}
util.inherits(Parser, events.EventEmitter);
Parser.prototype.error = function(e) {
this.emit('error', e);
};
Parser.prototype.open = function(node) {
var key, obj = {};
// Set the node name (deleted later)
obj[namekey] = node.name.toLowerCase();
obj[charkey] = '';
// Iterate over all the attributes
for (key in node.attributes) {
if (!(attrkey in obj)) {
obj[attrkey] = {};
}
obj[attrkey][key] = node.attributes[key];
}
this.stack.push(obj);
};
Parser.prototype.close = function(node) {
var obj = this.stack.pop(), name = obj[namekey], parent;
delete obj[namekey];
if (orderNumberKey in obj) {
delete obj[orderNumberKey];
}
parent = this.stack[this.stack.length - 1];
if (!obj[charkey].trim().length) { // No text content
delete obj[charkey];
} else if (Object.keys(obj).length === 1) { // Text node
obj = obj[charkey];
}
// If the object is empty, translate it to "true"
if (obj && typeof obj === 'object' && !Object.keys(obj).length) {
obj = true;
}
if (this.stack.length > 0) {
// Assign order number, so that tag order is preserved
if (this.settings.preserveOrder) {
assignOrderNumber(obj, name, parent);
}
if (name in parent) {
parent[name] = util.isArray(parent[name]) ? parent[name] : [parent[name]];
parent[name].push(obj);
} else {
parent[name] = obj;
}
} else {
var returnobj = {};
returnobj[name] = obj;
this.emit('end', returnobj);
}
};
Parser.prototype.text = function(text) {
var last = this.stack[this.stack.length - 1];
if (last) {
last[charkey] += text;
}
};
Parser.prototype.parse = function(string, callback) {
this.on('end', function(result) { callback(null, result); });
this.on('error', callback);
this.sax.write(string);
};
// Translates a MusicJSON element to MusicXML
function toXML(root, el, nodeName) {
var element, i, attr, type = typeof el, children = [];
if (!root) {
// Create <?xml, DOCTYPE and root element
element = root = builder.create(nodeName, {
version: '1.0',
encoding: 'UTF-8'
}, { ext: doctype });
} else {
element = root.element(nodeName);
}
if (type === 'number' || type === 'string') {
return element.text(el);
}
for (i in el) {
switch (i) {
// Set attributes of node
case '$':
for (attr in el[i]) {
element.attribute(attr, el[i][attr]);
}
break;
// Set textual content of node
case '_':
element.text(el[i]);
break;
case '%':
// Do nothing
break;
// Append child
default:
if (util.isArray(el[i])) {
children = children.concat(el[i].map(function(el) {
return { el: el, name: i };
}));
} else {
children.push({el: el[i], name: i});
}
break;
}
}
// Find all children with no ordering
var sorted = children.filter(function(child) {
return !(typeof child.el === 'object' && orderkey in child.el);
});
// Find all children with ordering, and splice them into
// the sorted array
children.filter(function(child) {
return typeof child.el === 'object' && orderkey in child.el;
}).sort(function(a, b) {
return +a.el[orderkey] - +b.el[orderkey];
}).forEach(function(child, i) {
var index = +child.el[orderkey] - 1;
sorted.splice(index, 0, child);
});
sorted.forEach(function(child) {
toXML(element, child.el, child.name);
});
return element;
}
exports.musicJSON = function(source, callback) {
var settings = { preserveOrder: true };
if (typeof source === 'object') {
settings.preserveOrder = source.preserveOrder;
source = source.source;
}
var parser = new Parser(settings);
parser.parse(source, callback);
};
exports.musicXML = function musicXML(source, callback) {
var root = toXML(null, source[partwise.type], partwise.type);
callback(null, root);
};