UNPKG

node-webodf

Version:

WebODF - JavaScript Document Engine http://webodf.org/

1,612 lines (1,562 loc) 58.2 kB
/** * Copyright (C) 2012-2013 KO GmbH <copyright@kogmbh.com> * * @licstart * This file is part of WebODF. * * WebODF is free software: you can redistribute it and/or modify it * under the terms of the GNU Affero General Public License (GNU AGPL) * as published by the Free Software Foundation, either version 3 of * the License, or (at your option) any later version. * * WebODF is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with WebODF. If not, see <http://www.gnu.org/licenses/>. * @licend * * @source: http://www.webodf.org/ * @source: https://github.com/kogmbh/WebODF/ */ /*jslint nomen: true, bitwise: true, emptyblock: true, unparam: true */ /*global window, XMLHttpRequest, require, console, DOMParser, document, process, __dirname, setTimeout, Packages, print, readFile, quit, Buffer, ArrayBuffer, Uint8Array, navigator, VBArray, alert, now, clearTimeout, webodf_version */ /** * Three implementations of a runtime for browser, node.js and rhino. */ /** * Abstraction of the runtime environment. * @class * @interface */ function Runtime() {"use strict"; } /** * @param {!string} name * @return {*} */ Runtime.prototype.getVariable = function (name) { "use strict"; }; /** * @param {*} anything * @return {!string} */ Runtime.prototype.toJson = function (anything) { "use strict"; }; /** * @param {!string} jsonstr * @return {*} */ Runtime.prototype.fromJson = function (jsonstr) { "use strict"; }; /** * @param {!string} string * @param {!string} encoding * @return {!Uint8Array} */ Runtime.prototype.byteArrayFromString = function (string, encoding) {"use strict"; }; /** * @param {!Uint8Array} bytearray * @param {!string} encoding * @return {!string} */ Runtime.prototype.byteArrayToString = function (bytearray, encoding) {"use strict"; }; /** * Read part of a binary file. * @param {!string} path * @param {!number} offset * @param {!number} length * @param {!function(?string,?Uint8Array):undefined} callback * @return {undefined} */ Runtime.prototype.read = function (path, offset, length, callback) {"use strict"; }; /** * Read the contents of a file. Returns the result via a callback. If the * encoding is 'binary', the result is returned as a Uint8Array, * otherwise, it is returned as a string. * @param {!string} path * @param {!string} encoding text encoding or 'binary' * @param {!function(?string,?(string|Uint8Array)):undefined} callback * @return {undefined} */ Runtime.prototype.readFile = function (path, encoding, callback) {"use strict"; }; /** * Read a file completely, throw an exception if there is a problem. * @param {!string} path * @param {!string} encoding text encoding or 'binary' * @return {!string|!Uint8Array} */ Runtime.prototype.readFileSync = function (path, encoding) {"use strict"; }; /** * @param {!string} path * @param {!function(?string,?Document):undefined} callback * @return {undefined} */ Runtime.prototype.loadXML = function (path, callback) {"use strict"; }; /** * @param {!string} path * @param {!Uint8Array} data * @param {!function(?string):undefined} callback * @return {undefined} */ Runtime.prototype.writeFile = function (path, data, callback) {"use strict"; }; /** * @param {!string} path * @param {!function(?string):undefined} callback * @return {undefined} */ Runtime.prototype.deleteFile = function (path, callback) {"use strict"; }; /** * @param {!string} msgOrCategory * @param {!string=} msg * @return {undefined} */ Runtime.prototype.log = function (msgOrCategory, msg) {"use strict"; }; /** * @param {!function():undefined} callback * @param {!number} milliseconds * @return {!number} */ Runtime.prototype.setTimeout = function (callback, milliseconds) {"use strict"; }; /** * @param {!number} timeoutID * @return {undefined} */ Runtime.prototype.clearTimeout = function (timeoutID) {"use strict"; }; /** * @return {!Array.<string>} */ Runtime.prototype.libraryPaths = function () {"use strict"; }; /** * @return {!string} */ Runtime.prototype.currentDirectory = function () {"use strict"; }; /** * @param {!string} dir * @return {undefined} */ Runtime.prototype.setCurrentDirectory = function (dir) {"use strict"; }; /** * @return {string} */ Runtime.prototype.type = function () {"use strict"; }; /** * @return {?DOMImplementation} */ Runtime.prototype.getDOMImplementation = function () {"use strict"; }; /** * @param {!string} xml * @return {?Document} */ Runtime.prototype.parseXML = function (xml) {"use strict"; }; /** * @param {!number} exitCode */ Runtime.prototype.exit = function (exitCode) {"use strict"; }; /** * @return {?Window} */ Runtime.prototype.getWindow = function () {"use strict"; }; /** * @param {!function():undefined} callback * @return {!number} */ Runtime.prototype.requestAnimationFrame = function (callback) {"use strict"; }; /** * @param {!number} requestId * @return {undefined} */ Runtime.prototype.cancelAnimationFrame = function (requestId) {"use strict"; }; /** * @param {!boolean} condition * @param {!string} message * @return {undefined} */ Runtime.prototype.assert = function (condition, message) { "use strict"; }; /*jslint emptyblock: false, unparam: false */ /** @define {boolean} */ var IS_COMPILED_CODE = false; /** * @this {Runtime} * @param {!Uint8Array} bytearray * @param {!string} encoding * @return {!string} */ Runtime.byteArrayToString = function (bytearray, encoding) { "use strict"; /** * @param {!Uint8Array} bytearray * @return {!string} */ function byteArrayToString(bytearray) { var s = "", i, l = bytearray.length; for (i = 0; i < l; i += 1) { s += String.fromCharCode(bytearray[i] & 0xff); } return s; } /** * @param {!Uint8Array} bytearray * @return {!string} */ function utf8ByteArrayToString(bytearray) { var s = "", startPos, i, l = bytearray.length, chars = [], c0, c1, c2, c3, codepoint; // skip a possible UTF-8 BOM if (l >= 3 && bytearray[0] === 0xef && bytearray[1] === 0xbb && bytearray[2] === 0xbf) { startPos = 3; } else { startPos = 0; } for (i = startPos; i < l; i += 1) { c0 = /**@type{!number}*/(bytearray[i]); if (c0 < 0x80) { chars.push(c0); } else { i += 1; c1 = /**@type{!number}*/(bytearray[i]); if (c0 >= 0xc2 && c0 < 0xe0) { chars.push(((c0 & 0x1f) << 6) | (c1 & 0x3f)); } else { i += 1; c2 = /**@type{!number}*/(bytearray[i]); if (c0 >= 0xe0 && c0 < 0xf0) { chars.push(((c0 & 0x0f) << 12) | ((c1 & 0x3f) << 6) | (c2 & 0x3f)); } else { i += 1; c3 = /**@type{!number}*/(bytearray[i]); if (c0 >= 0xf0 && c0 < 0xf5) { codepoint = ((c0 & 0x07) << 18) | ((c1 & 0x3f) << 12) | ((c2 & 0x3f) << 6) | (c3 & 0x3f); codepoint -= 0x10000; chars.push((codepoint >> 10) + 0xd800, (codepoint & 0x3ff) + 0xdc00); } } } } if (chars.length >= 1000) { // more than 2 chars can be added in the above logic, so the length might exceed 1000 // Char-to-string conversion is done using apply as it provides a roughly %30 improvement vs. // converting the characters 1-by-1, and improves memory usage significantly as well. // However, the apply function has an upper limit on the size of the arguments array. If it is exceeded, // most browsers with throw a RangeError. Avoid this problem by converting no more than 1000(ish) // characters per call. s += String.fromCharCode.apply(null, chars); chars.length = 0; } } // Based on the above chars.length check, there is guaranteed to be less than 1000 chars left in the array return s + String.fromCharCode.apply(null, chars); } var result; if (encoding === "utf8") { result = utf8ByteArrayToString(bytearray); } else { if (encoding !== "binary") { this.log("Unsupported encoding: " + encoding); } result = byteArrayToString(bytearray); } return result; }; /** * @param {!string} name * @return {*} */ Runtime.getVariable = function (name) { "use strict"; /*jslint evil: true*/ try { return eval(name); } catch (e) { return undefined; } /*jslint evil: false*/ }; /** * @param {*} anything * @return {!string} */ Runtime.toJson = function (anything) { "use strict"; return JSON.stringify(anything); }; /** * @param {!string} jsonstr * @return {*} */ Runtime.fromJson = function (jsonstr) { "use strict"; return JSON.parse(jsonstr); }; /** * @param {!Function} f * @return {?string} */ Runtime.getFunctionName = function getFunctionName(f) { "use strict"; var m; if (f.name === undefined) { m = new RegExp("function\\s+(\\w+)").exec(f); return m && m[1]; } return f.name; }; /** * @this {Runtime} * @param {!boolean} condition * @param {!string} message * @return {undefined} */ Runtime.assert = function (condition, message) { "use strict"; if (!condition) { this.log("alert", "ASSERTION FAILED:\n" + message); throw new Error(message); // interrupt execution and provide a backtrace } }; /** * @class * @constructor * @augments Runtime * @implements {Runtime} */ function BrowserRuntime() { "use strict"; var self = this; /** * Return the number of bytes a string would take up when encoded as utf-8. * @param {string} string * @return {number} */ function getUtf8LengthForString(string) { var l = string.length, i, n, j = 0; for (i = 0; i < l; i += 1) { n = string.charCodeAt(i); j += 1 + (n > 0x80) + (n > 0x800); if (n > 0xd700 && n < 0xe000) { // first of a surrogate pair j += 1; i += 1; // skip second half of in surrogate pair } } return j; } /** * Convert UCS-2 string to UTF-8 array. * @param {string} string * @param {number} length the length of the resulting array * @param {boolean} addBOM whether or not to start with a BOM * @return {!Uint8Array} */ function utf8ByteArrayFromString(string, length, addBOM) { var l = string.length, bytearray, i, n, j; // allocate a buffer and convert to a utf8 array bytearray = new Uint8Array(new ArrayBuffer(length)); if (addBOM) { bytearray[0] = 0xef; bytearray[1] = 0xbb; bytearray[2] = 0xbf; j = 3; } else { j = 0; } for (i = 0; i < l; i += 1) { n = string.charCodeAt(i); if (n < 0x80) { bytearray[j] = n; j += 1; } else if (n < 0x800) { bytearray[j] = 0xc0 | (n >>> 6); bytearray[j + 1] = 0x80 | (n & 0x3f); j += 2; } else if (n <= 0xd700 || n >= 0xe000) { bytearray[j] = 0xe0 | ((n >>> 12) & 0x0f); bytearray[j + 1] = 0x80 | ((n >>> 6) & 0x3f); bytearray[j + 2] = 0x80 | (n & 0x3f); j += 3; } else { // surrogate pair i += 1; n = (((n - 0xd800) << 10) | (string.charCodeAt(i) - 0xdc00)) + 0x10000; bytearray[j] = 0xf0 | (n >>> 18 & 0x07); bytearray[j + 1] = 0x80 | (n >>> 12 & 0x3f); bytearray[j + 2] = 0x80 | (n >>> 6 & 0x3f); bytearray[j + 3] = 0x80 | (n & 0x3f); j += 4; } } return bytearray; } /** * Convert UCS-2 string to UTF-8 array. * wishLength is the desired length, if it is 3 bytes longer than * forsee by the string data, a BOM is prepended. * @param {string} string * @param {(number|string)=} wishLength * @return {!Uint8Array|undefined} */ function utf8ByteArrayFromXHRString(string, wishLength) { var addBOM = false, length = getUtf8LengthForString(string); if (typeof wishLength === "number") { if (wishLength !== length && wishLength !== length + 3) { // the desired length does not match the content of the string return undefined; } addBOM = length + 3 === wishLength; length = wishLength; } return utf8ByteArrayFromString(string, length, addBOM); } /** * @param {!string} string * @return {!Uint8Array} */ function byteArrayFromString(string) { // ignore encoding for now var l = string.length, a = new Uint8Array(new ArrayBuffer(l)), i; for (i = 0; i < l; i += 1) { a[i] = string.charCodeAt(i) & 0xff; } return a; } /** * @param {!string} string * @param {!string} encoding * @return {!Uint8Array} */ this.byteArrayFromString = function (string, encoding) { var result; if (encoding === "utf8") { result = utf8ByteArrayFromString(string, getUtf8LengthForString(string), false); } else { if (encoding !== "binary") { self.log("unknown encoding: " + encoding); } result = byteArrayFromString(string); } return result; }; this.byteArrayToString = Runtime.byteArrayToString; /** * @param {!string} name * @return {*} */ this.getVariable = Runtime.getVariable; /** * @param {!string} jsonstr * @return {*} */ this.fromJson = Runtime.fromJson; /** * @param {*} anything * @return {!string} */ this.toJson = Runtime.toJson; /** * @param {!string} msgOrCategory * @param {string=} msg * @return {undefined} */ function log(msgOrCategory, msg) { var category; if (msg !== undefined) { category = msgOrCategory; } else { msg = msgOrCategory; } console.log(msg); if (self.enableAlerts && category === "alert") { alert(msg); } } /** * @param {!Array.<!number>} buffer * @return {!Uint8Array} */ function arrayToUint8Array(buffer) { var l = buffer.length, i, a = new Uint8Array(new ArrayBuffer(l)); for (i = 0; i < l; i += 1) { a[i] = buffer[i]; } return a; } /** * Convert the text received by XHR to a byte array. * An XHR request can send a text as a response even though binary content * was requested. This text string should be converted to a byte array. * If the length of the text is equal to the reported length of the content * then each character becomes one byte. * If the length is different, which can happen on WebKit and Blink * browsers, the string should be converted while taking into account the * encoding. Currently, only utf8 is supported for this procedure. * @param {!XMLHttpRequest} xhr * @return {!Uint8Array} */ function stringToBinaryWorkaround(xhr) { var cl, data; cl = xhr.getResponseHeader("Content-Length"); if (cl) { cl = parseInt(cl, 10); } // If Content-Length was found and is a valid number that is not equal // to the length of the string, the byte array should be reconstructed // from the encoding. if (cl && cl !== xhr.responseText.length) { // The text is not simple ascii, so we assume it is utf8 and try to // reconstruct the text from that. data = utf8ByteArrayFromXHRString(xhr.responseText, cl); } if (data === undefined) { data = byteArrayFromString(xhr.responseText); } return data; } /** * @param {!string} path * @param {!string} encoding * @param {!XMLHttpRequest} xhr * @return {!{err:?string,data:(?string|?Uint8Array)}} */ function handleXHRResult(path, encoding, xhr) { var r, d, a, /**@type{!Uint8Array|!string}*/ data; if (xhr.status === 0 && !xhr.responseText) { // for local files there is no difference between missing // and empty files, so empty files are considered as errors r = {err: "File " + path + " is empty.", data: null}; } else if (xhr.status === 200 || xhr.status === 0) { // report file if (xhr.response && typeof xhr.response !== "string") { // w3c compliant way http://www.w3.org/TR/XMLHttpRequest2/#the-response-attribute if (encoding === "binary") { d = /**@type{!ArrayBuffer}*/(xhr.response); data = new Uint8Array(d); } else { data = String(xhr.response); } } else if (encoding === "binary") { if (xhr.responseBody !== null && String(typeof VBArray) !== "undefined") { // fallback for IE <= 10 a = (new VBArray(xhr.responseBody)).toArray(); data = arrayToUint8Array(a); } else { data = stringToBinaryWorkaround(xhr); } } else { // if we just want text, it's simple data = xhr.responseText; } r = {err: null, data: data}; } else { // report error r = {err: xhr.responseText || xhr.statusText, data: null}; } return r; } /** * @param {!string} path * @param {!string} encoding * @param {!boolean} async * @return {!XMLHttpRequest} */ function createXHR(path, encoding, async) { var xhr = new XMLHttpRequest(); xhr.open('GET', path, async); if (xhr.overrideMimeType) { if (encoding !== "binary") { xhr.overrideMimeType("text/plain; charset=" + encoding); } else { xhr.overrideMimeType("text/plain; charset=x-user-defined"); } } return xhr; } /** * Read the contents of a file. Returns the result via a callback. If the * encoding is 'binary', the result is returned as a Uint8Array, * otherwise, it is returned as a string. * @param {!string} path * @param {!string} encoding text encoding or 'binary' * @param {!function(?string,?(string|Uint8Array)):undefined} callback * @return {undefined} */ function readFile(path, encoding, callback) { var xhr = createXHR(path, encoding, true); function handleResult() { var r; if (xhr.readyState === 4) { r = handleXHRResult(path, encoding, xhr); callback(r.err, r.data); } } xhr.onreadystatechange = handleResult; try { xhr.send(null); } catch (/**@type{!Error}*/e) { callback(e.message, null); } } /** * @param {!string} path * @param {!number} offset * @param {!number} length * @param {!function(?string,?Uint8Array):undefined} callback * @return {undefined} */ function read(path, offset, length, callback) { readFile(path, "binary", function (err, result) { var r = null; if (result) { if (typeof result === "string") { throw "This should not happen."; } r = /**@type{!Uint8Array}*/(result.subarray(offset, offset + length)); } callback(err, r); }); } /** * @param {!string} path * @param {!string} encoding text encoding or 'binary' * @return {!string|!Uint8Array} */ function readFileSync(path, encoding) { var xhr = createXHR(path, encoding, false), r; try { xhr.send(null); r = handleXHRResult(path, encoding, xhr); if (r.err) { throw r.err; } if (r.data === null) { throw "No data read from " + path + "."; } } catch (/**@type{!Error}*/e) { throw e; } return r.data; } /** * @param {!string} path * @param {!Uint8Array} data * @param {!function(?string):undefined} callback * @return {undefined} */ function writeFile(path, data, callback) { var xhr = new XMLHttpRequest(), /**@type{!string|!ArrayBuffer}*/ d; function handleResult() { if (xhr.readyState === 4) { if (xhr.status === 0 && !xhr.responseText) { // for local files there is no difference between missing // and empty files, so empty files are considered as errors callback("File " + path + " is empty."); } else if ((xhr.status >= 200 && xhr.status < 300) || xhr.status === 0) { // report success callback(null); } else { // report error callback("Status " + String(xhr.status) + ": " + xhr.responseText || xhr.statusText); } } } xhr.open('PUT', path, true); xhr.onreadystatechange = handleResult; // ArrayBufferView will have an ArrayBuffer property, in WebKit, XHR // can send() an ArrayBuffer, In Firefox, one must use sendAsBinary with // a string if (data.buffer && !xhr.sendAsBinary) { d = data.buffer; // webkit supports sending an ArrayBuffer } else { // encode into a string, this works in FireFox >= 3 d = self.byteArrayToString(data, "binary"); } try { if (xhr.sendAsBinary) { xhr.sendAsBinary(d); } else { xhr.send(d); } } catch (/**@type{!Error}*/e) { self.log("HUH? " + e + " " + data); callback(e.message); } } /** * @param {!string} path * @param {!function(?string):undefined} callback * @return {undefined} */ function deleteFile(path, callback) { var xhr = new XMLHttpRequest(); xhr.open('DELETE', path, true); xhr.onreadystatechange = function () { if (xhr.readyState === 4) { if (xhr.status < 200 && xhr.status >= 300) { callback(xhr.responseText); } else { callback(null); } } }; xhr.send(null); } /** * @param {!string} path * @param {!function(?string,?Document):undefined} callback * @return {undefined} */ function loadXML(path, callback) { var xhr = new XMLHttpRequest(); function handleResult() { if (xhr.readyState === 4) { if (xhr.status === 0 && !xhr.responseText) { callback("File " + path + " is empty.", null); } else if (xhr.status === 200 || xhr.status === 0) { // report file callback(null, xhr.responseXML); } else { // report error callback(xhr.responseText, null); } } } xhr.open("GET", path, true); if (xhr.overrideMimeType) { xhr.overrideMimeType("text/xml"); } xhr.onreadystatechange = handleResult; try { xhr.send(null); } catch (/**@type{!Error}*/e) { callback(e.message, null); } } this.readFile = readFile; this.read = read; this.readFileSync = readFileSync; this.writeFile = writeFile; this.deleteFile = deleteFile; this.loadXML = loadXML; this.log = log; this.enableAlerts = true; this.assert = Runtime.assert; /** * @param {!function():undefined} f * @param {!number} msec * @return {!number} */ this.setTimeout = function (f, msec) { return setTimeout(function () { f(); }, msec); }; /** * @param {!number} timeoutID * @return {undefined} */ this.clearTimeout = function (timeoutID) { clearTimeout(timeoutID); }; /** * @return {!Array.<!string>} */ this.libraryPaths = function () { return ["lib"]; // TODO: find a good solution // probably let html app specify it }; /*jslint emptyblock: true*/ this.setCurrentDirectory = function () { }; /*jslint emptyblock: false*/ /** * @return {!string} */ this.currentDirectory = function () { return ""; }; this.type = function () { return "BrowserRuntime"; }; this.getDOMImplementation = function () { return window.document.implementation; }; /** * @param {!string} xml * @return {?Document} */ this.parseXML = function (xml) { var parser = new DOMParser(); return parser.parseFromString(xml, "text/xml"); }; /** * @param {!number} exitCode */ this.exit = function (exitCode) { log("Calling exit with code " + String(exitCode) + ", but exit() is not implemented."); }; /** * @return {!Window} */ this.getWindow = function () { return window; }; /** * @param {!function():undefined} callback * @return {!number} */ this.requestAnimationFrame = function (callback) { var rAF = window.requestAnimationFrame || window.webkitRequestAnimationFrame || window.mozRequestAnimationFrame || window.msRequestAnimationFrame, requestId = 0; if (rAF) { // This code is to satisfy Closure, which expects // that the `this` of rAF should be window. rAF.bind(window); requestId = /**@type{function(!function():undefined):!number}*/(rAF)(callback); } else { return setTimeout(callback, 15); } return requestId; }; /** * @param {!number} requestId * @return {undefined} */ this.cancelAnimationFrame = function (requestId) { var cAF = window.cancelAnimationFrame || window.webkitCancelAnimationFrame || window.mozCancelAnimationFrame || window.msCancelAnimationFrame; if (cAF) { cAF.bind(window); /**@type{function(!number)}*/(cAF)(requestId); } else { clearTimeout(requestId); } }; } /** * @constructor * @implements {Runtime} */ function NodeJSRuntime() { "use strict"; var self = this, fs = require('fs'), pathmod = require('path'), /**@type{!string}*/ currentDirectory = "", /**@type{!DOMParser}*/ parser, domImplementation; /** * @param {!Buffer} buffer * @return {!Uint8Array} */ function bufferToUint8Array(buffer) { var l = buffer.length, i, a = new Uint8Array(new ArrayBuffer(l)); for (i = 0; i < l; i += 1) { a[i] = buffer[i]; } return a; } /** * @param {!string} string * @param {!string} encoding * @return {!Uint8Array} */ this.byteArrayFromString = function (string, encoding) { var buf = new Buffer(string, encoding), i, l = buf.length, a = new Uint8Array(new ArrayBuffer(l)); for (i = 0; i < l; i += 1) { a[i] = buf[i]; } return a; }; this.byteArrayToString = Runtime.byteArrayToString; /** * @param {!string} name * @return {*} */ this.getVariable = Runtime.getVariable; /** * @param {!string} jsonstr * @return {*} */ this.fromJson = Runtime.fromJson; /** * @param {*} anything * @return {!string} */ this.toJson = Runtime.toJson; /** * Read the contents of a file. Returns the result via a callback. If the * encoding is 'binary', the result is returned as a Uint8Array, * otherwise, it is returned as a string. * @param {!string} path * @param {!string} encoding text encoding or 'binary' * @param {!function(?string,?(string|Uint8Array)):undefined} callback * @return {undefined} */ function readFile(path, encoding, callback) { /** * @param {?string} err * @param {?Buffer|?string} data * @return {undefined} */ function convert(err, data) { if (err) { return callback(err, null); } if (!data) { return callback("No data for " + path + ".", null); } var d; if (typeof data === "string") { d = /**@type{!string}*/(data); return callback(err, d); } d = /**@type{!Buffer}*/(data); callback(err, bufferToUint8Array(d)); } path = pathmod.resolve(currentDirectory, path); if (encoding !== "binary") { fs.readFile(path, encoding, convert); } else { fs.readFile(path, null, convert); } } this.readFile = readFile; /** * @param {!string} path * @param {!function(?string,?Document):undefined} callback * @return {undefined} */ function loadXML(path, callback) { readFile(path, "utf-8", function (err, data) { if (err) { return callback(err, null); } if (!data) { return callback("No data for " + path + ".", null); } var d = /**@type{!string}*/(data); callback(null, self.parseXML(d)); }); } this.loadXML = loadXML; /** * @param {!string} path * @param {!Uint8Array} data * @param {!function(?string):undefined} callback * @return {undefined} */ this.writeFile = function (path, data, callback) { var buf = new Buffer(data); path = pathmod.resolve(currentDirectory, path); fs.writeFile(path, buf, "binary", function (err) { callback(err || null); }); }; /** * @param {!string} path * @param {!function(?string):undefined} callback * @return {undefined} */ this.deleteFile = function (path, callback) { path = pathmod.resolve(currentDirectory, path); fs.unlink(path, callback); }; /** * @param {!string} path * @param {!number} offset * @param {!number} length * @param {!function(?string,?Uint8Array):undefined} callback * @return {undefined} */ this.read = function (path, offset, length, callback) { path = pathmod.resolve(currentDirectory, path); fs.open(path, "r+", 666, function (err, fd) { if (err) { callback(err, null); return; } var buffer = new Buffer(length); fs.read(fd, buffer, 0, length, offset, function (err) { fs.close(fd); callback(err, bufferToUint8Array(buffer)); }); }); }; /** * @param {!string} path * @param {!string} encoding text encoding or 'binary' * @return {!string|!Uint8Array} */ this.readFileSync = function (path, encoding) { var s, enc = (encoding === "binary") ? null : encoding, r = fs.readFileSync(path, enc); if (r === null) { throw "File " + path + " could not be read."; } if (encoding === "binary") { s = /**@type{!Buffer}*/(r); s = bufferToUint8Array(s); } else { s = /**@type{!string}*/(r); } return s; }; /** * @param {!string} msgOrCategory * @param {string=} msg * @return {undefined} */ function log(msgOrCategory, msg) { var category; if (msg !== undefined) { category = msgOrCategory; } else { msg = msgOrCategory; } if (category === "alert") { process.stderr.write("\n!!!!! ALERT !!!!!" + '\n'); } process.stderr.write(msg + '\n'); if (category === "alert") { process.stderr.write("!!!!! ALERT !!!!!" + '\n'); } } this.log = log; this.assert = Runtime.assert; /** * @param {!function():undefined} f * @param {!number} msec * @return {!number} */ this.setTimeout = function (f, msec) { return setTimeout(function () { f(); }, msec); }; /** * @param {!number} timeoutID * @return {undefined} */ this.clearTimeout = function (timeoutID) { clearTimeout(timeoutID); }; /** * @return {!Array.<!string>} */ this.libraryPaths = function () { return [__dirname]; }; /** * @param {!string} dir * @return {undefined} */ this.setCurrentDirectory = function (dir) { currentDirectory = dir; }; this.currentDirectory = function () { return currentDirectory; }; this.type = function () { return "NodeJSRuntime"; }; this.getDOMImplementation = function () { return domImplementation; }; /** * @param {!string} xml * @return {?Document} */ this.parseXML = function (xml) { return parser.parseFromString(xml, "text/xml"); }; this.exit = process.exit; this.getWindow = function () { return null; }; /** * @param {!function():undefined} callback * @return {!number} */ this.requestAnimationFrame = function (callback) { return setTimeout(callback, 15); }; /** * @param {!number} requestId * @return {undefined} */ this.cancelAnimationFrame = function (requestId) { clearTimeout(requestId); }; function init() { var /**@type{function(new:DOMParser)}*/ DOMParser = require('xmldom').DOMParser; parser = new DOMParser(); domImplementation = self.parseXML("<a/>").implementation; } init(); } /** * @constructor * @implements {Runtime} */ function RhinoRuntime() { "use strict"; var self = this, Packages = {}, dom = Packages.javax.xml.parsers.DocumentBuilderFactory.newInstance(), /**@type{!Packages.javax.xml.parsers.DocumentBuilder}*/ builder, entityresolver, /**@type{!string}*/ currentDirectory = ""; dom.setValidating(false); dom.setNamespaceAware(true); dom.setExpandEntityReferences(false); dom.setSchema(null); /*jslint unparam: true */ entityresolver = Packages.org.xml.sax.EntityResolver({ /** * @param {!string} publicId * @param {!string} systemId * @return {!Packages.org.xml.sax.InputSource} */ resolveEntity: function (publicId, systemId) { var file; /** * @param {!string} path * @return {!Packages.org.xml.sax.InputSource} */ function open(path) { var reader = new Packages.java.io.FileReader(path), source = new Packages.org.xml.sax.InputSource(reader); return source; } file = systemId; //file = /[^\/]*$/.exec(systemId); // what should this do? return open(file); } }); /*jslint unparam: false */ //dom.setEntityResolver(entityresolver); builder = dom.newDocumentBuilder(); builder.setEntityResolver(entityresolver); /*jslint unparam: true*/ /** * @param {!string} string * @param {!string} encoding * @return {!Uint8Array} */ this.byteArrayFromString = function (string, encoding) { // ignore encoding for now var i, l = string.length, a = new Uint8Array(new ArrayBuffer(l)); for (i = 0; i < l; i += 1) { a[i] = string.charCodeAt(i) & 0xff; } return a; }; /*jslint unparam: false*/ this.byteArrayToString = Runtime.byteArrayToString; /** * @param {!string} name * @return {*} */ this.getVariable = Runtime.getVariable; /** * @param {!string} jsonstr * @return {*} */ this.fromJson = Runtime.fromJson; /** * @param {*} anything * @return {!string} */ this.toJson = Runtime.toJson; /** * @param {!string} path * @param {!function(?string,?Document):undefined} callback * @return {undefined} */ function loadXML(path, callback) { var file = new Packages.java.io.File(path), xmlDocument = null; try { xmlDocument = builder.parse(file); } catch (/**@type{!string}*/err) { print(err); return callback(err, null); } callback(null, xmlDocument); } /** * @param {!string} path * @param {!string} encoding text encoding or 'binary' * @param {!function(?string,?(string|Uint8Array)):undefined} callback * @return {undefined} */ function runtimeReadFile(path, encoding, callback) { if (currentDirectory) { path = currentDirectory + "/" + path; } var file = new Packages.java.io.File(path), data, // read binary, seems hacky but works rhinoencoding = (encoding === "binary") ? "latin1" : encoding; if (!file.isFile()) { callback(path + " is not a file.", null); } else { data = readFile(path, rhinoencoding); if (data && encoding === "binary") { data = self.byteArrayFromString(data, "binary"); } callback(null, data); } } /** * @param {!string} path * @param {!string} encoding * @return {?string} */ function runtimeReadFileSync(path, encoding) { var file = new Packages.java.io.File(path); if (!file.isFile()) { return null; } if (encoding === "binary") { encoding = "latin1"; // read binary, seems hacky but works } return readFile(path, encoding); } this.loadXML = loadXML; this.readFile = runtimeReadFile; /** * @param {!string} path * @param {!Uint8Array} data * @param {!function(?string):undefined} callback * @return {undefined} */ this.writeFile = function (path, data, callback) { if (currentDirectory) { path = currentDirectory + "/" + path; } var out = new Packages.java.io.FileOutputStream(path), i, l = data.length; for (i = 0; i < l; i += 1) { out.write(data[i]); } out.close(); callback(null); }; /** * @param {!string} path * @param {!function(?string):undefined} callback * @return {undefined} */ this.deleteFile = function (path, callback) { if (currentDirectory) { path = currentDirectory + "/" + path; } var file = new Packages.java.io.File(path), otherPath = path + Math.random(), other = new Packages.java.io.File(otherPath); // 'delete' cannot be used with closure compiler, so we use a workaround if (file.rename(other)) { other.deleteOnExit(); callback(null); } else { callback("Could not delete " + path); } }; /** * @param {!string} path * @param {!number} offset * @param {!number} length * @param {!function(?string,?Uint8Array):undefined} callback * @return {undefined} */ this.read = function (path, offset, length, callback) { // TODO: adapt to read only a part instead of the whole file if (currentDirectory) { path = currentDirectory + "/" + path; } var data = runtimeReadFileSync(path, "binary"); if (data) { callback(null, this.byteArrayFromString( data.substring(offset, offset + length), "binary" )); } else { callback("Cannot read " + path, null); } }; /** * @param {!string} path * @param {!string} encoding text encoding or 'binary' * @return {!string} */ this.readFileSync = function (path, encoding) { if (!encoding) { return ""; } var s = readFile(path, encoding); if (s === null) { throw "File could not be read."; } return s; }; /** * @param {!string} msgOrCategory * @param {string=} msg * @return {undefined} */ function log(msgOrCategory, msg) { var category; if (msg !== undefined) { category = msgOrCategory; } else { msg = msgOrCategory; } if (category === "alert") { print("\n!!!!! ALERT !!!!!"); } print(msg); if (category === "alert") { print("!!!!! ALERT !!!!!"); } } this.log = log; this.assert = Runtime.assert; /** * @param {!function():undefined} f * @return {!number} */ this.setTimeout = function (f) { f(); return 0; }; /*jslint emptyblock: true */ /** * @return {undefined} */ this.clearTimeout = function () { }; /*jslint emptyblock: false */ /** * @return {!Array.<!string>} */ this.libraryPaths = function () { return ["lib"]; }; /** * @param {!string} dir */ this.setCurrentDirectory = function (dir) { currentDirectory = dir; }; this.currentDirectory = function () { return currentDirectory; }; this.type = function () { return "RhinoRuntime"; }; this.getDOMImplementation = function () { return builder.getDOMImplementation(); }; /** * @param {!string} xml * @return {?Document} */ this.parseXML = function (xml) { var reader = new Packages.java.io.StringReader(xml), source = new Packages.org.xml.sax.InputSource(reader); return builder.parse(source); }; this.exit = quit; this.getWindow = function () { return null; }; /** * @param {!function():undefined} callback * @return {!number} */ this.requestAnimationFrame = function (callback) { callback(); return 0; }; /*jslint emptyblock: true */ /** * @return {undefined} */ this.cancelAnimationFrame = function () { }; /*jslint emptyblock: false */ } /** * @return {!Runtime} */ Runtime.create = function create() { "use strict"; var /**@type{!Runtime}*/ result; if (String(typeof window) !== "undefined") { result = new BrowserRuntime(); } else if (String(typeof require) !== "undefined") { result = new NodeJSRuntime(); } else { result = new RhinoRuntime(); } return result; }; /** * @const * @type {!Runtime} */ var runtime = Runtime.create(); /** * @namespace The core package. */ var core = {}; /** * @namespace The gui package. */ var gui = {}; /** * @namespace The xmldom package. */ var xmldom = {}; /** * @namespace The ODF package. */ var odf = {}; /** * @namespace The editing operations */ var ops = {}; /** * @namespace The webodf namespace */ var webodf = {}; (function () { "use strict"; /** * @return {string} */ function getWebODFVersion() { var version = (String(typeof webodf_version) !== "undefined" ? webodf_version : "From Source" ); return version; } /** * @const * @type {!string} */ webodf.Version = getWebODFVersion(); }()); /*jslint sloppy: true*/ (function () { /** * @param {string} dir Needs to be non-empty, use "." to denote same directory * @param {!Object.<string,!{dir:string, deps:!Array.<string>}>} dependencies * @param {!boolean=} expectFail Set to true if it is not known if there is a manifest */ function loadDependenciesFromManifest(dir, dependencies, expectFail) { "use strict"; var path = dir + "/manifest.json", content, list, manifest, /**@type{string}*/ m; runtime.log("Loading manifest: "+path); try { content = runtime.readFileSync(path, "utf-8"); } catch (/**@type{string}*/e) { if (expectFail) { runtime.log("No loadable manifest found."); } else { console.log(String(e)); throw e; } return; } list = JSON.parse(/**@type{string}*/(content)); manifest = /**@type{!Object.<!Array.<string>>}*/(list); for (m in manifest) { if (manifest.hasOwnProperty(m)) { dependencies[m] = {dir: dir, deps: manifest[m]}; } } } /** * @return {!Object.<string,!{dir:string, deps:!Array.<string>}>} */ function loadDependenciesFromManifests() { "use strict"; var /**@type{!Object.<string,!{dir:string, deps:!Array.<string>}>}*/ dependencies = [], paths = runtime.libraryPaths(), i; // Convenience: try to load any possible manifest in the current directory // but only if it not also included in the library paths if (runtime.currentDirectory() && paths.indexOf(runtime.currentDirectory()) === -1) { // there is no need to have a manifest there, so allow loading to fail loadDependenciesFromManifest(runtime.currentDirectory(), dependencies, true); } for (i = 0; i < paths.length; i += 1) { loadDependenciesFromManifest(paths[i], dependencies); } return dependencies; } /** * @param {string} dir * @param {string} className * @return {string} */ function getPath(dir, className) { "use strict"; return dir + "/" + className.replace(".", "/") + ".js"; } /** * Create a list of required classes from a list of desired classes. * A new list is created that lists all classes that still need to be loaded * to load the list of desired classes. * @param {!Array.<string>} classNames * @param {!Object.<string,!{dir:string, deps:!Array.<string>}>} dependencies * @param {function(string):boolean} isDefined * @return {!Array.<string>} */ function getLoadList(classNames, dependencies, isDefined) { "use strict"; var loadList = [], stack = {}, /**@type{!Object.<stri