node-webodf
Version:
WebODF - JavaScript Document Engine http://webodf.org/
682 lines (651 loc) • 25.2 kB
JavaScript
/**
* Copyright (C) 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/
*/
/*global runtime, process, require, console*/
var exitCode = 0;
function Main(cmakeListPath) {
"use strict";
var pathModule = require("path"),
fs = require("fs"),
buildDir = pathModule.dirname(cmakeListPath),
// these files are not compiled into webodf.js
ignoredFiles = [
"externs/JSZip.js",
"odf/CommandLineTools.js",
"xmldom/RelaxNG.js",
"xmldom/RelaxNG2.js",
"xmldom/RelaxNGParser.js"
],
jslintconfig;
function className(path) {
return path.substr(0, path.length - 3).replace(pathModule.sep, '.');
}
function add(occs, key, value) {
var a = occs[key] || [];
occs[key] = a;
if (a.indexOf(value) === -1) {
a.push(value);
}
}
/**
* Scan javascript source for the given symbols.
* @param {string} source
* @param {!Array.<string>} symbols
* @param {!Object.<string,string> files
* @param {!Object.<string,!Array.<string,>>
*/
function findOccurances(source, symbols, files, occs) {
symbols = symbols.join("|").split(".").join("\\.");
var classname,
content,
regex = new RegExp("\\b(" + symbols + ")\\b");
for (classname in files) {
if (files.hasOwnProperty(classname)) {
content = files[classname];
if (source !== classname && regex.test(content)) {
add(occs, classname, source);
}
}
}
}
/**
* Determine if the javascript source defines an ops.Operation.
*/
function isOperation(fileContent) {
return (/ \* @implements ops\.Operation\b/).test(fileContent);
}
function getStyle(path, content, colors) {
var constructor = content.indexOf("@constructor") !== -1,
iface = content.indexOf("@interface") !== -1,
style = (iface && !constructor) ? "\"\"" : "filled",
group = path.split("/"),
color;
group = group.length > 1 ? group[0] : "";
color = colors.indexOf(group);
if (color === -1) {
color = colors.length;
colors.push(group);
}
return " [color=" + (color + 1) + ", style=" + style + "];\n";
}
/**
* Create a dot file.
* This function returns a string that can be saved to a .dot file.
*/
function createDotFile(occs, files) {
var i, j, m, n, done = {}, out, content, colors = [];
out = "# This is a graphviz file.\n";
out += "# dot -Tsvg dependencies.dot > dependencies.svg\n";
out += "# dot -Tpng dependencies.dot > dependencies.png\n";
out += "digraph webodf {\n";
out += "node [colorscheme=pastel19]\n"; // pastel19, set312 or accent8
for (i in occs) {
if (occs.hasOwnProperty(i)) {
m = occs[i];
for (j = 0; j < m.length; j += 1) {
content = files[pathModule.join("lib", i)];
i = isOperation(content) ? "{Operation}" : i;
if (!done[i]) {
done[i] = {};
out += '"' + i + '"' + getStyle(i, content, colors);
}
n = m[j];
content = files[pathModule.join("lib", n)];
// omit leaf nodes unless they are interfaces
if (occs[n].length || content.indexOf("@interface") !== -1) {
n = isOperation(content) ? "{Operation}" : n;
if (!done[i].hasOwnProperty(n)) {
if (!done[n]) {
done[n] = {};
out += '"' + n + '"' + getStyle(n, content, colors);
}
out += '"' + i + '" -> "' + n + '";\n';
done[i][n] = 1;
}
}
}
}
}
out += "}\n";
return out;
}
/**
* @param {string} path
* @param {string} content
* @return {!Array.<string>}
*/
function getTopLevelDefines(path, content) {
var names = path.split(pathModule.sep),
re,
defines = {},
match,
m;
re = new RegExp("^\\s*(" + names[1] + "\\.[\\w.]+) = ", "gm");
if (names.length !== 3) {
throw path + " Only two levels are supported.";
}
match = re.exec(content);
while (match !== null) {
m = match[1];
defines[m] = 1;
match = re.exec(content);
}
if (Object.keys(defines).length === 0) {
throw "No defines found in " + path;
}
return Object.keys(defines);
}
/**
* Save data to a file if the contents is different then it currently is.
* If the file is written anew, saveCallback is called.
*/
function saveIfDifferent(path, content, saveCallback) {
fs.readFile(path, "utf8", function (err, data) {
if (err || data !== content) {
if (!err && data.length !== content.length) {
console.log("Different file length for " + path
+ ": " + data.length + " " + content.length);
}
fs.writeFile(path, content, function (err2) {
if (err2) {
throw err2;
}
if (saveCallback) {
saveCallback();
}
});
}
});
}
/**
* @param {!Array.<string>} list
* @param {!Object.<string,!Array.<string>>} deps
* @param {!Object.<string,boolean>} defined
* @return {!Array.<string>} list
*/
function createOrderedList(list, deps, defined) {
var sorted = [],
stack = {};
/**
* @param {string} n
* @param {string} parent
*/
function visit(n, parent) {
if (defined[n]) {
return;
}
if (stack[n]) {
throw "Circular dependency caused by " + n + " and " + parent;
}
stack[n] = true;
var d = deps[n], i, l = d.length;
for (i = 0; i < l; i += 1) {
visit(d[i], n);
}
stack[n] = false;
defined[n] = true;
sorted.push(n);
}
list.forEach(visit);
return sorted;
}
function createCMakeLists(typed) {
var path = cmakeListPath, content;
content = "set(LIBJSFILES\n" +
// include webodfversion.js before runtime.js, latter uses var from the first
" ${CMAKE_BINARY_DIR}/webodf/webodfversion.js\n" +
" lib/runtime.js\n lib/" +
typed.join("\n lib/") + "\n)\n";
saveIfDifferent(path, content, function () {
console.log("JS file dependencies were updated.");
exitCode = 1;
});
}
/**
* Replace the platform-specific file separator with forward slash '/'
* @param {!string} path
* @return {!string}
*/
function deNormalizePath(path) {
return path.replace(pathModule.sep, "/");
}
/**
* Returns true if the supplied path is compiled into webodf.js
* @param {!string} path
* @return {!boolean}
*/
function isCompiledJs(path) {
return ignoredFiles.indexOf(deNormalizePath(path)) === -1;
}
function updateCMakeLists(deps) {
var lib = deps.lib,
sortedTyped,
compiledFiles;
compiledFiles = Object.keys(lib).sort();
sortedTyped = createOrderedList(compiledFiles, lib, {});
createCMakeLists(sortedTyped.filter(isCompiledJs).map(deNormalizePath));
}
function updateKarmaConfig(deps) {
var path = "tools/karma.conf.js",
lib = deps.lib,
modules = createOrderedList(Object.keys(deps.lib).sort(), lib, {});
fs.readFile(path, "utf8", function (err, content) {
if (err) {
throw err;
}
var re = new RegExp("// MODULES\r?\n[^!]+!");
content = content.replace(re,
"// MODULES\n 'lib/" +
modules.map(deNormalizePath)
.join("',\n 'lib/") + "', // !");
saveIfDifferent(path, content, function () {
console.log("karma.conf.js was updated. Rerun the build.");
});
});
}
/**
* Deterministic serialization of manifest.
* JSON.stringify does not order objects by key.
*/
function serializeManifest(manifest) {
var out = "{\n",
list = Object.keys(manifest).sort(),
a,
i,
j;
for (j = 0; j < list.length; j += 1) {
a = manifest[list[j]];
out += ' "' + className(list[j]) + '": [\n';
for (i = 0; i < a.length; i += 1) {
out += ' "' + className(a[i]);
out += i === a.length - 1 ? '"\n' : '",\n';
}
out += j === list.length - 1 ? ' ]\n' : ' ],\n';
}
out += "}";
return out;
}
/**
* Remove dependencies that depend on other dependencies.
* @param {!Object.<string,!Array.<string>>} deps
*/
function reduce(deps) {
// return true if a depends on b
function dependsOn(a, b) {
return a === b || deps[b].some(function (c) {
return dependsOn(a, c);
});
}
Object.keys(deps).forEach(function (key) {
var d = deps[key],
i = 0,
dep,
redundant;
function f(a) {
return dep !== a && dependsOn(dep, a);
}
while (i < d.length) {
dep = d[i];
redundant = d.some(f);
if (redundant) {
d.splice(i, 1);
} else {
i += 1;
}
}
});
}
/**
* @param {!Object.<string,string>}
* @param {!Array.<string>}
* @return {!Object.<string,!Array.<string>>}
*/
this.createManifestsAndCMakeLists = function (files, dirs) {
var list = Object.keys(files),
classes = {},
paths = {},
defines = {},
occs = {},
deps = {},
classname,
d,
i,
j;
list.sort();
// make map with classname as key and file content as value
list.forEach(function (path) {
var k, dir, name;
for (k = 0; k < dirs.length; k += 1) {
dir = dirs[k];
if (path.indexOf(dir) === 0) {
name = className(path.substr(dir.length));
if (classes.hasOwnProperty(name)) {
throw name + " occurs more than once.";
}
classes[name] = files[path];
defines[name] = getTopLevelDefines(path, files[path]);
paths[name] = path;
}
}
});
list = Object.keys(classes);
for (i = 0; i < list.length; i += 1) {
occs[list[i]] = [];
}
for (i = 0; i < list.length; i += 1) {
classname = list[i];
d = defines[classname];
findOccurances(classname, d, classes, occs);
}
function prefixDir(cn) {
var p = paths[cn],
n = p.indexOf(pathModule.sep);
return p.substr(n + 1);
}
for (classname in occs) {
if (occs.hasOwnProperty(classname)) {
d = paths[classname];
i = d.indexOf(pathModule.sep);
j = d.substr(i + 1);
i = d.substr(0, i);
d = deps[i] = deps[i] || {};
d[j] = occs[classname].map(prefixDir).sort();
}
}
saveIfDifferent(pathModule.join(buildDir, "dependencies-full.dot"),
createDotFile(deps.lib, files));
reduce(deps.lib);
saveIfDifferent(pathModule.join(buildDir, "dependencies.dot"),
createDotFile(deps.lib, files));
for (i = 0; i < dirs.length; i += 1) {
d = dirs[i];
j = deps[d.split(pathModule.sep)[0]];
saveIfDifferent(d + "manifest.json", serializeManifest(j));
}
updateCMakeLists(deps);
updateKarmaConfig(deps);
};
/**
* Load content of a directory recursively.
* Directories are listed as Objects and files are read as text and stored
* as strings.
* The filter allows loading only a subset of files based on file or
* directory name.
* @param {!Array.<string>} dirs
* @param {function(string,boolean):boolean} filter
* @param {function(!Object}:undefined} callback
* @return {undefined}
*/
this.readFiles = function (dirs, filter, callback) {
var contents = {},
read;
function readMore() {
var done = true,
p,
content;
for (p in contents) {
if (contents.hasOwnProperty(p)) {
content = contents[p];
if (content === undefined) {
contents[p] = null; // null means it's loading
read(p);
}
if (content === null || content === undefined) {
done = false;
}
}
}
if (done) {
callback(contents);
}
}
function readFile(filepath) {
fs.readFile(filepath, "utf8", function (err, data) {
if (err) {
throw err;
}
contents[filepath] = data;
readMore();
});
}
function readDirectory(dirpath) {
fs.readdir(dirpath, function (err, files) {
if (err) {
throw err;
}
var i;
files.sort();
for (i = 0; i < files.length; i += 1) {
contents[pathModule.join(dirpath, files[i])] = undefined;
}
contents[dirpath] = files;
readMore();
});
}
read = function (p) {
fs.stat(p, function (err, stats) {
if (err) {
throw err;
}
var ok = filter(p, stats.isFile());
if (ok && stats.isFile()) {
readFile(p);
} else if (ok && stats.isDirectory()) {
readDirectory(p);
} else {
contents[p] = -1;
readMore();
}
});
};
dirs.forEach(function (dir) {
contents[dir] = undefined;
});
readMore();
};
jslintconfig = {
ass: false, // if assignment expressions should be allowed
bitwise: false, // if bitwise operators should be allowed
browser: false, // if the standard browser globals should be predefined
closure: true, // if Google Closure idioms should be tolerated
'continue': false, // if the continuation statement should be tolerated
debug: false, // if debugger statements should be allowed
defined: false, // if already defined variables are allowed
devel: false, // if logging should be allowed (console, alert, etc.)
emptyblock: false, // if empty blocks should be allowed
eqeq: false, // if == should be allowed
evil: false, // if eval should be allowed
forin: false, // if for in statements need not filter
indent: 4, //the indentation factor
maxerr: 20, //the maximum number of errors to allow
//maxlen: 300 the maximum length of a source line
newcap: false, // if constructor names capitalization is ignored
node: false, // if Node.js globals should be predefined
nomen: false, // if names may have dangling _
passfail: false, // if the scan should stop on first error
plusplus: false, // if increment/decrement should be allowed
properties: false, // if all property names must be declared with /*properties*/
regexp: false, // if the . should be allowed in regexp literals
rhino: false, // if the Rhino environment globals should be predefined
unparam: false, // if unused parameters should be tolerated
sloppy: false, // if the 'use strict'; pragma is optional
stupid: true, // if really stupid practices are tolerated
sub: false, // if all forms of subscript notation are tolerated
todo: true, // if TODO comments are tolerated
unvar: false, // if unused variables should be tolerated
vars: false, // if multiple var statements per function should be allowed
white: true // if sloppy whitespace is tolerated
};
function mkdir(path) {
if (fs.existsSync(path)) {
return;
}
var parent = pathModule.dirname(path);
mkdir(parent);
fs.mkdirSync(path);
}
function runJSLint(jslint, path, contents) {
var lpath = pathModule.join(buildDir, "jslint", path);
// checking jslint is quite slow, so only do it if the file content is
// different from the file content at the previous successful run.
fs.readFile(lpath, "utf8", function (err, data) {
if (data === contents) {
return;
}
var result, i, errors;
result = jslint(contents, jslintconfig);
if (result) {
// The file has no errors, save it so it will be skipped at the
// next run.
mkdir(pathModule.dirname(lpath));
fs.writeFile(lpath, contents);
} else {
errors = jslint.errors;
for (i = 0; i < errors.length && errors[i]; i += 1) {
err = errors[i];
console.log(path + ":" + err.line + ":" + err.character +
": error: " + err.reason);
}
exitCode = 1;
}
});
}
/**
* @param {!string} path
* @param {!string} content
* @return {!string}
*/
function getLicense(path, content) {
var re = new RegExp("^(/[^]*?)(Copyright.*?>)+([^]*?) \\*/", "m"),
lic,
match;
match = re.exec(content);
if (match === null) {
console.log("No license was found for " + path);
process.exit(1);
}
lic = match[1] + match[3];
return lic;
}
/**
* @param {!Object.<!string,!string>} contents
* @return {undefined}
*/
this.checkJSFiles = function (contents) {
var core = {},
jslint,
path,
license,
licenses = {},
// files for which jslint is not run
jslintExceptions = [
"lib/externs/JSZip.js",
"../programs/editor/plugins/bella/seedrandom.js"].map(pathModule.normalize),
// files for which the license is not checked
licenseExceptions = [
"lib/HeaderCompiled.js",
"lib/core/JSLint.js",
"lib/externs/JSZip.js",
"../programs/editor/plugins/bella/seedrandom.js"].map(pathModule.normalize),
commonLicense;
// load JSLint
/*jslint evil: true*/
eval(contents[pathModule.normalize("lib/core/JSLint.js")]);
/*jslint evil: false*/
jslint = new core.JSLint().JSLINT;
for (path in contents) {
if (contents.hasOwnProperty(path)
&& typeof contents[path] === "string") {
// run jslint of the content of a file
if (jslintExceptions.indexOf(path) === -1) {
runJSLint(jslint, path, contents[path]);
}
// collect the license from the file
if (licenseExceptions.indexOf(path) === -1) {
license = getLicense(path, contents[path]);
if (licenses.hasOwnProperty(license)) {
licenses[license].push(path);
} else {
licenses[license] = [path];
}
}
}
}
// determine which license is used most often
commonLicense = Object.keys(licenses).reduce(function (prev, current) {
var value = licenses[current];
if (value.length > prev.count) {
prev = {license: current, count: value.length, path: value};
}
return prev;
}, {license: "", count: 0});
// report any license that is different from the most common license
for (license in licenses) {
if (licenses.hasOwnProperty(license) && license !== commonLicense.license) {
console.log(licenses[license] + " has an uncommon license.");
process.exit(1);
}
}
};
}
function main(f) {
"use strict";
var pathModule = require("path");
// recursively read all the files in the lib and tests directories
f.readFiles(["lib", "tests", "../programs/editor", "../programs/docnosis", "../programs/benchmark"], function (name, isfile) {
if (name.indexOf("/dojo-deps/") !== -1) {
return false;
}
if (name.indexOf("programs/editor/FileSaver.js") !== -1) {
return false;
}
if (name.indexOf("programs/benchmark/require.js") !== -1) {
return false;
}
// only read directories and js files
return !isfile || name.indexOf(".js") === name.length - 3;
}, function (contents) {
var files = {};
f.checkJSFiles(contents);
// remove files that should not go in the manifest.json files
delete contents[pathModule.normalize("lib/HeaderCompiled.js")];
delete contents[pathModule.normalize("lib/runtime.js")];
delete contents[pathModule.normalize("lib/core/JSLint.js")];
delete contents[pathModule.normalize("tests/tests.js")];
delete contents[pathModule.normalize("tests/testruntimeadaption.js")];
Object.keys(contents).forEach(function (name) {
if (typeof contents[name] === "string") {
files[name] = contents[name];
}
});
f.createManifestsAndCMakeLists(files, ["lib" + pathModule.sep, "tests" + pathModule.sep]);
});
process.on("uncaughtException", function (err) {
console.log(err);
throw err;
});
process.on('exit', function () {
process.exit(exitCode);
});
}
main(new Main(process.argv[3]));