Merge branch 'develop' of git://github.com/Pita/etherpad-lite into develop

This commit is contained in:
0ip 2012-04-09 15:10:08 +02:00
commit 60eea0a3cf
152 changed files with 15112 additions and 2934 deletions

5
.gitignore vendored
View File

@ -1,7 +1,5 @@
node_modules node_modules
settings.json settings.json
static/js/jquery.js
static/js/prefixfree.js
APIKEY.txt APIKEY.txt
bin/abiword.exe bin/abiword.exe
bin/node.exe bin/node.exe
@ -10,4 +8,7 @@ var/dirty.db
bin/convertSettings.json bin/convertSettings.json
*~ *~
*.patch *.patch
src/static/js/jquery.js
npm-debug.log
*.DS_Store *.DS_Store
.ep_initialized

16
README.plugins Normal file
View File

@ -0,0 +1,16 @@
So, a plugin is an npm package whose name starts with ep_ and that contains a file ep.json
require("ep_etherpad-lite/static/js/plugingfw/plugins").update() will use npm to list all installed modules and read their ep.json files. These will contain registrations for hooks which are loaded
A hook registration is a pairs of a hook name and a function reference (filename for require() plus function name)
require("ep_etherpad-lite/static/js/plugingfw/hooks").callAll("hook_name", {argname:value}) will call all hook functions registered for hook_name
That is the basis.
Ok, so that was a slight simplification: inside ep.json, hook registrations are grouped into groups called "parts". Parts from all plugins are ordered using a topological sort according to "pre" and "post" pointers to other plugins/parts (just like dependencies, but non-installed plugins are silently ignored).
This ordering is honored when you do callAll(hook_name) - hook functions for that hook_name are called in that order
Ordering between plugins is undefined, only parts are ordered.
A plugin usually has one part, but it van have multiple.
This is so that it can insert some hook registration before that of another plugin, and another one after.
This is important for e.g. registering URL-handlers for the express webserver, if you have some very generic and some very specific url-regexps
So, that's basically it... apart from client-side hooks
which works the same way, but uses a separate member of the part (part.client_hooks vs part.hooks), and where the hook function must obviously reside in a file require():able from the client...
One thing more: The main etherpad tree is actually a plugin itself, called ep_etherpad-lite, and it has it's own ep.json...
was that clear?

View File

@ -0,0 +1,7 @@
.git*
docs/
examples/
support/
test/
testing.js
.DS_Store

View File

@ -0,0 +1,36 @@
{
"parts": [
{
"name": "somepart",
"pre": [],
"post": ["ep_onemoreplugin/partone"]
},
{
"name": "partlast",
"pre": ["ep_fintest/otherpart"],
"post": [],
"hooks": {
"somehookname": "ep_fintest/partlast:somehook"
}
},
{
"name": "partfirst",
"pre": [],
"post": ["ep_onemoreplugin/somepart"]
},
{
"name": "otherpart",
"pre": ["ep_fintest/somepart", "ep_otherplugin/main"],
"post": [],
"hooks": {
"somehookname": "ep_fintest/otherpart:somehook",
"morehook": "ep_fintest/otherpart:morehook",
"expressCreateServer": "ep_fintest/otherpart:expressServer",
"eejsBlock_editbarMenuLeft": "ep_fintest/otherpart:eejsBlock_editbarMenuLeft"
},
"client_hooks": {
"somehookname": "ep_fintest/static/js/test:bar"
}
}
]
}

View File

@ -0,0 +1,25 @@
test = require("ep_fintest/static/js/test.js");
console.log("FOOO:", test.foo);
exports.somehook = function (hook_name, args, cb) {
return cb(["otherpart:somehook was here"]);
}
exports.morehook = function (hook_name, args, cb) {
return cb(["otherpart:morehook was here"]);
}
exports.expressServer = function (hook_name, args, cb) {
args.app.get('/otherpart', function(req, res) {
res.send("<em>Abra cadabra</em>");
});
}
exports.eejsBlock_editbarMenuLeft = function (hook_name, args, cb) {
args.content = args.content + '\
<li id="testButton" onClick="window.pad&amp;&amp;pad.editbarClick(\'clearauthorship\');return false;">\
<a class="buttonicon buttonicon-test" title="Test test test"></a>\
</li>\
';
return cb();
}

View File

@ -0,0 +1,9 @@
{
"name": "ep_fintest",
"description": "A test plugin",
"version": "0.0.1",
"author": "RedHog (Egil Moeller) <egil.moller@freecode.no>",
"contributors": [],
"dependencies": {},
"engines": { "node": ">= 0.4.1 < 0.7.0" }
}

View File

@ -0,0 +1,3 @@
exports.somehook = function (hook_name, args, cb) {
return cb(["partlast:somehook was here"]);
}

View File

@ -0,0 +1,5 @@
exports.foo = 42;
exports.bar = function (hook_name, args, cb) {
return cb(["FOOOO"]);
}

View File

@ -0,0 +1 @@
<em>Test bla bla</em>

View File

@ -15,8 +15,8 @@ var log4js = require("log4js");
log4js.setGlobalLogLevel("INFO"); log4js.setGlobalLogLevel("INFO");
var async = require("async"); var async = require("async");
var db = require('../node/db/DB'); var db = require('../node/db/DB');
var CommonCode = require('../node/utils/common_code');
var Changeset = CommonCode.require("/Changeset"); var Changeset = require("ep_etherpad-lite/static/js/Changeset");
var padManager; var padManager;
async.series([ async.series([

View File

@ -1,12 +1,12 @@
var CommonCode = require('../node/utils/common_code');
var startTime = new Date().getTime(); var startTime = new Date().getTime();
var fs = require("fs"); var fs = require("fs");
var ueberDB = require("ueberDB"); var ueberDB = require("ueberDB");
var mysql = require("mysql"); var mysql = require("mysql");
var async = require("async"); var async = require("async");
var Changeset = CommonCode.require("/Changeset"); var Changeset = require("ep_etherpad-lite/static/js/Changeset");
var randomString = CommonCode.require('/pad_utils').randomString; var randomString = require('ep_etherpad-lite/static/js/pad_utils').randomString;
var AttributePoolFactory = CommonCode.require("/AttributePoolFactory"); var AttributePool = require("ep_etherpad-lite/static/js/AttributePool");
var settingsFile = process.argv[2]; var settingsFile = process.argv[2];
var sqlOutputFile = process.argv[3]; var sqlOutputFile = process.argv[3];
@ -384,7 +384,7 @@ function convertPad(padId, callback)
} }
//generate the latest atext //generate the latest atext
var fullAPool = AttributePoolFactory.createAttributePool().fromJsonable(apool); var fullAPool = (new AttributePool()).fromJsonable(apool);
var keyRev = Math.floor(padmeta.head / padmeta.keyRevInterval) * padmeta.keyRevInterval; var keyRev = Math.floor(padmeta.head / padmeta.keyRevInterval) * padmeta.keyRevInterval;
var atext = changesetsMeta[keyRev].atext; var atext = changesetsMeta[keyRev].atext;
var curRev = keyRev; var curRev = keyRev;

View File

@ -22,8 +22,7 @@ node-inspector &
echo "If you are new to node-inspector, take a look at this video: http://youtu.be/AOnK3NVnxL8" echo "If you are new to node-inspector, take a look at this video: http://youtu.be/AOnK3NVnxL8"
cd "node" node --debug node_modules/ep_etherpad-lite/node/server.js $*
node --debug server.js
#kill node-inspector before ending #kill node-inspector before ending
kill $! kill $!

View File

@ -55,7 +55,13 @@ if [ ! -f $settings ]; then
fi fi
echo "Ensure that all dependencies are up to date..." echo "Ensure that all dependencies are up to date..."
npm install || { (
mkdir -p node_modules
cd node_modules
[ -e ep_etherpad-lite ] || ln -s ../src ep_etherpad-lite
cd ep_etherpad-lite
npm install
) || {
rm -rf node_modules rm -rf node_modules
exit 1 exit 1
} }
@ -63,8 +69,8 @@ npm install || {
echo "Ensure jQuery is downloaded and up to date..." echo "Ensure jQuery is downloaded and up to date..."
DOWNLOAD_JQUERY="true" DOWNLOAD_JQUERY="true"
NEEDED_VERSION="1.7.1" NEEDED_VERSION="1.7.1"
if [ -f "static/js/jquery.js" ]; then if [ -f "src/static/js/jquery.js" ]; then
VERSION=$(cat static/js/jquery.js | head -n 3 | grep -o "v[0-9]\.[0-9]\(\.[0-9]\)\?"); VERSION=$(cat src/static/js/jquery.js | head -n 3 | grep -o "v[0-9]\.[0-9]\(\.[0-9]\)\?");
if [ ${VERSION#v} = $NEEDED_VERSION ]; then if [ ${VERSION#v} = $NEEDED_VERSION ]; then
DOWNLOAD_JQUERY="false" DOWNLOAD_JQUERY="false"
@ -72,22 +78,7 @@ if [ -f "static/js/jquery.js" ]; then
fi fi
if [ $DOWNLOAD_JQUERY = "true" ]; then if [ $DOWNLOAD_JQUERY = "true" ]; then
curl -lo static/js/jquery.js http://code.jquery.com/jquery-$NEEDED_VERSION.js || exit 1 curl -lo src/static/js/jquery.js http://code.jquery.com/jquery-$NEEDED_VERSION.js || exit 1
fi
echo "Ensure prefixfree is downloaded and up to date..."
DOWNLOAD_PREFIXFREE="true"
NEEDED_VERSION="1.0.4"
if [ -f "static/js/prefixfree.js" ]; then
VERSION=$(cat static/js/prefixfree.js | grep "PrefixFree" | grep -o "[0-9].[0-9].[0-9]");
if [ $VERSION = $NEEDED_VERSION ]; then
DOWNLOAD_PREFIXFREE="false"
fi
fi
if [ $DOWNLOAD_PREFIXFREE = "true" ]; then
curl -lo static/js/prefixfree.js -k https://raw.github.com/LeaVerou/prefixfree/master/prefixfree.js || exit 1
fi fi
#Remove all minified data to force node creating it new #Remove all minified data to force node creating it new
@ -98,12 +89,12 @@ echo "ensure custom css/js files are created..."
for f in "index" "pad" "timeslider" for f in "index" "pad" "timeslider"
do do
if [ ! -f "static/custom/$f.js" ]; then if [ ! -f "src/static/custom/$f.js" ]; then
cp -v "static/custom/js.template" "static/custom/$f.js" || exit 1 cp -v "src/static/custom/js.template" "src/static/custom/$f.js" || exit 1
fi fi
if [ ! -f "static/custom/$f.css" ]; then if [ ! -f "src/static/custom/$f.css" ]; then
cp -v "static/custom/css.template" "static/custom/$f.css" || exit 1 cp -v "src/static/custom/css.template" "src/static/custom/$f.css" || exit 1
fi fi
done done

View File

@ -25,5 +25,4 @@ bin/installDeps.sh $* || exit 1
#Move to the node folder and start #Move to the node folder and start
echo "start..." echo "start..."
cd "node" node node_modules/ep_etherpad-lite/node/server.js $*
node server.js $*

View File

@ -1,500 +0,0 @@
/**
* This module is started with bin/run.sh. It sets up a Express HTTP and a Socket.IO Server.
* Static file Requests are answered directly from this module, Socket.IO messages are passed
* to MessageHandler and minfied requests are passed to minified.
*/
/*
* 2011 Peter 'Pita' Martischka (Primary Technology Ltd)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS-IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
var ERR = require("async-stacktrace");
var log4js = require('log4js');
var os = require("os");
var socketio = require('socket.io');
var fs = require('fs');
var settings = require('./utils/Settings');
var db = require('./db/DB');
var async = require('async');
var express = require('express');
var path = require('path');
var minify = require('./utils/Minify');
var CachingMiddleware = require('./utils/caching_middleware');
var Yajsml = require('yajsml');
var formidable = require('formidable');
var apiHandler;
var exportHandler;
var importHandler;
var exporthtml;
var readOnlyManager;
var padManager;
var securityManager;
var socketIORouter;
//try to get the git version
var version = "";
try
{
var rootPath = path.normalize(__dirname + "/../")
var ref = fs.readFileSync(rootPath + ".git/HEAD", "utf-8");
var refPath = rootPath + ".git/" + ref.substring(5, ref.indexOf("\n"));
version = fs.readFileSync(refPath, "utf-8");
version = version.substring(0, 7);
console.log("Your Etherpad Lite git version is " + version);
}
catch(e)
{
console.warn("Can't get git version for server header\n" + e.message)
}
console.log("Report bugs at https://github.com/Pita/etherpad-lite/issues")
var serverName = "Etherpad-Lite " + version + " (http://j.mp/ep-lite)";
exports.maxAge = settings.maxAge;
//set loglevel
log4js.setGlobalLogLevel(settings.loglevel);
async.waterfall([
//initalize the database
function (callback)
{
db.init(callback);
},
//initalize the http server
function (callback)
{
//create server
var app = express.createServer();
app.use(function (req, res, next) {
res.header("Server", serverName);
next();
});
//redirects browser to the pad's sanitized url if needed. otherwise, renders the html
app.param('pad', function (req, res, next, padId) {
//ensure the padname is valid and the url doesn't end with a /
if(!padManager.isValidPadId(padId) || /\/$/.test(req.url))
{
res.send('Such a padname is forbidden', 404);
}
else
{
padManager.sanitizePadId(padId, function(sanitizedPadId) {
//the pad id was sanitized, so we redirect to the sanitized version
if(sanitizedPadId != padId)
{
var real_path = req.path.replace(/^\/p\/[^\/]+/, './' + sanitizedPadId);
res.header('Location', real_path);
res.send('You should be redirected to <a href="' + real_path + '">' + real_path + '</a>', 302);
}
//the pad id was fine, so just render it
else
{
next();
}
});
}
});
//load modules that needs a initalized db
readOnlyManager = require("./db/ReadOnlyManager");
exporthtml = require("./utils/ExportHtml");
exportHandler = require('./handler/ExportHandler');
importHandler = require('./handler/ImportHandler');
apiHandler = require('./handler/APIHandler');
padManager = require('./db/PadManager');
securityManager = require('./db/SecurityManager');
socketIORouter = require("./handler/SocketIORouter");
//install logging
var httpLogger = log4js.getLogger("http");
app.configure(function()
{
// Activate http basic auth if it has been defined in settings.json
if(settings.httpAuth != null) app.use(basic_auth);
// If the log level specified in the config file is WARN or ERROR the application server never starts listening to requests as reported in issue #158.
// Not installing the log4js connect logger when the log level has a higher severity than INFO since it would not log at that level anyway.
if (!(settings.loglevel === "WARN" || settings.loglevel == "ERROR"))
app.use(log4js.connectLogger(httpLogger, { level: log4js.levels.INFO, format: ':status, :method :url'}));
app.use(express.cookieParser());
});
app.error(function(err, req, res, next){
res.send(500);
console.error(err.stack ? err.stack : err.toString());
gracefulShutdown();
});
// Cache both minified and static.
var assetCache = new CachingMiddleware;
app.all('/(minified|static)/*', assetCache.handle);
// Minify will serve static files compressed (minify enabled). It also has
// file-specific hacks for ace/require-kernel/etc.
app.all('/static/:filename(*)', minify.minify);
// Setup middleware that will package JavaScript files served by minify for
// CommonJS loader on the client-side.
var jsServer = new (Yajsml.Server)({
rootPath: 'minified/'
, rootURI: 'http://localhost:' + settings.port + '/static/js/'
});
var StaticAssociator = Yajsml.associators.StaticAssociator;
var associations =
Yajsml.associators.associationsForSimpleMapping(minify.tar);
var associator = new StaticAssociator(associations);
jsServer.setAssociator(associator);
app.use(jsServer);
//checks for padAccess
function hasPadAccess(req, res, callback)
{
securityManager.checkAccess(req.params.pad, req.cookies.sessionid, req.cookies.token, req.cookies.password, function(err, accessObj)
{
if(ERR(err, callback)) return;
//there is access, continue
if(accessObj.accessStatus == "grant")
{
callback();
}
//no access
else
{
res.send("403 - Can't touch this", 403);
}
});
}
//checks for basic http auth
function basic_auth (req, res, next) {
if (req.headers.authorization && req.headers.authorization.search('Basic ') === 0) {
// fetch login and password
if (new Buffer(req.headers.authorization.split(' ')[1], 'base64').toString() == settings.httpAuth) {
next();
return;
}
}
res.header('WWW-Authenticate', 'Basic realm="Protected Area"');
if (req.headers.authorization) {
setTimeout(function () {
res.send('Authentication required', 401);
}, 1000);
} else {
res.send('Authentication required', 401);
}
}
//serve read only pad
app.get('/ro/:id', function(req, res)
{
var html;
var padId;
var pad;
async.series([
//translate the read only pad to a padId
function(callback)
{
readOnlyManager.getPadId(req.params.id, function(err, _padId)
{
if(ERR(err, callback)) return;
padId = _padId;
//we need that to tell hasPadAcess about the pad
req.params.pad = padId;
callback();
});
},
//render the html document
function(callback)
{
//return if the there is no padId
if(padId == null)
{
callback("notfound");
return;
}
hasPadAccess(req, res, function()
{
//render the html document
exporthtml.getPadHTMLDocument(padId, null, false, function(err, _html)
{
if(ERR(err, callback)) return;
html = _html;
callback();
});
});
}
], function(err)
{
//throw any unexpected error
if(err && err != "notfound")
ERR(err);
if(err == "notfound")
res.send('404 - Not Found', 404);
else
res.send(html);
});
});
//serve pad.html under /p
app.get('/p/:pad', function(req, res, next)
{
var filePath = path.normalize(__dirname + "/../static/pad.html");
res.sendfile(filePath, { maxAge: exports.maxAge });
});
//serve timeslider.html under /p/$padname/timeslider
app.get('/p/:pad/timeslider', function(req, res, next)
{
var filePath = path.normalize(__dirname + "/../static/timeslider.html");
res.sendfile(filePath, { maxAge: exports.maxAge });
});
//serve timeslider.html under /p/$padname/timeslider
app.get('/p/:pad/:rev?/export/:type', function(req, res, next)
{
var types = ["pdf", "doc", "txt", "html", "odt", "dokuwiki"];
//send a 404 if we don't support this filetype
if(types.indexOf(req.params.type) == -1)
{
next();
return;
}
//if abiword is disabled, and this is a format we only support with abiword, output a message
if(settings.abiword == null &&
["odt", "pdf", "doc"].indexOf(req.params.type) !== -1)
{
res.send("Abiword is not enabled at this Etherpad Lite instance. Set the path to Abiword in settings.json to enable this feature");
return;
}
res.header("Access-Control-Allow-Origin", "*");
hasPadAccess(req, res, function()
{
exportHandler.doExport(req, res, req.params.pad, req.params.type);
});
});
//handle import requests
app.post('/p/:pad/import', function(req, res, next)
{
//if abiword is disabled, skip handling this request
if(settings.abiword == null)
{
next();
return;
}
hasPadAccess(req, res, function()
{
importHandler.doImport(req, res, req.params.pad);
});
});
var apiLogger = log4js.getLogger("API");
//This is for making an api call, collecting all post information and passing it to the apiHandler
var apiCaller = function(req, res, fields)
{
res.header("Content-Type", "application/json; charset=utf-8");
apiLogger.info("REQUEST, " + req.params.func + ", " + JSON.stringify(fields));
//wrap the send function so we can log the response
res._send = res.send;
res.send = function(response)
{
response = JSON.stringify(response);
apiLogger.info("RESPONSE, " + req.params.func + ", " + response);
//is this a jsonp call, if yes, add the function call
if(req.query.jsonp)
response = req.query.jsonp + "(" + response + ")";
res._send(response);
}
//call the api handler
apiHandler.handle(req.params.func, fields, req, res);
}
//This is a api GET call, collect all post informations and pass it to the apiHandler
app.get('/api/1/:func', function(req, res)
{
apiCaller(req, res, req.query)
});
//This is a api POST call, collect all post informations and pass it to the apiHandler
app.post('/api/1/:func', function(req, res)
{
new formidable.IncomingForm().parse(req, function(err, fields, files)
{
apiCaller(req, res, fields)
});
});
//The Etherpad client side sends information about how a disconnect happen
app.post('/ep/pad/connection-diagnostic-info', function(req, res)
{
new formidable.IncomingForm().parse(req, function(err, fields, files)
{
console.log("DIAGNOSTIC-INFO: " + fields.diagnosticInfo);
res.end("OK");
});
});
//The Etherpad client side sends information about client side javscript errors
app.post('/jserror', function(req, res)
{
new formidable.IncomingForm().parse(req, function(err, fields, files)
{
console.error("CLIENT SIDE JAVASCRIPT ERROR: " + fields.errorInfo);
res.end("OK");
});
});
//serve index.html under /
app.get('/', function(req, res)
{
var filePath = path.normalize(__dirname + "/../static/index.html");
res.sendfile(filePath, { maxAge: exports.maxAge });
});
//serve robots.txt
app.get('/robots.txt', function(req, res)
{
var filePath = path.normalize(__dirname + "/../static/robots.txt");
res.sendfile(filePath, { maxAge: exports.maxAge });
});
//serve favicon.ico
app.get('/favicon.ico', function(req, res)
{
var filePath = path.normalize(__dirname + "/../static/custom/favicon.ico");
res.sendfile(filePath, { maxAge: exports.maxAge }, function(err)
{
//there is no custom favicon, send the default favicon
if(err)
{
filePath = path.normalize(__dirname + "/../static/favicon.ico");
res.sendfile(filePath, { maxAge: exports.maxAge });
}
});
});
//let the server listen
app.listen(settings.port, settings.ip);
console.log("Server is listening at " + settings.ip + ":" + settings.port);
var onShutdown = false;
var gracefulShutdown = function(err)
{
if(err && err.stack)
{
console.error(err.stack);
}
else if(err)
{
console.error(err);
}
//ensure there is only one graceful shutdown running
if(onShutdown) return;
onShutdown = true;
console.log("graceful shutdown...");
//stop the http server
app.close();
//do the db shutdown
db.db.doShutdown(function()
{
console.log("db sucessfully closed.");
process.exit(0);
});
setTimeout(function(){
process.exit(1);
}, 3000);
}
//connect graceful shutdown with sigint and uncaughtexception
if(os.type().indexOf("Windows") == -1)
{
//sigint is so far not working on windows
//https://github.com/joyent/node/issues/1553
process.on('SIGINT', gracefulShutdown);
}
process.on('uncaughtException', gracefulShutdown);
//init socket.io and redirect all requests to the MessageHandler
var io = socketio.listen(app);
//this is only a workaround to ensure it works with all browers behind a proxy
//we should remove this when the new socket.io version is more stable
io.set('transports', ['xhr-polling']);
var socketIOLogger = log4js.getLogger("socket.io");
io.set('logger', {
debug: function (str)
{
socketIOLogger.debug.apply(socketIOLogger, arguments);
},
info: function (str)
{
socketIOLogger.info.apply(socketIOLogger, arguments);
},
warn: function (str)
{
socketIOLogger.warn.apply(socketIOLogger, arguments);
},
error: function (str)
{
socketIOLogger.error.apply(socketIOLogger, arguments);
},
});
//minify socket.io javascript
if(settings.minify)
io.enable('browser client minification');
var padMessageHandler = require("./handler/PadMessageHandler");
var timesliderMessageHandler = require("./handler/TimesliderMessageHandler");
//Initalize the Socket.IO Router
socketIORouter.setSocketIO(io);
socketIORouter.addComponent("pad", padMessageHandler);
socketIORouter.addComponent("timeslider", timesliderMessageHandler);
callback(null);
}
]);

View File

@ -13,7 +13,7 @@
"dbType" : "dirty", "dbType" : "dirty",
//the database specific settings //the database specific settings
"dbSettings" : { "dbSettings" : {
"filename" : "../var/dirty.db" "filename" : "var/dirty.db"
}, },
/* An Example of MySQL Configuration /* An Example of MySQL Configuration
@ -39,9 +39,9 @@
but makes it impossible to debug the javascript/css */ but makes it impossible to debug the javascript/css */
"minify" : true, "minify" : true,
/* How long may clients use served javascript code? Without versioning this /* How long may clients use served javascript code (in seconds)? Without versioning this
is may cause problems during deployment. */ is may cause problems during deployment. Set to 0 to disable caching */
"maxAge" : 21600000, // 6 hours "maxAge" : 21600, // 6 hours
/* This is the path to the Abiword executable. Setting it to null, disables abiword. /* This is the path to the Abiword executable. Setting it to null, disables abiword.
Abiword is needed to enable the import/export of pads*/ Abiword is needed to enable the import/export of pads*/
@ -50,6 +50,12 @@
/* This setting is used if you need http basic auth */ /* This setting is used if you need http basic auth */
// "httpAuth" : "user:pass", // "httpAuth" : "user:pass",
/* This setting is used for http basic auth for admin pages. If not set, the admin page won't be accessible from web*/
// "adminHttpAuth" : "user:pass",
/* The log level we are using, can be: DEBUG, INFO, WARN, ERROR */ /* The log level we are using, can be: DEBUG, INFO, WARN, ERROR */
"loglevel": "INFO" "loglevel": "INFO",
/* cache 6 hours = 1000*60*60*6 */
"maxAge": 21600000
} }

View File

@ -12,7 +12,7 @@
"dbType" : "dirty", "dbType" : "dirty",
//the database specific settings //the database specific settings
"dbSettings" : { "dbSettings" : {
"filename" : "../var/dirty.db" "filename" : "var/dirty.db"
}, },
/* An Example of MySQL Configuration /* An Example of MySQL Configuration
@ -40,5 +40,9 @@
/* This is the path to the Abiword executable. Setting it to null, disables abiword. /* This is the path to the Abiword executable. Setting it to null, disables abiword.
Abiword is needed to enable the import/export of pads*/ Abiword is needed to enable the import/export of pads*/
"abiword" : null "abiword" : null,
/* cache 6 hours = 1000*60*60*6 */
"maxAge": 21600000
} }

16
src/ep.json Normal file
View File

@ -0,0 +1,16 @@
{
"parts": [
{ "name": "static", "hooks": { "expressCreateServer": "ep_etherpad-lite/node/hooks/express/static:expressCreateServer" } },
{ "name": "specialpages", "hooks": { "expressCreateServer": "ep_etherpad-lite/node/hooks/express/specialpages:expressCreateServer" } },
{ "name": "padurlsanitize", "hooks": { "expressCreateServer": "ep_etherpad-lite/node/hooks/express/padurlsanitize:expressCreateServer" } },
{ "name": "padreadonly", "hooks": { "expressCreateServer": "ep_etherpad-lite/node/hooks/express/padreadonly:expressCreateServer" } },
{ "name": "webaccess", "hooks": { "expressConfigure": "ep_etherpad-lite/node/hooks/express/webaccess:expressConfigure" } },
{ "name": "apicalls", "hooks": { "expressCreateServer": "ep_etherpad-lite/node/hooks/express/apicalls:expressCreateServer" } },
{ "name": "importexport", "hooks": { "expressCreateServer": "ep_etherpad-lite/node/hooks/express/importexport:expressCreateServer" } },
{ "name": "errorhandling", "hooks": { "expressCreateServer": "ep_etherpad-lite/node/hooks/express/errorhandling:expressCreateServer" } },
{ "name": "socketio", "hooks": { "expressCreateServer": "ep_etherpad-lite/node/hooks/express/socketio:expressCreateServer" } },
{ "name": "adminplugins", "hooks": {
"expressCreateServer": "ep_etherpad-lite/node/hooks/express/adminplugins:expressCreateServer",
"socketio": "ep_etherpad-lite/node/hooks/express/adminplugins:socketio" } }
]
}

View File

@ -18,11 +18,11 @@
* limitations under the License. * limitations under the License.
*/ */
var CommonCode = require('../utils/common_code');
var ERR = require("async-stacktrace"); var ERR = require("async-stacktrace");
var db = require("./DB").db; var db = require("./DB").db;
var async = require("async"); var async = require("async");
var randomString = CommonCode.require('/pad_utils').randomString; var randomString = require('ep_etherpad-lite/static/js/pad_utils').randomString;
/** /**
* Checks if the author exists * Checks if the author exists

View File

@ -18,10 +18,10 @@
* limitations under the License. * limitations under the License.
*/ */
var CommonCode = require('../utils/common_code');
var ERR = require("async-stacktrace"); var ERR = require("async-stacktrace");
var customError = require("../utils/customError"); var customError = require("../utils/customError");
var randomString = CommonCode.require('/pad_utils').randomString; var randomString = require('ep_etherpad-lite/static/js/pad_utils').randomString;
var db = require("./DB").db; var db = require("./DB").db;
var async = require("async"); var async = require("async");
var padManager = require("./PadManager"); var padManager = require("./PadManager");

View File

@ -2,11 +2,11 @@
* The pad object, defined with joose * The pad object, defined with joose
*/ */
var CommonCode = require('../utils/common_code');
var ERR = require("async-stacktrace"); var ERR = require("async-stacktrace");
var Changeset = CommonCode.require("/Changeset"); var Changeset = require("ep_etherpad-lite/static/js/Changeset");
var AttributePoolFactory = CommonCode.require("/AttributePoolFactory"); var AttributePool = require("ep_etherpad-lite/static/js/AttributePool");
var randomString = CommonCode.require('/pad_utils').randomString; var randomString = require('ep_etherpad-lite/static/js/pad_utils').randomString;
var db = require("./DB").db; var db = require("./DB").db;
var async = require("async"); var async = require("async");
var settings = require('../utils/Settings'); var settings = require('../utils/Settings');
@ -15,6 +15,11 @@ var padManager = require("./PadManager");
var padMessageHandler = require("../handler/PadMessageHandler"); var padMessageHandler = require("../handler/PadMessageHandler");
var readOnlyManager = require("./ReadOnlyManager"); var readOnlyManager = require("./ReadOnlyManager");
var crypto = require("crypto"); var crypto = require("crypto");
var randomString = require("../utils/randomstring");
//serialization/deserialization attributes
var attributeBlackList = ["id"];
var jsonableList = ["pool"];
/** /**
* Copied from the Etherpad source code. It converts Windows line breaks to Unix line breaks and convert Tabs to spaces * Copied from the Etherpad source code. It converts Windows line breaks to Unix line breaks and convert Tabs to spaces
@ -28,13 +33,13 @@ exports.cleanText = function (txt) {
var Pad = function Pad(id) { var Pad = function Pad(id) {
this.atext = Changeset.makeAText("\n"); this.atext = Changeset.makeAText("\n");
this.pool = AttributePoolFactory.createAttributePool(); this.pool = new AttributePool();
this.head = -1; this.head = -1;
this.chatHead = -1; this.chatHead = -1;
this.publicStatus = false; this.publicStatus = false;
this.passwordHash = null; this.passwordHash = null;
this.id = id; this.id = id;
this.savedRevisions = [];
}; };
exports.Pad = Pad; exports.Pad = Pad;
@ -76,14 +81,27 @@ Pad.prototype.appendRevision = function appendRevision(aChangeset, author) {
} }
db.set("pad:"+this.id+":revs:"+newRev, newRevData); db.set("pad:"+this.id+":revs:"+newRev, newRevData);
db.set("pad:"+this.id, {atext: this.atext, this.saveToDatabase();
pool: this.pool.toJsonable(),
head: this.head,
chatHead: this.chatHead,
publicStatus: this.publicStatus,
passwordHash: this.passwordHash});
}; };
//save all attributes to the database
Pad.prototype.saveToDatabase = function saveToDatabase(){
var dbObject = {};
for(var attr in this){
if(typeof this[attr] === "function") continue;
if(attributeBlackList.indexOf(attr) !== -1) continue;
dbObject[attr] = this[attr];
if(jsonableList.indexOf(attr) !== -1){
dbObject[attr] = dbObject[attr].toJsonable();
}
}
db.set("pad:"+this.id, dbObject);
}
Pad.prototype.getRevisionChangeset = function getRevisionChangeset(revNum, callback) { Pad.prototype.getRevisionChangeset = function getRevisionChangeset(revNum, callback) {
db.getSub("pad:"+this.id+":revs:"+revNum, ["changeset"], callback); db.getSub("pad:"+this.id+":revs:"+revNum, ["changeset"], callback);
}; };
@ -203,8 +221,7 @@ Pad.prototype.appendChatMessage = function appendChatMessage(text, userId, time)
this.chatHead++; this.chatHead++;
//save the chat entry in the database //save the chat entry in the database
db.set("pad:"+this.id+":chat:"+this.chatHead, {"text": text, "userId": userId, "time": time}); db.set("pad:"+this.id+":chat:"+this.chatHead, {"text": text, "userId": userId, "time": time});
//save the new chat head this.saveToDatabase();
db.setSub("pad:"+this.id, ["chatHead"], this.chatHead);
}; };
Pad.prototype.getChatMessage = function getChatMessage(entryNum, callback) { Pad.prototype.getChatMessage = function getChatMessage(entryNum, callback) {
@ -324,27 +341,14 @@ Pad.prototype.init = function init(text, callback) {
//if this pad exists, load it //if this pad exists, load it
if(value != null) if(value != null)
{ {
_this.head = value.head; //copy all attr. To a transfrom via fromJsonable if necassary
_this.atext = value.atext; for(var attr in value){
_this.pool = _this.pool.fromJsonable(value.pool); if(jsonableList.indexOf(attr) !== -1){
_this[attr] = _this[attr].fromJsonable(value[attr]);
//ensure we have a local chatHead variable } else {
if(value.chatHead != null) _this[attr] = value[attr];
_this.chatHead = value.chatHead; }
else }
_this.chatHead = -1;
//ensure we have a local publicStatus variable
if(value.publicStatus != null)
_this.publicStatus = value.publicStatus;
else
_this.publicStatus = false;
//ensure we have a local passwordHash variable
if(value.passwordHash != null)
_this.passwordHash = value.passwordHash;
else
_this.passwordHash = null;
} }
//this pad doesn't exist, so create it //this pad doesn't exist, so create it
else else
@ -452,12 +456,12 @@ Pad.prototype.remove = function remove(callback) {
//set in db //set in db
Pad.prototype.setPublicStatus = function setPublicStatus(publicStatus) { Pad.prototype.setPublicStatus = function setPublicStatus(publicStatus) {
this.publicStatus = publicStatus; this.publicStatus = publicStatus;
db.setSub("pad:"+this.id, ["publicStatus"], this.publicStatus); this.saveToDatabase();
}; };
Pad.prototype.setPassword = function setPassword(password) { Pad.prototype.setPassword = function setPassword(password) {
this.passwordHash = password == null ? null : hash(password, generateSalt()); this.passwordHash = password == null ? null : hash(password, generateSalt());
db.setSub("pad:"+this.id, ["passwordHash"], this.passwordHash); this.saveToDatabase();
}; };
Pad.prototype.isCorrectPassword = function isCorrectPassword(password) { Pad.prototype.isCorrectPassword = function isCorrectPassword(password) {
@ -468,6 +472,31 @@ Pad.prototype.isPasswordProtected = function isPasswordProtected() {
return this.passwordHash != null; return this.passwordHash != null;
}; };
Pad.prototype.addSavedRevision = function addSavedRevision(revNum, savedById, label) {
//if this revision is already saved, return silently
for(var i in this.savedRevisions){
if(this.savedRevisions.revNum === revNum){
return;
}
}
//build the saved revision object
var savedRevision = {};
savedRevision.revNum = revNum;
savedRevision.savedById = savedById;
savedRevision.label = label || "Revision " + revNum;
savedRevision.timestamp = new Date().getTime();
savedRevision.id = randomString(10);
//save this new saved revision
this.savedRevisions.push(savedRevision);
this.saveToDatabase();
};
Pad.prototype.getSavedRevisions = function getSavedRevisions() {
return this.savedRevisions;
};
/* Crypto helper methods */ /* Crypto helper methods */
function hash(password, salt) function hash(password, salt)

View File

@ -18,11 +18,11 @@
* limitations under the License. * limitations under the License.
*/ */
var CommonCode = require('../utils/common_code');
var ERR = require("async-stacktrace"); var ERR = require("async-stacktrace");
var db = require("./DB").db; var db = require("./DB").db;
var async = require("async"); var async = require("async");
var randomString = CommonCode.require('/pad_utils').randomString; var randomString = require('ep_etherpad-lite/static/js/pad_utils').randomString;
/** /**
* returns a read only id for a pad * returns a read only id for a pad

View File

@ -18,7 +18,7 @@
* limitations under the License. * limitations under the License.
*/ */
var CommonCode = require('../utils/common_code');
var ERR = require("async-stacktrace"); var ERR = require("async-stacktrace");
var db = require("./DB").db; var db = require("./DB").db;
var async = require("async"); var async = require("async");
@ -26,7 +26,7 @@ var authorManager = require("./AuthorManager");
var padManager = require("./PadManager"); var padManager = require("./PadManager");
var sessionManager = require("./SessionManager"); var sessionManager = require("./SessionManager");
var settings = require("../utils/Settings") var settings = require("../utils/Settings")
var randomString = CommonCode.require('/pad_utils').randomString; var randomString = require('ep_etherpad-lite/static/js/pad_utils').randomString;
/** /**
* This function controlls the access to a pad, it checks if the user can access a pad. * This function controlls the access to a pad, it checks if the user can access a pad.

View File

@ -18,10 +18,10 @@
* limitations under the License. * limitations under the License.
*/ */
var CommonCode = require('../utils/common_code');
var ERR = require("async-stacktrace"); var ERR = require("async-stacktrace");
var customError = require("../utils/customError"); var customError = require("../utils/customError");
var randomString = CommonCode.require('/pad_utils').randomString; var randomString = require('ep_etherpad-lite/static/js/pad_utils').randomString;
var db = require("./DB").db; var db = require("./DB").db;
var async = require("async"); var async = require("async");
var groupMangager = require("./GroupManager"); var groupMangager = require("./GroupManager");

View File

@ -20,9 +20,9 @@
* limitations under the License. * limitations under the License.
*/ */
var CommonCode = require('./utils/common_code');
var Changeset = CommonCode.require("/Changeset"); var Changeset = require("ep_etherpad-lite/static/js/Changeset");
var AttributePoolFactory = CommonCode.require("/AttributePoolFactory"); var AttributePool = require("ep_etherpad-lite/static/js/AttributePool");
function random() { function random() {
this.nextInt = function (maxValue) { this.nextInt = function (maxValue) {
@ -227,7 +227,7 @@ function runTests() {
return attribs; // it's already an attrib pool return attribs; // it's already an attrib pool
} else { } else {
// assume it's an array of attrib strings to be split and added // assume it's an array of attrib strings to be split and added
var p = AttributePoolFactory.createAttributePool(); var p = new AttributePool();
attribs.forEach(function (kv) { attribs.forEach(function (kv) {
p.putAttrib(kv.split(',')); p.putAttrib(kv.split(','));
}); });
@ -325,7 +325,7 @@ function runTests() {
runMutateAttributionTest(4, ['foo,bar', 'line,1', 'line,2', 'line,3', 'line,4', 'line,5'], "Z:5>1|2=2+1$x", ["?*1|1+1", "?*2|1+1", "*3|1+1", "?*4|1+1", "?*5|1+1"], ["?*1|1+1", "?*2|1+1", "+1*3|1+1", "?*4|1+1", "?*5|1+1"]); runMutateAttributionTest(4, ['foo,bar', 'line,1', 'line,2', 'line,3', 'line,4', 'line,5'], "Z:5>1|2=2+1$x", ["?*1|1+1", "?*2|1+1", "*3|1+1", "?*4|1+1", "?*5|1+1"], ["?*1|1+1", "?*2|1+1", "+1*3|1+1", "?*4|1+1", "?*5|1+1"]);
var testPoolWithChars = (function () { var testPoolWithChars = (function () {
var p = AttributePoolFactory.createAttributePool(); var p = new AttributePool();
p.putAttrib(['char', 'newline']); p.putAttrib(['char', 'newline']);
for (var i = 1; i < 36; i++) { for (var i = 1; i < 36; i++) {
p.putAttrib(['char', Changeset.numToString(i)]); p.putAttrib(['char', Changeset.numToString(i)]);
@ -560,7 +560,7 @@ function runTests() {
var rand = new random(); var rand = new random();
print("> testCompose#" + randomSeed); print("> testCompose#" + randomSeed);
var p = AttributePoolFactory.createAttributePool(); var p = new AttributePool();
var startText = randomMultiline(10, 20, rand) + '\n'; var startText = randomMultiline(10, 20, rand) + '\n';
@ -594,7 +594,7 @@ function runTests() {
(function simpleComposeAttributesTest() { (function simpleComposeAttributesTest() {
print("> simpleComposeAttributesTest"); print("> simpleComposeAttributesTest");
var p = AttributePoolFactory.createAttributePool(); var p = new AttributePool();
p.putAttrib(['bold', '']); p.putAttrib(['bold', '']);
p.putAttrib(['bold', 'true']); p.putAttrib(['bold', 'true']);
var cs1 = Changeset.checkRep("Z:2>1*1+1*1=1$x"); var cs1 = Changeset.checkRep("Z:2>1*1+1*1=1$x");
@ -604,7 +604,7 @@ function runTests() {
})(); })();
(function followAttributesTest() { (function followAttributesTest() {
var p = AttributePoolFactory.createAttributePool(); var p = new AttributePool();
p.putAttrib(['x', '']); p.putAttrib(['x', '']);
p.putAttrib(['x', 'abc']); p.putAttrib(['x', 'abc']);
p.putAttrib(['x', 'def']); p.putAttrib(['x', 'def']);
@ -633,7 +633,7 @@ function runTests() {
var rand = new random(); var rand = new random();
print("> testFollow#" + randomSeed); print("> testFollow#" + randomSeed);
var p = AttributePoolFactory.createAttributePool(); var p = new AttributePool();
var startText = randomMultiline(10, 20, rand) + '\n'; var startText = randomMultiline(10, 20, rand) + '\n';
@ -682,8 +682,8 @@ function runTests() {
(function testMoveOpsToNewPool() { (function testMoveOpsToNewPool() {
print("> testMoveOpsToNewPool"); print("> testMoveOpsToNewPool");
var pool1 = AttributePoolFactory.createAttributePool(); var pool1 = new AttributePool();
var pool2 = AttributePoolFactory.createAttributePool(); var pool2 = new AttributePool();
pool1.putAttrib(['baz', 'qux']); pool1.putAttrib(['baz', 'qux']);
pool1.putAttrib(['foo', 'bar']); pool1.putAttrib(['foo', 'bar']);
@ -738,7 +738,7 @@ function runTests() {
(function testOpAttributeValue() { (function testOpAttributeValue() {
print("> testOpAttributeValue"); print("> testOpAttributeValue");
var p = AttributePoolFactory.createAttributePool(); var p = new AttributePool();
p.putAttrib(['name', 'david']); p.putAttrib(['name', 'david']);
p.putAttrib(['color', 'green']); p.putAttrib(['color', 'green']);

View File

@ -0,0 +1,9 @@
a
<% e.begin_block("bar"); %>
A
<% e.begin_block("foo"); %>
XX
<% e.end_block(); %>
B
<% e.end_block(); %>
b

View File

@ -0,0 +1,7 @@
<% e.inherit("./bar.ejs"); %>
<% e.begin_define_block("foo"); %>
YY
<% e.super(); %>
ZZ
<% e.end_define_block(); %>

115
src/node/eejs/index.js Normal file
View File

@ -0,0 +1,115 @@
/*
* Copyright (c) 2011 RedHog (Egil Möller) <egil.moller@freecode.no>
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS-IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/* Basic usage:
*
* require("./index").require("./examples/foo.ejs")
*/
var ejs = require("ejs");
var fs = require("fs");
var path = require("path");
var hooks = require("ep_etherpad-lite/static/js/pluginfw/hooks.js");
exports.info = {
buf_stack: [],
block_stack: [],
blocks: {},
file_stack: [],
};
exports._init = function (b, recursive) {
exports.info.buf_stack.push(exports.info.buf);
exports.info.buf = b;
}
exports._exit = function (b, recursive) {
exports.info.file_stack[exports.info.file_stack.length-1].inherit.forEach(function (item) {
exports._require(item.name, item.args);
});
exports.info.buf = exports.info.buf_stack.pop();
}
exports.begin_capture = function() {
exports.info.buf_stack.push(exports.info.buf.concat());
exports.info.buf.splice(0, exports.info.buf.length);
}
exports.end_capture = function () {
var res = exports.info.buf.join("");
exports.info.buf.splice.apply(
exports.info.buf,
[0, exports.info.buf.length].concat(exports.info.buf_stack.pop()));
return res;
}
exports.begin_define_block = function (name) {
if (typeof exports.info.blocks[name] == "undefined")
exports.info.blocks[name] = {};
exports.info.block_stack.push(name);
exports.begin_capture();
}
exports.super = function () {
exports.info.buf.push('<!eejs!super!>');
}
exports.end_define_block = function () {
content = exports.end_capture();
var name = exports.info.block_stack.pop();
if (typeof exports.info.blocks[name].content == "undefined")
exports.info.blocks[name].content = content;
else if (typeof exports.info.blocks[name].content.indexOf('<!eejs!super!>'))
exports.info.blocks[name].content = exports.info.blocks[name].content.replace('<!eejs!super!>', content);
return exports.info.blocks[name].content;
}
exports.end_block = function () {
var name = exports.info.block_stack[exports.info.block_stack.length-1];
var args = {content: exports.end_define_block()};
hooks.callAll("eejsBlock_" + name, args);
exports.info.buf.push(args.content);
}
exports.begin_block = exports.begin_define_block;
exports.inherit = function (name, args) {
exports.info.file_stack[exports.info.file_stack.length-1].inherit.push({name:name, args:args});
}
exports.require = function (name, args) {
if (args == undefined) args = {};
if ((name.indexOf("./") == 0 || name.indexOf("../") == 0) && exports.info.file_stack.length) {
name = path.join(path.dirname(exports.info.file_stack[exports.info.file_stack.length-1].path), name);
}
var ejspath = require.resolve(name)
args.e = exports;
args.require = require;
var template = '<% e._init(buf); %>' + fs.readFileSync(ejspath).toString() + '<% e._exit(); %>';
exports.info.file_stack.push({path: ejspath, inherit: []});
var res = ejs.render(template, args);
exports.info.file_stack.pop();
return res;
}
exports._require = function (name, args) {
exports.info.buf.push(exports.require(name, args));
}

View File

@ -18,12 +18,12 @@
* limitations under the License. * limitations under the License.
*/ */
var CommonCode = require('../utils/common_code');
var ERR = require("async-stacktrace"); var ERR = require("async-stacktrace");
var fs = require("fs"); var fs = require("fs");
var api = require("../db/API"); var api = require("../db/API");
var padManager = require("../db/PadManager"); var padManager = require("../db/PadManager");
var randomString = CommonCode.require('/pad_utils').randomString; var randomString = require('ep_etherpad-lite/static/js/pad_utils').randomString;
//ensure we have an apikey //ensure we have an apikey
var apikey = null; var apikey = null;

View File

@ -18,18 +18,21 @@
* limitations under the License. * limitations under the License.
*/ */
var CommonCode = require('../utils/common_code');
var ERR = require("async-stacktrace"); var ERR = require("async-stacktrace");
var async = require("async"); var async = require("async");
var padManager = require("../db/PadManager"); var padManager = require("../db/PadManager");
var Changeset = CommonCode.require("/Changeset"); var Changeset = require("ep_etherpad-lite/static/js/Changeset");
var AttributePoolFactory = CommonCode.require("/AttributePoolFactory"); var AttributePool = require("ep_etherpad-lite/static/js/AttributePool");
var AttributeManager = require("ep_etherpad-lite/static/js/AttributeManager");
var authorManager = require("../db/AuthorManager"); var authorManager = require("../db/AuthorManager");
var readOnlyManager = require("../db/ReadOnlyManager"); var readOnlyManager = require("../db/ReadOnlyManager");
var settings = require('../utils/Settings'); var settings = require('../utils/Settings');
var securityManager = require("../db/SecurityManager"); var securityManager = require("../db/SecurityManager");
var plugins = require("ep_etherpad-lite/static/js/pluginfw/plugins.js");
var log4js = require('log4js'); var log4js = require('log4js');
var messageLogger = log4js.getLogger("message"); var messageLogger = log4js.getLogger("message");
var _ = require('underscore');
/** /**
* A associative array that translates a session to a pad * A associative array that translates a session to a pad
@ -127,7 +130,11 @@ exports.handleDisconnect = function(client)
//Go trough all user that are still on the pad, and send them the USER_LEAVE message //Go trough all user that are still on the pad, and send them the USER_LEAVE message
for(i in pad2sessions[sessionPad]) for(i in pad2sessions[sessionPad])
{ {
socketio.sockets.sockets[pad2sessions[sessionPad][i]].json.send(messageToTheOtherUsers); var socket = socketio.sockets.sockets[pad2sessions[sessionPad][i]];
if(socket !== undefined){
socket.json.send(messageToTheOtherUsers);
}
} }
}); });
} }
@ -185,6 +192,11 @@ exports.handleMessage = function(client, message)
{ {
handleChatMessage(client, message); handleChatMessage(client, message);
} }
else if(message.type == "COLLABROOM" &&
message.data.type == "SAVE_REVISION")
{
handleSaveRevisionMessage(client, message);
}
else if(message.type == "COLLABROOM" && else if(message.type == "COLLABROOM" &&
message.data.type == "CLIENT_MESSAGE" && message.data.type == "CLIENT_MESSAGE" &&
message.data.payload.type == "suggestUserName") message.data.payload.type == "suggestUserName")
@ -198,6 +210,23 @@ exports.handleMessage = function(client, message)
} }
} }
/**
* Handles a save revision message
* @param client the client that send this message
* @param message the message from the client
*/
function handleSaveRevisionMessage(client, message){
var padId = session2pad[client.id];
var userId = sessioninfos[client.id].author;
padManager.getPad(padId, function(err, pad)
{
if(ERR(err)) return;
pad.addSavedRevision(pad.head, userId);
});
}
/** /**
* Handles a Chat Message * Handles a Chat Message
* @param client the client that send this message * @param client the client that send this message
@ -367,7 +396,7 @@ function handleUserChanges(client, message)
//get all Vars we need //get all Vars we need
var baseRev = message.data.baseRev; var baseRev = message.data.baseRev;
var wireApool = (AttributePoolFactory.createAttributePool()).fromJsonable(message.data.apool); var wireApool = (new AttributePool()).fromJsonable(message.data.apool);
var changeset = message.data.changeset; var changeset = message.data.changeset;
var r, apool, pad; var r, apool, pad;
@ -564,8 +593,12 @@ function _correctMarkersInPad(atext, apool) {
var offset = 0; var offset = 0;
while (iter.hasNext()) { while (iter.hasNext()) {
var op = iter.next(); var op = iter.next();
var listValue = Changeset.opAttributeValue(op, 'list', apool);
if (listValue) { var hasMarker = _.find(AttributeManager.lineAttributes, function(attribute){
return Changeset.opAttributeValue(op, attribute, apool);
}) !== undefined;
if (hasMarker) {
for(var i=0;i<op.chars;i++) { for(var i=0;i<op.chars;i++) {
if (offset > 0 && text.charAt(offset-1) != '\n') { if (offset > 0 && text.charAt(offset-1) != '\n') {
badMarkers.push(offset); badMarkers.push(offset);
@ -737,9 +770,10 @@ function handleClientReady(client, message)
{ {
for(var i in pad2sessions[message.padId]) for(var i in pad2sessions[message.padId])
{ {
if(sessioninfos[pad2sessions[message.padId][i]].author == author) if(sessioninfos[pad2sessions[message.padId][i]] && sessioninfos[pad2sessions[message.padId][i]].author == author)
{ {
socketio.sockets.sockets[pad2sessions[message.padId][i]].json.send({disconnect:"userdup"}); var socket = socketio.sockets.sockets[pad2sessions[message.padId][i]];
if(socket) socket.json.send({disconnect:"userdup"});
} }
} }
} }
@ -800,7 +834,10 @@ function handleClientReady(client, message)
"hideSidebar": false "hideSidebar": false
}, },
"abiwordAvailable": settings.abiwordAvailable(), "abiwordAvailable": settings.abiwordAvailable(),
"hooks": {} "plugins": {
"plugins": plugins.plugins,
"parts": plugins.parts,
}
} }
//Add a username to the clientVars if one avaiable //Add a username to the clientVars if one avaiable

View File

@ -18,12 +18,12 @@
* limitations under the License. * limitations under the License.
*/ */
var CommonCode = require('../utils/common_code');
var ERR = require("async-stacktrace"); var ERR = require("async-stacktrace");
var async = require("async"); var async = require("async");
var padManager = require("../db/PadManager"); var padManager = require("../db/PadManager");
var Changeset = CommonCode.require("/Changeset"); var Changeset = require("ep_etherpad-lite/static/js/Changeset");
var AttributePoolFactory = CommonCode.require("/AttributePoolFactory"); var AttributePool = require("ep_etherpad-lite/static/js/AttributePool");
var settings = require('../utils/Settings'); var settings = require('../utils/Settings');
var authorManager = require("../db/AuthorManager"); var authorManager = require("../db/AuthorManager");
var log4js = require('log4js'); var log4js = require('log4js');
@ -166,6 +166,7 @@ function createTimesliderClientVars (padId, callback)
hooks: [], hooks: [],
initialStyledContents: {} initialStyledContents: {}
}; };
var pad; var pad;
var initialChangesets = []; var initialChangesets = [];
@ -180,6 +181,12 @@ function createTimesliderClientVars (padId, callback)
callback(); callback();
}); });
}, },
//get all saved revisions and add them
function(callback)
{
clientVars.savedRevisions = pad.getSavedRevisions();
callback();
},
//get all authors and add them to //get all authors and add them to
function(callback) function(callback)
{ {
@ -265,7 +272,7 @@ function getChangesetInfo(padId, startNum, endNum, granularity, callback)
var forwardsChangesets = []; var forwardsChangesets = [];
var backwardsChangesets = []; var backwardsChangesets = [];
var timeDeltas = []; var timeDeltas = [];
var apool = AttributePoolFactory.createAttributePool(); var apool = new AttributePool();
var pad; var pad;
var composedChangesets = {}; var composedChangesets = {};
var revisionDate = []; var revisionDate = [];

View File

@ -0,0 +1,51 @@
var path = require('path');
var eejs = require('ep_etherpad-lite/node/eejs');
var installer = require('ep_etherpad-lite/static/js/pluginfw/installer');
var plugins = require('ep_etherpad-lite/static/js/pluginfw/plugins');
exports.expressCreateServer = function (hook_name, args, cb) {
args.app.get('/admin/plugins', function(req, res) {
var plugins = require("ep_etherpad-lite/static/js/pluginfw/plugins");
var render_args = {
plugins: plugins.plugins,
search_results: {},
errors: [],
};
res.send(eejs.require(
"ep_etherpad-lite/templates/admin/plugins.html",
render_args), {});
});
}
exports.socketio = function (hook_name, args, cb) {
var io = args.io.of("/pluginfw/installer");
io.on('connection', function (socket) {
socket.on("load", function (query) {
socket.emit("installed-results", {results: plugins.plugins});
});
socket.on("search", function (query) {
socket.emit("progress", {progress:0, message:'Fetching results...'});
installer.search(query, function (progress) {
if (progress.results)
socket.emit("search-result", progress);
socket.emit("progress", progress);
});
});
socket.on("install", function (plugin_name) {
socket.emit("progress", {progress:0, message:'Downloading and installing ' + plugin_name + "..."});
installer.install(plugin_name, function (progress) {
socket.emit("progress", progress);
});
});
socket.on("uninstall", function (plugin_name) {
socket.emit("progress", {progress:0, message:'Uninstalling ' + plugin_name + "..."});
installer.uninstall(plugin_name, function (progress) {
socket.emit("progress", progress);
});
});
});
}

View File

@ -0,0 +1,60 @@
var log4js = require('log4js');
var apiLogger = log4js.getLogger("API");
var formidable = require('formidable');
var apiHandler = require('../../handler/APIHandler');
//This is for making an api call, collecting all post information and passing it to the apiHandler
var apiCaller = function(req, res, fields) {
res.header("Content-Type", "application/json; charset=utf-8");
apiLogger.info("REQUEST, " + req.params.func + ", " + JSON.stringify(fields));
//wrap the send function so we can log the response
//note: res._send seems to be already in use, so better use a "unique" name
res._____send = res.send;
res.send = function (response) {
response = JSON.stringify(response);
apiLogger.info("RESPONSE, " + req.params.func + ", " + response);
//is this a jsonp call, if yes, add the function call
if(req.query.jsonp)
response = req.query.jsonp + "(" + response + ")";
res._____send(response);
}
//call the api handler
apiHandler.handle(req.params.func, fields, req, res);
}
exports.apiCaller = apiCaller;
exports.expressCreateServer = function (hook_name, args, cb) {
//This is a api GET call, collect all post informations and pass it to the apiHandler
args.app.get('/api/1/:func', function (req, res) {
apiCaller(req, res, req.query)
});
//This is a api POST call, collect all post informations and pass it to the apiHandler
args.app.post('/api/1/:func', function(req, res) {
new formidable.IncomingForm().parse(req, function (err, fields, files) {
apiCaller(req, res, fields)
});
});
//The Etherpad client side sends information about how a disconnect happen
args.app.post('/ep/pad/connection-diagnostic-info', function(req, res) {
new formidable.IncomingForm().parse(req, function(err, fields, files) {
console.log("DIAGNOSTIC-INFO: " + fields.diagnosticInfo);
res.end("OK");
});
});
//The Etherpad client side sends information about client side javscript errors
args.app.post('/jserror', function(req, res) {
new formidable.IncomingForm().parse(req, function(err, fields, files) {
console.error("CLIENT SIDE JAVASCRIPT ERROR: " + fields.errorInfo);
res.end("OK");
});
});
}

View File

@ -0,0 +1,52 @@
var os = require("os");
var db = require('../../db/DB');
exports.onShutdown = false;
exports.gracefulShutdown = function(err) {
if(err && err.stack) {
console.error(err.stack);
} else if(err) {
console.error(err);
}
//ensure there is only one graceful shutdown running
if(exports.onShutdown) return;
exports.onShutdown = true;
console.log("graceful shutdown...");
//stop the http server
exports.app.close();
//do the db shutdown
db.db.doShutdown(function() {
console.log("db sucessfully closed.");
process.exit(0);
});
setTimeout(function(){
process.exit(1);
}, 3000);
}
exports.expressCreateServer = function (hook_name, args, cb) {
exports.app = args.app;
args.app.error(function(err, req, res, next){
res.send(500);
console.error(err.stack ? err.stack : err.toString());
exports.gracefulShutdown();
});
//connect graceful shutdown with sigint and uncaughtexception
if(os.type().indexOf("Windows") == -1) {
//sigint is so far not working on windows
//https://github.com/joyent/node/issues/1553
process.on('SIGINT', exports.gracefulShutdown);
}
process.on('uncaughtException', exports.gracefulShutdown);
}

View File

@ -0,0 +1,41 @@
var hasPadAccess = require("../../padaccess");
var settings = require('../../utils/Settings');
var exportHandler = require('../../handler/ExportHandler');
var importHandler = require('../../handler/ImportHandler');
exports.expressCreateServer = function (hook_name, args, cb) {
args.app.get('/p/:pad/:rev?/export/:type', function(req, res, next) {
var types = ["pdf", "doc", "txt", "html", "odt", "dokuwiki"];
//send a 404 if we don't support this filetype
if (types.indexOf(req.params.type) == -1) {
next();
return;
}
//if abiword is disabled, and this is a format we only support with abiword, output a message
if (settings.abiword == null &&
["odt", "pdf", "doc"].indexOf(req.params.type) !== -1) {
res.send("Abiword is not enabled at this Etherpad Lite instance. Set the path to Abiword in settings.json to enable this feature");
return;
}
res.header("Access-Control-Allow-Origin", "*");
hasPadAccess(req, res, function() {
exportHandler.doExport(req, res, req.params.pad, req.params.type);
});
});
//handle import requests
args.app.post('/p/:pad/import', function(req, res, next) {
//if abiword is disabled, skip handling this request
if(settings.abiword == null) {
next();
return;
}
hasPadAccess(req, res, function() {
importHandler.doImport(req, res, req.params.pad);
});
});
}

View File

@ -0,0 +1,65 @@
var async = require('async');
var ERR = require("async-stacktrace");
var readOnlyManager = require("../../db/ReadOnlyManager");
var hasPadAccess = require("../../padaccess");
var exporthtml = require("../../utils/ExportHtml");
exports.expressCreateServer = function (hook_name, args, cb) {
//serve read only pad
args.app.get('/ro/:id', function(req, res)
{
var html;
var padId;
var pad;
async.series([
//translate the read only pad to a padId
function(callback)
{
readOnlyManager.getPadId(req.params.id, function(err, _padId)
{
if(ERR(err, callback)) return;
padId = _padId;
//we need that to tell hasPadAcess about the pad
req.params.pad = padId;
callback();
});
},
//render the html document
function(callback)
{
//return if the there is no padId
if(padId == null)
{
callback("notfound");
return;
}
hasPadAccess(req, res, function()
{
//render the html document
exporthtml.getPadHTMLDocument(padId, null, false, function(err, _html)
{
if(ERR(err, callback)) return;
html = _html;
callback();
});
});
}
], function(err)
{
//throw any unexpected error
if(err && err != "notfound")
ERR(err);
if(err == "notfound")
res.send('404 - Not Found', 404);
else
res.send(html);
});
});
}

View File

@ -0,0 +1,29 @@
var padManager = require('../../db/PadManager');
exports.expressCreateServer = function (hook_name, args, cb) {
//redirects browser to the pad's sanitized url if needed. otherwise, renders the html
args.app.param('pad', function (req, res, next, padId) {
//ensure the padname is valid and the url doesn't end with a /
if(!padManager.isValidPadId(padId) || /\/$/.test(req.url))
{
res.send('Such a padname is forbidden', 404);
}
else
{
padManager.sanitizePadId(padId, function(sanitizedPadId) {
//the pad id was sanitized, so we redirect to the sanitized version
if(sanitizedPadId != padId)
{
var real_path = req.path.replace(/^\/p\/[^\/]+/, '/p/' + sanitizedPadId);
res.header('Location', real_path);
res.send('You should be redirected to <a href="' + real_path + '">' + real_path + '</a>', 302);
}
//the pad id was fine, so just render it
else
{
next();
}
});
}
});
}

View File

@ -0,0 +1,49 @@
var log4js = require('log4js');
var socketio = require('socket.io');
var settings = require('../../utils/Settings');
var socketIORouter = require("../../handler/SocketIORouter");
var hooks = require("ep_etherpad-lite/static/js/pluginfw/hooks");
var padMessageHandler = require("../../handler/PadMessageHandler");
var timesliderMessageHandler = require("../../handler/TimesliderMessageHandler");
exports.expressCreateServer = function (hook_name, args, cb) {
//init socket.io and redirect all requests to the MessageHandler
var io = socketio.listen(args.app);
//this is only a workaround to ensure it works with all browers behind a proxy
//we should remove this when the new socket.io version is more stable
io.set('transports', ['xhr-polling']);
var socketIOLogger = log4js.getLogger("socket.io");
io.set('logger', {
debug: function (str)
{
socketIOLogger.debug.apply(socketIOLogger, arguments);
},
info: function (str)
{
socketIOLogger.info.apply(socketIOLogger, arguments);
},
warn: function (str)
{
socketIOLogger.warn.apply(socketIOLogger, arguments);
},
error: function (str)
{
socketIOLogger.error.apply(socketIOLogger, arguments);
},
});
//minify socket.io javascript
if(settings.minify)
io.enable('browser client minification');
//Initalize the Socket.IO Router
socketIORouter.setSocketIO(io);
socketIORouter.addComponent("pad", padMessageHandler);
socketIORouter.addComponent("timeslider", timesliderMessageHandler);
hooks.callAll("socketio", {"app": args.app, "io": io});
}

View File

@ -0,0 +1,46 @@
var path = require('path');
var eejs = require('ep_etherpad-lite/node/eejs');
exports.expressCreateServer = function (hook_name, args, cb) {
//serve index.html under /
args.app.get('/', function(req, res)
{
res.send(eejs.require("ep_etherpad-lite/templates/index.html"));
});
//serve robots.txt
args.app.get('/robots.txt', function(req, res)
{
var filePath = path.normalize(__dirname + "/../../../static/robots.txt");
res.sendfile(filePath);
});
//serve favicon.ico
args.app.get('/favicon.ico', function(req, res)
{
var filePath = path.normalize(__dirname + "/../../../static/custom/favicon.ico");
res.sendfile(filePath, function(err)
{
//there is no custom favicon, send the default favicon
if(err)
{
filePath = path.normalize(__dirname + "/../../../static/favicon.ico");
res.sendfile(filePath);
}
});
});
//serve pad.html under /p
args.app.get('/p/:pad', function(req, res, next)
{
res.send(eejs.require("ep_etherpad-lite/templates/pad.html"));
});
//serve timeslider.html under /p/$padname/timeslider
args.app.get('/p/:pad/timeslider', function(req, res, next)
{
res.send(eejs.require("ep_etherpad-lite/templates/timeslider.html"));
});
}

View File

@ -0,0 +1,57 @@
var path = require('path');
var minify = require('../../utils/Minify');
var plugins = require("ep_etherpad-lite/static/js/pluginfw/plugins");
var CachingMiddleware = require('../../utils/caching_middleware');
var settings = require("../../utils/Settings");
var Yajsml = require('yajsml');
var fs = require("fs");
var ERR = require("async-stacktrace");
var _ = require("underscore");
exports.expressCreateServer = function (hook_name, args, cb) {
// Cache both minified and static.
var assetCache = new CachingMiddleware;
args.app.all('/(javascripts|static)/*', assetCache.handle);
// Minify will serve static files compressed (minify enabled). It also has
// file-specific hacks for ace/require-kernel/etc.
args.app.all('/static/:filename(*)', minify.minify);
// Setup middleware that will package JavaScript files served by minify for
// CommonJS loader on the client-side.
var jsServer = new (Yajsml.Server)({
rootPath: 'javascripts/src/'
, rootURI: 'http://localhost:' + settings.port + '/static/js/'
, libraryPath: 'javascripts/lib/'
, libraryURI: 'http://localhost:' + settings.port + '/static/plugins/'
});
var StaticAssociator = Yajsml.associators.StaticAssociator;
var associations =
Yajsml.associators.associationsForSimpleMapping(minify.tar);
var associator = new StaticAssociator(associations);
jsServer.setAssociator(associator);
args.app.use(jsServer);
// serve plugin definitions
// not very static, but served here so that client can do require("pluginfw/static/js/plugin-definitions.js");
args.app.get('/pluginfw/plugin-definitions.json', function (req, res, next) {
var clientParts = _(plugins.parts)
.filter(function(part){ return _(part).has('client_hooks') });
var clientPlugins = {};
_(clientParts).chain()
.map(function(part){ return part.plugin })
.uniq()
.each(function(name){
clientPlugins[name] = _(plugins.plugins[name]).clone();
delete clientPlugins[name]['package'];
});
res.header("Content-Type","application/json; charset=utf-8");
res.write(JSON.stringify({"plugins": clientPlugins, "parts": clientParts}));
res.end();
});
}

View File

@ -0,0 +1,51 @@
var express = require('express');
var log4js = require('log4js');
var httpLogger = log4js.getLogger("http");
var settings = require('../../utils/Settings');
//checks for basic http auth
exports.basicAuth = function (req, res, next) {
// When handling HTTP-Auth, an undefined password will lead to no authorization at all
var pass = settings.httpAuth || '';
if (req.path.indexOf('/admin') == 0) {
var pass = settings.adminHttpAuth;
}
// Just pass if password is an empty string
if (pass === '') {
return next();
}
// If a password has been set and auth headers are present...
if (pass && req.headers.authorization && req.headers.authorization.search('Basic ') === 0) {
// ...check login and password
if (new Buffer(req.headers.authorization.split(' ')[1], 'base64').toString() === pass) {
return next();
}
}
// Otherwise return Auth required Headers, delayed for 1 second, if auth failed.
res.header('WWW-Authenticate', 'Basic realm="Protected Area"');
if (req.headers.authorization) {
setTimeout(function () {
res.send('Authentication required', 401);
}, 1000);
} else {
res.send('Authentication required', 401);
}
}
exports.expressConfigure = function (hook_name, args, cb) {
args.app.use(exports.basicAuth);
// If the log level specified in the config file is WARN or ERROR the application server never starts listening to requests as reported in issue #158.
// Not installing the log4js connect logger when the log level has a higher severity than INFO since it would not log at that level anyway.
if (!(settings.loglevel === "WARN" || settings.loglevel == "ERROR"))
args.app.use(log4js.connectLogger(httpLogger, { level: log4js.levels.INFO, format: ':status, :method :url'}));
args.app.use(express.cookieParser());
}

21
src/node/padaccess.js Normal file
View File

@ -0,0 +1,21 @@
var ERR = require("async-stacktrace");
var securityManager = require('./db/SecurityManager');
//checks for padAccess
module.exports = function (req, res, callback) {
// FIXME: Why is this ever undefined??
if (req.cookies === undefined) req.cookies = {};
securityManager.checkAccess(req.params.pad, req.cookies.sessionid, req.cookies.token, req.cookies.password, function(err, accessObj) {
if(ERR(err, callback)) return;
//there is access, continue
if(accessObj.accessStatus == "grant") {
callback();
//no access
} else {
res.send("403 - Can't touch this", 403);
}
});
}

99
src/node/server.js Normal file
View File

@ -0,0 +1,99 @@
/**
* This module is started with bin/run.sh. It sets up a Express HTTP and a Socket.IO Server.
* Static file Requests are answered directly from this module, Socket.IO messages are passed
* to MessageHandler and minfied requests are passed to minified.
*/
/*
* 2011 Peter 'Pita' Martischka (Primary Technology Ltd)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS-IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
var log4js = require('log4js');
var fs = require('fs');
var settings = require('./utils/Settings');
var db = require('./db/DB');
var async = require('async');
var express = require('express');
var path = require('path');
var plugins = require("ep_etherpad-lite/static/js/pluginfw/plugins");
var hooks = require("ep_etherpad-lite/static/js/pluginfw/hooks");
var npm = require("npm/lib/npm.js");
//try to get the git version
var version = "";
try
{
var rootPath = path.resolve(npm.dir, '..');
var ref = fs.readFileSync(rootPath + "/.git/HEAD", "utf-8");
var refPath = rootPath + "/.git/" + ref.substring(5, ref.indexOf("\n"));
version = fs.readFileSync(refPath, "utf-8");
version = version.substring(0, 7);
console.log("Your Etherpad Lite git version is " + version);
}
catch(e)
{
console.warn("Can't get git version for server header\n" + e.message)
}
console.log("Report bugs at https://github.com/Pita/etherpad-lite/issues")
var serverName = "Etherpad-Lite " + version + " (http://j.mp/ep-lite)";
//set loglevel
log4js.setGlobalLogLevel(settings.loglevel);
async.waterfall([
//initalize the database
function (callback)
{
db.init(callback);
},
plugins.update,
function (callback) {
console.log("Installed plugins: " + plugins.formatPlugins());
console.log("Installed parts:\n" + plugins.formatParts());
console.log("Installed hooks:\n" + plugins.formatHooks());
callback();
},
//initalize the http server
function (callback)
{
//create server
var app = express.createServer();
app.use(function (req, res, next) {
res.header("Server", serverName);
next();
});
app.configure(function() { hooks.callAll("expressConfigure", {"app": app}); });
hooks.callAll("expressCreateServer", {"app": app});
//let the server listen
app.listen(settings.port, settings.ip);
console.log("Server is listening at " + settings.ip + ":" + settings.port);
if(settings.adminHttpAuth){
console.log("Plugin admin page listening at " + settings.ip + ":" + settings.port + "/admin/plugins");
}
else{
console.log("Admin username and password not set in settings.json. To access admin please uncomment and edit adminHttpAuth in settings.json");
}
callback(null);
}
]);

View File

@ -15,8 +15,8 @@
*/ */
var async = require("async"); var async = require("async");
var CommonCode = require('./common_code');
var Changeset = CommonCode.require("/Changeset"); var Changeset = require("ep_etherpad-lite/static/js/Changeset");
var padManager = require("../db/PadManager"); var padManager = require("../db/PadManager");
function getPadDokuWiki(pad, revNum, callback) function getPadDokuWiki(pad, revNum, callback)

View File

@ -14,12 +14,12 @@
* limitations under the License. * limitations under the License.
*/ */
var CommonCode = require('./common_code');
var async = require("async"); var async = require("async");
var Changeset = CommonCode.require("/Changeset"); var Changeset = require("ep_etherpad-lite/static/js/Changeset");
var padManager = require("../db/PadManager"); var padManager = require("../db/PadManager");
var ERR = require("async-stacktrace"); var ERR = require("async-stacktrace");
var Security = CommonCode.require('/security'); var Security = require('ep_etherpad-lite/static/js/security');
function getPadPlainText(pad, revNum) function getPadPlainText(pad, revNum)
{ {

View File

@ -17,10 +17,10 @@
var jsdom = require('jsdom-nocontextifiy').jsdom; var jsdom = require('jsdom-nocontextifiy').jsdom;
var log4js = require('log4js'); var log4js = require('log4js');
var CommonCode = require('../utils/common_code');
var Changeset = CommonCode.require("/Changeset"); var Changeset = require("ep_etherpad-lite/static/js/Changeset");
var contentcollector = CommonCode.require("/contentcollector"); var contentcollector = require("ep_etherpad-lite/static/js/contentcollector");
var map = CommonCode.require("/ace2_common").map; var map = require("ep_etherpad-lite/static/js/ace2_common").map;
function setPadHTML(pad, html, callback) function setPadHTML(pad, html, callback)
{ {

View File

@ -27,19 +27,22 @@ var cleanCSS = require('clean-css');
var jsp = require("uglify-js").parser; var jsp = require("uglify-js").parser;
var pro = require("uglify-js").uglify; var pro = require("uglify-js").uglify;
var path = require('path'); var path = require('path');
var plugins = require("ep_etherpad-lite/static/js/pluginfw/plugins");
var RequireKernel = require('require-kernel'); var RequireKernel = require('require-kernel');
var server = require('../server');
var ROOT_DIR = path.normalize(__dirname + "/../../static/"); var ROOT_DIR = path.normalize(__dirname + "/../../static/");
var TAR_PATH = path.join(__dirname, 'tar.json'); var TAR_PATH = path.join(__dirname, 'tar.json');
var tar = JSON.parse(fs.readFileSync(TAR_PATH, 'utf8')); var tar = JSON.parse(fs.readFileSync(TAR_PATH, 'utf8'));
// Rewrite tar to include modules with no extensions and proper rooted paths. // Rewrite tar to include modules with no extensions and proper rooted paths.
var LIBRARY_PREFIX = 'ep_etherpad-lite/static/js';
exports.tar = {}; exports.tar = {};
for (var key in tar) { for (var key in tar) {
exports.tar['/' + key] = exports.tar[LIBRARY_PREFIX + '/' + key] =
tar[key].map(function (p) {return '/' + p}).concat( tar[key].map(function (p) {return LIBRARY_PREFIX + '/' + p}).concat(
tar[key].map(function (p) {return '/' + p.replace(/\.js$/, '')}) tar[key].map(function (p) {
return LIBRARY_PREFIX + '/' + p.replace(/\.js$/, '')
})
); );
} }
@ -63,6 +66,22 @@ exports.minify = function(req, res, next)
return; return;
} }
/* Handle static files for plugins:
paths like "plugins/ep_myplugin/static/js/test.js"
are rewritten into ROOT_PATH_OF_MYPLUGIN/static/js/test.js,
commonly ETHERPAD_ROOT/node_modules/ep_myplugin/static/js/test.js
*/
var match = filename.match(/^plugins\/([^\/]+)\/static\/(.*)/);
if (match) {
var pluginName = match[1];
var resourcePath = match[2];
var plugin = plugins.plugins[pluginName];
if (plugin) {
var pluginPath = plugin.package.realPath;
filename = path.relative(ROOT_DIR, pluginPath + '/static/' + resourcePath);
}
}
// What content type should this be? // What content type should this be?
// TODO: This should use a MIME module. // TODO: This should use a MIME module.
var contentType; var contentType;
@ -89,10 +108,10 @@ exports.minify = function(req, res, next)
date = new Date(date); date = new Date(date);
res.setHeader('last-modified', date.toUTCString()); res.setHeader('last-modified', date.toUTCString());
res.setHeader('date', (new Date()).toUTCString()); res.setHeader('date', (new Date()).toUTCString());
if (server.maxAge) { if (settings.maxAge !== undefined) {
var expiresDate = new Date((new Date()).getTime()+server.maxAge*1000); var expiresDate = new Date((new Date()).getTime()+settings.maxAge*1000);
res.setHeader('expires', expiresDate.toUTCString()); res.setHeader('expires', expiresDate.toUTCString());
res.setHeader('cache-control', 'max-age=' + server.maxAge); res.setHeader('cache-control', 'max-age=' + settings.maxAge);
} }
} }
@ -112,7 +131,10 @@ exports.minify = function(req, res, next)
res.end(); res.end();
} else if (req.method == 'GET') { } else if (req.method == 'GET') {
getFileCompressed(filename, contentType, function (error, content) { getFileCompressed(filename, contentType, function (error, content) {
if(ERR(error)) return; if(ERR(error, function(){
res.writeHead(500, {});
res.end();
})) return;
res.header("Content-Type", contentType); res.header("Content-Type", contentType);
res.writeHead(200, {}); res.writeHead(200, {});
res.write(content); res.write(content);

View File

@ -0,0 +1,513 @@
/**
* This Module manages all /minified/* requests. It controls the
* minification && compression of Javascript and CSS.
*/
/*
* 2011 Peter 'Pita' Martischka (Primary Technology Ltd)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS-IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
var ERR = require("async-stacktrace");
var settings = require('./Settings');
var async = require('async');
var fs = require('fs');
var cleanCSS = require('clean-css');
var jsp = require("uglify-js").parser;
var pro = require("uglify-js").uglify;
var path = require('path');
var RequireKernel = require('require-kernel');
var server = require('../server');
<<<<<<< HEAD
var ROOT_DIR = path.normalize(__dirname + "/../" );
var JS_DIR = ROOT_DIR + '../static/js/';
var CSS_DIR = ROOT_DIR + '../static/css/';
var CACHE_DIR = path.join(settings.root, 'var');
=======
var ROOT_DIR = path.normalize(__dirname + "/../../static/");
>>>>>>> pita
var TAR_PATH = path.join(__dirname, 'tar.json');
var tar = JSON.parse(fs.readFileSync(TAR_PATH, 'utf8'));
// Rewrite tar to include modules with no extensions and proper rooted paths.
exports.tar = {};
for (var key in tar) {
exports.tar['/' + key] =
tar[key].map(function (p) {return '/' + p}).concat(
tar[key].map(function (p) {return '/' + p.replace(/\.js$/, '')})
);
}
/**
* creates the minifed javascript for the given minified name
* @param req the Express request
* @param res the Express response
*/
exports.minify = function(req, res, next)
{
<<<<<<< HEAD
var jsFilename = req.params[0];
//choose the js files we need
var jsFiles = undefined;
if (Object.prototype.hasOwnProperty.call(tar, jsFilename)) {
jsFiles = tar[jsFilename];
} else {
/* Not in tar list, but try anyways, if it fails, pass to `next`.
Actually try, not check in filesystem here because
we don't want to duplicate the require.resolve() handling
*/
jsFiles = [jsFilename];
}
_handle(req, res, jsFilename, jsFiles, function (err) {
console.log("Unable to load minified file " + jsFilename + ": " + err.toString());
/* Throw away error and generate a 404, not 500 */
next();
});
}
function _handle(req, res, jsFilename, jsFiles, next) {
res.header("Content-Type","text/javascript");
var cacheName = CACHE_DIR + "/minified_" + jsFilename.replace(/\//g, "_");
//minifying is enabled
if(settings.minify)
{
var result = undefined;
var latestModification = 0;
async.series([
//find out the highest modification date
function(callback)
{
var folders2check = [CSS_DIR, JS_DIR];
//go trough this two folders
async.forEach(folders2check, function(path, callback)
{
//read the files in the folder
fs.readdir(path, function(err, files)
{
if(ERR(err, callback)) return;
//we wanna check the directory itself for changes too
files.push(".");
//go trough all files in this folder
async.forEach(files, function(filename, callback)
{
//get the stat data of this file
fs.stat(path + "/" + filename, function(err, stats)
{
if(ERR(err, callback)) return;
//get the modification time
var modificationTime = stats.mtime.getTime();
//compare the modification time to the highest found
if(modificationTime > latestModification)
{
latestModification = modificationTime;
}
callback();
});
}, callback);
});
}, callback);
},
function(callback)
{
//check the modification time of the minified js
fs.stat(cacheName, function(err, stats)
{
if(err && err.code != "ENOENT")
{
ERR(err, callback);
return;
}
//there is no minfied file or there new changes since this file was generated, so continue generating this file
if((err && err.code == "ENOENT") || stats.mtime.getTime() < latestModification)
{
callback();
}
//the minified file is still up to date, stop minifying
else
{
callback("stop");
}
});
},
//load all js files
function (callback)
{
var values = [];
tarCode(
jsFiles
, function (content) {values.push(content)}
, function (err) {
if(ERR(err, next)) return;
result = values.join('');
callback();
});
},
//put all together and write it into a file
function(callback)
{
async.parallel([
//write the results plain in a file
function(callback)
{
fs.writeFile(cacheName, result, "utf8", callback);
},
//write the results compressed in a file
function(callback)
{
zlib.gzip(result, function(err, compressedResult){
//weird gzip bug that returns 0 instead of null if everything is ok
err = err === 0 ? null : err;
if(ERR(err, callback)) return;
fs.writeFile(cacheName + ".gz", compressedResult, callback);
});
}
],callback);
}
], function(err)
{
if(err && err != "stop")
{
if(ERR(err)) return;
}
//check if gzip is supported by this browser
var gzipSupport = req.header('Accept-Encoding', '').indexOf('gzip') != -1;
var pathStr;
if(gzipSupport && os.type().indexOf("Windows") == -1)
{
pathStr = path.normalize(cacheName + ".gz");
res.header('Content-Encoding', 'gzip');
}
else
{
pathStr = path.normalize(cacheName);
}
res.sendfile(pathStr, { maxAge: server.maxAge });
})
}
//minifying is disabled, so put the files together in one file
else
{
tarCode(
jsFiles
, function (content) {res.write(content)}
, function (err) {
if(ERR(err, next)) return;
=======
var filename = req.params['filename'];
// No relative paths, especially if they may go up the file hierarchy.
filename = path.normalize(path.join(ROOT_DIR, filename));
if (filename.indexOf(ROOT_DIR) == 0) {
filename = filename.slice(ROOT_DIR.length);
filename = filename.replace(/\\/g, '/'); // Windows (safe generally?)
} else {
res.writeHead(404, {});
res.end();
return;
}
// What content type should this be?
// TODO: This should use a MIME module.
var contentType;
if (filename.match(/\.js$/)) {
contentType = "text/javascript";
} else if (filename.match(/\.css$/)) {
contentType = "text/css";
} else if (filename.match(/\.html$/)) {
contentType = "text/html";
} else if (filename.match(/\.txt$/)) {
contentType = "text/plain";
} else if (filename.match(/\.png$/)) {
contentType = "image/png";
} else if (filename.match(/\.gif$/)) {
contentType = "image/gif";
} else if (filename.match(/\.ico$/)) {
contentType = "image/x-icon";
} else {
contentType = "application/octet-stream";
}
statFile(filename, function (error, date, exists) {
if (date) {
date = new Date(date);
res.setHeader('last-modified', date.toUTCString());
res.setHeader('date', (new Date()).toUTCString());
if (server.maxAge) {
var expiresDate = new Date((new Date()).getTime()+server.maxAge*1000);
res.setHeader('expires', expiresDate.toUTCString());
res.setHeader('cache-control', 'max-age=' + server.maxAge);
}
}
if (error) {
res.writeHead(500, {});
>>>>>>> pita
res.end();
} else if (!exists) {
res.writeHead(404, {});
res.end();
} else if (new Date(req.headers['if-modified-since']) >= date) {
res.writeHead(304, {});
res.end();
} else {
if (req.method == 'HEAD') {
res.header("Content-Type", contentType);
res.writeHead(200, {});
res.end();
} else if (req.method == 'GET') {
getFileCompressed(filename, contentType, function (error, content) {
if(ERR(error)) return;
res.header("Content-Type", contentType);
res.writeHead(200, {});
res.write(content);
res.end();
});
} else {
res.writeHead(405, {'allow': 'HEAD, GET'});
res.end();
}
}
});
}
// find all includes in ace.js and embed them.
function getAceFile(callback) {
fs.readFile(ROOT_DIR + 'js/ace.js', "utf8", function(err, data) {
if(ERR(err, callback)) return;
// Find all includes in ace.js and embed them
var founds = data.match(/\$\$INCLUDE_[a-zA-Z_]+\("[^"]*"\)/gi);
if (!settings.minify) {
founds = [];
}
// Always include the require kernel.
founds.push('$$INCLUDE_JS("../static/js/require-kernel.js")');
data += ';\n';
data += 'Ace2Editor.EMBEDED = Ace2Editor.EMBEDED || {};\n';
// Request the contents of the included file on the server-side and write
// them into the file.
async.forEach(founds, function (item, callback) {
var filename = item.match(/"([^"]*)"/)[1];
var request = require('request');
var baseURI = 'http://localhost:' + settings.port
request(baseURI + path.normalize(path.join('/static/', filename)), function (error, response, body) {
if (!error && response.statusCode == 200) {
data += 'Ace2Editor.EMBEDED[' + JSON.stringify(filename) + '] = '
+ JSON.stringify(body || '') + ';\n';
} else {
// Silence?
}
callback();
});
}, function(error) {
callback(error, data);
});
});
}
// Check for the existance of the file and get the last modification date.
function statFile(filename, callback) {
if (filename == 'js/ace.js') {
// Sometimes static assets are inlined into this file, so we have to stat
// everything.
lastModifiedDateOfEverything(function (error, date) {
callback(error, date, !error);
});
} else if (filename == 'js/require-kernel.js') {
callback(null, requireLastModified(), true);
} else {
fs.stat(ROOT_DIR + filename, function (error, stats) {
if (error) {
if (error.code == "ENOENT") {
// Stat the directory instead.
fs.stat(path.dirname(ROOT_DIR + filename), function (error, stats) {
if (error) {
if (error.code == "ENOENT") {
callback(null, null, false);
} else {
callback(error);
}
} else {
callback(null, stats.mtime.getTime(), false);
}
});
} else {
callback(error);
}
} else {
callback(null, stats.mtime.getTime(), true);
}
});
}
}
function lastModifiedDateOfEverything(callback) {
var folders2check = [ROOT_DIR + 'js/', ROOT_DIR + 'css/'];
var latestModification = 0;
//go trough this two folders
async.forEach(folders2check, function(path, callback)
{
//read the files in the folder
fs.readdir(path, function(err, files)
{
if(ERR(err, callback)) return;
//we wanna check the directory itself for changes too
files.push(".");
//go trough all files in this folder
async.forEach(files, function(filename, callback)
{
//get the stat data of this file
fs.stat(path + "/" + filename, function(err, stats)
{
if(ERR(err, callback)) return;
//get the modification time
var modificationTime = stats.mtime.getTime();
//compare the modification time to the highest found
if(modificationTime > latestModification)
{
latestModification = modificationTime;
}
callback();
});
}, callback);
});
}, function () {
callback(null, latestModification);
});
}
// This should be provided by the module, but until then, just use startup
// time.
var _requireLastModified = new Date();
function requireLastModified() {
return _requireLastModified.toUTCString();
}
function requireDefinition() {
return 'var require = ' + RequireKernel.kernelSource + ';\n';
}
<<<<<<< HEAD
function tarCode(jsFiles, write, callback) {
write('require.define({');
var initialEntry = true;
async.forEach(jsFiles, function (filename, callback){
var path;
var srcPath;
if (filename.indexOf('plugins/') == 0) {
srcPath = filename.substring('plugins/'.length);
path = require.resolve(srcPath);
} else {
srcPath = '/' + filename;
path = JS_DIR + filename;
}
srcPath = JSON.stringify(srcPath);
var srcPathAbbv = JSON.stringify(srcPath.replace(/\.js$/, ''));
if (filename == 'ace.js') {
getAceFile(handleFile);
} else {
fs.readFile(path, "utf8", handleFile);
}
function handleFile(err, data) {
if(ERR(err, callback)) return;
if (!initialEntry) {
write('\n,');
} else {
initialEntry = false;
}
write(srcPath + ': ')
data = '(function (require, exports, module) {' + data + '})';
=======
function getFileCompressed(filename, contentType, callback) {
getFile(filename, function (error, content) {
if (error || !content) {
callback(error, content);
} else {
>>>>>>> pita
if (settings.minify) {
if (contentType == 'text/javascript') {
try {
content = compressJS([content]);
} catch (error) {
// silence
}
} else if (contentType == 'text/css') {
content = compressCSS([content]);
}
}
callback(null, content);
}
<<<<<<< HEAD
}, function (err) {
if(ERR(err, callback)) return;
write('});\n');
callback();
=======
>>>>>>> pita
});
}
function getFile(filename, callback) {
if (filename == 'js/ace.js') {
getAceFile(callback);
} else if (filename == 'js/require-kernel.js') {
callback(undefined, requireDefinition());
} else {
fs.readFile(ROOT_DIR + filename, callback);
}
}
function compressJS(values)
{
var complete = values.join("\n");
var ast = jsp.parse(complete); // parse code and get the initial AST
ast = pro.ast_mangle(ast); // get a new AST with mangled names
ast = pro.ast_squeeze(ast); // get an AST with compression optimizations
return pro.gen_code(ast); // compressed code here
}
function compressCSS(values)
{
var complete = values.join("\n");
return cleanCSS.process(complete);
}

View File

@ -23,6 +23,10 @@ var fs = require("fs");
var os = require("os"); var os = require("os");
var path = require('path'); var path = require('path');
var argv = require('./Cli').argv; var argv = require('./Cli').argv;
var npm = require("npm/lib/npm.js");
/* Root path of the installation */
exports.root = path.normalize(path.join(npm.dir, ".."));
/** /**
* The IP ep-lite should listen to * The IP ep-lite should listen to
@ -40,7 +44,7 @@ exports.dbType = "dirty";
/** /**
* This setting is passed with dbType to ueberDB to set up the database * This setting is passed with dbType to ueberDB to set up the database
*/ */
exports.dbSettings = { "filename" : "../var/dirty.db" }; exports.dbSettings = { "filename" : path.join(exports.root, "var/dirty.db") };
/** /**
* The default Text of a new pad * The default Text of a new pad
*/ */
@ -81,6 +85,11 @@ exports.loglevel = "INFO";
*/ */
exports.httpAuth = null; exports.httpAuth = null;
/**
* Http basic auth, with "user:password" format
*/
exports.adminHttpAuth = null;
//checks if abiword is avaiable //checks if abiword is avaiable
exports.abiwordAvailable = function() exports.abiwordAvailable = function()
{ {
@ -96,10 +105,12 @@ exports.abiwordAvailable = function()
// Discover where the settings file lives // Discover where the settings file lives
var settingsFilename = argv.settings || "settings.json"; var settingsFilename = argv.settings || "settings.json";
var settingsPath = settingsFilename.charAt(0) == '/' ? '' : path.normalize(__dirname + "/../../"); if (settingsFilename.charAt(0) != '/') {
settingsFilename = path.normalize(path.join(root, settingsFilename));
}
//read the settings sync //read the settings sync
var settingsStr = fs.readFileSync(settingsPath + settingsFilename).toString(); var settingsStr = fs.readFileSync(settingsFilename).toString();
//remove all comments //remove all comments
settingsStr = settingsStr.replace(/\*([^*]|[\r\n]|(\*+([^*/]|[\r\n])))*\*+/gm,"").replace(/#.*/g,"").replace(/\/\/.*/g,""); settingsStr = settingsStr.replace(/\*([^*]|[\r\n]|(\*+([^*/]|[\r\n])))*\*+/gm,"").replace(/#.*/g,"").replace(/\/\/.*/g,"");

View File

@ -18,12 +18,12 @@ var async = require('async');
var Buffer = require('buffer').Buffer; var Buffer = require('buffer').Buffer;
var fs = require('fs'); var fs = require('fs');
var path = require('path'); var path = require('path');
var server = require('../server');
var zlib = require('zlib'); var zlib = require('zlib');
var util = require('util'); var util = require('util');
var settings = require('./Settings');
var ROOT_DIR = path.normalize(__dirname + "/../"); var CACHE_DIR = path.normalize(path.join(settings.root, 'var/'));
var CACHE_DIR = ROOT_DIR + '../var/'; CACHE_DIR = path.existsSync(CACHE_DIR) ? CACHE_DIR : undefined;
var responseCache = {}; var responseCache = {};
@ -37,7 +37,7 @@ function CachingMiddleware() {
} }
CachingMiddleware.prototype = new function () { CachingMiddleware.prototype = new function () {
function handle(req, res, next) { function handle(req, res, next) {
if (!(req.method == "GET" || req.method == "HEAD")) { if (!(req.method == "GET" || req.method == "HEAD") || !CACHE_DIR) {
return next(undefined, req, res); return next(undefined, req, res);
} }

View File

@ -0,0 +1,16 @@
/**
* Generates a random String with the given length. Is needed to generate the Author, Group, readonly, session Ids
*/
var randomString = function randomString(len)
{
var chars = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
var randomstring = '';
for (var i = 0; i < len; i++)
{
var rnum = Math.floor(Math.random() * chars.length);
randomstring += chars.substring(rnum, rnum + 1);
}
return randomstring;
};
module.exports = randomString;

View File

@ -1,11 +1,11 @@
{ {
"pad.js": [ "pad.js": [
"jquery.js" "jquery.js"
, "underscore.js"
, "security.js" , "security.js"
, "pad.js" , "pad.js"
, "ace2_common.js" , "ace2_common.js"
, "pad_utils.js" , "pad_utils.js"
, "plugins.js"
, "undo-xpopup.js" , "undo-xpopup.js"
, "json2.js" , "json2.js"
, "pad_cookie.js" , "pad_cookie.js"
@ -22,12 +22,11 @@
, "chat.js" , "chat.js"
, "excanvas.js" , "excanvas.js"
, "farbtastic.js" , "farbtastic.js"
, "prefixfree.js"
] ]
, "timeslider.js": [ , "timeslider.js": [
"jquery.js" "jquery.js"
, "underscore.js"
, "security.js" , "security.js"
, "plugins.js"
, "undo-xpopup.js" , "undo-xpopup.js"
, "json2.js" , "json2.js"
, "colorutils.js" , "colorutils.js"
@ -41,7 +40,7 @@
, "pad_modals.js" , "pad_modals.js"
, "pad_savedrevs.js" , "pad_savedrevs.js"
, "pad_impexp.js" , "pad_impexp.js"
, "AttributePoolFactory.js" , "AttributePool.js"
, "Changeset.js" , "Changeset.js"
, "domline.js" , "domline.js"
, "linestylefilter.js" , "linestylefilter.js"
@ -53,8 +52,11 @@
] ]
, "ace2_inner.js": [ , "ace2_inner.js": [
"ace2_common.js" "ace2_common.js"
, "AttributePoolFactory.js" , "underscore.js"
, "rjquery.js"
, "AttributePool.js"
, "Changeset.js" , "Changeset.js"
, "ChangesetUtils.js"
, "security.js" , "security.js"
, "skiplist.js" , "skiplist.js"
, "virtual_lines.js" , "virtual_lines.js"
@ -65,6 +67,7 @@
, "changesettracker.js" , "changesettracker.js"
, "linestylefilter.js" , "linestylefilter.js"
, "domline.js" , "domline.js"
, "AttributeManager.js"
, "ace2_inner.js" , "ace2_inner.js"
] ]
} }

View File

@ -1,5 +1,5 @@
{ {
"name" : "etherpad-lite", "name" : "ep_etherpad-lite",
"description" : "A Etherpad based on node.js", "description" : "A Etherpad based on node.js",
"homepage" : "https://github.com/Pita/etherpad-lite", "homepage" : "https://github.com/Pita/etherpad-lite",
"keywords" : ["etherpad", "realtime", "collaborative", "editor"], "keywords" : ["etherpad", "realtime", "collaborative", "editor"],
@ -12,7 +12,7 @@
"dependencies" : { "dependencies" : {
"yajsml" : "1.1.2", "yajsml" : "1.1.2",
"request" : "2.9.100", "request" : "2.9.100",
"require-kernel" : "1.0.3", "require-kernel" : "1.0.5",
"socket.io" : "0.8.7", "socket.io" : "0.8.7",
"ueberDB" : "0.1.7", "ueberDB" : "0.1.7",
"async" : "0.1.18", "async" : "0.1.18",
@ -22,7 +22,13 @@
"formidable" : "1.0.9", "formidable" : "1.0.9",
"log4js" : "0.4.1", "log4js" : "0.4.1",
"jsdom-nocontextifiy" : "0.2.10", "jsdom-nocontextifiy" : "0.2.10",
"async-stacktrace" : "0.0.2" "async-stacktrace" : "0.0.2",
"npm" : "1.1",
"ejs" : "0.6.1",
"graceful-fs" : "1.1.5",
"slide" : "1.1.3",
"semver" : "1.0.13",
"underscore" : "1.3.1"
}, },
"devDependencies": { "devDependencies": {
"jshint" : "*" "jshint" : "*"

49
src/static/css/admin.css Normal file
View File

@ -0,0 +1,49 @@
table {
border-collapse: collapse;
}
td, th {
border: 1px solid black;
padding-left: 10px;
padding-right: 10px;
padding-top: 2px;
padding-bottom: 2px;
}
.template {
display: none;
}
.dialog {
display: none;
position: absolute;
left: 50%;
top: 50%;
width: 700px;
height: 500px;
margin-left: -350px;
margin-top: -250px;
border: 3px solid #999999;
background: #eeeeee;
}
.dialog .title {
margin: 0;
padding: 2px;
border-bottom: 3px solid #999999;
font-size: 24px;
line-height: 24px;
height: 24px;
overflow: hidden;
}
.dialog .title .close {
float: right;
}
.dialog .history {
background: #222222;
color: #eeeeee;
position: absolute;
top: 41px;
bottom: 10px;
left: 10px;
right: 10px;
padding: 2px;
overflow: auto;
}

View File

@ -24,7 +24,7 @@ a img
} }
/* menu */ /* menu */
#editbar ul .toolbar ul
{ {
position: relative; position: relative;
list-style: none; list-style: none;
@ -35,18 +35,19 @@ a img
} }
#editbar .toolbar
{ {
background: #f7f7f7; background: #f7f7f7;
background: linear-gradient(#f7f7f7, #f1f1f1 80%); background: linear-gradient(#f7f7f7, #f1f1f1 80%);
border-bottom: 1px solid #ccc; border-bottom: 1px solid #ccc;
height: 32px;
overflow: hidden; overflow: hidden;
padding-top: 3px; padding-top: 3px;
width: 100%; width: 100%;
white-space: nowrap;
height: 32px;
} }
#editbar ul li .toolbar ul li
{ {
background: #fff; background: #fff;
background: linear-gradient(#fff, #f0f0f0); background: linear-gradient(#fff, #f0f0f0);
@ -61,52 +62,52 @@ a img
width: 18px; width: 18px;
} }
#editbar ul li a .toolbar ul li a
{ {
text-decoration: none; text-decoration: none;
color: #ccc; color: #ccc;
position: absolute; position: absolute;
} }
#editbar ul li a span .toolbar ul li a span
{ {
position: relative; position: relative;
top:-2px top:-2px
} }
#editbar ul li:hover { .toolbar ul li:hover {
background: #fff; background: #fff;
background: linear-gradient(#f4f4f4, #e4e4e4); background: linear-gradient(#f4f4f4, #e4e4e4);
} }
#editbar ul li:active { .toolbar ul li:active {
background: #eee; background: #eee;
background: linear-gradient(#ddd, #fff); background: linear-gradient(#ddd, #fff);
box-shadow: 0 0 8px rgba(0,0,0,.1) inset; box-shadow: 0 0 8px rgba(0,0,0,.1) inset;
} }
#editbar ul li.separator .toolbar ul li.separator
{ {
border: inherit; border: inherit;
background: inherit; background: inherit;
visibility:hidden; visibility:hidden;
width: 0px; width: 0px;
} }
#editbar ul li a .toolbar ul li a
{ {
display: block; display: block;
} }
#editbar ul li a img .toolbar ul li a img
{ {
padding: 1px; padding: 1px;
} }
#editbar ul .toolbar ul
{ {
float: left; float: left;
} }
#editbar ul#menu_right .toolbar ul.menu_right
{ {
float: right; float: right;
} }
@ -175,7 +176,6 @@ a#backtoprosite { padding-left: 20px; left: 6px;
background: url(static/img/protop.gif) no-repeat -5px -6px; } background: url(static/img/protop.gif) no-repeat -5px -6px; }
#accountnav { right: 30px; color: #fff; } #accountnav { right: 30px; color: #fff; }
.propad a#topbaretherpad { background: url(static/img/protop.gif) no-repeat -397px -3px; }
#specialkeyarea { top: 5px; left: 250px; color: yellow; font-weight: bold; #specialkeyarea { top: 5px; left: 250px; color: yellow; font-weight: bold;
font-size: 1.5em; position: absolute; } font-size: 1.5em; position: absolute; }
@ -320,7 +320,7 @@ a#hidetopmsg { position: absolute; right: 5px; bottom: 5px; }
z-index: 10; z-index: 10;
} }
#editbarsavetable .toolbarsavetable
{ {
position:absolute; position:absolute;
top: 6px; top: 6px;
@ -328,7 +328,7 @@ a#hidetopmsg { position: absolute; right: 5px; bottom: 5px; }
height: 24px; height: 24px;
} }
#editbarsavetable td, #editbartable td .toolbarsavetable td, .toolbartable td
{ {
white-space: nowrap; white-space: nowrap;
} }
@ -604,8 +604,6 @@ table#otheruserstable { display: none; }
text-align: left; text-align: left;
} }
.nonprouser #sharebox-stripe { display: none; }
.sharebox-url { .sharebox-url {
width: 440px; height: 18px; width: 440px; height: 18px;
text-align: left; text-align: left;
@ -688,14 +686,15 @@ a#topbarmaximize {
background: url(static/img/maximize_maximized.png); background: url(static/img/maximize_maximized.png);
} }
#editbarinner h1 { .toolbarinner h1 {
line-height: 29px; line-height: 29px;
font-size: 16px; font-size: 16px;
padding-left: 6pt; padding-left: 6pt;
margin-top: 0; margin-top: 0;
white-space: nowrap;
} }
#editbarinner h1 a { .toolbarinner h1 a {
font-size: 12px; font-size: 12px;
} }
@ -1034,6 +1033,9 @@ margin-top: 1px;
background-position: 0px -183px; background-position: 0px -183px;
display: inline-block; display: inline-block;
} }
.buttonicon-savedRevision {
background-position: 0px -493px
}
#usericon #usericon
{ {
@ -1173,13 +1175,13 @@ input[type=checkbox] {
} }
@media screen and (max-width: 600px) { @media screen and (max-width: 600px) {
#editbar ul li { .toolbar ul li {
padding: 4px 1px; padding: 4px 1px;
} }
} }
@media only screen and (min-device-width: 320px) and (max-device-width: 720px) { @media only screen and (min-device-width: 320px) and (max-device-width: 720px) {
#editbar ul li { .toolbar ul li {
padding: 4px 3px; padding: 4px 3px;
} }
#users { #users {
@ -1194,7 +1196,7 @@ input[type=checkbox] {
#editorcontainer { #editorcontainer {
margin-bottom: 33px; margin-bottom: 33px;
} }
#editbar ul#menu_right { .toolbar ul.menu_right {
background: #f7f7f7; background: #f7f7f7;
background: linear-gradient(#f7f7f7, #f1f1f1 80%); background: linear-gradient(#f7f7f7, #f1f1f1 80%);
width: 100%; width: 100%;
@ -1204,7 +1206,7 @@ input[type=checkbox] {
bottom: 0; bottom: 0;
border-top: 1px solid #ccc; border-top: 1px solid #ccc;
} }
#editbar ul#menu_right li:last-child { .toolbar ul.menu_right li:last-child {
height: 24px; height: 24px;
border-radius: 0; border-radius: 0;
margin-top: 0; margin-top: 0;
@ -1226,7 +1228,7 @@ input[type=checkbox] {
border-top-right-radius: 0; border-top-right-radius: 0;
border-right: none; border-right: none;
} }
#editbar ul li a span { .toolbar ul li a span {
top: -3px; top: -3px;
} }
#usericonback { #usericonback {
@ -1235,10 +1237,10 @@ input[type=checkbox] {
#qrcode { #qrcode {
display: none; display: none;
} }
#editbar ul#menu_right li:not(:last-child) { .toolbar ul.menu_right li:not(:last-child) {
display: block; display: block;
} }
#editbar ul#menu_right > li { .toolbar ul.menu_right > li {
background: none; background: none;
border: none; border: none;
margin-top: 4px; margin-top: 4px;

View File

@ -1,4 +1,7 @@
#editorcontainerbox {overflow:auto; top:40px;} #editorcontainerbox {
overflow:auto; top:40px;
position: static;
}
#padcontent {font-size:12px; padding:10px;} #padcontent {font-size:12px; padding:10px;}
@ -42,10 +45,10 @@
#leftstar, #rightstar, #leftstep, #rightstep #leftstar, #rightstar, #leftstep, #rightstep
{background:url(../../static/img/stepper_buttons.png) 0 0 no-repeat; height:21px; overflow:hidden; position:absolute;} {background:url(../../static/img/stepper_buttons.png) 0 0 no-repeat; height:21px; overflow:hidden; position:absolute;}
#leftstar {background-position:0 44px; right:34px; top:8px; width:30px;} #leftstar {background-position:0 -44px; right:34px; top:8px; width:30px;}
#rightstar {background-position:29px 44px; right:5px; top:8px; width:29px;} #rightstar {background-position:-29px -44px; right:5px; top:8px; width:29px;}
#leftstep {background-position:0 22px; right:34px; top:20px; width:30px;} #leftstep {background-position:0 -22px; right:34px; top:20px; width:30px;}
#rightstep {background-position:29px 22px; right:5px; top:20px; width:29px;} #rightstep {background-position:-29px -22px; right:5px; top:20px; width:29px;}
#timeslider .star { #timeslider .star {
background-image:url(../../static/img/star.png); background-image:url(../../static/img/star.png);
@ -67,12 +70,63 @@
width:122px; width:122px;
} }
.topbarcenter, #docbar {display:none;} .topbarcenter, #docbar {display:none;}
#padmain {top:30px;} #padmain {top:0px !important;}
#editbarright {float:right;} #editbarright {float:right;}
#returnbutton {color:#222; font-size:16px; line-height:29px; margin-top:0; padding-right:6px;} #returnbutton {color:#222; font-size:16px; line-height:29px; margin-top:0; padding-right:6px;}
#importexport {top:118px;}
#importexport .popup {width:185px;} #importexport .popup {width:185px;}
#importexport{
top:118px;
width:185px;
}
.timeslider-bar
{
background: #f7f7f7;
background: linear-gradient(#f7f7f7, #f1f1f1 80%);
border-bottom: 1px solid #ccc;
overflow: hidden;
padding-top: 3px;
width: 100%;
}
.timeslider-bar #editbar
{
border-bottom: none;
float: right;
width: 170px;
width: initial;
}
.timeslider-bar h1
{
margin: 5px;
}
.timeslider-bar p
{
margin: 5px;
}
#timeslider-top {
width: 100%;
position: fixed;
z-index: 1;
}
#authorsList .author {
padding-left: 0.4em;
padding-right: 0.4em;
}
#authorsList .author-anonymous {
padding-left: 0.6em;
padding-right: 0.6em;
}
#padeditor {
position: static;
}
/* lists */ /* lists */
.list-bullet2, .list-indent2, .list-number2 {margin-left:3em;} .list-bullet2, .list-indent2, .list-number2 {margin-left:3em;}

3
src/static/custom/.gitignore vendored Normal file
View File

@ -0,0 +1,3 @@
*
!.gitignore
!*.template

View File

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

Before

Width:  |  Height:  |  Size: 697 B

After

Width:  |  Height:  |  Size: 697 B

View File

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 11 KiB

View File

Before

Width:  |  Height:  |  Size: 4.0 KiB

After

Width:  |  Height:  |  Size: 4.0 KiB

View File

Before

Width:  |  Height:  |  Size: 4.1 KiB

After

Width:  |  Height:  |  Size: 4.1 KiB

View File

Before

Width:  |  Height:  |  Size: 1009 B

After

Width:  |  Height:  |  Size: 1009 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.1 KiB

View File

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

Before

Width:  |  Height:  |  Size: 494 B

After

Width:  |  Height:  |  Size: 494 B

View File

Before

Width:  |  Height:  |  Size: 658 B

After

Width:  |  Height:  |  Size: 658 B

View File

Before

Width:  |  Height:  |  Size: 2.8 KiB

After

Width:  |  Height:  |  Size: 2.8 KiB

View File

Before

Width:  |  Height:  |  Size: 2.9 KiB

After

Width:  |  Height:  |  Size: 2.9 KiB

View File

Before

Width:  |  Height:  |  Size: 123 B

After

Width:  |  Height:  |  Size: 123 B

View File

Before

Width:  |  Height:  |  Size: 131 B

After

Width:  |  Height:  |  Size: 131 B

BIN
src/static/img/star.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

View File

Before

Width:  |  Height:  |  Size: 4.7 KiB

After

Width:  |  Height:  |  Size: 4.7 KiB

View File

Before

Width:  |  Height:  |  Size: 182 B

After

Width:  |  Height:  |  Size: 182 B

View File

Before

Width:  |  Height:  |  Size: 686 B

After

Width:  |  Height:  |  Size: 686 B

View File

Before

Width:  |  Height:  |  Size: 517 B

After

Width:  |  Height:  |  Size: 517 B

View File

@ -0,0 +1,164 @@
var Changeset = require('./Changeset');
var ChangesetUtils = require('./ChangesetUtils');
var _ = require('./underscore');
var lineMarkerAttribute = 'lmkr';
// If one of these attributes are set to the first character of a
// line it is considered as a line attribute marker i.e. attributes
// set on this marker are applied to the whole line.
// The list attribute is only maintained for compatibility reasons
var lineAttributes = [lineMarkerAttribute,'list'];
/*
The Attribute manager builds changesets based on a document
representation for setting and removing range or line-based attributes.
@param rep the document representation to be used
@param applyChangesetCallback this callback will be called
once a changeset has been built.
A document representation contains
- an array `alines` containing 1 attributes string for each line
- an Attribute pool `apool`
- a SkipList `lines` containing the text lines of the document.
*/
var AttributeManager = function(rep, applyChangesetCallback)
{
this.rep = rep;
this.applyChangesetCallback = applyChangesetCallback;
this.author = '';
// If the first char in a line has one of the following attributes
// it will be considered as a line marker
};
AttributeManager.lineAttributes = lineAttributes;
AttributeManager.prototype = _(AttributeManager.prototype).extend({
applyChangeset: function(changeset){
if(!this.applyChangesetCallback) return changeset;
var cs = changeset.toString();
if (!Changeset.isIdentity(cs))
{
this.applyChangesetCallback(cs);
}
return changeset;
},
/*
Sets attributes on a range
@param start [row, col] tuple pointing to the start of the range
@param end [row, col] tuple pointing to the end of the range
@param attribute: an array of attributes
*/
setAttributesOnRange: function(start, end, attribs)
{
var builder = Changeset.builder(this.rep.lines.totalWidth());
ChangesetUtils.buildKeepToStartOfRange(this.rep, builder, start);
ChangesetUtils.buildKeepRange(this.rep, builder, start, end, attribs, this.rep.apool);
return this.applyChangeset(builder);
},
/*
Returns if the line already has a line marker
@param lineNum: the number of the line
*/
lineHasMarker: function(lineNum){
var that = this;
return _.find(lineAttributes, function(attribute){
return that.getAttributeOnLine(lineNum, attribute) != '';
}) !== undefined;
},
/*
Gets a specified attribute on a line
@param lineNum: the number of the line to set the attribute for
@param attributeKey: the name of the attribute to get, e.g. list
*/
getAttributeOnLine: function(lineNum, attributeName){
// get `attributeName` attribute of first char of line
var aline = this.rep.alines[lineNum];
if (aline)
{
var opIter = Changeset.opIterator(aline);
if (opIter.hasNext())
{
return Changeset.opAttributeValue(opIter.next(), attributeName, this.rep.apool) || '';
}
}
return '';
},
/*
Sets a specified attribute on a line
@param lineNum: the number of the line to set the attribute for
@param attributeKey: the name of the attribute to set, e.g. list
@param attributeValue: an optional parameter to pass to the attribute (e.g. indention level)
*/
setAttributeOnLine: function(lineNum, attributeName, attributeValue){
var loc = [0,0];
var builder = Changeset.builder(this.rep.lines.totalWidth());
var hasMarker = this.lineHasMarker(lineNum);
ChangesetUtils.buildKeepRange(this.rep, builder, loc, (loc = [lineNum, 0]));
if(hasMarker){
ChangesetUtils.buildKeepRange(this.rep, builder, loc, (loc = [lineNum, 1]), [
[attributeName, attributeValue]
], this.rep.apool);
}else{
// add a line marker
builder.insert('*', [
['author', this.author],
['insertorder', 'first'],
[lineMarkerAttribute, '1'],
[attributeName, attributeValue]
], this.rep.apool);
}
return this.applyChangeset(builder);
},
/*
Removes a specified attribute on a line
@param lineNum: the number of the affected line
@param attributeKey: the name of the attribute to remove, e.g. list
*/
removeAttributeOnLine: function(lineNum, attributeName, attributeValue){
var loc = [0,0];
var builder = Changeset.builder(this.rep.lines.totalWidth());
var hasMarker = this.lineHasMarker(lineNum);
if(hasMarker){
ChangesetUtils.buildKeepRange(this.rep, builder, loc, (loc = [lineNum, 0]));
ChangesetUtils.buildRemoveRange(this.rep, builder, loc, (loc = [lineNum, 1]));
}
return this.applyChangeset(builder);
},
/*
Sets a specified attribute on a line
@param lineNum: the number of the line to set the attribute for
@param attributeKey: the name of the attribute to set, e.g. list
@param attributeValue: an optional parameter to pass to the attribute (e.g. indention level)
*/
toggleAttributeOnLine: function(lineNum, attributeName, attributeValue) {
return this.getAttributeOnLine(attributeName) ?
this.removeAttributeOnLine(lineNum, attributeName) :
this.setAttributeOnLine(lineNum, attributeName, attributeValue);
}
});
module.exports = AttributeManager;

View File

@ -0,0 +1,96 @@
/**
* This code represents the Attribute Pool Object of the original Etherpad.
* 90% of the code is still like in the original Etherpad
* Look at https://github.com/ether/pad/blob/master/infrastructure/ace/www/easysync2.js
* You can find a explanation what a attribute pool is here:
* https://github.com/Pita/etherpad-lite/blob/master/doc/easysync/easysync-notes.txt
*/
/*
* Copyright 2009 Google Inc., 2011 Peter 'Pita' Martischka (Primary Technology Ltd)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS-IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/*
An AttributePool maintains a mapping from [key,value] Pairs called
Attributes to Numbers (unsigened integers) and vice versa. These numbers are
used to reference Attributes in Changesets.
*/
var AttributePool = function () {
this.numToAttrib = {}; // e.g. {0: ['foo','bar']}
this.attribToNum = {}; // e.g. {'foo,bar': 0}
this.nextNum = 0;
};
AttributePool.prototype.putAttrib = function (attrib, dontAddIfAbsent) {
var str = String(attrib);
if (str in this.attribToNum) {
return this.attribToNum[str];
}
if (dontAddIfAbsent) {
return -1;
}
var num = this.nextNum++;
this.attribToNum[str] = num;
this.numToAttrib[num] = [String(attrib[0] || ''), String(attrib[1] || '')];
return num;
};
AttributePool.prototype.getAttrib = function (num) {
var pair = this.numToAttrib[num];
if (!pair) {
return pair;
}
return [pair[0], pair[1]]; // return a mutable copy
};
AttributePool.prototype.getAttribKey = function (num) {
var pair = this.numToAttrib[num];
if (!pair) return '';
return pair[0];
};
AttributePool.prototype.getAttribValue = function (num) {
var pair = this.numToAttrib[num];
if (!pair) return '';
return pair[1];
};
AttributePool.prototype.eachAttrib = function (func) {
for (var n in this.numToAttrib) {
var pair = this.numToAttrib[n];
func(pair[0], pair[1]);
}
};
AttributePool.prototype.toJsonable = function () {
return {
numToAttrib: this.numToAttrib,
nextNum: this.nextNum
};
};
AttributePool.prototype.fromJsonable = function (obj) {
this.numToAttrib = obj.numToAttrib;
this.nextNum = obj.nextNum;
this.attribToNum = {};
for (var n in this.numToAttrib) {
this.attribToNum[String(this.numToAttrib[n])] = Number(n);
}
return this;
};
module.exports = AttributePool;

View File

@ -25,7 +25,7 @@
* limitations under the License. * limitations under the License.
*/ */
var AttributePoolFactory = require("/AttributePoolFactory"); var AttributePool = require("./AttributePool");
var _opt = null; var _opt = null;
@ -1731,7 +1731,7 @@ exports.appendATextToAssembler = function (atext, assem) {
* @param pool {AtributePool} * @param pool {AtributePool}
*/ */
exports.prepareForWire = function (cs, pool) { exports.prepareForWire = function (cs, pool) {
var newPool = AttributePoolFactory.createAttributePool();; var newPool = new AttributePool();
var newCs = exports.moveOpsToNewPool(cs, pool, newPool); var newCs = exports.moveOpsToNewPool(cs, pool, newPool);
return { return {
translated: newCs, translated: newCs,

View File

@ -0,0 +1,60 @@
/**
* This module contains several helper Functions to build Changesets
* based on a SkipList
*/
/**
* Copyright 2009 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS-IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
exports.buildRemoveRange = function(rep, builder, start, end)
{
var startLineOffset = rep.lines.offsetOfIndex(start[0]);
var endLineOffset = rep.lines.offsetOfIndex(end[0]);
if (end[0] > start[0])
{
builder.remove(endLineOffset - startLineOffset - start[1], end[0] - start[0]);
builder.remove(end[1]);
}
else
{
builder.remove(end[1] - start[1]);
}
}
exports.buildKeepRange = function(rep, builder, start, end, attribs, pool)
{
var startLineOffset = rep.lines.offsetOfIndex(start[0]);
var endLineOffset = rep.lines.offsetOfIndex(end[0]);
if (end[0] > start[0])
{
builder.keep(endLineOffset - startLineOffset - start[1], end[0] - start[0], attribs, pool);
builder.keep(end[1], 0, attribs, pool);
}
else
{
builder.keep(end[1] - start[1], 0, attribs, pool);
}
}
exports.buildKeepToStartOfRange = function(rep, builder, start)
{
var startLineOffset = rep.lines.offsetOfIndex(start[0]);
builder.keep(startLineOffset, start[0]);
builder.keep(start[1]);
}

View File

@ -28,7 +28,8 @@ Ace2Editor.registry = {
nextId: 1 nextId: 1
}; };
var plugins = require('/plugins').plugins; var hooks = require('./pluginfw/hooks');
var _ = require('./underscore');
function Ace2Editor() function Ace2Editor()
{ {
@ -70,7 +71,7 @@ function Ace2Editor()
function doActionsPendingInit() function doActionsPendingInit()
{ {
$.each(actionsPendingInit, function(i,fn){ _.each(actionsPendingInit, function(fn,i){
fn() fn()
}); });
actionsPendingInit = []; actionsPendingInit = [];
@ -87,7 +88,7 @@ function Ace2Editor()
'setUserChangeNotificationCallback', 'setAuthorInfo', 'setUserChangeNotificationCallback', 'setAuthorInfo',
'setAuthorSelectionRange', 'callWithAce', 'execCommand', 'replaceRange']; 'setAuthorSelectionRange', 'callWithAce', 'execCommand', 'replaceRange'];
$.each(aceFunctionsPendingInit, function(i,fnName){ _.each(aceFunctionsPendingInit, function(fnName,i){
var prefix = 'ace_'; var prefix = 'ace_';
var name = prefix + fnName; var name = prefix + fnName;
editor[fnName] = pendingInit(function(){ editor[fnName] = pendingInit(function(){
@ -156,7 +157,11 @@ function Ace2Editor()
} }
function pushRequireScriptTo(buffer) { function pushRequireScriptTo(buffer) {
var KERNEL_SOURCE = '../static/js/require-kernel.js'; var KERNEL_SOURCE = '../static/js/require-kernel.js';
var KERNEL_BOOT = 'require.setRootURI("../minified/");\nrequire.setGlobalKeyPath("require");' var KERNEL_BOOT = '\
require.setRootURI("../javascripts/src");\n\
require.setLibraryURI("../javascripts/lib");\n\
require.setGlobalKeyPath("require");\n\
';
if (Ace2Editor.EMBEDED && Ace2Editor.EMBEDED[KERNEL_SOURCE]) { if (Ace2Editor.EMBEDED && Ace2Editor.EMBEDED[KERNEL_SOURCE]) {
buffer.push('<script type="text/javascript">'); buffer.push('<script type="text/javascript">');
buffer.push(Ace2Editor.EMBEDED[KERNEL_SOURCE]); buffer.push(Ace2Editor.EMBEDED[KERNEL_SOURCE]);
@ -166,17 +171,18 @@ function Ace2Editor()
} }
function pushScriptsTo(buffer) { function pushScriptsTo(buffer) {
/* Folling is for packaging regular expression. */ /* Folling is for packaging regular expression. */
/* $$INCLUDE_JS("../minified/ace2_inner.js?callback=require.define"); */ /* $$INCLUDE_JS("../javascripts/src/ace2_inner.js?callback=require.define"); */
var ACE_SOURCE = '../minified/ace2_inner.js?callback=require.define'; var ACE_SOURCE = '../javascripts/src/ace2_inner.js?callback=require.define';
if (Ace2Editor.EMBEDED && Ace2Editor.EMBEDED[ACE_SOURCE]) { if (Ace2Editor.EMBEDED && Ace2Editor.EMBEDED[ACE_SOURCE]) {
buffer.push('<script type="text/javascript">'); buffer.push('<script type="text/javascript">');
buffer.push(Ace2Editor.EMBEDED[ACE_SOURCE]); buffer.push(Ace2Editor.EMBEDED[ACE_SOURCE]);
buffer.push('require("/ace2_inner");'); buffer.push('require("ep_etherpad-lite/static/js/ace2_inner");');
buffer.push('<\/script>'); buffer.push('<\/script>');
} else { } else {
file = ACE_SOURCE;
buffer.push('<script type="application/javascript" src="' + ACE_SOURCE + '"><\/script>'); buffer.push('<script type="application/javascript" src="' + ACE_SOURCE + '"><\/script>');
buffer.push('<script type="text/javascript">'); buffer.push('<script type="text/javascript">');
buffer.push('require("/ace2_inner");'); buffer.push('require("ep_etherpad-lite/static/js/ace2_inner");');
buffer.push('<\/script>'); buffer.push('<\/script>');
} }
} }
@ -227,14 +233,16 @@ function Ace2Editor()
iframeHTML.push(doctype); iframeHTML.push(doctype);
iframeHTML.push("<html><head>"); iframeHTML.push("<html><head>");
iframeHTML.push('<script type="text/javascript" src="../static/js/jquery.js"></script>');
hooks.callAll("aceInitInnerdocbodyHead", {
iframeHTML: iframeHTML
});
// For compatability's sake transform in and out. // For compatability's sake transform in and out.
for (var i = 0, ii = iframeHTML.length; i < ii; i++) { for (var i = 0, ii = iframeHTML.length; i < ii; i++) {
iframeHTML[i] = JSON.stringify(iframeHTML[i]); iframeHTML[i] = JSON.stringify(iframeHTML[i]);
} }
plugins.callHook("aceInitInnerdocbodyHead", {
iframeHTML: iframeHTML
});
for (var i = 0, ii = iframeHTML.length; i < ii; i++) { for (var i = 0, ii = iframeHTML.length; i < ii; i++) {
iframeHTML[i] = JSON.parse(iframeHTML[i]); iframeHTML[i] = JSON.parse(iframeHTML[i]);
} }
@ -248,6 +256,10 @@ function Ace2Editor()
$$INCLUDE_CSS("../static/css/iframe_editor.css"); $$INCLUDE_CSS("../static/css/iframe_editor.css");
$$INCLUDE_CSS("../static/css/pad.css"); $$INCLUDE_CSS("../static/css/pad.css");
$$INCLUDE_CSS("../static/custom/pad.css"); $$INCLUDE_CSS("../static/custom/pad.css");
var additionalCSS = _(hooks.callAll("aceEditorCSS")).map(function(path){ return '../static/plugins/' + path });
includedCSS = includedCSS.concat(additionalCSS);
pushStyleTagsFor(iframeHTML, includedCSS); pushStyleTagsFor(iframeHTML, includedCSS);
var includedJS = []; var includedJS = [];
@ -256,9 +268,14 @@ function Ace2Editor()
// Inject my plugins into my child. // Inject my plugins into my child.
iframeHTML.push('\ iframeHTML.push('\
<script type="text/javascript">\ <script type="text/javascript">\
parent_req = require("./pluginfw/parent_require.js");\
parent_req.getRequirementFromParent(require, "ep_etherpad-lite/static/js/pluginfw/hooks");\
parent_req.getRequirementFromParent(require, "ep_etherpad-lite/static/js/pluginfw/plugins");\
parent_req.getRequirementFromParent(require, "./pluginfw/hooks");\
parent_req.getRequirementFromParent(require, "./pluginfw/plugins");\
require.define("/plugins", null);\n\ require.define("/plugins", null);\n\
require.define("/plugins.js", function (require, exports, module) {\ require.define("/plugins.js", function (require, exports, module) {\
module.exports = parent.parent.require("/plugins");\ module.exports = require("ep_etherpad-lite/static/js/plugins");\
});\ });\
</script>\ </script>\
'); ');
@ -271,7 +288,7 @@ function Ace2Editor()
var thisFunctionsName = "ChildAccessibleAce2Editor"; var thisFunctionsName = "ChildAccessibleAce2Editor";
(function () {return this}())[thisFunctionsName] = Ace2Editor; (function () {return this}())[thisFunctionsName] = Ace2Editor;
var outerScript = 'editorId = "' + info.id + '"; editorInfo = parent.' + thisFunctionsName + '.registry[editorId]; ' + 'window.onload = function() ' + '{ window.onload = null; setTimeout' + '(function() ' + '{ var iframe = document.createElement("IFRAME"); ' + 'iframe.scrolling = "no"; var outerdocbody = document.getElementById("outerdocbody"); ' + 'iframe.frameBorder = 0; iframe.allowTransparency = true; ' + // for IE var outerScript = 'editorId = "' + info.id + '"; editorInfo = parent.' + thisFunctionsName + '.registry[editorId]; ' + 'window.onload = function() ' + '{ window.onload = null; setTimeout' + '(function() ' + '{ var iframe = document.createElement("IFRAME"); iframe.name = "ace_inner";' + 'iframe.scrolling = "no"; var outerdocbody = document.getElementById("outerdocbody"); ' + 'iframe.frameBorder = 0; iframe.allowTransparency = true; ' + // for IE
'outerdocbody.insertBefore(iframe, outerdocbody.firstChild); ' + 'iframe.ace_outerWin = window; ' + 'readyFunc = function() { editorInfo.onEditorReady(); readyFunc = null; editorInfo = null; }; ' + 'var doc = iframe.contentWindow.document; doc.open(); var text = (' + JSON.stringify(iframeHTML.join('\n')) + ');doc.write(text); doc.close(); ' + '}, 0); }'; 'outerdocbody.insertBefore(iframe, outerdocbody.firstChild); ' + 'iframe.ace_outerWin = window; ' + 'readyFunc = function() { editorInfo.onEditorReady(); readyFunc = null; editorInfo = null; }; ' + 'var doc = iframe.contentWindow.document; doc.open(); var text = (' + JSON.stringify(iframeHTML.join('\n')) + ');doc.write(text); doc.close(); ' + '}, 0); }';
var outerHTML = [doctype, '<html><head>'] var outerHTML = [doctype, '<html><head>']
@ -281,6 +298,11 @@ function Ace2Editor()
$$INCLUDE_CSS("../static/css/iframe_editor.css"); $$INCLUDE_CSS("../static/css/iframe_editor.css");
$$INCLUDE_CSS("../static/css/pad.css"); $$INCLUDE_CSS("../static/css/pad.css");
$$INCLUDE_CSS("../static/custom/pad.css"); $$INCLUDE_CSS("../static/custom/pad.css");
var additionalCSS = _(hooks.callAll("aceEditorCSS")).map(function(path){ return '../static/plugins/' + path });
includedCSS = includedCSS.concat(additionalCSS);
pushStyleTagsFor(outerHTML, includedCSS); pushStyleTagsFor(outerHTML, includedCSS);
// bizarrely, in FF2, a file with no "external" dependencies won't finish loading properly // bizarrely, in FF2, a file with no "external" dependencies won't finish loading properly
@ -288,6 +310,7 @@ function Ace2Editor()
outerHTML.push('<link rel="stylesheet" type="text/css" href="data:text/css,"/>', '\x3cscript>\n', outerScript.replace(/<\//g, '<\\/'), '\n\x3c/script>', '</head><body id="outerdocbody"><div id="sidediv"><!-- --></div><div id="linemetricsdiv">x</div><div id="overlaysdiv"><!-- --></div></body></html>'); outerHTML.push('<link rel="stylesheet" type="text/css" href="data:text/css,"/>', '\x3cscript>\n', outerScript.replace(/<\//g, '<\\/'), '\n\x3c/script>', '</head><body id="outerdocbody"><div id="sidediv"><!-- --></div><div id="linemetricsdiv">x</div><div id="overlaysdiv"><!-- --></div></body></html>');
var outerFrame = document.createElement("IFRAME"); var outerFrame = document.createElement("IFRAME");
outerFrame.name = "ace_outer";
outerFrame.frameBorder = 0; // for IE outerFrame.frameBorder = 0; // for IE
info.frame = outerFrame; info.frame = outerFrame;
document.getElementById(containerId).appendChild(outerFrame); document.getElementById(containerId).appendChild(outerFrame);

View File

@ -20,7 +20,7 @@
* limitations under the License. * limitations under the License.
*/ */
var Security = require('/security'); var Security = require('./security');
function isNodeText(node) function isNodeText(node)
{ {
@ -29,58 +29,10 @@ function isNodeText(node)
function object(o) function object(o)
{ {
var f = function() var f = function(){};
{};
f.prototype = o; f.prototype = o;
return new f(); return new f();
} }
function extend(obj, props)
{
for (var p in props)
{
obj[p] = props[p];
}
return obj;
}
function forEach(array, func)
{
for (var i = 0; i < array.length; i++)
{
var result = func(array[i], i);
if (result) break;
}
}
function map(array, func)
{
var result = [];
// must remain compatible with "arguments" pseudo-array
for (var i = 0; i < array.length; i++)
{
if (func) result.push(func(array[i], i));
else result.push(array[i]);
}
return result;
}
function filter(array, func)
{
var result = [];
// must remain compatible with "arguments" pseudo-array
for (var i = 0; i < array.length; i++)
{
if (func(array[i], i)) result.push(array[i]);
}
return result;
}
function isArray(testObject)
{
return testObject && typeof testObject === 'object' && !(testObject.propertyIsEnumerable('length')) && typeof testObject.length === 'number';
}
var userAgent = (((function () {return this;})().navigator || {}).userAgent || 'node-js').toLowerCase(); var userAgent = (((function () {return this;})().navigator || {}).userAgent || 'node-js').toLowerCase();
// Figure out what browser is being used (stolen from jquery 1.2.1) // Figure out what browser is being used (stolen from jquery 1.2.1)
@ -142,21 +94,13 @@ function htmlPrettyEscape(str)
} }
var noop = function(){}; var noop = function(){};
var identity = function(x){return x};
exports.isNodeText = isNodeText; exports.isNodeText = isNodeText;
exports.object = object; exports.object = object;
exports.extend = extend;
exports.forEach = forEach;
exports.map = map;
exports.filter = filter;
exports.isArray = isArray;
exports.browser = browser; exports.browser = browser;
exports.getAssoc = getAssoc; exports.getAssoc = getAssoc;
exports.setAssoc = setAssoc; exports.setAssoc = setAssoc;
exports.binarySearch = binarySearch; exports.binarySearch = binarySearch;
exports.binarySearchInfinite = binarySearchInfinite; exports.binarySearchInfinite = binarySearchInfinite;
exports.htmlPrettyEscape = htmlPrettyEscape; exports.htmlPrettyEscape = htmlPrettyEscape;
exports.map = map;
exports.noop = noop; exports.noop = noop;
exports.identity = identity;

View File

@ -19,39 +19,41 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
var editor, _, $, jQuery, plugins, Ace2Common;
var Ace2Common = require('/ace2_common'); Ace2Common = require('./ace2_common');
// Extract useful method defined in the other module. plugins = require('ep_etherpad-lite/static/js/pluginfw/plugins');
var isNodeText = Ace2Common.isNodeText; $ = jQuery = require('./rjquery').$;
var object = Ace2Common.object; _ = require("./underscore");
var extend = Ace2Common.extend;
var forEach = Ace2Common.forEach;
var map = Ace2Common.map;
var filter = Ace2Common.filter;
var isArray = Ace2Common.isArray;
var browser = Ace2Common.browser;
var getAssoc = Ace2Common.getAssoc;
var setAssoc = Ace2Common.setAssoc;
var binarySearchInfinite = Ace2Common.binarySearchInfinite;
var htmlPrettyEscape = Ace2Common.htmlPrettyEscape;
var map = Ace2Common.map;
var noop = Ace2Common.noop;
var makeChangesetTracker = require('/changesettracker').makeChangesetTracker; var isNodeText = Ace2Common.isNodeText,
var colorutils = require('/colorutils').colorutils; browser = Ace2Common.browser,
var makeContentCollector = require('/contentcollector').makeContentCollector; getAssoc = Ace2Common.getAssoc,
var makeCSSManager = require('/cssmanager').makeCSSManager; setAssoc = Ace2Common.setAssoc,
var domline = require('/domline').domline; isTextNode = Ace2Common.isTextNode,
var AttribPool = require('/AttributePoolFactory').createAttributePool; binarySearchInfinite = Ace2Common.binarySearchInfinite,
var Changeset = require('/Changeset'); htmlPrettyEscape = Ace2Common.htmlPrettyEscape,
var linestylefilter = require('/linestylefilter').linestylefilter; noop = Ace2Common.noop;
var newSkipList = require('/skiplist').newSkipList; var hooks = require('./pluginfw/hooks');
var undoModule = require('/undomodule').undoModule;
var makeVirtualLineView = require('/virtual_lines').makeVirtualLineView;
function Ace2Inner(){ function Ace2Inner(){
var makeChangesetTracker = require('./changesettracker').makeChangesetTracker;
var colorutils = require('./colorutils').colorutils;
var makeContentCollector = require('./contentcollector').makeContentCollector;
var makeCSSManager = require('./cssmanager').makeCSSManager;
var domline = require('./domline').domline;
var AttribPool = require('./AttributePool');
var Changeset = require('./Changeset');
var ChangesetUtils = require('./ChangesetUtils');
var linestylefilter = require('./linestylefilter').linestylefilter;
var SkipList = require('./skiplist');
var undoModule = require('./undomodule').undoModule;
var makeVirtualLineView = require('./virtual_lines').makeVirtualLineView;
var AttributeManager = require('./AttributeManager');
var DEBUG = false; //$$ build script replaces the string "var DEBUG=true;//$$" with "var DEBUG=false;" var DEBUG = false; //$$ build script replaces the string "var DEBUG=true;//$$" with "var DEBUG=false;"
// changed to false // changed to false
var isSetUp = false; var isSetUp = false;
@ -70,7 +72,6 @@ function Ace2Inner(){
var thisAuthor = ''; var thisAuthor = '';
var disposed = false; var disposed = false;
var editorInfo = parent.editorInfo; var editorInfo = parent.editorInfo;
var iframe = window.frameElement; var iframe = window.frameElement;
@ -81,21 +82,18 @@ function Ace2Inner(){
var overlaysdiv = lineMetricsDiv.nextSibling; var overlaysdiv = lineMetricsDiv.nextSibling;
initLineNumbers(); initLineNumbers();
var outsideKeyDown = function(evt) var outsideKeyDown = noop;
{};
var outsideKeyPress = function(evt) var outsideKeyPress = function(){return true;};
{
return true; var outsideNotifyDirty = noop;
};
var outsideNotifyDirty = function()
{};
// selFocusAtStart -- determines whether the selection extends "backwards", so that the focus // selFocusAtStart -- determines whether the selection extends "backwards", so that the focus
// point (controlled with the arrow keys) is at the beginning; not supported in IE, though // point (controlled with the arrow keys) is at the beginning; not supported in IE, though
// native IE selections have that behavior (which we try not to interfere with). // native IE selections have that behavior (which we try not to interfere with).
// Must be false if selection is collapsed! // Must be false if selection is collapsed!
var rep = { var rep = {
lines: newSkipList(), lines: new SkipList(),
selStart: null, selStart: null,
selEnd: null, selEnd: null,
selFocusAtStart: false, selFocusAtStart: false,
@ -103,6 +101,7 @@ function Ace2Inner(){
alines: [], alines: [],
apool: new AttribPool() apool: new AttribPool()
}; };
// lines, alltext, alines, and DOM are set up in setup() // lines, alltext, alines, and DOM are set up in setup()
if (undoModule.enabled) if (undoModule.enabled)
{ {
@ -122,6 +121,7 @@ function Ace2Inner(){
iframePadRight = 0; iframePadRight = 0;
var console = (DEBUG && window.console); var console = (DEBUG && window.console);
var documentAttributeManager;
if (!window.console) if (!window.console)
{ {
@ -159,6 +159,7 @@ function Ace2Inner(){
var textFace = 'monospace'; var textFace = 'monospace';
var textSize = 12; var textSize = 12;
function textLineHeight() function textLineHeight()
{ {
return Math.round(textSize * 4 / 3); return Math.round(textSize * 4 / 3);
@ -685,7 +686,7 @@ function Ace2Inner(){
} }
else else
{ {
lines = map(text.split('\n'), textify); lines = _.map(text.split('\n'), textify);
} }
var newText = "\n"; var newText = "\n";
if (lines.length > 0) if (lines.length > 0)
@ -847,7 +848,7 @@ function Ace2Inner(){
var cmdArgs = Array.prototype.slice.call(arguments, 1); var cmdArgs = Array.prototype.slice.call(arguments, 1);
if (CMDS[cmd]) if (CMDS[cmd])
{ {
inCallStack(cmd, function() inCallStackIfNecessary(cmd, function()
{ {
fastIncorp(9); fastIncorp(9);
CMDS[cmd].apply(CMDS, cmdArgs); CMDS[cmd].apply(CMDS, cmdArgs);
@ -857,7 +858,7 @@ function Ace2Inner(){
function replaceRange(start, end, text) function replaceRange(start, end, text)
{ {
inCallStack('replaceRange', function() inCallStackIfNecessary('replaceRange', function()
{ {
fastIncorp(9); fastIncorp(9);
performDocumentReplaceRange(start, end, text); performDocumentReplaceRange(start, end, text);
@ -885,8 +886,6 @@ function Ace2Inner(){
return fn(editorInfo); return fn(editorInfo);
}; };
if (normalize !== undefined) if (normalize !== undefined)
{ {
var wrapper1 = wrapper; var wrapper1 = wrapper;
@ -934,7 +933,10 @@ function Ace2Inner(){
}, },
grayedout: setClassPresenceNamed(outerWin.document.body, "grayedout"), grayedout: setClassPresenceNamed(outerWin.document.body, "grayedout"),
dmesg: function(){ dmesg = window.dmesg = value; }, dmesg: function(){ dmesg = window.dmesg = value; },
userauthor: function(value){ thisAuthor = String(value); }, userauthor: function(value){
thisAuthor = String(value);
documentAttributeManager.author = thisAuthor;
},
styled: setStyled, styled: setStyled,
textface: setTextFace, textface: setTextFace,
textsize: setTextSize, textsize: setTextSize,
@ -1160,7 +1162,7 @@ function Ace2Inner(){
return; return;
} }
inCallStack("idleWorkTimer", function() inCallStackIfNecessary("idleWorkTimer", function()
{ {
var isTimeUp = newTimeLimit(250); var isTimeUp = newTimeLimit(250);
@ -1623,8 +1625,7 @@ function Ace2Inner(){
} }
//var fragment = magicdom.wrapDom(document.createDocumentFragment()); //var fragment = magicdom.wrapDom(document.createDocumentFragment());
domInsertsNeeded.push([nodeToAddAfter, lineNodeInfos]); domInsertsNeeded.push([nodeToAddAfter, lineNodeInfos]);
forEach(dirtyNodes, function(n) _.each(dirtyNodes,function(n){
{
toDeleteAtEnd.push(n); toDeleteAtEnd.push(n);
}); });
var spliceHints = {}; var spliceHints = {};
@ -1646,7 +1647,7 @@ function Ace2Inner(){
// update the representation // update the representation
p.mark("splice"); p.mark("splice");
forEach(splicesToDo, function(splice) _.each(splicesToDo, function(splice)
{ {
doIncorpLineSplice(splice[0], splice[1], splice[2], splice[3], splice[4]); doIncorpLineSplice(splice[0], splice[1], splice[2], splice[3], splice[4]);
}); });
@ -1656,14 +1657,14 @@ function Ace2Inner(){
//var isTimeUp = newTimeLimit(100); //var isTimeUp = newTimeLimit(100);
// do DOM inserts // do DOM inserts
p.mark("insert"); p.mark("insert");
forEach(domInsertsNeeded, function(ins) _.each(domInsertsNeeded,function(ins)
{ {
insertDomLines(ins[0], ins[1], isTimeUp); insertDomLines(ins[0], ins[1], isTimeUp);
}); });
p.mark("del"); p.mark("del");
// delete old dom nodes // delete old dom nodes
forEach(toDeleteAtEnd, function(n) _.each(toDeleteAtEnd,function(n)
{ {
//var id = n.uniqueId(); //var id = n.uniqueId();
// parent of n may not be "root" in IE due to non-tree-shaped DOM (wtf) // parent of n may not be "root" in IE due to non-tree-shaped DOM (wtf)
@ -1773,7 +1774,7 @@ function Ace2Inner(){
var charEnd = rep.lines.offsetOfEntry(endEntry) + endEntry.width; var charEnd = rep.lines.offsetOfEntry(endEntry) + endEntry.width;
//rep.lexer.lexCharRange([charStart, charEnd], isTimeUp); //rep.lexer.lexCharRange([charStart, charEnd], isTimeUp);
forEach(infoStructs, function(info) _.each(infoStructs, function(info)
{ {
var p2 = PROFILER("insertLine", false); var p2 = PROFILER("insertLine", false);
var node = info.node; var node = info.node;
@ -1870,55 +1871,6 @@ function Ace2Inner(){
} }
} }
function setupMozillaCaretHack(lineNum)
{
// This is really ugly, but by god, it works!
// Fixes annoying Firefox caret artifact (observed in 2.0.0.12
// and unfixed in Firefox 2 as of now) where mutating the DOM
// and then moving the caret to the beginning of a line causes
// an image of the caret to be XORed at the top of the iframe.
// The previous solution involved remembering to set the selection
// later, in response to the next event in the queue, which was hugely
// annoying.
// This solution: add a space character (0x20) to the beginning of the line.
// After setting the selection, remove the space.
var lineNode = rep.lines.atIndex(lineNum).lineNode;
var fc = lineNode.firstChild;
while (isBlockElement(fc) && fc.firstChild)
{
fc = fc.firstChild;
}
var textNode;
if (isNodeText(fc))
{
fc.nodeValue = " " + fc.nodeValue;
textNode = fc;
}
else
{
textNode = doc.createTextNode(" ");
fc.parentNode.insertBefore(textNode, fc);
}
markNodeClean(lineNode);
return {
unhack: function()
{
if (textNode.nodeValue == " ")
{
textNode.parentNode.removeChild(textNode);
}
else
{
textNode.nodeValue = textNode.nodeValue.substring(1);
}
markNodeClean(lineNode);
}
};
}
function getPointForLineAndChar(lineAndChar) function getPointForLineAndChar(lineAndChar)
{ {
var line = lineAndChar[0]; var line = lineAndChar[0];
@ -2049,6 +2001,7 @@ function Ace2Inner(){
return [lineNum, col]; return [lineNum, col];
} }
} }
editorInfo.ace_getLineAndCharForPoint = getLineAndCharForPoint;
function createDomLineEntry(lineString) function createDomLineEntry(lineString)
{ {
@ -2084,10 +2037,8 @@ function Ace2Inner(){
var linesMutatee = { var linesMutatee = {
splice: function(start, numRemoved, newLinesVA) splice: function(start, numRemoved, newLinesVA)
{ {
domAndRepSplice(start, numRemoved, map(Array.prototype.slice.call(arguments, 2), function(s) var args = Array.prototype.slice.call(arguments, 2);
{ domAndRepSplice(start, numRemoved, _.map(args, function(s){ return s.slice(0, -1); }), null);
return s.slice(0, -1);
}), null);
}, },
get: function(i) get: function(i)
{ {
@ -2099,7 +2050,7 @@ function Ace2Inner(){
}, },
slice_notused: function(start, end) slice_notused: function(start, end)
{ {
return map(rep.lines.slice(start, end), function(e) return _.map(rep.lines.slice(start, end), function(e)
{ {
return e.text + '\n'; return e.text + '\n';
}); });
@ -2132,7 +2083,7 @@ function Ace2Inner(){
} }
} }
var lineEntries = map(newLineStrings, createDomLineEntry); var lineEntries = _.map(newLineStrings, createDomLineEntry);
doRepLineSplice(startLine, deleteCount, lineEntries); doRepLineSplice(startLine, deleteCount, lineEntries);
@ -2143,12 +2094,12 @@ function Ace2Inner(){
} }
else nodeToAddAfter = null; else nodeToAddAfter = null;
insertDomLines(nodeToAddAfter, map(lineEntries, function(entry) insertDomLines(nodeToAddAfter, _.map(lineEntries, function(entry)
{ {
return entry.domInfo; return entry.domInfo;
}), isTimeUp); }), isTimeUp);
forEach(keysToDelete, function(k) _.each(keysToDelete, function(k)
{ {
var n = doc.getElementById(k); var n = doc.getElementById(k);
n.parentNode.removeChild(n); n.parentNode.removeChild(n);
@ -2254,6 +2205,9 @@ function Ace2Inner(){
} }
/*
Converts the position of a char (index in String) into a [row, col] tuple
*/
function lineAndColumnFromChar(x) function lineAndColumnFromChar(x)
{ {
var lineEntry = rep.lines.atOffset(x); var lineEntry = rep.lines.atOffset(x);
@ -2308,8 +2262,8 @@ function Ace2Inner(){
// CCCC\n // CCCC\n
// end[0]: <CCC end[1] CCC>-------\n // end[0]: <CCC end[1] CCC>-------\n
var builder = Changeset.builder(rep.lines.totalWidth()); var builder = Changeset.builder(rep.lines.totalWidth());
buildKeepToStartOfRange(builder, start); ChangesetUtils.buildKeepToStartOfRange(rep, builder, start);
buildRemoveRange(builder, start, end); ChangesetUtils.buildRemoveRange(rep, builder, start, end);
builder.insert(newText, [ builder.insert(newText, [
['author', thisAuthor] ['author', thisAuthor]
], rep.apool); ], rep.apool);
@ -2320,68 +2274,17 @@ function Ace2Inner(){
function performDocumentApplyAttributesToCharRange(start, end, attribs) function performDocumentApplyAttributesToCharRange(start, end, attribs)
{ {
if (end >= rep.alltext.length) end = Math.min(end, rep.alltext.length - 1);
{ documentAttributeManager.setAttributesOnRange(lineAndColumnFromChar(start), lineAndColumnFromChar(end), attribs);
end = rep.alltext.length - 1;
}
performDocumentApplyAttributesToRange(lineAndColumnFromChar(start), lineAndColumnFromChar(end), attribs);
} }
editorInfo.ace_performDocumentApplyAttributesToCharRange = performDocumentApplyAttributesToCharRange; editorInfo.ace_performDocumentApplyAttributesToCharRange = performDocumentApplyAttributesToCharRange;
function performDocumentApplyAttributesToRange(start, end, attribs)
{
var builder = Changeset.builder(rep.lines.totalWidth());
buildKeepToStartOfRange(builder, start);
buildKeepRange(builder, start, end, attribs, rep.apool);
var cs = builder.toString();
performDocumentApplyChangeset(cs);
}
function buildKeepToStartOfRange(builder, start)
{
var startLineOffset = rep.lines.offsetOfIndex(start[0]);
builder.keep(startLineOffset, start[0]);
builder.keep(start[1]);
}
function buildRemoveRange(builder, start, end)
{
var startLineOffset = rep.lines.offsetOfIndex(start[0]);
var endLineOffset = rep.lines.offsetOfIndex(end[0]);
if (end[0] > start[0])
{
builder.remove(endLineOffset - startLineOffset - start[1], end[0] - start[0]);
builder.remove(end[1]);
}
else
{
builder.remove(end[1] - start[1]);
}
}
function buildKeepRange(builder, start, end, attribs, pool)
{
var startLineOffset = rep.lines.offsetOfIndex(start[0]);
var endLineOffset = rep.lines.offsetOfIndex(end[0]);
if (end[0] > start[0])
{
builder.keep(endLineOffset - startLineOffset - start[1], end[0] - start[0], attribs, pool);
builder.keep(end[1], 0, attribs, pool);
}
else
{
builder.keep(end[1] - start[1], 0, attribs, pool);
}
}
function setAttributeOnSelection(attributeName, attributeValue) function setAttributeOnSelection(attributeName, attributeValue)
{ {
if (!(rep.selStart && rep.selEnd)) return; if (!(rep.selStart && rep.selEnd)) return;
performDocumentApplyAttributesToRange(rep.selStart, rep.selEnd, [ documentAttributeManager.setAttributesOnRange(rep.selStart, rep.selEnd, [
[attributeName, attributeValue] [attributeName, attributeValue]
]); ]);
} }
@ -2442,13 +2345,13 @@ function Ace2Inner(){
if (selectionAllHasIt) if (selectionAllHasIt)
{ {
performDocumentApplyAttributesToRange(rep.selStart, rep.selEnd, [ documentAttributeManager.setAttributesOnRange(rep.selStart, rep.selEnd, [
[attributeName, ''] [attributeName, '']
]); ]);
} }
else else
{ {
performDocumentApplyAttributesToRange(rep.selStart, rep.selEnd, [ documentAttributeManager.setAttributesOnRange(rep.selStart, rep.selEnd, [
[attributeName, 'true'] [attributeName, 'true']
]); ]);
} }
@ -2468,7 +2371,7 @@ function Ace2Inner(){
function doRepLineSplice(startLine, deleteCount, newLineEntries) function doRepLineSplice(startLine, deleteCount, newLineEntries)
{ {
forEach(newLineEntries, function(entry) _.each(newLineEntries, function(entry)
{ {
entry.width = entry.text.length + 1; entry.width = entry.text.length + 1;
}); });
@ -2483,7 +2386,7 @@ function Ace2Inner(){
currentCallStack.repChanged = true; currentCallStack.repChanged = true;
var newRegionEnd = rep.lines.offsetOfIndex(startLine + newLineEntries.length); var newRegionEnd = rep.lines.offsetOfIndex(startLine + newLineEntries.length);
var newText = map(newLineEntries, function(e) var newText = _.map(newLineEntries, function(e)
{ {
return e.text + '\n'; return e.text + '\n';
}).join(''); }).join('');
@ -2513,7 +2416,7 @@ function Ace2Inner(){
selEndHintChar = rep.lines.offsetOfIndex(hints.selEnd[0]) + hints.selEnd[1] - oldRegionStart; selEndHintChar = rep.lines.offsetOfIndex(hints.selEnd[0]) + hints.selEnd[1] - oldRegionStart;
} }
var newText = map(newLineEntries, function(e) var newText = _.map(newLineEntries, function(e)
{ {
return e.text + '\n'; return e.text + '\n';
}).join(''); }).join('');
@ -2861,6 +2764,7 @@ function Ace2Inner(){
currentCallStack.selectionAffected = true; currentCallStack.selectionAffected = true;
} }
} }
editorInfo.ace_performSelectionChange = performSelectionChange;
// Change the abstract representation of the document to have a different selection. // Change the abstract representation of the document to have a different selection.
// Should not rely on the line representation. Should not affect the DOM. // Should not rely on the line representation. Should not affect the DOM.
@ -2969,6 +2873,10 @@ function Ace2Inner(){
"ul": 1 "ul": 1
}; };
_.each(hooks.callAll('aceRegisterBlockElements'), function(element){
_blockElems[element] = 1;
});
function isBlockElement(n) function isBlockElement(n)
{ {
return !!_blockElems[(n.tagName || "").toLowerCase()]; return !!_blockElems[(n.tagName || "").toLowerCase()];
@ -3054,7 +2962,7 @@ function Ace2Inner(){
{ {
// returns index of cleanRange containing i, or -1 if none // returns index of cleanRange containing i, or -1 if none
var answer = -1; var answer = -1;
forEach(cleanRanges, function(r, idx) _.each(cleanRanges ,function(r, idx)
{ {
if (i >= r[1]) return false; // keep looking if (i >= r[1]) return false; // keep looking
if (i < r[0]) return true; // not found, stop looking if (i < r[0]) return true; // not found, stop looking
@ -3288,7 +3196,7 @@ function Ace2Inner(){
function handleClick(evt) function handleClick(evt)
{ {
inCallStack("handleClick", function() inCallStackIfNecessary("handleClick", function()
{ {
idleWorkTimer.atMost(200); idleWorkTimer.atMost(200);
}); });
@ -3333,6 +3241,7 @@ function Ace2Inner(){
{ {
return; return;
} }
var lineNum = rep.selStart[0]; var lineNum = rep.selStart[0];
var listType = getLineListType(lineNum); var listType = getLineListType(lineNum);
@ -3404,11 +3313,9 @@ function Ace2Inner(){
} }
} }
if (mods.length > 0) _.each(mods, function(mod){
{ setLineListType(mod[0], mod[1]);
setLineListTypes(mods); });
}
return true; return true;
} }
editorInfo.ace_doIndentOutdent = doIndentOutdent; editorInfo.ace_doIndentOutdent = doIndentOutdent;
@ -3458,6 +3365,9 @@ function Ace2Inner(){
var thisLineListType = getLineListType(theLine); var thisLineListType = getLineListType(theLine);
var prevLineEntry = (theLine > 0 && rep.lines.atIndex(theLine - 1)); var prevLineEntry = (theLine > 0 && rep.lines.atIndex(theLine - 1));
var prevLineBlank = (prevLineEntry && prevLineEntry.text.length == prevLineEntry.lineMarker); var prevLineBlank = (prevLineEntry && prevLineEntry.text.length == prevLineEntry.lineMarker);
var thisLineHasMarker = documentAttributeManager.lineHasMarker(theLine);
if (thisLineListType) if (thisLineListType)
{ {
// this line is a list // this line is a list
@ -3471,6 +3381,9 @@ function Ace2Inner(){
// delistify // delistify
performDocumentReplaceRange([theLine, 0], [theLine, lineEntry.lineMarker], ''); performDocumentReplaceRange([theLine, 0], [theLine, lineEntry.lineMarker], '');
} }
}else if (thisLineHasMarker && prevLineEntry){
// If the line has any attributes assigned, remove them by removing the marker '*'
performDocumentReplaceRange([theLine -1 , prevLineEntry.text.length], [theLine, lineEntry.lineMarker], '');
} }
else if (theLine > 0) else if (theLine > 0)
{ {
@ -3610,7 +3523,7 @@ function Ace2Inner(){
var stopped = false; var stopped = false;
inCallStack("handleKeyEvent", function() inCallStackIfNecessary("handleKeyEvent", function()
{ {
if (type == "keypress" || (isTypeForSpecialKey && keyCode == 13 /*return*/ )) if (type == "keypress" || (isTypeForSpecialKey && keyCode == 13 /*return*/ ))
@ -3823,31 +3736,22 @@ function Ace2Inner(){
return; return;
} }
var mozillaCaretHack = (false && browser.mozilla && selStart && selEnd && selStart[0] == selEnd[0] && selStart[1] == rep.lines.atIndex(selStart[0]).lineMarker && selEnd[1] == rep.lines.atIndex(selEnd[0]).lineMarker && setupMozillaCaretHack(selStart[0]));
var selection = {}; var selection = {};
var ss = [selStart[0], selStart[1]]; var ss = [selStart[0], selStart[1]];
if (mozillaCaretHack) ss[1] += 1;
selection.startPoint = getPointForLineAndChar(ss); selection.startPoint = getPointForLineAndChar(ss);
var se = [selEnd[0], selEnd[1]]; var se = [selEnd[0], selEnd[1]];
if (mozillaCaretHack) se[1] += 1;
selection.endPoint = getPointForLineAndChar(se); selection.endPoint = getPointForLineAndChar(se);
selection.focusAtStart = !! rep.selFocusAtStart; selection.focusAtStart = !! rep.selFocusAtStart;
setSelection(selection); setSelection(selection);
if (mozillaCaretHack)
{
mozillaCaretHack.unhack();
}
} }
function getRepHTML() function getRepHTML()
{ {
return map(rep.lines.slice(), function(entry) return _.map(rep.lines.slice(), function(entry)
{ {
var text = entry.text; var text = entry.text;
var content; var content;
@ -4533,7 +4437,7 @@ function Ace2Inner(){
enforceEditability(); enforceEditability();
addClass(sideDiv, 'sidedivdelayed'); $(sideDiv).addClass('sidedivdelayed');
} }
function getScrollXY() function getScrollXY()
@ -4586,14 +4490,12 @@ function Ace2Inner(){
function teardown() function teardown()
{ {
forEach(_teardownActions, function(a) _.each(_teardownActions, function(a)
{ {
a(); a();
}); });
} }
bindEventHandler(window, "load", setup);
function setDesignMode(newVal) function setDesignMode(newVal)
{ {
try try
@ -4670,20 +4572,20 @@ function Ace2Inner(){
function bindTheEventHandlers() function bindTheEventHandlers()
{ {
bindEventHandler(document, "keydown", handleKeyEvent); $(document).on("keydown", handleKeyEvent);
bindEventHandler(document, "keypress", handleKeyEvent); $(document).on("keypress", handleKeyEvent);
bindEventHandler(document, "keyup", handleKeyEvent); $(document).on("keyup", handleKeyEvent);
bindEventHandler(document, "click", handleClick); $(document).on("click", handleClick);
bindEventHandler(root, "blur", handleBlur); $(root).on("blur", handleBlur);
if (browser.msie) if (browser.msie)
{ {
bindEventHandler(document, "click", handleIEOuterClick); $(document).on("click", handleIEOuterClick);
} }
if (browser.msie) bindEventHandler(root, "paste", handleIEPaste); if (browser.msie) $(root).on("paste", handleIEPaste);
if ((!browser.msie) && document.documentElement) if ((!browser.msie) && document.documentElement)
{ {
bindEventHandler(document.documentElement, "compositionstart", handleCompositionEvent); $(document.documentElement).on("compositionstart", handleCompositionEvent);
bindEventHandler(document.documentElement, "compositionend", handleCompositionEvent); $(document.documentElement).on("compositionend", handleCompositionEvent);
} }
} }
@ -4699,7 +4601,7 @@ function Ace2Inner(){
} }
// click below the body // click below the body
inCallStack("handleOuterClick", function() inCallStackIfNecessary("handleOuterClick", function()
{ {
// put caret at bottom of doc // put caret at bottom of doc
fastIncorp(11); fastIncorp(11);
@ -4730,49 +4632,16 @@ function Ace2Inner(){
elem.className = array.join(' '); elem.className = array.join(' ');
} }
function addClass(elem, className)
{
var seen = false;
var cc = getClassArray(elem, function(c)
{
if (c == className) seen = true;
return true;
});
if (!seen)
{
cc.push(className);
setClassArray(elem, cc);
}
}
function removeClass(elem, className)
{
var seen = false;
var cc = getClassArray(elem, function(c)
{
if (c == className)
{
seen = true;
return false;
}
return true;
});
if (seen)
{
setClassArray(elem, cc);
}
}
function setClassPresence(elem, className, present) function setClassPresence(elem, className, present)
{ {
if (present) addClass(elem, className); if (present) $(elem).addClass(className);
else removeClass(elem, className); else $(elem).removeClass(elem, className);
} }
function setup() function setup()
{ {
doc = document; // defined as a var in scope outside doc = document; // defined as a var in scope outside
inCallStack("setup", function() inCallStackIfNecessary("setup", function()
{ {
var body = doc.getElementById("innerdocbody"); var body = doc.getElementById("innerdocbody");
root = body; // defined as a var in scope outside root = body; // defined as a var in scope outside
@ -4833,32 +4702,6 @@ function Ace2Inner(){
} }
} }
function bindEventHandler(target, type, func)
{
var handler;
if ((typeof func._wrapper) != "function")
{
func._wrapper = function(event)
{
func(fixEvent(event || window.event || {}));
}
}
var handler = func._wrapper;
if (target.addEventListener) target.addEventListener(type, handler, false);
else target.attachEvent("on" + type, handler);
_teardownActions.push(function()
{
unbindEventHandler(target, type, func);
});
}
function unbindEventHandler(target, type, func)
{
var handler = func._wrapper;
if (target.removeEventListener) target.removeEventListener(type, handler, false);
else target.detachEvent("on" + type, handler);
}
function getSelectionPointX(point) function getSelectionPointX(point)
{ {
// doesn't work in wrap-mode // doesn't work in wrap-mode
@ -4993,26 +4836,29 @@ function Ace2Inner(){
} }
} }
var listAttributeName = 'list';
function getLineListType(lineNum) function getLineListType(lineNum)
{ {
// get "list" attribute of first char of line return documentAttributeManager.getAttributeOnLine(lineNum, listAttributeName)
var aline = rep.alines[lineNum];
if (aline)
{
var opIter = Changeset.opIterator(aline);
if (opIter.hasNext())
{
return Changeset.opAttributeValue(opIter.next(), 'list', rep.apool) || '';
}
}
return '';
} }
function setLineListType(lineNum, listType) function setLineListType(lineNum, listType)
{ {
setLineListTypes([ if(listType == ''){
[lineNum, listType] documentAttributeManager.removeAttributeOnLine(lineNum, listAttributeName);
]); }else{
documentAttributeManager.setAttributeOnLine(lineNum, listAttributeName, listType);
}
//if the list has been removed, it is necessary to renumber
//starting from the *next* line because the list may have been
//separated. If it returns null, it means that the list was not cut, try
//from the current one.
if(renumberList(lineNum+1)==null)
{
renumberList(lineNum);
}
} }
function renumberList(lineNum){ function renumberList(lineNum){
@ -5059,8 +4905,8 @@ function Ace2Inner(){
} }
else if(curLevel == level) else if(curLevel == level)
{ {
buildKeepRange(builder, loc, (loc = [line, 0])); ChangesetUtils.buildKeepRange(rep, builder, loc, (loc = [line, 0]));
buildKeepRange(builder, loc, (loc = [line, 1]), [ ChangesetUtils.buildKeepRange(rep, builder, loc, (loc = [line, 1]), [
['start', position] ['start', position]
], rep.apool); ], rep.apool);
@ -5091,62 +4937,6 @@ function Ace2Inner(){
} }
function setLineListTypes(lineNumTypePairsInOrder)
{
var loc = [0, 0];
var builder = Changeset.builder(rep.lines.totalWidth());
for (var i = 0; i < lineNumTypePairsInOrder.length; i++)
{
var pair = lineNumTypePairsInOrder[i];
var lineNum = pair[0];
var listType = pair[1];
buildKeepRange(builder, loc, (loc = [lineNum, 0]));
if (getLineListType(lineNum))
{
// already a line marker
if (listType)
{
// make different list type
buildKeepRange(builder, loc, (loc = [lineNum, 1]), [
['list', listType]
], rep.apool);
}
else
{
// remove list marker
buildRemoveRange(builder, loc, (loc = [lineNum, 1]));
}
}
else
{
// currently no line marker
if (listType)
{
// add a line marker
builder.insert('*', [
['author', thisAuthor],
['insertorder', 'first'],
['list', listType]
], rep.apool);
}
}
}
var cs = builder.toString();
if (!Changeset.isIdentity(cs))
{
performDocumentApplyChangeset(cs);
}
//if the list has been removed, it is necessary to renumber
//starting from the *next* line because the list may have been
//separated. If it returns null, it means that the list was not cut, try
//from the current one.
if(renumberList(lineNum+1)==null)
{
renumberList(lineNum);
}
}
function doInsertList(type) function doInsertList(type)
{ {
@ -5184,7 +4974,10 @@ function Ace2Inner(){
var t = getLineListType(n); var t = getLineListType(n);
mods.push([n, allLinesAreList ? 'indent' + level : (t ? type + level : type + '1')]); mods.push([n, allLinesAreList ? 'indent' + level : (t ? type + level : type + '1')]);
} }
setLineListTypes(mods);
_.each(mods, function(mod){
setLineListType(mod[0], mod[1]);
});
} }
function doInsertUnorderedList(){ function doInsertUnorderedList(){
@ -5521,66 +5314,6 @@ function Ace2Inner(){
}; };
})()); })());
// stolen from jquery-1.2.1
function fixEvent(event)
{
// store a copy of the original event object
// and clone to set read-only properties
var originalEvent = event;
event = extend(
{}, originalEvent);
// add preventDefault and stopPropagation since
// they will not work on the clone
event.preventDefault = function()
{
// if preventDefault exists run it on the original event
if (originalEvent.preventDefault) originalEvent.preventDefault();
// otherwise set the returnValue property of the original event to false (IE)
originalEvent.returnValue = false;
};
event.stopPropagation = function()
{
// if stopPropagation exists run it on the original event
if (originalEvent.stopPropagation) originalEvent.stopPropagation();
// otherwise set the cancelBubble property of the original event to true (IE)
originalEvent.cancelBubble = true;
};
// Fix target property, if necessary
if (!event.target && event.srcElement) event.target = event.srcElement;
// check if target is a textnode (safari)
if (browser.safari && event.target.nodeType == 3) event.target = originalEvent.target.parentNode;
// Add relatedTarget, if necessary
if (!event.relatedTarget && event.fromElement) event.relatedTarget = event.fromElement == event.target ? event.toElement : event.fromElement;
// Calculate pageX/Y if missing and clientX/Y available
if (event.pageX == null && event.clientX != null)
{
var e = document.documentElement,
b = document.body;
event.pageX = event.clientX + (e && e.scrollLeft || b.scrollLeft || 0);
event.pageY = event.clientY + (e && e.scrollTop || b.scrollTop || 0);
}
// Add which for key events
if (!event.which && (event.charCode || event.keyCode)) event.which = event.charCode || event.keyCode;
// Add metaKey to non-Mac browsers (use ctrl for PC's and Meta for Macs)
if (!event.metaKey && event.ctrlKey) event.metaKey = event.ctrlKey;
// Add which for click: 1 == left; 2 == middle; 3 == right
// Note: button is not normalized, so don't use it
if (!event.which && event.button) event.which = (event.button & 1 ? 1 : (event.button & 2 ? 3 : (event.button & 4 ? 2 : 0)));
return event;
}
var lineNumbersShown; var lineNumbersShown;
var sideDivInner; var sideDivInner;
@ -5665,6 +5398,67 @@ function Ace2Inner(){
} }
} }
// Init documentAttributeManager
documentAttributeManager = new AttributeManager(rep, performDocumentApplyChangeset);
editorInfo.ace_performDocumentApplyAttributesToRange = documentAttributeManager.setAttributesOnRange;
$(document).ready(function(){
doc = document; // defined as a var in scope outside
inCallStack("setup", function()
{
var body = doc.getElementById("innerdocbody");
root = body; // defined as a var in scope outside
if (browser.mozilla) $(root).addClass("mozilla");
if (browser.safari) $(root).addClass("safari");
if (browser.msie) $(root).addClass("msie");
if (browser.msie)
{
// cache CSS background images
try
{
doc.execCommand("BackgroundImageCache", false, true);
}
catch (e)
{ /* throws an error in some IE 6 but not others! */
}
}
setClassPresence(root, "authorColors", true);
setClassPresence(root, "doesWrap", doesWrap);
initDynamicCSS();
enforceEditability();
// set up dom and rep
while (root.firstChild) root.removeChild(root.firstChild);
var oneEntry = createDomLineEntry("");
doRepLineSplice(0, rep.lines.length(), [oneEntry]);
insertDomLines(null, [oneEntry.domInfo], null);
rep.alines = Changeset.splitAttributionLines(
Changeset.makeAttribution("\n"), "\n");
bindTheEventHandlers();
});
hooks.callAll('aceInitialized', {
editorInfo: editorInfo,
rep: rep,
documentAttributeManager: documentAttributeManager
});
scheduler.setTimeout(function()
{
parent.readyFunc(); // defined in code that sets up the inner iframe
}, 0);
isSetUp = true;
});
} }
exports.editor = new Ace2Inner(); // Ensure that plugins are loaded before initializing the editor
plugins.ensure(function () {
var editor = new Ace2Inner();
});

View File

@ -20,16 +20,13 @@
* limitations under the License. * limitations under the License.
*/ */
var makeCSSManager = require('/cssmanager').makeCSSManager; var makeCSSManager = require('./cssmanager').makeCSSManager;
var domline = require('/domline').domline; var domline = require('./domline').domline;
var AttribPool = require('/AttributePoolFactory').createAttributePool; var AttribPool = require('./AttributePool');
var Changeset = require('/Changeset'); var Changeset = require('./Changeset');
var linestylefilter = require('/linestylefilter').linestylefilter; var linestylefilter = require('./linestylefilter').linestylefilter;
var colorutils = require('/colorutils').colorutils; var colorutils = require('./colorutils').colorutils;
var Ace2Common = require('./ace2_common'); var _ = require('./underscore');
var map = Ace2Common.map;
var forEach = Ace2Common.forEach;
// These parameters were global, now they are injected. A reference to the // These parameters were global, now they are injected. A reference to the
// Timeslider controller would probably be more appropriate. // Timeslider controller would probably be more appropriate.
@ -155,7 +152,7 @@ function loadBroadcastJS(socket, sendSocketMsg, fireWhenAllScriptsAreLoaded, Bro
// splice the lines // splice the lines
splice: function(start, numRemoved, newLinesVA) splice: function(start, numRemoved, newLinesVA)
{ {
var newLines = map(Array.prototype.slice.call(arguments, 2), function(s) { var newLines = _.map(Array.prototype.slice.call(arguments, 2), function(s) {
return s; return s;
}); });
@ -278,7 +275,7 @@ function loadBroadcastJS(socket, sendSocketMsg, fireWhenAllScriptsAreLoaded, Bro
debugLog('Time Delta: ', timeDelta) debugLog('Time Delta: ', timeDelta)
updateTimer(); updateTimer();
var authors = map(padContents.getActiveAuthors(), function(name) var authors = _.map(padContents.getActiveAuthors(), function(name)
{ {
return authorData[name]; return authorData[name];
}); });
@ -384,7 +381,7 @@ function loadBroadcastJS(socket, sendSocketMsg, fireWhenAllScriptsAreLoaded, Bro
changesetLoader.queueUp(start, 1, update); changesetLoader.queueUp(start, 1, update);
} }
var authors = map(padContents.getActiveAuthors(), function(name){ var authors = _.map(padContents.getActiveAuthors(), function(name){
return authorData[name]; return authorData[name];
}); });
BroadcastSlider.setAuthors(authors); BroadcastSlider.setAuthors(authors);
@ -527,7 +524,7 @@ function loadBroadcastJS(socket, sendSocketMsg, fireWhenAllScriptsAreLoaded, Bro
authorMap[obj.author] = obj.data; authorMap[obj.author] = obj.data;
receiveAuthorData(authorMap); receiveAuthorData(authorMap);
var authors = map(padContents.getActiveAuthors(),function(name) { var authors = _.map(padContents.getActiveAuthors(), function(name) {
return authorData[name]; return authorData[name];
}); });
@ -607,10 +604,13 @@ function loadBroadcastJS(socket, sendSocketMsg, fireWhenAllScriptsAreLoaded, Bro
setChannelState("DISCONNECTED", reason); setChannelState("DISCONNECTED", reason);
} }
/// Since its not used, import 'forEach' has been dropped
/*window['onloadFuncts'] = []; /*window['onloadFuncts'] = [];
window.onload = function () window.onload = function ()
{ {
window['isloaded'] = true; window['isloaded'] = true;
forEach(window['onloadFuncts'],function (funct) forEach(window['onloadFuncts'],function (funct)
{ {
funct(); funct();

View File

@ -22,7 +22,7 @@
// These parameters were global, now they are injected. A reference to the // These parameters were global, now they are injected. A reference to the
// Timeslider controller would probably be more appropriate. // Timeslider controller would probably be more appropriate.
var forEach = require('./ace2_common').forEach; var _ = require('./underscore');
function loadBroadcastSliderJS(fireWhenAllScriptsAreLoaded) function loadBroadcastSliderJS(fireWhenAllScriptsAreLoaded)
{ {
@ -170,41 +170,67 @@ function loadBroadcastSliderJS(fireWhenAllScriptsAreLoaded)
$('#error').show(); $('#error').show();
} }
var fixPadHeight = _.throttle(function(){
var height = $('#timeslider-top').height();
$('#editorcontainerbox').css({marginTop: height});
}, 600);
function setAuthors(authors) function setAuthors(authors)
{ {
$("#authorstable").empty(); var authorsList = $("#authorsList");
authorsList.empty();
var numAnonymous = 0; var numAnonymous = 0;
var numNamed = 0; var numNamed = 0;
forEach(authors, function(author) var colorsAnonymous = [];
_.each(authors, function(author)
{ {
var authorColor = clientVars.colorPalette[author.colorId] || author.colorId;
if (author.name) if (author.name)
{ {
if (numNamed !== 0) authorsList.append(', ');
$('<span />')
.text(author.name || "unnamed")
.css('background-color', authorColor)
.addClass('author')
.appendTo(authorsList);
numNamed++; numNamed++;
var tr = $('<tr></tr>');
var swatchtd = $('<td></td>');
var swatch = $('<div class="swatch"></div>');
swatch.css('background-color', clientVars.colorPalette[author.colorId]);
swatchtd.append(swatch);
tr.append(swatchtd);
var nametd = $('<td></td>');
nametd.text(author.name || "unnamed");
tr.append(nametd);
$("#authorstable").append(tr);
} }
else else
{ {
numAnonymous++; numAnonymous++;
if(authorColor) colorsAnonymous.push(authorColor);
} }
}); });
if (numAnonymous > 0) if (numAnonymous > 0)
{ {
var html = "<tr><td colspan=\"2\" style=\"color:#999; padding-left: 10px\">" + (numNamed > 0 ? "...and " : "") + numAnonymous + " unnamed author" + (numAnonymous > 1 ? "s" : "") + "</td></tr>"; var anonymousAuthorString = numAnonymous + " unnamed author" + (numAnonymous > 1 ? "s" : "")
$("#authorstable").append($(html)); if (numNamed !== 0){
authorsList.append(' + ' + anonymousAuthorString);
} else {
authorsList.append(anonymousAuthorString);
}
if(colorsAnonymous.length > 0){
authorsList.append(' (');
_.each(colorsAnonymous, function(color, i){
if( i > 0 ) authorsList.append(' ');
$('<span /> ')
.css('background-color', color)
.addClass('author author-anonymous')
.appendTo(authorsList);
});
authorsList.append(')');
}
} }
if (authors.length == 0) if (authors.length == 0)
{ {
$("#authorstable").append($("<tr><td colspan=\"2\" style=\"color:#999; padding-left: 10px\">No Authors</td></tr>")) authorsList.append("No Authors");
} }
fixPadHeight();
} }
BroadcastSlider = { BroadcastSlider = {
@ -465,11 +491,10 @@ function loadBroadcastSliderJS(fireWhenAllScriptsAreLoaded)
{ {
if (clientVars.supportsSlider) if (clientVars.supportsSlider)
{ {
$("#padmain, #rightbars").css('top', "130px");
$("#timeslider").show(); $("#timeslider").show();
setSliderLength(clientVars.totalRevs); setSliderLength(clientVars.totalRevs);
setSliderPosition(clientVars.revNum); setSliderPosition(clientVars.revNum);
forEach(clientVars.savedRevisions, function(revision) _.each(clientVars.savedRevisions, function(revision)
{ {
addSavedRevision(revision.revNum, revision); addSavedRevision(revision.revNum, revision);
}) })

View File

@ -20,8 +20,8 @@
* limitations under the License. * limitations under the License.
*/ */
var AttribPool = require('/AttributePoolFactory').createAttributePool; var AttributePool = require('./AttributePool');
var Changeset = require('/Changeset'); var Changeset = require('./Changeset');
function makeChangesetTracker(scheduler, apool, aceCallbacksProvider) function makeChangesetTracker(scheduler, apool, aceCallbacksProvider)
{ {
@ -83,7 +83,7 @@ function makeChangesetTracker(scheduler, apool, aceCallbacksProvider)
baseAText = Changeset.cloneAText(atext); baseAText = Changeset.cloneAText(atext);
if (apoolJsonObj) if (apoolJsonObj)
{ {
var wireApool = (new AttribPool()).fromJsonable(apoolJsonObj); var wireApool = (new AttributePool()).fromJsonable(apoolJsonObj);
baseAText.attribs = Changeset.moveOpsToNewPool(baseAText.attribs, wireApool, apool); baseAText.attribs = Changeset.moveOpsToNewPool(baseAText.attribs, wireApool, apool);
} }
submittedChangeset = null; submittedChangeset = null;
@ -117,7 +117,7 @@ function makeChangesetTracker(scheduler, apool, aceCallbacksProvider)
if (apoolJsonObj) if (apoolJsonObj)
{ {
var wireApool = (new AttribPool()).fromJsonable(apoolJsonObj); var wireApool = (new AttributePool()).fromJsonable(apoolJsonObj);
c = Changeset.moveOpsToNewPool(c, wireApool, apool); c = Changeset.moveOpsToNewPool(c, wireApool, apool);
} }

Some files were not shown because too many files have changed in this diff Show More