diff options
author | Matthias Bartelmeß <mba@fourplusone.de> | 2012-03-13 19:40:33 +0100 |
---|---|---|
committer | Matthias Bartelmeß <mba@fourplusone.de> | 2012-03-13 19:40:33 +0100 |
commit | 70940521f2cdc3cdb3361bd148e141f43505ee4d (patch) | |
tree | 96eb9974ee057b9fc3c8947d33bdd1601de8cd30 /src | |
parent | cfb58a80a30486156a15515164c9c0f4647f165b (diff) | |
parent | cccd8a923cfa21c7e248a2a5fcffa54caf512e0b (diff) | |
download | etherpad-lite-70940521f2cdc3cdb3361bd148e141f43505ee4d.zip |
Merge branch 'develop' of git://github.com/Pita/etherpad-lite
Diffstat (limited to 'src')
113 files changed, 41507 insertions, 0 deletions
diff --git a/src/ep.json b/src/ep.json new file mode 100644 index 00000000..59cbf3aa --- /dev/null +++ b/src/ep.json @@ -0,0 +1,14 @@ +{ + "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" } } + + ] +} diff --git a/src/node/README.md b/src/node/README.md new file mode 100644 index 00000000..4b443289 --- /dev/null +++ b/src/node/README.md @@ -0,0 +1,13 @@ +# About the folder structure + +* **db** - all modules that are accesing the data structure and are communicating directly to the database +* **handler** - all modules that responds directly to requests/messages of the browser +* **utils** - helper modules + +# Module name conventions + +Module file names start with a capital letter and uses camelCase + +# Where does it start? + +server.js is started directly diff --git a/src/node/db/API.js b/src/node/db/API.js new file mode 100644 index 00000000..09cc95af --- /dev/null +++ b/src/node/db/API.js @@ -0,0 +1,520 @@ +/** + * This module provides all API functions + */ + +/* + * 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 customError = require("../utils/customError"); +var padManager = require("./PadManager"); +var padMessageHandler = require("../handler/PadMessageHandler"); +var readOnlyManager = require("./ReadOnlyManager"); +var groupManager = require("./GroupManager"); +var authorManager = require("./AuthorManager"); +var sessionManager = require("./SessionManager"); +var async = require("async"); +var exportHtml = require("../utils/ExportHtml"); +var importHtml = require("../utils/ImportHtml"); +var cleanText = require("./Pad").cleanText; + +/**********************/ +/**GROUP FUNCTIONS*****/ +/**********************/ + +exports.createGroup = groupManager.createGroup; +exports.createGroupIfNotExistsFor = groupManager.createGroupIfNotExistsFor; +exports.deleteGroup = groupManager.deleteGroup; +exports.listPads = groupManager.listPads; +exports.createGroupPad = groupManager.createGroupPad; + +/**********************/ +/**AUTHOR FUNCTIONS****/ +/**********************/ + +exports.createAuthor = authorManager.createAuthor; +exports.createAuthorIfNotExistsFor = authorManager.createAuthorIfNotExistsFor; + +/**********************/ +/**SESSION FUNCTIONS***/ +/**********************/ + +exports.createSession = sessionManager.createSession; +exports.deleteSession = sessionManager.deleteSession; +exports.getSessionInfo = sessionManager.getSessionInfo; +exports.listSessionsOfGroup = sessionManager.listSessionsOfGroup; +exports.listSessionsOfAuthor = sessionManager.listSessionsOfAuthor; + +/************************/ +/**PAD CONTENT FUNCTIONS*/ +/************************/ + +/** +getText(padID, [rev]) returns the text of a pad + +Example returns: + +{code: 0, message:"ok", data: {text:"Welcome Text"}} +{code: 1, message:"padID does not exist", data: null} +*/ +exports.getText = function(padID, rev, callback) +{ + //check if rev is set + if(typeof rev == "function") + { + callback = rev; + rev = undefined; + } + + //check if rev is a number + if(rev !== undefined && typeof rev != "number") + { + //try to parse the number + if(!isNaN(parseInt(rev))) + { + rev = parseInt(rev); + } + else + { + callback(new customError("rev is not a number", "apierror")); + return; + } + } + + //ensure this is not a negativ number + if(rev !== undefined && rev < 0) + { + callback(new customError("rev is a negativ number","apierror")); + return; + } + + //ensure this is not a float value + if(rev !== undefined && !is_int(rev)) + { + callback(new customError("rev is a float value","apierror")); + return; + } + + //get the pad + getPadSafe(padID, true, function(err, pad) + { + if(ERR(err, callback)) return; + + //the client asked for a special revision + if(rev !== undefined) + { + //check if this is a valid revision + if(rev > pad.getHeadRevisionNumber()) + { + callback(new customError("rev is higher than the head revision of the pad","apierror")); + return; + } + + //get the text of this revision + pad.getInternalRevisionAText(rev, function(err, atext) + { + if(ERR(err, callback)) return; + + data = {text: atext.text}; + + callback(null, data); + }) + } + //the client wants the latest text, lets return it to him + else + { + callback(null, {"text": pad.text()}); + } + }); +} + +/** +setText(padID, text) sets the text of a pad + +Example returns: + +{code: 0, message:"ok", data: null} +{code: 1, message:"padID does not exist", data: null} +{code: 1, message:"text too long", data: null} +*/ +exports.setText = function(padID, text, callback) +{ + //get the pad + getPadSafe(padID, true, function(err, pad) + { + if(ERR(err, callback)) return; + + //set the text + pad.setText(text); + + //update the clients on the pad + padMessageHandler.updatePadClients(pad, callback); + }); +} + +/** +getHTML(padID, [rev]) returns the html of a pad + +Example returns: + +{code: 0, message:"ok", data: {text:"Welcome <strong>Text</strong>"}} +{code: 1, message:"padID does not exist", data: null} +*/ +exports.getHTML = function(padID, rev, callback) +{ + if(typeof rev == "function") + { + callback = rev; + rev = undefined; + } + + if (rev !== undefined && typeof rev != "number") + { + if (!isNaN(parseInt(rev))) + { + rev = parseInt(rev); + } + else + { + callback(new customError("rev is not a number","apierror")); + return; + } + } + + if(rev !== undefined && rev < 0) + { + callback(new customError("rev is a negative number","apierror")); + return; + } + + if(rev !== undefined && !is_int(rev)) + { + callback(new customError("rev is a float value","apierror")); + return; + } + + getPadSafe(padID, true, function(err, pad) + { + if(ERR(err, callback)) return; + + //the client asked for a special revision + if(rev !== undefined) + { + //check if this is a valid revision + if(rev > pad.getHeadRevisionNumber()) + { + callback(new customError("rev is higher than the head revision of the pad","apierror")); + return; + } + + //get the html of this revision + exportHtml.getPadHTML(pad, rev, function(err, html) + { + if(ERR(err, callback)) return; + data = {html: html}; + callback(null, data); + }); + } + //the client wants the latest text, lets return it to him + else + { + exportHtml.getPadHTML(pad, undefined, function (err, html) + { + if(ERR(err, callback)) return; + + data = {html: html}; + + callback(null, data); + }); + } + }); +} + +exports.setHTML = function(padID, html, callback) +{ + //get the pad + getPadSafe(padID, true, function(err, pad) + { + if(ERR(err, callback)) return; + + // add a new changeset with the new html to the pad + importHtml.setPadHTML(pad, cleanText(html)); + + //update the clients on the pad + padMessageHandler.updatePadClients(pad, callback); + + }); +} + +/*****************/ +/**PAD FUNCTIONS */ +/*****************/ + +/** +getRevisionsCount(padID) returns the number of revisions of this pad + +Example returns: + +{code: 0, message:"ok", data: {revisions: 56}} +{code: 1, message:"padID does not exist", data: null} +*/ +exports.getRevisionsCount = function(padID, callback) +{ + //get the pad + getPadSafe(padID, true, function(err, pad) + { + if(ERR(err, callback)) return; + + callback(null, {revisions: pad.getHeadRevisionNumber()}); + }); +} + +/** +createPad(padName [, text]) creates a new pad in this group + +Example returns: + +{code: 0, message:"ok", data: null} +{code: 1, message:"pad does already exist", data: null} +*/ +exports.createPad = function(padID, text, callback) +{ + //ensure there is no $ in the padID + if(padID && padID.indexOf("$") != -1) + { + callback(new customError("createPad can't create group pads","apierror")); + return; + } + + //create pad + getPadSafe(padID, false, text, function(err) + { + if(ERR(err, callback)) return; + callback(); + }); +} + +/** +deletePad(padID) deletes a pad + +Example returns: + +{code: 0, message:"ok", data: null} +{code: 1, message:"padID does not exist", data: null} +*/ +exports.deletePad = function(padID, callback) +{ + getPadSafe(padID, true, function(err, pad) + { + if(ERR(err, callback)) return; + + pad.remove(callback); + }); +} + +/** +getReadOnlyLink(padID) returns the read only link of a pad + +Example returns: + +{code: 0, message:"ok", data: null} +{code: 1, message:"padID does not exist", data: null} +*/ +exports.getReadOnlyID = function(padID, callback) +{ + //we don't need the pad object, but this function does all the security stuff for us + getPadSafe(padID, true, function(err) + { + if(ERR(err, callback)) return; + + //get the readonlyId + readOnlyManager.getReadOnlyId(padID, function(err, readOnlyId) + { + if(ERR(err, callback)) return; + callback(null, {readOnlyID: readOnlyId}); + }); + }); +} + +/** +setPublicStatus(padID, publicStatus) sets a boolean for the public status of a pad + +Example returns: + +{code: 0, message:"ok", data: null} +{code: 1, message:"padID does not exist", data: null} +*/ +exports.setPublicStatus = function(padID, publicStatus, callback) +{ + //ensure this is a group pad + if(padID && padID.indexOf("$") == -1) + { + callback(new customError("You can only get/set the publicStatus of pads that belong to a group","apierror")); + return; + } + + //get the pad + getPadSafe(padID, true, function(err, pad) + { + if(ERR(err, callback)) return; + + //convert string to boolean + if(typeof publicStatus == "string") + publicStatus = publicStatus == "true" ? true : false; + + //set the password + pad.setPublicStatus(publicStatus); + + callback(); + }); +} + +/** +getPublicStatus(padID) return true of false + +Example returns: + +{code: 0, message:"ok", data: {publicStatus: true}} +{code: 1, message:"padID does not exist", data: null} +*/ +exports.getPublicStatus = function(padID, callback) +{ + //ensure this is a group pad + if(padID && padID.indexOf("$") == -1) + { + callback(new customError("You can only get/set the publicStatus of pads that belong to a group","apierror")); + return; + } + + //get the pad + getPadSafe(padID, true, function(err, pad) + { + if(ERR(err, callback)) return; + + callback(null, {publicStatus: pad.getPublicStatus()}); + }); +} + +/** +setPassword(padID, password) returns ok or a error message + +Example returns: + +{code: 0, message:"ok", data: null} +{code: 1, message:"padID does not exist", data: null} +*/ +exports.setPassword = function(padID, password, callback) +{ + //ensure this is a group pad + if(padID && padID.indexOf("$") == -1) + { + callback(new customError("You can only get/set the password of pads that belong to a group","apierror")); + return; + } + + //get the pad + getPadSafe(padID, true, function(err, pad) + { + if(ERR(err, callback)) return; + + //set the password + pad.setPassword(password); + + callback(); + }); +} + +/** +isPasswordProtected(padID) returns true or false + +Example returns: + +{code: 0, message:"ok", data: {passwordProtection: true}} +{code: 1, message:"padID does not exist", data: null} +*/ +exports.isPasswordProtected = function(padID, callback) +{ + //ensure this is a group pad + if(padID && padID.indexOf("$") == -1) + { + callback(new customError("You can only get/set the password of pads that belong to a group","apierror")); + return; + } + + //get the pad + getPadSafe(padID, true, function(err, pad) + { + if(ERR(err, callback)) return; + + callback(null, {isPasswordProtected: pad.isPasswordProtected()}); + }); +} + +/******************************/ +/** INTERNAL HELPER FUNCTIONS */ +/******************************/ + +//checks if a number is an int +function is_int(value) +{ + return (parseFloat(value) == parseInt(value)) && !isNaN(value) +} + +//gets a pad safe +function getPadSafe(padID, shouldExist, text, callback) +{ + if(typeof text == "function") + { + callback = text; + text = null; + } + + //check if padID is a string + if(typeof padID != "string") + { + callback(new customError("padID is not a string","apierror")); + return; + } + + //check if the padID maches the requirements + if(!padManager.isValidPadId(padID)) + { + callback(new customError("padID did not match requirements","apierror")); + return; + } + + //check if the pad exists + padManager.doesPadExists(padID, function(err, exists) + { + if(ERR(err, callback)) return; + + //does not exist, but should + if(exists == false && shouldExist == true) + { + callback(new customError("padID does not exist","apierror")); + } + //does exists, but shouldn't + else if(exists == true && shouldExist == false) + { + callback(new customError("padID does already exist","apierror")); + } + //pad exists, let's get it + else + { + padManager.getPad(padID, text, callback); + } + }); +} diff --git a/src/node/db/AuthorManager.js b/src/node/db/AuthorManager.js new file mode 100644 index 00000000..f644de12 --- /dev/null +++ b/src/node/db/AuthorManager.js @@ -0,0 +1,181 @@ +/** + * The AuthorManager controlls all information about the Pad authors + */ + +/* + * 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 db = require("./DB").db; +var async = require("async"); +var randomString = require('ep_etherpad-lite/static/js/pad_utils').randomString; + +/** + * Checks if the author exists + */ +exports.doesAuthorExists = function (authorID, callback) +{ + //check if the database entry of this author exists + db.get("globalAuthor:" + authorID, function (err, author) + { + if(ERR(err, callback)) return; + callback(null, author != null); + }); +} + +/** + * Returns the AuthorID for a token. + * @param {String} token The token + * @param {Function} callback callback (err, author) + */ +exports.getAuthor4Token = function (token, callback) +{ + mapAuthorWithDBKey("token2author", token, function(err, author) + { + if(ERR(err, callback)) return; + //return only the sub value authorID + callback(null, author ? author.authorID : author); + }); +} + +/** + * Returns the AuthorID for a mapper. + * @param {String} token The mapper + * @param {Function} callback callback (err, author) + */ +exports.createAuthorIfNotExistsFor = function (authorMapper, name, callback) +{ + mapAuthorWithDBKey("mapper2author", authorMapper, function(err, author) + { + if(ERR(err, callback)) return; + + //set the name of this author + if(name) + exports.setAuthorName(author.authorID, name); + + //return the authorID + callback(null, author); + }); +} + +/** + * Returns the AuthorID for a mapper. We can map using a mapperkey, + * so far this is token2author and mapper2author + * @param {String} mapperkey The database key name for this mapper + * @param {String} mapper The mapper + * @param {Function} callback callback (err, author) + */ +function mapAuthorWithDBKey (mapperkey, mapper, callback) +{ + //try to map to an author + db.get(mapperkey + ":" + mapper, function (err, author) + { + if(ERR(err, callback)) return; + + //there is no author with this mapper, so create one + if(author == null) + { + exports.createAuthor(null, function(err, author) + { + if(ERR(err, callback)) return; + + //create the token2author relation + db.set(mapperkey + ":" + mapper, author.authorID); + + //return the author + callback(null, author); + }); + } + //there is a author with this mapper + else + { + //update the timestamp of this author + db.setSub("globalAuthor:" + author, ["timestamp"], new Date().getTime()); + + //return the author + callback(null, {authorID: author}); + } + }); +} + +/** + * Internal function that creates the database entry for an author + * @param {String} name The name of the author + */ +exports.createAuthor = function(name, callback) +{ + //create the new author name + var author = "a." + randomString(16); + + //create the globalAuthors db entry + var authorObj = {"colorId" : Math.floor(Math.random()*32), "name": name, "timestamp": new Date().getTime()}; + + //set the global author db entry + db.set("globalAuthor:" + author, authorObj); + + callback(null, {authorID: author}); +} + +/** + * Returns the Author Obj of the author + * @param {String} author The id of the author + * @param {Function} callback callback(err, authorObj) + */ +exports.getAuthor = function (author, callback) +{ + db.get("globalAuthor:" + author, callback); +} + +/** + * Returns the color Id of the author + * @param {String} author The id of the author + * @param {Function} callback callback(err, colorId) + */ +exports.getAuthorColorId = function (author, callback) +{ + db.getSub("globalAuthor:" + author, ["colorId"], callback); +} + +/** + * Sets the color Id of the author + * @param {String} author The id of the author + * @param {Function} callback (optional) + */ +exports.setAuthorColorId = function (author, colorId, callback) +{ + db.setSub("globalAuthor:" + author, ["colorId"], colorId, callback); +} + +/** + * Returns the name of the author + * @param {String} author The id of the author + * @param {Function} callback callback(err, name) + */ +exports.getAuthorName = function (author, callback) +{ + db.getSub("globalAuthor:" + author, ["name"], callback); +} + +/** + * Sets the name of the author + * @param {String} author The id of the author + * @param {Function} callback (optional) + */ +exports.setAuthorName = function (author, name, callback) +{ + db.setSub("globalAuthor:" + author, ["name"], name, callback); +} diff --git a/src/node/db/DB.js b/src/node/db/DB.js new file mode 100644 index 00000000..7273c83e --- /dev/null +++ b/src/node/db/DB.js @@ -0,0 +1,57 @@ +/** + * The DB Module provides a database initalized with the settings + * provided by the settings module + */ + +/* + * 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 ueberDB = require("ueberDB"); +var settings = require("../utils/Settings"); +var log4js = require('log4js'); + +//set database settings +var db = new ueberDB.database(settings.dbType, settings.dbSettings, null, log4js.getLogger("ueberDB")); + +/** + * The UeberDB Object that provides the database functions + */ +exports.db = null; + +/** + * Initalizes the database with the settings provided by the settings module + * @param {Function} callback + */ +exports.init = function(callback) +{ + //initalize the database async + db.init(function(err) + { + //there was an error while initializing the database, output it and stop + if(err) + { + console.error("ERROR: Problem while initalizing the database"); + console.error(err.stack ? err.stack : err); + process.exit(1); + } + //everything ok + else + { + exports.db = db; + callback(null); + } + }); +} diff --git a/src/node/db/GroupManager.js b/src/node/db/GroupManager.js new file mode 100644 index 00000000..bd19507f --- /dev/null +++ b/src/node/db/GroupManager.js @@ -0,0 +1,263 @@ +/** + * The Group Manager provides functions to manage groups in the database + */ + +/* + * 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 customError = require("../utils/customError"); +var randomString = require('ep_etherpad-lite/static/js/pad_utils').randomString; +var db = require("./DB").db; +var async = require("async"); +var padManager = require("./PadManager"); +var sessionManager = require("./SessionManager"); + +exports.deleteGroup = function(groupID, callback) +{ + var group; + + async.series([ + //ensure group exists + function (callback) + { + //try to get the group entry + db.get("group:" + groupID, function (err, _group) + { + if(ERR(err, callback)) return; + + //group does not exist + if(_group == null) + { + callback(new customError("groupID does not exist","apierror")); + } + //group exists, everything is fine + else + { + group = _group; + callback(); + } + }); + }, + //iterate trough all pads of this groups and delete them + function(callback) + { + //collect all padIDs in an array, that allows us to use async.forEach + var padIDs = []; + for(var i in group.pads) + { + padIDs.push(i); + } + + //loop trough all pads and delete them + async.forEach(padIDs, function(padID, callback) + { + padManager.getPad(padID, function(err, pad) + { + if(ERR(err, callback)) return; + + pad.remove(callback); + }); + }, callback); + }, + //iterate trough group2sessions and delete all sessions + function(callback) + { + //try to get the group entry + db.get("group2sessions:" + groupID, function (err, group2sessions) + { + if(ERR(err, callback)) return; + + //skip if there is no group2sessions entry + if(group2sessions == null) {callback(); return} + + //collect all sessions in an array, that allows us to use async.forEach + var sessions = []; + for(var i in group2sessions.sessionsIDs) + { + sessions.push(i); + } + + //loop trough all sessions and delete them + async.forEach(sessions, function(session, callback) + { + sessionManager.deleteSession(session, callback); + }, callback); + }); + }, + //remove group and group2sessions entry + function(callback) + { + db.remove("group2sessions:" + groupID); + db.remove("group:" + groupID); + callback(); + } + ], function(err) + { + if(ERR(err, callback)) return; + callback(); + }); +} + +exports.doesGroupExist = function(groupID, callback) +{ + //try to get the group entry + db.get("group:" + groupID, function (err, group) + { + if(ERR(err, callback)) return; + callback(null, group != null); + }); +} + +exports.createGroup = function(callback) +{ + //search for non existing groupID + var groupID = "g." + randomString(16); + + //create the group + db.set("group:" + groupID, {pads: {}}); + callback(null, {groupID: groupID}); +} + +exports.createGroupIfNotExistsFor = function(groupMapper, callback) +{ + //ensure mapper is optional + if(typeof groupMapper != "string") + { + callback(new customError("groupMapper is no string","apierror")); + return; + } + + //try to get a group for this mapper + db.get("mapper2group:"+groupMapper, function(err, groupID) + { + if(ERR(err, callback)) return; + + //there is no group for this mapper, let's create a group + if(groupID == null) + { + exports.createGroup(function(err, responseObj) + { + if(ERR(err, callback)) return; + + //create the mapper entry for this group + db.set("mapper2group:"+groupMapper, responseObj.groupID); + + callback(null, responseObj); + }); + } + //there is a group for this mapper, let's return it + else + { + if(ERR(err, callback)) return; + callback(null, {groupID: groupID}); + } + }); +} + +exports.createGroupPad = function(groupID, padName, text, callback) +{ + //create the padID + var padID = groupID + "$" + padName; + + async.series([ + //ensure group exists + function (callback) + { + exports.doesGroupExist(groupID, function(err, exists) + { + if(ERR(err, callback)) return; + + //group does not exist + if(exists == false) + { + callback(new customError("groupID does not exist","apierror")); + } + //group exists, everything is fine + else + { + callback(); + } + }); + }, + //ensure pad does not exists + function (callback) + { + padManager.doesPadExists(padID, function(err, exists) + { + if(ERR(err, callback)) return; + + //pad exists already + if(exists == true) + { + callback(new customError("padName does already exist","apierror")); + } + //pad does not exist, everything is fine + else + { + callback(); + } + }); + }, + //create the pad + function (callback) + { + padManager.getPad(padID, text, function(err) + { + if(ERR(err, callback)) return; + callback(); + }); + }, + //create an entry in the group for this pad + function (callback) + { + db.setSub("group:" + groupID, ["pads", padID], 1); + callback(); + } + ], function(err) + { + if(ERR(err, callback)) return; + callback(null, {padID: padID}); + }); +} + +exports.listPads = function(groupID, callback) +{ + exports.doesGroupExist(groupID, function(err, exists) + { + if(ERR(err, callback)) return; + + //group does not exist + if(exists == false) + { + callback(new customError("groupID does not exist","apierror")); + } + //group exists, let's get the pads + else + { + db.getSub("group:" + groupID, ["pads"], function(err, result) + { + if(ERR(err, callback)) return; + var pads = []; + for ( var padId in result ) { + pads.push(padId); + } + callback(null, {padIDs: pads}); + }); + } + }); +} diff --git a/src/node/db/Pad.js b/src/node/db/Pad.js new file mode 100644 index 00000000..b69780a4 --- /dev/null +++ b/src/node/db/Pad.js @@ -0,0 +1,488 @@ +/** + * The pad object, defined with joose + */ + + +var ERR = require("async-stacktrace"); +var Changeset = require("ep_etherpad-lite/static/js/Changeset"); +var AttributePoolFactory = require("ep_etherpad-lite/static/js/AttributePoolFactory"); +var randomString = require('ep_etherpad-lite/static/js/pad_utils').randomString; +var db = require("./DB").db; +var async = require("async"); +var settings = require('../utils/Settings'); +var authorManager = require("./AuthorManager"); +var padManager = require("./PadManager"); +var padMessageHandler = require("../handler/PadMessageHandler"); +var readOnlyManager = require("./ReadOnlyManager"); +var crypto = require("crypto"); + +/** + * Copied from the Etherpad source code. It converts Windows line breaks to Unix line breaks and convert Tabs to spaces + * @param txt + */ +exports.cleanText = function (txt) { + return txt.replace(/\r\n/g,'\n').replace(/\r/g,'\n').replace(/\t/g, ' ').replace(/\xa0/g, ' '); +}; + + +var Pad = function Pad(id) { + + this.atext = Changeset.makeAText("\n"); + this.pool = AttributePoolFactory.createAttributePool(); + this.head = -1; + this.chatHead = -1; + this.publicStatus = false; + this.passwordHash = null; + this.id = id; + +}; + +exports.Pad = Pad; + +Pad.prototype.apool = function apool() { + return this.pool; +}; + +Pad.prototype.getHeadRevisionNumber = function getHeadRevisionNumber() { + return this.head; +}; + +Pad.prototype.getPublicStatus = function getPublicStatus() { + return this.publicStatus; +}; + +Pad.prototype.appendRevision = function appendRevision(aChangeset, author) { + if(!author) + author = ''; + + var newAText = Changeset.applyToAText(aChangeset, this.atext, this.pool); + Changeset.copyAText(newAText, this.atext); + + var newRev = ++this.head; + + var newRevData = {}; + newRevData.changeset = aChangeset; + newRevData.meta = {}; + newRevData.meta.author = author; + newRevData.meta.timestamp = new Date().getTime(); + + //ex. getNumForAuthor + if(author != '') + this.pool.putAttrib(['author', author || '']); + + if(newRev % 100 == 0) + { + newRevData.meta.atext = this.atext; + } + + db.set("pad:"+this.id+":revs:"+newRev, newRevData); + db.set("pad:"+this.id, {atext: this.atext, + pool: this.pool.toJsonable(), + head: this.head, + chatHead: this.chatHead, + publicStatus: this.publicStatus, + passwordHash: this.passwordHash}); +}; + +Pad.prototype.getRevisionChangeset = function getRevisionChangeset(revNum, callback) { + db.getSub("pad:"+this.id+":revs:"+revNum, ["changeset"], callback); +}; + +Pad.prototype.getRevisionAuthor = function getRevisionAuthor(revNum, callback) { + db.getSub("pad:"+this.id+":revs:"+revNum, ["meta", "author"], callback); +}; + +Pad.prototype.getRevisionDate = function getRevisionDate(revNum, callback) { + db.getSub("pad:"+this.id+":revs:"+revNum, ["meta", "timestamp"], callback); +}; + +Pad.prototype.getAllAuthors = function getAllAuthors() { + var authors = []; + + for(key in this.pool.numToAttrib) + { + if(this.pool.numToAttrib[key][0] == "author" && this.pool.numToAttrib[key][1] != "") + { + authors.push(this.pool.numToAttrib[key][1]); + } + } + + return authors; +}; + +Pad.prototype.getInternalRevisionAText = function getInternalRevisionAText(targetRev, callback) { + var _this = this; + + var keyRev = this.getKeyRevisionNumber(targetRev); + var atext; + var changesets = []; + + //find out which changesets are needed + var neededChangesets = []; + var curRev = keyRev; + while (curRev < targetRev) + { + curRev++; + neededChangesets.push(curRev); + } + + async.series([ + //get all needed data out of the database + function(callback) + { + async.parallel([ + //get the atext of the key revision + function (callback) + { + db.getSub("pad:"+_this.id+":revs:"+keyRev, ["meta", "atext"], function(err, _atext) + { + if(ERR(err, callback)) return; + atext = Changeset.cloneAText(_atext); + callback(); + }); + }, + //get all needed changesets + function (callback) + { + async.forEach(neededChangesets, function(item, callback) + { + _this.getRevisionChangeset(item, function(err, changeset) + { + if(ERR(err, callback)) return; + changesets[item] = changeset; + callback(); + }); + }, callback); + } + ], callback); + }, + //apply all changesets to the key changeset + function(callback) + { + var apool = _this.apool(); + var curRev = keyRev; + + while (curRev < targetRev) + { + curRev++; + var cs = changesets[curRev]; + atext = Changeset.applyToAText(cs, atext, apool); + } + + callback(null); + } + ], function(err) + { + if(ERR(err, callback)) return; + callback(null, atext); + }); +}; + +Pad.prototype.getKeyRevisionNumber = function getKeyRevisionNumber(revNum) { + return Math.floor(revNum / 100) * 100; +}; + +Pad.prototype.text = function text() { + return this.atext.text; +}; + +Pad.prototype.setText = function setText(newText) { + //clean the new text + newText = exports.cleanText(newText); + + var oldText = this.text(); + + //create the changeset + var changeset = Changeset.makeSplice(oldText, 0, oldText.length-1, newText); + + //append the changeset + this.appendRevision(changeset); +}; + +Pad.prototype.appendChatMessage = function appendChatMessage(text, userId, time) { + this.chatHead++; + //save the chat entry in the database + db.set("pad:"+this.id+":chat:"+this.chatHead, {"text": text, "userId": userId, "time": time}); + //save the new chat head + db.setSub("pad:"+this.id, ["chatHead"], this.chatHead); +}; + +Pad.prototype.getChatMessage = function getChatMessage(entryNum, callback) { + var _this = this; + var entry; + + async.series([ + //get the chat entry + function(callback) + { + db.get("pad:"+_this.id+":chat:"+entryNum, function(err, _entry) + { + if(ERR(err, callback)) return; + entry = _entry; + callback(); + }); + }, + //add the authorName + function(callback) + { + //this chat message doesn't exist, return null + if(entry == null) + { + callback(); + return; + } + + //get the authorName + authorManager.getAuthorName(entry.userId, function(err, authorName) + { + if(ERR(err, callback)) return; + entry.userName = authorName; + callback(); + }); + } + ], function(err) + { + if(ERR(err, callback)) return; + callback(null, entry); + }); +}; + +Pad.prototype.getLastChatMessages = function getLastChatMessages(count, callback) { + //return an empty array if there are no chat messages + if(this.chatHead == -1) + { + callback(null, []); + return; + } + + var _this = this; + + //works only if we decrement the amount, for some reason + count--; + + //set the startpoint + var start = this.chatHead-count; + if(start < 0) + start = 0; + + //set the endpoint + var end = this.chatHead; + + //collect the numbers of chat entries and in which order we need them + var neededEntries = []; + var order = 0; + for(var i=start;i<=end; i++) + { + neededEntries.push({entryNum:i, order: order}); + order++; + } + + //get all entries out of the database + var entries = []; + async.forEach(neededEntries, function(entryObject, callback) + { + _this.getChatMessage(entryObject.entryNum, function(err, entry) + { + if(ERR(err, callback)) return; + entries[entryObject.order] = entry; + callback(); + }); + }, function(err) + { + if(ERR(err, callback)) return; + + //sort out broken chat entries + //it looks like in happend in the past that the chat head was + //incremented, but the chat message wasn't added + var cleanedEntries = []; + for(var i=0;i<entries.length;i++) + { + if(entries[i]!=null) + cleanedEntries.push(entries[i]); + else + console.warn("WARNING: Found broken chat entry in pad " + _this.id); + } + + callback(null, cleanedEntries); + }); +}; + +Pad.prototype.init = function init(text, callback) { + var _this = this; + + //replace text with default text if text isn't set + if(text == null) + { + text = settings.defaultPadText; + } + + //try to load the pad + db.get("pad:"+this.id, function(err, value) + { + if(ERR(err, callback)) return; + + //if this pad exists, load it + if(value != null) + { + _this.head = value.head; + _this.atext = value.atext; + _this.pool = _this.pool.fromJsonable(value.pool); + + //ensure we have a local chatHead variable + if(value.chatHead != null) + _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 + else + { + var firstChangeset = Changeset.makeSplice("\n", 0, 0, exports.cleanText(text)); + + _this.appendRevision(firstChangeset, ''); + } + + callback(null); + }); +}; + +Pad.prototype.remove = function remove(callback) { + var padID = this.id; + var _this = this; + + //kick everyone from this pad + padMessageHandler.kickSessionsFromPad(padID); + + async.series([ + //delete all relations + function(callback) + { + async.parallel([ + //is it a group pad? -> delete the entry of this pad in the group + function(callback) + { + //is it a group pad? + if(padID.indexOf("$")!=-1) + { + var groupID = padID.substring(0,padID.indexOf("$")); + + db.get("group:" + groupID, function (err, group) + { + if(ERR(err, callback)) return; + + //remove the pad entry + delete group.pads[padID]; + + //set the new value + db.set("group:" + groupID, group); + + callback(); + }); + } + //its no group pad, nothing to do here + else + { + callback(); + } + }, + //remove the readonly entries + function(callback) + { + readOnlyManager.getReadOnlyId(padID, function(err, readonlyID) + { + if(ERR(err, callback)) return; + + db.remove("pad2readonly:" + padID); + db.remove("readonly2pad:" + readonlyID); + + callback(); + }); + }, + //delete all chat messages + function(callback) + { + var chatHead = _this.chatHead; + + for(var i=0;i<=chatHead;i++) + { + db.remove("pad:"+padID+":chat:"+i); + } + + callback(); + }, + //delete all revisions + function(callback) + { + var revHead = _this.head; + + for(var i=0;i<=revHead;i++) + { + db.remove("pad:"+padID+":revs:"+i); + } + + callback(); + } + ], callback); + }, + //delete the pad entry and delete pad from padManager + function(callback) + { + db.remove("pad:"+padID); + padManager.unloadPad(padID); + callback(); + } + ], function(err) + { + if(ERR(err, callback)) return; + callback(); + }); +}; + //set in db +Pad.prototype.setPublicStatus = function setPublicStatus(publicStatus) { + this.publicStatus = publicStatus; + db.setSub("pad:"+this.id, ["publicStatus"], this.publicStatus); +}; + +Pad.prototype.setPassword = function setPassword(password) { + this.passwordHash = password == null ? null : hash(password, generateSalt()); + db.setSub("pad:"+this.id, ["passwordHash"], this.passwordHash); +}; + +Pad.prototype.isCorrectPassword = function isCorrectPassword(password) { + return compare(this.passwordHash, password); +}; + +Pad.prototype.isPasswordProtected = function isPasswordProtected() { + return this.passwordHash != null; +}; + +/* Crypto helper methods */ + +function hash(password, salt) +{ + var shasum = crypto.createHash('sha512'); + shasum.update(password + salt); + return shasum.digest("hex") + "$" + salt; +} + +function generateSalt() +{ + return randomString(86); +} + +function compare(hashStr, password) +{ + return hash(password, hashStr.split("$")[1]) === hashStr; +} diff --git a/src/node/db/PadManager.js b/src/node/db/PadManager.js new file mode 100644 index 00000000..4e3a3199 --- /dev/null +++ b/src/node/db/PadManager.js @@ -0,0 +1,165 @@ +/** + * The Pad Manager is a Factory for pad Objects + */ + +/* + * 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 customError = require("../utils/customError"); +var Pad = require("../db/Pad").Pad; +var db = require("./DB").db; + +/** + * An Object containing all known Pads. Provides "get" and "set" functions, + * which should be used instead of indexing with brackets. These prepend a + * colon to the key, to avoid conflicting with built-in Object methods or with + * these functions themselves. + * + * If this is needed in other places, it would be wise to make this a prototype + * that's defined somewhere more sensible. + */ +var globalPads = { + get: function (name) { return this[':'+name]; }, + set: function (name, value) { this[':'+name] = value; }, + remove: function (name) { delete this[':'+name]; } +}; + +/** + * An array of padId transformations. These represent changes in pad name policy over + * time, and allow us to "play back" these changes so legacy padIds can be found. + */ +var padIdTransforms = [ + [/\s+/g, '_'], + [/:+/g, '_'] +]; + +/** + * Returns a Pad Object with the callback + * @param id A String with the id of the pad + * @param {Function} callback + */ +exports.getPad = function(id, text, callback) +{ + //check if this is a valid padId + if(!exports.isValidPadId(id)) + { + callback(new customError(id + " is not a valid padId","apierror")); + return; + } + + //make text an optional parameter + if(typeof text == "function") + { + callback = text; + text = null; + } + + //check if this is a valid text + if(text != null) + { + //check if text is a string + if(typeof text != "string") + { + callback(new customError("text is not a string","apierror")); + return; + } + + //check if text is less than 100k chars + if(text.length > 100000) + { + callback(new customError("text must be less than 100k chars","apierror")); + return; + } + } + + var pad = globalPads.get(id); + + //return pad if its already loaded + if(pad != null) + { + callback(null, pad); + } + //try to load pad + else + { + pad = new Pad(id); + + //initalize the pad + pad.init(text, function(err) + { + if(ERR(err, callback)) return; + + globalPads.set(id, pad); + callback(null, pad); + }); + } +} + +//checks if a pad exists +exports.doesPadExists = function(padId, callback) +{ + db.get("pad:"+padId, function(err, value) + { + if(ERR(err, callback)) return; + callback(null, value != null && value.atext); + }); +} + +//returns a sanitized padId, respecting legacy pad id formats +exports.sanitizePadId = function(padId, callback) { + var transform_index = arguments[2] || 0; + //we're out of possible transformations, so just return it + if(transform_index >= padIdTransforms.length) + { + callback(padId); + } + //check if padId exists + else + { + exports.doesPadExists(padId, function(junk, exists) + { + if(exists) + { + callback(padId); + } + else + { + //get the next transformation *that's different* + var transformedPadId = padId; + while(transformedPadId == padId && transform_index < padIdTransforms.length) + { + transformedPadId = padId.replace(padIdTransforms[transform_index][0], padIdTransforms[transform_index][1]); + transform_index += 1; + } + //check the next transform + exports.sanitizePadId(transformedPadId, callback, transform_index); + } + }); + } +} + +exports.isValidPadId = function(padId) +{ + return /^(g.[a-zA-Z0-9]{16}\$)?[^$]{1,50}$/.test(padId); +} + +//removes a pad from the array +exports.unloadPad = function(padId) +{ + if(globalPads.get(padId)) + globalPads.remove(padId); +} diff --git a/src/node/db/ReadOnlyManager.js b/src/node/db/ReadOnlyManager.js new file mode 100644 index 00000000..34340630 --- /dev/null +++ b/src/node/db/ReadOnlyManager.js @@ -0,0 +1,74 @@ +/** + * The ReadOnlyManager manages the database and rendering releated to read only pads + */ + +/* + * 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 db = require("./DB").db; +var async = require("async"); +var randomString = require('ep_etherpad-lite/static/js/pad_utils').randomString; + +/** + * returns a read only id for a pad + * @param {String} padId the id of the pad + */ +exports.getReadOnlyId = function (padId, callback) +{ + var readOnlyId; + + async.waterfall([ + //check if there is a pad2readonly entry + function(callback) + { + db.get("pad2readonly:" + padId, callback); + }, + function(dbReadOnlyId, callback) + { + //there is no readOnly Entry in the database, let's create one + if(dbReadOnlyId == null) + { + readOnlyId = "r." + randomString(16); + + db.set("pad2readonly:" + padId, readOnlyId); + db.set("readonly2pad:" + readOnlyId, padId); + } + //there is a readOnly Entry in the database, let's take this one + else + { + readOnlyId = dbReadOnlyId; + } + + callback(); + } + ], function(err) + { + if(ERR(err, callback)) return; + //return the results + callback(null, readOnlyId); + }) +} + +/** + * returns a the padId for a read only id + * @param {String} readOnlyId read only id + */ +exports.getPadId = function(readOnlyId, callback) +{ + db.get("readonly2pad:" + readOnlyId, callback); +} diff --git a/src/node/db/SecurityManager.js b/src/node/db/SecurityManager.js new file mode 100644 index 00000000..a092453a --- /dev/null +++ b/src/node/db/SecurityManager.js @@ -0,0 +1,280 @@ +/** + * Controls the security of pad access + */ + +/* + * 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 db = require("./DB").db; +var async = require("async"); +var authorManager = require("./AuthorManager"); +var padManager = require("./PadManager"); +var sessionManager = require("./SessionManager"); +var settings = require("../utils/Settings") +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. + * @param padID the pad the user wants to access + * @param sesssionID the session the user has (set via api) + * @param token the token of the author (randomly generated at client side, used for public pads) + * @param password the password the user has given to access this pad, can be null + * @param callback will be called with (err, {accessStatus: grant|deny|wrongPassword|needPassword, authorID: a.xxxxxx}) + */ +exports.checkAccess = function (padID, sessionID, token, password, callback) +{ + var statusObject; + + // a valid session is required (api-only mode) + if(settings.requireSession) + { + // no sessionID, access is denied + if(!sessionID) + { + callback(null, {accessStatus: "deny"}); + return; + } + } + // a session is not required, so we'll check if it's a public pad + else + { + // it's not a group pad, means we can grant access + if(padID.indexOf("$") == -1) + { + //get author for this token + authorManager.getAuthor4Token(token, function(err, author) + { + if(ERR(err, callback)) return; + + // assume user has access + statusObject = {accessStatus: "grant", authorID: author}; + // user can't create pads + if(settings.editOnly) + { + // check if pad exists + padManager.doesPadExists(padID, function(err, exists) + { + if(ERR(err, callback)) return; + + // pad doesn't exist - user can't have access + if(!exists) statusObject.accessStatus = "deny"; + // grant or deny access, with author of token + callback(null, statusObject); + }); + } + // user may create new pads - no need to check anything + else + { + // grant access, with author of token + callback(null, statusObject); + } + }) + + //don't continue + return; + } + } + + var groupID = padID.split("$")[0]; + var padExists = false; + var validSession = false; + var sessionAuthor; + var tokenAuthor; + var isPublic; + var isPasswordProtected; + var passwordStatus = password == null ? "notGiven" : "wrong"; // notGiven, correct, wrong + + async.series([ + //get basic informations from the database + function(callback) + { + async.parallel([ + //does pad exists + function(callback) + { + padManager.doesPadExists(padID, function(err, exists) + { + if(ERR(err, callback)) return; + padExists = exists; + callback(); + }); + }, + //get informations about this session + function(callback) + { + sessionManager.getSessionInfo(sessionID, function(err, sessionInfo) + { + //skip session validation if the session doesn't exists + if(err && err.message == "sessionID does not exist") + { + callback(); + return; + } + + if(ERR(err, callback)) return; + + var now = Math.floor(new Date().getTime()/1000); + + //is it for this group? and is validUntil still ok? --> validSession + if(sessionInfo.groupID == groupID && sessionInfo.validUntil > now) + { + validSession = true; + } + + sessionAuthor = sessionInfo.authorID; + + callback(); + }); + }, + //get author for token + function(callback) + { + //get author for this token + authorManager.getAuthor4Token(token, function(err, author) + { + if(ERR(err, callback)) return; + tokenAuthor = author; + callback(); + }); + } + ], callback); + }, + //get more informations of this pad, if avaiable + function(callback) + { + //skip this if the pad doesn't exists + if(padExists == false) + { + callback(); + return; + } + + padManager.getPad(padID, function(err, pad) + { + if(ERR(err, callback)) return; + + //is it a public pad? + isPublic = pad.getPublicStatus(); + + //is it password protected? + isPasswordProtected = pad.isPasswordProtected(); + + //is password correct? + if(isPasswordProtected && password && pad.isCorrectPassword(password)) + { + passwordStatus = "correct"; + } + + callback(); + }); + }, + function(callback) + { + //- a valid session for this group is avaible AND pad exists + if(validSession && padExists) + { + //- the pad is not password protected + if(!isPasswordProtected) + { + //--> grant access + statusObject = {accessStatus: "grant", authorID: sessionAuthor}; + } + //- the pad is password protected and password is correct + else if(isPasswordProtected && passwordStatus == "correct") + { + //--> grant access + statusObject = {accessStatus: "grant", authorID: sessionAuthor}; + } + //- the pad is password protected but wrong password given + else if(isPasswordProtected && passwordStatus == "wrong") + { + //--> deny access, ask for new password and tell them that the password is wrong + statusObject = {accessStatus: "wrongPassword"}; + } + //- the pad is password protected but no password given + else if(isPasswordProtected && passwordStatus == "notGiven") + { + //--> ask for password + statusObject = {accessStatus: "needPassword"}; + } + else + { + throw new Error("Ops, something wrong happend"); + } + } + //- a valid session for this group avaible but pad doesn't exists + else if(validSession && !padExists) + { + //--> grant access + statusObject = {accessStatus: "grant", authorID: sessionAuthor}; + //--> deny access if user isn't allowed to create the pad + if(settings.editOnly) statusObject.accessStatus = "deny"; + } + // there is no valid session avaiable AND pad exists + else if(!validSession && padExists) + { + //-- its public and not password protected + if(isPublic && !isPasswordProtected) + { + //--> grant access, with author of token + statusObject = {accessStatus: "grant", authorID: tokenAuthor}; + } + //- its public and password protected and password is correct + else if(isPublic && isPasswordProtected && passwordStatus == "correct") + { + //--> grant access, with author of token + statusObject = {accessStatus: "grant", authorID: tokenAuthor}; + } + //- its public and the pad is password protected but wrong password given + else if(isPublic && isPasswordProtected && passwordStatus == "wrong") + { + //--> deny access, ask for new password and tell them that the password is wrong + statusObject = {accessStatus: "wrongPassword"}; + } + //- its public and the pad is password protected but no password given + else if(isPublic && isPasswordProtected && passwordStatus == "notGiven") + { + //--> ask for password + statusObject = {accessStatus: "needPassword"}; + } + //- its not public + else if(!isPublic) + { + //--> deny access + statusObject = {accessStatus: "deny"}; + } + else + { + throw new Error("Ops, something wrong happend"); + } + } + // there is no valid session avaiable AND pad doesn't exists + else + { + //--> deny access + statusObject = {accessStatus: "deny"}; + } + + callback(); + } + ], function(err) + { + if(ERR(err, callback)) return; + callback(null, statusObject); + }); +} diff --git a/src/node/db/SessionManager.js b/src/node/db/SessionManager.js new file mode 100644 index 00000000..ec4948a6 --- /dev/null +++ b/src/node/db/SessionManager.js @@ -0,0 +1,367 @@ +/** + * The Session Manager provides functions to manage session in the database + */ + +/* + * 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 customError = require("../utils/customError"); +var randomString = require('ep_etherpad-lite/static/js/pad_utils').randomString; +var db = require("./DB").db; +var async = require("async"); +var groupMangager = require("./GroupManager"); +var authorMangager = require("./AuthorManager"); + +exports.doesSessionExist = function(sessionID, callback) +{ + //check if the database entry of this session exists + db.get("session:" + sessionID, function (err, session) + { + if(ERR(err, callback)) return; + callback(null, session != null); + }); +} + +/** + * Creates a new session between an author and a group + */ +exports.createSession = function(groupID, authorID, validUntil, callback) +{ + var sessionID; + + async.series([ + //check if group exists + function(callback) + { + groupMangager.doesGroupExist(groupID, function(err, exists) + { + if(ERR(err, callback)) return; + + //group does not exist + if(exists == false) + { + callback(new customError("groupID does not exist","apierror")); + } + //everything is fine, continue + else + { + callback(); + } + }); + }, + //check if author exists + function(callback) + { + authorMangager.doesAuthorExists(authorID, function(err, exists) + { + if(ERR(err, callback)) return; + + //author does not exist + if(exists == false) + { + callback(new customError("authorID does not exist","apierror")); + } + //everything is fine, continue + else + { + callback(); + } + }); + }, + //check validUntil and create the session db entry + function(callback) + { + //check if rev is a number + if(typeof validUntil != "number") + { + //try to parse the number + if(!isNaN(parseInt(validUntil))) + { + validUntil = parseInt(validUntil); + } + else + { + callback(new customError("validUntil is not a number","apierror")); + return; + } + } + + //ensure this is not a negativ number + if(validUntil < 0) + { + callback(new customError("validUntil is a negativ number","apierror")); + return; + } + + //ensure this is not a float value + if(!is_int(validUntil)) + { + callback(new customError("validUntil is a float value","apierror")); + return; + } + + //check if validUntil is in the future + if(Math.floor(new Date().getTime()/1000) > validUntil) + { + callback(new customError("validUntil is in the past","apierror")); + return; + } + + //generate sessionID + sessionID = "s." + randomString(16); + + //set the session into the database + db.set("session:" + sessionID, {"groupID": groupID, "authorID": authorID, "validUntil": validUntil}); + + callback(); + }, + //set the group2sessions entry + function(callback) + { + //get the entry + db.get("group2sessions:" + groupID, function(err, group2sessions) + { + if(ERR(err, callback)) return; + + //the entry doesn't exist so far, let's create it + if(group2sessions == null) + { + group2sessions = {sessionIDs : {}}; + } + + //add the entry for this session + group2sessions.sessionIDs[sessionID] = 1; + + //save the new element back + db.set("group2sessions:" + groupID, group2sessions); + + callback(); + }); + }, + //set the author2sessions entry + function(callback) + { + //get the entry + db.get("author2sessions:" + authorID, function(err, author2sessions) + { + if(ERR(err, callback)) return; + + //the entry doesn't exist so far, let's create it + if(author2sessions == null) + { + author2sessions = {sessionIDs : {}}; + } + + //add the entry for this session + author2sessions.sessionIDs[sessionID] = 1; + + //save the new element back + db.set("author2sessions:" + authorID, author2sessions); + + callback(); + }); + } + ], function(err) + { + if(ERR(err, callback)) return; + + //return error and sessionID + callback(null, {sessionID: sessionID}); + }) +} + +exports.getSessionInfo = function(sessionID, callback) +{ + //check if the database entry of this session exists + db.get("session:" + sessionID, function (err, session) + { + if(ERR(err, callback)) return; + + //session does not exists + if(session == null) + { + callback(new customError("sessionID does not exist","apierror")) + } + //everything is fine, return the sessioninfos + else + { + callback(null, session); + } + }); +} + +/** + * Deletes a session + */ +exports.deleteSession = function(sessionID, callback) +{ + var authorID, groupID; + var group2sessions, author2sessions; + + async.series([ + function(callback) + { + //get the session entry + db.get("session:" + sessionID, function (err, session) + { + if(ERR(err, callback)) return; + + //session does not exists + if(session == null) + { + callback(new customError("sessionID does not exist","apierror")) + } + //everything is fine, return the sessioninfos + else + { + authorID = session.authorID; + groupID = session.groupID; + + callback(); + } + }); + }, + //get the group2sessions entry + function(callback) + { + db.get("group2sessions:" + groupID, function (err, _group2sessions) + { + if(ERR(err, callback)) return; + group2sessions = _group2sessions; + callback(); + }); + }, + //get the author2sessions entry + function(callback) + { + db.get("author2sessions:" + authorID, function (err, _author2sessions) + { + if(ERR(err, callback)) return; + author2sessions = _author2sessions; + callback(); + }); + }, + //remove the values from the database + function(callback) + { + //remove the session + db.remove("session:" + sessionID); + + //remove session from group2sessions + delete group2sessions.sessionIDs[sessionID]; + db.set("group2sessions:" + groupID, group2sessions); + + //remove session from author2sessions + delete author2sessions.sessionIDs[sessionID]; + db.set("author2sessions:" + authorID, author2sessions); + + callback(); + } + ], function(err) + { + if(ERR(err, callback)) return; + callback(); + }) +} + +exports.listSessionsOfGroup = function(groupID, callback) +{ + groupMangager.doesGroupExist(groupID, function(err, exists) + { + if(ERR(err, callback)) return; + + //group does not exist + if(exists == false) + { + callback(new customError("groupID does not exist","apierror")); + } + //everything is fine, continue + else + { + listSessionsWithDBKey("group2sessions:" + groupID, callback); + } + }); +} + +exports.listSessionsOfAuthor = function(authorID, callback) +{ + authorMangager.doesAuthorExists(authorID, function(err, exists) + { + if(ERR(err, callback)) return; + + //group does not exist + if(exists == false) + { + callback(new customError("authorID does not exist","apierror")); + } + //everything is fine, continue + else + { + listSessionsWithDBKey("author2sessions:" + authorID, callback); + } + }); +} + +//this function is basicly the code listSessionsOfAuthor and listSessionsOfGroup has in common +function listSessionsWithDBKey (dbkey, callback) +{ + var sessions; + + async.series([ + function(callback) + { + //get the group2sessions entry + db.get(dbkey, function(err, sessionObject) + { + if(ERR(err, callback)) return; + sessions = sessionObject ? sessionObject.sessionIDs : null; + callback(); + }); + }, + function(callback) + { + //collect all sessionIDs in an arrary + var sessionIDs = []; + for (var i in sessions) + { + sessionIDs.push(i); + } + + //foreach trough the sessions and get the sessioninfos + async.forEach(sessionIDs, function(sessionID, callback) + { + exports.getSessionInfo(sessionID, function(err, sessionInfo) + { + if(ERR(err, callback)) return; + sessions[sessionID] = sessionInfo; + callback(); + }); + }, callback); + } + ], function(err) + { + if(ERR(err, callback)) return; + callback(null, sessions); + }); +} + +//checks if a number is an int +function is_int(value) +{ + return (parseFloat(value) == parseInt(value)) && !isNaN(value) +} diff --git a/src/node/easysync_tests.js b/src/node/easysync_tests.js new file mode 100644 index 00000000..7a148289 --- /dev/null +++ b/src/node/easysync_tests.js @@ -0,0 +1,949 @@ +/** + * I found this tests in the old Etherpad and used it to test if the Changeset library can be run on node.js. + * It has no use for ep-lite, but I thought I keep it cause it may help someone to understand the Changeset library + * https://github.com/ether/pad/blob/master/infrastructure/ace/www/easysync2_tests.js + */ + +/* + * 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. + */ + + +var Changeset = require("ep_etherpad-lite/static/js/Changeset"); +var AttributePoolFactory = require("ep_etherpad-lite/static/js/AttributePoolFactory"); + +function random() { + this.nextInt = function (maxValue) { + return Math.floor(Math.random() * maxValue); + } + + this.nextDouble = function (maxValue) { + return Math.random(); + } +} + +function runTests() { + + function print(str) { + console.log(str); + } + + function assert(code, optMsg) { + if (!eval(code)) throw new Error("FALSE: " + (optMsg || code)); + } + + function literal(v) { + if ((typeof v) == "string") { + return '"' + v.replace(/[\\\"]/g, '\\$1').replace(/\n/g, '\\n') + '"'; + } else + return JSON.stringify(v); + } + + function assertEqualArrays(a, b) { + assert("JSON.stringify(" + literal(a) + ") == JSON.stringify(" + literal(b) + ")"); + } + + function assertEqualStrings(a, b) { + assert(literal(a) + " == " + literal(b)); + } + + function throughIterator(opsStr) { + var iter = Changeset.opIterator(opsStr); + var assem = Changeset.opAssembler(); + while (iter.hasNext()) { + assem.append(iter.next()); + } + return assem.toString(); + } + + function throughSmartAssembler(opsStr) { + var iter = Changeset.opIterator(opsStr); + var assem = Changeset.smartOpAssembler(); + while (iter.hasNext()) { + assem.append(iter.next()); + } + assem.endDocument(); + return assem.toString(); + } + + (function () { + print("> throughIterator"); + var x = '-c*3*4+6|3=az*asdf0*1*2*3+1=1-1+1*0+1=1-1+1|c=c-1'; + assert("throughIterator(" + literal(x) + ") == " + literal(x)); + })(); + + (function () { + print("> throughSmartAssembler"); + var x = '-c*3*4+6|3=az*asdf0*1*2*3+1=1-1+1*0+1=1-1+1|c=c-1'; + assert("throughSmartAssembler(" + literal(x) + ") == " + literal(x)); + })(); + + function applyMutations(mu, arrayOfArrays) { + arrayOfArrays.forEach(function (a) { + var result = mu[a[0]].apply(mu, a.slice(1)); + if (a[0] == 'remove' && a[3]) { + assertEqualStrings(a[3], result); + } + }); + } + + function mutationsToChangeset(oldLen, arrayOfArrays) { + var assem = Changeset.smartOpAssembler(); + var op = Changeset.newOp(); + var bank = Changeset.stringAssembler(); + var oldPos = 0; + var newLen = 0; + arrayOfArrays.forEach(function (a) { + if (a[0] == 'skip') { + op.opcode = '='; + op.chars = a[1]; + op.lines = (a[2] || 0); + assem.append(op); + oldPos += op.chars; + newLen += op.chars; + } else if (a[0] == 'remove') { + op.opcode = '-'; + op.chars = a[1]; + op.lines = (a[2] || 0); + assem.append(op); + oldPos += op.chars; + } else if (a[0] == 'insert') { + op.opcode = '+'; + bank.append(a[1]); + op.chars = a[1].length; + op.lines = (a[2] || 0); + assem.append(op); + newLen += op.chars; + } + }); + newLen += oldLen - oldPos; + assem.endDocument(); + return Changeset.pack(oldLen, newLen, assem.toString(), bank.toString()); + } + + function runMutationTest(testId, origLines, muts, correct) { + print("> runMutationTest#" + testId); + var lines = origLines.slice(); + var mu = Changeset.textLinesMutator(lines); + applyMutations(mu, muts); + mu.close(); + assertEqualArrays(correct, lines); + + var inText = origLines.join(''); + var cs = mutationsToChangeset(inText.length, muts); + lines = origLines.slice(); + Changeset.mutateTextLines(cs, lines); + assertEqualArrays(correct, lines); + + var correctText = correct.join(''); + //print(literal(cs)); + var outText = Changeset.applyToText(cs, inText); + assertEqualStrings(correctText, outText); + } + + runMutationTest(1, ["apple\n", "banana\n", "cabbage\n", "duffle\n", "eggplant\n"], [ + ['remove', 1, 0, "a"], + ['insert', "tu"], + ['remove', 1, 0, "p"], + ['skip', 4, 1], + ['skip', 7, 1], + ['insert', "cream\npie\n", 2], + ['skip', 2], + ['insert', "bot"], + ['insert', "\n", 1], + ['insert', "bu"], + ['skip', 3], + ['remove', 3, 1, "ge\n"], + ['remove', 6, 0, "duffle"] + ], ["tuple\n", "banana\n", "cream\n", "pie\n", "cabot\n", "bubba\n", "eggplant\n"]); + + runMutationTest(2, ["apple\n", "banana\n", "cabbage\n", "duffle\n", "eggplant\n"], [ + ['remove', 1, 0, "a"], + ['remove', 1, 0, "p"], + ['insert', "tu"], + ['skip', 11, 2], + ['insert', "cream\npie\n", 2], + ['skip', 2], + ['insert', "bot"], + ['insert', "\n", 1], + ['insert', "bu"], + ['skip', 3], + ['remove', 3, 1, "ge\n"], + ['remove', 6, 0, "duffle"] + ], ["tuple\n", "banana\n", "cream\n", "pie\n", "cabot\n", "bubba\n", "eggplant\n"]); + + runMutationTest(3, ["apple\n", "banana\n", "cabbage\n", "duffle\n", "eggplant\n"], [ + ['remove', 6, 1, "apple\n"], + ['skip', 15, 2], + ['skip', 6], + ['remove', 1, 1, "\n"], + ['remove', 8, 0, "eggplant"], + ['skip', 1, 1] + ], ["banana\n", "cabbage\n", "duffle\n"]); + + runMutationTest(4, ["15\n"], [ + ['skip', 1], + ['insert', "\n2\n3\n4\n", 4], + ['skip', 2, 1] + ], ["1\n", "2\n", "3\n", "4\n", "5\n"]); + + runMutationTest(5, ["1\n", "2\n", "3\n", "4\n", "5\n"], [ + ['skip', 1], + ['remove', 7, 4, "\n2\n3\n4\n"], + ['skip', 2, 1] + ], ["15\n"]); + + runMutationTest(6, ["123\n", "abc\n", "def\n", "ghi\n", "xyz\n"], [ + ['insert', "0"], + ['skip', 4, 1], + ['skip', 4, 1], + ['remove', 8, 2, "def\nghi\n"], + ['skip', 4, 1] + ], ["0123\n", "abc\n", "xyz\n"]); + + runMutationTest(7, ["apple\n", "banana\n", "cabbage\n", "duffle\n", "eggplant\n"], [ + ['remove', 6, 1, "apple\n"], + ['skip', 15, 2, true], + ['skip', 6, 0, true], + ['remove', 1, 1, "\n"], + ['remove', 8, 0, "eggplant"], + ['skip', 1, 1, true] + ], ["banana\n", "cabbage\n", "duffle\n"]); + + function poolOrArray(attribs) { + if (attribs.getAttrib) { + return attribs; // it's already an attrib pool + } else { + // assume it's an array of attrib strings to be split and added + var p = AttributePoolFactory.createAttributePool(); + attribs.forEach(function (kv) { + p.putAttrib(kv.split(',')); + }); + return p; + } + } + + function runApplyToAttributionTest(testId, attribs, cs, inAttr, outCorrect) { + print("> applyToAttribution#" + testId); + var p = poolOrArray(attribs); + var result = Changeset.applyToAttribution( + Changeset.checkRep(cs), inAttr, p); + assertEqualStrings(outCorrect, result); + } + + // turn c<b>a</b>ctus\n into a<b>c</b>tusabcd\n + runApplyToAttributionTest(1, ['bold,', 'bold,true'], "Z:7>3-1*0=1*1=1=3+4$abcd", "+1*1+1|1+5", "+1*1+1|1+8"); + + // turn "david\ngreenspan\n" into "<b>david\ngreen</b>\n" + runApplyToAttributionTest(2, ['bold,', 'bold,true'], "Z:g<4*1|1=6*1=5-4$", "|2+g", "*1|1+6*1+5|1+1"); + + (function () { + print("> mutatorHasMore"); + var lines = ["1\n", "2\n", "3\n", "4\n"]; + var mu; + + mu = Changeset.textLinesMutator(lines); + assert(mu.hasMore() + ' == true'); + mu.skip(8, 4); + assert(mu.hasMore() + ' == false'); + mu.close(); + assert(mu.hasMore() + ' == false'); + + // still 1,2,3,4 + mu = Changeset.textLinesMutator(lines); + assert(mu.hasMore() + ' == true'); + mu.remove(2, 1); + assert(mu.hasMore() + ' == true'); + mu.skip(2, 1); + assert(mu.hasMore() + ' == true'); + mu.skip(2, 1); + assert(mu.hasMore() + ' == true'); + mu.skip(2, 1); + assert(mu.hasMore() + ' == false'); + mu.insert("5\n", 1); + assert(mu.hasMore() + ' == false'); + mu.close(); + assert(mu.hasMore() + ' == false'); + + // 2,3,4,5 now + mu = Changeset.textLinesMutator(lines); + assert(mu.hasMore() + ' == true'); + mu.remove(6, 3); + assert(mu.hasMore() + ' == true'); + mu.remove(2, 1); + assert(mu.hasMore() + ' == false'); + mu.insert("hello\n", 1); + assert(mu.hasMore() + ' == false'); + mu.close(); + assert(mu.hasMore() + ' == false'); + + })(); + + function runMutateAttributionTest(testId, attribs, cs, alines, outCorrect) { + print("> runMutateAttributionTest#" + testId); + var p = poolOrArray(attribs); + var alines2 = Array.prototype.slice.call(alines); + var result = Changeset.mutateAttributionLines( + Changeset.checkRep(cs), alines2, p); + assertEqualArrays(outCorrect, alines2); + + print("> runMutateAttributionTest#" + testId + ".applyToAttribution"); + + function removeQuestionMarks(a) { + return a.replace(/\?/g, ''); + } + var inMerged = Changeset.joinAttributionLines(alines.map(removeQuestionMarks)); + var correctMerged = Changeset.joinAttributionLines(outCorrect.map(removeQuestionMarks)); + var mergedResult = Changeset.applyToAttribution(cs, inMerged, p); + assertEqualStrings(correctMerged, mergedResult); + } + + // turn 123\n 456\n 789\n into 123\n 4<b>5</b>6\n 789\n + runMutateAttributionTest(1, ["bold,true"], "Z:c>0|1=4=1*0=1$", ["|1+4", "|1+4", "|1+4"], ["|1+4", "+1*0+1|1+2", "|1+4"]); + + // make a document bold + runMutateAttributionTest(2, ["bold,true"], "Z:c>0*0|3=c$", ["|1+4", "|1+4", "|1+4"], ["*0|1+4", "*0|1+4", "*0|1+4"]); + + // clear bold on document + runMutateAttributionTest(3, ["bold,", "bold,true"], "Z:c>0*0|3=c$", ["*1+1+1*1+1|1+1", "+1*1+1|1+2", "*1+1+1*1+1|1+1"], ["|1+4", "|1+4", "|1+4"]); + + // add a character on line 3 of a document with 5 blank lines, and make sure + // the optimization that skips purely-kept lines is working; if any attribution string + // with a '?' is parsed it will cause an error. + 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 p = AttributePoolFactory.createAttributePool(); + p.putAttrib(['char', 'newline']); + for (var i = 1; i < 36; i++) { + p.putAttrib(['char', Changeset.numToString(i)]); + } + p.putAttrib(['char', '']); + return p; + })(); + + // based on runMutationTest#1 + runMutateAttributionTest(5, testPoolWithChars, "Z:11>7-2*t+1*u+1|2=b|2+a=2*b+1*o+1*t+1*0|1+1*b+1*u+1=3|1-3-6$" + "tucream\npie\nbot\nbu", ["*a+1*p+2*l+1*e+1*0|1+1", "*b+1*a+1*n+1*a+1*n+1*a+1*0|1+1", "*c+1*a+1*b+2*a+1*g+1*e+1*0|1+1", "*d+1*u+1*f+2*l+1*e+1*0|1+1", "*e+1*g+2*p+1*l+1*a+1*n+1*t+1*0|1+1"], ["*t+1*u+1*p+1*l+1*e+1*0|1+1", "*b+1*a+1*n+1*a+1*n+1*a+1*0|1+1", "|1+6", "|1+4", "*c+1*a+1*b+1*o+1*t+1*0|1+1", "*b+1*u+1*b+2*a+1*0|1+1", "*e+1*g+2*p+1*l+1*a+1*n+1*t+1*0|1+1"]); + + // based on runMutationTest#3 + runMutateAttributionTest(6, testPoolWithChars, "Z:11<f|1-6|2=f=6|1-1-8$", ["*a|1+6", "*b|1+7", "*c|1+8", "*d|1+7", "*e|1+9"], ["*b|1+7", "*c|1+8", "*d+6*e|1+1"]); + + // based on runMutationTest#4 + runMutateAttributionTest(7, testPoolWithChars, "Z:3>7=1|4+7$\n2\n3\n4\n", ["*1+1*5|1+2"], ["*1+1|1+1", "|1+2", "|1+2", "|1+2", "*5|1+2"]); + + // based on runMutationTest#5 + runMutateAttributionTest(8, testPoolWithChars, "Z:a<7=1|4-7$", ["*1|1+2", "*2|1+2", "*3|1+2", "*4|1+2", "*5|1+2"], ["*1+1*5|1+2"]); + + // based on runMutationTest#6 + runMutateAttributionTest(9, testPoolWithChars, "Z:k<7*0+1*10|2=8|2-8$0", ["*1+1*2+1*3+1|1+1", "*a+1*b+1*c+1|1+1", "*d+1*e+1*f+1|1+1", "*g+1*h+1*i+1|1+1", "?*x+1*y+1*z+1|1+1"], ["*0+1|1+4", "|1+4", "?*x+1*y+1*z+1|1+1"]); + + runMutateAttributionTest(10, testPoolWithChars, "Z:6>4=1+1=1+1|1=1+1=1*0+1$abcd", ["|1+3", "|1+3"], ["|1+5", "+2*0+1|1+2"]); + + + runMutateAttributionTest(11, testPoolWithChars, "Z:s>1|1=4=6|1+1$\n", ["*0|1+4", "*0|1+8", "*0+5|1+1", "*0|1+1", "*0|1+5", "*0|1+1", "*0|1+1", "*0|1+1", "|1+1"], ["*0|1+4", "*0+6|1+1", "*0|1+2", "*0+5|1+1", "*0|1+1", "*0|1+5", "*0|1+1", "*0|1+1", "*0|1+1", "|1+1"]); + + function randomInlineString(len, rand) { + var assem = Changeset.stringAssembler(); + for (var i = 0; i < len; i++) { + assem.append(String.fromCharCode(rand.nextInt(26) + 97)); + } + return assem.toString(); + } + + function randomMultiline(approxMaxLines, approxMaxCols, rand) { + var numParts = rand.nextInt(approxMaxLines * 2) + 1; + var txt = Changeset.stringAssembler(); + txt.append(rand.nextInt(2) ? '\n' : ''); + for (var i = 0; i < numParts; i++) { + if ((i % 2) == 0) { + if (rand.nextInt(10)) { + txt.append(randomInlineString(rand.nextInt(approxMaxCols) + 1, rand)); + } else { + txt.append('\n'); + } + } else { + txt.append('\n'); + } + } + return txt.toString(); + } + + function randomStringOperation(numCharsLeft, rand) { + var result; + switch (rand.nextInt(9)) { + case 0: + { + // insert char + result = { + insert: randomInlineString(1, rand) + }; + break; + } + case 1: + { + // delete char + result = { + remove: 1 + }; + break; + } + case 2: + { + // skip char + result = { + skip: 1 + }; + break; + } + case 3: + { + // insert small + result = { + insert: randomInlineString(rand.nextInt(4) + 1, rand) + }; + break; + } + case 4: + { + // delete small + result = { + remove: rand.nextInt(4) + 1 + }; + break; + } + case 5: + { + // skip small + result = { + skip: rand.nextInt(4) + 1 + }; + break; + } + case 6: + { + // insert multiline; + result = { + insert: randomMultiline(5, 20, rand) + }; + break; + } + case 7: + { + // delete multiline + result = { + remove: Math.round(numCharsLeft * rand.nextDouble() * rand.nextDouble()) + }; + break; + } + case 8: + { + // skip multiline + result = { + skip: Math.round(numCharsLeft * rand.nextDouble() * rand.nextDouble()) + }; + break; + } + case 9: + { + // delete to end + result = { + remove: numCharsLeft + }; + break; + } + case 10: + { + // skip to end + result = { + skip: numCharsLeft + }; + break; + } + } + var maxOrig = numCharsLeft - 1; + if ('remove' in result) { + result.remove = Math.min(result.remove, maxOrig); + } else if ('skip' in result) { + result.skip = Math.min(result.skip, maxOrig); + } + return result; + } + + function randomTwoPropAttribs(opcode, rand) { + // assumes attrib pool like ['apple,','apple,true','banana,','banana,true'] + if (opcode == '-' || rand.nextInt(3)) { + return ''; + } else if (rand.nextInt(3)) { + if (opcode == '+' || rand.nextInt(2)) { + return '*' + Changeset.numToString(rand.nextInt(2) * 2 + 1); + } else { + return '*' + Changeset.numToString(rand.nextInt(2) * 2); + } + } else { + if (opcode == '+' || rand.nextInt(4) == 0) { + return '*1*3'; + } else { + return ['*0*2', '*0*3', '*1*2'][rand.nextInt(3)]; + } + } + } + + function randomTestChangeset(origText, rand, withAttribs) { + var charBank = Changeset.stringAssembler(); + var textLeft = origText; // always keep final newline + var outTextAssem = Changeset.stringAssembler(); + var opAssem = Changeset.smartOpAssembler(); + var oldLen = origText.length; + + var nextOp = Changeset.newOp(); + + function appendMultilineOp(opcode, txt) { + nextOp.opcode = opcode; + if (withAttribs) { + nextOp.attribs = randomTwoPropAttribs(opcode, rand); + } + txt.replace(/\n|[^\n]+/g, function (t) { + if (t == '\n') { + nextOp.chars = 1; + nextOp.lines = 1; + opAssem.append(nextOp); + } else { + nextOp.chars = t.length; + nextOp.lines = 0; + opAssem.append(nextOp); + } + return ''; + }); + } + + function doOp() { + var o = randomStringOperation(textLeft.length, rand); + if (o.insert) { + var txt = o.insert; + charBank.append(txt); + outTextAssem.append(txt); + appendMultilineOp('+', txt); + } else if (o.skip) { + var txt = textLeft.substring(0, o.skip); + textLeft = textLeft.substring(o.skip); + outTextAssem.append(txt); + appendMultilineOp('=', txt); + } else if (o.remove) { + var txt = textLeft.substring(0, o.remove); + textLeft = textLeft.substring(o.remove); + appendMultilineOp('-', txt); + } + } + + while (textLeft.length > 1) doOp(); + for (var i = 0; i < 5; i++) doOp(); // do some more (only insertions will happen) + var outText = outTextAssem.toString() + '\n'; + opAssem.endDocument(); + var cs = Changeset.pack(oldLen, outText.length, opAssem.toString(), charBank.toString()); + Changeset.checkRep(cs); + return [cs, outText]; + } + + function testCompose(randomSeed) { + var rand = new random(); + print("> testCompose#" + randomSeed); + + var p = AttributePoolFactory.createAttributePool(); + + var startText = randomMultiline(10, 20, rand) + '\n'; + + var x1 = randomTestChangeset(startText, rand); + var change1 = x1[0]; + var text1 = x1[1]; + + var x2 = randomTestChangeset(text1, rand); + var change2 = x2[0]; + var text2 = x2[1]; + + var x3 = randomTestChangeset(text2, rand); + var change3 = x3[0]; + var text3 = x3[1]; + + //print(literal(Changeset.toBaseTen(startText))); + //print(literal(Changeset.toBaseTen(change1))); + //print(literal(Changeset.toBaseTen(change2))); + var change12 = Changeset.checkRep(Changeset.compose(change1, change2, p)); + var change23 = Changeset.checkRep(Changeset.compose(change2, change3, p)); + var change123 = Changeset.checkRep(Changeset.compose(change12, change3, p)); + var change123a = Changeset.checkRep(Changeset.compose(change1, change23, p)); + assertEqualStrings(change123, change123a); + + assertEqualStrings(text2, Changeset.applyToText(change12, startText)); + assertEqualStrings(text3, Changeset.applyToText(change23, text1)); + assertEqualStrings(text3, Changeset.applyToText(change123, startText)); + } + + for (var i = 0; i < 30; i++) testCompose(i); + + (function simpleComposeAttributesTest() { + print("> simpleComposeAttributesTest"); + var p = AttributePoolFactory.createAttributePool(); + p.putAttrib(['bold', '']); + p.putAttrib(['bold', 'true']); + var cs1 = Changeset.checkRep("Z:2>1*1+1*1=1$x"); + var cs2 = Changeset.checkRep("Z:3>0*0|1=3$"); + var cs12 = Changeset.checkRep(Changeset.compose(cs1, cs2, p)); + assertEqualStrings("Z:2>1+1*0|1=2$x", cs12); + })(); + + (function followAttributesTest() { + var p = AttributePoolFactory.createAttributePool(); + p.putAttrib(['x', '']); + p.putAttrib(['x', 'abc']); + p.putAttrib(['x', 'def']); + p.putAttrib(['y', '']); + p.putAttrib(['y', 'abc']); + p.putAttrib(['y', 'def']); + + function testFollow(a, b, afb, bfa, merge) { + assertEqualStrings(afb, Changeset.followAttributes(a, b, p)); + assertEqualStrings(bfa, Changeset.followAttributes(b, a, p)); + assertEqualStrings(merge, Changeset.composeAttributes(a, afb, true, p)); + assertEqualStrings(merge, Changeset.composeAttributes(b, bfa, true, p)); + } + + testFollow('', '', '', '', ''); + testFollow('*0', '', '', '*0', '*0'); + testFollow('*0', '*0', '', '', '*0'); + testFollow('*0', '*1', '', '*0', '*0'); + testFollow('*1', '*2', '', '*1', '*1'); + testFollow('*0*1', '', '', '*0*1', '*0*1'); + testFollow('*0*4', '*2*3', '*3', '*0', '*0*3'); + testFollow('*0*4', '*2', '', '*0*4', '*0*4'); + })(); + + function testFollow(randomSeed) { + var rand = new random(); + print("> testFollow#" + randomSeed); + + var p = AttributePoolFactory.createAttributePool(); + + var startText = randomMultiline(10, 20, rand) + '\n'; + + var cs1 = randomTestChangeset(startText, rand)[0]; + var cs2 = randomTestChangeset(startText, rand)[0]; + + var afb = Changeset.checkRep(Changeset.follow(cs1, cs2, false, p)); + var bfa = Changeset.checkRep(Changeset.follow(cs2, cs1, true, p)); + + var merge1 = Changeset.checkRep(Changeset.compose(cs1, afb)); + var merge2 = Changeset.checkRep(Changeset.compose(cs2, bfa)); + + assertEqualStrings(merge1, merge2); + } + + for (var i = 0; i < 30; i++) testFollow(i); + + function testSplitJoinAttributionLines(randomSeed) { + var rand = new random(); + print("> testSplitJoinAttributionLines#" + randomSeed); + + var doc = randomMultiline(10, 20, rand) + '\n'; + + function stringToOps(str) { + var assem = Changeset.mergingOpAssembler(); + var o = Changeset.newOp('+'); + o.chars = 1; + for (var i = 0; i < str.length; i++) { + var c = str.charAt(i); + o.lines = (c == '\n' ? 1 : 0); + o.attribs = (c == 'a' || c == 'b' ? '*' + c : ''); + assem.append(o); + } + return assem.toString(); + } + + var theJoined = stringToOps(doc); + var theSplit = doc.match(/[^\n]*\n/g).map(stringToOps); + + assertEqualArrays(theSplit, Changeset.splitAttributionLines(theJoined, doc)); + assertEqualStrings(theJoined, Changeset.joinAttributionLines(theSplit)); + } + + for (var i = 0; i < 10; i++) testSplitJoinAttributionLines(i); + + (function testMoveOpsToNewPool() { + print("> testMoveOpsToNewPool"); + + var pool1 = AttributePoolFactory.createAttributePool(); + var pool2 = AttributePoolFactory.createAttributePool(); + + pool1.putAttrib(['baz', 'qux']); + pool1.putAttrib(['foo', 'bar']); + + pool2.putAttrib(['foo', 'bar']); + + assertEqualStrings(Changeset.moveOpsToNewPool('Z:1>2*1+1*0+1$ab', pool1, pool2), 'Z:1>2*0+1*1+1$ab'); + assertEqualStrings(Changeset.moveOpsToNewPool('*1+1*0+1', pool1, pool2), '*0+1*1+1'); + })(); + + + (function testMakeSplice() { + print("> testMakeSplice"); + + var t = "a\nb\nc\n"; + var t2 = Changeset.applyToText(Changeset.makeSplice(t, 5, 0, "def"), t); + assertEqualStrings("a\nb\ncdef\n", t2); + + })(); + + (function testToSplices() { + print("> testToSplices"); + + var cs = Changeset.checkRep('Z:z>9*0=1=4-3+9=1|1-4-4+1*0+a$123456789abcdefghijk'); + var correctSplices = [ + [5, 8, "123456789"], + [9, 17, "abcdefghijk"] + ]; + assertEqualArrays(correctSplices, Changeset.toSplices(cs)); + })(); + + function testCharacterRangeFollow(testId, cs, oldRange, insertionsAfter, correctNewRange) { + print("> testCharacterRangeFollow#" + testId); + + var cs = Changeset.checkRep(cs); + assertEqualArrays(correctNewRange, Changeset.characterRangeFollow(cs, oldRange[0], oldRange[1], insertionsAfter)); + + } + + testCharacterRangeFollow(1, 'Z:z>9*0=1=4-3+9=1|1-4-4+1*0+a$123456789abcdefghijk', [7, 10], false, [14, 15]); + testCharacterRangeFollow(2, "Z:bc<6|x=b4|2-6$", [400, 407], false, [400, 401]); + testCharacterRangeFollow(3, "Z:4>0-3+3$abc", [0, 3], false, [3, 3]); + testCharacterRangeFollow(4, "Z:4>0-3+3$abc", [0, 3], true, [0, 0]); + testCharacterRangeFollow(5, "Z:5>1+1=1-3+3$abcd", [1, 4], false, [5, 5]); + testCharacterRangeFollow(6, "Z:5>1+1=1-3+3$abcd", [1, 4], true, [2, 2]); + testCharacterRangeFollow(7, "Z:5>1+1=1-3+3$abcd", [0, 6], false, [1, 7]); + testCharacterRangeFollow(8, "Z:5>1+1=1-3+3$abcd", [0, 3], false, [1, 2]); + testCharacterRangeFollow(9, "Z:5>1+1=1-3+3$abcd", [2, 5], false, [5, 6]); + testCharacterRangeFollow(10, "Z:2>1+1$a", [0, 0], false, [1, 1]); + testCharacterRangeFollow(11, "Z:2>1+1$a", [0, 0], true, [0, 0]); + + (function testOpAttributeValue() { + print("> testOpAttributeValue"); + + var p = AttributePoolFactory.createAttributePool(); + p.putAttrib(['name', 'david']); + p.putAttrib(['color', 'green']); + + assertEqualStrings("david", Changeset.opAttributeValue(Changeset.stringOp('*0*1+1'), 'name', p)); + assertEqualStrings("david", Changeset.opAttributeValue(Changeset.stringOp('*0+1'), 'name', p)); + assertEqualStrings("", Changeset.opAttributeValue(Changeset.stringOp('*1+1'), 'name', p)); + assertEqualStrings("", Changeset.opAttributeValue(Changeset.stringOp('+1'), 'name', p)); + assertEqualStrings("green", Changeset.opAttributeValue(Changeset.stringOp('*0*1+1'), 'color', p)); + assertEqualStrings("green", Changeset.opAttributeValue(Changeset.stringOp('*1+1'), 'color', p)); + assertEqualStrings("", Changeset.opAttributeValue(Changeset.stringOp('*0+1'), 'color', p)); + assertEqualStrings("", Changeset.opAttributeValue(Changeset.stringOp('+1'), 'color', p)); + })(); + + function testAppendATextToAssembler(testId, atext, correctOps) { + print("> testAppendATextToAssembler#" + testId); + + var assem = Changeset.smartOpAssembler(); + Changeset.appendATextToAssembler(atext, assem); + assertEqualStrings(correctOps, assem.toString()); + } + + testAppendATextToAssembler(1, { + text: "\n", + attribs: "|1+1" + }, ""); + testAppendATextToAssembler(2, { + text: "\n\n", + attribs: "|2+2" + }, "|1+1"); + testAppendATextToAssembler(3, { + text: "\n\n", + attribs: "*x|2+2" + }, "*x|1+1"); + testAppendATextToAssembler(4, { + text: "\n\n", + attribs: "*x|1+1|1+1" + }, "*x|1+1"); + testAppendATextToAssembler(5, { + text: "foo\n", + attribs: "|1+4" + }, "+3"); + testAppendATextToAssembler(6, { + text: "\nfoo\n", + attribs: "|2+5" + }, "|1+1+3"); + testAppendATextToAssembler(7, { + text: "\nfoo\n", + attribs: "*x|2+5" + }, "*x|1+1*x+3"); + testAppendATextToAssembler(8, { + text: "\n\n\nfoo\n", + attribs: "|2+2*x|2+5" + }, "|2+2*x|1+1*x+3"); + + function testMakeAttribsString(testId, pool, opcode, attribs, correctString) { + print("> testMakeAttribsString#" + testId); + + var p = poolOrArray(pool); + var str = Changeset.makeAttribsString(opcode, attribs, p); + assertEqualStrings(correctString, str); + } + + testMakeAttribsString(1, ['bold,'], '+', [ + ['bold', ''] + ], ''); + testMakeAttribsString(2, ['abc,def', 'bold,'], '=', [ + ['bold', ''] + ], '*1'); + testMakeAttribsString(3, ['abc,def', 'bold,true'], '+', [ + ['abc', 'def'], + ['bold', 'true'] + ], '*0*1'); + testMakeAttribsString(4, ['abc,def', 'bold,true'], '+', [ + ['bold', 'true'], + ['abc', 'def'] + ], '*0*1'); + + function testSubattribution(testId, astr, start, end, correctOutput) { + print("> testSubattribution#" + testId); + + var str = Changeset.subattribution(astr, start, end); + assertEqualStrings(correctOutput, str); + } + + testSubattribution(1, "+1", 0, 0, ""); + testSubattribution(2, "+1", 0, 1, "+1"); + testSubattribution(3, "+1", 0, undefined, "+1"); + testSubattribution(4, "|1+1", 0, 0, ""); + testSubattribution(5, "|1+1", 0, 1, "|1+1"); + testSubattribution(6, "|1+1", 0, undefined, "|1+1"); + testSubattribution(7, "*0+1", 0, 0, ""); + testSubattribution(8, "*0+1", 0, 1, "*0+1"); + testSubattribution(9, "*0+1", 0, undefined, "*0+1"); + testSubattribution(10, "*0|1+1", 0, 0, ""); + testSubattribution(11, "*0|1+1", 0, 1, "*0|1+1"); + testSubattribution(12, "*0|1+1", 0, undefined, "*0|1+1"); + testSubattribution(13, "*0+2+1*1+3", 0, 1, "*0+1"); + testSubattribution(14, "*0+2+1*1+3", 0, 2, "*0+2"); + testSubattribution(15, "*0+2+1*1+3", 0, 3, "*0+2+1"); + testSubattribution(16, "*0+2+1*1+3", 0, 4, "*0+2+1*1+1"); + testSubattribution(17, "*0+2+1*1+3", 0, 5, "*0+2+1*1+2"); + testSubattribution(18, "*0+2+1*1+3", 0, 6, "*0+2+1*1+3"); + testSubattribution(19, "*0+2+1*1+3", 0, 7, "*0+2+1*1+3"); + testSubattribution(20, "*0+2+1*1+3", 0, undefined, "*0+2+1*1+3"); + testSubattribution(21, "*0+2+1*1+3", 1, undefined, "*0+1+1*1+3"); + testSubattribution(22, "*0+2+1*1+3", 2, undefined, "+1*1+3"); + testSubattribution(23, "*0+2+1*1+3", 3, undefined, "*1+3"); + testSubattribution(24, "*0+2+1*1+3", 4, undefined, "*1+2"); + testSubattribution(25, "*0+2+1*1+3", 5, undefined, "*1+1"); + testSubattribution(26, "*0+2+1*1+3", 6, undefined, ""); + testSubattribution(27, "*0+2+1*1|1+3", 0, 1, "*0+1"); + testSubattribution(28, "*0+2+1*1|1+3", 0, 2, "*0+2"); + testSubattribution(29, "*0+2+1*1|1+3", 0, 3, "*0+2+1"); + testSubattribution(30, "*0+2+1*1|1+3", 0, 4, "*0+2+1*1+1"); + testSubattribution(31, "*0+2+1*1|1+3", 0, 5, "*0+2+1*1+2"); + testSubattribution(32, "*0+2+1*1|1+3", 0, 6, "*0+2+1*1|1+3"); + testSubattribution(33, "*0+2+1*1|1+3", 0, 7, "*0+2+1*1|1+3"); + testSubattribution(34, "*0+2+1*1|1+3", 0, undefined, "*0+2+1*1|1+3"); + testSubattribution(35, "*0+2+1*1|1+3", 1, undefined, "*0+1+1*1|1+3"); + testSubattribution(36, "*0+2+1*1|1+3", 2, undefined, "+1*1|1+3"); + testSubattribution(37, "*0+2+1*1|1+3", 3, undefined, "*1|1+3"); + testSubattribution(38, "*0+2+1*1|1+3", 4, undefined, "*1|1+2"); + testSubattribution(39, "*0+2+1*1|1+3", 5, undefined, "*1|1+1"); + testSubattribution(40, "*0+2+1*1|1+3", 1, 5, "*0+1+1*1+2"); + testSubattribution(41, "*0+2+1*1|1+3", 2, 6, "+1*1|1+3"); + testSubattribution(42, "*0+2+1*1+3", 2, 6, "+1*1+3"); + + function testFilterAttribNumbers(testId, cs, filter, correctOutput) { + print("> testFilterAttribNumbers#" + testId); + + var str = Changeset.filterAttribNumbers(cs, filter); + assertEqualStrings(correctOutput, str); + } + + testFilterAttribNumbers(1, "*0*1+1+2+3*1+4*2+5*0*2*1*b*c+6", function (n) { + return (n % 2) == 0; + }, "*0+1+2+3+4*2+5*0*2*c+6"); + testFilterAttribNumbers(2, "*0*1+1+2+3*1+4*2+5*0*2*1*b*c+6", function (n) { + return (n % 2) == 1; + }, "*1+1+2+3*1+4+5*1*b+6"); + + function testInverse(testId, cs, lines, alines, pool, correctOutput) { + print("> testInverse#" + testId); + + pool = poolOrArray(pool); + var str = Changeset.inverse(Changeset.checkRep(cs), lines, alines, pool); + assertEqualStrings(correctOutput, str); + } + + // take "FFFFTTTTT" and apply "-FT--FFTT", the inverse of which is "--F--TT--" + testInverse(1, "Z:9>0=1*0=1*1=1=2*0=2*1|1=2$", null, ["+4*1+5"], ['bold,', 'bold,true'], "Z:9>0=2*0=1=2*1=2$"); + + function testMutateTextLines(testId, cs, lines, correctLines) { + print("> testMutateTextLines#" + testId); + + var a = lines.slice(); + Changeset.mutateTextLines(cs, a); + assertEqualArrays(correctLines, a); + } + + testMutateTextLines(1, "Z:4<1|1-2-1|1+1+1$\nc", ["a\n", "b\n"], ["\n", "c\n"]); + testMutateTextLines(2, "Z:4>0|1-2-1|2+3$\nc\n", ["a\n", "b\n"], ["\n", "c\n", "\n"]); + + function testInverseRandom(randomSeed) { + var rand = new random(); + print("> testInverseRandom#" + randomSeed); + + var p = poolOrArray(['apple,', 'apple,true', 'banana,', 'banana,true']); + + var startText = randomMultiline(10, 20, rand) + '\n'; + var alines = Changeset.splitAttributionLines(Changeset.makeAttribution(startText), startText); + var lines = startText.slice(0, -1).split('\n').map(function (s) { + return s + '\n'; + }); + + var stylifier = randomTestChangeset(startText, rand, true)[0]; + + //print(alines.join('\n')); + Changeset.mutateAttributionLines(stylifier, alines, p); + //print(stylifier); + //print(alines.join('\n')); + Changeset.mutateTextLines(stylifier, lines); + + var changeset = randomTestChangeset(lines.join(''), rand, true)[0]; + var inverseChangeset = Changeset.inverse(changeset, lines, alines, p); + + var origLines = lines.slice(); + var origALines = alines.slice(); + + Changeset.mutateTextLines(changeset, lines); + Changeset.mutateAttributionLines(changeset, alines, p); + //print(origALines.join('\n')); + //print(changeset); + //print(inverseChangeset); + //print(origLines.map(function(s) { return '1: '+s.slice(0,-1); }).join('\n')); + //print(lines.map(function(s) { return '2: '+s.slice(0,-1); }).join('\n')); + //print(alines.join('\n')); + Changeset.mutateTextLines(inverseChangeset, lines); + Changeset.mutateAttributionLines(inverseChangeset, alines, p); + //print(lines.map(function(s) { return '3: '+s.slice(0,-1); }).join('\n')); + assertEqualArrays(origLines, lines); + assertEqualArrays(origALines, alines); + } + + for (var i = 0; i < 30; i++) testInverseRandom(i); +} + +runTests(); diff --git a/src/node/handler/APIHandler.js b/src/node/handler/APIHandler.js new file mode 100644 index 00000000..d4d7d6ce --- /dev/null +++ b/src/node/handler/APIHandler.js @@ -0,0 +1,161 @@ +/** + * The API Handler handles all API http requests + */ + +/* + * 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 fs = require("fs"); +var api = require("../db/API"); +var padManager = require("../db/PadManager"); +var randomString = require('ep_etherpad-lite/static/js/pad_utils').randomString; + +//ensure we have an apikey +var apikey = null; +try +{ + apikey = fs.readFileSync("../APIKEY.txt","utf8"); +} +catch(e) +{ + apikey = randomString(32); + fs.writeFileSync("../APIKEY.txt",apikey,"utf8"); +} + +//a list of all functions +var functions = { + "createGroup" : [], + "createGroupIfNotExistsFor" : ["groupMapper"], + "deleteGroup" : ["groupID"], + "listPads" : ["groupID"], + "createPad" : ["padID", "text"], + "createGroupPad" : ["groupID", "padName", "text"], + "createAuthor" : ["name"], + "createAuthorIfNotExistsFor": ["authorMapper" , "name"], + "createSession" : ["groupID", "authorID", "validUntil"], + "deleteSession" : ["sessionID"], + "getSessionInfo" : ["sessionID"], + "listSessionsOfGroup" : ["groupID"], + "listSessionsOfAuthor" : ["authorID"], + "getText" : ["padID", "rev"], + "setText" : ["padID", "text"], + "getHTML" : ["padID", "rev"], + "setHTML" : ["padID", "html"], + "getRevisionsCount" : ["padID"], + "deletePad" : ["padID"], + "getReadOnlyID" : ["padID"], + "setPublicStatus" : ["padID", "publicStatus"], + "getPublicStatus" : ["padID"], + "setPassword" : ["padID", "password"], + "isPasswordProtected" : ["padID"] +}; + +/** + * Handles a HTTP API call + * @param functionName the name of the called function + * @param fields the params of the called function + * @req express request object + * @res express response object + */ +exports.handle = function(functionName, fields, req, res) +{ + //check the api key! + if(fields["apikey"] != apikey.trim()) + { + res.send({code: 4, message: "no or wrong API Key", data: null}); + return; + } + + //check if this is a valid function name + var isKnownFunctionname = false; + for(var knownFunctionname in functions) + { + if(knownFunctionname == functionName) + { + isKnownFunctionname = true; + break; + } + } + + //say goodbye if this is a unkown function + if(!isKnownFunctionname) + { + res.send({code: 3, message: "no such function", data: null}); + return; + } + + //sanitize any pad id's before continuing + if(fields["padID"]) + { + padManager.sanitizePadId(fields["padID"], function(padId) + { + fields["padID"] = padId; + callAPI(functionName, fields, req, res); + }); + } + else if(fields["padName"]) + { + padManager.sanitizePadId(fields["padName"], function(padId) + { + fields["padName"] = padId; + callAPI(functionName, fields, req, res); + }); + } + else + { + callAPI(functionName, fields, req, res); + } +} + +//calls the api function +function callAPI(functionName, fields, req, res) +{ + //put the function parameters in an array + var functionParams = []; + for(var i=0;i<functions[functionName].length;i++) + { + functionParams.push(fields[functions[functionName][i]]); + } + + //add a callback function to handle the response + functionParams.push(function(err, data) + { + // no error happend, everything is fine + if(err == null) + { + if(!data) + data = null; + + res.send({code: 0, message: "ok", data: data}); + } + // parameters were wrong and the api stopped execution, pass the error + else if(err.name == "apierror") + { + res.send({code: 1, message: err.message, data: null}); + } + //an unkown error happend + else + { + res.send({code: 2, message: "internal error", data: null}); + ERR(err); + } + }); + + //call the api function + api[functionName](functionParams[0],functionParams[1],functionParams[2],functionParams[3],functionParams[4]); +} diff --git a/src/node/handler/ExportHandler.js b/src/node/handler/ExportHandler.js new file mode 100644 index 00000000..1b7fcc26 --- /dev/null +++ b/src/node/handler/ExportHandler.js @@ -0,0 +1,165 @@ +/** + * Handles the export requests + */ + +/* + * 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 exporthtml = require("../utils/ExportHtml"); +var exportdokuwiki = require("../utils/ExportDokuWiki"); +var padManager = require("../db/PadManager"); +var async = require("async"); +var fs = require("fs"); +var settings = require('../utils/Settings'); +var os = require('os'); + +//load abiword only if its enabled +if(settings.abiword != null) + var abiword = require("../utils/Abiword"); + +var tempDirectory = "/tmp"; + +//tempDirectory changes if the operating system is windows +if(os.type().indexOf("Windows") > -1) +{ + tempDirectory = process.env.TEMP; +} + +/** + * do a requested export + */ +exports.doExport = function(req, res, padId, type) +{ + //tell the browser that this is a downloadable file + res.attachment(padId + "." + type); + + //if this is a plain text export, we can do this directly + if(type == "txt") + { + padManager.getPad(padId, function(err, pad) + { + ERR(err); + if(req.params.rev){ + pad.getInternalRevisionAText(req.params.rev, function(junk, text) + { + res.send(text.text ? text.text : null); + }); + } + else + { + res.send(pad.text()); + } + }); + } + else if(type == 'dokuwiki') + { + var randNum; + var srcFile, destFile; + + async.series([ + //render the dokuwiki document + function(callback) + { + exportdokuwiki.getPadDokuWikiDocument(padId, req.params.rev, function(err, dokuwiki) + { + res.send(dokuwiki); + callback("stop"); + }); + }, + ], function(err) + { + if(err && err != "stop") throw err; + }); + } + else + { + var html; + var randNum; + var srcFile, destFile; + + async.series([ + //render the html document + function(callback) + { + exporthtml.getPadHTMLDocument(padId, req.params.rev, false, function(err, _html) + { + if(ERR(err, callback)) return; + html = _html; + callback(); + }); + }, + //decide what to do with the html export + function(callback) + { + //if this is a html export, we can send this from here directly + if(type == "html") + { + res.send(html); + callback("stop"); + } + else //write the html export to a file + { + randNum = Math.floor(Math.random()*0xFFFFFFFF); + srcFile = tempDirectory + "/eplite_export_" + randNum + ".html"; + fs.writeFile(srcFile, html, callback); + } + }, + //send the convert job to abiword + function(callback) + { + //ensure html can be collected by the garbage collector + html = null; + + destFile = tempDirectory + "/eplite_export_" + randNum + "." + type; + abiword.convertFile(srcFile, destFile, type, callback); + }, + //send the file + function(callback) + { + res.sendfile(destFile, null, callback); + }, + //clean up temporary files + function(callback) + { + async.parallel([ + function(callback) + { + fs.unlink(srcFile, callback); + }, + function(callback) + { + //100ms delay to accomidate for slow windows fs + if(os.type().indexOf("Windows") > -1) + { + setTimeout(function() + { + fs.unlink(destFile, callback); + }, 100); + } + else + { + fs.unlink(destFile, callback); + } + } + ], callback); + } + ], function(err) + { + if(err && err != "stop") ERR(err); + }) + } +}; diff --git a/src/node/handler/ImportHandler.js b/src/node/handler/ImportHandler.js new file mode 100644 index 00000000..ed5eb05e --- /dev/null +++ b/src/node/handler/ImportHandler.js @@ -0,0 +1,201 @@ +/** + * Handles the import requests + */ + +/* + * 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 padManager = require("../db/PadManager"); +var padMessageHandler = require("./PadMessageHandler"); +var async = require("async"); +var fs = require("fs"); +var settings = require('../utils/Settings'); +var formidable = require('formidable'); +var os = require("os"); + +//load abiword only if its enabled +if(settings.abiword != null) + var abiword = require("../utils/Abiword"); + +var tempDirectory = "/tmp/"; + +//tempDirectory changes if the operating system is windows +if(os.type().indexOf("Windows") > -1) +{ + tempDirectory = process.env.TEMP; +} + +/** + * do a requested import + */ +exports.doImport = function(req, res, padId) +{ + //pipe to a file + //convert file to text via abiword + //set text in the pad + + var srcFile, destFile; + var pad; + var text; + + async.series([ + //save the uploaded file to /tmp + function(callback) + { + var form = new formidable.IncomingForm(); + form.keepExtensions = true; + form.uploadDir = tempDirectory; + + form.parse(req, function(err, fields, files) + { + //the upload failed, stop at this point + if(err || files.file === undefined) + { + console.warn("Uploading Error: " + err.stack); + callback("uploadFailed"); + } + //everything ok, continue + else + { + //save the path of the uploaded file + srcFile = files.file.path; + callback(); + } + }); + }, + + //ensure this is a file ending we know, else we change the file ending to .txt + //this allows us to accept source code files like .c or .java + function(callback) + { + var fileEnding = (srcFile.split(".")[1] || "").toLowerCase(); + var knownFileEndings = ["txt", "doc", "docx", "pdf", "odt", "html", "htm"]; + + //find out if this is a known file ending + var fileEndingKnown = false; + for(var i in knownFileEndings) + { + if(fileEnding == knownFileEndings[i]) + { + fileEndingKnown = true; + } + } + + //if the file ending is known, continue as normal + if(fileEndingKnown) + { + callback(); + } + //we need to rename this file with a .txt ending + else + { + var oldSrcFile = srcFile; + srcFile = srcFile.split(".")[0] + ".txt"; + + fs.rename(oldSrcFile, srcFile, callback); + } + }, + + //convert file to text + function(callback) + { + var randNum = Math.floor(Math.random()*0xFFFFFFFF); + destFile = tempDirectory + "eplite_import_" + randNum + ".txt"; + abiword.convertFile(srcFile, destFile, "txt", function(err){ + //catch convert errors + if(err){ + console.warn("Converting Error:", err); + return callback("convertFailed"); + } else { + callback(); + } + }); + }, + + //get the pad object + function(callback) + { + padManager.getPad(padId, function(err, _pad) + { + if(ERR(err, callback)) return; + pad = _pad; + callback(); + }); + }, + + //read the text + function(callback) + { + fs.readFile(destFile, "utf8", function(err, _text) + { + if(ERR(err, callback)) return; + text = _text; + + //node on windows has a delay on releasing of the file lock. + //We add a 100ms delay to work around this + if(os.type().indexOf("Windows") > -1) + { + setTimeout(function() + { + callback(); + }, 100); + } + else + { + callback(); + } + }); + }, + + //change text of the pad and broadcast the changeset + function(callback) + { + pad.setText(text); + padMessageHandler.updatePadClients(pad, callback); + }, + + //clean up temporary files + function(callback) + { + async.parallel([ + function(callback) + { + fs.unlink(srcFile, callback); + }, + function(callback) + { + fs.unlink(destFile, callback); + } + ], callback); + } + ], function(err) + { + var status = "ok"; + + //check for known errors and replace the status + if(err == "uploadFailed" || err == "convertFailed") + { + status = err; + err = null; + } + + ERR(err); + + //close the connection + res.send("<script>document.domain = document.domain; var impexp = window.top.require('/pad_impexp').padimpexp.handleFrameCall('" + status + "'); </script>", 200); + }); +} diff --git a/src/node/handler/PadMessageHandler.js b/src/node/handler/PadMessageHandler.js new file mode 100644 index 00000000..e26bb46e --- /dev/null +++ b/src/node/handler/PadMessageHandler.js @@ -0,0 +1,930 @@ +/** + * The MessageHandler handles all Messages that comes from Socket.IO and controls the sessions + */ + +/* + * 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. + */ + + +var ERR = require("async-stacktrace"); +var async = require("async"); +var padManager = require("../db/PadManager"); +var Changeset = require("ep_etherpad-lite/static/js/Changeset"); +var AttributePoolFactory = require("ep_etherpad-lite/static/js/AttributePoolFactory"); +var authorManager = require("../db/AuthorManager"); +var readOnlyManager = require("../db/ReadOnlyManager"); +var settings = require('../utils/Settings'); +var securityManager = require("../db/SecurityManager"); +var plugins = require("ep_etherpad-lite/static/js/pluginfw/plugins.js"); +var log4js = require('log4js'); +var messageLogger = log4js.getLogger("message"); + +/** + * A associative array that translates a session to a pad + */ +var session2pad = {}; +/** + * A associative array that saves which sessions belong to a pad + */ +var pad2sessions = {}; + +/** + * A associative array that saves some general informations about a session + * key = sessionId + * values = author, rev + * rev = That last revision that was send to this client + * author = the author name of this session + */ +var sessioninfos = {}; + +/** + * Saves the Socket class we need to send and recieve data from the client + */ +var socketio; + +/** + * This Method is called by server.js to tell the message handler on which socket it should send + * @param socket_io The Socket + */ +exports.setSocketIO = function(socket_io) +{ + socketio=socket_io; +} + +/** + * Handles the connection of a new user + * @param client the new client + */ +exports.handleConnect = function(client) +{ + //Initalize session2pad and sessioninfos for this new session + session2pad[client.id]=null; + sessioninfos[client.id]={}; +} + +/** + * Kicks all sessions from a pad + * @param client the new client + */ +exports.kickSessionsFromPad = function(padID) +{ + //skip if there is nobody on this pad + if(!pad2sessions[padID]) + return; + + //disconnect everyone from this pad + for(var i in pad2sessions[padID]) + { + socketio.sockets.sockets[pad2sessions[padID][i]].json.send({disconnect:"deleted"}); + } +} + +/** + * Handles the disconnection of a user + * @param client the client that leaves + */ +exports.handleDisconnect = function(client) +{ + //save the padname of this session + var sessionPad=session2pad[client.id]; + + //if this connection was already etablished with a handshake, send a disconnect message to the others + if(sessioninfos[client.id] && sessioninfos[client.id].author) + { + var author = sessioninfos[client.id].author; + + //get the author color out of the db + authorManager.getAuthorColorId(author, function(err, color) + { + ERR(err); + + //prepare the notification for the other users on the pad, that this user left + var messageToTheOtherUsers = { + "type": "COLLABROOM", + "data": { + type: "USER_LEAVE", + userInfo: { + "ip": "127.0.0.1", + "colorId": color, + "userAgent": "Anonymous", + "userId": author + } + } + }; + + //Go trough all user that are still on the pad, and send them the USER_LEAVE message + for(i in pad2sessions[sessionPad]) + { + socketio.sockets.sockets[pad2sessions[sessionPad][i]].json.send(messageToTheOtherUsers); + } + }); + } + + //Go trough all sessions of this pad, search and destroy the entry of this client + for(i in pad2sessions[sessionPad]) + { + if(pad2sessions[sessionPad][i] == client.id) + { + pad2sessions[sessionPad].splice(i, 1); + break; + } + } + + //Delete the session2pad and sessioninfos entrys of this session + delete session2pad[client.id]; + delete sessioninfos[client.id]; +} + +/** + * Handles a message from a user + * @param client the client that send this message + * @param message the message from the client + */ +exports.handleMessage = function(client, message) +{ + if(message == null) + { + messageLogger.warn("Message is null!"); + return; + } + if(!message.type) + { + messageLogger.warn("Message has no type attribute!"); + return; + } + + //Check what type of message we get and delegate to the other methodes + if(message.type == "CLIENT_READY") + { + handleClientReady(client, message); + } + else if(message.type == "COLLABROOM" && + message.data.type == "USER_CHANGES") + { + handleUserChanges(client, message); + } + else if(message.type == "COLLABROOM" && + message.data.type == "USERINFO_UPDATE") + { + handleUserInfoUpdate(client, message); + } + else if(message.type == "COLLABROOM" && + message.data.type == "CHAT_MESSAGE") + { + handleChatMessage(client, message); + } + else if(message.type == "COLLABROOM" && + message.data.type == "CLIENT_MESSAGE" && + message.data.payload.type == "suggestUserName") + { + handleSuggestUserName(client, message); + } + //if the message type is unknown, throw an exception + else + { + messageLogger.warn("Dropped message, unknown Message Type " + message.type); + } +} + +/** + * Handles a Chat Message + * @param client the client that send this message + * @param message the message from the client + */ +function handleChatMessage(client, message) +{ + var time = new Date().getTime(); + var userId = sessioninfos[client.id].author; + var text = message.data.text; + var padId = session2pad[client.id]; + + var pad; + var userName; + + async.series([ + //get the pad + function(callback) + { + padManager.getPad(padId, function(err, _pad) + { + if(ERR(err, callback)) return; + pad = _pad; + callback(); + }); + }, + function(callback) + { + authorManager.getAuthorName(userId, function(err, _userName) + { + if(ERR(err, callback)) return; + userName = _userName; + callback(); + }); + }, + //save the chat message and broadcast it + function(callback) + { + //save the chat message + pad.appendChatMessage(text, userId, time); + + var msg = { + type: "COLLABROOM", + data: { + type: "CHAT_MESSAGE", + userId: userId, + userName: userName, + time: time, + text: text + } + }; + + //broadcast the chat message to everyone on the pad + for(var i in pad2sessions[padId]) + { + socketio.sockets.sockets[pad2sessions[padId][i]].json.send(msg); + } + + callback(); + } + ], function(err) + { + ERR(err); + }); +} + + +/** + * Handles a handleSuggestUserName, that means a user have suggest a userName for a other user + * @param client the client that send this message + * @param message the message from the client + */ +function handleSuggestUserName(client, message) +{ + //check if all ok + if(message.data.payload.newName == null) + { + messageLogger.warn("Dropped message, suggestUserName Message has no newName!"); + return; + } + if(message.data.payload.unnamedId == null) + { + messageLogger.warn("Dropped message, suggestUserName Message has no unnamedId!"); + return; + } + + var padId = session2pad[client.id]; + + //search the author and send him this message + for(var i in pad2sessions[padId]) + { + if(sessioninfos[pad2sessions[padId][i]].author == message.data.payload.unnamedId) + { + socketio.sockets.sockets[pad2sessions[padId][i]].send(message); + break; + } + } +} + +/** + * Handles a USERINFO_UPDATE, that means that a user have changed his color or name. Anyway, we get both informations + * @param client the client that send this message + * @param message the message from the client + */ +function handleUserInfoUpdate(client, message) +{ + //check if all ok + if(message.data.userInfo.colorId == null) + { + messageLogger.warn("Dropped message, USERINFO_UPDATE Message has no colorId!"); + return; + } + + //Find out the author name of this session + var author = sessioninfos[client.id].author; + + //Tell the authorManager about the new attributes + authorManager.setAuthorColorId(author, message.data.userInfo.colorId); + authorManager.setAuthorName(author, message.data.userInfo.name); + + var padId = session2pad[client.id]; + + //set a null name, when there is no name set. cause the client wants it null + if(message.data.userInfo.name == null) + { + message.data.userInfo.name = null; + } + + //The Client don't know about a USERINFO_UPDATE, it can handle only new user_newinfo, so change the message type + message.data.type = "USER_NEWINFO"; + + //Send the other clients on the pad the update message + for(var i in pad2sessions[padId]) + { + if(pad2sessions[padId][i] != client.id) + { + socketio.sockets.sockets[pad2sessions[padId][i]].json.send(message); + } + } +} + +/** + * Handles a USERINFO_UPDATE, that means that a user have changed his color or name. Anyway, we get both informations + * This Method is nearly 90% copied out of the Etherpad Source Code. So I can't tell you what happens here exactly + * Look at https://github.com/ether/pad/blob/master/etherpad/src/etherpad/collab/collab_server.js in the function applyUserChanges() + * @param client the client that send this message + * @param message the message from the client + */ +function handleUserChanges(client, message) +{ + //check if all ok + if(message.data.baseRev == null) + { + messageLogger.warn("Dropped message, USER_CHANGES Message has no baseRev!"); + return; + } + if(message.data.apool == null) + { + messageLogger.warn("Dropped message, USER_CHANGES Message has no apool!"); + return; + } + if(message.data.changeset == null) + { + messageLogger.warn("Dropped message, USER_CHANGES Message has no changeset!"); + return; + } + + //get all Vars we need + var baseRev = message.data.baseRev; + var wireApool = (AttributePoolFactory.createAttributePool()).fromJsonable(message.data.apool); + var changeset = message.data.changeset; + + var r, apool, pad; + + async.series([ + //get the pad + function(callback) + { + padManager.getPad(session2pad[client.id], function(err, value) + { + if(ERR(err, callback)) return; + pad = value; + callback(); + }); + }, + //create the changeset + function(callback) + { + //ex. _checkChangesetAndPool + + //Copied from Etherpad, don't know what it does exactly + try + { + //this looks like a changeset check, it throws errors sometimes + Changeset.checkRep(changeset); + + Changeset.eachAttribNumber(changeset, function(n) { + if (! wireApool.getAttrib(n)) { + throw "Attribute pool is missing attribute "+n+" for changeset "+changeset; + } + }); + } + //there is an error in this changeset, so just refuse it + catch(e) + { + console.warn("Can't apply USER_CHANGES "+changeset+", cause it faild checkRep"); + client.json.send({disconnect:"badChangeset"}); + return; + } + + //ex. adoptChangesetAttribs + + //Afaik, it copies the new attributes from the changeset, to the global Attribute Pool + changeset = Changeset.moveOpsToNewPool(changeset, wireApool, pad.pool); + + //ex. applyUserChanges + apool = pad.pool; + r = baseRev; + + //https://github.com/caolan/async#whilst + async.whilst( + function() { return r < pad.getHeadRevisionNumber(); }, + function(callback) + { + r++; + + pad.getRevisionChangeset(r, function(err, c) + { + if(ERR(err, callback)) return; + + changeset = Changeset.follow(c, changeset, false, apool); + callback(null); + }); + }, + //use the callback of the series function + callback + ); + }, + //do correction changesets, and send it to all users + function (callback) + { + var prevText = pad.text(); + + if (Changeset.oldLen(changeset) != prevText.length) + { + console.warn("Can't apply USER_CHANGES "+changeset+" with oldLen " + Changeset.oldLen(changeset) + " to document of length " + prevText.length); + client.json.send({disconnect:"badChangeset"}); + callback(); + return; + } + + var thisAuthor = sessioninfos[client.id].author; + + pad.appendRevision(changeset, thisAuthor); + + var correctionChangeset = _correctMarkersInPad(pad.atext, pad.pool); + if (correctionChangeset) { + pad.appendRevision(correctionChangeset); + } + + if (pad.text().lastIndexOf("\n\n") != pad.text().length-2) { + var nlChangeset = Changeset.makeSplice(pad.text(), pad.text().length-1, 0, "\n"); + pad.appendRevision(nlChangeset); + } + + exports.updatePadClients(pad, callback); + } + ], function(err) + { + ERR(err); + }); +} + +exports.updatePadClients = function(pad, callback) +{ + //skip this step if noone is on this pad + if(!pad2sessions[pad.id]) + { + callback(); + return; + } + + //go trough all sessions on this pad + async.forEach(pad2sessions[pad.id], function(session, callback) + { + var lastRev = sessioninfos[session].rev; + + //https://github.com/caolan/async#whilst + //send them all new changesets + async.whilst( + function (){ return lastRev < pad.getHeadRevisionNumber()}, + function(callback) + { + var author, revChangeset; + + var r = ++lastRev; + + async.parallel([ + function (callback) + { + pad.getRevisionAuthor(r, function(err, value) + { + if(ERR(err, callback)) return; + author = value; + callback(); + }); + }, + function (callback) + { + pad.getRevisionChangeset(r, function(err, value) + { + if(ERR(err, callback)) return; + revChangeset = value; + callback(); + }); + } + ], function(err) + { + if(ERR(err, callback)) return; + // next if session has not been deleted + if(sessioninfos[session] == null) + { + callback(null); + return; + } + if(author == sessioninfos[session].author) + { + socketio.sockets.sockets[session].json.send({"type":"COLLABROOM","data":{type:"ACCEPT_COMMIT", newRev:r}}); + } + else + { + var forWire = Changeset.prepareForWire(revChangeset, pad.pool); + var wireMsg = {"type":"COLLABROOM","data":{type:"NEW_CHANGES", newRev:r, + changeset: forWire.translated, + apool: forWire.pool, + author: author}}; + + socketio.sockets.sockets[session].json.send(wireMsg); + } + + callback(null); + }); + }, + callback + ); + + if(sessioninfos[session] != null) + { + sessioninfos[session].rev = pad.getHeadRevisionNumber(); + } + },callback); +} + +/** + * Copied from the Etherpad Source Code. Don't know what this methode does excatly... + */ +function _correctMarkersInPad(atext, apool) { + var text = atext.text; + + // collect char positions of line markers (e.g. bullets) in new atext + // that aren't at the start of a line + var badMarkers = []; + var iter = Changeset.opIterator(atext.attribs); + var offset = 0; + while (iter.hasNext()) { + var op = iter.next(); + var listValue = Changeset.opAttributeValue(op, 'list', apool); + if (listValue) { + for(var i=0;i<op.chars;i++) { + if (offset > 0 && text.charAt(offset-1) != '\n') { + badMarkers.push(offset); + } + offset++; + } + } + else { + offset += op.chars; + } + } + + if (badMarkers.length == 0) { + return null; + } + + // create changeset that removes these bad markers + offset = 0; + var builder = Changeset.builder(text.length); + badMarkers.forEach(function(pos) { + builder.keepText(text.substring(offset, pos)); + builder.remove(1); + offset = pos+1; + }); + return builder.toString(); +} + +/** + * Handles a CLIENT_READY. A CLIENT_READY is the first message from the client to the server. The Client sends his token + * and the pad it wants to enter. The Server answers with the inital values (clientVars) of the pad + * @param client the client that send this message + * @param message the message from the client + */ +function handleClientReady(client, message) +{ + //check if all ok + if(!message.token) + { + messageLogger.warn("Dropped message, CLIENT_READY Message has no token!"); + return; + } + if(!message.padId) + { + messageLogger.warn("Dropped message, CLIENT_READY Message has no padId!"); + return; + } + if(!message.protocolVersion) + { + messageLogger.warn("Dropped message, CLIENT_READY Message has no protocolVersion!"); + return; + } + if(message.protocolVersion != 2) + { + messageLogger.warn("Dropped message, CLIENT_READY Message has a unknown protocolVersion '" + message.protocolVersion + "'!"); + return; + } + + var author; + var authorName; + var authorColorId; + var pad; + var historicalAuthorData = {}; + var readOnlyId; + var chatMessages; + + async.series([ + //check permissions + function(callback) + { + securityManager.checkAccess (message.padId, message.sessionID, message.token, message.password, function(err, statusObject) + { + if(ERR(err, callback)) return; + + //access was granted + if(statusObject.accessStatus == "grant") + { + author = statusObject.authorID; + callback(); + } + //no access, send the client a message that tell him why + else + { + client.json.send({accessStatus: statusObject.accessStatus}) + } + }); + }, + //get all authordata of this new user + function(callback) + { + async.parallel([ + //get colorId + function(callback) + { + authorManager.getAuthorColorId(author, function(err, value) + { + if(ERR(err, callback)) return; + authorColorId = value; + callback(); + }); + }, + //get author name + function(callback) + { + authorManager.getAuthorName(author, function(err, value) + { + if(ERR(err, callback)) return; + authorName = value; + callback(); + }); + }, + function(callback) + { + padManager.getPad(message.padId, function(err, value) + { + if(ERR(err, callback)) return; + pad = value; + callback(); + }); + }, + function(callback) + { + readOnlyManager.getReadOnlyId(message.padId, function(err, value) + { + if(ERR(err, callback)) return; + readOnlyId = value; + callback(); + }); + } + ], callback); + }, + //these db requests all need the pad object + function(callback) + { + var authors = pad.getAllAuthors(); + + async.parallel([ + //get all author data out of the database + function(callback) + { + async.forEach(authors, function(authorId, callback) + { + authorManager.getAuthor(authorId, function(err, author) + { + if(ERR(err, callback)) return; + delete author.timestamp; + historicalAuthorData[authorId] = author; + callback(); + }); + }, callback); + }, + //get the latest chat messages + function(callback) + { + pad.getLastChatMessages(100, function(err, _chatMessages) + { + if(ERR(err, callback)) return; + chatMessages = _chatMessages; + callback(); + }); + } + ], callback); + + + }, + function(callback) + { + //Check if this author is already on the pad, if yes, kick the other sessions! + if(pad2sessions[message.padId]) + { + for(var i in pad2sessions[message.padId]) + { + if(sessioninfos[pad2sessions[message.padId][i]].author == author) + { + socketio.sockets.sockets[pad2sessions[message.padId][i]].json.send({disconnect:"userdup"}); + } + } + } + + //Save in session2pad that this session belonges to this pad + var sessionId=String(client.id); + session2pad[sessionId] = message.padId; + + //check if there is already a pad2sessions entry, if not, create one + if(!pad2sessions[message.padId]) + { + pad2sessions[message.padId] = []; + } + + //Saves in pad2sessions that this session belongs to this pad + pad2sessions[message.padId].push(sessionId); + + //prepare all values for the wire + var atext = Changeset.cloneAText(pad.atext); + var attribsForWire = Changeset.prepareForWire(atext.attribs, pad.pool); + var apool = attribsForWire.pool.toJsonable(); + atext.attribs = attribsForWire.translated; + + var clientVars = { + "accountPrivs": { + "maxRevisions": 100 + }, + "initialRevisionList": [], + "initialOptions": { + "guestPolicy": "deny" + }, + "collab_client_vars": { + "initialAttributedText": atext, + "clientIp": "127.0.0.1", + //"clientAgent": "Anonymous Agent", + "padId": message.padId, + "historicalAuthorData": historicalAuthorData, + "apool": apool, + "rev": pad.getHeadRevisionNumber(), + "globalPadId": message.padId + }, + "colorPalette": ["#ffc7c7", "#fff1c7", "#e3ffc7", "#c7ffd5", "#c7ffff", "#c7d5ff", "#e3c7ff", "#ffc7f1", "#ff8f8f", "#ffe38f", "#c7ff8f", "#8fffab", "#8fffff", "#8fabff", "#c78fff", "#ff8fe3", "#d97979", "#d9c179", "#a9d979", "#79d991", "#79d9d9", "#7991d9", "#a979d9", "#d979c1", "#d9a9a9", "#d9cda9", "#c1d9a9", "#a9d9b5", "#a9d9d9", "#a9b5d9", "#c1a9d9", "#d9a9cd", "#4c9c82", "#12d1ad", "#2d8e80", "#7485c3", "#a091c7", "#3185ab", "#6818b4", "#e6e76d", "#a42c64", "#f386e5", "#4ecc0c", "#c0c236", "#693224", "#b5de6a", "#9b88fd", "#358f9b", "#496d2f", "#e267fe", "#d23056", "#1a1a64", "#5aa335", "#d722bb", "#86dc6c", "#b5a714", "#955b6a", "#9f2985", "#4b81c8", "#3d6a5b", "#434e16", "#d16084", "#af6a0e", "#8c8bd8"], + "clientIp": "127.0.0.1", + "userIsGuest": true, + "userColor": authorColorId, + "padId": message.padId, + "initialTitle": "Pad: " + message.padId, + "opts": {}, + "chatHistory": chatMessages, + "numConnectedUsers": pad2sessions[message.padId].length, + "isProPad": false, + "readOnlyId": readOnlyId, + "serverTimestamp": new Date().getTime(), + "globalPadId": message.padId, + "userId": author, + "cookiePrefsToSet": { + "fullWidth": false, + "hideSidebar": false + }, + "abiwordAvailable": settings.abiwordAvailable(), + "plugins": { + "plugins": plugins.plugins, + "parts": plugins.parts, + } + } + + //Add a username to the clientVars if one avaiable + if(authorName != null) + { + clientVars.userName = authorName; + } + + if(sessioninfos[client.id] !== undefined) + { + //This is a reconnect, so we don't have to send the client the ClientVars again + if(message.reconnect == true) + { + //Save the revision in sessioninfos, we take the revision from the info the client send to us + sessioninfos[client.id].rev = message.client_rev; + } + //This is a normal first connect + else + { + //Send the clientVars to the Client + client.json.send(clientVars); + //Save the revision in sessioninfos + sessioninfos[client.id].rev = pad.getHeadRevisionNumber(); + } + + //Save the revision and the author id in sessioninfos + sessioninfos[client.id].author = author; + } + + //prepare the notification for the other users on the pad, that this user joined + var messageToTheOtherUsers = { + "type": "COLLABROOM", + "data": { + type: "USER_NEWINFO", + userInfo: { + "ip": "127.0.0.1", + "colorId": authorColorId, + "userAgent": "Anonymous", + "userId": author + } + } + }; + + //Add the authorname of this new User, if avaiable + if(authorName != null) + { + messageToTheOtherUsers.data.userInfo.name = authorName; + } + + //Run trough all sessions of this pad + async.forEach(pad2sessions[message.padId], function(sessionID, callback) + { + var author, socket, sessionAuthorName, sessionAuthorColorId; + + //Since sessioninfos might change while being enumerated, check if the + //sessionID is still assigned to a valid session + if(sessioninfos[sessionID] !== undefined && + socketio.sockets.sockets[sessionID] !== undefined){ + author = sessioninfos[sessionID].author; + socket = socketio.sockets.sockets[sessionID]; + }else { + // If the sessionID is not valid, callback(); + callback(); + return; + } + async.series([ + //get the authorname & colorId + function(callback) + { + async.parallel([ + function(callback) + { + authorManager.getAuthorColorId(author, function(err, value) + { + if(ERR(err, callback)) return; + sessionAuthorColorId = value; + callback(); + }) + }, + function(callback) + { + authorManager.getAuthorName(author, function(err, value) + { + if(ERR(err, callback)) return; + sessionAuthorName = value; + callback(); + }) + } + ],callback); + }, + function (callback) + { + //Jump over, if this session is the connection session + if(sessionID != client.id) + { + //Send this Session the Notification about the new user + socket.json.send(messageToTheOtherUsers); + + //Send the new User a Notification about this other user + var messageToNotifyTheClientAboutTheOthers = { + "type": "COLLABROOM", + "data": { + type: "USER_NEWINFO", + userInfo: { + "ip": "127.0.0.1", + "colorId": sessionAuthorColorId, + "name": sessionAuthorName, + "userAgent": "Anonymous", + "userId": author + } + } + }; + client.json.send(messageToNotifyTheClientAboutTheOthers); + } + } + ], callback); + }, callback); + } + ],function(err) + { + ERR(err); + }); +} diff --git a/src/node/handler/SocketIORouter.js b/src/node/handler/SocketIORouter.js new file mode 100644 index 00000000..f3b82b8c --- /dev/null +++ b/src/node/handler/SocketIORouter.js @@ -0,0 +1,163 @@ +/** + * This is the Socket.IO Router. It routes the Messages between the + * components of the Server. The components are at the moment: pad and timeslider + */ + +/* + * 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 messageLogger = log4js.getLogger("message"); +var securityManager = require("../db/SecurityManager"); + +/** + * Saves all components + * key is the component name + * value is the component module + */ +var components = {}; + +var socket; + +/** + * adds a component + */ +exports.addComponent = function(moduleName, module) +{ + //save the component + components[moduleName] = module; + + //give the module the socket + module.setSocketIO(socket); +} + +/** + * sets the socket.io and adds event functions for routing + */ +exports.setSocketIO = function(_socket) +{ + //save this socket internaly + socket = _socket; + + socket.sockets.on('connection', function(client) + { + var clientAuthorized = false; + + //wrap the original send function to log the messages + client._send = client.send; + client.send = function(message) + { + messageLogger.info("to " + client.id + ": " + stringifyWithoutPassword(message)); + client._send(message); + } + + //tell all components about this connect + for(var i in components) + { + components[i].handleConnect(client); + } + + //try to handle the message of this client + function handleMessage(message) + { + if(message.component && components[message.component]) + { + //check if component is registered in the components array + if(components[message.component]) + { + messageLogger.info("from " + client.id + ": " + stringifyWithoutPassword(message)); + components[message.component].handleMessage(client, message); + } + } + else + { + messageLogger.error("Can't route the message:" + stringifyWithoutPassword(message)); + } + } + + client.on('message', function(message) + { + if(message.protocolVersion && message.protocolVersion != 2) + { + messageLogger.warn("Protocolversion header is not correct:" + stringifyWithoutPassword(message)); + return; + } + + //client is authorized, everything ok + if(clientAuthorized) + { + handleMessage(message); + } + //try to authorize the client + else + { + //this message has everything to try an authorization + if(message.padId !== undefined && message.sessionID !== undefined && message.token !== undefined && message.password !== undefined) + { + securityManager.checkAccess (message.padId, message.sessionID, message.token, message.password, function(err, statusObject) + { + ERR(err); + + //access was granted, mark the client as authorized and handle the message + if(statusObject.accessStatus == "grant") + { + clientAuthorized = true; + handleMessage(message); + } + //no access, send the client a message that tell him why + else + { + messageLogger.warn("Authentication try failed:" + stringifyWithoutPassword(message)); + client.json.send({accessStatus: statusObject.accessStatus}); + } + }); + } + //drop message + else + { + messageLogger.warn("Dropped message cause of bad permissions:" + stringifyWithoutPassword(message)); + } + } + }); + + client.on('disconnect', function() + { + //tell all components about this disconnect + for(var i in components) + { + components[i].handleDisconnect(client); + } + }); + }); +} + +//returns a stringified representation of a message, removes the password +//this ensures there are no passwords in the log +function stringifyWithoutPassword(message) +{ + var newMessage = {}; + + for(var i in message) + { + if(i == "password" && message[i] != null) + newMessage["password"] = "xxx"; + else + newMessage[i]=message[i]; + } + + return JSON.stringify(newMessage); +} diff --git a/src/node/handler/TimesliderMessageHandler.js b/src/node/handler/TimesliderMessageHandler.js new file mode 100644 index 00000000..0081e645 --- /dev/null +++ b/src/node/handler/TimesliderMessageHandler.js @@ -0,0 +1,529 @@ +/** + * The MessageHandler handles all Messages that comes from Socket.IO and controls the sessions + */ + +/* + * 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. + */ + + +var ERR = require("async-stacktrace"); +var async = require("async"); +var padManager = require("../db/PadManager"); +var Changeset = require("ep_etherpad-lite/static/js/Changeset"); +var AttributePoolFactory = require("ep_etherpad-lite/static/js/AttributePoolFactory"); +var settings = require('../utils/Settings'); +var authorManager = require("../db/AuthorManager"); +var log4js = require('log4js'); +var messageLogger = log4js.getLogger("message"); + +/** + * Saves the Socket class we need to send and recieve data from the client + */ +var socketio; + +/** + * This Method is called by server.js to tell the message handler on which socket it should send + * @param socket_io The Socket + */ +exports.setSocketIO = function(socket_io) +{ + socketio=socket_io; +} + +/** + * Handles the connection of a new user + * @param client the new client + */ +exports.handleConnect = function(client) +{ + +} + +/** + * Handles the disconnection of a user + * @param client the client that leaves + */ +exports.handleDisconnect = function(client) +{ + +} + +/** + * Handles a message from a user + * @param client the client that send this message + * @param message the message from the client + */ +exports.handleMessage = function(client, message) +{ + //Check what type of message we get and delegate to the other methodes + if(message.type == "CLIENT_READY") + { + handleClientReady(client, message); + } + else if(message.type == "CHANGESET_REQ") + { + handleChangesetRequest(client, message); + } + //if the message type is unkown, throw an exception + else + { + messageLogger.warn("Dropped message, unknown Message Type: '" + message.type + "'"); + } +} + +function handleClientReady(client, message) +{ + if(message.padId == null) + { + messageLogger.warn("Dropped message, changeset request has no padId!"); + return; + } + + //send the timeslider client the clientVars, with this values its able to start + createTimesliderClientVars (message.padId, function(err, clientVars) + { + ERR(err); + + client.json.send({type: "CLIENT_VARS", data: clientVars}); + }) +} + +/** + * Handles a request for a rough changeset, the timeslider client needs it + */ +function handleChangesetRequest(client, message) +{ + //check if all ok + if(message.data == null) + { + messageLogger.warn("Dropped message, changeset request has no data!"); + return; + } + if(message.padId == null) + { + messageLogger.warn("Dropped message, changeset request has no padId!"); + return; + } + if(message.data.granularity == null) + { + messageLogger.warn("Dropped message, changeset request has no granularity!"); + return; + } + if(message.data.start == null) + { + messageLogger.warn("Dropped message, changeset request has no start!"); + return; + } + if(message.data.requestID == null) + { + messageLogger.warn("Dropped message, changeset request has no requestID!"); + return; + } + + var granularity = message.data.granularity; + var start = message.data.start; + var end = start + (100 * granularity); + var padId = message.padId; + + //build the requested rough changesets and send them back + getChangesetInfo(padId, start, end, granularity, function(err, changesetInfo) + { + ERR(err); + + var data = changesetInfo; + data.requestID = message.data.requestID; + + client.json.send({type: "CHANGESET_REQ", data: data}); + }); +} + +function createTimesliderClientVars (padId, callback) +{ + var clientVars = { + viewId: padId, + colorPalette: ["#ffc7c7", "#fff1c7", "#e3ffc7", "#c7ffd5", "#c7ffff", "#c7d5ff", "#e3c7ff", "#ffc7f1", "#ff8f8f", "#ffe38f", "#c7ff8f", "#8fffab", "#8fffff", "#8fabff", "#c78fff", "#ff8fe3", "#d97979", "#d9c179", "#a9d979", "#79d991", "#79d9d9", "#7991d9", "#a979d9", "#d979c1", "#d9a9a9", "#d9cda9", "#c1d9a9", "#a9d9b5", "#a9d9d9", "#a9b5d9", "#c1a9d9", "#d9a9cd"], + sliderEnabled : true, + supportsSlider: true, + savedRevisions: [], + padIdForUrl: padId, + fullWidth: false, + disableRightBar: false, + initialChangesets: [], + abiwordAvailable: settings.abiwordAvailable(), + hooks: [], + initialStyledContents: {} + }; + var pad; + var initialChangesets = []; + + async.series([ + //get the pad from the database + function(callback) + { + padManager.getPad(padId, function(err, _pad) + { + if(ERR(err, callback)) return; + pad = _pad; + callback(); + }); + }, + //get all authors and add them to + function(callback) + { + var historicalAuthorData = {}; + //get all authors out of the attribut pool + var authors = pad.getAllAuthors(); + + //get all author data out of the database + async.forEach(authors, function(authorId, callback) + { + authorManager.getAuthor(authorId, function(err, author) + { + if(ERR(err, callback)) return; + historicalAuthorData[authorId] = author; + callback(); + }); + }, function(err) + { + if(ERR(err, callback)) return; + //add historicalAuthorData to the clientVars and continue + clientVars.historicalAuthorData = historicalAuthorData; + clientVars.initialStyledContents.historicalAuthorData = historicalAuthorData; + callback(); + }); + }, + //get the timestamp of the last revision + function(callback) + { + pad.getRevisionDate(pad.getHeadRevisionNumber(), function(err, date) + { + if(ERR(err, callback)) return; + clientVars.currentTime = date; + callback(); + }); + }, + function(callback) + { + //get the head revision Number + var lastRev = pad.getHeadRevisionNumber(); + + //add the revNum to the client Vars + clientVars.revNum = lastRev; + clientVars.totalRevs = lastRev; + + var atext = Changeset.cloneAText(pad.atext); + var attribsForWire = Changeset.prepareForWire(atext.attribs, pad.pool); + var apool = attribsForWire.pool.toJsonable(); + atext.attribs = attribsForWire.translated; + + clientVars.initialStyledContents.apool = apool; + clientVars.initialStyledContents.atext = atext; + + var granularities = [100, 10, 1]; + + //get the latest rough changesets + async.forEach(granularities, function(granularity, callback) + { + var topGranularity = granularity*10; + + getChangesetInfo(padId, Math.floor(lastRev / topGranularity)*topGranularity, + Math.floor(lastRev / topGranularity)*topGranularity+topGranularity, granularity, + function(err, changeset) + { + if(ERR(err, callback)) return; + clientVars.initialChangesets.push(changeset); + callback(); + }); + }, callback); + } + ], function(err) + { + if(ERR(err, callback)) return; + callback(null, clientVars); + }); +} + +/** + * Tries to rebuild the getChangestInfo function of the original Etherpad + * https://github.com/ether/pad/blob/master/etherpad/src/etherpad/control/pad/pad_changeset_control.js#L144 + */ +function getChangesetInfo(padId, startNum, endNum, granularity, callback) +{ + var forwardsChangesets = []; + var backwardsChangesets = []; + var timeDeltas = []; + var apool = AttributePoolFactory.createAttributePool(); + var pad; + var composedChangesets = {}; + var revisionDate = []; + var lines; + + async.series([ + //get the pad from the database + function(callback) + { + padManager.getPad(padId, function(err, _pad) + { + if(ERR(err, callback)) return; + pad = _pad; + callback(); + }); + }, + function(callback) + { + //calculate the last full endnum + var lastRev = pad.getHeadRevisionNumber(); + if (endNum > lastRev+1) { + endNum = lastRev+1; + } + endNum = Math.floor(endNum / granularity)*granularity; + + var compositesChangesetNeeded = []; + var revTimesNeeded = []; + + //figure out which composite Changeset and revTimes we need, to load them in bulk + var compositeStart = startNum; + while (compositeStart < endNum) + { + var compositeEnd = compositeStart + granularity; + + //add the composite Changeset we needed + compositesChangesetNeeded.push({start: compositeStart, end: compositeEnd}); + + //add the t1 time we need + revTimesNeeded.push(compositeStart == 0 ? 0 : compositeStart - 1); + //add the t2 time we need + revTimesNeeded.push(compositeEnd - 1); + + compositeStart += granularity; + } + + //get all needed db values parallel + async.parallel([ + function(callback) + { + //get all needed composite Changesets + async.forEach(compositesChangesetNeeded, function(item, callback) + { + composePadChangesets(padId, item.start, item.end, function(err, changeset) + { + if(ERR(err, callback)) return; + composedChangesets[item.start + "/" + item.end] = changeset; + callback(); + }); + }, callback); + }, + function(callback) + { + //get all needed revision Dates + async.forEach(revTimesNeeded, function(revNum, callback) + { + pad.getRevisionDate(revNum, function(err, revDate) + { + if(ERR(err, callback)) return; + revisionDate[revNum] = Math.floor(revDate/1000); + callback(); + }); + }, callback); + }, + //get the lines + function(callback) + { + getPadLines(padId, startNum-1, function(err, _lines) + { + if(ERR(err, callback)) return; + lines = _lines; + callback(); + }); + } + ], callback); + }, + //doesn't know what happens here excatly :/ + function(callback) + { + var compositeStart = startNum; + + while (compositeStart < endNum) + { + if (compositeStart + granularity > endNum) + { + break; + } + + var compositeEnd = compositeStart + granularity; + + var forwards = composedChangesets[compositeStart + "/" + compositeEnd]; + var backwards = Changeset.inverse(forwards, lines.textlines, lines.alines, pad.apool()); + + Changeset.mutateAttributionLines(forwards, lines.alines, pad.apool()); + Changeset.mutateTextLines(forwards, lines.textlines); + + var forwards2 = Changeset.moveOpsToNewPool(forwards, pad.apool(), apool); + var backwards2 = Changeset.moveOpsToNewPool(backwards, pad.apool(), apool); + + var t1, t2; + if (compositeStart == 0) + { + t1 = revisionDate[0]; + } + else + { + t1 = revisionDate[compositeStart - 1]; + } + + t2 = revisionDate[compositeEnd - 1]; + + timeDeltas.push(t2 - t1); + forwardsChangesets.push(forwards2); + backwardsChangesets.push(backwards2); + + compositeStart += granularity; + } + + callback(); + } + ], function(err) + { + if(ERR(err, callback)) return; + + callback(null, {forwardsChangesets: forwardsChangesets, + backwardsChangesets: backwardsChangesets, + apool: apool.toJsonable(), + actualEndNum: endNum, + timeDeltas: timeDeltas, + start: startNum, + granularity: granularity }); + }); +} + +/** + * Tries to rebuild the getPadLines function of the original Etherpad + * https://github.com/ether/pad/blob/master/etherpad/src/etherpad/control/pad/pad_changeset_control.js#L263 + */ +function getPadLines(padId, revNum, callback) +{ + var atext; + var result = {}; + var pad; + + async.series([ + //get the pad from the database + function(callback) + { + padManager.getPad(padId, function(err, _pad) + { + if(ERR(err, callback)) return; + pad = _pad; + callback(); + }); + }, + //get the atext + function(callback) + { + if(revNum >= 0) + { + pad.getInternalRevisionAText(revNum, function(err, _atext) + { + if(ERR(err, callback)) return; + atext = _atext; + callback(); + }); + } + else + { + atext = Changeset.makeAText("\n"); + callback(null); + } + }, + function(callback) + { + result.textlines = Changeset.splitTextLines(atext.text); + result.alines = Changeset.splitAttributionLines(atext.attribs, atext.text); + callback(null); + } + ], function(err) + { + if(ERR(err, callback)) return; + callback(null, result); + }); +} + +/** + * Tries to rebuild the composePadChangeset function of the original Etherpad + * https://github.com/ether/pad/blob/master/etherpad/src/etherpad/control/pad/pad_changeset_control.js#L241 + */ +function composePadChangesets(padId, startNum, endNum, callback) +{ + var pad; + var changesets = []; + var changeset; + + async.series([ + //get the pad from the database + function(callback) + { + padManager.getPad(padId, function(err, _pad) + { + if(ERR(err, callback)) return; + pad = _pad; + callback(); + }); + }, + //fetch all changesets we need + function(callback) + { + var changesetsNeeded=[]; + + //create a array for all changesets, we will + //replace the values with the changeset later + for(var r=startNum;r<endNum;r++) + { + changesetsNeeded.push(r); + } + + //get all changesets + async.forEach(changesetsNeeded, function(revNum,callback) + { + pad.getRevisionChangeset(revNum, function(err, value) + { + if(ERR(err, callback)) return; + changesets[revNum] = value; + callback(); + }); + },callback); + }, + //compose Changesets + function(callback) + { + changeset = changesets[startNum]; + var pool = pad.apool(); + + for(var r=startNum+1;r<endNum;r++) + { + var cs = changesets[r]; + changeset = Changeset.compose(changeset, cs, pool); + } + + callback(null); + } + ], + //return err and changeset + function(err) + { + if(ERR(err, callback)) return; + callback(null, changeset); + }); +} diff --git a/src/node/hooks/express/apicalls.js b/src/node/hooks/express/apicalls.js new file mode 100644 index 00000000..8a9751eb --- /dev/null +++ b/src/node/hooks/express/apicalls.js @@ -0,0 +1,58 @@ +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 +exports.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); +} + + +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"); + }); + }); +}
\ No newline at end of file diff --git a/src/node/hooks/express/errorhandling.js b/src/node/hooks/express/errorhandling.js new file mode 100644 index 00000000..cb8c5898 --- /dev/null +++ b/src/node/hooks/express/errorhandling.js @@ -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); +} diff --git a/src/node/hooks/express/importexport.js b/src/node/hooks/express/importexport.js new file mode 100644 index 00000000..9e78f34d --- /dev/null +++ b/src/node/hooks/express/importexport.js @@ -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); + }); + }); +} diff --git a/src/node/hooks/express/padreadonly.js b/src/node/hooks/express/padreadonly.js new file mode 100644 index 00000000..60ece0ad --- /dev/null +++ b/src/node/hooks/express/padreadonly.js @@ -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); + }); + }); + +}
\ No newline at end of file diff --git a/src/node/hooks/express/padurlsanitize.js b/src/node/hooks/express/padurlsanitize.js new file mode 100644 index 00000000..4f5dd7a5 --- /dev/null +++ b/src/node/hooks/express/padurlsanitize.js @@ -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(); + } + }); + } + }); +} diff --git a/src/node/hooks/express/socketio.js b/src/node/hooks/express/socketio.js new file mode 100644 index 00000000..e040f7ac --- /dev/null +++ b/src/node/hooks/express/socketio.js @@ -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}); +} diff --git a/src/node/hooks/express/specialpages.js b/src/node/hooks/express/specialpages.js new file mode 100644 index 00000000..13cfd821 --- /dev/null +++ b/src/node/hooks/express/specialpages.js @@ -0,0 +1,48 @@ +var path = require('path'); + +exports.expressCreateServer = function (hook_name, args, cb) { + + //serve index.html under / + args.app.get('/', function(req, res) + { + var filePath = path.normalize(__dirname + "/../../../static/index.html"); + res.sendfile(filePath, { maxAge: exports.maxAge }); + }); + + //serve robots.txt + args.app.get('/robots.txt', function(req, res) + { + var filePath = path.normalize(__dirname + "/../../../static/robots.txt"); + res.sendfile(filePath, { maxAge: exports.maxAge }); + }); + + //serve favicon.ico + args.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 }); + } + }); + }); + + //serve pad.html under /p + args.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 + args.app.get('/p/:pad/timeslider', function(req, res, next) + { + var filePath = path.normalize(__dirname + "/../../../static/timeslider.html"); + res.sendfile(filePath, { maxAge: exports.maxAge }); + }); + +}
\ No newline at end of file diff --git a/src/node/hooks/express/static.js b/src/node/hooks/express/static.js new file mode 100644 index 00000000..e8f9afbb --- /dev/null +++ b/src/node/hooks/express/static.js @@ -0,0 +1,42 @@ +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"); + +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) { + res.header("Content-Type","application/json; charset: utf-8"); + res.write(JSON.stringify({"plugins": plugins.plugins, "parts": plugins.parts})); + res.end(); + }); +} diff --git a/src/node/hooks/express/webaccess.js b/src/node/hooks/express/webaccess.js new file mode 100644 index 00000000..8e9f967a --- /dev/null +++ b/src/node/hooks/express/webaccess.js @@ -0,0 +1,36 @@ +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) { + 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); + } +} + +exports.expressConfigure = function (hook_name, args, cb) { + // Activate http basic auth if it has been defined in settings.json + if(settings.httpAuth != null) 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()); +} diff --git a/src/node/padaccess.js b/src/node/padaccess.js new file mode 100644 index 00000000..a3d1df33 --- /dev/null +++ b/src/node/padaccess.js @@ -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); + } + }); +} diff --git a/src/node/server.js b/src/node/server.js new file mode 100644 index 00000000..19df6e72 --- /dev/null +++ b/src/node/server.js @@ -0,0 +1,97 @@ +/** + * 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)"; + +//cache 6 hours +exports.maxAge = 1000*60*60*6; + +//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); + + callback(null); + } +]); diff --git a/src/node/utils/Abiword.js b/src/node/utils/Abiword.js new file mode 100644 index 00000000..27138e64 --- /dev/null +++ b/src/node/utils/Abiword.js @@ -0,0 +1,147 @@ +/** + * Controls the communication with the Abiword application + */ + +/* + * 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 util = require('util'); +var spawn = require('child_process').spawn; +var async = require("async"); +var settings = require("./Settings"); +var os = require('os'); + +var doConvertTask; + +//on windows we have to spawn a process for each convertion, cause the plugin abicommand doesn't exist on this platform +if(os.type().indexOf("Windows") > -1) +{ + var stdoutBuffer = ""; + + doConvertTask = function(task, callback) + { + //span an abiword process to perform the conversion + var abiword = spawn(settings.abiword, ["--to=" + task.destFile, task.srcFile]); + + //delegate the processing of stdout to another function + abiword.stdout.on('data', function (data) + { + //add data to buffer + stdoutBuffer+=data.toString(); + }); + + //append error messages to the buffer + abiword.stderr.on('data', function (data) + { + stdoutBuffer += data.toString(); + }); + + //throw exceptions if abiword is dieing + abiword.on('exit', function (code) + { + if(code != 0) { + return callback("Abiword died with exit code " + code); + } + + if(stdoutBuffer != "") + { + console.log(stdoutBuffer); + } + + callback(); + }); + } + + exports.convertFile = function(srcFile, destFile, type, callback) + { + doConvertTask({"srcFile": srcFile, "destFile": destFile, "type": type}, callback); + }; +} +//on unix operating systems, we can start abiword with abicommand and communicate with it via stdin/stdout +//thats much faster, about factor 10 +else +{ + //spawn the abiword process + var abiword; + var stdoutCallback = null; + var spawnAbiword = function (){ + abiword = spawn(settings.abiword, ["--plugin", "AbiCommand"]); + var stdoutBuffer = ""; + var firstPrompt = true; + + //append error messages to the buffer + abiword.stderr.on('data', function (data) + { + stdoutBuffer += data.toString(); + }); + + //abiword died, let's restart abiword and return an error with the callback + abiword.on('exit', function (code) + { + spawnAbiword(); + stdoutCallback("Abiword died with exit code " + code); + }); + + //delegate the processing of stdout to a other function + abiword.stdout.on('data',function (data) + { + //add data to buffer + stdoutBuffer+=data.toString(); + + //we're searching for the prompt, cause this means everything we need is in the buffer + if(stdoutBuffer.search("AbiWord:>") != -1) + { + //filter the feedback message + var err = stdoutBuffer.search("OK") != -1 ? null : stdoutBuffer; + + //reset the buffer + stdoutBuffer = ""; + + //call the callback with the error message + //skip the first prompt + if(stdoutCallback != null && !firstPrompt) + { + stdoutCallback(err); + stdoutCallback = null; + } + + firstPrompt = false; + } + }); + } + spawnAbiword(); + + doConvertTask = function(task, callback) + { + abiword.stdin.write("convert " + task.srcFile + " " + task.destFile + " " + task.type + "\n"); + + //create a callback that calls the task callback and the caller callback + stdoutCallback = function (err) + { + callback(); + console.log("queue continue"); + task.callback(err); + }; + } + + //Queue with the converts we have to do + var queue = async.queue(doConvertTask, 1); + + exports.convertFile = function(srcFile, destFile, type, callback) + { + queue.push({"srcFile": srcFile, "destFile": destFile, "type": type, "callback": callback}); + }; +} diff --git a/src/node/utils/Cli.js b/src/node/utils/Cli.js new file mode 100644 index 00000000..0c7947e9 --- /dev/null +++ b/src/node/utils/Cli.js @@ -0,0 +1,38 @@ +/** + * The CLI module handles command line parameters + */ + +/* + * 2012 Jordan Hollinger + * + * 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 object containing the parsed command-line options +exports.argv = {}; + +var argv = process.argv.slice(2); +var arg, prevArg; + +// Loop through args +for ( var i = 0; i < argv.length; i++ ) { + arg = argv[i]; + + // Override location of settings.json file + if ( prevArg == '--settings' || prevArg == '-s' ) { + exports.argv.settings = arg; + } + + prevArg = arg; +} diff --git a/src/node/utils/ExportDokuWiki.js b/src/node/utils/ExportDokuWiki.js new file mode 100644 index 00000000..bcb21108 --- /dev/null +++ b/src/node/utils/ExportDokuWiki.js @@ -0,0 +1,345 @@ +/** + * Copyright 2011 Adrian Lang + * + * 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 async = require("async"); + +var Changeset = require("ep_etherpad-lite/static/js/Changeset"); +var padManager = require("../db/PadManager"); + +function getPadDokuWiki(pad, revNum, callback) +{ + var atext = pad.atext; + var dokuwiki; + async.waterfall([ + // fetch revision atext + + + function (callback) + { + if (revNum != undefined) + { + pad.getInternalRevisionAText(revNum, function (err, revisionAtext) + { + atext = revisionAtext; + callback(err); + }); + } + else + { + callback(null); + } + }, + + // convert atext to dokuwiki text + + function (callback) + { + dokuwiki = getDokuWikiFromAtext(pad, atext); + callback(null); + }], + // run final callback + + + function (err) + { + callback(err, dokuwiki); + }); +} + +function getDokuWikiFromAtext(pad, atext) +{ + var apool = pad.apool(); + var textLines = atext.text.slice(0, -1).split('\n'); + var attribLines = Changeset.splitAttributionLines(atext.attribs, atext.text); + + var tags = ['======', '=====', '**', '//', '__', 'del>']; + var props = ['heading1', 'heading2', 'bold', 'italic', 'underline', 'strikethrough']; + var anumMap = {}; + + props.forEach(function (propName, i) + { + var propTrueNum = apool.putAttrib([propName, true], true); + if (propTrueNum >= 0) + { + anumMap[propTrueNum] = i; + } + }); + + function getLineDokuWiki(text, attribs) + { + var propVals = [false, false, false]; + var ENTER = 1; + var STAY = 2; + var LEAVE = 0; + + // Use order of tags (b/i/u) as order of nesting, for simplicity + // and decent nesting. For example, + // <b>Just bold<b> <b><i>Bold and italics</i></b> <i>Just italics</i> + // becomes + // <b>Just bold <i>Bold and italics</i></b> <i>Just italics</i> + var taker = Changeset.stringIterator(text); + var assem = Changeset.stringAssembler(); + + function emitOpenTag(i) + { + if (tags[i].indexOf('>') !== -1) { + assem.append('<'); + } + assem.append(tags[i]); + } + + function emitCloseTag(i) + { + if (tags[i].indexOf('>') !== -1) { + assem.append('</'); + } + assem.append(tags[i]); + } + + var urls = _findURLs(text); + + var idx = 0; + + function processNextChars(numChars) + { + if (numChars <= 0) + { + return; + } + + var iter = Changeset.opIterator(Changeset.subattribution(attribs, idx, idx + numChars)); + idx += numChars; + + while (iter.hasNext()) + { + var o = iter.next(); + var propChanged = false; + Changeset.eachAttribNumber(o.attribs, function (a) + { + if (a in anumMap) + { + var i = anumMap[a]; // i = 0 => bold, etc. + if (!propVals[i]) + { + propVals[i] = ENTER; + propChanged = true; + } + else + { + propVals[i] = STAY; + } + } + }); + for (var i = 0; i < propVals.length; i++) + { + if (propVals[i] === true) + { + propVals[i] = LEAVE; + propChanged = true; + } + else if (propVals[i] === STAY) + { + propVals[i] = true; // set it back + } + } + // now each member of propVal is in {false,LEAVE,ENTER,true} + // according to what happens at start of span + if (propChanged) + { + // leaving bold (e.g.) also leaves italics, etc. + var left = false; + for (var i = 0; i < propVals.length; i++) + { + var v = propVals[i]; + if (!left) + { + if (v === LEAVE) + { + left = true; + } + } + else + { + if (v === true) + { + propVals[i] = STAY; // tag will be closed and re-opened + } + } + } + + for (var i = propVals.length - 1; i >= 0; i--) + { + if (propVals[i] === LEAVE) + { + emitCloseTag(i); + propVals[i] = false; + } + else if (propVals[i] === STAY) + { + emitCloseTag(i); + } + } + for (var i = 0; i < propVals.length; i++) + { + if (propVals[i] === ENTER || propVals[i] === STAY) + { + emitOpenTag(i); + propVals[i] = true; + } + } + // propVals is now all {true,false} again + } // end if (propChanged) + var chars = o.chars; + if (o.lines) + { + chars--; // exclude newline at end of line, if present + } + var s = taker.take(chars); + + assem.append(_escapeDokuWiki(s)); + } // end iteration over spans in line + for (var i = propVals.length - 1; i >= 0; i--) + { + if (propVals[i]) + { + emitCloseTag(i); + propVals[i] = false; + } + } + } // end processNextChars + if (urls) + { + urls.forEach(function (urlData) + { + var startIndex = urlData[0]; + var url = urlData[1]; + var urlLength = url.length; + processNextChars(startIndex - idx); + assem.append('[['); + + // Do not use processNextChars since a link does not contain syntax and + // needs no escaping + var iter = Changeset.opIterator(Changeset.subattribution(attribs, idx, idx + urlLength)); + idx += urlLength; + assem.append(taker.take(iter.next().chars)); + + assem.append(']]'); + }); + } + processNextChars(text.length - idx); + + return assem.toString() + "\n"; + } // end getLineDokuWiki + var pieces = []; + + for (var i = 0; i < textLines.length; i++) + { + var line = _analyzeLine(textLines[i], attribLines[i], apool); + var lineContent = getLineDokuWiki(line.text, line.aline); + + if (line.listLevel && lineContent) + { + pieces.push(new Array(line.listLevel + 1).join(' ') + '* '); + } + pieces.push(lineContent); + } + + return pieces.join(''); +} + +function _analyzeLine(text, aline, apool) +{ + var line = {}; + + // identify list + var lineMarker = 0; + line.listLevel = 0; + if (aline) + { + var opIter = Changeset.opIterator(aline); + if (opIter.hasNext()) + { + var listType = Changeset.opAttributeValue(opIter.next(), 'list', apool); + if (listType) + { + lineMarker = 1; + listType = /([a-z]+)([12345678])/.exec(listType); + if (listType) + { + line.listTypeName = listType[1]; + line.listLevel = Number(listType[2]); + } + } + } + } + if (lineMarker) + { + line.text = text.substring(1); + line.aline = Changeset.subattribution(aline, 1); + } + else + { + line.text = text; + line.aline = aline; + } + + return line; +} + +exports.getPadDokuWikiDocument = function (padId, revNum, callback) +{ + padManager.getPad(padId, function (err, pad) + { + if (err) + { + callback(err); + return; + } + + getPadDokuWiki(pad, revNum, callback); + }); +} + +function _escapeDokuWiki(s) +{ + s = s.replace(/(\/\/|\*\*|__)/g, '%%$1%%'); + return s; +} + +// copied from ACE +var _REGEX_WORDCHAR = /[\u0030-\u0039\u0041-\u005A\u0061-\u007A\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u00FF\u0100-\u1FFF\u3040-\u9FFF\uF900-\uFDFF\uFE70-\uFEFE\uFF10-\uFF19\uFF21-\uFF3A\uFF41-\uFF5A\uFF66-\uFFDC]/; +var _REGEX_SPACE = /\s/; +var _REGEX_URLCHAR = new RegExp('(' + /[-:@a-zA-Z0-9_.,~%+\/\\?=&#;()$]/.source + '|' + _REGEX_WORDCHAR.source + ')'); +var _REGEX_URL = new RegExp(/(?:(?:https?|s?ftp|ftps|file|smb|afp|nfs|(x-)?man|gopher|txmt):\/\/|mailto:)/.source + _REGEX_URLCHAR.source + '*(?![:.,;])' + _REGEX_URLCHAR.source, 'g'); + +// returns null if no URLs, or [[startIndex1, url1], [startIndex2, url2], ...] + + +function _findURLs(text) +{ + _REGEX_URL.lastIndex = 0; + var urls = null; + var execResult; + while ((execResult = _REGEX_URL.exec(text))) + { + urls = (urls || []); + var startIndex = execResult.index; + var url = execResult[0]; + urls.push([startIndex, url]); + } + + return urls; +} diff --git a/src/node/utils/ExportHtml.js b/src/node/utils/ExportHtml.js new file mode 100644 index 00000000..91ebe59f --- /dev/null +++ b/src/node/utils/ExportHtml.js @@ -0,0 +1,595 @@ +/** + * 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. + */ + + +var async = require("async"); +var Changeset = require("ep_etherpad-lite/static/js/Changeset"); +var padManager = require("../db/PadManager"); +var ERR = require("async-stacktrace"); +var Security = require('ep_etherpad-lite/static/js/security'); + +function getPadPlainText(pad, revNum) +{ + var atext = ((revNum !== undefined) ? pad.getInternalRevisionAText(revNum) : pad.atext()); + var textLines = atext.text.slice(0, -1).split('\n'); + var attribLines = Changeset.splitAttributionLines(atext.attribs, atext.text); + var apool = pad.pool(); + + var pieces = []; + for (var i = 0; i < textLines.length; i++) + { + var line = _analyzeLine(textLines[i], attribLines[i], apool); + if (line.listLevel) + { + var numSpaces = line.listLevel * 2 - 1; + var bullet = '*'; + pieces.push(new Array(numSpaces + 1).join(' '), bullet, ' ', line.text, '\n'); + } + else + { + pieces.push(line.text, '\n'); + } + } + + return pieces.join(''); +} + +function getPadHTML(pad, revNum, callback) +{ + var atext = pad.atext; + var html; + async.waterfall([ + // fetch revision atext + + + function (callback) + { + if (revNum != undefined) + { + pad.getInternalRevisionAText(revNum, function (err, revisionAtext) + { + if(ERR(err, callback)) return; + atext = revisionAtext; + callback(); + }); + } + else + { + callback(null); + } + }, + + // convert atext to html + + + function (callback) + { + html = getHTMLFromAtext(pad, atext); + callback(null); + }], + // run final callback + + + function (err) + { + if(ERR(err, callback)) return; + callback(null, html); + }); +} + +exports.getPadHTML = getPadHTML; + +function getHTMLFromAtext(pad, atext) +{ + var apool = pad.apool(); + var textLines = atext.text.slice(0, -1).split('\n'); + var attribLines = Changeset.splitAttributionLines(atext.attribs, atext.text); + + var tags = ['h1', 'h2', 'strong', 'em', 'u', 's']; + var props = ['heading1', 'heading2', 'bold', 'italic', 'underline', 'strikethrough']; + var anumMap = {}; + + props.forEach(function (propName, i) + { + var propTrueNum = apool.putAttrib([propName, true], true); + if (propTrueNum >= 0) + { + anumMap[propTrueNum] = i; + } + }); + + function getLineHTML(text, attribs) + { + var propVals = [false, false, false]; + var ENTER = 1; + var STAY = 2; + var LEAVE = 0; + + // Use order of tags (b/i/u) as order of nesting, for simplicity + // and decent nesting. For example, + // <b>Just bold<b> <b><i>Bold and italics</i></b> <i>Just italics</i> + // becomes + // <b>Just bold <i>Bold and italics</i></b> <i>Just italics</i> + var taker = Changeset.stringIterator(text); + var assem = Changeset.stringAssembler(); + + var openTags = []; + function emitOpenTag(i) + { + openTags.unshift(i); + assem.append('<'); + assem.append(tags[i]); + assem.append('>'); + } + + function emitCloseTag(i) + { + openTags.shift(); + assem.append('</'); + assem.append(tags[i]); + assem.append('>'); + } + + function orderdCloseTags(tags2close) + { + for(var i=0;i<openTags.length;i++) + { + for(var j=0;j<tags2close.length;j++) + { + if(tags2close[j] == openTags[i]) + { + emitCloseTag(tags2close[j]); + i--; + break; + } + } + } + } + + var urls = _findURLs(text); + + var idx = 0; + + function processNextChars(numChars) + { + if (numChars <= 0) + { + return; + } + + var iter = Changeset.opIterator(Changeset.subattribution(attribs, idx, idx + numChars)); + idx += numChars; + + while (iter.hasNext()) + { + var o = iter.next(); + var propChanged = false; + Changeset.eachAttribNumber(o.attribs, function (a) + { + if (a in anumMap) + { + var i = anumMap[a]; // i = 0 => bold, etc. + if (!propVals[i]) + { + propVals[i] = ENTER; + propChanged = true; + } + else + { + propVals[i] = STAY; + } + } + }); + for (var i = 0; i < propVals.length; i++) + { + if (propVals[i] === true) + { + propVals[i] = LEAVE; + propChanged = true; + } + else if (propVals[i] === STAY) + { + propVals[i] = true; // set it back + } + } + // now each member of propVal is in {false,LEAVE,ENTER,true} + // according to what happens at start of span + if (propChanged) + { + // leaving bold (e.g.) also leaves italics, etc. + var left = false; + for (var i = 0; i < propVals.length; i++) + { + var v = propVals[i]; + if (!left) + { + if (v === LEAVE) + { + left = true; + } + } + else + { + if (v === true) + { + propVals[i] = STAY; // tag will be closed and re-opened + } + } + } + + var tags2close = []; + + for (var i = propVals.length - 1; i >= 0; i--) + { + if (propVals[i] === LEAVE) + { + //emitCloseTag(i); + tags2close.push(i); + propVals[i] = false; + } + else if (propVals[i] === STAY) + { + //emitCloseTag(i); + tags2close.push(i); + } + } + + orderdCloseTags(tags2close); + + for (var i = 0; i < propVals.length; i++) + { + if (propVals[i] === ENTER || propVals[i] === STAY) + { + emitOpenTag(i); + propVals[i] = true; + } + } + // propVals is now all {true,false} again + } // end if (propChanged) + var chars = o.chars; + if (o.lines) + { + chars--; // exclude newline at end of line, if present + } + + var s = taker.take(chars); + + //removes the characters with the code 12. Don't know where they come + //from but they break the abiword parser and are completly useless + s = s.replace(String.fromCharCode(12), ""); + + assem.append(_encodeWhitespace(Security.escapeHTML(s))); + } // end iteration over spans in line + + var tags2close = []; + for (var i = propVals.length - 1; i >= 0; i--) + { + if (propVals[i]) + { + tags2close.push(i); + propVals[i] = false; + } + } + + orderdCloseTags(tags2close); + } // end processNextChars + if (urls) + { + urls.forEach(function (urlData) + { + var startIndex = urlData[0]; + var url = urlData[1]; + var urlLength = url.length; + processNextChars(startIndex - idx); + assem.append('<a href="' + Security.escapeHTMLAttribute(url) + '">'); + processNextChars(urlLength); + assem.append('</a>'); + }); + } + processNextChars(text.length - idx); + + return _processSpaces(assem.toString()); + } // end getLineHTML + var pieces = []; + + // Need to deal with constraints imposed on HTML lists; can + // only gain one level of nesting at once, can't change type + // mid-list, etc. + // People might use weird indenting, e.g. skip a level, + // so we want to do something reasonable there. We also + // want to deal gracefully with blank lines. + // => keeps track of the parents level of indentation + var lists = []; // e.g. [[1,'bullet'], [3,'bullet'], ...] + for (var i = 0; i < textLines.length; i++) + { + var line = _analyzeLine(textLines[i], attribLines[i], apool); + var lineContent = getLineHTML(line.text, line.aline); + + if (line.listLevel)//If we are inside a list + { + // do list stuff + var whichList = -1; // index into lists or -1 + if (line.listLevel) + { + whichList = lists.length; + for (var j = lists.length - 1; j >= 0; j--) + { + if (line.listLevel <= lists[j][0]) + { + whichList = j; + } + } + } + + if (whichList >= lists.length)//means we are on a deeper level of indentation than the previous line + { + lists.push([line.listLevel, line.listTypeName]); + if(line.listTypeName == "number") + { + pieces.push('<ol class="'+line.listTypeName+'"><li>', lineContent || '<br>'); + } + else + { + pieces.push('<ul class="'+line.listTypeName+'"><li>', lineContent || '<br>'); + } + } + //the following code *seems* dead after my patch. + //I keep it just in case I'm wrong... + /*else if (whichList == -1)//means we are not inside a list + { + if (line.text) + { + console.log('trace 1'); + // non-blank line, end all lists + if(line.listTypeName == "number") + { + pieces.push(new Array(lists.length + 1).join('</li></ol>')); + } + else + { + pieces.push(new Array(lists.length + 1).join('</li></ul>')); + } + lists.length = 0; + pieces.push(lineContent, '<br>'); + } + else + { + console.log('trace 2'); + pieces.push('<br><br>'); + } + }*/ + else//means we are getting closer to the lowest level of indentation + { + while (whichList < lists.length - 1) + { + if(lists[lists.length - 1][1] == "number") + { + pieces.push('</li></ol>'); + } + else + { + pieces.push('</li></ul>'); + } + lists.length--; + } + pieces.push('</li><li>', lineContent || '<br>'); + } + } + else//outside any list + { + while (lists.length > 0)//if was in a list: close it before + { + if(lists[lists.length - 1][1] == "number") + { + pieces.push('</li></ol>'); + } + else + { + pieces.push('</li></ul>'); + } + lists.length--; + } + pieces.push(lineContent, '<br>'); + } + } + + for (var k = lists.length - 1; k >= 0; k--) + { + if(lists[k][1] == "number") + { + pieces.push('</li></ol>'); + } + else + { + pieces.push('</li></ul>'); + } + } + + return pieces.join(''); +} + +function _analyzeLine(text, aline, apool) +{ + var line = {}; + + // identify list + var lineMarker = 0; + line.listLevel = 0; + if (aline) + { + var opIter = Changeset.opIterator(aline); + if (opIter.hasNext()) + { + var listType = Changeset.opAttributeValue(opIter.next(), 'list', apool); + if (listType) + { + lineMarker = 1; + listType = /([a-z]+)([12345678])/.exec(listType); + if (listType) + { + line.listTypeName = listType[1]; + line.listLevel = Number(listType[2]); + } + } + } + } + if (lineMarker) + { + line.text = text.substring(1); + line.aline = Changeset.subattribution(aline, 1); + } + else + { + line.text = text; + line.aline = aline; + } + + return line; +} + +exports.getPadHTMLDocument = function (padId, revNum, noDocType, callback) +{ + padManager.getPad(padId, function (err, pad) + { + if(ERR(err, callback)) return; + + var head = + (noDocType ? '' : '<!doctype html>\n') + + '<html lang="en">\n' + (noDocType ? '' : '<head>\n' + + '<meta charset="utf-8">\n' + + '<style> * { font-family: arial, sans-serif;\n' + + 'font-size: 13px;\n' + + 'line-height: 17px; }' + + 'ul.indent { list-style-type: none; }' + + 'ol { list-style-type: decimal; }' + + 'ol ol { list-style-type: lower-latin; }' + + 'ol ol ol { list-style-type: lower-roman; }' + + 'ol ol ol ol { list-style-type: decimal; }' + + 'ol ol ol ol ol { list-style-type: lower-latin; }' + + 'ol ol ol ol ol ol{ list-style-type: lower-roman; }' + + 'ol ol ol ol ol ol ol { list-style-type: decimal; }' + + 'ol ol ol ol ol ol ol ol{ list-style-type: lower-latin; }' + + '</style>\n' + '</head>\n') + + '<body>'; + + var foot = '</body>\n</html>\n'; + + getPadHTML(pad, revNum, function (err, html) + { + if(ERR(err, callback)) return; + callback(null, head + html + foot); + }); + }); +} + +function _encodeWhitespace(s) { + return s.replace(/[^\x21-\x7E\s\t\n\r]/g, function(c) + { + return "&#" +c.charCodeAt(0) + ";" + }); +} + +// copied from ACE + + +function _processSpaces(s) +{ + var doesWrap = true; + if (s.indexOf("<") < 0 && !doesWrap) + { + // short-cut + return s.replace(/ /g, ' '); + } + var parts = []; + s.replace(/<[^>]*>?| |[^ <]+/g, function (m) + { + parts.push(m); + }); + if (doesWrap) + { + var endOfLine = true; + var beforeSpace = false; + // last space in a run is normal, others are nbsp, + // end of line is nbsp + for (var i = parts.length - 1; i >= 0; i--) + { + var p = parts[i]; + if (p == " ") + { + if (endOfLine || beforeSpace) parts[i] = ' '; + endOfLine = false; + beforeSpace = true; + } + else if (p.charAt(0) != "<") + { + endOfLine = false; + beforeSpace = false; + } + } + // beginning of line is nbsp + for (var i = 0; i < parts.length; i++) + { + var p = parts[i]; + if (p == " ") + { + parts[i] = ' '; + break; + } + else if (p.charAt(0) != "<") + { + break; + } + } + } + else + { + for (var i = 0; i < parts.length; i++) + { + var p = parts[i]; + if (p == " ") + { + parts[i] = ' '; + } + } + } + return parts.join(''); +} + + +// copied from ACE +var _REGEX_WORDCHAR = /[\u0030-\u0039\u0041-\u005A\u0061-\u007A\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u00FF\u0100-\u1FFF\u3040-\u9FFF\uF900-\uFDFF\uFE70-\uFEFE\uFF10-\uFF19\uFF21-\uFF3A\uFF41-\uFF5A\uFF66-\uFFDC]/; +var _REGEX_SPACE = /\s/; +var _REGEX_URLCHAR = new RegExp('(' + /[-:@a-zA-Z0-9_.,~%+\/\\?=&#;()$]/.source + '|' + _REGEX_WORDCHAR.source + ')'); +var _REGEX_URL = new RegExp(/(?:(?:https?|s?ftp|ftps|file|smb|afp|nfs|(x-)?man|gopher|txmt):\/\/|mailto:)/.source + _REGEX_URLCHAR.source + '*(?![:.,;])' + _REGEX_URLCHAR.source, 'g'); + +// returns null if no URLs, or [[startIndex1, url1], [startIndex2, url2], ...] + + +function _findURLs(text) +{ + _REGEX_URL.lastIndex = 0; + var urls = null; + var execResult; + while ((execResult = _REGEX_URL.exec(text))) + { + urls = (urls || []); + var startIndex = execResult.index; + var url = execResult[0]; + urls.push([startIndex, url]); + } + + return urls; +} diff --git a/src/node/utils/ImportHtml.js b/src/node/utils/ImportHtml.js new file mode 100644 index 00000000..4b50b032 --- /dev/null +++ b/src/node/utils/ImportHtml.js @@ -0,0 +1,93 @@ +/** + * Copyright Yaco Sistemas S.L. 2011. + * + * 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 jsdom = require('jsdom-nocontextifiy').jsdom; +var log4js = require('log4js'); + + +var Changeset = require("ep_etherpad-lite/static/js/Changeset"); +var contentcollector = require("ep_etherpad-lite/static/js/contentcollector"); +var map = require("ep_etherpad-lite/static/js/ace2_common").map; + +function setPadHTML(pad, html, callback) +{ + var apiLogger = log4js.getLogger("ImportHtml"); + + // Clean the pad. This makes the rest of the code easier + // by several orders of magnitude. + pad.setText(""); + var padText = pad.text(); + + // Parse the incoming HTML with jsdom + var doc = jsdom(html.replace(/>\n+</g, '><')); + apiLogger.debug('html:'); + apiLogger.debug(html); + + // Convert a dom tree into a list of lines and attribute liens + // using the content collector object + var cc = contentcollector.makeContentCollector(true, null, pad.pool); + cc.collectContent(doc.childNodes[0]); + var result = cc.finish(); + apiLogger.debug('Lines:'); + var i; + for (i = 0; i < result.lines.length; i += 1) + { + apiLogger.debug('Line ' + (i + 1) + ' text: ' + result.lines[i]); + apiLogger.debug('Line ' + (i + 1) + ' attributes: ' + result.lineAttribs[i]); + } + + // Get the new plain text and its attributes + var newText = map(result.lines, function (e) { + return e + '\n'; + }).join(''); + apiLogger.debug('newText:'); + apiLogger.debug(newText); + var newAttribs = result.lineAttribs.join('|1+1') + '|1+1'; + + function eachAttribRun(attribs, func /*(startInNewText, endInNewText, attribs)*/ ) + { + var attribsIter = Changeset.opIterator(attribs); + var textIndex = 0; + var newTextStart = 0; + var newTextEnd = newText.length - 1; + while (attribsIter.hasNext()) + { + var op = attribsIter.next(); + var nextIndex = textIndex + op.chars; + if (!(nextIndex <= newTextStart || textIndex >= newTextEnd)) + { + func(Math.max(newTextStart, textIndex), Math.min(newTextEnd, nextIndex), op.attribs); + } + textIndex = nextIndex; + } + } + + // create a new changeset with a helper builder object + var builder = Changeset.builder(1); + + // assemble each line into the builder + eachAttribRun(newAttribs, function(start, end, attribs) + { + builder.insert(newText.substring(start, end), attribs); + }); + + // the changeset is ready! + var theChangeset = builder.toString(); + apiLogger.debug('The changeset: ' + theChangeset); + pad.appendRevision(theChangeset); +} + +exports.setPadHTML = setPadHTML; diff --git a/src/node/utils/Minify.js b/src/node/utils/Minify.js new file mode 100644 index 00000000..f569d4b9 --- /dev/null +++ b/src/node/utils/Minify.js @@ -0,0 +1,319 @@ +/** + * 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 plugins = require("ep_etherpad-lite/static/js/pluginfw/plugins"); +var RequireKernel = require('require-kernel'); +var server = require('../server'); + +var ROOT_DIR = path.normalize(__dirname + "/../../static/"); +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. +var LIBRARY_PREFIX = 'ep_etherpad-lite/static/js'; +exports.tar = {}; +for (var key in tar) { + exports.tar[LIBRARY_PREFIX + '/' + key] = + tar[key].map(function (p) {return LIBRARY_PREFIX + '/' + p}).concat( + tar[key].map(function (p) { + return LIBRARY_PREFIX + '/' + 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) +{ + 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; + } + + /* 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? + // 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, {}); + 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 + var resourceURI = baseURI + path.normalize(path.join('/static/', filename)); + resourceURI = resourceURI.replace(/\\/g, '/'); // Windows (safe generally?) + + request(resourceURI, 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'; +} + +function getFileCompressed(filename, contentType, callback) { + getFile(filename, function (error, content) { + if (error || !content) { + callback(error, content); + } else { + 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); + } + }); +} + +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); +} diff --git a/src/node/utils/Minify.js.rej b/src/node/utils/Minify.js.rej new file mode 100644 index 00000000..f09f49dc --- /dev/null +++ b/src/node/utils/Minify.js.rej @@ -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); +} diff --git a/src/node/utils/Settings.js b/src/node/utils/Settings.js new file mode 100644 index 00000000..24237de4 --- /dev/null +++ b/src/node/utils/Settings.js @@ -0,0 +1,146 @@ +/** + * The Settings Modul reads the settings out of settings.json and provides + * this information to the other modules + */ + +/* + * 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 fs = require("fs"); +var os = require("os"); +var path = require('path'); +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 + */ +exports.ip = "0.0.0.0"; + +/** + * The Port ep-lite should listen to + */ +exports.port = 9001; +/* + * The Type of the database + */ +exports.dbType = "dirty"; +/** + * This setting is passed with dbType to ueberDB to set up the database + */ +exports.dbSettings = { "filename" : path.join(exports.root, "var/dirty.db") }; +/** + * The default Text of a new pad + */ +exports.defaultPadText = "Welcome to Etherpad Lite!\n\nThis pad text is synchronized as you type, so that everyone viewing this page sees the same text. This allows you to collaborate seamlessly on documents!\n\nEtherpad Lite on Github: http:\/\/j.mp/ep-lite\n"; + +/** + * A flag that requires any user to have a valid session (via the api) before accessing a pad + */ +exports.requireSession = false; + +/** + * A flag that prevents users from creating new pads + */ +exports.editOnly = false; + +/** + * Max age that responses will have (affects caching layer). + */ +exports.maxAge = 1000*60*60*6; // 6 hours + +/** + * A flag that shows if minification is enabled or not + */ +exports.minify = true; + +/** + * The path of the abiword executable + */ +exports.abiword = null; + +/** + * The log level of log4js + */ +exports.loglevel = "INFO"; + +/** + * Http basic auth, with "user:password" format + */ +exports.httpAuth = null; + +//checks if abiword is avaiable +exports.abiwordAvailable = function() +{ + if(exports.abiword != null) + { + return os.type().indexOf("Windows") != -1 ? "withoutPDF" : "yes"; + } + else + { + return "no"; + } +} + +// Discover where the settings file lives +var settingsFilename = argv.settings || "settings.json"; +if (settingsFilename.charAt(0) != '/') { + settingsFilename = path.normalize(path.join(root, settingsFilename)); +} + +//read the settings sync +var settingsStr = fs.readFileSync(settingsFilename).toString(); + +//remove all comments +settingsStr = settingsStr.replace(/\*([^*]|[\r\n]|(\*+([^*/]|[\r\n])))*\*+/gm,"").replace(/#.*/g,"").replace(/\/\/.*/g,""); + +//try to parse the settings +var settings; +try +{ + settings = JSON.parse(settingsStr); +} +catch(e) +{ + console.error("There is a syntax error in your settings.json file"); + console.error(e.message); + process.exit(1); +} + +//loop trough the settings +for(var i in settings) +{ + //test if the setting start with a low character + if(i.charAt(0).search("[a-z]") !== 0) + { + console.warn("Settings should start with a low character: '" + i + "'"); + } + + //we know this setting, so we overwrite it + if(exports[i] !== undefined) + { + exports[i] = settings[i]; + } + //this setting is unkown, output a warning and throw it away + else + { + console.warn("Unkown Setting: '" + i + "'"); + console.warn("This setting doesn't exist or it was removed"); + } +} diff --git a/src/node/utils/caching_middleware.js b/src/node/utils/caching_middleware.js new file mode 100644 index 00000000..b8b7e1f1 --- /dev/null +++ b/src/node/utils/caching_middleware.js @@ -0,0 +1,179 @@ +/* + * 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 async = require('async'); +var Buffer = require('buffer').Buffer; +var fs = require('fs'); +var path = require('path'); +var server = require('../server'); +var zlib = require('zlib'); +var util = require('util'); + +var ROOT_DIR = path.normalize(__dirname + "/../"); +var CACHE_DIR = path.normalize(ROOT_DIR + '../../var/'); +console.log(CACHE_DIR) +CACHE_DIR = path.existsSync(CACHE_DIR) ? CACHE_DIR : undefined; + +var responseCache = {}; + +/* + This caches and compresses 200 and 404 responses to GET and HEAD requests. + TODO: Caching and compressing are solved problems, a middleware configuration + should replace this. +*/ + +function CachingMiddleware() { +} +CachingMiddleware.prototype = new function () { + function handle(req, res, next) { + if (!(req.method == "GET" || req.method == "HEAD") || !CACHE_DIR) { + return next(undefined, req, res); + } + + var old_req = {}; + var old_res = {}; + + var supportsGzip = + req.header('Accept-Encoding', '').indexOf('gzip') != -1; + + var path = require('url').parse(req.url).path; + var cacheKey = (new Buffer(path)).toString('base64').replace(/[\/\+=]/g, ''); + + fs.stat(CACHE_DIR + 'minified_' + cacheKey, function (error, stats) { + var modifiedSince = (req.headers['if-modified-since'] + && new Date(req.headers['if-modified-since'])); + var lastModifiedCache = !error && stats.mtime; + if (lastModifiedCache && responseCache[cacheKey]) { + req.headers['if-modified-since'] = lastModifiedCache.toUTCString(); + } else { + delete req.headers['if-modified-since']; + } + + // Always issue get to downstream. + old_req.method = req.method; + req.method = 'GET'; + + var expirationDate = new Date(((responseCache[cacheKey] || {}).headers || {})['expires']); + if (expirationDate > new Date()) { + // Our cached version is still valid. + return respond(); + } + + var _headers = {}; + old_res.setHeader = res.setHeader; + res.setHeader = function (key, value) { + _headers[key.toLowerCase()] = value; + old_res.setHeader.call(res, key, value); + }; + + old_res.writeHead = res.writeHead; + res.writeHead = function (status, headers) { + var lastModified = (res.getHeader('last-modified') + && new Date(res.getHeader('last-modified'))); + + res.writeHead = old_res.writeHead; + if (status == 200) { + // Update cache + var buffer = ''; + + Object.keys(headers || {}).forEach(function (key) { + res.setHeader(key, headers[key]); + }); + headers = _headers; + + old_res.write = res.write; + old_res.end = res.end; + res.write = function(data, encoding) { + buffer += data.toString(encoding); + }; + res.end = function(data, encoding) { + async.parallel([ + function (callback) { + var path = CACHE_DIR + 'minified_' + cacheKey; + fs.writeFile(path, buffer, function (error, stats) { + callback(); + }); + } + , function (callback) { + var path = CACHE_DIR + 'minified_' + cacheKey + '.gz'; + zlib.gzip(buffer, function(error, content) { + if (error) { + callback(); + } else { + fs.writeFile(path, content, function (error, stats) { + callback(); + }); + } + }); + } + ], function () { + responseCache[cacheKey] = {statusCode: status, headers: headers}; + respond(); + }); + }; + } else if (status == 304) { + // Nothing new changed from the cached version. + old_res.write = res.write; + old_res.end = res.end; + res.write = function(data, encoding) {}; + res.end = function(data, encoding) { respond() }; + } else { + res.writeHead(status, headers); + } + }; + + next(undefined, req, res); + + // This handles read/write synchronization as well as its predecessor, + // which is to say, not at all. + // TODO: Implement locking on write or ditch caching of gzip and use + // existing middlewares. + function respond() { + req.method = old_req.method || req.method; + res.write = old_res.write || res.write; + res.end = old_res.end || res.end; + + var headers = responseCache[cacheKey].headers; + var statusCode = responseCache[cacheKey].statusCode; + + var pathStr = CACHE_DIR + 'minified_' + cacheKey; + if (supportsGzip && (headers['content-type'] || '').match(/^text\//)) { + pathStr = pathStr + '.gz'; + headers['content-encoding'] = 'gzip'; + } + + var lastModified = (headers['last-modified'] + && new Date(headers['last-modified'])); + + if (statusCode == 200 && lastModified <= modifiedSince) { + res.writeHead(304, headers); + res.end(); + } else if (req.method == 'GET') { + var readStream = fs.createReadStream(pathStr); + res.writeHead(statusCode, headers); + util.pump(readStream, res); + } else { + res.writeHead(statusCode, headers); + res.end(); + } + } + }); + } + + this.handle = handle; +}(); + +module.exports = CachingMiddleware; diff --git a/src/node/utils/customError.js b/src/node/utils/customError.js new file mode 100644 index 00000000..5ca7a7a4 --- /dev/null +++ b/src/node/utils/customError.js @@ -0,0 +1,17 @@ +/* + This helper modules allows us to create different type of errors we can throw +*/ +function customError(message, errorName) +{ + this.name = errorName || "Error"; + this.message = message; + + var stackParts = new Error().stack.split("\n"); + stackParts.splice(0,2); + stackParts.unshift(this.name + ": " + message); + + this.stack = stackParts.join("\n"); +} +customError.prototype = Error.prototype; + +module.exports = customError; diff --git a/src/node/utils/tar.json b/src/node/utils/tar.json new file mode 100644 index 00000000..a905eb44 --- /dev/null +++ b/src/node/utils/tar.json @@ -0,0 +1,68 @@ +{ + "pad.js": [ + "jquery.js" + , "security.js" + , "pad.js" + , "ace2_common.js" + , "pad_utils.js" + , "undo-xpopup.js" + , "json2.js" + , "pad_cookie.js" + , "pad_editor.js" + , "pad_editbar.js" + , "pad_docbar.js" + , "pad_modals.js" + , "ace.js" + , "collab_client.js" + , "pad_userlist.js" + , "pad_impexp.js" + , "pad_savedrevs.js" + , "pad_connectionstatus.js" + , "chat.js" + , "excanvas.js" + , "farbtastic.js" + , "prefixfree.js" + ] +, "timeslider.js": [ + "jquery.js" + , "security.js" + , "undo-xpopup.js" + , "json2.js" + , "colorutils.js" + , "draggable.js" + , "ace2_common.js" + , "pad_utils.js" + , "pad_cookie.js" + , "pad_editor.js" + , "pad_editbar.js" + , "pad_docbar.js" + , "pad_modals.js" + , "pad_savedrevs.js" + , "pad_impexp.js" + , "AttributePoolFactory.js" + , "Changeset.js" + , "domline.js" + , "linestylefilter.js" + , "cssmanager.js" + , "broadcast.js" + , "broadcast_slider.js" + , "broadcast_revisions.js" + , "timeslider.js" + ] +, "ace2_inner.js": [ + "ace2_common.js" + , "AttributePoolFactory.js" + , "Changeset.js" + , "security.js" + , "skiplist.js" + , "virtual_lines.js" + , "cssmanager.js" + , "colorutils.js" + , "undomodule.js" + , "contentcollector.js" + , "changesettracker.js" + , "linestylefilter.js" + , "domline.js" + , "ace2_inner.js" + ] +} diff --git a/src/package.json b/src/package.json new file mode 100644 index 00000000..b6cdd5df --- /dev/null +++ b/src/package.json @@ -0,0 +1,35 @@ +{ + "name" : "ep_etherpad-lite", + "description" : "A Etherpad based on node.js", + "homepage" : "https://github.com/Pita/etherpad-lite", + "keywords" : ["etherpad", "realtime", "collaborative", "editor"], + "author" : "Peter 'Pita' Martischka <petermartischka@googlemail.com> - Primary Technology Ltd", + "contributors" : [ + { "name": "John McLear", + "name": "Hans Pinckaers", + "name": "Robin Buse" } + ], + "dependencies" : { + "yajsml" : "1.1.2", + "request" : "2.9.100", + "require-kernel" : "1.0.5", + "socket.io" : "0.8.7", + "ueberDB" : "0.1.7", + "async" : "0.1.18", + "express" : "2.5.8", + "clean-css" : "0.3.2", + "uglify-js" : "1.2.5", + "formidable" : "1.0.9", + "log4js" : "0.4.1", + "jsdom-nocontextifiy" : "0.2.10", + "async-stacktrace" : "0.0.2", + "npm" : "1.1" + }, + "devDependencies": { + "jshint" : "*" + }, + "engines" : { "node" : ">=0.6.0", + "npm" : ">=1.0" + }, + "version" : "1.0.0" +} diff --git a/src/static/css/iframe_editor.css b/src/static/css/iframe_editor.css new file mode 100644 index 00000000..d2d2f977 --- /dev/null +++ b/src/static/css/iframe_editor.css @@ -0,0 +1,172 @@ + +/* These CSS rules are included in both the outer and inner ACE iframe. + Also see inner.css, included only in the inner one. +*/ +html { cursor: text; } /* in Safari, produces text cursor for whole doc (inc. below body) */ +span { cursor: auto; } + +a { cursor: pointer !important; } + +ul, ol, li { + padding: 0; + margin: 0; +} +ul { margin-left: 1.5em; } +ul ul { margin-left: 0 !important; } +ul.list-bullet1 { margin-left: 1.5em; } +ul.list-bullet2 { margin-left: 3em; } +ul.list-bullet3 { margin-left: 4.5em; } +ul.list-bullet4 { margin-left: 6em; } +ul.list-bullet5 { margin-left: 7.5em; } +ul.list-bullet6 { margin-left: 9em; } +ul.list-bullet7 { margin-left: 10.5em; } +ul.list-bullet8 { margin-left: 12em; } + +ul { list-style-type: disc; } +ul.list-bullet1 { list-style-type: disc; } +ul.list-bullet2 { list-style-type: circle; } +ul.list-bullet3 { list-style-type: square; } +ul.list-bullet4 { list-style-type: disc; } +ul.list-bullet5 { list-style-type: circle; } +ul.list-bullet6 { list-style-type: square; } +ul.list-bullet7 { list-style-type: disc; } +ul.list-bullet8 { list-style-type: circle; } + +ol.list-number1 { margin-left: 1.5em; } +ol.list-number2 { margin-left: 3em; } +ol.list-number3 { margin-left: 4.5em; } +ol.list-number4 { margin-left: 6em; } +ol.list-number5 { margin-left: 7.5em; } +ol.list-number6 { margin-left: 9em; } +ol.list-number7 { margin-left: 10.5em; } +ol.list-number8 { margin-left: 12em; } + +ol { list-style-type: decimal; } +ol.list-number1 { list-style-type: decimal; } +ol.list-number2 { list-style-type: lower-latin; } +ol.list-number3 { list-style-type: lower-roman; } +ol.list-number4 { list-style-type: decimal; } +ol.list-number5 { list-style-type: lower-latin; } +ol.list-number6 { list-style-type: lower-roman; } +ol.list-number7 { list-style-type: decimal; } +ol.list-number8 { list-style-type: lower-latin; } + +ul.list-indent1 { margin-left: 1.5em; } +ul.list-indent2 { margin-left: 3em; } +ul.list-indent3 { margin-left: 4.5em; } +ul.list-indent4 { margin-left: 6em; } +ul.list-indent5 { margin-left: 7.5em; } +ul.list-indent6 { margin-left: 9em; } +ul.list-indent7 { margin-left: 10.5em; } +ul.list-indent8 { margin-left: 12em; } + +ul.list-indent1 { list-style-type: none; } +ul.list-indent2 { list-style-type: none; } +ul.list-indent3 { list-style-type: none; } +ul.list-indent4 { list-style-type: none; } +ul.list-indent5 { list-style-type: none; } +ul.list-indent6 { list-style-type: none; } +ul.list-indent7 { list-style-type: none; } +ul.list-indent8 { list-style-type: none; } + +body { + margin: 0; + white-space: nowrap; +} + +#outerdocbody { + background-color: #fff; +} +body.grayedout { background-color: #eee !important } + +#innerdocbody { + font-size: 12px; /* overridden by body.style */ + font-family: monospace; /* overridden by body.style */ + line-height: 16px; /* overridden by body.style */ +} + +body.doesWrap { + white-space: normal; +} + +#innerdocbody { + padding-top: 1px; /* important for some reason? */ + padding-right: 10px; + padding-bottom: 8px; + padding-left: 1px /* prevents characters from looking chopped off in FF3 -- Removed because it added too much whitespace */; + overflow: hidden; + /* blank 1x1 gif, so that IE8 doesn't consider the body transparent */ + background-image: url(data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw==); +} + +#sidediv { + font-size: 11px; + font-family: monospace; + line-height: 16px; /* overridden by sideDiv.style */ + padding-top: 8px; /* EDIT_BODY_PADDING_TOP */ + padding-right: 3px; /* LINE_NUMBER_PADDING_RIGHT - 1 */ + position: absolute; + width: 20px; /* MIN_LINEDIV_WIDTH */ + top: 0; + left: 0; + cursor: default; + color: white; +} + +#sidedivinner { + text-align: right; +} + +.sidedivdelayed { /* class set after sizes are set */ + background-color: #eee; + color: #888 !important; + border-right: 1px solid #999; +} +.sidedivhidden { + display: none; +} + +#outerdocbody iframe { + display: block; /* codemirror says it suppresses bugs */ + position: relative; + left: 32px; /* MIN_LINEDIV_WIDTH + LINE_NUMBER_PADDING_RIGHT + EDIT_BODY_PADDING_LEFT */ + top: 7px; /* EDIT_BODY_PADDING_TOP - 1*/ + border: 0; + width: 1px; /* changed programmatically */ + height: 1px; /* changed programmatically */ +} + +#outerdocbody .hotrect { + border: 1px solid #999; + position: absolute; +} + +/* cause "body" area (e.g. where clicks are heard) to grow horizontally with text */ +body.mozilla, body.safari { + display: table-cell; +} + +body.doesWrap { + display: block !important; +} + +.safari div { + /* prevents the caret from disappearing on the longest line of the doc */ + padding-right: 1px; +} + +p { + margin: 0; +} + +#linemetricsdiv { + position: absolute; + left: -1000px; + top: -1000px; + color: white; + z-index: -1; + font-size: 12px; /* overridden by lineMetricsDiv.style */ + font-family: monospace; /* overridden by lineMetricsDiv.style */ +} + +#overlaysdiv { position: absolute; left: -1000px; top: -1000px; } diff --git a/src/static/css/pad.css b/src/static/css/pad.css new file mode 100644 index 00000000..969d0027 --- /dev/null +++ b/src/static/css/pad.css @@ -0,0 +1,1270 @@ +*,html,body,p{ margin: 0; padding: 0; } +.clear { clear: both; } +html { font-size: 62.5%; width: 100%; } +body, textarea { font-family: Helvetica, Arial, sans-serif; } +iframe {position:absolute;} + +#users +{ + position: absolute; + z-index:500; + background-color: #000; + background-color: rgba(0,0,0,0.7); + width: 160px; + right: 20px; + top: 40px; + color: #fff; + padding: 5px; + border-radius: 6px; +} + +a img +{ + border: 0; +} + +/* menu */ +#editbar ul +{ + position: relative; + list-style: none; + padding-right: 3px; + padding-left: 1px; + z-index: 2; + overflow: hidden; + +} + +#editbar +{ + background: #f7f7f7; + background: linear-gradient(#f7f7f7, #f1f1f1 80%); + border-bottom: 1px solid #ccc; + height: 32px; + overflow: hidden; + padding-top: 3px; + width: 100%; +} + +#editbar ul li +{ + background: #fff; + background: linear-gradient(#fff, #f0f0f0); + border: 1px solid #ccc; + border-radius: 4px; + cursor: pointer; + float: left; + height: 18px; + margin-left: 2px; + overflow: hidden; + padding: 4px 5px; + width: 18px; +} + +#editbar ul li a +{ + text-decoration: none; + color: #ccc; + position: absolute; +} + +#editbar ul li a span +{ + position: relative; + top:-2px +} + +#editbar ul li:hover { + background: #fff; + background: linear-gradient(#f4f4f4, #e4e4e4); +} + +#editbar ul li:active { + background: #eee; + background: linear-gradient(#ddd, #fff); + box-shadow: 0 0 8px rgba(0,0,0,.1) inset; +} + +#editbar ul li.separator +{ + border: inherit; + background: inherit; + visibility:hidden; + width: 0px; +} +#editbar ul li a +{ + display: block; +} +#editbar ul li a img +{ + padding: 1px; +} + + +#editbar ul +{ + float: left; +} +#editbar ul#menu_right +{ + float: right; +} + +#users +{ + display: none; +} + +#editorcontainer +{ + position: absolute; + + width: 100%; + + top: 36px; + left: 0px; + bottom: 0px; + + z-index: 1; +} + +#editorcontainer iframe { + height: 100%; + width: 100%; + padding: 0; + margin: 0; +} + +#editorloadingbox { padding-top: 100px; padding-bottom: 100px; font-size: 2.5em; color: #aaa; + text-align: center; position: absolute; width: 100%; height: 30px; z-index: 100; } + +#editorcontainerbox{ + position:absolute; + bottom:0; + top:0; + width:100%; +} + + +#padpage { + position: absolute; + top: 0px; + bottom: 0px; + width: 100%; +} + +.maximized #padpage { + left: 8px; + right: 8px; + width: auto; + margin-left: 0; +} + +body.fullwidth #padpage { width: auto; margin-left: 6px; margin-right: 6px; } +body.squish1width #padpage { width: 800px; } +body.squish2width #padpage { width: 700px; } + +a#backtoprosite, #accountnav { + display: block; position: absolute; height: 15px; line-height: 15px; + width: auto; top: 5px; font-size: 1.2em; display:none; +} +a#backtoprosite, #accountnav a { color: #cde7ff; text-decoration: underline; } + +a#backtoprosite { padding-left: 20px; left: 6px; + background: url(static/img/protop.gif) no-repeat -5px -6px; } +#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; + font-size: 1.5em; position: absolute; } + +#alertbar { + margin-top: 6px; + opacity: 0; + display: none; + position:absolute; + left:0; + right:0; + z-index:53; +} + +#servermsg { position: relative; zoom: 1; border: 1px solid #992; + background: #ffc; padding: 0.8em; font-size: 1.2em; } +#servermsg h3 { font-weight: bold; margin-right: 10px; + margin-bottom: 1em; float: left; width: auto; } +#servermsg #servermsgdate { font-style: italic; font-weight: normal; color: #666; } +a#hidetopmsg { position: absolute; right: 5px; bottom: 5px; } + +#shuttingdown { position: relative; zoom: 1; border: 1px solid #992; + background: #ffc; padding: 0.6em; font-size: 1.2em; margin-top: 6px; } + +#docbar { margin-top: 6px; height: 25px; position: relative; zoom: 1; + background: #fbfbfb url(static/img/padtopback2.gif) repeat-x 0 -31px; } + +.docbarbutton +{ + padding-top: 2px; + padding-bottom: 2px; + padding-left: 4px; + padding-right: 4px; + border-left: 1px solid #CCC; + white-space: nowrap; +} + +.docbarbutton img +{ + border: 0px; + width: 13px; + margin-right: 2px; + vertical-align: middle; + margin-top: 3px; + margin-bottom: 2px; +} + +.menu, +.menu ul { + font-size: 10pt; + list-style-type: none; +} + +.menu ul { + padding-left: 20px; +} + +.menu a { + font-size: 10px; + line-height: 18px; + text-decoration: none; + color: #444; + font-weight: bold; +} + +.docbarbutton.highlight +{ + background-color: #fef2bd; + border: 1px solid #CCC; + border-right: 0px; +} + +#docbarleft { position: absolute; left: 0; top: 0; height: 100%; + overflow: hidden; + background: url(static/img/padtop5.gif) no-repeat left -31px; width: 7px; } + + + +#docbarpadtitle { position: absolute; height: auto; left: 9px; + width: 280px; font-size: 1.6em; color: #444; font-weight: normal; + line-height: 22px; margin-left: 2px; height: 22px; top: 2px; + overflow: hidden; text-overflow: ellipsis /*not supported in FF*/; + white-space:nowrap; } +.docbar-public #docbarpadtitle { padding-left: 22px; + background: url(static/img/public.gif) no-repeat left center; } + +#docbarrenamelink { position: absolute; top: 6px; + font-size: 1.1em; display: none; } +#docbarrenamelink a { color: #999; } +#docbarrenamelink a:hover { color: #48d; } +#padtitlebuttons { position: absolute; width: 74px; zoom: 1; + height: 17px; top: 4px; left: 170px; display: none; + background: url(static/img/ok_or_cancel.gif) 0px 0px; } +#padtitlesave { position: absolute; display: block; + height: 0; padding-top: 17px; overflow: hidden; + width: 23px; left: 0; top: 0; } +#padtitlecancel { position: absolute; display: block; + height: 0; padding-top: 17px; overflow: hidden; + width: 35px; right: 0; top: 0; } +#padtitleedit { position: absolute; top: 2px; left: 5px; + height: 15px; padding: 2px; font-size: 1.4em; + background: white; border-left: 1px solid #c3c3c3; + border-top: 1px solid #c3c3c3; + border-right: 1px solid #e6e6e6; border-bottom: 1px solid #e6e6e6; + width: 150px; display: none; +} + +#padmain { + margin-top: 0px; + position: absolute; + top: 63px !important; + left: 0px; + right: 0px; + bottom: 0px; + zoom: 1; +} + +#padeditor { + bottom:0px; + left:0; + position:absolute; + right:0px; + top:0; + zoom: 1; +} +.hidesidebar #padeditor { right: 0; } + +#vdraggie { +/* background: url(static/img/vdraggie.gif) no-repeat top center;*/ + width:16px; + height:16px; + background-image:url('../../static/img/etherpad_lite_icons.png'); + background-repeat: no-repeat; + background-position: 0px -300px; + + cursor: W-resize; + bottom:0; + position:absolute; + right:268px; + top:0; + width:56px; + z-index: 10; +} + +#editbarsavetable +{ + position:absolute; + top: 6px; + right: 8px; + height: 24px; +} + +#editbarsavetable td, #editbartable td +{ + white-space: nowrap; +} + +#myswatchbox { + position: absolute; + left: 5px; + top: 5px; + width: 24px; + height: 24px; + border: 1px solid #000; + background: transparent; + cursor: pointer; +} + +#myswatch { width: 100%; height: 100%; background: transparent;/*...initially*/ } + +#mycolorpicker { + width: 232px; height: 265px; + position: absolute; + left: -250px; top: 0px; z-index: 101; + display: none; + border-radius: 5px; + background: rgba(0, 0, 0, 0.7); + padding-left:10px; + padding-top:10px; +} +/* +#mycolorpicker ul li +{ + float: left; +} +#mycolorpicker .picked { border: 1px solid #000 !important; } + +#mycolorpicker .picked .pickerswatch { border: 1px solid #fff; } +*/ +#mycolorpickersave { + left: 10px; + font-weight: bold; +} + +#mycolorpickercancel { + left: 85px; +} + +#mycolorpickersave, #mycolorpickercancel { + background: #fff; + background: linear-gradient(#fff, #ccc); + border: 1px solid #ccc; + border-radius: 4px; + font-size:12px; + cursor: pointer; + color:#000; + overflow: hidden; + padding: 4px; + top: 240px; + text-align:center; + position: absolute; + width: 60px; +} + +#mycolorpickerpreview { + position: absolute; + left: 207px; + top: 240px; + width:16px; + height:16px; + padding:4px; + overflow: hidden; + color: #fff; + border-radius:5px; +} + + +#myusernameform { margin-left: 35px; } +#myusernameedit { font-size: 1.3em; color: #fff; + padding: 3px; height: 18px; margin: 0; border: 0; + width: 117px; background: transparent; } +#myusernameform input.editable { border: 1px solid #444; } +#myuser .myusernameedithoverable:hover { background: white; color: black} +#mystatusform { margin-left: 35px; margin-top: 5px; } +#mystatusedit { font-size: 1.2em; color: #777; + font-style: italic; display: none; + padding: 2px; height: 14px; margin: 0; border: 1px solid #bbb; + width: 199px; background: transparent; } +#myusernameform .editactive, #myusernameform .editempty { + background: white; border-left: 1px solid #c3c3c3; + border-top: 1px solid #c3c3c3; + border-right: 1px solid #e6e6e6; border-bottom: 1px solid #e6e6e6; + color: #000 +} +#myusernameform .editempty { color: #333; } + +table#otheruserstable { display: none; } +#nootherusers { padding: 10px; font-size: 1.2em; color: #eee; font-weight: bold;} +#nootherusers a { color: #3C88FF; } + +#otheruserstable td { + border-top: 1px solid #555; + height: 26px; + vertical-align: middle; + padding: 0 2px; + color: #fff; +} + +#otheruserstable .swatch { + border: 1px solid #000; width: 13px; height: 13px; overflow: hidden; + margin: 0 4px; +} + +.usertdswatch { width: 1%; } +.usertdname { font-size: 1.3em; color: #444; } +.usertdstatus { font-size: 1.1em; font-style: italic; color: #999; } +.usertdactivity { font-size: 1.1em; color: #777; } + +.usertdname input { border: 1px solid #bbb; width: 80px; padding: 2px; } +.usertdname input.editactive, .usertdname input.editempty { + background: white; border-left: 1px solid #c3c3c3; + border-top: 1px solid #c3c3c3; + border-right: 1px solid #e6e6e6; border-bottom: 1px solid #e6e6e6; +} +.usertdname input.editempty { color: #888; font-style: italic;} + +.modaldialog.cboxreconnecting .modaldialog-inner, +.modaldialog.cboxconnecting .modaldialog-inner { + background: url(../../static/img/connectingbar.gif) no-repeat center 60px; + height: 100px; +} +.modaldialog.cboxreconnecting, +.modaldialog.cboxconnecting, +.modaldialog.cboxdisconnected { + background: #8FCDE0; +} +.cboxdisconnected #connectionboxinner div { display: none; } +.cboxdisconnected_userdup #connectionboxinner #disconnected_userdup { display: block; } +.cboxdisconnected_deleted #connectionboxinner #disconnected_deleted { display: block; } +.cboxdisconnected_initsocketfail #connectionboxinner #disconnected_initsocketfail { display: block; } +.cboxdisconnected_looping #connectionboxinner #disconnected_looping { display: block; } +.cboxdisconnected_slowcommit #connectionboxinner #disconnected_slowcommit { display: block; } +.cboxdisconnected_unauth #connectionboxinner #disconnected_unauth { display: block; } +.cboxdisconnected_unknown #connectionboxinner #disconnected_unknown { display: block; } +.cboxdisconnected_initsocketfail #connectionboxinner #reconnect_advise, +.cboxdisconnected_looping #connectionboxinner #reconnect_advise, +.cboxdisconnected_slowcommit #connectionboxinner #reconnect_advise, +.cboxdisconnected_unknown #connectionboxinner #reconnect_advise { display: block; } +.cboxdisconnected div#reconnect_form { display: block; } +.cboxdisconnected .disconnected h2 { display: none; } +.cboxdisconnected .disconnected .h2_disconnect { display: block; } +.cboxdisconnected_userdup .disconnected h2.h2_disconnect { display: none; } +.cboxdisconnected_userdup .disconnected h2.h2_userdup { display: block; } +.cboxdisconnected_unauth .disconnected h2.h2_disconnect { display: none; } +.cboxdisconnected_unauth .disconnected h2.h2_unauth { display: block; } + +#connectionstatus { + position: absolute; width: 37px; height: 41px; overflow: hidden; + right: 0; + z-index: 11; +} +#connectionboxinner .connecting { + margin-top: 20px; + font-size: 2.0em; color: #555; + text-align: center; display: none; +} +.cboxconnecting #connectionboxinner .connecting { display: block; } + +#connectionboxinner .disconnected h2 { + font-size: 1.8em; color: #333; + text-align: left; + margin-top: 10px; margin-left: 10px; margin-right: 10px; + margin-bottom: 10px; +} +#connectionboxinner .disconnected p { + margin: 10px 10px; + font-size: 1.2em; + line-height: 1.1; + color: #333; +} +#connectionboxinner .disconnected { display: none; } +.cboxdisconnected #connectionboxinner .disconnected { display: block; } + +#connectionboxinner .reconnecting { + margin-top: 20px; + font-size: 1.6em; color: #555; + text-align: center; display: none; +} +.cboxreconnecting #connectionboxinner .reconnecting { display: block; } + +#reconnect_form button { + font-size: 12pt; + padding: 5px; +} + +/* We give docbar a higher z-index than its descendant impexp-wrapper in + order to allow the Import/Export panel to be on top of stuff lower + down on the page in IE. Strange but it works! */ +#docbar { z-index: 52; } + +#impexp-wrapper { width: 650px; right: 10px; } +#impexp-panel { height: 160px; } +.docbarimpexp-closing #impexp-wrapper { z-index: 50; } + +#savedrevs-wrapper { width: 100%; left: 0; } +#savedrevs-panel { height: 79px; } +.docbarsavedrevs-closing #savedrevs-wrapper { z-index: 50; } +#savedrevs-wrapper .dbpanel-rightedge { background-position: 0 -10px; } + +#options-wrapper { width: 340px; right: 200px; } +#options-panel { height: 114px; } +.docbaroptions-closing #options-wrapper { z-index: 50; } + +#security-wrapper { width: 320px; right: 300px; } +#security-panel { height: 130px; } +.docbarsecurity-closing #security-wrapper { z-index: 50; } + +#revision-notifier { position: absolute; right: 8px; top: 25px; + width: auto; height: auto; font-size: 1.2em; background: #ffc; + border: 1px solid #aaa; color: #444; padding: 3px 5px; + display: none; z-index: 55; } +#revision-notifier .label { color: #777; font-weight: bold; } + +#mainmodals { z-index: 600; /* higher than the modals themselves + so that modals are on top in IE */ } +.modalfield { font-size: 1.2em; padding: 1px; border: 1px solid #bbb;} +#mainmodals .editempty { color: #aaa; } + +.expand-collapse { + height: 22px; + background-image: url(static/img/sharedistri.gif); + background-repeat: no-repeat; + background-position: 0 3px; + padding-left: 17px; + text-decoration: none; +} +.expand-collapse.expanded { + background-position: 0 -31px; +} + + +.modaldialog { + position: absolute; + top: 100px; + left:50%; + margin-left:-243px; + width: 485px; + display: none; + z-index: 501; + zoom: 1; + overflow: hidden; + background: white; + border: 1px solid #999; +} +.modaldialog .modaldialog-inner { padding: 10pt; } +.modaldialog .modaldialog-hide { + float: right; + background-repeat: no-repeat; + background-image: url(static/img/sharebox4.gif); + display: block; + width: 22px; height: 22px; + background-position: -454px -6px; + margin-right:-5px; + margin-top:-5px; +} + +.modaldialog label, +.modaldialog h1 { + color:#222222; + font-size:125%; + font-weight:bold; +} + +.modaldialog th { + vertical-align: top; + text-align: left; +} + +.nonprouser #sharebox-stripe { display: none; } + +.sharebox-url { + width: 440px; height: 18px; + text-align: left; + font-size: 1.3em; + line-height: 18px; + padding: 2px; +} + +#sharebox-send { + float: right; + background-repeat: no-repeat; + background-image: url(static/img/sharebox4.gif); + display: block; + width: 87px; height: 22px; + background-position: -383px -289px; +} + + +#viewbarcontents { display: none; } +#viewzoomtitle { + position: absolute; left: 10px; top: 4px; height: 20px; line-height: 20px; + width: auto; +} +#viewzoommenu { + width: 65px; +} +#bottomarea { + height: 28px; + overflow: hidden; + position: absolute; + height: 28px; + bottom: 0px; + left: 0px; + right: 0px; + font-size: 1.2em; + color: #444; +} +#widthprefcheck { position: absolute; + background-image: url(static/img/layoutbuttons.gif); + background-repeat: no-repeat; cursor: pointer; + width: 86px; height: 20px; top: 4px; right: 2px; } +.widthprefunchecked { background-position: -1px -1px; } +.widthprefchecked { background-position: -1px -23px; } +#sidebarcheck { position: absolute; + background-image: url(static/img/layoutbuttons.gif); + background-repeat: no-repeat; cursor: pointer; + width: 86px; height: 20px; top: 4px; right: 90px; } +.sidebarunchecked { background-position: -1px -45px; } +.sidebarchecked { background-position: -1px -67px; } +#feedbackbutton { display: block; position: absolute; width: 68px; + height: 0; padding-top: 17px; overflow: hidden; + background: url(static/img/bottomareagfx.gif); + top: 5px; right: 220px; +} + +#modaloverlay { + z-index: 500; display: none; + background-repeat: repeat-both; + width: 100%; position: absolute; + height: 100%; left: 0; top: 0; +} + +* html #modaloverlay { /* for IE 6+ */ + opacity: 1; /* in case this is looked at */ + background-image: none; + background-repeat: no-repeat; + /* scale the image */ +} + +a#topbarmaximize { + float: right; + width: 16px; + height: 16px; + margin-right:-143px; + margin-top:4px; + background: url(static/img/maximize_normal.png); +} + +.maximized a#topbarmaximize { + background: url(static/img/maximize_maximized.png); +} + +#editbarinner h1 { + line-height: 29px; + font-size: 16px; + padding-left: 6pt; + margin-top: 0; +} + +#editbarinner h1 a { + font-size: 12px; +} + +.bigbutton { + display: block; + background-color: #a3bde0; + color: #555555; + border-style: solid; + border-width: 2px; + border-left-color: #d6e2f1; + border-right-color: #86aee1; + border-top-color: #d6e2f1; + border-bottom-color: #86aee1; + margin: 10pt; + text-align: center; + text-decoration: none; + padding: 50pt; + font-size: 20pt; + border-radius: 3pt; +} + +.modaldialog .bigbutton { + padding-left: 0; + padding-right: 0; + width: 100%; +} + +} + + +ul#colorpickerswatches +{ + padding-left: 3px; + padding-top: 5px; +} + +ul#colorpickerswatches li +{ + border: 1px solid #ccc; + width: 14px; + height: 14px; + overflow: hidden; + margin: 3px 6px; + padding: 0px; +} + +ul#colorpickerswatches li:hover +{ + border: 1px solid #000; + cursor: pointer; +} + + + +#chatbox +{ + position:absolute; + bottom:0px; + right: 20px; + width: 180px; + height: 200px; + z-index: 400; + background-color:#f7f7f7; + border-left: 1px solid #999; + border-right: 1px solid #999; + border-top: 1px solid #999; + padding: 3px; + padding-bottom: 10px; + border-top-left-radius: 5px; + border-top-right-radius: 5px; + display:none; +} + +#chattext +{ + background-color: white; + border: 1px solid white; + overflow-y:scroll; + font-size: 12px; + position:absolute; + right:0px; + left:0px; + top:25px; + bottom:25px; + z-index:1002; +} + +#chattext p +{ + padding: 3px; + overflow-x: hidden; +} + +#chatinputbox +{ + padding: 3px 2px; + position: absolute; + bottom:0px; + right:0px; + left:3px; +} + +#chatlabel +{ + font-size:13px; + font-weight:bold; + color:#555; + text-decoration: none; + margin-right: 3px; + vertical-align: middle; +} + +#chatinput +{ + border: 1px solid #BBBBBB; + width: 100%; + float:right; +} + +#chaticon +{ + z-index: 400; + position: fixed; + bottom: 0px; + right: 20px; + padding: 5px; + border-left: 1px solid #999; + border-right: 1px solid #999; + border-top: 1px solid #999; + border-top-left-radius: 5px; + border-top-right-radius: 5px; + background-color:#fff; + cursor: pointer; +} + +#chaticon a +{ + text-decoration: none; +} + +#chatcounter +{ + color:#555; + font-size:9px; + vertical-align: middle; +} + +#titlebar +{ + line-height:16px; + font-weight:bold; + color:#555; + position: relative; + bottom: 2px; +} + +#titlelabel +{ + font-size:13px; + margin:4px 0 0 4px; + position:absolute; +} + +#titlecross +{ + font-size:25px; + float:right; + text-align: right; + text-decoration: none; + cursor: pointer; + color:#555; +} + +.time +{ + float:right; + color:#333; + font-style:italic; + font-size: 10px; + margin-left: 3px; + margin-right: 3px; + margin-top:2px; +} + +.exporttype{ + margin-top: 2px; + background-repeat:no-repeat; + padding-left:25px; + background-image: url("../../static/img/etherpad_lite_icons.png"); + color:#fff; + text-decoration:none; +} + +#importexportline{ + border-left: 1px solid #fff; + height: 190px; + position:absolute; + width:0px; + left:260px; + opacity:.8; +} + +.impexpbutton{ + background-image: linear-gradient(center top , #EEEEEE, white 20%, white 20%); + padding:3px; +} + +#exporthtml{ + background-position: 0px -299px; +} + +#exportplain{ + background-position: 0px -395px; +} + +#exportword{ + background-position: 0px -275px; +} + +#exportpdf{ + background-position: 0px -371px; +} + +#exportopen{ + background-position: 0px -347px; +} + +#exportwordle{ + background-position: 0px -323px; +} + +#exportdokuwiki{ + background-position: 0px -459px; +} + +#importstatusball{ + display:none; +} + +#importarrow{ + display:none; +} + +#importmessagesuccess{ + display:none; +} + +#importsubmitinput{ + height:25px; + width:85px; + margin-top:12px; +} + +#importstatusball{ + height:50px; +} + +#chatthrob{ +display:none; +position:absolute; +bottom:40px; +font-size:14px; +width:150px; +height:40px; +right: 20px; +z-index: 200; +background-color: #000; +color: white; +background-color: rgb(0,0,0); +background-color: rgba(0,0,0,0.7); +padding: 10px; +border-radius: 6px; +opacity:.8; +} + +.buttonicon{ +width:16px; +height:16px; +background-image:url('../../static/img/etherpad_lite_icons.png'); +background-repeat: no-repeat; +margin-left: 1px; +margin-top: 1px; +} +.buttonicon-bold { + background-position: 0px -116px; +} +.buttonicon-italic { + background-position: 0px 0px; +} +.buttonicon-underline { + background-position: 0px -236px; +} +.buttonicon-strikethrough { + background-position: 0px -200px; +} +.buttonicon-insertorderedlist { + background-position: 0px -477px +} +.buttonicon-insertunorderedlist { + background-position: 0px -34px; +} +.buttonicon-indent { + background-position: 0px -52px; +} +.buttonicon-outdent { + background-position: 0px -134px; +} +.buttonicon-undo { + background-position: 0px -255px; +} +.buttonicon-redo { + background-position :0px -166px; +} +.buttonicon-clearauthorship { + background-position: 0px -86px; +} +.buttonicon-settings { + background-position: 0px -436px; +} +.buttonicon-import_export { + background-position: 0px -68px; +} +.buttonicon-embed { + background-position: 0px -18px; +} +.buttonicon-history { + background-position: 0px -218px; +} +.buttonicon-chat { + background-position: 0px -102px; + display: inline-block; + vertical-align: middle; + margin: 0 !important; +} +.buttonicon-showusers { + background-position: 0px -183px; + display: inline-block; +} + +#usericon +{ +width:33px !important; +} + +#focusprotector +{ + z-index: 100; + position: absolute; + bottom: 0px; + top: 0px; + left: 0px; + right: 0px; + background-color: white; + opacity:0.01; + display:none; +} + +#online_count{ + color: #888; + font-size: 11px; + line-height: 18px; + position: fixed; +} + +#qr_center { + margin: 10px 10px auto 0; + text-align: center; +} + +#embedreadonlyqr { + box-shadow: 0 0 10px #000; + border-radius: 3px; + transition: all .2s ease-in-out; +} + +#embedreadonlyqr:hover { + cursor: none; + transform: scale(1.5); +} + +.rtl{ + direction:RTL; +} + +#chattext p { + word-wrap: break-word; +} + +/* fix for misaligned checkboxes */ +input[type=checkbox] { + vertical-align: -1px; +} + +.right { + float:right; +} + +.popup { + font-size: 14px; + width: 450px; + z-index: 500; + padding: 10px; + border-radius: 6px; + background: #222; + background: rgba(0,0,0,.7); + background: linear-gradient(rgba(0,0,0,.6), rgba(0,0,0,.7) 35px, rgba(0,0,0,.6)); + box-shadow: 0 0 8px #888; + color: #fff; +} + +.popup input[type=text] { + width: 100%; + padding: 5px; + box-sizing: border-box; + display:block; + margin-top: 10px; +} + +.popup a { + text-decoration: none; +} + +.popup h1 { + font-size: 18px; +} +.popup h2 { + font-size: 15px; +} +.popup p { + margin: 5px 0; +} + +.column { + float: left; + width: 50%; +} + +#settingsmenu, #importexport, #embed { + position: absolute; + top: 55px; + right: 20px; + display: none; +} + +.note { + color: #ddd; + font-size: 11px; + font-weight: bold; +} + +.selected { + background: #eee !important; + background: linear-gradient(#EEE, #F0F0F0) !important; +} + +.stickyChat { + background-color: #f1f1f1 !important; + right: 0px !important; + top: 36px; + border-radius: 0px !important; + height: auto !important; + border: none !important; + border-left: 1px solid #ccc !important; + width: 185px !important; +} + +@media screen and (max-width: 960px) { + .modaldialog { + position: relative; + margin: 0 auto; + width: 80%; + top: 40px; + left: 0; + } +} + +@media screen and (max-width: 600px) { + #editbar ul li { + padding: 4px 1px; + } +} + +@media only screen and (min-device-width: 320px) and (max-device-width: 720px) { + #editbar ul li { + padding: 4px 3px; + } + #users { + right: 0; + top: 36px; + bottom: 33px; + border-radius: none; + } + #mycolorpicker { + left: -72px; /* #mycolorpicker:width - #users:width */ + } + #editorcontainer { + margin-bottom: 33px; + } + #editbar ul#menu_right { + background: #f7f7f7; + background: linear-gradient(#f7f7f7, #f1f1f1 80%); + width: 100%; + overflow: hidden; + height: 32px; + position: fixed; + bottom: 0; + border-top: 1px solid #ccc; + } + #editbar ul#menu_right li:last-child { + height: 24px; + border-radius: 0; + margin-top: 0; + border: 0; + float: right; + } + #chaticon { + bottom: 3px; + right: 55px; + border-right: none; + border-radius: 0; + background: #f7f7f7; + background: linear-gradient(#f7f7f7, #f1f1f1 80%); + border: 0; + } + #chatbox { + bottom: 32px; + right: 0; + border-top-right-radius: 0; + border-right: none; + } + #editbar ul li a span { + top: -3px; + } + #usericonback { + margin-top: 4px; + } + #qrcode { + display: none; + } + #editbar ul#menu_right li:not(:last-child) { + display: block; + } + #editbar ul#menu_right > li { + background: none; + border: none; + margin-top: 4px; + padding: 4px 8px; + } + .selected { + background: none !important; + } + #timesliderlink { + display: none !important; + } + .popup { + border-radius: 0; + box-sizing: border-box; + width: 100%; + } + #settingsmenu, #importexport, #embed { + left: 0; + top: 0; + bottom: 33px; + right: 0; + } + .separator { + display: none; + } + #online_count { + line-height: 24px; + } +}
\ No newline at end of file diff --git a/src/static/css/timeslider.css b/src/static/css/timeslider.css new file mode 100644 index 00000000..926c8012 --- /dev/null +++ b/src/static/css/timeslider.css @@ -0,0 +1,106 @@ +#editorcontainerbox {overflow:auto; top:40px;} + +#padcontent {font-size:12px; padding:10px;} + +#timeslider-wrapper {left:0; position:relative; right:0; top:0;} +#timeslider-left {background-image:url(../../static/img/timeslider_left.png); height:63px; left:0; position:absolute; width:134px;} +#timeslider-right {background-image:url(../../static/img/timeslider_right.png); height:63px; position:absolute; right:0; top:0; width:155px;} +#timeslider {background-image:url(../../static/img/timeslider_background.png); height:63px; margin:0 9px;} +#timeslider #timeslider-slider {height:61px; left:0; position:absolute; top:1px; width:100%;} +#ui-slider-handle { + -khtml-user-select:none; + -moz-user-select:none; + -ms-user-select:none; + -webkit-user-select:none; + background-image:url(../../static/img/crushed_current_location.png); + cursor:pointer; + height:61px; + left:0; + position:absolute; + top:0; + user-select:none; + width:13px; +} +#ui-slider-bar { + -khtml-user-select:none; + -moz-user-select:none; + -ms-user-select:none; + -webkit-user-select:none; + cursor:pointer; + height:35px; + margin-left:5px; + margin-right:148px; + position:relative; + top:20px; + user-select:none; +} + +#playpause_button, #playpause_button_icon {height:47px; position:absolute; width:47px;} +#playpause_button {background-image:url(../../static/img/crushed_button_undepressed.png); right:77px; top:9px;} +#playpause_button_icon {background-image:url(../../static/img/play.png); left:0; top:0;} +.pause#playpause_button_icon {background-image:url(../../static/img/pause.png);} + +#leftstar, #rightstar, #leftstep, #rightstep + {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;} +#rightstar {background-position:29px 44px; right:5px; top:8px; width:29px;} +#leftstep {background-position:0 22px; right:34px; top:20px; width:30px;} +#rightstep {background-position:29px 22px; right:5px; top:20px; width:29px;} + +#timeslider .star { + background-image:url(../../static/img/star.png); + cursor:pointer; + height:16px; + position:absolute; + top:40px; + width:15px; +} + +#timeslider #timer { + color:#fff; + font-family:Arial, sans-serif; + font-size:11px; + left:7px; + position:absolute; + text-align:center; + top:9px; + width:122px; +} + +.topbarcenter, #docbar {display:none;} +#padmain {top:30px;} +#editbarright {float:right;} +#returnbutton {color:#222; font-size:16px; line-height:29px; margin-top:0; padding-right:6px;} +#importexport {top:118px;} +#importexport .popup {width:185px;} + +/* lists */ +.list-bullet2, .list-indent2, .list-number2 {margin-left:3em;} +.list-bullet3, .list-indent3, .list-number3 {margin-left:4.5em;} +.list-bullet4, .list-indent4, .list-number4 {margin-left:6em;} +.list-bullet5, .list-indent5, .list-number5 {margin-left:7.5em;} +.list-bullet6, .list-indent6, .list-number6 {margin-left:9em;} +.list-bullet7, .list-indent7, .list-number7 {margin-left:10.5em;} +.list-bullet8, .list-indent8, .list-number8 {margin-left:12em;} + +/* unordered lists */ +UL {list-style-type:disc; margin-left:1.5em;} +UL UL {margin-left:0 !important;} + +.list-bullet2, .list-bullet5, .list-bullet8 {list-style-type:circle;} +.list-bullet3, .list-bullet6 {list-style-type:square;} + +.list-indent1, .list-indent2, .list-indent3, .list-indent5, .list-indent5, .list-indent6, .list-indent7, .list-indent8 {list-style-type:none;} + +/* ordered lists */ +OL {list-style-type:decimal; margin-left:1.5em;} +.list-number2, .list-number5, .list-number8 {list-style-type:lower-latin;} +.list-number3, .list-number6 {list-style-type:lower-roman;} + + + +/* IE 6/7 fixes ################################################################ */ +* HTML #ui-slider-handle {background-image:url(../../static/img/current_location.gif);} +* HTML #timeslider .star {background-image:url(../../static/img/star.gif);} +* HTML #playpause_button_icon {background-image:url(../../static/img/play.gif);} +* HTML .pause#playpause_button_icon {background-image:url(../../static/img/pause.gif);}
\ No newline at end of file diff --git a/src/static/custom/.gitignore b/src/static/custom/.gitignore new file mode 100644 index 00000000..aae16bb2 --- /dev/null +++ b/src/static/custom/.gitignore @@ -0,0 +1,3 @@ +* +!.gitignore +!*.template diff --git a/src/static/custom/css.template b/src/static/custom/css.template new file mode 100644 index 00000000..236251d9 --- /dev/null +++ b/src/static/custom/css.template @@ -0,0 +1,8 @@ +/* + custom css files are loaded after core css files. Simply use the same selector to override a style. + Example: + #editbar LI {border:1px solid #000;} + overrides + #editbar LI {border:1px solid #d5d5d5;} + from pad.css +*/ diff --git a/src/static/custom/js.template b/src/static/custom/js.template new file mode 100644 index 00000000..152c3d5d --- /dev/null +++ b/src/static/custom/js.template @@ -0,0 +1,6 @@ +function customStart() +{ + //define your javascript here + //jquery is available - except index.js + //you can load extra scripts with $.getScript http://api.jquery.com/jQuery.getScript/ +} diff --git a/src/static/favicon.ico b/src/static/favicon.ico Binary files differnew file mode 100644 index 00000000..df7b6289 --- /dev/null +++ b/src/static/favicon.ico diff --git a/src/static/img/backgrad.gif b/src/static/img/backgrad.gif Binary files differnew file mode 100644 index 00000000..8fee1a5b --- /dev/null +++ b/src/static/img/backgrad.gif diff --git a/src/static/img/connectingbar.gif b/src/static/img/connectingbar.gif Binary files differnew file mode 100644 index 00000000..34f54e90 --- /dev/null +++ b/src/static/img/connectingbar.gif diff --git a/src/static/img/crushed_button_depressed.png b/src/static/img/crushed_button_depressed.png Binary files differnew file mode 100644 index 00000000..d75dcce2 --- /dev/null +++ b/src/static/img/crushed_button_depressed.png diff --git a/src/static/img/crushed_button_undepressed.png b/src/static/img/crushed_button_undepressed.png Binary files differnew file mode 100644 index 00000000..d86e3f39 --- /dev/null +++ b/src/static/img/crushed_button_undepressed.png diff --git a/src/static/img/crushed_current_location.png b/src/static/img/crushed_current_location.png Binary files differnew file mode 100644 index 00000000..76e08359 --- /dev/null +++ b/src/static/img/crushed_current_location.png diff --git a/src/static/img/etherpad_lite_icons.png b/src/static/img/etherpad_lite_icons.png Binary files differnew file mode 100644 index 00000000..cadf5ed2 --- /dev/null +++ b/src/static/img/etherpad_lite_icons.png diff --git a/src/static/img/fileicons.gif b/src/static/img/fileicons.gif Binary files differnew file mode 100644 index 00000000..c03b6031 --- /dev/null +++ b/src/static/img/fileicons.gif diff --git a/src/static/img/leftarrow.png b/src/static/img/leftarrow.png Binary files differnew file mode 100644 index 00000000..1bec1288 --- /dev/null +++ b/src/static/img/leftarrow.png diff --git a/src/static/img/loading.gif b/src/static/img/loading.gif Binary files differnew file mode 100644 index 00000000..bb42be59 --- /dev/null +++ b/src/static/img/loading.gif diff --git a/src/static/img/pause.png b/src/static/img/pause.png Binary files differnew file mode 100644 index 00000000..657782c0 --- /dev/null +++ b/src/static/img/pause.png diff --git a/src/static/img/play.png b/src/static/img/play.png Binary files differnew file mode 100644 index 00000000..19afe034 --- /dev/null +++ b/src/static/img/play.png diff --git a/src/static/img/roundcorner_left.gif b/src/static/img/roundcorner_left.gif Binary files differnew file mode 100644 index 00000000..000de752 --- /dev/null +++ b/src/static/img/roundcorner_left.gif diff --git a/src/static/img/roundcorner_right.gif b/src/static/img/roundcorner_right.gif Binary files differnew file mode 100644 index 00000000..97acfbf2 --- /dev/null +++ b/src/static/img/roundcorner_right.gif diff --git a/src/static/img/stepper_buttons.png b/src/static/img/stepper_buttons.png Binary files differnew file mode 100644 index 00000000..e011a451 --- /dev/null +++ b/src/static/img/stepper_buttons.png diff --git a/src/static/img/timeslider_background.png b/src/static/img/timeslider_background.png Binary files differnew file mode 100644 index 00000000..851af4e8 --- /dev/null +++ b/src/static/img/timeslider_background.png diff --git a/src/static/img/timeslider_left.png b/src/static/img/timeslider_left.png Binary files differnew file mode 100644 index 00000000..48a9b0e1 --- /dev/null +++ b/src/static/img/timeslider_left.png diff --git a/src/static/img/timeslider_right.png b/src/static/img/timeslider_right.png Binary files differnew file mode 100644 index 00000000..1a1b2685 --- /dev/null +++ b/src/static/img/timeslider_right.png diff --git a/src/static/index.html b/src/static/index.html new file mode 100644 index 00000000..58f68801 --- /dev/null +++ b/src/static/index.html @@ -0,0 +1,161 @@ +<!doctype html> +<html> + + <title>Etherpad Lite</title> + + <meta charset="utf-8"> + <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=0"> + + <style> + body { + margin: 0; + height: 100%; + color: #333; + font: 14px helvetica, sans-serif; + background: #ddd; + background: -webkit-radial-gradient(circle,#aaa,#eee 60%) center fixed; + background: -moz-radial-gradient(circle,#aaa,#eee 60%) center fixed; + background: -ms-radial-gradient(circle,#aaa,#eee 60%) center fixed; + background: -o-radial-gradient(circle,#aaa,#eee 60%) center fixed; + border-top: 8px solid rgba(51,51,51,.8); + } + #wrapper { + border-top: 1px solid #999; + margin-top: 160px; + padding: 15px; + background: #eee; + background: -webkit-linear-gradient(#fff,#ccc); + background: -moz-linear-gradient(#fff,#ccc); + background: -ms-linear-gradient(#fff,#ccc); + background: -o-linear-gradient(#fff,#ccc); + opacity: .9; + box-shadow: 0px 1px 8px rgba(0,0,0,0.3); + } + #inner { + width: 300px; + margin: 0 auto; + } + #button { + margin: 0 auto; + border-radius: 3px; + text-align: center; + font: 36px verdana,arial,sans-serif; + color: white; + text-shadow: 0 -1px 0 rgba(0,0,0,.8); + height: 70px; + line-height: 70px; + background: #555; + background: -webkit-linear-gradient(#5F5F5F,#565656 50%,#4C4C4C 51%,#373737); + background: -moz-linear-gradient(#5F5F5F,#565656 50%,#4C4C4C 51%,#373737); + background: -ms-linear-gradient(#5F5F5F,#565656 50%,#4C4C4C 51%,#373737); + background: -o-linear-gradient(#5F5F5F,#565656 50%,#4C4C4C 51%,#373737); + box-shadow: inset 0 1px 3px rgba(0,0,0,0.9); + } + #button:hover { + cursor: pointer; + background: #666; + background: -webkit-linear-gradient(#707070,#666666 50%,#5B5B5B 51%,#474747); + background: -moz-linear-gradient(#707070,#666666 50%,#5B5B5B 51%,#474747); + background: -ms-linear-gradient(#707070,#666666 50%,#5B5B5B 51%,#474747); + background: -o-linear-gradient(#707070,#666666 50%,#5B5B5B 51%,#474747); + } + #button:active { + box-shadow: inset 0 1px 12px rgba(0,0,0,0.9); + background: #444; + } + #label { + text-align: left; + text-shadow: 0 1px 1px #fff; + margin: 16px auto 0; + } + form { + height: 38px; + background: #fff; + border: 1px solid #bbb; + border-radius: 3px; + position: relative; + } + button, input { + font-weight: bold; + font-size: 15px; + } + input[type="text"] { + border-radius: 3px; + box-sizing: border-box; + -moz-box-sizing: border-box; + padding: 0 45px 0 10px; + *padding: 0; /* IE7 hack */ + width: 100%; + height: 100%; + outline: none; + border: none; + position: absolute; + } + button[type="submit"] { + position: absolute; + right: 0; + width: 45px; + height: 38px; + } + @media only screen and (min-device-width: 320px) and (max-device-width: 720px) { + body { + background: #bbb; + background: -webkit-linear-gradient(#aaa,#eee 60%) center fixed; + background: -moz-linear-gradient(#aaa,#eee 60%) center fixed; + background: -ms-linear-gradient(#aaa,#eee 60%) center fixed; + } + #wrapper { + margin-top: 0; + } + #inner { + width: 95%; + } + #label { + text-align: center; + } + } + </style> + <link href="static/custom/index.css" rel="stylesheet"> + + <div id="wrapper"> + <div id="inner"> + <div id="button" onclick="go2Random()" class="translate">New Pad</div> + <div id="label" class="translate">or create/open a Pad with the name</div> + <form action="#" onsubmit="go2Name();return false;"> + <input type="text" id="padname" autofocus x-webkit-speech> + <button type="submit">OK</button> + </form> + </div> + </div> + + <script src="static/custom/index.js"></script> + <script> + function go2Name() + { + var padname = document.getElementById("padname").value; + padname.length > 0 ? window.location = "p/" + padname : alert("Please enter a name") + } + + function go2Random() + { + window.location = "p/" + randomPadName(); + } + + function randomPadName() + { + var chars = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"; + var string_length = 10; + var randomstring = ''; + for (var i = 0; i < string_length; i++) + { + var rnum = Math.floor(Math.random() * chars.length); + randomstring += chars.substring(rnum, rnum + 1); + } + return randomstring; + } + + // start the custom js + if (typeof customStart == "function") customStart(); + </script> + +</html> diff --git a/src/static/js/AttributePoolFactory.js b/src/static/js/AttributePoolFactory.js new file mode 100644 index 00000000..00b58dbb --- /dev/null +++ b/src/static/js/AttributePoolFactory.js @@ -0,0 +1,90 @@ +/** + * 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. + */ + +exports.createAttributePool = function () { + var p = {}; + p.numToAttrib = {}; // e.g. {0: ['foo','bar']} + p.attribToNum = {}; // e.g. {'foo,bar': 0} + p.nextNum = 0; + + p.putAttrib = function (attrib, dontAddIfAbsent) { + var str = String(attrib); + if (str in p.attribToNum) { + return p.attribToNum[str]; + } + if (dontAddIfAbsent) { + return -1; + } + var num = p.nextNum++; + p.attribToNum[str] = num; + p.numToAttrib[num] = [String(attrib[0] || ''), String(attrib[1] || '')]; + return num; + }; + + p.getAttrib = function (num) { + var pair = p.numToAttrib[num]; + if (!pair) { + return pair; + } + return [pair[0], pair[1]]; // return a mutable copy + }; + + p.getAttribKey = function (num) { + var pair = p.numToAttrib[num]; + if (!pair) return ''; + return pair[0]; + }; + + p.getAttribValue = function (num) { + var pair = p.numToAttrib[num]; + if (!pair) return ''; + return pair[1]; + }; + + p.eachAttrib = function (func) { + for (var n in p.numToAttrib) { + var pair = p.numToAttrib[n]; + func(pair[0], pair[1]); + } + }; + + p.toJsonable = function () { + return { + numToAttrib: p.numToAttrib, + nextNum: p.nextNum + }; + }; + + p.fromJsonable = function (obj) { + p.numToAttrib = obj.numToAttrib; + p.nextNum = obj.nextNum; + p.attribToNum = {}; + for (var n in p.numToAttrib) { + p.attribToNum[String(p.numToAttrib[n])] = Number(n); + } + return p; + }; + + return p; +} diff --git a/src/static/js/Changeset.js b/src/static/js/Changeset.js new file mode 100644 index 00000000..fd1900ba --- /dev/null +++ b/src/static/js/Changeset.js @@ -0,0 +1,2251 @@ +/* + * This is the Changeset library copied from the old Etherpad with some modifications to use it in node.js + * Can be found in https://github.com/ether/pad/blob/master/infrastructure/ace/www/easysync2.js + */ + +/** + * This code is mostly from the old Etherpad. Please help us to comment this code. + * This helps other people to understand this code better and helps them to improve it. + * TL;DR COMMENTS ON THIS FILE ARE HIGHLY APPRECIATED + */ + +/* + * 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. + */ + +var AttributePoolFactory = require("./AttributePoolFactory"); + +var _opt = null; + +/** + * ==================== General Util Functions ======================= + */ + +/** + * This method is called whenever there is an error in the sync process + * @param msg {string} Just some message + */ +exports.error = function error(msg) { + var e = new Error(msg); + e.easysync = true; + throw e; +}; + +/** + * This method is user for assertions with Messages + * if assert fails, the error function called. + * @param b {boolean} assertion condition + * @param msgParts {string} error to be passed if it fails + */ +exports.assert = function assert(b, msgParts) { + if (!b) { + var msg = Array.prototype.slice.call(arguments, 1).join(''); + exports.error("exports: " + msg); + } +}; + +/** + * Parses a number from string base 36 + * @param str {string} string of the number in base 36 + * @returns {int} number + */ +exports.parseNum = function (str) { + return parseInt(str, 36); +}; + +/** + * Writes a number in base 36 and puts it in a string + * @param num {int} number + * @returns {string} string + */ +exports.numToString = function (num) { + return num.toString(36).toLowerCase(); +}; + +/** + * Converts stuff before $ to base 10 + * @obsolete not really used anywhere?? + * @param cs {string} the string + * @return integer + */ +exports.toBaseTen = function (cs) { + var dollarIndex = cs.indexOf('$'); + var beforeDollar = cs.substring(0, dollarIndex); + var fromDollar = cs.substring(dollarIndex); + return beforeDollar.replace(/[0-9a-z]+/g, function (s) { + return String(exports.parseNum(s)); + }) + fromDollar; +}; + + +/** + * ==================== Changeset Functions ======================= + */ + +/** + * returns the required length of the text before changeset + * can be applied + * @param cs {string} String representation of the Changeset + */ +exports.oldLen = function (cs) { + return exports.unpack(cs).oldLen; +}; + +/** + * returns the length of the text after changeset is applied + * @param cs {string} String representation of the Changeset + */ +exports.newLen = function (cs) { + return exports.unpack(cs).newLen; +}; + +/** + * this function creates an iterator which decodes string changeset operations + * @param opsStr {string} String encoding of the change operations to be performed + * @param optStartIndex {int} from where in the string should the iterator start + * @return {Op} type object iterator + */ +exports.opIterator = function (opsStr, optStartIndex) { + //print(opsStr); + var regex = /((?:\*[0-9a-z]+)*)(?:\|([0-9a-z]+))?([-+=])([0-9a-z]+)|\?|/g; + var startIndex = (optStartIndex || 0); + var curIndex = startIndex; + var prevIndex = curIndex; + + function nextRegexMatch() { + prevIndex = curIndex; + var result; + if (_opt) { + result = _opt.nextOpInString(opsStr, curIndex); + if (result) { + if (result.opcode() == '?') { + exports.error("Hit error opcode in op stream"); + } + curIndex = result.lastIndex(); + } + } else { + regex.lastIndex = curIndex; + result = regex.exec(opsStr); + curIndex = regex.lastIndex; + if (result[0] == '?') { + exports.error("Hit error opcode in op stream"); + } + } + return result; + } + var regexResult = nextRegexMatch(); + var obj = exports.newOp(); + + function next(optObj) { + var op = (optObj || obj); + if (_opt && regexResult) { + op.attribs = regexResult.attribs(); + op.lines = regexResult.lines(); + op.chars = regexResult.chars(); + op.opcode = regexResult.opcode(); + regexResult = nextRegexMatch(); + } else if ((!_opt) && regexResult[0]) { + op.attribs = regexResult[1]; + op.lines = exports.parseNum(regexResult[2] || 0); + op.opcode = regexResult[3]; + op.chars = exports.parseNum(regexResult[4]); + regexResult = nextRegexMatch(); + } else { + exports.clearOp(op); + } + return op; + } + + function hasNext() { + return !!(_opt ? regexResult : regexResult[0]); + } + + function lastIndex() { + return prevIndex; + } + return { + next: next, + hasNext: hasNext, + lastIndex: lastIndex + }; +}; + +/** + * Cleans an Op object + * @param {Op} object to be cleared + */ +exports.clearOp = function (op) { + op.opcode = ''; + op.chars = 0; + op.lines = 0; + op.attribs = ''; +}; + +/** + * Creates a new Op object + * @param optOpcode the type operation of the Op object + */ +exports.newOp = function (optOpcode) { + return { + opcode: (optOpcode || ''), + chars: 0, + lines: 0, + attribs: '' + }; +}; + +/** + * Clones an Op + * @param op Op to be cloned + */ +exports.cloneOp = function (op) { + return { + opcode: op.opcode, + chars: op.chars, + lines: op.lines, + attribs: op.attribs + }; +}; + +/** + * Copies op1 to op2 + * @param op1 src Op + * @param op2 dest Op + */ +exports.copyOp = function (op1, op2) { + op2.opcode = op1.opcode; + op2.chars = op1.chars; + op2.lines = op1.lines; + op2.attribs = op1.attribs; +}; + +/** + * Writes the Op in a string the way that changesets need it + */ +exports.opString = function (op) { + // just for debugging + if (!op.opcode) return 'null'; + var assem = exports.opAssembler(); + assem.append(op); + return assem.toString(); +}; + +/** + * Used just for debugging + */ +exports.stringOp = function (str) { + // just for debugging + return exports.opIterator(str).next(); +}; + +/** + * Used to check if a Changeset if valid + * @param cs {Changeset} Changeset to be checked + */ +exports.checkRep = function (cs) { + // doesn't check things that require access to attrib pool (e.g. attribute order) + // or original string (e.g. newline positions) + var unpacked = exports.unpack(cs); + var oldLen = unpacked.oldLen; + var newLen = unpacked.newLen; + var ops = unpacked.ops; + var charBank = unpacked.charBank; + + var assem = exports.smartOpAssembler(); + var oldPos = 0; + var calcNewLen = 0; + var numInserted = 0; + var iter = exports.opIterator(ops); + while (iter.hasNext()) { + var o = iter.next(); + switch (o.opcode) { + case '=': + oldPos += o.chars; + calcNewLen += o.chars; + break; + case '-': + oldPos += o.chars; + exports.assert(oldPos < oldLen, oldPos, " >= ", oldLen, " in ", cs); + break; + case '+': + { + calcNewLen += o.chars; + numInserted += o.chars; + exports.assert(calcNewLen < newLen, calcNewLen, " >= ", newLen, " in ", cs); + break; + } + } + assem.append(o); + } + + calcNewLen += oldLen - oldPos; + charBank = charBank.substring(0, numInserted); + while (charBank.length < numInserted) { + charBank += "?"; + } + + assem.endDocument(); + var normalized = exports.pack(oldLen, calcNewLen, assem.toString(), charBank); + exports.assert(normalized == cs, normalized, ' != ', cs); + + return cs; +} + + +/** + * ==================== Util Functions ======================= + */ + +/** + * creates an object that allows you to append operations (type Op) and also + * compresses them if possible + */ +exports.smartOpAssembler = function () { + // Like opAssembler but able to produce conforming exportss + // from slightly looser input, at the cost of speed. + // Specifically: + // - merges consecutive operations that can be merged + // - strips final "=" + // - ignores 0-length changes + // - reorders consecutive + and - (which margingOpAssembler doesn't do) + var minusAssem = exports.mergingOpAssembler(); + var plusAssem = exports.mergingOpAssembler(); + var keepAssem = exports.mergingOpAssembler(); + var assem = exports.stringAssembler(); + var lastOpcode = ''; + var lengthChange = 0; + + function flushKeeps() { + assem.append(keepAssem.toString()); + keepAssem.clear(); + } + + function flushPlusMinus() { + assem.append(minusAssem.toString()); + minusAssem.clear(); + assem.append(plusAssem.toString()); + plusAssem.clear(); + } + + function append(op) { + if (!op.opcode) return; + if (!op.chars) return; + + if (op.opcode == '-') { + if (lastOpcode == '=') { + flushKeeps(); + } + minusAssem.append(op); + lengthChange -= op.chars; + } else if (op.opcode == '+') { + if (lastOpcode == '=') { + flushKeeps(); + } + plusAssem.append(op); + lengthChange += op.chars; + } else if (op.opcode == '=') { + if (lastOpcode != '=') { + flushPlusMinus(); + } + keepAssem.append(op); + } + lastOpcode = op.opcode; + } + + function appendOpWithText(opcode, text, attribs, pool) { + var op = exports.newOp(opcode); + op.attribs = exports.makeAttribsString(opcode, attribs, pool); + var lastNewlinePos = text.lastIndexOf('\n'); + if (lastNewlinePos < 0) { + op.chars = text.length; + op.lines = 0; + append(op); + } else { + op.chars = lastNewlinePos + 1; + op.lines = text.match(/\n/g).length; + append(op); + op.chars = text.length - (lastNewlinePos + 1); + op.lines = 0; + append(op); + } + } + + function toString() { + flushPlusMinus(); + flushKeeps(); + return assem.toString(); + } + + function clear() { + minusAssem.clear(); + plusAssem.clear(); + keepAssem.clear(); + assem.clear(); + lengthChange = 0; + } + + function endDocument() { + keepAssem.endDocument(); + } + + function getLengthChange() { + return lengthChange; + } + + return { + append: append, + toString: toString, + clear: clear, + endDocument: endDocument, + appendOpWithText: appendOpWithText, + getLengthChange: getLengthChange + }; +}; + +if (_opt) { + exports.mergingOpAssembler = function () { + var assem = _opt.mergingOpAssembler(); + + function append(op) { + assem.append(op.opcode, op.chars, op.lines, op.attribs); + } + + function toString() { + return assem.toString(); + } + + function clear() { + assem.clear(); + } + + function endDocument() { + assem.endDocument(); + } + + return { + append: append, + toString: toString, + clear: clear, + endDocument: endDocument + }; + }; +} else { + exports.mergingOpAssembler = function () { + // This assembler can be used in production; it efficiently + // merges consecutive operations that are mergeable, ignores + // no-ops, and drops final pure "keeps". It does not re-order + // operations. + var assem = exports.opAssembler(); + var bufOp = exports.newOp(); + + // If we get, for example, insertions [xxx\n,yyy], those don't merge, + // but if we get [xxx\n,yyy,zzz\n], that merges to [xxx\nyyyzzz\n]. + // This variable stores the length of yyy and any other newline-less + // ops immediately after it. + var bufOpAdditionalCharsAfterNewline = 0; + + function flush(isEndDocument) { + if (bufOp.opcode) { + if (isEndDocument && bufOp.opcode == '=' && !bufOp.attribs) { + // final merged keep, leave it implicit + } else { + assem.append(bufOp); + if (bufOpAdditionalCharsAfterNewline) { + bufOp.chars = bufOpAdditionalCharsAfterNewline; + bufOp.lines = 0; + assem.append(bufOp); + bufOpAdditionalCharsAfterNewline = 0; + } + } + bufOp.opcode = ''; + } + } + + function append(op) { + if (op.chars > 0) { + if (bufOp.opcode == op.opcode && bufOp.attribs == op.attribs) { + if (op.lines > 0) { + // bufOp and additional chars are all mergeable into a multi-line op + bufOp.chars += bufOpAdditionalCharsAfterNewline + op.chars; + bufOp.lines += op.lines; + bufOpAdditionalCharsAfterNewline = 0; + } else if (bufOp.lines == 0) { + // both bufOp and op are in-line + bufOp.chars += op.chars; + } else { + // append in-line text to multi-line bufOp + bufOpAdditionalCharsAfterNewline += op.chars; + } + } else { + flush(); + exports.copyOp(op, bufOp); + } + } + } + + function endDocument() { + flush(true); + } + + function toString() { + flush(); + return assem.toString(); + } + + function clear() { + assem.clear(); + exports.clearOp(bufOp); + } + return { + append: append, + toString: toString, + clear: clear, + endDocument: endDocument + }; + }; +} + +if (_opt) { + exports.opAssembler = function () { + var assem = _opt.opAssembler(); + // this function allows op to be mutated later (doesn't keep a ref) + + function append(op) { + assem.append(op.opcode, op.chars, op.lines, op.attribs); + } + + function toString() { + return assem.toString(); + } + + function clear() { + assem.clear(); + } + return { + append: append, + toString: toString, + clear: clear + }; + }; +} else { + exports.opAssembler = function () { + var pieces = []; + // this function allows op to be mutated later (doesn't keep a ref) + + function append(op) { + pieces.push(op.attribs); + if (op.lines) { + pieces.push('|', exports.numToString(op.lines)); + } + pieces.push(op.opcode); + pieces.push(exports.numToString(op.chars)); + } + + function toString() { + return pieces.join(''); + } + + function clear() { + pieces.length = 0; + } + return { + append: append, + toString: toString, + clear: clear + }; + }; +} + +/** + * A custom made String Iterator + * @param str {string} String to be iterated over + */ +exports.stringIterator = function (str) { + var curIndex = 0; + + function assertRemaining(n) { + exports.assert(n <= remaining(), "!(", n, " <= ", remaining(), ")"); + } + + function take(n) { + assertRemaining(n); + var s = str.substr(curIndex, n); + curIndex += n; + return s; + } + + function peek(n) { + assertRemaining(n); + var s = str.substr(curIndex, n); + return s; + } + + function skip(n) { + assertRemaining(n); + curIndex += n; + } + + function remaining() { + return str.length - curIndex; + } + return { + take: take, + skip: skip, + remaining: remaining, + peek: peek + }; +}; + +/** + * A custom made StringBuffer + */ +exports.stringAssembler = function () { + var pieces = []; + + function append(x) { + pieces.push(String(x)); + } + + function toString() { + return pieces.join(''); + } + return { + append: append, + toString: toString + }; +}; + +/** + * This class allows to iterate and modify texts which have several lines + * It is used for applying Changesets on arrays of lines + * Note from prev docs: "lines" need not be an array as long as it supports certain calls (lines_foo inside). + */ +exports.textLinesMutator = function (lines) { + // Mutates lines, an array of strings, in place. + // Mutation operations have the same constraints as exports operations + // with respect to newlines, but not the other additional constraints + // (i.e. ins/del ordering, forbidden no-ops, non-mergeability, final newline). + // Can be used to mutate lists of strings where the last char of each string + // is not actually a newline, but for the purposes of N and L values, + // the caller should pretend it is, and for things to work right in that case, the input + // to insert() should be a single line with no newlines. + var curSplice = [0, 0]; + var inSplice = false; + // position in document after curSplice is applied: + var curLine = 0, + curCol = 0; + // invariant: if (inSplice) then (curLine is in curSplice[0] + curSplice.length - {2,3}) && + // curLine >= curSplice[0] + // invariant: if (inSplice && (curLine >= curSplice[0] + curSplice.length - 2)) then + // curCol == 0 + + function lines_applySplice(s) { + lines.splice.apply(lines, s); + } + + function lines_toSource() { + return lines.toSource(); + } + + function lines_get(idx) { + if (lines.get) { + return lines.get(idx); + } else { + return lines[idx]; + } + } + // can be unimplemented if removeLines's return value not needed + + function lines_slice(start, end) { + if (lines.slice) { + return lines.slice(start, end); + } else { + return []; + } + } + + function lines_length() { + if ((typeof lines.length) == "number") { + return lines.length; + } else { + return lines.length(); + } + } + + function enterSplice() { + curSplice[0] = curLine; + curSplice[1] = 0; + if (curCol > 0) { + putCurLineInSplice(); + } + inSplice = true; + } + + function leaveSplice() { + lines_applySplice(curSplice); + curSplice.length = 2; + curSplice[0] = curSplice[1] = 0; + inSplice = false; + } + + function isCurLineInSplice() { + return (curLine - curSplice[0] < (curSplice.length - 2)); + } + + function debugPrint(typ) { + print(typ + ": " + curSplice.toSource() + " / " + curLine + "," + curCol + " / " + lines_toSource()); + } + + function putCurLineInSplice() { + if (!isCurLineInSplice()) { + curSplice.push(lines_get(curSplice[0] + curSplice[1])); + curSplice[1]++; + } + return 2 + curLine - curSplice[0]; + } + + function skipLines(L, includeInSplice) { + if (L) { + if (includeInSplice) { + if (!inSplice) { + enterSplice(); + } + for (var i = 0; i < L; i++) { + curCol = 0; + putCurLineInSplice(); + curLine++; + } + } else { + if (inSplice) { + if (L > 1) { + leaveSplice(); + } else { + putCurLineInSplice(); + } + } + curLine += L; + curCol = 0; + } + //print(inSplice+" / "+isCurLineInSplice()+" / "+curSplice[0]+" / "+curSplice[1]+" / "+lines.length); +/*if (inSplice && (! isCurLineInSplice()) && (curSplice[0] + curSplice[1] < lines.length)) { + print("BLAH"); + putCurLineInSplice(); + }*/ + // tests case foo in remove(), which isn't otherwise covered in current impl + } + //debugPrint("skip"); + } + + function skip(N, L, includeInSplice) { + if (N) { + if (L) { + skipLines(L, includeInSplice); + } else { + if (includeInSplice && !inSplice) { + enterSplice(); + } + if (inSplice) { + putCurLineInSplice(); + } + curCol += N; + //debugPrint("skip"); + } + } + } + + function removeLines(L) { + var removed = ''; + if (L) { + if (!inSplice) { + enterSplice(); + } + + function nextKLinesText(k) { + var m = curSplice[0] + curSplice[1]; + return lines_slice(m, m + k).join(''); + } + if (isCurLineInSplice()) { + //print(curCol); + if (curCol == 0) { + removed = curSplice[curSplice.length - 1]; + // print("FOO"); // case foo + curSplice.length--; + removed += nextKLinesText(L - 1); + curSplice[1] += L - 1; + } else { + removed = nextKLinesText(L - 1); + curSplice[1] += L - 1; + var sline = curSplice.length - 1; + removed = curSplice[sline].substring(curCol) + removed; + curSplice[sline] = curSplice[sline].substring(0, curCol) + lines_get(curSplice[0] + curSplice[1]); + curSplice[1] += 1; + } + } else { + removed = nextKLinesText(L); + curSplice[1] += L; + } + //debugPrint("remove"); + } + return removed; + } + + function remove(N, L) { + var removed = ''; + if (N) { + if (L) { + return removeLines(L); + } else { + if (!inSplice) { + enterSplice(); + } + var sline = putCurLineInSplice(); + removed = curSplice[sline].substring(curCol, curCol + N); + curSplice[sline] = curSplice[sline].substring(0, curCol) + curSplice[sline].substring(curCol + N); + //debugPrint("remove"); + } + } + return removed; + } + + function insert(text, L) { + if (text) { + if (!inSplice) { + enterSplice(); + } + if (L) { + var newLines = exports.splitTextLines(text); + if (isCurLineInSplice()) { + //if (curCol == 0) { + //curSplice.length--; + //curSplice[1]--; + //Array.prototype.push.apply(curSplice, newLines); + //curLine += newLines.length; + //} + //else { + var sline = curSplice.length - 1; + var theLine = curSplice[sline]; + var lineCol = curCol; + curSplice[sline] = theLine.substring(0, lineCol) + newLines[0]; + curLine++; + newLines.splice(0, 1); + Array.prototype.push.apply(curSplice, newLines); + curLine += newLines.length; + curSplice.push(theLine.substring(lineCol)); + curCol = 0; + //} + } else { + Array.prototype.push.apply(curSplice, newLines); + curLine += newLines.length; + } + } else { + var sline = putCurLineInSplice(); + curSplice[sline] = curSplice[sline].substring(0, curCol) + text + curSplice[sline].substring(curCol); + curCol += text.length; + } + //debugPrint("insert"); + } + } + + function hasMore() { + //print(lines.length+" / "+inSplice+" / "+(curSplice.length - 2)+" / "+curSplice[1]); + var docLines = lines_length(); + if (inSplice) { + docLines += curSplice.length - 2 - curSplice[1]; + } + return curLine < docLines; + } + + function close() { + if (inSplice) { + leaveSplice(); + } + //debugPrint("close"); + } + + var self = { + skip: skip, + remove: remove, + insert: insert, + close: close, + hasMore: hasMore, + removeLines: removeLines, + skipLines: skipLines + }; + return self; +}; + +/** + * Function allowing iterating over two Op strings. + * @params in1 {string} first Op string + * @params idx1 {int} integer where 1st iterator should start + * @params in2 {string} second Op string + * @params idx2 {int} integer where 2nd iterator should start + * @params func {function} which decides how 1st or 2nd iterator + * advances. When opX.opcode = 0, iterator X advances to + * next element + * func has signature f(op1, op2, opOut) + * op1 - current operation of the first iterator + * op2 - current operation of the second iterator + * opOut - result operator to be put into Changeset + * @return {string} the integrated changeset + */ +exports.applyZip = function (in1, idx1, in2, idx2, func) { + var iter1 = exports.opIterator(in1, idx1); + var iter2 = exports.opIterator(in2, idx2); + var assem = exports.smartOpAssembler(); + var op1 = exports.newOp(); + var op2 = exports.newOp(); + var opOut = exports.newOp(); + while (op1.opcode || iter1.hasNext() || op2.opcode || iter2.hasNext()) { + if ((!op1.opcode) && iter1.hasNext()) iter1.next(op1); + if ((!op2.opcode) && iter2.hasNext()) iter2.next(op2); + func(op1, op2, opOut); + if (opOut.opcode) { + //print(opOut.toSource()); + assem.append(opOut); + opOut.opcode = ''; + } + } + assem.endDocument(); + return assem.toString(); +}; + +/** + * Unpacks a string encoded Changeset into a proper Changeset object + * @params cs {string} String encoded Changeset + * @returns {Changeset} a Changeset class + */ +exports.unpack = function (cs) { + var headerRegex = /Z:([0-9a-z]+)([><])([0-9a-z]+)|/; + var headerMatch = headerRegex.exec(cs); + if ((!headerMatch) || (!headerMatch[0])) { + exports.error("Not a exports: " + cs); + } + var oldLen = exports.parseNum(headerMatch[1]); + var changeSign = (headerMatch[2] == '>') ? 1 : -1; + var changeMag = exports.parseNum(headerMatch[3]); + var newLen = oldLen + changeSign * changeMag; + var opsStart = headerMatch[0].length; + var opsEnd = cs.indexOf("$"); + if (opsEnd < 0) opsEnd = cs.length; + return { + oldLen: oldLen, + newLen: newLen, + ops: cs.substring(opsStart, opsEnd), + charBank: cs.substring(opsEnd + 1) + }; +}; + +/** + * Packs Changeset object into a string + * @params oldLen {int} Old length of the Changeset + * @params newLen {int] New length of the Changeset + * @params opsStr {string} String encoding of the changes to be made + * @params bank {string} Charbank of the Changeset + * @returns {Changeset} a Changeset class + */ +exports.pack = function (oldLen, newLen, opsStr, bank) { + var lenDiff = newLen - oldLen; + var lenDiffStr = (lenDiff >= 0 ? '>' + exports.numToString(lenDiff) : '<' + exports.numToString(-lenDiff)); + var a = []; + a.push('Z:', exports.numToString(oldLen), lenDiffStr, opsStr, '$', bank); + return a.join(''); +}; + +/** + * Applies a Changeset to a string + * @params cs {string} String encoded Changeset + * @params str {string} String to which a Changeset should be applied + */ +exports.applyToText = function (cs, str) { + var unpacked = exports.unpack(cs); + exports.assert(str.length == unpacked.oldLen, "mismatched apply: ", str.length, " / ", unpacked.oldLen); + var csIter = exports.opIterator(unpacked.ops); + var bankIter = exports.stringIterator(unpacked.charBank); + var strIter = exports.stringIterator(str); + var assem = exports.stringAssembler(); + while (csIter.hasNext()) { + var op = csIter.next(); + switch (op.opcode) { + case '+': + assem.append(bankIter.take(op.chars)); + break; + case '-': + strIter.skip(op.chars); + break; + case '=': + assem.append(strIter.take(op.chars)); + break; + } + } + assem.append(strIter.take(strIter.remaining())); + return assem.toString(); +}; + +/** + * applies a changeset on an array of lines + * @param CS {Changeset} the changeset to be applied + * @param lines The lines to which the changeset needs to be applied + */ +exports.mutateTextLines = function (cs, lines) { + var unpacked = exports.unpack(cs); + var csIter = exports.opIterator(unpacked.ops); + var bankIter = exports.stringIterator(unpacked.charBank); + var mut = exports.textLinesMutator(lines); + while (csIter.hasNext()) { + var op = csIter.next(); + switch (op.opcode) { + case '+': + mut.insert(bankIter.take(op.chars), op.lines); + break; + case '-': + mut.remove(op.chars, op.lines); + break; + case '=': + mut.skip(op.chars, op.lines, ( !! op.attribs)); + break; + } + } + mut.close(); +}; + +/** + * Composes two attribute strings (see below) into one. + * @param att1 {string} first attribute string + * @param att2 {string} second attribue string + * @param resultIsMutaton {boolean} + * @param pool {AttribPool} attribute pool + */ +exports.composeAttributes = function (att1, att2, resultIsMutation, pool) { + // att1 and att2 are strings like "*3*f*1c", asMutation is a boolean. + // Sometimes attribute (key,value) pairs are treated as attribute presence + // information, while other times they are treated as operations that + // mutate a set of attributes, and this affects whether an empty value + // is a deletion or a change. + // Examples, of the form (att1Items, att2Items, resultIsMutation) -> result + // ([], [(bold, )], true) -> [(bold, )] + // ([], [(bold, )], false) -> [] + // ([], [(bold, true)], true) -> [(bold, true)] + // ([], [(bold, true)], false) -> [(bold, true)] + // ([(bold, true)], [(bold, )], true) -> [(bold, )] + // ([(bold, true)], [(bold, )], false) -> [] + // pool can be null if att2 has no attributes. + if ((!att1) && resultIsMutation) { + // In the case of a mutation (i.e. composing two exportss), + // an att2 composed with an empy att1 is just att2. If att1 + // is part of an attribution string, then att2 may remove + // attributes that are already gone, so don't do this optimization. + return att2; + } + if (!att2) return att1; + var atts = []; + att1.replace(/\*([0-9a-z]+)/g, function (_, a) { + atts.push(pool.getAttrib(exports.parseNum(a))); + return ''; + }); + att2.replace(/\*([0-9a-z]+)/g, function (_, a) { + var pair = pool.getAttrib(exports.parseNum(a)); + var found = false; + for (var i = 0; i < atts.length; i++) { + var oldPair = atts[i]; + if (oldPair[0] == pair[0]) { + if (pair[1] || resultIsMutation) { + oldPair[1] = pair[1]; + } else { + atts.splice(i, 1); + } + found = true; + break; + } + } + if ((!found) && (pair[1] || resultIsMutation)) { + atts.push(pair); + } + return ''; + }); + atts.sort(); + var buf = exports.stringAssembler(); + for (var i = 0; i < atts.length; i++) { + buf.append('*'); + buf.append(exports.numToString(pool.putAttrib(atts[i]))); + } + //print(att1+" / "+att2+" / "+buf.toString()); + return buf.toString(); +}; + +/** + * Function used as parameter for applyZip to apply a Changeset to an + * attribute + */ +exports._slicerZipperFunc = function (attOp, csOp, opOut, pool) { + // attOp is the op from the sequence that is being operated on, either an + // attribution string or the earlier of two exportss being composed. + // pool can be null if definitely not needed. + //print(csOp.toSource()+" "+attOp.toSource()+" "+opOut.toSource()); + if (attOp.opcode == '-') { + exports.copyOp(attOp, opOut); + attOp.opcode = ''; + } else if (!attOp.opcode) { + exports.copyOp(csOp, opOut); + csOp.opcode = ''; + } else { + switch (csOp.opcode) { + case '-': + { + if (csOp.chars <= attOp.chars) { + // delete or delete part + if (attOp.opcode == '=') { + opOut.opcode = '-'; + opOut.chars = csOp.chars; + opOut.lines = csOp.lines; + opOut.attribs = ''; + } + attOp.chars -= csOp.chars; + attOp.lines -= csOp.lines; + csOp.opcode = ''; + if (!attOp.chars) { + attOp.opcode = ''; + } + } else { + // delete and keep going + if (attOp.opcode == '=') { + opOut.opcode = '-'; + opOut.chars = attOp.chars; + opOut.lines = attOp.lines; + opOut.attribs = ''; + } + csOp.chars -= attOp.chars; + csOp.lines -= attOp.lines; + attOp.opcode = ''; + } + break; + } + case '+': + { + // insert + exports.copyOp(csOp, opOut); + csOp.opcode = ''; + break; + } + case '=': + { + if (csOp.chars <= attOp.chars) { + // keep or keep part + opOut.opcode = attOp.opcode; + opOut.chars = csOp.chars; + opOut.lines = csOp.lines; + opOut.attribs = exports.composeAttributes(attOp.attribs, csOp.attribs, attOp.opcode == '=', pool); + csOp.opcode = ''; + attOp.chars -= csOp.chars; + attOp.lines -= csOp.lines; + if (!attOp.chars) { + attOp.opcode = ''; + } + } else { + // keep and keep going + opOut.opcode = attOp.opcode; + opOut.chars = attOp.chars; + opOut.lines = attOp.lines; + opOut.attribs = exports.composeAttributes(attOp.attribs, csOp.attribs, attOp.opcode == '=', pool); + attOp.opcode = ''; + csOp.chars -= attOp.chars; + csOp.lines -= attOp.lines; + } + break; + } + case '': + { + exports.copyOp(attOp, opOut); + attOp.opcode = ''; + break; + } + } + } +}; + +/** + * Applies a Changeset to the attribs string of a AText. + * @param cs {string} Changeset + * @param astr {string} the attribs string of a AText + * @param pool {AttribsPool} the attibutes pool + */ +exports.applyToAttribution = function (cs, astr, pool) { + var unpacked = exports.unpack(cs); + + return exports.applyZip(astr, 0, unpacked.ops, 0, function (op1, op2, opOut) { + return exports._slicerZipperFunc(op1, op2, opOut, pool); + }); +}; + +/*exports.oneInsertedLineAtATimeOpIterator = function(opsStr, optStartIndex, charBank) { + var iter = exports.opIterator(opsStr, optStartIndex); + var bankIndex = 0; + +};*/ + +exports.mutateAttributionLines = function (cs, lines, pool) { + //dmesg(cs); + //dmesg(lines.toSource()+" ->"); + var unpacked = exports.unpack(cs); + var csIter = exports.opIterator(unpacked.ops); + var csBank = unpacked.charBank; + var csBankIndex = 0; + // treat the attribution lines as text lines, mutating a line at a time + var mut = exports.textLinesMutator(lines); + + var lineIter = null; + + function isNextMutOp() { + return (lineIter && lineIter.hasNext()) || mut.hasMore(); + } + + function nextMutOp(destOp) { + if ((!(lineIter && lineIter.hasNext())) && mut.hasMore()) { + var line = mut.removeLines(1); + lineIter = exports.opIterator(line); + } + if (lineIter && lineIter.hasNext()) { + lineIter.next(destOp); + } else { + destOp.opcode = ''; + } + } + var lineAssem = null; + + function outputMutOp(op) { + //print("outputMutOp: "+op.toSource()); + if (!lineAssem) { + lineAssem = exports.mergingOpAssembler(); + } + lineAssem.append(op); + if (op.lines > 0) { + exports.assert(op.lines == 1, "Can't have op.lines of ", op.lines, " in attribution lines"); + // ship it to the mut + mut.insert(lineAssem.toString(), 1); + lineAssem = null; + } + } + + var csOp = exports.newOp(); + var attOp = exports.newOp(); + var opOut = exports.newOp(); + while (csOp.opcode || csIter.hasNext() || attOp.opcode || isNextMutOp()) { + if ((!csOp.opcode) && csIter.hasNext()) { + csIter.next(csOp); + } + //print(csOp.toSource()+" "+attOp.toSource()+" "+opOut.toSource()); + //print(csOp.opcode+"/"+csOp.lines+"/"+csOp.attribs+"/"+lineAssem+"/"+lineIter+"/"+(lineIter?lineIter.hasNext():null)); + //print("csOp: "+csOp.toSource()); + if ((!csOp.opcode) && (!attOp.opcode) && (!lineAssem) && (!(lineIter && lineIter.hasNext()))) { + break; // done + } else if (csOp.opcode == '=' && csOp.lines > 0 && (!csOp.attribs) && (!attOp.opcode) && (!lineAssem) && (!(lineIter && lineIter.hasNext()))) { + // skip multiple lines; this is what makes small changes not order of the document size + mut.skipLines(csOp.lines); + //print("skipped: "+csOp.lines); + csOp.opcode = ''; + } else if (csOp.opcode == '+') { + if (csOp.lines > 1) { + var firstLineLen = csBank.indexOf('\n', csBankIndex) + 1 - csBankIndex; + exports.copyOp(csOp, opOut); + csOp.chars -= firstLineLen; + csOp.lines--; + opOut.lines = 1; + opOut.chars = firstLineLen; + } else { + exports.copyOp(csOp, opOut); + csOp.opcode = ''; + } + outputMutOp(opOut); + csBankIndex += opOut.chars; + opOut.opcode = ''; + } else { + if ((!attOp.opcode) && isNextMutOp()) { + nextMutOp(attOp); + } + //print("attOp: "+attOp.toSource()); + exports._slicerZipperFunc(attOp, csOp, opOut, pool); + if (opOut.opcode) { + outputMutOp(opOut); + opOut.opcode = ''; + } + } + } + + exports.assert(!lineAssem, "line assembler not finished"); + mut.close(); + + //dmesg("-> "+lines.toSource()); +}; + +/** + * joins several Attribution lines + * @param theAlines collection of Attribution lines + * @returns {string} joined Attribution lines + */ +exports.joinAttributionLines = function (theAlines) { + var assem = exports.mergingOpAssembler(); + for (var i = 0; i < theAlines.length; i++) { + var aline = theAlines[i]; + var iter = exports.opIterator(aline); + while (iter.hasNext()) { + assem.append(iter.next()); + } + } + return assem.toString(); +}; + +exports.splitAttributionLines = function (attrOps, text) { + var iter = exports.opIterator(attrOps); + var assem = exports.mergingOpAssembler(); + var lines = []; + var pos = 0; + + function appendOp(op) { + assem.append(op); + if (op.lines > 0) { + lines.push(assem.toString()); + assem.clear(); + } + pos += op.chars; + } + + while (iter.hasNext()) { + var op = iter.next(); + var numChars = op.chars; + var numLines = op.lines; + while (numLines > 1) { + var newlineEnd = text.indexOf('\n', pos) + 1; + exports.assert(newlineEnd > 0, "newlineEnd <= 0 in splitAttributionLines"); + op.chars = newlineEnd - pos; + op.lines = 1; + appendOp(op); + numChars -= op.chars; + numLines -= op.lines; + } + if (numLines == 1) { + op.chars = numChars; + op.lines = 1; + } + appendOp(op); + } + + return lines; +}; + +/** + * splits text into lines + * @param {string} text to be splitted + */ +exports.splitTextLines = function (text) { + return text.match(/[^\n]*(?:\n|[^\n]$)/g); +}; + +/** + * compose two Changesets + * @param cs1 {Changeset} first Changeset + * @param cs2 {Changeset} second Changeset + * @param pool {AtribsPool} Attribs pool + */ +exports.compose = function (cs1, cs2, pool) { + var unpacked1 = exports.unpack(cs1); + var unpacked2 = exports.unpack(cs2); + var len1 = unpacked1.oldLen; + var len2 = unpacked1.newLen; + exports.assert(len2 == unpacked2.oldLen, "mismatched composition"); + var len3 = unpacked2.newLen; + var bankIter1 = exports.stringIterator(unpacked1.charBank); + var bankIter2 = exports.stringIterator(unpacked2.charBank); + var bankAssem = exports.stringAssembler(); + + var newOps = exports.applyZip(unpacked1.ops, 0, unpacked2.ops, 0, function (op1, op2, opOut) { + //var debugBuilder = exports.stringAssembler(); + //debugBuilder.append(exports.opString(op1)); + //debugBuilder.append(','); + //debugBuilder.append(exports.opString(op2)); + //debugBuilder.append(' / '); + var op1code = op1.opcode; + var op2code = op2.opcode; + if (op1code == '+' && op2code == '-') { + bankIter1.skip(Math.min(op1.chars, op2.chars)); + } + exports._slicerZipperFunc(op1, op2, opOut, pool); + if (opOut.opcode == '+') { + if (op2code == '+') { + bankAssem.append(bankIter2.take(opOut.chars)); + } else { + bankAssem.append(bankIter1.take(opOut.chars)); + } + } + + //debugBuilder.append(exports.opString(op1)); + //debugBuilder.append(','); + //debugBuilder.append(exports.opString(op2)); + //debugBuilder.append(' -> '); + //debugBuilder.append(exports.opString(opOut)); + //print(debugBuilder.toString()); + }); + + return exports.pack(len1, len3, newOps, bankAssem.toString()); +}; + +/** + * returns a function that tests if a string of attributes + * (e.g. *3*4) contains a given attribute key,value that + * is already present in the pool. + * @param attribPair array [key,value] of the attribute + * @param pool {AttribPool} Attribute pool + */ +exports.attributeTester = function (attribPair, pool) { + if (!pool) { + return never; + } + var attribNum = pool.putAttrib(attribPair, true); + if (attribNum < 0) { + return never; + } else { + var re = new RegExp('\\*' + exports.numToString(attribNum) + '(?!\\w)'); + return function (attribs) { + return re.test(attribs); + }; + } + + function never(attribs) { + return false; + } +}; + +/** + * creates the identity Changeset of length N + * @param N {int} length of the identity changeset + */ +exports.identity = function (N) { + return exports.pack(N, N, "", ""); +}; + + +/** + * creates a Changeset which works on oldFullText and removes text + * from spliceStart to spliceStart+numRemoved and inserts newText + * instead. Also gives possibility to add attributes optNewTextAPairs + * for the new text. + * @param oldFullText {string} old text + * @param spliecStart {int} where splicing starts + * @param numRemoved {int} number of characters to be removed + * @param newText {string} string to be inserted + * @param optNewTextAPairs {string} new pairs to be inserted + * @param pool {AttribPool} Attribution Pool + */ +exports.makeSplice = function (oldFullText, spliceStart, numRemoved, newText, optNewTextAPairs, pool) { + var oldLen = oldFullText.length; + + if (spliceStart >= oldLen) { + spliceStart = oldLen - 1; + } + if (numRemoved > oldFullText.length - spliceStart - 1) { + numRemoved = oldFullText.length - spliceStart - 1; + } + var oldText = oldFullText.substring(spliceStart, spliceStart + numRemoved); + var newLen = oldLen + newText.length - oldText.length; + + var assem = exports.smartOpAssembler(); + assem.appendOpWithText('=', oldFullText.substring(0, spliceStart)); + assem.appendOpWithText('-', oldText); + assem.appendOpWithText('+', newText, optNewTextAPairs, pool); + assem.endDocument(); + return exports.pack(oldLen, newLen, assem.toString(), newText); +}; + +/** + * Transforms a changeset into a list of splices in the form + * [startChar, endChar, newText] meaning replace text from + * startChar to endChar with newText + * @param cs Changeset + */ +exports.toSplices = function (cs) { + // + var unpacked = exports.unpack(cs); + var splices = []; + + var oldPos = 0; + var iter = exports.opIterator(unpacked.ops); + var charIter = exports.stringIterator(unpacked.charBank); + var inSplice = false; + while (iter.hasNext()) { + var op = iter.next(); + if (op.opcode == '=') { + oldPos += op.chars; + inSplice = false; + } else { + if (!inSplice) { + splices.push([oldPos, oldPos, ""]); + inSplice = true; + } + if (op.opcode == '-') { + oldPos += op.chars; + splices[splices.length - 1][1] += op.chars; + } else if (op.opcode == '+') { + splices[splices.length - 1][2] += charIter.take(op.chars); + } + } + } + + return splices; +}; + +/** + * + */ +exports.characterRangeFollow = function (cs, startChar, endChar, insertionsAfter) { + var newStartChar = startChar; + var newEndChar = endChar; + var splices = exports.toSplices(cs); + var lengthChangeSoFar = 0; + for (var i = 0; i < splices.length; i++) { + var splice = splices[i]; + var spliceStart = splice[0] + lengthChangeSoFar; + var spliceEnd = splice[1] + lengthChangeSoFar; + var newTextLength = splice[2].length; + var thisLengthChange = newTextLength - (spliceEnd - spliceStart); + + if (spliceStart <= newStartChar && spliceEnd >= newEndChar) { + // splice fully replaces/deletes range + // (also case that handles insertion at a collapsed selection) + if (insertionsAfter) { + newStartChar = newEndChar = spliceStart; + } else { + newStartChar = newEndChar = spliceStart + newTextLength; + } + } else if (spliceEnd <= newStartChar) { + // splice is before range + newStartChar += thisLengthChange; + newEndChar += thisLengthChange; + } else if (spliceStart >= newEndChar) { + // splice is after range + } else if (spliceStart >= newStartChar && spliceEnd <= newEndChar) { + // splice is inside range + newEndChar += thisLengthChange; + } else if (spliceEnd < newEndChar) { + // splice overlaps beginning of range + newStartChar = spliceStart + newTextLength; + newEndChar += thisLengthChange; + } else { + // splice overlaps end of range + newEndChar = spliceStart; + } + + lengthChangeSoFar += thisLengthChange; + } + + return [newStartChar, newEndChar]; +}; + +/** + * Iterate over attributes in a changeset and move them from + * oldPool to newPool + * @param cs {Changeset} Chageset/attribution string to be iterated over + * @param oldPool {AttribPool} old attributes pool + * @param newPool {AttribPool} new attributes pool + * @return {string} the new Changeset + */ +exports.moveOpsToNewPool = function (cs, oldPool, newPool) { + // works on exports or attribution string + var dollarPos = cs.indexOf('$'); + if (dollarPos < 0) { + dollarPos = cs.length; + } + var upToDollar = cs.substring(0, dollarPos); + var fromDollar = cs.substring(dollarPos); + // order of attribs stays the same + return upToDollar.replace(/\*([0-9a-z]+)/g, function (_, a) { + var oldNum = exports.parseNum(a); + var pair = oldPool.getAttrib(oldNum); + var newNum = newPool.putAttrib(pair); + return '*' + exports.numToString(newNum); + }) + fromDollar; +}; + +/** + * create an attribution inserting a text + * @param text {string} text to be inserted + */ +exports.makeAttribution = function (text) { + var assem = exports.smartOpAssembler(); + assem.appendOpWithText('+', text); + return assem.toString(); +}; + +/** + * Iterates over attributes in exports, attribution string, or attribs property of an op + * and runs function func on them + * @param cs {Changeset} changeset + * @param func {function} function to be called + */ +exports.eachAttribNumber = function (cs, func) { + var dollarPos = cs.indexOf('$'); + if (dollarPos < 0) { + dollarPos = cs.length; + } + var upToDollar = cs.substring(0, dollarPos); + + upToDollar.replace(/\*([0-9a-z]+)/g, function (_, a) { + func(exports.parseNum(a)); + return ''; + }); +}; + +/** + * Filter attributes which should remain in a Changeset + * callable on a exports, attribution string, or attribs property of an op, + * though it may easily create adjacent ops that can be merged. + * @param cs {Changeset} changeset to be filtered + * @param filter {function} fnc which returns true if an + * attribute X (int) should be kept in the Changeset + */ +exports.filterAttribNumbers = function (cs, filter) { + return exports.mapAttribNumbers(cs, filter); +}; + +/** + * does exactly the same as exports.filterAttribNumbers + */ +exports.mapAttribNumbers = function (cs, func) { + var dollarPos = cs.indexOf('$'); + if (dollarPos < 0) { + dollarPos = cs.length; + } + var upToDollar = cs.substring(0, dollarPos); + + var newUpToDollar = upToDollar.replace(/\*([0-9a-z]+)/g, function (s, a) { + var n = func(exports.parseNum(a)); + if (n === true) { + return s; + } else if ((typeof n) === "number") { + return '*' + exports.numToString(n); + } else { + return ''; + } + }); + + return newUpToDollar + cs.substring(dollarPos); +}; + +/** + * Create a Changeset going from Identity to a certain state + * @params text {string} text of the final change + * @attribs attribs {string} optional, operations which insert + * the text and also puts the right attributes + */ +exports.makeAText = function (text, attribs) { + return { + text: text, + attribs: (attribs || exports.makeAttribution(text)) + }; +}; + +/** + * Apply a Changeset to a AText + * @param cs {Changeset} Changeset to be applied + * @param atext {AText} + * @param pool {AttribPool} Attribute Pool to add to + */ +exports.applyToAText = function (cs, atext, pool) { + return { + text: exports.applyToText(cs, atext.text), + attribs: exports.applyToAttribution(cs, atext.attribs, pool) + }; +}; + +/** + * Clones a AText structure + * @param atext {AText} + */ +exports.cloneAText = function (atext) { + return { + text: atext.text, + attribs: atext.attribs + }; +}; + +/** + * Copies a AText structure from atext1 to atext2 + * @param atext {AText} + */ +exports.copyAText = function (atext1, atext2) { + atext2.text = atext1.text; + atext2.attribs = atext1.attribs; +}; + +/** + * Append the set of operations from atext to an assembler + * @param atext {AText} + * @param assem Assembler like smartOpAssembler + */ +exports.appendATextToAssembler = function (atext, assem) { + // intentionally skips last newline char of atext + var iter = exports.opIterator(atext.attribs); + var op = exports.newOp(); + while (iter.hasNext()) { + iter.next(op); + if (!iter.hasNext()) { + // last op, exclude final newline + if (op.lines <= 1) { + op.lines = 0; + op.chars--; + if (op.chars) { + assem.append(op); + } + } else { + var nextToLastNewlineEnd = + atext.text.lastIndexOf('\n', atext.text.length - 2) + 1; + var lastLineLength = atext.text.length - nextToLastNewlineEnd - 1; + op.lines--; + op.chars -= (lastLineLength + 1); + assem.append(op); + op.lines = 0; + op.chars = lastLineLength; + if (op.chars) { + assem.append(op); + } + } + } else { + assem.append(op); + } + } +}; + +/** + * Creates a clone of a Changeset and it's APool + * @param cs {Changeset} + * @param pool {AtributePool} + */ +exports.prepareForWire = function (cs, pool) { + var newPool = AttributePoolFactory.createAttributePool();; + var newCs = exports.moveOpsToNewPool(cs, pool, newPool); + return { + translated: newCs, + pool: newPool + }; +}; + +/** + * Checks if a changeset s the identity changeset + */ +exports.isIdentity = function (cs) { + var unpacked = exports.unpack(cs); + return unpacked.ops == "" && unpacked.oldLen == unpacked.newLen; +}; + +/** + * returns all the values of attributes with a certain key + * in an Op attribs string + * @param attribs {string} Attribute string of a Op + * @param key {string} string to be seached for + * @param pool {AttribPool} attribute pool + */ +exports.opAttributeValue = function (op, key, pool) { + return exports.attribsAttributeValue(op.attribs, key, pool); +}; + +/** + * returns all the values of attributes with a certain key + * in an attribs string + * @param attribs {string} Attribute string + * @param key {string} string to be seached for + * @param pool {AttribPool} attribute pool + */ +exports.attribsAttributeValue = function (attribs, key, pool) { + var value = ''; + if (attribs) { + exports.eachAttribNumber(attribs, function (n) { + if (pool.getAttribKey(n) == key) { + value = pool.getAttribValue(n); + } + }); + } + return value; +}; + +/** + * Creates a Changeset builder for a string with initial + * length oldLen. Allows to add/remove parts of it + * @param oldLen {int} Old length + */ +exports.builder = function (oldLen) { + var assem = exports.smartOpAssembler(); + var o = exports.newOp(); + var charBank = exports.stringAssembler(); + + var self = { + // attribs are [[key1,value1],[key2,value2],...] or '*0*1...' (no pool needed in latter case) + keep: function (N, L, attribs, pool) { + o.opcode = '='; + o.attribs = (attribs && exports.makeAttribsString('=', attribs, pool)) || ''; + o.chars = N; + o.lines = (L || 0); + assem.append(o); + return self; + }, + keepText: function (text, attribs, pool) { + assem.appendOpWithText('=', text, attribs, pool); + return self; + }, + insert: function (text, attribs, pool) { + assem.appendOpWithText('+', text, attribs, pool); + charBank.append(text); + return self; + }, + remove: function (N, L) { + o.opcode = '-'; + o.attribs = ''; + o.chars = N; + o.lines = (L || 0); + assem.append(o); + return self; + }, + toString: function () { + assem.endDocument(); + var newLen = oldLen + assem.getLengthChange(); + return exports.pack(oldLen, newLen, assem.toString(), charBank.toString()); + } + }; + + return self; +}; + +exports.makeAttribsString = function (opcode, attribs, pool) { + // makeAttribsString(opcode, '*3') or makeAttribsString(opcode, [['foo','bar']], myPool) work + if (!attribs) { + return ''; + } else if ((typeof attribs) == "string") { + return attribs; + } else if (pool && attribs && attribs.length) { + if (attribs.length > 1) { + attribs = attribs.slice(); + attribs.sort(); + } + var result = []; + for (var i = 0; i < attribs.length; i++) { + var pair = attribs[i]; + if (opcode == '=' || (opcode == '+' && pair[1])) { + result.push('*' + exports.numToString(pool.putAttrib(pair))); + } + } + return result.join(''); + } +}; + +// like "substring" but on a single-line attribution string +exports.subattribution = function (astr, start, optEnd) { + var iter = exports.opIterator(astr, 0); + var assem = exports.smartOpAssembler(); + var attOp = exports.newOp(); + var csOp = exports.newOp(); + var opOut = exports.newOp(); + + function doCsOp() { + if (csOp.chars) { + while (csOp.opcode && (attOp.opcode || iter.hasNext())) { + if (!attOp.opcode) iter.next(attOp); + + if (csOp.opcode && attOp.opcode && csOp.chars >= attOp.chars && attOp.lines > 0 && csOp.lines <= 0) { + csOp.lines++; + } + + exports._slicerZipperFunc(attOp, csOp, opOut, null); + if (opOut.opcode) { + assem.append(opOut); + opOut.opcode = ''; + } + } + } + } + + csOp.opcode = '-'; + csOp.chars = start; + + doCsOp(); + + if (optEnd === undefined) { + if (attOp.opcode) { + assem.append(attOp); + } + while (iter.hasNext()) { + iter.next(attOp); + assem.append(attOp); + } + } else { + csOp.opcode = '='; + csOp.chars = optEnd - start; + doCsOp(); + } + + return assem.toString(); +}; + +exports.inverse = function (cs, lines, alines, pool) { + // lines and alines are what the exports is meant to apply to. + // They may be arrays or objects with .get(i) and .length methods. + // They include final newlines on lines. + + function lines_get(idx) { + if (lines.get) { + return lines.get(idx); + } else { + return lines[idx]; + } + } + + function lines_length() { + if ((typeof lines.length) == "number") { + return lines.length; + } else { + return lines.length(); + } + } + + function alines_get(idx) { + if (alines.get) { + return alines.get(idx); + } else { + return alines[idx]; + } + } + + function alines_length() { + if ((typeof alines.length) == "number") { + return alines.length; + } else { + return alines.length(); + } + } + + var curLine = 0; + var curChar = 0; + var curLineOpIter = null; + var curLineOpIterLine; + var curLineNextOp = exports.newOp('+'); + + var unpacked = exports.unpack(cs); + var csIter = exports.opIterator(unpacked.ops); + var builder = exports.builder(unpacked.newLen); + + function consumeAttribRuns(numChars, func /*(len, attribs, endsLine)*/ ) { + + if ((!curLineOpIter) || (curLineOpIterLine != curLine)) { + // create curLineOpIter and advance it to curChar + curLineOpIter = exports.opIterator(alines_get(curLine)); + curLineOpIterLine = curLine; + var indexIntoLine = 0; + var done = false; + while (!done) { + curLineOpIter.next(curLineNextOp); + if (indexIntoLine + curLineNextOp.chars >= curChar) { + curLineNextOp.chars -= (curChar - indexIntoLine); + done = true; + } else { + indexIntoLine += curLineNextOp.chars; + } + } + } + + while (numChars > 0) { + if ((!curLineNextOp.chars) && (!curLineOpIter.hasNext())) { + curLine++; + curChar = 0; + curLineOpIterLine = curLine; + curLineNextOp.chars = 0; + curLineOpIter = exports.opIterator(alines_get(curLine)); + } + if (!curLineNextOp.chars) { + curLineOpIter.next(curLineNextOp); + } + var charsToUse = Math.min(numChars, curLineNextOp.chars); + func(charsToUse, curLineNextOp.attribs, charsToUse == curLineNextOp.chars && curLineNextOp.lines > 0); + numChars -= charsToUse; + curLineNextOp.chars -= charsToUse; + curChar += charsToUse; + } + + if ((!curLineNextOp.chars) && (!curLineOpIter.hasNext())) { + curLine++; + curChar = 0; + } + } + + function skip(N, L) { + if (L) { + curLine += L; + curChar = 0; + } else { + if (curLineOpIter && curLineOpIterLine == curLine) { + consumeAttribRuns(N, function () {}); + } else { + curChar += N; + } + } + } + + function nextText(numChars) { + var len = 0; + var assem = exports.stringAssembler(); + var firstString = lines_get(curLine).substring(curChar); + len += firstString.length; + assem.append(firstString); + + var lineNum = curLine + 1; + while (len < numChars) { + var nextString = lines_get(lineNum); + len += nextString.length; + assem.append(nextString); + lineNum++; + } + + return assem.toString().substring(0, numChars); + } + + function cachedStrFunc(func) { + var cache = {}; + return function (s) { + if (!cache[s]) { + cache[s] = func(s); + } + return cache[s]; + }; + } + + var attribKeys = []; + var attribValues = []; + while (csIter.hasNext()) { + var csOp = csIter.next(); + if (csOp.opcode == '=') { + if (csOp.attribs) { + attribKeys.length = 0; + attribValues.length = 0; + exports.eachAttribNumber(csOp.attribs, function (n) { + attribKeys.push(pool.getAttribKey(n)); + attribValues.push(pool.getAttribValue(n)); + }); + var undoBackToAttribs = cachedStrFunc(function (attribs) { + var backAttribs = []; + for (var i = 0; i < attribKeys.length; i++) { + var appliedKey = attribKeys[i]; + var appliedValue = attribValues[i]; + var oldValue = exports.attribsAttributeValue(attribs, appliedKey, pool); + if (appliedValue != oldValue) { + backAttribs.push([appliedKey, oldValue]); + } + } + return exports.makeAttribsString('=', backAttribs, pool); + }); + consumeAttribRuns(csOp.chars, function (len, attribs, endsLine) { + builder.keep(len, endsLine ? 1 : 0, undoBackToAttribs(attribs)); + }); + } else { + skip(csOp.chars, csOp.lines); + builder.keep(csOp.chars, csOp.lines); + } + } else if (csOp.opcode == '+') { + builder.remove(csOp.chars, csOp.lines); + } else if (csOp.opcode == '-') { + var textBank = nextText(csOp.chars); + var textBankIndex = 0; + consumeAttribRuns(csOp.chars, function (len, attribs, endsLine) { + builder.insert(textBank.substr(textBankIndex, len), attribs); + textBankIndex += len; + }); + } + } + + return exports.checkRep(builder.toString()); +}; + +// %CLIENT FILE ENDS HERE% +exports.follow = function (cs1, cs2, reverseInsertOrder, pool) { + var unpacked1 = exports.unpack(cs1); + var unpacked2 = exports.unpack(cs2); + var len1 = unpacked1.oldLen; + var len2 = unpacked2.oldLen; + exports.assert(len1 == len2, "mismatched follow"); + var chars1 = exports.stringIterator(unpacked1.charBank); + var chars2 = exports.stringIterator(unpacked2.charBank); + + var oldLen = unpacked1.newLen; + var oldPos = 0; + var newLen = 0; + + var hasInsertFirst = exports.attributeTester(['insertorder', 'first'], pool); + + var newOps = exports.applyZip(unpacked1.ops, 0, unpacked2.ops, 0, function (op1, op2, opOut) { + if (op1.opcode == '+' || op2.opcode == '+') { + var whichToDo; + if (op2.opcode != '+') { + whichToDo = 1; + } else if (op1.opcode != '+') { + whichToDo = 2; + } else { + // both + + var firstChar1 = chars1.peek(1); + var firstChar2 = chars2.peek(1); + var insertFirst1 = hasInsertFirst(op1.attribs); + var insertFirst2 = hasInsertFirst(op2.attribs); + if (insertFirst1 && !insertFirst2) { + whichToDo = 1; + } else if (insertFirst2 && !insertFirst1) { + whichToDo = 2; + } + // insert string that doesn't start with a newline first so as not to break up lines + else if (firstChar1 == '\n' && firstChar2 != '\n') { + whichToDo = 2; + } else if (firstChar1 != '\n' && firstChar2 == '\n') { + whichToDo = 1; + } + // break symmetry: + else if (reverseInsertOrder) { + whichToDo = 2; + } else { + whichToDo = 1; + } + } + if (whichToDo == 1) { + chars1.skip(op1.chars); + opOut.opcode = '='; + opOut.lines = op1.lines; + opOut.chars = op1.chars; + opOut.attribs = ''; + op1.opcode = ''; + } else { + // whichToDo == 2 + chars2.skip(op2.chars); + exports.copyOp(op2, opOut); + op2.opcode = ''; + } + } else if (op1.opcode == '-') { + if (!op2.opcode) { + op1.opcode = ''; + } else { + if (op1.chars <= op2.chars) { + op2.chars -= op1.chars; + op2.lines -= op1.lines; + op1.opcode = ''; + if (!op2.chars) { + op2.opcode = ''; + } + } else { + op1.chars -= op2.chars; + op1.lines -= op2.lines; + op2.opcode = ''; + } + } + } else if (op2.opcode == '-') { + exports.copyOp(op2, opOut); + if (!op1.opcode) { + op2.opcode = ''; + } else if (op2.chars <= op1.chars) { + // delete part or all of a keep + op1.chars -= op2.chars; + op1.lines -= op2.lines; + op2.opcode = ''; + if (!op1.chars) { + op1.opcode = ''; + } + } else { + // delete all of a keep, and keep going + opOut.lines = op1.lines; + opOut.chars = op1.chars; + op2.lines -= op1.lines; + op2.chars -= op1.chars; + op1.opcode = ''; + } + } else if (!op1.opcode) { + exports.copyOp(op2, opOut); + op2.opcode = ''; + } else if (!op2.opcode) { + exports.copyOp(op1, opOut); + op1.opcode = ''; + } else { + // both keeps + opOut.opcode = '='; + opOut.attribs = exports.followAttributes(op1.attribs, op2.attribs, pool); + if (op1.chars <= op2.chars) { + opOut.chars = op1.chars; + opOut.lines = op1.lines; + op2.chars -= op1.chars; + op2.lines -= op1.lines; + op1.opcode = ''; + if (!op2.chars) { + op2.opcode = ''; + } + } else { + opOut.chars = op2.chars; + opOut.lines = op2.lines; + op1.chars -= op2.chars; + op1.lines -= op2.lines; + op2.opcode = ''; + } + } + switch (opOut.opcode) { + case '=': + oldPos += opOut.chars; + newLen += opOut.chars; + break; + case '-': + oldPos += opOut.chars; + break; + case '+': + newLen += opOut.chars; + break; + } + }); + newLen += oldLen - oldPos; + + return exports.pack(oldLen, newLen, newOps, unpacked2.charBank); +}; + +exports.followAttributes = function (att1, att2, pool) { + // The merge of two sets of attribute changes to the same text + // takes the lexically-earlier value if there are two values + // for the same key. Otherwise, all key/value changes from + // both attribute sets are taken. This operation is the "follow", + // so a set of changes is produced that can be applied to att1 + // to produce the merged set. + if ((!att2) || (!pool)) return ''; + if (!att1) return att2; + var atts = []; + att2.replace(/\*([0-9a-z]+)/g, function (_, a) { + atts.push(pool.getAttrib(exports.parseNum(a))); + return ''; + }); + att1.replace(/\*([0-9a-z]+)/g, function (_, a) { + var pair1 = pool.getAttrib(exports.parseNum(a)); + for (var i = 0; i < atts.length; i++) { + var pair2 = atts[i]; + if (pair1[0] == pair2[0]) { + if (pair1[1] <= pair2[1]) { + // winner of merge is pair1, delete this attribute + atts.splice(i, 1); + } + break; + } + } + return ''; + }); + // we've only removed attributes, so they're already sorted + var buf = exports.stringAssembler(); + for (var i = 0; i < atts.length; i++) { + buf.append('*'); + buf.append(exports.numToString(pool.putAttrib(atts[i]))); + } + return buf.toString(); +}; diff --git a/src/static/js/ace.js b/src/static/js/ace.js new file mode 100644 index 00000000..1306dba0 --- /dev/null +++ b/src/static/js/ace.js @@ -0,0 +1,312 @@ +/** + * This code is mostly from the old Etherpad. Please help us to comment this code. + * This helps other people to understand this code better and helps them to improve it. + * TL;DR COMMENTS ON THIS FILE ARE HIGHLY APPRECIATED + */ + +/** + * 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. + */ + +// requires: top +// requires: plugins +// requires: undefined + +Ace2Editor.registry = { + nextId: 1 +}; + +var hooks = require('./pluginfw/hooks'); + +function Ace2Editor() +{ + var ace2 = Ace2Editor; + + var editor = {}; + var info = { + editor: editor, + id: (ace2.registry.nextId++) + }; + var loaded = false; + + var actionsPendingInit = []; + + function pendingInit(func, optDoNow) + { + return function() + { + var that = this; + var args = arguments; + var action = function() + { + func.apply(that, args); + } + if (optDoNow) + { + optDoNow.apply(that, args); + } + if (loaded) + { + action(); + } + else + { + actionsPendingInit.push(action); + } + }; + } + + function doActionsPendingInit() + { + $.each(actionsPendingInit, function(i,fn){ + fn() + }); + actionsPendingInit = []; + } + + ace2.registry[info.id] = info; + + // The following functions (prefixed by 'ace_') are exposed by editor, but + // execution is delayed until init is complete + var aceFunctionsPendingInit = ['importText', 'importAText', 'focus', + 'setEditable', 'getFormattedCode', 'setOnKeyPress', 'setOnKeyDown', + 'setNotifyDirty', 'setProperty', 'setBaseText', 'setBaseAttributedText', + 'applyChangesToBase', 'applyPreparedChangesetToBase', + 'setUserChangeNotificationCallback', 'setAuthorInfo', + 'setAuthorSelectionRange', 'callWithAce', 'execCommand', 'replaceRange']; + + $.each(aceFunctionsPendingInit, function(i,fnName){ + var prefix = 'ace_'; + var name = prefix + fnName; + editor[fnName] = pendingInit(function(){ + info[prefix + fnName].apply(this, arguments); + }); + }); + + editor.exportText = function() + { + if (!loaded) return "(awaiting init)\n"; + return info.ace_exportText(); + }; + + editor.getFrame = function() + { + return info.frame || null; + }; + + editor.getDebugProperty = function(prop) + { + return info.ace_getDebugProperty(prop); + }; + + // prepareUserChangeset: + // Returns null if no new changes or ACE not ready. Otherwise, bundles up all user changes + // to the latest base text into a Changeset, which is returned (as a string if encodeAsString). + // If this method returns a truthy value, then applyPreparedChangesetToBase can be called + // at some later point to consider these changes part of the base, after which prepareUserChangeset + // must be called again before applyPreparedChangesetToBase. Multiple consecutive calls + // to prepareUserChangeset will return an updated changeset that takes into account the + // latest user changes, and modify the changeset to be applied by applyPreparedChangesetToBase + // accordingly. + editor.prepareUserChangeset = function() + { + if (!loaded) return null; + return info.ace_prepareUserChangeset(); + }; + + editor.getUnhandledErrors = function() + { + if (!loaded) return []; + // returns array of {error: <browser Error object>, time: +new Date()} + return info.ace_getUnhandledErrors(); + }; + + + + function sortFilesByEmbeded(files) { + var embededFiles = []; + var remoteFiles = []; + + if (Ace2Editor.EMBEDED) { + for (var i = 0, ii = files.length; i < ii; i++) { + var file = files[i]; + if (Object.prototype.hasOwnProperty.call(Ace2Editor.EMBEDED, file)) { + embededFiles.push(file); + } else { + remoteFiles.push(file); + } + } + } else { + remoteFiles = files; + } + + return {embeded: embededFiles, remote: remoteFiles}; + } + function pushRequireScriptTo(buffer) { + var KERNEL_SOURCE = '../static/js/require-kernel.js'; + var KERNEL_BOOT = '\ +require.setRootURI("../javascripts/src");\n\ +require.setLibraryURI("../javascripts/lib");\n\ +require.setGlobalKeyPath("require");\n\ +'; + if (Ace2Editor.EMBEDED && Ace2Editor.EMBEDED[KERNEL_SOURCE]) { + buffer.push('<script type="text/javascript">'); + buffer.push(Ace2Editor.EMBEDED[KERNEL_SOURCE]); + buffer.push(KERNEL_BOOT); + buffer.push('<\/script>'); + } + } + function pushScriptsTo(buffer) { + /* Folling is for packaging regular expression. */ + /* $$INCLUDE_JS("../javascripts/src/ace2_inner.js?callback=require.define"); */ + var ACE_SOURCE = '../javascripts/src/ace2_inner.js?callback=require.define'; + if (Ace2Editor.EMBEDED && Ace2Editor.EMBEDED[ACE_SOURCE]) { + buffer.push('<script type="text/javascript">'); + buffer.push(Ace2Editor.EMBEDED[ACE_SOURCE]); + buffer.push('require("ep_etherpad-lite/static/js/ace2_inner");'); + buffer.push('<\/script>'); + } else { + file = ACE_SOURCE; + buffer.push('<script type="application/javascript" src="' + ACE_SOURCE + '"><\/script>'); + buffer.push('<script type="text/javascript">'); + buffer.push('require("ep_etherpad-lite/static/js/ace2_inner");'); + buffer.push('<\/script>'); + } + } + function pushStyleTagsFor(buffer, files) { + var sorted = sortFilesByEmbeded(files); + var embededFiles = sorted.embeded; + var remoteFiles = sorted.remote; + + if (embededFiles.length > 0) { + buffer.push('<style type="text/css">'); + for (var i = 0, ii = embededFiles.length; i < ii; i++) { + var file = embededFiles[i]; + buffer.push(Ace2Editor.EMBEDED[file].replace(/<\//g, '<\\/')); + } + buffer.push('<\/style>'); + } + for (var i = 0, ii = remoteFiles.length; i < ii; i++) { + var file = remoteFiles[i]; + buffer.push('<link rel="stylesheet" type="text/css" href="' + file + '"\/>'); + } + } + + editor.destroy = pendingInit(function() + { + info.ace_dispose(); + info.frame.parentNode.removeChild(info.frame); + delete ace2.registry[info.id]; + info = null; // prevent IE 6 closure memory leaks + }); + + editor.init = function(containerId, initialCode, doneFunc) + { + + editor.importText(initialCode); + + info.onEditorReady = function() + { + loaded = true; + doActionsPendingInit(); + doneFunc(); + }; + + (function() + { + var doctype = "<!doctype html>"; + + var iframeHTML = []; + + iframeHTML.push(doctype); + iframeHTML.push("<html><head>"); + + // For compatability's sake transform in and out. + for (var i = 0, ii = iframeHTML.length; i < ii; i++) { + iframeHTML[i] = JSON.stringify(iframeHTML[i]); + } + hooks.callAll("aceInitInnerdocbodyHead", { + iframeHTML: iframeHTML + }); + for (var i = 0, ii = iframeHTML.length; i < ii; i++) { + iframeHTML[i] = JSON.parse(iframeHTML[i]); + } + + // calls to these functions ($$INCLUDE_...) are replaced when this file is processed + // and compressed, putting the compressed code from the named file directly into the + // source here. + // these lines must conform to a specific format because they are passed by the build script: + var includedCSS = []; + var $$INCLUDE_CSS = function(filename) {includedCSS.push(filename)}; + $$INCLUDE_CSS("../static/css/iframe_editor.css"); + $$INCLUDE_CSS("../static/css/pad.css"); + $$INCLUDE_CSS("../static/custom/pad.css"); + pushStyleTagsFor(iframeHTML, includedCSS); + + var includedJS = []; + var $$INCLUDE_JS = function(filename) {includedJS.push(filename)}; + pushRequireScriptTo(iframeHTML); + // Inject my plugins into my child. + iframeHTML.push('\ +<script type="text/javascript">\ + require.define("/plugins", null);\n\ + require.define("/plugins.js", function (require, exports, module) {\ + module.exports = require("ep_etherpad-lite/static/js/plugins");\ + });\ +</script>\ +'); + pushScriptsTo(iframeHTML); + + iframeHTML.push('<style type="text/css" title="dynamicsyntax"></style>'); + iframeHTML.push('</head><body id="innerdocbody" class="syntax" spellcheck="false"> </body></html>'); + + // Expose myself to global for my child frame. + var thisFunctionsName = "ChildAccessibleAce2Editor"; + (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.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); }'; + + var outerHTML = [doctype, '<html><head>'] + + var includedCSS = []; + var $$INCLUDE_CSS = function(filename) {includedCSS.push(filename)}; + $$INCLUDE_CSS("../static/css/iframe_editor.css"); + $$INCLUDE_CSS("../static/css/pad.css"); + $$INCLUDE_CSS("../static/custom/pad.css"); + pushStyleTagsFor(outerHTML, includedCSS); + + // bizarrely, in FF2, a file with no "external" dependencies won't finish loading properly + // (throbs busy while typing) + 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"); + outerFrame.name = "ace_outer"; + outerFrame.frameBorder = 0; // for IE + info.frame = outerFrame; + document.getElementById(containerId).appendChild(outerFrame); + + var editorDocument = outerFrame.contentWindow.document; + + editorDocument.open(); + editorDocument.write(outerHTML.join('')); + editorDocument.close(); + })(); + }; + + return editor; +} + +exports.Ace2Editor = Ace2Editor; diff --git a/src/static/js/ace2_common.js b/src/static/js/ace2_common.js new file mode 100644 index 00000000..9f217045 --- /dev/null +++ b/src/static/js/ace2_common.js @@ -0,0 +1,162 @@ +/** + * This code is mostly from the old Etherpad. Please help us to comment this code. + * This helps other people to understand this code better and helps them to improve it. + * TL;DR COMMENTS ON THIS FILE ARE HIGHLY APPRECIATED + */ + +/** + * 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. + */ + +var Security = require('./security'); + +function isNodeText(node) +{ + return (node.nodeType == 3); +} + +function object(o) +{ + var f = function() + {}; + f.prototype = o; + 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(); + +// Figure out what browser is being used (stolen from jquery 1.2.1) +var browser = { + version: (userAgent.match(/.+(?:rv|it|ra|ie)[\/: ]([\d.]+)/) || [])[1], + safari: /webkit/.test(userAgent), + opera: /opera/.test(userAgent), + msie: /msie/.test(userAgent) && !/opera/.test(userAgent), + mozilla: /mozilla/.test(userAgent) && !/(compatible|webkit)/.test(userAgent), + windows: /windows/.test(userAgent), + mobile: /mobile/.test(userAgent) || /android/.test(userAgent) +}; + + +function getAssoc(obj, name) +{ + return obj["_magicdom_" + name]; +} + +function setAssoc(obj, name, value) +{ + // note that in IE designMode, properties of a node can get + // copied to new nodes that are spawned during editing; also, + // properties representable in HTML text can survive copy-and-paste + obj["_magicdom_" + name] = value; +} + +// "func" is a function over 0..(numItems-1) that is monotonically +// "increasing" with index (false, then true). Finds the boundary +// between false and true, a number between 0 and numItems inclusive. + + +function binarySearch(numItems, func) +{ + if (numItems < 1) return 0; + if (func(0)) return 0; + if (!func(numItems - 1)) return numItems; + var low = 0; // func(low) is always false + var high = numItems - 1; // func(high) is always true + while ((high - low) > 1) + { + var x = Math.floor((low + high) / 2); // x != low, x != high + if (func(x)) high = x; + else low = x; + } + return high; +} + +function binarySearchInfinite(expectedLength, func) +{ + var i = 0; + while (!func(i)) i += expectedLength; + return binarySearch(i, func); +} + +function htmlPrettyEscape(str) +{ + return Security.escapeHTML(str).replace(/\r?\n/g, '\\n'); +} + +var noop = function(){}; +var identity = function(x){return x}; + +exports.isNodeText = isNodeText; +exports.object = object; +exports.extend = extend; +exports.forEach = forEach; +exports.map = map; +exports.filter = filter; +exports.isArray = isArray; +exports.browser = browser; +exports.getAssoc = getAssoc; +exports.setAssoc = setAssoc; +exports.binarySearch = binarySearch; +exports.binarySearchInfinite = binarySearchInfinite; +exports.htmlPrettyEscape = htmlPrettyEscape; +exports.map = map; +exports.noop = noop; +exports.identity = identity; diff --git a/src/static/js/ace2_inner.js b/src/static/js/ace2_inner.js new file mode 100644 index 00000000..66f19faf --- /dev/null +++ b/src/static/js/ace2_inner.js @@ -0,0 +1,5669 @@ +/** + * This code is mostly from the old Etherpad. Please help us to comment this code. + * This helps other people to understand this code better and helps them to improve it. + * TL;DR COMMENTS ON THIS FILE ARE HIGHLY APPRECIATED + */ + +/** + * 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. + */ + +var Ace2Common = require('./ace2_common'); + +// Extract useful method defined in the other module. +var isNodeText = Ace2Common.isNodeText; +var object = Ace2Common.object; +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 colorutils = require('./colorutils').colorutils; +var makeContentCollector = require('./contentcollector').makeContentCollector; +var makeCSSManager = require('./cssmanager').makeCSSManager; +var domline = require('./domline').domline; +var AttribPool = require('./AttributePoolFactory').createAttributePool; +var Changeset = require('./Changeset'); +var linestylefilter = require('./linestylefilter').linestylefilter; +var newSkipList = require('./skiplist').newSkipList; +var undoModule = require('./undomodule').undoModule; +var makeVirtualLineView = require('./virtual_lines').makeVirtualLineView; + +function Ace2Inner(){ + var DEBUG = false; //$$ build script replaces the string "var DEBUG=true;//$$" with "var DEBUG=false;" + // changed to false + var isSetUp = false; + + var THE_TAB = ' '; //4 + var MAX_LIST_LEVEL = 8; + + var LINE_NUMBER_PADDING_RIGHT = 4; + var LINE_NUMBER_PADDING_LEFT = 4; + var MIN_LINEDIV_WIDTH = 20; + var EDIT_BODY_PADDING_TOP = 8; + var EDIT_BODY_PADDING_LEFT = 8; + + var caughtErrors = []; + + var thisAuthor = ''; + + var disposed = false; + + var editorInfo = parent.editorInfo; + + var iframe = window.frameElement; + var outerWin = iframe.ace_outerWin; + iframe.ace_outerWin = null; // prevent IE 6 memory leak + var sideDiv = iframe.nextSibling; + var lineMetricsDiv = sideDiv.nextSibling; + var overlaysdiv = lineMetricsDiv.nextSibling; + initLineNumbers(); + + var outsideKeyDown = function(evt) + {}; + var outsideKeyPress = function(evt) + { + return true; + }; + var outsideNotifyDirty = function() + {}; + + // 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 + // native IE selections have that behavior (which we try not to interfere with). + // Must be false if selection is collapsed! + var rep = { + lines: newSkipList(), + selStart: null, + selEnd: null, + selFocusAtStart: false, + alltext: "", + alines: [], + apool: new AttribPool() + }; + // lines, alltext, alines, and DOM are set up in setup() + if (undoModule.enabled) + { + undoModule.apool = rep.apool; + } + + var root, doc; // set in setup() + var isEditable = true; + var doesWrap = true; + var hasLineNumbers = true; + var isStyled = true; + + // space around the innermost iframe element + var iframePadLeft = MIN_LINEDIV_WIDTH + LINE_NUMBER_PADDING_RIGHT + EDIT_BODY_PADDING_LEFT; + var iframePadTop = EDIT_BODY_PADDING_TOP; + var iframePadBottom = 0, + iframePadRight = 0; + + var console = (DEBUG && window.console); + + if (!window.console) + { + var names = ["log", "debug", "info", "warn", "error", "assert", "dir", "dirxml", "group", "groupEnd", "time", "timeEnd", "count", "trace", "profile", "profileEnd"]; + console = {}; + for (var i = 0; i < names.length; ++i) + console[names[i]] = noop; + //console.error = function(str) { alert(str); }; + } + + var PROFILER = window.PROFILER; + if (!PROFILER) + { + PROFILER = function() + { + return { + start: noop, + mark: noop, + literal: noop, + end: noop, + cancel: noop + }; + }; + } + + // "dmesg" is for displaying messages in the in-page output pane + // visible when "?djs=1" is appended to the pad URL. It generally + // remains a no-op unless djs is enabled, but we make a habit of + // only calling it in error cases or while debugging. + var dmesg = noop; + window.dmesg = noop; + + var scheduler = parent; + + var textFace = 'monospace'; + var textSize = 12; + + function textLineHeight() + { + return Math.round(textSize * 4 / 3); + } + + var dynamicCSS = null; + var dynamicCSSTop = null; + + function initDynamicCSS() + { + dynamicCSS = makeCSSManager("dynamicsyntax"); + dynamicCSSTop = makeCSSManager("dynamicsyntax", true); + } + + var changesetTracker = makeChangesetTracker(scheduler, rep.apool, { + withCallbacks: function(operationName, f) + { + inCallStackIfNecessary(operationName, function() + { + fastIncorp(1); + f( + { + setDocumentAttributedText: function(atext) + { + setDocAText(atext); + }, + applyChangesetToDocument: function(changeset, preferInsertionAfterCaret) + { + var oldEventType = currentCallStack.editEvent.eventType; + currentCallStack.startNewEvent("nonundoable"); + + performDocumentApplyChangeset(changeset, preferInsertionAfterCaret); + + currentCallStack.startNewEvent(oldEventType); + } + }); + }); + } + }); + + var authorInfos = {}; // presence of key determines if author is present in doc + + function setAuthorInfo(author, info) + { + if ((typeof author) != "string") + { + throw new Error("setAuthorInfo: author (" + author + ") is not a string"); + } + if (!info) + { + delete authorInfos[author]; + if (dynamicCSS) + { + dynamicCSS.removeSelectorStyle(getAuthorColorClassSelector(getAuthorClassName(author))); + dynamicCSSTop.removeSelectorStyle(getAuthorColorClassSelector(getAuthorClassName(author))); + } + } + else + { + authorInfos[author] = info; + if (info.bgcolor) + { + if (dynamicCSS) + { + var bgcolor = info.bgcolor; + if ((typeof info.fade) == "number") + { + bgcolor = fadeColor(bgcolor, info.fade); + } + + var authorStyle = dynamicCSS.selectorStyle(getAuthorColorClassSelector( + getAuthorClassName(author))); + var authorStyleTop = dynamicCSSTop.selectorStyle(getAuthorColorClassSelector( + getAuthorClassName(author))); + var anchorStyle = dynamicCSS.selectorStyle(getAuthorColorClassSelector( + getAuthorClassName(author))+' > a') + + // author color + authorStyle.backgroundColor = bgcolor; + authorStyleTop.backgroundColor = bgcolor; + + // text contrast + if(colorutils.luminosity(colorutils.css2triple(bgcolor)) < 0.5) + { + authorStyle.color = '#ffffff'; + authorStyleTop.color = '#ffffff'; + }else{ + authorStyle.color = null; + authorStyleTop.color = null; + } + + // anchor text contrast + if(colorutils.luminosity(colorutils.css2triple(bgcolor)) < 0.55) + { + anchorStyle.color = colorutils.triple2css(colorutils.complementary(colorutils.css2triple(bgcolor))); + }else{ + anchorStyle.color = null; + } + } + } + } + } + + function getAuthorClassName(author) + { + return "author-" + author.replace(/[^a-y0-9]/g, function(c) + { + if (c == ".") return "-"; + return 'z' + c.charCodeAt(0) + 'z'; + }); + } + + function className2Author(className) + { + if (className.substring(0, 7) == "author-") + { + return className.substring(7).replace(/[a-y0-9]+|-|z.+?z/g, function(cc) + { + if (cc == '-') return '.'; + else if (cc.charAt(0) == 'z') + { + return String.fromCharCode(Number(cc.slice(1, -1))); + } + else + { + return cc; + } + }); + } + return null; + } + + function getAuthorColorClassSelector(oneClassName) + { + return ".authorColors ." + oneClassName; + } + + function setUpTrackingCSS() + { + if (dynamicCSS) + { + var backgroundHeight = lineMetricsDiv.offsetHeight; + var lineHeight = textLineHeight(); + var extraBodding = 0; + var extraTodding = 0; + if (backgroundHeight < lineHeight) + { + extraBodding = Math.ceil((lineHeight - backgroundHeight) / 2); + extraTodding = lineHeight - backgroundHeight - extraBodding; + } + var spanStyle = dynamicCSS.selectorStyle("#innerdocbody span"); + spanStyle.paddingTop = extraTodding + "px"; + spanStyle.paddingBottom = extraBodding + "px"; + } + } + + function boldColorFromColor(lightColorCSS) + { + var color = colorutils.css2triple(lightColorCSS); + + // amp up the saturation to full + color = colorutils.saturate(color); + + // normalize brightness based on luminosity + color = colorutils.scaleColor(color, 0, 0.5 / colorutils.luminosity(color)); + + return colorutils.triple2css(color); + } + + function fadeColor(colorCSS, fadeFrac) + { + var color = colorutils.css2triple(colorCSS); + color = colorutils.blend(color, [1, 1, 1], fadeFrac); + return colorutils.triple2css(color); + } + + function doAlert(str) + { + scheduler.setTimeout(function() + { + alert(str); + }, 0); + } + + editorInfo.ace_getRep = function() + { + return rep; + }; + + var currentCallStack = null; + + function inCallStack(type, action) + { + if (disposed) return; + + if (currentCallStack) + { + console.error("Can't enter callstack " + type + ", already in " + currentCallStack.type); + } + + var profiling = false; + + function profileRest() + { + profiling = true; + console.profile(); + } + + function newEditEvent(eventType) + { + return { + eventType: eventType, + backset: null + }; + } + + function submitOldEvent(evt) + { + if (rep.selStart && rep.selEnd) + { + var selStartChar = rep.lines.offsetOfIndex(rep.selStart[0]) + rep.selStart[1]; + var selEndChar = rep.lines.offsetOfIndex(rep.selEnd[0]) + rep.selEnd[1]; + evt.selStart = selStartChar; + evt.selEnd = selEndChar; + evt.selFocusAtStart = rep.selFocusAtStart; + } + if (undoModule.enabled) + { + var undoWorked = false; + try + { + if (evt.eventType == "setup" || evt.eventType == "importText" || evt.eventType == "setBaseText") + { + undoModule.clearHistory(); + } + else if (evt.eventType == "nonundoable") + { + if (evt.changeset) + { + undoModule.reportExternalChange(evt.changeset); + } + } + else + { + undoModule.reportEvent(evt); + } + undoWorked = true; + } + finally + { + if (!undoWorked) + { + undoModule.enabled = false; // for safety + } + } + } + } + + function startNewEvent(eventType, dontSubmitOld) + { + var oldEvent = currentCallStack.editEvent; + if (!dontSubmitOld) + { + submitOldEvent(oldEvent); + } + currentCallStack.editEvent = newEditEvent(eventType); + return oldEvent; + } + + currentCallStack = { + type: type, + docTextChanged: false, + selectionAffected: false, + userChangedSelection: false, + domClean: false, + profileRest: profileRest, + isUserChange: false, + // is this a "user change" type of call-stack + repChanged: false, + editEvent: newEditEvent(type), + startNewEvent: startNewEvent + }; + var cleanExit = false; + var result; + try + { + result = action(); + //console.log("Just did action for: "+type); + cleanExit = true; + } + catch (e) + { + caughtErrors.push( + { + error: e, + time: +new Date() + }); + dmesg(e.toString()); + throw e; + } + finally + { + var cs = currentCallStack; + //console.log("Finished action for: "+type); + if (cleanExit) + { + submitOldEvent(cs.editEvent); + if (cs.domClean && cs.type != "setup") + { + // if (cs.isUserChange) + // { + // if (cs.repChanged) parenModule.notifyChange(); + // else parenModule.notifyTick(); + // } + if (cs.selectionAffected) + { + updateBrowserSelectionFromRep(); + } + if ((cs.docTextChanged || cs.userChangedSelection) && cs.type != "applyChangesToBase") + { + scrollSelectionIntoView(); + } + if (cs.docTextChanged && cs.type.indexOf("importText") < 0) + { + outsideNotifyDirty(); + } + } + } + else + { + // non-clean exit + if (currentCallStack.type == "idleWorkTimer") + { + idleWorkTimer.atLeast(1000); + } + } + currentCallStack = null; + if (profiling) console.profileEnd(); + } + return result; + } + editorInfo.ace_inCallStack = inCallStack; + + function inCallStackIfNecessary(type, action) + { + if (!currentCallStack) + { + inCallStack(type, action); + } + else + { + action(); + } + } + editorInfo.ace_inCallStackIfNecessary = inCallStackIfNecessary; + + function recolorLineByKey(key) + { + if (rep.lines.containsKey(key)) + { + var offset = rep.lines.offsetOfKey(key); + var width = rep.lines.atKey(key).width; + recolorLinesInRange(offset, offset + width); + } + } + + function getLineKeyForOffset(charOffset) + { + return rep.lines.atOffset(charOffset).key; + } + + function dispose() + { + disposed = true; + if (idleWorkTimer) idleWorkTimer.never(); + teardown(); + } + + function checkALines() + { + return; // disable for speed + + + function error() + { + throw new Error("checkALines"); + } + if (rep.alines.length != rep.lines.length()) + { + error(); + } + for (var i = 0; i < rep.alines.length; i++) + { + var aline = rep.alines[i]; + var lineText = rep.lines.atIndex(i).text + "\n"; + var lineTextLength = lineText.length; + var opIter = Changeset.opIterator(aline); + var alineLength = 0; + while (opIter.hasNext()) + { + var o = opIter.next(); + alineLength += o.chars; + if (opIter.hasNext()) + { + if (o.lines !== 0) error(); + } + else + { + if (o.lines != 1) error(); + } + } + if (alineLength != lineTextLength) + { + error(); + } + } + } + + function setWraps(newVal) + { + doesWrap = newVal; + var dwClass = "doesWrap"; + setClassPresence(root, "doesWrap", doesWrap); + scheduler.setTimeout(function() + { + inCallStackIfNecessary("setWraps", function() + { + fastIncorp(7); + recreateDOM(); + fixView(); + }); + }, 0); + } + + function setStyled(newVal) + { + var oldVal = isStyled; + isStyled = !! newVal; + + if (newVal != oldVal) + { + if (!newVal) + { + // clear styles + inCallStackIfNecessary("setStyled", function() + { + fastIncorp(12); + var clearStyles = []; + for (var k in STYLE_ATTRIBS) + { + clearStyles.push([k, '']); + } + performDocumentApplyAttributesToCharRange(0, rep.alltext.length, clearStyles); + }); + } + } + } + + function setTextFace(face) + { + textFace = face; + root.style.fontFamily = textFace; + lineMetricsDiv.style.fontFamily = textFace; + scheduler.setTimeout(function() + { + setUpTrackingCSS(); + }, 0); + } + + function setTextSize(size) + { + textSize = size; + root.style.fontSize = textSize + "px"; + root.style.lineHeight = textLineHeight() + "px"; + sideDiv.style.lineHeight = textLineHeight() + "px"; + lineMetricsDiv.style.fontSize = textSize + "px"; + scheduler.setTimeout(function() + { + setUpTrackingCSS(); + }, 0); + } + + function recreateDOM() + { + // precond: normalized + recolorLinesInRange(0, rep.alltext.length); + } + + function setEditable(newVal) + { + isEditable = newVal; + + // the following may fail, e.g. if iframe is hidden + if (!isEditable) + { + setDesignMode(false); + } + else + { + setDesignMode(true); + } + setClassPresence(root, "static", !isEditable); + } + + function enforceEditability() + { + setEditable(isEditable); + } + + function importText(text, undoable, dontProcess) + { + var lines; + if (dontProcess) + { + if (text.charAt(text.length - 1) != "\n") + { + throw new Error("new raw text must end with newline"); + } + if (/[\r\t\xa0]/.exec(text)) + { + throw new Error("new raw text must not contain CR, tab, or nbsp"); + } + lines = text.substring(0, text.length - 1).split('\n'); + } + else + { + lines = map(text.split('\n'), textify); + } + var newText = "\n"; + if (lines.length > 0) + { + newText = lines.join('\n') + '\n'; + } + + inCallStackIfNecessary("importText" + (undoable ? "Undoable" : ""), function() + { + setDocText(newText); + }); + + if (dontProcess && rep.alltext != text) + { + throw new Error("mismatch error setting raw text in importText"); + } + } + + function importAText(atext, apoolJsonObj, undoable) + { + atext = Changeset.cloneAText(atext); + if (apoolJsonObj) + { + var wireApool = (new AttribPool()).fromJsonable(apoolJsonObj); + atext.attribs = Changeset.moveOpsToNewPool(atext.attribs, wireApool, rep.apool); + } + inCallStackIfNecessary("importText" + (undoable ? "Undoable" : ""), function() + { + setDocAText(atext); + }); + } + + function setDocAText(atext) + { + fastIncorp(8); + + var oldLen = rep.lines.totalWidth(); + var numLines = rep.lines.length(); + var upToLastLine = rep.lines.offsetOfIndex(numLines - 1); + var lastLineLength = rep.lines.atIndex(numLines - 1).text.length; + var assem = Changeset.smartOpAssembler(); + var o = Changeset.newOp('-'); + o.chars = upToLastLine; + o.lines = numLines - 1; + assem.append(o); + o.chars = lastLineLength; + o.lines = 0; + assem.append(o); + Changeset.appendATextToAssembler(atext, assem); + var newLen = oldLen + assem.getLengthChange(); + var changeset = Changeset.checkRep( + Changeset.pack(oldLen, newLen, assem.toString(), atext.text.slice(0, -1))); + performDocumentApplyChangeset(changeset); + + performSelectionChange([0, rep.lines.atIndex(0).lineMarker], [0, rep.lines.atIndex(0).lineMarker]); + + idleWorkTimer.atMost(100); + + if (rep.alltext != atext.text) + { + dmesg(htmlPrettyEscape(rep.alltext)); + dmesg(htmlPrettyEscape(atext.text)); + throw new Error("mismatch error setting raw text in setDocAText"); + } + } + + function setDocText(text) + { + setDocAText(Changeset.makeAText(text)); + } + + function getDocText() + { + var alltext = rep.alltext; + var len = alltext.length; + if (len > 0) len--; // final extra newline + return alltext.substring(0, len); + } + + function exportText() + { + if (currentCallStack && !currentCallStack.domClean) + { + inCallStackIfNecessary("exportText", function() + { + fastIncorp(2); + }); + } + return getDocText(); + } + + function editorChangedSize() + { + fixView(); + } + + function setOnKeyPress(handler) + { + outsideKeyPress = handler; + } + + function setOnKeyDown(handler) + { + outsideKeyDown = handler; + } + + function setNotifyDirty(handler) + { + outsideNotifyDirty = handler; + } + + function getFormattedCode() + { + if (currentCallStack && !currentCallStack.domClean) + { + inCallStackIfNecessary("getFormattedCode", incorporateUserChanges); + } + var buf = []; + if (rep.lines.length() > 0) + { + // should be the case, even for empty file + var entry = rep.lines.atIndex(0); + while (entry) + { + var domInfo = entry.domInfo; + buf.push((domInfo && domInfo.getInnerHTML()) || domline.processSpaces(domline.escapeHTML(entry.text), doesWrap) || ' ' /*empty line*/ ); + entry = rep.lines.next(entry); + } + } + return '<div class="syntax"><div>' + buf.join('</div>\n<div>') + '</div></div>'; + } + + var CMDS = { + clearauthorship: function(prompt) + { + if ((!(rep.selStart && rep.selEnd)) || isCaret()) + { + if (prompt) + { + prompt(); + } + else + { + performDocumentApplyAttributesToCharRange(0, rep.alltext.length, [ + ['author', ''] + ]); + } + } + else + { + setAttributeOnSelection('author', ''); + } + } + }; + + function execCommand(cmd) + { + cmd = cmd.toLowerCase(); + var cmdArgs = Array.prototype.slice.call(arguments, 1); + if (CMDS[cmd]) + { + inCallStack(cmd, function() + { + fastIncorp(9); + CMDS[cmd].apply(CMDS, cmdArgs); + }); + } + } + + function replaceRange(start, end, text) + { + inCallStack('replaceRange', function() + { + fastIncorp(9); + performDocumentReplaceRange(start, end, text); + }); + } + + editorInfo.ace_focus = focus; + editorInfo.ace_importText = importText; + editorInfo.ace_importAText = importAText; + editorInfo.ace_exportText = exportText; + editorInfo.ace_editorChangedSize = editorChangedSize; + editorInfo.ace_setOnKeyPress = setOnKeyPress; + editorInfo.ace_setOnKeyDown = setOnKeyDown; + editorInfo.ace_setNotifyDirty = setNotifyDirty; + editorInfo.ace_dispose = dispose; + editorInfo.ace_getFormattedCode = getFormattedCode; + editorInfo.ace_setEditable = setEditable; + editorInfo.ace_execCommand = execCommand; + editorInfo.ace_replaceRange = replaceRange; + + editorInfo.ace_callWithAce = function(fn, callStack, normalize) + { + var wrapper = function() + { + return fn(editorInfo); + }; + + + + if (normalize !== undefined) + { + var wrapper1 = wrapper; + wrapper = function() + { + editorInfo.ace_fastIncorp(9); + wrapper1(); + }; + } + + if (callStack !== undefined) + { + return editorInfo.ace_inCallStack(callStack, wrapper); + } + else + { + return wrapper(); + } + }; + + // This methed exposes a setter for some ace properties + // @param key the name of the parameter + // @param value the value to set to + editorInfo.ace_setProperty = function(key, value) + { + + // Convinience function returning a setter for a class on an element + var setClassPresenceNamed = function(element, cls){ + return function(value){ + setClassPresence(element, cls, !! value) + } + }; + + // These properties are exposed + var setters = { + wraps: setWraps, + showsauthorcolors: setClassPresenceNamed(root, "authorColors"), + showsuserselections: setClassPresenceNamed(root, "userSelections"), + showslinenumbers : function(value){ + hasLineNumbers = !! value; + // disable line numbers on mobile devices + if (browser.mobile) hasLineNumbers = false; + setClassPresence(sideDiv, "sidedivhidden", !hasLineNumbers); + fixView(); + }, + grayedout: setClassPresenceNamed(outerWin.document.body, "grayedout"), + dmesg: function(){ dmesg = window.dmesg = value; }, + userauthor: function(value){ thisAuthor = String(value); }, + styled: setStyled, + textface: setTextFace, + textsize: setTextSize, + rtlistrue: setClassPresenceNamed(root, "rtl") + }; + + var setter = setters[key.toLowerCase()]; + + // check if setter is present + if(setter !== undefined){ + setter(value) + } + }; + + editorInfo.ace_setBaseText = function(txt) + { + changesetTracker.setBaseText(txt); + }; + editorInfo.ace_setBaseAttributedText = function(atxt, apoolJsonObj) + { + setUpTrackingCSS(); + changesetTracker.setBaseAttributedText(atxt, apoolJsonObj); + }; + editorInfo.ace_applyChangesToBase = function(c, optAuthor, apoolJsonObj) + { + changesetTracker.applyChangesToBase(c, optAuthor, apoolJsonObj); + }; + editorInfo.ace_prepareUserChangeset = function() + { + return changesetTracker.prepareUserChangeset(); + }; + editorInfo.ace_applyPreparedChangesetToBase = function() + { + changesetTracker.applyPreparedChangesetToBase(); + }; + editorInfo.ace_setUserChangeNotificationCallback = function(f) + { + changesetTracker.setUserChangeNotificationCallback(f); + }; + editorInfo.ace_setAuthorInfo = function(author, info) + { + setAuthorInfo(author, info); + }; + editorInfo.ace_setAuthorSelectionRange = function(author, start, end) + { + changesetTracker.setAuthorSelectionRange(author, start, end); + }; + + editorInfo.ace_getUnhandledErrors = function() + { + return caughtErrors.slice(); + }; + + editorInfo.ace_getDebugProperty = function(prop) + { + if (prop == "debugger") + { + // obfuscate "eval" so as not to scare yuicompressor + window['ev' + 'al']("debugger"); + } + else if (prop == "rep") + { + return rep; + } + else if (prop == "window") + { + return window; + } + else if (prop == "document") + { + return document; + } + return undefined; + }; + + function now() + { + return (new Date()).getTime(); + } + + function newTimeLimit(ms) + { + //console.debug("new time limit"); + var startTime = now(); + var lastElapsed = 0; + var exceededAlready = false; + var printedTrace = false; + var isTimeUp = function() + { + if (exceededAlready) + { + if ((!printedTrace)) + { // && now() - startTime - ms > 300) { + //console.trace(); + printedTrace = true; + } + return true; + } + var elapsed = now() - startTime; + if (elapsed > ms) + { + exceededAlready = true; + //console.debug("time limit hit, before was %d/%d", lastElapsed, ms); + //console.trace(); + return true; + } + else + { + lastElapsed = elapsed; + return false; + } + }; + + isTimeUp.elapsed = function() + { + return now() - startTime; + }; + return isTimeUp; + } + + + function makeIdleAction(func) + { + var scheduledTimeout = null; + var scheduledTime = 0; + + function unschedule() + { + if (scheduledTimeout) + { + scheduler.clearTimeout(scheduledTimeout); + scheduledTimeout = null; + } + } + + function reschedule(time) + { + unschedule(); + scheduledTime = time; + var delay = time - now(); + if (delay < 0) delay = 0; + scheduledTimeout = scheduler.setTimeout(callback, delay); + } + + function callback() + { + scheduledTimeout = null; + // func may reschedule the action + func(); + } + return { + atMost: function(ms) + { + var latestTime = now() + ms; + if ((!scheduledTimeout) || scheduledTime > latestTime) + { + reschedule(latestTime); + } + }, + // atLeast(ms) will schedule the action if not scheduled yet. + // In other words, "infinity" is replaced by ms, even though + // it is technically larger. + atLeast: function(ms) + { + var earliestTime = now() + ms; + if ((!scheduledTimeout) || scheduledTime < earliestTime) + { + reschedule(earliestTime); + } + }, + never: function() + { + unschedule(); + } + }; + } + + function fastIncorp(n) + { + // normalize but don't do any lexing or anything + incorporateUserChanges(newTimeLimit(0)); + } + editorInfo.ace_fastIncorp = fastIncorp; + + function incorpIfQuick() + { + var me = incorpIfQuick; + var failures = (me.failures || 0); + if (failures < 5) + { + var isTimeUp = newTimeLimit(40); + var madeChanges = incorporateUserChanges(isTimeUp); + if (isTimeUp()) + { + me.failures = failures + 1; + } + return true; + } + else + { + var skipCount = (me.skipCount || 0); + skipCount++; + if (skipCount == 20) + { + skipCount = 0; + me.failures = 0; + } + me.skipCount = skipCount; + } + return false; + } + + var idleWorkTimer = makeIdleAction(function() + { + + //if (! top.BEFORE) top.BEFORE = []; + //top.BEFORE.push(magicdom.root.dom.innerHTML); + //if (! isEditable) return; // and don't reschedule + if (inInternationalComposition) + { + // don't do idle input incorporation during international input composition + idleWorkTimer.atLeast(500); + return; + } + + inCallStack("idleWorkTimer", function() + { + + var isTimeUp = newTimeLimit(250); + + //console.time("idlework"); + var finishedImportantWork = false; + var finishedWork = false; + + try + { + + // isTimeUp() is a soft constraint for incorporateUserChanges, + // which always renormalizes the DOM, no matter how long it takes, + // but doesn't necessarily lex and highlight it + incorporateUserChanges(isTimeUp); + + if (isTimeUp()) return; + + updateLineNumbers(); // update line numbers if any time left + if (isTimeUp()) return; + + var visibleRange = getVisibleCharRange(); + var docRange = [0, rep.lines.totalWidth()]; + //console.log("%o %o", docRange, visibleRange); + finishedImportantWork = true; + finishedWork = true; + } + finally + { + //console.timeEnd("idlework"); + if (finishedWork) + { + idleWorkTimer.atMost(1000); + } + else if (finishedImportantWork) + { + // if we've finished highlighting the view area, + // more highlighting could be counter-productive, + // e.g. if the user just opened a triple-quote and will soon close it. + idleWorkTimer.atMost(500); + } + else + { + var timeToWait = Math.round(isTimeUp.elapsed() / 2); + if (timeToWait < 100) timeToWait = 100; + idleWorkTimer.atMost(timeToWait); + } + } + }); + + //if (! top.AFTER) top.AFTER = []; + //top.AFTER.push(magicdom.root.dom.innerHTML); + }); + + var _nextId = 1; + + function uniqueId(n) + { + // not actually guaranteed to be unique, e.g. if user copy-pastes + // nodes with ids + var nid = n.id; + if (nid) return nid; + return (n.id = "magicdomid" + (_nextId++)); + } + + + function recolorLinesInRange(startChar, endChar, isTimeUp, optModFunc) + { + if (endChar <= startChar) return; + if (startChar < 0 || startChar >= rep.lines.totalWidth()) return; + var lineEntry = rep.lines.atOffset(startChar); // rounds down to line boundary + var lineStart = rep.lines.offsetOfEntry(lineEntry); + var lineIndex = rep.lines.indexOfEntry(lineEntry); + var selectionNeedsResetting = false; + var firstLine = null; + var lastLine = null; + isTimeUp = (isTimeUp || noop); + + // tokenFunc function; accesses current value of lineEntry and curDocChar, + // also mutates curDocChar + var curDocChar; + var tokenFunc = function(tokenText, tokenClass) + { + lineEntry.domInfo.appendSpan(tokenText, tokenClass); + }; + if (optModFunc) + { + var f = tokenFunc; + tokenFunc = function(tokenText, tokenClass) + { + optModFunc(tokenText, tokenClass, f, curDocChar); + curDocChar += tokenText.length; + }; + } + + while (lineEntry && lineStart < endChar && !isTimeUp()) + { + //var timer = newTimeLimit(200); + var lineEnd = lineStart + lineEntry.width; + + curDocChar = lineStart; + lineEntry.domInfo.clearSpans(); + getSpansForLine(lineEntry, tokenFunc, lineStart); + lineEntry.domInfo.finishUpdate(); + + markNodeClean(lineEntry.lineNode); + + if (rep.selStart && rep.selStart[0] == lineIndex || rep.selEnd && rep.selEnd[0] == lineIndex) + { + selectionNeedsResetting = true; + } + + //if (timer()) console.dirxml(lineEntry.lineNode.dom); + if (firstLine === null) firstLine = lineIndex; + lastLine = lineIndex; + lineStart = lineEnd; + lineEntry = rep.lines.next(lineEntry); + lineIndex++; + } + if (selectionNeedsResetting) + { + currentCallStack.selectionAffected = true; + } + //console.debug("Recolored line range %d-%d", firstLine, lastLine); + } + + // like getSpansForRange, but for a line, and the func takes (text,class) + // instead of (width,class); excludes the trailing '\n' from + // consideration by func + + + function getSpansForLine(lineEntry, textAndClassFunc, lineEntryOffsetHint) + { + var lineEntryOffset = lineEntryOffsetHint; + if ((typeof lineEntryOffset) != "number") + { + lineEntryOffset = rep.lines.offsetOfEntry(lineEntry); + } + var text = lineEntry.text; + var width = lineEntry.width; // text.length+1 + if (text.length === 0) + { + // allow getLineStyleFilter to set line-div styles + var func = linestylefilter.getLineStyleFilter( + 0, '', textAndClassFunc, rep.apool); + func('', ''); + } + else + { + var offsetIntoLine = 0; + var filteredFunc = linestylefilter.getFilterStack(text, textAndClassFunc, browser); + var lineNum = rep.lines.indexOfEntry(lineEntry); + var aline = rep.alines[lineNum]; + filteredFunc = linestylefilter.getLineStyleFilter( + text.length, aline, filteredFunc, rep.apool); + filteredFunc(text, ''); + } + } + + var observedChanges; + + function clearObservedChanges() + { + observedChanges = { + cleanNodesNearChanges: {} + }; + } + clearObservedChanges(); + + function getCleanNodeByKey(key) + { + var p = PROFILER("getCleanNodeByKey", false); + p.extra = 0; + var n = doc.getElementById(key); + // copying and pasting can lead to duplicate ids + while (n && isNodeDirty(n)) + { + p.extra++; + n.id = ""; + n = doc.getElementById(key); + } + p.literal(p.extra, "extra"); + p.end(); + return n; + } + + function observeChangesAroundNode(node) + { + // Around this top-level DOM node, look for changes to the document + // (from how it looks in our representation) and record them in a way + // that can be used to "normalize" the document (apply the changes to our + // representation, and put the DOM in a canonical form). + //top.console.log("observeChangesAroundNode(%o)", node); + var cleanNode; + var hasAdjacentDirtyness; + if (!isNodeDirty(node)) + { + cleanNode = node; + var prevSib = cleanNode.previousSibling; + var nextSib = cleanNode.nextSibling; + hasAdjacentDirtyness = ((prevSib && isNodeDirty(prevSib)) || (nextSib && isNodeDirty(nextSib))); + } + else + { + // node is dirty, look for clean node above + var upNode = node.previousSibling; + while (upNode && isNodeDirty(upNode)) + { + upNode = upNode.previousSibling; + } + if (upNode) + { + cleanNode = upNode; + } + else + { + var downNode = node.nextSibling; + while (downNode && isNodeDirty(downNode)) + { + downNode = downNode.nextSibling; + } + if (downNode) + { + cleanNode = downNode; + } + } + if (!cleanNode) + { + // Couldn't find any adjacent clean nodes! + // Since top and bottom of doc is dirty, the dirty area will be detected. + return; + } + hasAdjacentDirtyness = true; + } + + if (hasAdjacentDirtyness) + { + // previous or next line is dirty + observedChanges.cleanNodesNearChanges['$' + uniqueId(cleanNode)] = true; + } + else + { + // next and prev lines are clean (if they exist) + var lineKey = uniqueId(cleanNode); + var prevSib = cleanNode.previousSibling; + var nextSib = cleanNode.nextSibling; + var actualPrevKey = ((prevSib && uniqueId(prevSib)) || null); + var actualNextKey = ((nextSib && uniqueId(nextSib)) || null); + var repPrevEntry = rep.lines.prev(rep.lines.atKey(lineKey)); + var repNextEntry = rep.lines.next(rep.lines.atKey(lineKey)); + var repPrevKey = ((repPrevEntry && repPrevEntry.key) || null); + var repNextKey = ((repNextEntry && repNextEntry.key) || null); + if (actualPrevKey != repPrevKey || actualNextKey != repNextKey) + { + observedChanges.cleanNodesNearChanges['$' + uniqueId(cleanNode)] = true; + } + } + } + + function observeChangesAroundSelection() + { + if (currentCallStack.observedSelection) return; + currentCallStack.observedSelection = true; + + var p = PROFILER("getSelection", false); + var selection = getSelection(); + p.end(); + + function topLevel(n) + { + if ((!n) || n == root) return null; + while (n.parentNode != root) + { + n = n.parentNode; + } + return n; + } + + if (selection) + { + var node1 = topLevel(selection.startPoint.node); + var node2 = topLevel(selection.endPoint.node); + if (node1) observeChangesAroundNode(node1); + if (node2 && node1 != node2) + { + observeChangesAroundNode(node2); + } + } + } + + function observeSuspiciousNodes() + { + // inspired by Firefox bug #473255, where pasting formatted text + // causes the cursor to jump away, making the new HTML never found. + if (root.getElementsByTagName) + { + var nds = root.getElementsByTagName("style"); + for (var i = 0; i < nds.length; i++) + { + var n = nds[i]; + while (n.parentNode && n.parentNode != root) + { + n = n.parentNode; + } + if (n.parentNode == root) + { + observeChangesAroundNode(n); + } + } + } + } + + function incorporateUserChanges(isTimeUp) + { + + if (currentCallStack.domClean) return false; + + inInternationalComposition = false; // if we need the document normalized, so be it + currentCallStack.isUserChange = true; + + isTimeUp = (isTimeUp || + function() + { + return false; + }); + + if (DEBUG && window.DONT_INCORP || window.DEBUG_DONT_INCORP) return false; + + var p = PROFILER("incorp", false); + + //if (doc.body.innerHTML.indexOf("AppJet") >= 0) + //dmesg(htmlPrettyEscape(doc.body.innerHTML)); + //if (top.RECORD) top.RECORD.push(doc.body.innerHTML); + // returns true if dom changes were made + if (!root.firstChild) + { + root.innerHTML = "<div><!-- --></div>"; + } + + p.mark("obs"); + observeChangesAroundSelection(); + observeSuspiciousNodes(); + p.mark("dirty"); + var dirtyRanges = getDirtyRanges(); + //console.log("dirtyRanges: "+toSource(dirtyRanges)); + var dirtyRangesCheckOut = true; + var j = 0; + var a, b; + while (j < dirtyRanges.length) + { + a = dirtyRanges[j][0]; + b = dirtyRanges[j][1]; + if (!((a === 0 || getCleanNodeByKey(rep.lines.atIndex(a - 1).key)) && (b == rep.lines.length() || getCleanNodeByKey(rep.lines.atIndex(b).key)))) + { + dirtyRangesCheckOut = false; + break; + } + j++; + } + if (!dirtyRangesCheckOut) + { + var numBodyNodes = root.childNodes.length; + for (var k = 0; k < numBodyNodes; k++) + { + var bodyNode = root.childNodes.item(k); + if ((bodyNode.tagName) && ((!bodyNode.id) || (!rep.lines.containsKey(bodyNode.id)))) + { + observeChangesAroundNode(bodyNode); + } + } + dirtyRanges = getDirtyRanges(); + } + + clearObservedChanges(); + + p.mark("getsel"); + var selection = getSelection(); + + //console.log(magicdom.root.dom.innerHTML); + //console.log("got selection: %o", selection); + var selStart, selEnd; // each one, if truthy, has [line,char] needed to set selection + var i = 0; + var splicesToDo = []; + var netNumLinesChangeSoFar = 0; + var toDeleteAtEnd = []; + p.mark("ranges"); + p.literal(dirtyRanges.length, "numdirt"); + var domInsertsNeeded = []; // each entry is [nodeToInsertAfter, [info1, info2, ...]] + while (i < dirtyRanges.length) + { + var range = dirtyRanges[i]; + a = range[0]; + b = range[1]; + var firstDirtyNode = (((a === 0) && root.firstChild) || getCleanNodeByKey(rep.lines.atIndex(a - 1).key).nextSibling); + firstDirtyNode = (firstDirtyNode && isNodeDirty(firstDirtyNode) && firstDirtyNode); + var lastDirtyNode = (((b == rep.lines.length()) && root.lastChild) || getCleanNodeByKey(rep.lines.atIndex(b).key).previousSibling); + lastDirtyNode = (lastDirtyNode && isNodeDirty(lastDirtyNode) && lastDirtyNode); + if (firstDirtyNode && lastDirtyNode) + { + var cc = makeContentCollector(isStyled, browser, rep.apool, null, className2Author); + cc.notifySelection(selection); + var dirtyNodes = []; + for (var n = firstDirtyNode; n && !(n.previousSibling && n.previousSibling == lastDirtyNode); + n = n.nextSibling) + { + if (browser.msie) + { + // try to undo IE's pesky and overzealous linkification + try + { + n.createTextRange().execCommand("unlink", false, null); + } + catch (e) + {} + } + cc.collectContent(n); + dirtyNodes.push(n); + } + cc.notifyNextNode(lastDirtyNode.nextSibling); + var lines = cc.getLines(); + if ((lines.length <= 1 || lines[lines.length - 1] !== "") && lastDirtyNode.nextSibling) + { + // dirty region doesn't currently end a line, even taking the following node + // (or lack of node) into account, so include the following clean node. + // It could be SPAN or a DIV; basically this is any case where the contentCollector + // decides it isn't done. + // Note that this clean node might need to be there for the next dirty range. + //console.log("inclusive of "+lastDirtyNode.next().dom.tagName); + b++; + var cleanLine = lastDirtyNode.nextSibling; + cc.collectContent(cleanLine); + toDeleteAtEnd.push(cleanLine); + cc.notifyNextNode(cleanLine.nextSibling); + } + + var ccData = cc.finish(); + var ss = ccData.selStart; + var se = ccData.selEnd; + lines = ccData.lines; + var lineAttribs = ccData.lineAttribs; + var linesWrapped = ccData.linesWrapped; + + if (linesWrapped > 0) + { + doAlert("Editor warning: " + linesWrapped + " long line" + (linesWrapped == 1 ? " was" : "s were") + " hard-wrapped into " + ccData.numLinesAfter + " lines."); + } + + if (ss[0] >= 0) selStart = [ss[0] + a + netNumLinesChangeSoFar, ss[1]]; + if (se[0] >= 0) selEnd = [se[0] + a + netNumLinesChangeSoFar, se[1]]; + + var entries = []; + var nodeToAddAfter = lastDirtyNode; + var lineNodeInfos = new Array(lines.length); + for (var k = 0; k < lines.length; k++) + { + var lineString = lines[k]; + var newEntry = createDomLineEntry(lineString); + entries.push(newEntry); + lineNodeInfos[k] = newEntry.domInfo; + } + //var fragment = magicdom.wrapDom(document.createDocumentFragment()); + domInsertsNeeded.push([nodeToAddAfter, lineNodeInfos]); + forEach(dirtyNodes, function(n) + { + toDeleteAtEnd.push(n); + }); + var spliceHints = {}; + if (selStart) spliceHints.selStart = selStart; + if (selEnd) spliceHints.selEnd = selEnd; + splicesToDo.push([a + netNumLinesChangeSoFar, b - a, entries, lineAttribs, spliceHints]); + netNumLinesChangeSoFar += (lines.length - (b - a)); + } + else if (b > a) + { + splicesToDo.push([a + netNumLinesChangeSoFar, b - a, [], + [] + ]); + } + i++; + } + + var domChanges = (splicesToDo.length > 0); + + // update the representation + p.mark("splice"); + forEach(splicesToDo, function(splice) + { + doIncorpLineSplice(splice[0], splice[1], splice[2], splice[3], splice[4]); + }); + + //p.mark("relex"); + //rep.lexer.lexCharRange(getVisibleCharRange(), function() { return false; }); + //var isTimeUp = newTimeLimit(100); + // do DOM inserts + p.mark("insert"); + forEach(domInsertsNeeded, function(ins) + { + insertDomLines(ins[0], ins[1], isTimeUp); + }); + + p.mark("del"); + // delete old dom nodes + forEach(toDeleteAtEnd, function(n) + { + //var id = n.uniqueId(); + // parent of n may not be "root" in IE due to non-tree-shaped DOM (wtf) + n.parentNode.removeChild(n); + + //dmesg(htmlPrettyEscape(htmlForRemovedChild(n))); + //console.log("removed: "+id); + }); + + p.mark("findsel"); + // if the nodes that define the selection weren't encountered during + // content collection, figure out where those nodes are now. + if (selection && !selStart) + { + //if (domChanges) dmesg("selection not collected"); + selStart = getLineAndCharForPoint(selection.startPoint); + } + if (selection && !selEnd) + { + selEnd = getLineAndCharForPoint(selection.endPoint); + } + + // selection from content collection can, in various ways, extend past final + // BR in firefox DOM, so cap the line + var numLines = rep.lines.length(); + if (selStart && selStart[0] >= numLines) + { + selStart[0] = numLines - 1; + selStart[1] = rep.lines.atIndex(selStart[0]).text.length; + } + if (selEnd && selEnd[0] >= numLines) + { + selEnd[0] = numLines - 1; + selEnd[1] = rep.lines.atIndex(selEnd[0]).text.length; + } + + p.mark("repsel"); + // update rep if we have a new selection + // NOTE: IE loses the selection when you click stuff in e.g. the + // editbar, so removing the selection when it's lost is not a good + // idea. + if (selection) repSelectionChange(selStart, selEnd, selection && selection.focusAtStart); + // update browser selection + p.mark("browsel"); + if (selection && (domChanges || isCaret())) + { + // if no DOM changes (not this case), want to treat range selection delicately, + // e.g. in IE not lose which end of the selection is the focus/anchor; + // on the other hand, we may have just noticed a press of PageUp/PageDown + currentCallStack.selectionAffected = true; + } + + currentCallStack.domClean = true; + + p.mark("fixview"); + + fixView(); + + p.end("END"); + + return domChanges; + } + + function htmlForRemovedChild(n) + { + var div = doc.createElement("DIV"); + div.appendChild(n); + return div.innerHTML; + } + + var STYLE_ATTRIBS = { + bold: true, + italic: true, + underline: true, + strikethrough: true, + list: true + }; + var OTHER_INCORPED_ATTRIBS = { + insertorder: true, + author: true + }; + + function isStyleAttribute(aname) + { + return !!STYLE_ATTRIBS[aname]; + } + + function isIncorpedAttribute(aname) + { + return ( !! STYLE_ATTRIBS[aname]) || ( !! OTHER_INCORPED_ATTRIBS[aname]); + } + + function insertDomLines(nodeToAddAfter, infoStructs, isTimeUp) + { + isTimeUp = (isTimeUp || + function() + { + return false; + }); + + var lastEntry; + var lineStartOffset; + if (infoStructs.length < 1) return; + var startEntry = rep.lines.atKey(uniqueId(infoStructs[0].node)); + var endEntry = rep.lines.atKey(uniqueId(infoStructs[infoStructs.length - 1].node)); + var charStart = rep.lines.offsetOfEntry(startEntry); + var charEnd = rep.lines.offsetOfEntry(endEntry) + endEntry.width; + + //rep.lexer.lexCharRange([charStart, charEnd], isTimeUp); + forEach(infoStructs, function(info) + { + var p2 = PROFILER("insertLine", false); + var node = info.node; + var key = uniqueId(node); + var entry; + p2.mark("findEntry"); + if (lastEntry) + { + // optimization to avoid recalculation + var next = rep.lines.next(lastEntry); + if (next && next.key == key) + { + entry = next; + lineStartOffset += lastEntry.width; + } + } + if (!entry) + { + p2.literal(1, "nonopt"); + entry = rep.lines.atKey(key); + lineStartOffset = rep.lines.offsetOfKey(key); + } + else p2.literal(0, "nonopt"); + lastEntry = entry; + p2.mark("spans"); + getSpansForLine(entry, function(tokenText, tokenClass) + { + info.appendSpan(tokenText, tokenClass); + }, lineStartOffset, isTimeUp()); + //else if (entry.text.length > 0) { + //info.appendSpan(entry.text, 'dirty'); + //} + p2.mark("addLine"); + info.prepareForAdd(); + entry.lineMarker = info.lineMarker; + if (!nodeToAddAfter) + { + root.insertBefore(node, root.firstChild); + } + else + { + root.insertBefore(node, nodeToAddAfter.nextSibling); + } + nodeToAddAfter = node; + info.notifyAdded(); + p2.mark("markClean"); + markNodeClean(node); + p2.end(); + }); + } + + function isCaret() + { + return (rep.selStart && rep.selEnd && rep.selStart[0] == rep.selEnd[0] && rep.selStart[1] == rep.selEnd[1]); + } + editorInfo.ace_isCaret = isCaret; + + // prereq: isCaret() + + + function caretLine() + { + return rep.selStart[0]; + } + + function caretColumn() + { + return rep.selStart[1]; + } + + function caretDocChar() + { + return rep.lines.offsetOfIndex(caretLine()) + caretColumn(); + } + + function handleReturnIndentation() + { + // on return, indent to level of previous line + if (isCaret() && caretColumn() === 0 && caretLine() > 0) + { + var lineNum = caretLine(); + var thisLine = rep.lines.atIndex(lineNum); + var prevLine = rep.lines.prev(thisLine); + var prevLineText = prevLine.text; + var theIndent = /^ *(?:)/.exec(prevLineText)[0]; + if (/[\[\(\{]\s*$/.exec(prevLineText)) theIndent += THE_TAB; + var cs = Changeset.builder(rep.lines.totalWidth()).keep( + rep.lines.offsetOfIndex(lineNum), lineNum).insert( + theIndent, [ + ['author', thisAuthor] + ], rep.apool).toString(); + performDocumentApplyChangeset(cs); + performSelectionChange([lineNum, theIndent.length], [lineNum, theIndent.length]); + } + } + + + 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) + { + var line = lineAndChar[0]; + var charsLeft = lineAndChar[1]; + //console.log("line: %d, key: %s, node: %o", line, rep.lines.atIndex(line).key, + //getCleanNodeByKey(rep.lines.atIndex(line).key)); + var lineEntry = rep.lines.atIndex(line); + charsLeft -= lineEntry.lineMarker; + if (charsLeft < 0) + { + charsLeft = 0; + } + var lineNode = lineEntry.lineNode; + var n = lineNode; + var after = false; + if (charsLeft === 0) + { + var index = 0; + if (browser.msie && line == (rep.lines.length() - 1) && lineNode.childNodes.length === 0) + { + // best to stay at end of last empty div in IE + index = 1; + } + return { + node: lineNode, + index: index, + maxIndex: 1 + }; + } + while (!(n == lineNode && after)) + { + if (after) + { + if (n.nextSibling) + { + n = n.nextSibling; + after = false; + } + else n = n.parentNode; + } + else + { + if (isNodeText(n)) + { + var len = n.nodeValue.length; + if (charsLeft <= len) + { + return { + node: n, + index: charsLeft, + maxIndex: len + }; + } + charsLeft -= len; + after = true; + } + else + { + if (n.firstChild) n = n.firstChild; + else after = true; + } + } + } + return { + node: lineNode, + index: 1, + maxIndex: 1 + }; + } + + function nodeText(n) + { + return n.innerText || n.textContent || n.nodeValue || ''; + } + + function getLineAndCharForPoint(point) + { + // Turn DOM node selection into [line,char] selection. + // This method has to work when the DOM is not pristine, + // assuming the point is not in a dirty node. + if (point.node == root) + { + if (point.index === 0) + { + return [0, 0]; + } + else + { + var N = rep.lines.length(); + var ln = rep.lines.atIndex(N - 1); + return [N - 1, ln.text.length]; + } + } + else + { + var n = point.node; + var col = 0; + // if this part fails, it probably means the selection node + // was dirty, and we didn't see it when collecting dirty nodes. + if (isNodeText(n)) + { + col = point.index; + } + else if (point.index > 0) + { + col = nodeText(n).length; + } + var parNode, prevSib; + while ((parNode = n.parentNode) != root) + { + if ((prevSib = n.previousSibling)) + { + n = prevSib; + col += nodeText(n).length; + } + else + { + n = parNode; + } + } + if (n.id === "") console.debug("BAD"); + if (n.firstChild && isBlockElement(n.firstChild)) + { + col += 1; // lineMarker + } + var lineEntry = rep.lines.atKey(n.id); + var lineNum = rep.lines.indexOfEntry(lineEntry); + return [lineNum, col]; + } + } + + function createDomLineEntry(lineString) + { + var info = doCreateDomLine(lineString.length > 0); + var newNode = info.node; + return { + key: uniqueId(newNode), + text: lineString, + lineNode: newNode, + domInfo: info, + lineMarker: 0 + }; + } + + function canApplyChangesetToDocument(changes) + { + return Changeset.oldLen(changes) == rep.alltext.length; + } + + function performDocumentApplyChangeset(changes, insertsAfterSelection) + { + doRepApplyChangeset(changes, insertsAfterSelection); + + var requiredSelectionSetting = null; + if (rep.selStart && rep.selEnd) + { + var selStartChar = rep.lines.offsetOfIndex(rep.selStart[0]) + rep.selStart[1]; + var selEndChar = rep.lines.offsetOfIndex(rep.selEnd[0]) + rep.selEnd[1]; + var result = Changeset.characterRangeFollow(changes, selStartChar, selEndChar, insertsAfterSelection); + requiredSelectionSetting = [result[0], result[1], rep.selFocusAtStart]; + } + + var linesMutatee = { + splice: function(start, numRemoved, newLinesVA) + { + domAndRepSplice(start, numRemoved, map(Array.prototype.slice.call(arguments, 2), function(s) + { + return s.slice(0, -1); + }), null); + }, + get: function(i) + { + return rep.lines.atIndex(i).text + '\n'; + }, + length: function() + { + return rep.lines.length(); + }, + slice_notused: function(start, end) + { + return map(rep.lines.slice(start, end), function(e) + { + return e.text + '\n'; + }); + } + }; + + Changeset.mutateTextLines(changes, linesMutatee); + + checkALines(); + + if (requiredSelectionSetting) + { + performSelectionChange(lineAndColumnFromChar(requiredSelectionSetting[0]), lineAndColumnFromChar(requiredSelectionSetting[1]), requiredSelectionSetting[2]); + } + + function domAndRepSplice(startLine, deleteCount, newLineStrings, isTimeUp) + { + // dgreensp 3/2009: the spliced lines may be in the middle of a dirty region, + // so if no explicit time limit, don't spend a lot of time highlighting + isTimeUp = (isTimeUp || newTimeLimit(50)); + + var keysToDelete = []; + if (deleteCount > 0) + { + var entryToDelete = rep.lines.atIndex(startLine); + for (var i = 0; i < deleteCount; i++) + { + keysToDelete.push(entryToDelete.key); + entryToDelete = rep.lines.next(entryToDelete); + } + } + + var lineEntries = map(newLineStrings, createDomLineEntry); + + doRepLineSplice(startLine, deleteCount, lineEntries); + + var nodeToAddAfter; + if (startLine > 0) + { + nodeToAddAfter = getCleanNodeByKey(rep.lines.atIndex(startLine - 1).key); + } + else nodeToAddAfter = null; + + insertDomLines(nodeToAddAfter, map(lineEntries, function(entry) + { + return entry.domInfo; + }), isTimeUp); + + forEach(keysToDelete, function(k) + { + var n = doc.getElementById(k); + n.parentNode.removeChild(n); + }); + + if ((rep.selStart && rep.selStart[0] >= startLine && rep.selStart[0] <= startLine + deleteCount) || (rep.selEnd && rep.selEnd[0] >= startLine && rep.selEnd[0] <= startLine + deleteCount)) + { + currentCallStack.selectionAffected = true; + } + } + } + + function checkChangesetLineInformationAgainstRep(changes) + { + return true; // disable for speed + var opIter = Changeset.opIterator(Changeset.unpack(changes).ops); + var curOffset = 0; + var curLine = 0; + var curCol = 0; + while (opIter.hasNext()) + { + var o = opIter.next(); + if (o.opcode == '-' || o.opcode == '=') + { + curOffset += o.chars; + if (o.lines) + { + curLine += o.lines; + curCol = 0; + } + else + { + curCol += o.chars; + } + } + var calcLine = rep.lines.indexOfOffset(curOffset); + var calcLineStart = rep.lines.offsetOfIndex(calcLine); + var calcCol = curOffset - calcLineStart; + if (calcCol != curCol || calcLine != curLine) + { + return false; + } + } + return true; + } + + function doRepApplyChangeset(changes, insertsAfterSelection) + { + Changeset.checkRep(changes); + + if (Changeset.oldLen(changes) != rep.alltext.length) throw new Error("doRepApplyChangeset length mismatch: " + Changeset.oldLen(changes) + "/" + rep.alltext.length); + + if (!checkChangesetLineInformationAgainstRep(changes)) + { + throw new Error("doRepApplyChangeset line break mismatch"); + } + + (function doRecordUndoInformation(changes) + { + var editEvent = currentCallStack.editEvent; + if (editEvent.eventType == "nonundoable") + { + if (!editEvent.changeset) + { + editEvent.changeset = changes; + } + else + { + editEvent.changeset = Changeset.compose(editEvent.changeset, changes, rep.apool); + } + } + else + { + var inverseChangeset = Changeset.inverse(changes, { + get: function(i) + { + return rep.lines.atIndex(i).text + '\n'; + }, + length: function() + { + return rep.lines.length(); + } + }, rep.alines, rep.apool); + + if (!editEvent.backset) + { + editEvent.backset = inverseChangeset; + } + else + { + editEvent.backset = Changeset.compose(inverseChangeset, editEvent.backset, rep.apool); + } + } + })(changes); + + //rep.alltext = Changeset.applyToText(changes, rep.alltext); + Changeset.mutateAttributionLines(changes, rep.alines, rep.apool); + + if (changesetTracker.isTracking()) + { + changesetTracker.composeUserChangeset(changes); + } + + } + + function lineAndColumnFromChar(x) + { + var lineEntry = rep.lines.atOffset(x); + var lineStart = rep.lines.offsetOfEntry(lineEntry); + var lineNum = rep.lines.indexOfEntry(lineEntry); + return [lineNum, x - lineStart]; + } + + function performDocumentReplaceCharRange(startChar, endChar, newText) + { + if (startChar == endChar && newText.length === 0) + { + return; + } + // Requires that the replacement preserve the property that the + // internal document text ends in a newline. Given this, we + // rewrite the splice so that it doesn't touch the very last + // char of the document. + if (endChar == rep.alltext.length) + { + if (startChar == endChar) + { + // an insert at end + startChar--; + endChar--; + newText = '\n' + newText.substring(0, newText.length - 1); + } + else if (newText.length === 0) + { + // a delete at end + startChar--; + endChar--; + } + else + { + // a replace at end + endChar--; + newText = newText.substring(0, newText.length - 1); + } + } + performDocumentReplaceRange(lineAndColumnFromChar(startChar), lineAndColumnFromChar(endChar), newText); + } + + function performDocumentReplaceRange(start, end, newText) + { + if (start === undefined) start = rep.selStart; + if (end === undefined) end = rep.selEnd; + + //dmesg(String([start.toSource(),end.toSource(),newText.toSource()])); + // start[0]: <--- start[1] --->CCCCCCCCCCC\n + // CCCCCCCCCCCCCCCCCCCC\n + // CCCC\n + // end[0]: <CCC end[1] CCC>-------\n + var builder = Changeset.builder(rep.lines.totalWidth()); + buildKeepToStartOfRange(builder, start); + buildRemoveRange(builder, start, end); + builder.insert(newText, [ + ['author', thisAuthor] + ], rep.apool); + var cs = builder.toString(); + + performDocumentApplyChangeset(cs); + } + + function performDocumentApplyAttributesToCharRange(start, end, attribs) + { + if (end >= rep.alltext.length) + { + end = rep.alltext.length - 1; + } + performDocumentApplyAttributesToRange(lineAndColumnFromChar(start), lineAndColumnFromChar(end), attribs); + } + 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) + { + if (!(rep.selStart && rep.selEnd)) return; + + performDocumentApplyAttributesToRange(rep.selStart, rep.selEnd, [ + [attributeName, attributeValue] + ]); + } + editorInfo.ace_setAttributeOnSelection = setAttributeOnSelection; + + function toggleAttributeOnSelection(attributeName) + { + if (!(rep.selStart && rep.selEnd)) return; + + var selectionAllHasIt = true; + var withIt = Changeset.makeAttribsString('+', [ + [attributeName, 'true'] + ], rep.apool); + var withItRegex = new RegExp(withIt.replace(/\*/g, '\\*') + "(\\*|$)"); + + function hasIt(attribs) + { + return withItRegex.test(attribs); + } + + var selStartLine = rep.selStart[0]; + var selEndLine = rep.selEnd[0]; + for (var n = selStartLine; n <= selEndLine; n++) + { + var opIter = Changeset.opIterator(rep.alines[n]); + var indexIntoLine = 0; + var selectionStartInLine = 0; + var selectionEndInLine = rep.lines.atIndex(n).text.length; // exclude newline + if (n == selStartLine) + { + selectionStartInLine = rep.selStart[1]; + } + if (n == selEndLine) + { + selectionEndInLine = rep.selEnd[1]; + } + while (opIter.hasNext()) + { + var op = opIter.next(); + var opStartInLine = indexIntoLine; + var opEndInLine = opStartInLine + op.chars; + if (!hasIt(op.attribs)) + { + // does op overlap selection? + if (!(opEndInLine <= selectionStartInLine || opStartInLine >= selectionEndInLine)) + { + selectionAllHasIt = false; + break; + } + } + indexIntoLine = opEndInLine; + } + if (!selectionAllHasIt) + { + break; + } + } + + if (selectionAllHasIt) + { + performDocumentApplyAttributesToRange(rep.selStart, rep.selEnd, [ + [attributeName, ''] + ]); + } + else + { + performDocumentApplyAttributesToRange(rep.selStart, rep.selEnd, [ + [attributeName, 'true'] + ]); + } + } + editorInfo.ace_toggleAttributeOnSelection = toggleAttributeOnSelection; + + function performDocumentReplaceSelection(newText) + { + if (!(rep.selStart && rep.selEnd)) return; + performDocumentReplaceRange(rep.selStart, rep.selEnd, newText); + } + + // Change the abstract representation of the document to have a different set of lines. + // Must be called after rep.alltext is set. + + + function doRepLineSplice(startLine, deleteCount, newLineEntries) + { + + forEach(newLineEntries, function(entry) + { + entry.width = entry.text.length + 1; + }); + + var startOldChar = rep.lines.offsetOfIndex(startLine); + var endOldChar = rep.lines.offsetOfIndex(startLine + deleteCount); + + var oldRegionStart = rep.lines.offsetOfIndex(startLine); + var oldRegionEnd = rep.lines.offsetOfIndex(startLine + deleteCount); + rep.lines.splice(startLine, deleteCount, newLineEntries); + currentCallStack.docTextChanged = true; + currentCallStack.repChanged = true; + var newRegionEnd = rep.lines.offsetOfIndex(startLine + newLineEntries.length); + + var newText = map(newLineEntries, function(e) + { + return e.text + '\n'; + }).join(''); + + rep.alltext = rep.alltext.substring(0, startOldChar) + newText + rep.alltext.substring(endOldChar, rep.alltext.length); + + //var newTotalLength = rep.alltext.length; + //rep.lexer.updateBuffer(rep.alltext, oldRegionStart, oldRegionEnd - oldRegionStart, + //newRegionEnd - oldRegionStart); + } + + function doIncorpLineSplice(startLine, deleteCount, newLineEntries, lineAttribs, hints) + { + + var startOldChar = rep.lines.offsetOfIndex(startLine); + var endOldChar = rep.lines.offsetOfIndex(startLine + deleteCount); + + var oldRegionStart = rep.lines.offsetOfIndex(startLine); + + var selStartHintChar, selEndHintChar; + if (hints && hints.selStart) + { + selStartHintChar = rep.lines.offsetOfIndex(hints.selStart[0]) + hints.selStart[1] - oldRegionStart; + } + if (hints && hints.selEnd) + { + selEndHintChar = rep.lines.offsetOfIndex(hints.selEnd[0]) + hints.selEnd[1] - oldRegionStart; + } + + var newText = map(newLineEntries, function(e) + { + return e.text + '\n'; + }).join(''); + var oldText = rep.alltext.substring(startOldChar, endOldChar); + var oldAttribs = rep.alines.slice(startLine, startLine + deleteCount).join(''); + var newAttribs = lineAttribs.join('|1+1') + '|1+1'; // not valid in a changeset + var analysis = analyzeChange(oldText, newText, oldAttribs, newAttribs, selStartHintChar, selEndHintChar); + var commonStart = analysis[0]; + var commonEnd = analysis[1]; + var shortOldText = oldText.substring(commonStart, oldText.length - commonEnd); + var shortNewText = newText.substring(commonStart, newText.length - commonEnd); + var spliceStart = startOldChar + commonStart; + var spliceEnd = endOldChar - commonEnd; + var shiftFinalNewlineToBeforeNewText = false; + + // adjust the splice to not involve the final newline of the document; + // be very defensive + if (shortOldText.charAt(shortOldText.length - 1) == '\n' && shortNewText.charAt(shortNewText.length - 1) == '\n') + { + // replacing text that ends in newline with text that also ends in newline + // (still, after analysis, somehow) + shortOldText = shortOldText.slice(0, -1); + shortNewText = shortNewText.slice(0, -1); + spliceEnd--; + commonEnd++; + } + if (shortOldText.length === 0 && spliceStart == rep.alltext.length && shortNewText.length > 0) + { + // inserting after final newline, bad + spliceStart--; + spliceEnd--; + shortNewText = '\n' + shortNewText.slice(0, -1); + shiftFinalNewlineToBeforeNewText = true; + } + if (spliceEnd == rep.alltext.length && shortOldText.length > 0 && shortNewText.length === 0) + { + // deletion at end of rep.alltext + if (rep.alltext.charAt(spliceStart - 1) == '\n') + { + // (if not then what the heck? it will definitely lead + // to a rep.alltext without a final newline) + spliceStart--; + spliceEnd--; + } + } + + if (!(shortOldText.length === 0 && shortNewText.length === 0)) + { + var oldDocText = rep.alltext; + var oldLen = oldDocText.length; + + var spliceStartLine = rep.lines.indexOfOffset(spliceStart); + var spliceStartLineStart = rep.lines.offsetOfIndex(spliceStartLine); + + var startBuilder = function() + { + var builder = Changeset.builder(oldLen); + builder.keep(spliceStartLineStart, spliceStartLine); + builder.keep(spliceStart - spliceStartLineStart); + return builder; + }; + + var eachAttribRun = function(attribs, func /*(startInNewText, endInNewText, attribs)*/ ) + { + var attribsIter = Changeset.opIterator(attribs); + var textIndex = 0; + var newTextStart = commonStart; + var newTextEnd = newText.length - commonEnd - (shiftFinalNewlineToBeforeNewText ? 1 : 0); + while (attribsIter.hasNext()) + { + var op = attribsIter.next(); + var nextIndex = textIndex + op.chars; + if (!(nextIndex <= newTextStart || textIndex >= newTextEnd)) + { + func(Math.max(newTextStart, textIndex), Math.min(newTextEnd, nextIndex), op.attribs); + } + textIndex = nextIndex; + } + }; + + var justApplyStyles = (shortNewText == shortOldText); + var theChangeset; + + if (justApplyStyles) + { + // create changeset that clears the incorporated styles on + // the existing text. we compose this with the + // changeset the applies the styles found in the DOM. + // This allows us to incorporate, e.g., Safari's native "unbold". + var incorpedAttribClearer = cachedStrFunc(function(oldAtts) + { + return Changeset.mapAttribNumbers(oldAtts, function(n) + { + var k = rep.apool.getAttribKey(n); + if (isStyleAttribute(k)) + { + return rep.apool.putAttrib([k, '']); + } + return false; + }); + }); + + var builder1 = startBuilder(); + if (shiftFinalNewlineToBeforeNewText) + { + builder1.keep(1, 1); + } + eachAttribRun(oldAttribs, function(start, end, attribs) + { + builder1.keepText(newText.substring(start, end), incorpedAttribClearer(attribs)); + }); + var clearer = builder1.toString(); + + var builder2 = startBuilder(); + if (shiftFinalNewlineToBeforeNewText) + { + builder2.keep(1, 1); + } + eachAttribRun(newAttribs, function(start, end, attribs) + { + builder2.keepText(newText.substring(start, end), attribs); + }); + var styler = builder2.toString(); + + theChangeset = Changeset.compose(clearer, styler, rep.apool); + } + else + { + var builder = startBuilder(); + + var spliceEndLine = rep.lines.indexOfOffset(spliceEnd); + var spliceEndLineStart = rep.lines.offsetOfIndex(spliceEndLine); + if (spliceEndLineStart > spliceStart) + { + builder.remove(spliceEndLineStart - spliceStart, spliceEndLine - spliceStartLine); + builder.remove(spliceEnd - spliceEndLineStart); + } + else + { + builder.remove(spliceEnd - spliceStart); + } + + var isNewTextMultiauthor = false; + var authorAtt = Changeset.makeAttribsString('+', (thisAuthor ? [ + ['author', thisAuthor] + ] : []), rep.apool); + var authorizer = cachedStrFunc(function(oldAtts) + { + if (isNewTextMultiauthor) + { + // prefer colors from DOM + return Changeset.composeAttributes(authorAtt, oldAtts, true, rep.apool); + } + else + { + // use this author's color + return Changeset.composeAttributes(oldAtts, authorAtt, true, rep.apool); + } + }); + + var foundDomAuthor = ''; + eachAttribRun(newAttribs, function(start, end, attribs) + { + var a = Changeset.attribsAttributeValue(attribs, 'author', rep.apool); + if (a && a != foundDomAuthor) + { + if (!foundDomAuthor) + { + foundDomAuthor = a; + } + else + { + isNewTextMultiauthor = true; // multiple authors in DOM! + } + } + }); + + if (shiftFinalNewlineToBeforeNewText) + { + builder.insert('\n', authorizer('')); + } + + eachAttribRun(newAttribs, function(start, end, attribs) + { + builder.insert(newText.substring(start, end), authorizer(attribs)); + }); + theChangeset = builder.toString(); + } + + //dmesg(htmlPrettyEscape(theChangeset)); + doRepApplyChangeset(theChangeset); + } + + // do this no matter what, because we need to get the right + // line keys into the rep. + doRepLineSplice(startLine, deleteCount, newLineEntries); + + checkALines(); + } + + function cachedStrFunc(func) + { + var cache = {}; + return function(s) + { + if (!cache[s]) + { + cache[s] = func(s); + } + return cache[s]; + }; + } + + function analyzeChange(oldText, newText, oldAttribs, newAttribs, optSelStartHint, optSelEndHint) + { + function incorpedAttribFilter(anum) + { + return isStyleAttribute(rep.apool.getAttribKey(anum)); + } + + function attribRuns(attribs) + { + var lengs = []; + var atts = []; + var iter = Changeset.opIterator(attribs); + while (iter.hasNext()) + { + var op = iter.next(); + lengs.push(op.chars); + atts.push(op.attribs); + } + return [lengs, atts]; + } + + function attribIterator(runs, backward) + { + var lengs = runs[0]; + var atts = runs[1]; + var i = (backward ? lengs.length - 1 : 0); + var j = 0; + return function next() + { + while (j >= lengs[i]) + { + if (backward) i--; + else i++; + j = 0; + } + var a = atts[i]; + j++; + return a; + }; + } + + var oldLen = oldText.length; + var newLen = newText.length; + var minLen = Math.min(oldLen, newLen); + + var oldARuns = attribRuns(Changeset.filterAttribNumbers(oldAttribs, incorpedAttribFilter)); + var newARuns = attribRuns(Changeset.filterAttribNumbers(newAttribs, incorpedAttribFilter)); + + var commonStart = 0; + var oldStartIter = attribIterator(oldARuns, false); + var newStartIter = attribIterator(newARuns, false); + while (commonStart < minLen) + { + if (oldText.charAt(commonStart) == newText.charAt(commonStart) && oldStartIter() == newStartIter()) + { + commonStart++; + } + else break; + } + + var commonEnd = 0; + var oldEndIter = attribIterator(oldARuns, true); + var newEndIter = attribIterator(newARuns, true); + while (commonEnd < minLen) + { + if (commonEnd === 0) + { + // assume newline in common + oldEndIter(); + newEndIter(); + commonEnd++; + } + else if (oldText.charAt(oldLen - 1 - commonEnd) == newText.charAt(newLen - 1 - commonEnd) && oldEndIter() == newEndIter()) + { + commonEnd++; + } + else break; + } + + var hintedCommonEnd = -1; + if ((typeof optSelEndHint) == "number") + { + hintedCommonEnd = newLen - optSelEndHint; + } + + + if (commonStart + commonEnd > oldLen) + { + // ambiguous insertion + var minCommonEnd = oldLen - commonStart; + var maxCommonEnd = commonEnd; + if (hintedCommonEnd >= minCommonEnd && hintedCommonEnd <= maxCommonEnd) + { + commonEnd = hintedCommonEnd; + } + else + { + commonEnd = minCommonEnd; + } + commonStart = oldLen - commonEnd; + } + if (commonStart + commonEnd > newLen) + { + // ambiguous deletion + var minCommonEnd = newLen - commonStart; + var maxCommonEnd = commonEnd; + if (hintedCommonEnd >= minCommonEnd && hintedCommonEnd <= maxCommonEnd) + { + commonEnd = hintedCommonEnd; + } + else + { + commonEnd = minCommonEnd; + } + commonStart = newLen - commonEnd; + } + + return [commonStart, commonEnd]; + } + + function equalLineAndChars(a, b) + { + if (!a) return !b; + if (!b) return !a; + return (a[0] == b[0] && a[1] == b[1]); + } + + function performSelectionChange(selectStart, selectEnd, focusAtStart) + { + if (repSelectionChange(selectStart, selectEnd, focusAtStart)) + { + currentCallStack.selectionAffected = true; + } + } + + // Change the abstract representation of the document to have a different selection. + // Should not rely on the line representation. Should not affect the DOM. + + + function repSelectionChange(selectStart, selectEnd, focusAtStart) + { + focusAtStart = !! focusAtStart; + + var newSelFocusAtStart = (focusAtStart && ((!selectStart) || (!selectEnd) || (selectStart[0] != selectEnd[0]) || (selectStart[1] != selectEnd[1]))); + + if ((!equalLineAndChars(rep.selStart, selectStart)) || (!equalLineAndChars(rep.selEnd, selectEnd)) || (rep.selFocusAtStart != newSelFocusAtStart)) + { + rep.selStart = selectStart; + rep.selEnd = selectEnd; + rep.selFocusAtStart = newSelFocusAtStart; + if (mozillaFakeArrows) mozillaFakeArrows.notifySelectionChanged(); + currentCallStack.repChanged = true; + + return true; + //console.log("selStart: %o, selEnd: %o, focusAtStart: %s", rep.selStart, rep.selEnd, + //String(!!rep.selFocusAtStart)); + } + return false; + //console.log("%o %o %s", rep.selStart, rep.selEnd, rep.selFocusAtStart); + } + + function doCreateDomLine(nonEmpty) + { + if (browser.msie && (!nonEmpty)) + { + var result = { + node: null, + appendSpan: noop, + prepareForAdd: noop, + notifyAdded: noop, + clearSpans: noop, + finishUpdate: noop, + lineMarker: 0 + }; + + var lineElem = doc.createElement("div"); + result.node = lineElem; + + result.notifyAdded = function() + { + // magic -- settng an empty div's innerHTML to the empty string + // keeps it from collapsing. Apparently innerHTML must be set *after* + // adding the node to the DOM. + // Such a div is what IE 6 creates naturally when you make a blank line + // in a document of divs. However, when copy-and-pasted the div will + // contain a space, so we note its emptiness with a property. + lineElem.innerHTML = " "; // Frist we set a value that isnt blank + // a primitive-valued property survives copy-and-paste + setAssoc(lineElem, "shouldBeEmpty", true); + // an object property doesn't + setAssoc(lineElem, "unpasted", {}); + lineElem.innerHTML = ""; // Then we make it blank.. New line and no space = Awesome :) + }; + var lineClass = 'ace-line'; + result.appendSpan = function(txt, cls) + { + if ((!txt) && cls) + { + // gain a whole-line style (currently to show insertion point in CSS) + lineClass = domline.addToLineClass(lineClass, cls); + } + // otherwise, ignore appendSpan, this is an empty line + }; + result.clearSpans = function() + { + lineClass = ''; // non-null to cause update + }; + + var writeClass = function() + { + if (lineClass !== null) lineElem.className = lineClass; + }; + + result.prepareForAdd = writeClass; + result.finishUpdate = writeClass; + result.getInnerHTML = function() + { + return ""; + }; + + return result; + } + else + { + return domline.createDomLine(nonEmpty, doesWrap, browser, doc); + } + } + + function textify(str) + { + return str.replace(/[\n\r ]/g, ' ').replace(/\xa0/g, ' ').replace(/\t/g, ' '); + } + + var _blockElems = { + "div": 1, + "p": 1, + "pre": 1, + "li": 1, + "ol": 1, + "ul": 1 + }; + + function isBlockElement(n) + { + return !!_blockElems[(n.tagName || "").toLowerCase()]; + } + + function getDirtyRanges() + { + // based on observedChanges, return a list of ranges of original lines + // that need to be removed or replaced with new user content to incorporate + // the user's changes into the line representation. ranges may be zero-length, + // indicating inserted content. for example, [0,0] means content was inserted + // at the top of the document, while [3,4] means line 3 was deleted, modified, + // or replaced with one or more new lines of content. ranges do not touch. + var p = PROFILER("getDirtyRanges", false); + p.forIndices = 0; + p.consecutives = 0; + p.corrections = 0; + + var cleanNodeForIndexCache = {}; + var N = rep.lines.length(); // old number of lines + + + function cleanNodeForIndex(i) + { + // if line (i) in the un-updated line representation maps to a clean node + // in the document, return that node. + // if (i) is out of bounds, return true. else return false. + if (cleanNodeForIndexCache[i] === undefined) + { + p.forIndices++; + var result; + if (i < 0 || i >= N) + { + result = true; // truthy, but no actual node + } + else + { + var key = rep.lines.atIndex(i).key; + result = (getCleanNodeByKey(key) || false); + } + cleanNodeForIndexCache[i] = result; + } + return cleanNodeForIndexCache[i]; + } + var isConsecutiveCache = {}; + + function isConsecutive(i) + { + if (isConsecutiveCache[i] === undefined) + { + p.consecutives++; + isConsecutiveCache[i] = (function() + { + // returns whether line (i) and line (i-1), assumed to be map to clean DOM nodes, + // or document boundaries, are consecutive in the changed DOM + var a = cleanNodeForIndex(i - 1); + var b = cleanNodeForIndex(i); + if ((!a) || (!b)) return false; // violates precondition + if ((a === true) && (b === true)) return !root.firstChild; + if ((a === true) && b.previousSibling) return false; + if ((b === true) && a.nextSibling) return false; + if ((a === true) || (b === true)) return true; + return a.nextSibling == b; + })(); + } + return isConsecutiveCache[i]; + } + + function isClean(i) + { + // returns whether line (i) in the un-updated representation maps to a clean node, + // or is outside the bounds of the document + return !!cleanNodeForIndex(i); + } + // list of pairs, each representing a range of lines that is clean and consecutive + // in the changed DOM. lines (-1) and (N) are always clean, but may or may not + // be consecutive with lines in the document. pairs are in sorted order. + var cleanRanges = [ + [-1, N + 1] + ]; + + function rangeForLine(i) + { + // returns index of cleanRange containing i, or -1 if none + var answer = -1; + forEach(cleanRanges, function(r, idx) + { + if (i >= r[1]) return false; // keep looking + if (i < r[0]) return true; // not found, stop looking + answer = idx; + return true; // found, stop looking + }); + return answer; + } + + function removeLineFromRange(rng, line) + { + // rng is index into cleanRanges, line is line number + // precond: line is in rng + var a = cleanRanges[rng][0]; + var b = cleanRanges[rng][1]; + if ((a + 1) == b) cleanRanges.splice(rng, 1); + else if (line == a) cleanRanges[rng][0]++; + else if (line == (b - 1)) cleanRanges[rng][1]--; + else cleanRanges.splice(rng, 1, [a, line], [line + 1, b]); + } + + function splitRange(rng, pt) + { + // precond: pt splits cleanRanges[rng] into two non-empty ranges + var a = cleanRanges[rng][0]; + var b = cleanRanges[rng][1]; + cleanRanges.splice(rng, 1, [a, pt], [pt, b]); + } + var correctedLines = {}; + + function correctlyAssignLine(line) + { + if (correctedLines[line]) return true; + p.corrections++; + correctedLines[line] = true; + // "line" is an index of a line in the un-updated rep. + // returns whether line was already correctly assigned (i.e. correctly + // clean or dirty, according to cleanRanges, and if clean, correctly + // attached or not attached (i.e. in the same range as) the prev and next lines). + //console.log("correctly assigning: %d", line); + var rng = rangeForLine(line); + var lineClean = isClean(line); + if (rng < 0) + { + if (lineClean) + { + console.debug("somehow lost clean line"); + } + return true; + } + if (!lineClean) + { + // a clean-range includes this dirty line, fix it + removeLineFromRange(rng, line); + return false; + } + else + { + // line is clean, but could be wrongly connected to a clean line + // above or below + var a = cleanRanges[rng][0]; + var b = cleanRanges[rng][1]; + var didSomething = false; + // we'll leave non-clean adjacent nodes in the clean range for the caller to + // detect and deal with. we deal with whether the range should be split + // just above or just below this line. + if (a < line && isClean(line - 1) && !isConsecutive(line)) + { + splitRange(rng, line); + didSomething = true; + } + if (b > (line + 1) && isClean(line + 1) && !isConsecutive(line + 1)) + { + splitRange(rng, line + 1); + didSomething = true; + } + return !didSomething; + } + } + + function detectChangesAroundLine(line, reqInARow) + { + // make sure cleanRanges is correct about line number "line" and the surrounding + // lines; only stops checking at end of document or after no changes need + // making for several consecutive lines. note that iteration is over old lines, + // so this operation takes time proportional to the number of old lines + // that are changed or missing, not the number of new lines inserted. + var correctInARow = 0; + var currentIndex = line; + while (correctInARow < reqInARow && currentIndex >= 0) + { + if (correctlyAssignLine(currentIndex)) + { + correctInARow++; + } + else correctInARow = 0; + currentIndex--; + } + correctInARow = 0; + currentIndex = line; + while (correctInARow < reqInARow && currentIndex < N) + { + if (correctlyAssignLine(currentIndex)) + { + correctInARow++; + } + else correctInARow = 0; + currentIndex++; + } + } + + if (N === 0) + { + p.cancel(); + if (!isConsecutive(0)) + { + splitRange(0, 0); + } + } + else + { + p.mark("topbot"); + detectChangesAroundLine(0, 1); + detectChangesAroundLine(N - 1, 1); + + p.mark("obs"); + //console.log("observedChanges: "+toSource(observedChanges)); + for (var k in observedChanges.cleanNodesNearChanges) + { + var key = k.substring(1); + if (rep.lines.containsKey(key)) + { + var line = rep.lines.indexOfKey(key); + detectChangesAroundLine(line, 2); + } + } + p.mark("stats&calc"); + p.literal(p.forIndices, "byidx"); + p.literal(p.consecutives, "cons"); + p.literal(p.corrections, "corr"); + } + + var dirtyRanges = []; + for (var r = 0; r < cleanRanges.length - 1; r++) + { + dirtyRanges.push([cleanRanges[r][1], cleanRanges[r + 1][0]]); + } + + p.end(); + + return dirtyRanges; + } + + function markNodeClean(n) + { + // clean nodes have knownHTML that matches their innerHTML + var dirtiness = {}; + dirtiness.nodeId = uniqueId(n); + dirtiness.knownHTML = n.innerHTML; + if (browser.msie) + { + // adding a space to an "empty" div in IE designMode doesn't + // change the innerHTML of the div's parent; also, other + // browsers don't support innerText + dirtiness.knownText = n.innerText; + } + setAssoc(n, "dirtiness", dirtiness); + } + + function isNodeDirty(n) + { + var p = PROFILER("cleanCheck", false); + if (n.parentNode != root) return true; + var data = getAssoc(n, "dirtiness"); + if (!data) return true; + if (n.id !== data.nodeId) return true; + if (browser.msie) + { + if (n.innerText !== data.knownText) return true; + } + if (n.innerHTML !== data.knownHTML) return true; + p.end(); + return false; + } + + function getLineEntryTopBottom(entry, destObj) + { + var dom = entry.lineNode; + var top = dom.offsetTop; + var height = dom.offsetHeight; + var obj = (destObj || {}); + obj.top = top; + obj.bottom = (top + height); + return obj; + } + + function getViewPortTopBottom() + { + var theTop = getScrollY(); + var doc = outerWin.document; + var height = doc.documentElement.clientHeight; + return { + top: theTop, + bottom: (theTop + height) + }; + } + + function getVisibleLineRange() + { + var viewport = getViewPortTopBottom(); + //console.log("viewport top/bottom: %o", viewport); + var obj = {}; + var start = rep.lines.search(function(e) + { + return getLineEntryTopBottom(e, obj).bottom > viewport.top; + }); + var end = rep.lines.search(function(e) + { + return getLineEntryTopBottom(e, obj).top >= viewport.bottom; + }); + if (end < start) end = start; // unlikely + //console.log(start+","+end); + return [start, end]; + } + + function getVisibleCharRange() + { + var lineRange = getVisibleLineRange(); + return [rep.lines.offsetOfIndex(lineRange[0]), rep.lines.offsetOfIndex(lineRange[1])]; + } + + function handleClick(evt) + { + inCallStack("handleClick", function() + { + idleWorkTimer.atMost(200); + }); + + function isLink(n) + { + return (n.tagName || '').toLowerCase() == "a" && n.href; + } + + // only want to catch left-click + if ((!evt.ctrlKey) && (evt.button != 2) && (evt.button != 3)) + { + // find A tag with HREF + var n = evt.target; + while (n && n.parentNode && !isLink(n)) + { + n = n.parentNode; + } + if (n && isLink(n)) + { + try + { + var newWindow = window.open(n.href, '_blank'); + newWindow.focus(); + } + catch (e) + { + // absorb "user canceled" error in IE for certain prompts + } + evt.preventDefault(); + } + } + //hide the dropdownso + if(window.parent.parent.padeditbar){ // required in case its in an iframe should probably use parent.. See Issue 327 https://github.com/Pita/etherpad-lite/issues/327 + window.parent.parent.padeditbar.toogleDropDown("none"); + } + } + + function doReturnKey() + { + if (!(rep.selStart && rep.selEnd)) + { + return; + } + var lineNum = rep.selStart[0]; + var listType = getLineListType(lineNum); + + if (listType) + { + var text = rep.lines.atIndex(lineNum).text; + listType = /([a-z]+)([12345678])/.exec(listType); + var type = listType[1]; + var level = Number(listType[2]); + + //detect empty list item; exclude indentation + if(text === '*' && type !== "indent") + { + //if not already on the highest level + if(level > 1) + { + setLineListType(lineNum, type+(level-1));//automatically decrease the level + } + else + { + setLineListType(lineNum, '');//remove the list + renumberList(lineNum + 1);//trigger renumbering of list that may be right after + } + } + else if (lineNum + 1 < rep.lines.length()) + { + performDocumentReplaceSelection('\n'); + setLineListType(lineNum + 1, type+level); + } + } + else + { + performDocumentReplaceSelection('\n'); + handleReturnIndentation(); + } + } + + function doIndentOutdent(isOut) + { + if (!(rep.selStart && rep.selEnd) || + ((rep.selStart[0] == rep.selEnd[0]) && (rep.selStart[1] == rep.selEnd[1]) && rep.selEnd[1] > 1)) + { + return false; + } + + var firstLine, lastLine; + firstLine = rep.selStart[0]; + lastLine = Math.max(firstLine, rep.selEnd[0] - ((rep.selEnd[1] === 0) ? 1 : 0)); + + var mods = []; + for (var n = firstLine; n <= lastLine; n++) + { + var listType = getLineListType(n); + var t = 'indent'; + var level = 0; + if (listType) + { + listType = /([a-z]+)([12345678])/.exec(listType); + if (listType) + { + t = listType[1]; + level = Number(listType[2]); + } + } + var newLevel = Math.max(0, Math.min(MAX_LIST_LEVEL, level + (isOut ? -1 : 1))); + if (level != newLevel) + { + mods.push([n, (newLevel > 0) ? t + newLevel : '']); + } + } + + if (mods.length > 0) + { + setLineListTypes(mods); + } + + return true; + } + editorInfo.ace_doIndentOutdent = doIndentOutdent; + + function doTabKey(shiftDown) + { + if (!doIndentOutdent(shiftDown)) + { + performDocumentReplaceSelection(THE_TAB); + } + } + + function doDeleteKey(optEvt) + { + var evt = optEvt || {}; + var handled = false; + if (rep.selStart) + { + if (isCaret()) + { + var lineNum = caretLine(); + var col = caretColumn(); + var lineEntry = rep.lines.atIndex(lineNum); + var lineText = lineEntry.text; + var lineMarker = lineEntry.lineMarker; + if (/^ +$/.exec(lineText.substring(lineMarker, col))) + { + var col2 = col - lineMarker; + var tabSize = THE_TAB.length; + var toDelete = ((col2 - 1) % tabSize) + 1; + performDocumentReplaceRange([lineNum, col - toDelete], [lineNum, col], ''); + //scrollSelectionIntoView(); + handled = true; + } + } + if (!handled) + { + if (isCaret()) + { + var theLine = caretLine(); + var lineEntry = rep.lines.atIndex(theLine); + if (caretColumn() <= lineEntry.lineMarker) + { + // delete at beginning of line + var action = 'delete_newline'; + var prevLineListType = (theLine > 0 ? getLineListType(theLine - 1) : ''); + var thisLineListType = getLineListType(theLine); + var prevLineEntry = (theLine > 0 && rep.lines.atIndex(theLine - 1)); + var prevLineBlank = (prevLineEntry && prevLineEntry.text.length == prevLineEntry.lineMarker); + if (thisLineListType) + { + // this line is a list + if (prevLineBlank && !prevLineListType) + { + // previous line is blank, remove it + performDocumentReplaceRange([theLine - 1, prevLineEntry.text.length], [theLine, 0], ''); + } + else + { + // delistify + performDocumentReplaceRange([theLine, 0], [theLine, lineEntry.lineMarker], ''); + } + } + else if (theLine > 0) + { + // remove newline + performDocumentReplaceRange([theLine - 1, prevLineEntry.text.length], [theLine, 0], ''); + } + } + else + { + var docChar = caretDocChar(); + if (docChar > 0) + { + if (evt.metaKey || evt.ctrlKey || evt.altKey) + { + // delete as many unicode "letters or digits" in a row as possible; + // always delete one char, delete further even if that first char + // isn't actually a word char. + var deleteBackTo = docChar - 1; + while (deleteBackTo > lineEntry.lineMarker && isWordChar(rep.alltext.charAt(deleteBackTo - 1))) + { + deleteBackTo--; + } + performDocumentReplaceCharRange(deleteBackTo, docChar, ''); + } + else + { + // normal delete + performDocumentReplaceCharRange(docChar - 1, docChar, ''); + } + } + } + } + else + { + performDocumentReplaceSelection(''); + } + } + } + //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. + var line = caretLine(); + if(line != -1 && renumberList(line+1) === null) + { + renumberList(line); + } + } + + // set of "letter or digit" chars is based on section 20.5.16 of the original Java Language Spec + var REGEX_WORDCHAR = /[\u0030-\u0039\u0041-\u005A\u0061-\u007A\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u00FF\u0100-\u1FFF\u3040-\u9FFF\uF900-\uFDFF\uFE70-\uFEFE\uFF10-\uFF19\uFF21-\uFF3A\uFF41-\uFF5A\uFF66-\uFFDC]/; + var REGEX_SPACE = /\s/; + + function isWordChar(c) + { + return !!REGEX_WORDCHAR.exec(c); + } + + function isSpaceChar(c) + { + return !!REGEX_SPACE.exec(c); + } + + function moveByWordInLine(lineText, initialIndex, forwardNotBack) + { + var i = initialIndex; + + function nextChar() + { + if (forwardNotBack) return lineText.charAt(i); + else return lineText.charAt(i - 1); + } + + function advance() + { + if (forwardNotBack) i++; + else i--; + } + + function isDone() + { + if (forwardNotBack) return i >= lineText.length; + else return i <= 0; + } + + // On Mac and Linux, move right moves to end of word and move left moves to start; + // on Windows, always move to start of word. + // On Windows, Firefox and IE disagree on whether to stop for punctuation (FF says no). + if (browser.windows && forwardNotBack) + { + while ((!isDone()) && isWordChar(nextChar())) + { + advance(); + } + while ((!isDone()) && !isWordChar(nextChar())) + { + advance(); + } + } + else + { + while ((!isDone()) && !isWordChar(nextChar())) + { + advance(); + } + while ((!isDone()) && isWordChar(nextChar())) + { + advance(); + } + } + + return i; + } + + function handleKeyEvent(evt) + { + // if (DEBUG && window.DONT_INCORP) return; + if (!isEditable) return; + + var type = evt.type; + var charCode = evt.charCode; + var keyCode = evt.keyCode; + var which = evt.which; + + //dmesg("keyevent type: "+type+", which: "+which); + // Don't take action based on modifier keys going up and down. + // Modifier keys do not generate "keypress" events. + // 224 is the command-key under Mac Firefox. + // 91 is the Windows key in IE; it is ASCII for open-bracket but isn't the keycode for that key + // 20 is capslock in IE. + var isModKey = ((!charCode) && ((type == "keyup") || (type == "keydown")) && (keyCode == 16 || keyCode == 17 || keyCode == 18 || keyCode == 20 || keyCode == 224 || keyCode == 91)); + if (isModKey) return; + + var specialHandled = false; + var isTypeForSpecialKey = ((browser.msie || browser.safari) ? (type == "keydown") : (type == "keypress")); + var isTypeForCmdKey = ((browser.msie || browser.safari) ? (type == "keydown") : (type == "keypress")); + + var stopped = false; + + inCallStack("handleKeyEvent", function() + { + + if (type == "keypress" || (isTypeForSpecialKey && keyCode == 13 /*return*/ )) + { + // in IE, special keys don't send keypress, the keydown does the action + if (!outsideKeyPress(evt)) + { + evt.preventDefault(); + stopped = true; + } + } + else if (type == "keydown") + { + outsideKeyDown(evt); + } + + if (!stopped) + { + if (isTypeForSpecialKey && keyCode == 8) + { + // "delete" key; in mozilla, if we're at the beginning of a line, normalize now, + // or else deleting a blank line can take two delete presses. + // -- + // we do deletes completely customly now: + // - allows consistent (and better) meta-delete behavior + // - normalizing and then allowing default behavior confused IE + // - probably eliminates a few minor quirks + fastIncorp(3); + evt.preventDefault(); + doDeleteKey(evt); + specialHandled = true; + } + if ((!specialHandled) && isTypeForSpecialKey && keyCode == 13) + { + // return key, handle specially; + // note that in mozilla we need to do an incorporation for proper return behavior anyway. + fastIncorp(4); + evt.preventDefault(); + doReturnKey(); + //scrollSelectionIntoView(); + scheduler.setTimeout(function() + { + outerWin.scrollBy(-100, 0); + }, 0); + specialHandled = true; + } + if ((!specialHandled) && isTypeForSpecialKey && keyCode == 9 && !(evt.metaKey || evt.ctrlKey)) + { + // tab + fastIncorp(5); + evt.preventDefault(); + doTabKey(evt.shiftKey); + //scrollSelectionIntoView(); + specialHandled = true; + } + if ((!specialHandled) && isTypeForCmdKey && String.fromCharCode(which).toLowerCase() == "z" && (evt.metaKey || evt.ctrlKey) && !evt.altKey) + { + // cmd-Z (undo) + fastIncorp(6); + evt.preventDefault(); + if (evt.shiftKey) + { + doUndoRedo("redo"); + } + else + { + doUndoRedo("undo"); + } + specialHandled = true; + } + if ((!specialHandled) && isTypeForCmdKey && String.fromCharCode(which).toLowerCase() == "y" && (evt.metaKey || evt.ctrlKey)) + { + // cmd-Y (redo) + fastIncorp(10); + evt.preventDefault(); + doUndoRedo("redo"); + specialHandled = true; + } + if ((!specialHandled) && isTypeForCmdKey && String.fromCharCode(which).toLowerCase() == "b" && (evt.metaKey || evt.ctrlKey)) + { + // cmd-B (bold) + fastIncorp(13); + evt.preventDefault(); + toggleAttributeOnSelection('bold'); + specialHandled = true; + } + if ((!specialHandled) && isTypeForCmdKey && String.fromCharCode(which).toLowerCase() == "i" && (evt.metaKey || evt.ctrlKey)) + { + // cmd-I (italic) + fastIncorp(14); + evt.preventDefault(); + toggleAttributeOnSelection('italic'); + specialHandled = true; + } + if ((!specialHandled) && isTypeForCmdKey && String.fromCharCode(which).toLowerCase() == "u" && (evt.metaKey || evt.ctrlKey)) + { + // cmd-U (underline) + fastIncorp(15); + evt.preventDefault(); + toggleAttributeOnSelection('underline'); + specialHandled = true; + } + if ((!specialHandled) && isTypeForCmdKey && String.fromCharCode(which).toLowerCase() == "h" && (evt.ctrlKey)) + { + // cmd-H (backspace) + fastIncorp(20); + evt.preventDefault(); + doDeleteKey(); + specialHandled = true; + } + + if (mozillaFakeArrows && mozillaFakeArrows.handleKeyEvent(evt)) + { + evt.preventDefault(); + specialHandled = true; + } + } + + if (type == "keydown") + { + idleWorkTimer.atLeast(500); + } + else if (type == "keypress") + { + if ((!specialHandled) && false /*parenModule.shouldNormalizeOnChar(charCode)*/) + { + idleWorkTimer.atMost(0); + } + else + { + idleWorkTimer.atLeast(500); + } + } + else if (type == "keyup") + { + var wait = 200; + idleWorkTimer.atLeast(wait); + idleWorkTimer.atMost(wait); + } + + // Is part of multi-keystroke international character on Firefox Mac + var isFirefoxHalfCharacter = (browser.mozilla && evt.altKey && charCode === 0 && keyCode === 0); + + // Is part of multi-keystroke international character on Safari Mac + var isSafariHalfCharacter = (browser.safari && evt.altKey && keyCode == 229); + + if (thisKeyDoesntTriggerNormalize || isFirefoxHalfCharacter || isSafariHalfCharacter) + { + idleWorkTimer.atLeast(3000); // give user time to type + // if this is a keydown, e.g., the keyup shouldn't trigger a normalize + thisKeyDoesntTriggerNormalize = true; + } + + if ((!specialHandled) && (!thisKeyDoesntTriggerNormalize) && (!inInternationalComposition)) + { + if (type != "keyup" || !incorpIfQuick()) + { + observeChangesAroundSelection(); + } + } + + if (type == "keyup") + { + thisKeyDoesntTriggerNormalize = false; + } + }); + } + + var thisKeyDoesntTriggerNormalize = false; + + function doUndoRedo(which) + { + // precond: normalized DOM + if (undoModule.enabled) + { + var whichMethod; + if (which == "undo") whichMethod = 'performUndo'; + if (which == "redo") whichMethod = 'performRedo'; + if (whichMethod) + { + var oldEventType = currentCallStack.editEvent.eventType; + currentCallStack.startNewEvent(which); + undoModule[whichMethod](function(backset, selectionInfo) + { + if (backset) + { + performDocumentApplyChangeset(backset); + } + if (selectionInfo) + { + performSelectionChange(lineAndColumnFromChar(selectionInfo.selStart), lineAndColumnFromChar(selectionInfo.selEnd), selectionInfo.selFocusAtStart); + } + var oldEvent = currentCallStack.startNewEvent(oldEventType, true); + return oldEvent; + }); + } + } + } + editorInfo.ace_doUndoRedo = doUndoRedo; + + function updateBrowserSelectionFromRep() + { + // requires normalized DOM! + var selStart = rep.selStart, + selEnd = rep.selEnd; + + if (!(selStart && selEnd)) + { + setSelection(null); + 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 ss = [selStart[0], selStart[1]]; + if (mozillaCaretHack) ss[1] += 1; + selection.startPoint = getPointForLineAndChar(ss); + + var se = [selEnd[0], selEnd[1]]; + if (mozillaCaretHack) se[1] += 1; + selection.endPoint = getPointForLineAndChar(se); + + selection.focusAtStart = !! rep.selFocusAtStart; + + setSelection(selection); + + if (mozillaCaretHack) + { + mozillaCaretHack.unhack(); + } + } + + function getRepHTML() + { + return map(rep.lines.slice(), function(entry) + { + var text = entry.text; + var content; + if (text.length === 0) + { + content = '<span style="color: #aaa">--</span>'; + } + else + { + content = htmlPrettyEscape(text); + } + return '<div><code>' + content + '</div></code>'; + }).join(''); + } + + function nodeMaxIndex(nd) + { + if (isNodeText(nd)) return nd.nodeValue.length; + else return 1; + } + + function hasIESelection() + { + var browserSelection; + try + { + browserSelection = doc.selection; + } + catch (e) + {} + if (!browserSelection) return false; + var origSelectionRange; + try + { + origSelectionRange = browserSelection.createRange(); + } + catch (e) + {} + if (!origSelectionRange) return false; + return true; + } + + function getSelection() + { + // returns null, or a structure containing startPoint and endPoint, + // each of which has node (a magicdom node), index, and maxIndex. If the node + // is a text node, maxIndex is the length of the text; else maxIndex is 1. + // index is between 0 and maxIndex, inclusive. + if (browser.msie) + { + var browserSelection; + try + { + browserSelection = doc.selection; + } + catch (e) + {} + if (!browserSelection) return null; + var origSelectionRange; + try + { + origSelectionRange = browserSelection.createRange(); + } + catch (e) + {} + if (!origSelectionRange) return null; + var selectionParent = origSelectionRange.parentElement(); + if (selectionParent.ownerDocument != doc) return null; + + var newRange = function() + { + return doc.body.createTextRange(); + }; + + var rangeForElementNode = function(nd) + { + var rng = newRange(); + // doesn't work on text nodes + rng.moveToElementText(nd); + return rng; + }; + + var pointFromCollapsedRange = function(rng) + { + var parNode = rng.parentElement(); + var elemBelow = -1; + var elemAbove = parNode.childNodes.length; + var rangeWithin = rangeForElementNode(parNode); + + if (rng.compareEndPoints("StartToStart", rangeWithin) === 0) + { + return { + node: parNode, + index: 0, + maxIndex: 1 + }; + } + else if (rng.compareEndPoints("EndToEnd", rangeWithin) === 0) + { + if (isBlockElement(parNode) && parNode.nextSibling) + { + // caret after block is not consistent across browsers + // (same line vs next) so put caret before next node + return { + node: parNode.nextSibling, + index: 0, + maxIndex: 1 + }; + } + return { + node: parNode, + index: 1, + maxIndex: 1 + }; + } + else if (parNode.childNodes.length === 0) + { + return { + node: parNode, + index: 0, + maxIndex: 1 + }; + } + + for (var i = 0; i < parNode.childNodes.length; i++) + { + var n = parNode.childNodes.item(i); + if (!isNodeText(n)) + { + var nodeRange = rangeForElementNode(n); + var startComp = rng.compareEndPoints("StartToStart", nodeRange); + var endComp = rng.compareEndPoints("EndToEnd", nodeRange); + if (startComp >= 0 && endComp <= 0) + { + var index = 0; + if (startComp > 0) + { + index = 1; + } + return { + node: n, + index: index, + maxIndex: 1 + }; + } + else if (endComp > 0) + { + if (i > elemBelow) + { + elemBelow = i; + rangeWithin.setEndPoint("StartToEnd", nodeRange); + } + } + else if (startComp < 0) + { + if (i < elemAbove) + { + elemAbove = i; + rangeWithin.setEndPoint("EndToStart", nodeRange); + } + } + } + } + if ((elemAbove - elemBelow) == 1) + { + if (elemBelow >= 0) + { + return { + node: parNode.childNodes.item(elemBelow), + index: 1, + maxIndex: 1 + }; + } + else + { + return { + node: parNode.childNodes.item(elemAbove), + index: 0, + maxIndex: 1 + }; + } + } + var idx = 0; + var r = rng.duplicate(); + // infinite stateful binary search! call function for values 0 to inf, + // expecting the answer to be about 40. return index of smallest + // true value. + var indexIntoRange = binarySearchInfinite(40, function(i) + { + // the search algorithm whips the caret back and forth, + // though it has to be moved relatively and may hit + // the end of the buffer + var delta = i - idx; + var moved = Math.abs(r.move("character", -delta)); + // next line is work-around for fact that when moving left, the beginning + // of a text node is considered to be after the start of the parent element: + if (r.move("character", -1)) r.move("character", 1); + if (delta < 0) idx -= moved; + else idx += moved; + return (r.compareEndPoints("StartToStart", rangeWithin) <= 0); + }); + // iterate over consecutive text nodes, point is in one of them + var textNode = elemBelow + 1; + var indexLeft = indexIntoRange; + while (textNode < elemAbove) + { + var tn = parNode.childNodes.item(textNode); + if (indexLeft <= tn.nodeValue.length) + { + return { + node: tn, + index: indexLeft, + maxIndex: tn.nodeValue.length + }; + } + indexLeft -= tn.nodeValue.length; + textNode++; + } + var tn = parNode.childNodes.item(textNode - 1); + return { + node: tn, + index: tn.nodeValue.length, + maxIndex: tn.nodeValue.length + }; + }; + + var selection = {}; + if (origSelectionRange.compareEndPoints("StartToEnd", origSelectionRange) === 0) + { + // collapsed + var pnt = pointFromCollapsedRange(origSelectionRange); + selection.startPoint = pnt; + selection.endPoint = { + node: pnt.node, + index: pnt.index, + maxIndex: pnt.maxIndex + }; + } + else + { + var start = origSelectionRange.duplicate(); + start.collapse(true); + var end = origSelectionRange.duplicate(); + end.collapse(false); + selection.startPoint = pointFromCollapsedRange(start); + selection.endPoint = pointFromCollapsedRange(end); +/*if ((!selection.startPoint.node.isText) && (!selection.endPoint.node.isText)) { + console.log(selection.startPoint.node.uniqueId()+","+ + selection.startPoint.index+" / "+ + selection.endPoint.node.uniqueId()+","+ + selection.endPoint.index); + }*/ + } + return selection; + } + else + { + // non-IE browser + var browserSelection = window.getSelection(); + if (browserSelection && browserSelection.type != "None" && browserSelection.rangeCount !== 0) + { + var range = browserSelection.getRangeAt(0); + + function isInBody(n) + { + while (n && !(n.tagName && n.tagName.toLowerCase() == "body")) + { + n = n.parentNode; + } + return !!n; + } + + function pointFromRangeBound(container, offset) + { + if (!isInBody(container)) + { + // command-click in Firefox selects whole document, HEAD and BODY! + return { + node: root, + index: 0, + maxIndex: 1 + }; + } + var n = container; + var childCount = n.childNodes.length; + if (isNodeText(n)) + { + return { + node: n, + index: offset, + maxIndex: n.nodeValue.length + }; + } + else if (childCount === 0) + { + return { + node: n, + index: 0, + maxIndex: 1 + }; + } + // treat point between two nodes as BEFORE the second (rather than after the first) + // if possible; this way point at end of a line block-element is treated as + // at beginning of next line + else if (offset == childCount) + { + var nd = n.childNodes.item(childCount - 1); + var max = nodeMaxIndex(nd); + return { + node: nd, + index: max, + maxIndex: max + }; + } + else + { + var nd = n.childNodes.item(offset); + var max = nodeMaxIndex(nd); + return { + node: nd, + index: 0, + maxIndex: max + }; + } + } + var selection = {}; + selection.startPoint = pointFromRangeBound(range.startContainer, range.startOffset); + selection.endPoint = pointFromRangeBound(range.endContainer, range.endOffset); + selection.focusAtStart = (((range.startContainer != range.endContainer) || (range.startOffset != range.endOffset)) && browserSelection.anchorNode && (browserSelection.anchorNode == range.endContainer) && (browserSelection.anchorOffset == range.endOffset)); + return selection; + } + else return null; + } + } + + function setSelection(selection) + { + function copyPoint(pt) + { + return { + node: pt.node, + index: pt.index, + maxIndex: pt.maxIndex + }; + } + if (browser.msie) + { + // Oddly enough, accessing scrollHeight fixes return key handling on IE 8, + // presumably by forcing some kind of internal DOM update. + doc.body.scrollHeight; + + function moveToElementText(s, n) + { + while (n.firstChild && !isNodeText(n.firstChild)) + { + n = n.firstChild; + } + s.moveToElementText(n); + } + + function newRange() + { + return doc.body.createTextRange(); + } + + function setCollapsedBefore(s, n) + { + // s is an IE TextRange, n is a dom node + if (isNodeText(n)) + { + // previous node should not also be text, but prevent inf recurs + if (n.previousSibling && !isNodeText(n.previousSibling)) + { + setCollapsedAfter(s, n.previousSibling); + } + else + { + setCollapsedBefore(s, n.parentNode); + } + } + else + { + moveToElementText(s, n); + // work around for issue that caret at beginning of line + // somehow ends up at end of previous line + if (s.move('character', 1)) + { + s.move('character', -1); + } + s.collapse(true); // to start + } + } + + function setCollapsedAfter(s, n) + { + // s is an IE TextRange, n is a magicdom node + if (isNodeText(n)) + { + // can't use end of container when no nextSibling (could be on next line), + // so use previousSibling or start of container and move forward. + setCollapsedBefore(s, n); + s.move("character", n.nodeValue.length); + } + else + { + moveToElementText(s, n); + s.collapse(false); // to end + } + } + + function getPointRange(point) + { + var s = newRange(); + var n = point.node; + if (isNodeText(n)) + { + setCollapsedBefore(s, n); + s.move("character", point.index); + } + else if (point.index === 0) + { + setCollapsedBefore(s, n); + } + else + { + setCollapsedAfter(s, n); + } + return s; + } + + if (selection) + { + if (!hasIESelection()) + { + return; // don't steal focus + } + + var startPoint = copyPoint(selection.startPoint); + var endPoint = copyPoint(selection.endPoint); + + // fix issue where selection can't be extended past end of line + // with shift-rightarrow or shift-downarrow + if (endPoint.index == endPoint.maxIndex && endPoint.node.nextSibling) + { + endPoint.node = endPoint.node.nextSibling; + endPoint.index = 0; + endPoint.maxIndex = nodeMaxIndex(endPoint.node); + } + var range = getPointRange(startPoint); + range.setEndPoint("EndToEnd", getPointRange(endPoint)); + + // setting the selection in IE causes everything to scroll + // so that the selection is visible. if setting the selection + // definitely accomplishes nothing, don't do it. + + + function isEqualToDocumentSelection(rng) + { + var browserSelection; + try + { + browserSelection = doc.selection; + } + catch (e) + {} + if (!browserSelection) return false; + var rng2 = browserSelection.createRange(); + if (rng2.parentElement().ownerDocument != doc) return false; + if (rng.compareEndPoints("StartToStart", rng2) !== 0) return false; + if (rng.compareEndPoints("EndToEnd", rng2) !== 0) return false; + return true; + } + if (!isEqualToDocumentSelection(range)) + { + //dmesg(toSource(selection)); + //dmesg(escapeHTML(doc.body.innerHTML)); + range.select(); + } + } + else + { + try + { + doc.selection.empty(); + } + catch (e) + {} + } + } + else + { + // non-IE browser + var isCollapsed; + + function pointToRangeBound(pt) + { + var p = copyPoint(pt); + // Make sure Firefox cursor is deep enough; fixes cursor jumping when at top level, + // and also problem where cut/copy of a whole line selected with fake arrow-keys + // copies the next line too. + if (isCollapsed) + { + function diveDeep() + { + while (p.node.childNodes.length > 0) + { + //&& (p.node == root || p.node.parentNode == root)) { + if (p.index === 0) + { + p.node = p.node.firstChild; + p.maxIndex = nodeMaxIndex(p.node); + } + else if (p.index == p.maxIndex) + { + p.node = p.node.lastChild; + p.maxIndex = nodeMaxIndex(p.node); + p.index = p.maxIndex; + } + else break; + } + } + // now fix problem where cursor at end of text node at end of span-like element + // with background doesn't seem to show up... + if (isNodeText(p.node) && p.index == p.maxIndex) + { + var n = p.node; + while ((!n.nextSibling) && (n != root) && (n.parentNode != root)) + { + n = n.parentNode; + } + if (n.nextSibling && (!((typeof n.nextSibling.tagName) == "string" && n.nextSibling.tagName.toLowerCase() == "br")) && (n != p.node) && (n != root) && (n.parentNode != root)) + { + // found a parent, go to next node and dive in + p.node = n.nextSibling; + p.maxIndex = nodeMaxIndex(p.node); + p.index = 0; + diveDeep(); + } + } + // try to make sure insertion point is styled; + // also fixes other FF problems + if (!isNodeText(p.node)) + { + diveDeep(); + } + } + if (isNodeText(p.node)) + { + return { + container: p.node, + offset: p.index + }; + } + else + { + // p.index in {0,1} + return { + container: p.node.parentNode, + offset: childIndex(p.node) + p.index + }; + } + } + var browserSelection = window.getSelection(); + if (browserSelection) + { + browserSelection.removeAllRanges(); + if (selection) + { + isCollapsed = (selection.startPoint.node === selection.endPoint.node && selection.startPoint.index === selection.endPoint.index); + var start = pointToRangeBound(selection.startPoint); + var end = pointToRangeBound(selection.endPoint); + + if ((!isCollapsed) && selection.focusAtStart && browserSelection.collapse && browserSelection.extend) + { + // can handle "backwards"-oriented selection, shift-arrow-keys move start + // of selection + browserSelection.collapse(end.container, end.offset); + //console.trace(); + //console.log(htmlPrettyEscape(rep.alltext)); + //console.log("%o %o", rep.selStart, rep.selEnd); + //console.log("%o %d", start.container, start.offset); + browserSelection.extend(start.container, start.offset); + } + else + { + var range = doc.createRange(); + range.setStart(start.container, start.offset); + range.setEnd(end.container, end.offset); + browserSelection.removeAllRanges(); + browserSelection.addRange(range); + } + } + } + } + } + + function childIndex(n) + { + var idx = 0; + while (n.previousSibling) + { + idx++; + n = n.previousSibling; + } + return idx; + } + + function fixView() + { + // calling this method repeatedly should be fast + if (getInnerWidth() === 0 || getInnerHeight() === 0) + { + return; + } + + function setIfNecessary(obj, prop, value) + { + if (obj[prop] != value) + { + obj[prop] = value; + } + } + + var lineNumberWidth = sideDiv.firstChild.offsetWidth; + var newSideDivWidth = lineNumberWidth + LINE_NUMBER_PADDING_LEFT; + if (newSideDivWidth < MIN_LINEDIV_WIDTH) newSideDivWidth = MIN_LINEDIV_WIDTH; + iframePadLeft = EDIT_BODY_PADDING_LEFT; + if (hasLineNumbers) iframePadLeft += newSideDivWidth + LINE_NUMBER_PADDING_RIGHT; + setIfNecessary(iframe.style, "left", iframePadLeft + "px"); + setIfNecessary(sideDiv.style, "width", newSideDivWidth + "px"); + + for (var i = 0; i < 2; i++) + { + var newHeight = root.clientHeight; + var newWidth = (browser.msie ? root.createTextRange().boundingWidth : root.clientWidth); + var viewHeight = getInnerHeight() - iframePadBottom - iframePadTop; + var viewWidth = getInnerWidth() - iframePadLeft - iframePadRight; + if (newHeight < viewHeight) + { + newHeight = viewHeight; + if (browser.msie) setIfNecessary(outerWin.document.documentElement.style, 'overflowY', 'auto'); + } + else + { + if (browser.msie) setIfNecessary(outerWin.document.documentElement.style, 'overflowY', 'scroll'); + } + if (doesWrap) + { + newWidth = viewWidth; + } + else + { + if (newWidth < viewWidth) newWidth = viewWidth; + } + setIfNecessary(iframe.style, "height", newHeight + "px"); + setIfNecessary(iframe.style, "width", newWidth + "px"); + setIfNecessary(sideDiv.style, "height", newHeight + "px"); + } + if (browser.mozilla) + { + if (!doesWrap) + { + // the body:display:table-cell hack makes mozilla do scrolling + // correctly by shrinking the <body> to fit around its content, + // but mozilla won't act on clicks below the body. We keep the + // style.height property set to the viewport height (editor height + // not including scrollbar), so it will never shrink so that part of + // the editor isn't clickable. + var body = root; + var styleHeight = viewHeight + "px"; + setIfNecessary(body.style, "height", styleHeight); + } + else + { + setIfNecessary(root.style, "height", ""); + } + } + // if near edge, scroll to edge + var scrollX = getScrollX(); + var scrollY = getScrollY(); + var win = outerWin; + var r = 20; + + enforceEditability(); + + addClass(sideDiv, 'sidedivdelayed'); + } + + function getScrollXY() + { + var win = outerWin; + var odoc = outerWin.document; + if (typeof(win.pageYOffset) == "number") + { + return { + x: win.pageXOffset, + y: win.pageYOffset + }; + } + var docel = odoc.documentElement; + if (docel && typeof(docel.scrollTop) == "number") + { + return { + x: docel.scrollLeft, + y: docel.scrollTop + }; + } + } + + function getScrollX() + { + return getScrollXY().x; + } + + function getScrollY() + { + return getScrollXY().y; + } + + function setScrollX(x) + { + outerWin.scrollTo(x, getScrollY()); + } + + function setScrollY(y) + { + outerWin.scrollTo(getScrollX(), y); + } + + function setScrollXY(x, y) + { + outerWin.scrollTo(x, y); + } + + var _teardownActions = []; + + function teardown() + { + forEach(_teardownActions, function(a) + { + a(); + }); + } + + bindEventHandler(window, "load", setup); + + function setDesignMode(newVal) + { + try + { + function setIfNecessary(target, prop, val) + { + if (String(target[prop]).toLowerCase() != val) + { + target[prop] = val; + return true; + } + return false; + } + if (browser.msie || browser.safari) + { + setIfNecessary(root, 'contentEditable', (newVal ? 'true' : 'false')); + } + else + { + var wasSet = setIfNecessary(doc, 'designMode', (newVal ? 'on' : 'off')); + if (wasSet && newVal && browser.opera) + { + // turning on designMode clears event handlers + bindTheEventHandlers(); + } + } + return true; + } + catch (e) + { + return false; + } + } + + var iePastedLines = null; + + function handleIEPaste(evt) + { + // Pasting in IE loses blank lines in a way that loses information; + // "one\n\ntwo\nthree" becomes "<p>one</p><p>two</p><p>three</p>", + // which becomes "one\ntwo\nthree". We can get the correct text + // from the clipboard directly, but we still have to let the paste + // happen to get the style information. + var clipText = window.clipboardData && window.clipboardData.getData("Text"); + if (clipText && doc.selection) + { + // this "paste" event seems to mess with the selection whether we try to + // stop it or not, so can't really do document-level manipulation now + // or in an idle call-stack. instead, use IE native manipulation + //function escapeLine(txt) { + //return processSpaces(escapeHTML(textify(txt))); + //} + //var newHTML = map(clipText.replace(/\r/g,'').split('\n'), escapeLine).join('<br>'); + //doc.selection.createRange().pasteHTML(newHTML); + //evt.preventDefault(); + //iePastedLines = map(clipText.replace(/\r/g,'').split('\n'), textify); + } + } + + var inInternationalComposition = false; + + function handleCompositionEvent(evt) + { + // international input events, fired in FF3, at least; allow e.g. Japanese input + if (evt.type == "compositionstart") + { + inInternationalComposition = true; + } + else if (evt.type == "compositionend") + { + inInternationalComposition = false; + } + } + + function bindTheEventHandlers() + { + bindEventHandler(document, "keydown", handleKeyEvent); + bindEventHandler(document, "keypress", handleKeyEvent); + bindEventHandler(document, "keyup", handleKeyEvent); + bindEventHandler(document, "click", handleClick); + bindEventHandler(root, "blur", handleBlur); + if (browser.msie) + { + bindEventHandler(document, "click", handleIEOuterClick); + } + if (browser.msie) bindEventHandler(root, "paste", handleIEPaste); + if ((!browser.msie) && document.documentElement) + { + bindEventHandler(document.documentElement, "compositionstart", handleCompositionEvent); + bindEventHandler(document.documentElement, "compositionend", handleCompositionEvent); + } + } + + function handleIEOuterClick(evt) + { + if ((evt.target.tagName || '').toLowerCase() != "html") + { + return; + } + if (!(evt.pageY > root.clientHeight)) + { + return; + } + + // click below the body + inCallStack("handleOuterClick", function() + { + // put caret at bottom of doc + fastIncorp(11); + if (isCaret()) + { // don't interfere with drag + var lastLine = rep.lines.length() - 1; + var lastCol = rep.lines.atIndex(lastLine).text.length; + performSelectionChange([lastLine, lastCol], [lastLine, lastCol]); + } + }); + } + + function getClassArray(elem, optFilter) + { + var bodyClasses = []; + (elem.className || '').replace(/\S+/g, function(c) + { + if ((!optFilter) || (optFilter(c))) + { + bodyClasses.push(c); + } + }); + return bodyClasses; + } + + function setClassArray(elem, array) + { + 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) + { + if (present) addClass(elem, className); + else removeClass(elem, className); + } + + function setup() + { + 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) addClass(root, "mozilla"); + if (browser.safari) addClass(root, "safari"); + if (browser.msie) addClass(root, "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(); + + }); + + scheduler.setTimeout(function() + { + parent.readyFunc(); // defined in code that sets up the inner iframe + }, 0); + + isSetUp = true; + } + + function focus() + { + window.focus(); + } + + function handleBlur(evt) + { + if (browser.msie) + { + // a fix: in IE, clicking on a control like a button outside the + // iframe can "blur" the editor, causing it to stop getting + // events, though typing still affects it(!). + setSelection(null); + } + } + + 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) + { + // doesn't work in wrap-mode + var node = point.node; + var index = point.index; + + function leftOf(n) + { + return n.offsetLeft; + } + + function rightOf(n) + { + return n.offsetLeft + n.offsetWidth; + } + if (!isNodeText(node)) + { + if (index === 0) return leftOf(node); + else return rightOf(node); + } + else + { + // we can get bounds of element nodes, so look for those. + // allow consecutive text nodes for robustness. + var charsToLeft = index; + var charsToRight = node.nodeValue.length - index; + var n; + for (n = node.previousSibling; n && isNodeText(n); n = n.previousSibling) + charsToLeft += n.nodeValue; + var leftEdge = (n ? rightOf(n) : leftOf(node.parentNode)); + for (n = node.nextSibling; n && isNodeText(n); n = n.nextSibling) + charsToRight += n.nodeValue; + var rightEdge = (n ? leftOf(n) : rightOf(node.parentNode)); + var frac = (charsToLeft / (charsToLeft + charsToRight)); + var pixLoc = leftEdge + frac * (rightEdge - leftEdge); + return Math.round(pixLoc); + } + } + + function getPageHeight() + { + var win = outerWin; + var odoc = win.document; + if (win.innerHeight && win.scrollMaxY) return win.innerHeight + win.scrollMaxY; + else if (odoc.body.scrollHeight > odoc.body.offsetHeight) return odoc.body.scrollHeight; + else return odoc.body.offsetHeight; + } + + function getPageWidth() + { + var win = outerWin; + var odoc = win.document; + if (win.innerWidth && win.scrollMaxX) return win.innerWidth + win.scrollMaxX; + else if (odoc.body.scrollWidth > odoc.body.offsetWidth) return odoc.body.scrollWidth; + else return odoc.body.offsetWidth; + } + + function getInnerHeight() + { + var win = outerWin; + var odoc = win.document; + var h; + if (browser.opera) h = win.innerHeight; + else h = odoc.documentElement.clientHeight; + if (h) return h; + + // deal with case where iframe is hidden, hope that + // style.height of iframe container is set in px + return Number(editorInfo.frame.parentNode.style.height.replace(/[^0-9]/g, '') || 0); + } + + function getInnerWidth() + { + var win = outerWin; + var odoc = win.document; + return odoc.documentElement.clientWidth; + } + + function scrollNodeVerticallyIntoView(node) + { + // requires element (non-text) node; + // if node extends above top of viewport or below bottom of viewport (or top of scrollbar), + // scroll it the minimum distance needed to be completely in view. + var win = outerWin; + var odoc = outerWin.document; + var distBelowTop = node.offsetTop + iframePadTop - win.scrollY; + var distAboveBottom = win.scrollY + getInnerHeight() - (node.offsetTop + iframePadTop + node.offsetHeight); + + if (distBelowTop < 0) + { + win.scrollBy(0, distBelowTop); + } + else if (distAboveBottom < 0) + { + win.scrollBy(0, -distAboveBottom); + } + } + + function scrollXHorizontallyIntoView(pixelX) + { + var win = outerWin; + var odoc = outerWin.document; + pixelX += iframePadLeft; + var distInsideLeft = pixelX - win.scrollX; + var distInsideRight = win.scrollX + getInnerWidth() - pixelX; + if (distInsideLeft < 0) + { + win.scrollBy(distInsideLeft, 0); + } + else if (distInsideRight < 0) + { + win.scrollBy(-distInsideRight + 1, 0); + } + } + + function scrollSelectionIntoView() + { + if (!rep.selStart) return; + fixView(); + var focusLine = (rep.selFocusAtStart ? rep.selStart[0] : rep.selEnd[0]); + scrollNodeVerticallyIntoView(rep.lines.atIndex(focusLine).lineNode); + if (!doesWrap) + { + var browserSelection = getSelection(); + if (browserSelection) + { + var focusPoint = (browserSelection.focusAtStart ? browserSelection.startPoint : browserSelection.endPoint); + var selectionPointX = getSelectionPointX(focusPoint); + scrollXHorizontallyIntoView(selectionPointX); + fixView(); + } + } + } + + function getLineListType(lineNum) + { + // get "list" attribute of first char of line + 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) + { + setLineListTypes([ + [lineNum, listType] + ]); + } + + function renumberList(lineNum){ + //1-check we are in a list + var type = getLineListType(lineNum); + if(!type) + { + return null; + } + type = /([a-z]+)[12345678]/.exec(type); + if(type[1] == "indent") + { + return null; + } + + //2-find the first line of the list + while(lineNum-1 >= 0 && (type=getLineListType(lineNum-1))) + { + type = /([a-z]+)[12345678]/.exec(type); + if(type[1] == "indent") + break; + lineNum--; + } + + //3-renumber every list item of the same level from the beginning, level 1 + //IMPORTANT: never skip a level because there imbrication may be arbitrary + var builder = Changeset.builder(rep.lines.totalWidth()); + loc = [0,0]; + function applyNumberList(line, level) + { + //init + var position = 1; + var curLevel = level; + var listType; + //loop over the lines + while(listType = getLineListType(line)) + { + //apply new num + listType = /([a-z]+)([12345678])/.exec(listType); + curLevel = Number(listType[2]); + if(isNaN(curLevel) || listType[0] == "indent") + { + return line; + } + else if(curLevel == level) + { + buildKeepRange(builder, loc, (loc = [line, 0])); + buildKeepRange(builder, loc, (loc = [line, 1]), [ + ['start', position] + ], rep.apool); + + position++; + line++; + } + else if(curLevel < level) + { + return line;//back to parent + } + else + { + line = applyNumberList(line, level+1);//recursive call + } + } + return line; + } + + applyNumberList(lineNum, 1); + var cs = builder.toString(); + if (!Changeset.isIdentity(cs)) + { + performDocumentApplyChangeset(cs); + } + + //4-apply the modifications + + + } + + 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) + { + if (!(rep.selStart && rep.selEnd)) + { + return; + } + + var firstLine, lastLine; + firstLine = rep.selStart[0]; + lastLine = Math.max(firstLine, rep.selEnd[0] - ((rep.selEnd[1] === 0) ? 1 : 0)); + + var allLinesAreList = true; + for (var n = firstLine; n <= lastLine; n++) + { + var listType = getLineListType(n); + if (!listType || listType.slice(0, type.length) != type) + { + allLinesAreList = false; + break; + } + } + + var mods = []; + for (var n = firstLine; n <= lastLine; n++) + { + var t = ''; + var level = 0; + var listType = /([a-z]+)([12345678])/.exec(getLineListType(n)); + if (listType) + { + t = listType[1]; + level = Number(listType[2]); + } + var t = getLineListType(n); + mods.push([n, allLinesAreList ? 'indent' + level : (t ? type + level : type + '1')]); + } + setLineListTypes(mods); + } + + function doInsertUnorderedList(){ + doInsertList('bullet'); + } + function doInsertOrderedList(){ + doInsertList('number'); + } + editorInfo.ace_doInsertUnorderedList = doInsertUnorderedList; + editorInfo.ace_doInsertOrderedList = doInsertOrderedList; + + var mozillaFakeArrows = (browser.mozilla && (function() + { + // In Firefox 2, arrow keys are unstable while DOM-manipulating + // operations are going on. Specifically, if an operation + // (computation that ties up the event queue) is going on (in the + // call-stack of some event, like a timeout) that at some point + // mutates nodes involved in the selection, then the arrow + // keypress may (randomly) move the caret to the beginning or end + // of the document. If the operation also mutates the selection + // range, the old selection or the new selection may be used, or + // neither. + // As long as the arrow is pressed during the busy operation, it + // doesn't seem to matter that the keydown and keypress events + // aren't generated until afterwards, or that the arrow movement + // can still be stopped (meaning it hasn't been performed yet); + // Firefox must be preserving some old information about the + // selection or the DOM from when the key was initially pressed. + // However, it also doesn't seem to matter when the key was + // actually pressed relative to the time of the mutation within + // the prolonged operation. Also, even in very controlled tests + // (like a mutation followed by a long period of busyWaiting), the + // problem shows up often but not every time, with no discernable + // pattern. Who knows, it could have something to do with the + // caret-blinking timer, or DOM changes not being applied + // immediately. + // This problem, mercifully, does not show up at all in IE or + // Safari. My solution is to have my own, full-featured arrow-key + // implementation for Firefox. + // Note that the problem addressed here is potentially very subtle, + // especially if the operation is quick and is timed to usually happen + // when the user is idle. + // features: + // - 'up' and 'down' arrows preserve column when passing through shorter lines + // - shift-arrows extend the "focus" point, which may be start or end of range + // - the focus point is kept horizontally and vertically scrolled into view + // - arrows without shift cause caret to move to beginning or end of selection (left,right) + // or move focus point up or down a line (up,down) + // - command-(left,right,up,down) on Mac acts like (line-start, line-end, doc-start, doc-end) + // - takes wrapping into account when doesWrap is true, i.e. up-arrow and down-arrow move + // between the virtual lines within a wrapped line; this was difficult, and unfortunately + // requires mutating the DOM to get the necessary information + var savedFocusColumn = 0; // a value of 0 has no effect + var updatingSelectionNow = false; + + function getVirtualLineView(lineNum) + { + var lineNode = rep.lines.atIndex(lineNum).lineNode; + while (lineNode.firstChild && isBlockElement(lineNode.firstChild)) + { + lineNode = lineNode.firstChild; + } + return makeVirtualLineView(lineNode); + } + + function markerlessLineAndChar(line, chr) + { + return [line, chr - rep.lines.atIndex(line).lineMarker]; + } + + function markerfulLineAndChar(line, chr) + { + return [line, chr + rep.lines.atIndex(line).lineMarker]; + } + + return { + notifySelectionChanged: function() + { + if (!updatingSelectionNow) + { + savedFocusColumn = 0; + } + }, + handleKeyEvent: function(evt) + { + // returns "true" if handled + if (evt.type != "keypress") return false; + var keyCode = evt.keyCode; + if (keyCode < 37 || keyCode > 40) return false; + incorporateUserChanges(); + + if (!(rep.selStart && rep.selEnd)) return true; + + // {byWord,toEnd,normal} + var moveMode = (evt.altKey ? "byWord" : (evt.ctrlKey ? "byWord" : (evt.metaKey ? "toEnd" : "normal"))); + + var anchorCaret = markerlessLineAndChar(rep.selStart[0], rep.selStart[1]); + var focusCaret = markerlessLineAndChar(rep.selEnd[0], rep.selEnd[1]); + var wasCaret = isCaret(); + if (rep.selFocusAtStart) + { + var tmp = anchorCaret; + anchorCaret = focusCaret; + focusCaret = tmp; + } + var K_UP = 38, + K_DOWN = 40, + K_LEFT = 37, + K_RIGHT = 39; + var dontMove = false; + if (wasCaret && !evt.shiftKey) + { + // collapse, will mutate both together + anchorCaret = focusCaret; + } + else if ((!wasCaret) && (!evt.shiftKey)) + { + if (keyCode == K_LEFT) + { + // place caret at beginning + if (rep.selFocusAtStart) anchorCaret = focusCaret; + else focusCaret = anchorCaret; + if (moveMode == "normal") dontMove = true; + } + else if (keyCode == K_RIGHT) + { + // place caret at end + if (rep.selFocusAtStart) focusCaret = anchorCaret; + else anchorCaret = focusCaret; + if (moveMode == "normal") dontMove = true; + } + else + { + // collapse, will mutate both together + anchorCaret = focusCaret; + } + } + if (!dontMove) + { + function lineLength(i) + { + var entry = rep.lines.atIndex(i); + return entry.text.length - entry.lineMarker; + } + + function lineText(i) + { + var entry = rep.lines.atIndex(i); + return entry.text.substring(entry.lineMarker); + } + + if (keyCode == K_UP || keyCode == K_DOWN) + { + var up = (keyCode == K_UP); + var canChangeLines = ((up && focusCaret[0]) || ((!up) && focusCaret[0] < rep.lines.length() - 1)); + var virtualLineView, virtualLineSpot, canChangeVirtualLines = false; + if (doesWrap) + { + virtualLineView = getVirtualLineView(focusCaret[0]); + virtualLineSpot = virtualLineView.getVLineAndOffsetForChar(focusCaret[1]); + canChangeVirtualLines = ((up && virtualLineSpot.vline > 0) || ((!up) && virtualLineSpot.vline < ( + virtualLineView.getNumVirtualLines() - 1))); + } + var newColByVirtualLineChange; + if (moveMode == "toEnd") + { + if (up) + { + focusCaret[0] = 0; + focusCaret[1] = 0; + } + else + { + focusCaret[0] = rep.lines.length() - 1; + focusCaret[1] = lineLength(focusCaret[0]); + } + } + else if (moveMode == "byWord") + { + // move by "paragraph", a feature that Firefox lacks but IE and Safari both have + if (up) + { + if (focusCaret[1] === 0 && canChangeLines) + { + focusCaret[0]--; + focusCaret[1] = 0; + } + else focusCaret[1] = 0; + } + else + { + var lineLen = lineLength(focusCaret[0]); + if (browser.windows) + { + if (canChangeLines) + { + focusCaret[0]++; + focusCaret[1] = 0; + } + else + { + focusCaret[1] = lineLen; + } + } + else + { + if (focusCaret[1] == lineLen && canChangeLines) + { + focusCaret[0]++; + focusCaret[1] = lineLength(focusCaret[0]); + } + else + { + focusCaret[1] = lineLen; + } + } + } + savedFocusColumn = 0; + } + else if (canChangeVirtualLines) + { + var vline = virtualLineSpot.vline; + var offset = virtualLineSpot.offset; + if (up) vline--; + else vline++; + if (savedFocusColumn > offset) offset = savedFocusColumn; + else + { + savedFocusColumn = offset; + } + var newSpot = virtualLineView.getCharForVLineAndOffset(vline, offset); + focusCaret[1] = newSpot.lineChar; + } + else if (canChangeLines) + { + if (up) focusCaret[0]--; + else focusCaret[0]++; + var offset = focusCaret[1]; + if (doesWrap) + { + offset = virtualLineSpot.offset; + } + if (savedFocusColumn > offset) offset = savedFocusColumn; + else + { + savedFocusColumn = offset; + } + if (doesWrap) + { + var newLineView = getVirtualLineView(focusCaret[0]); + var vline = (up ? newLineView.getNumVirtualLines() - 1 : 0); + var newSpot = newLineView.getCharForVLineAndOffset(vline, offset); + focusCaret[1] = newSpot.lineChar; + } + else + { + var lineLen = lineLength(focusCaret[0]); + if (offset > lineLen) offset = lineLen; + focusCaret[1] = offset; + } + } + else + { + if (up) focusCaret[1] = 0; + else focusCaret[1] = lineLength(focusCaret[0]); + savedFocusColumn = 0; + } + } + else if (keyCode == K_LEFT || keyCode == K_RIGHT) + { + var left = (keyCode == K_LEFT); + if (left) + { + if (moveMode == "toEnd") focusCaret[1] = 0; + else if (focusCaret[1] > 0) + { + if (moveMode == "byWord") + { + focusCaret[1] = moveByWordInLine(lineText(focusCaret[0]), focusCaret[1], false); + } + else + { + focusCaret[1]--; + } + } + else if (focusCaret[0] > 0) + { + focusCaret[0]--; + focusCaret[1] = lineLength(focusCaret[0]); + if (moveMode == "byWord") + { + focusCaret[1] = moveByWordInLine(lineText(focusCaret[0]), focusCaret[1], false); + } + } + } + else + { + var lineLen = lineLength(focusCaret[0]); + if (moveMode == "toEnd") focusCaret[1] = lineLen; + else if (focusCaret[1] < lineLen) + { + if (moveMode == "byWord") + { + focusCaret[1] = moveByWordInLine(lineText(focusCaret[0]), focusCaret[1], true); + } + else + { + focusCaret[1]++; + } + } + else if (focusCaret[0] < rep.lines.length() - 1) + { + focusCaret[0]++; + focusCaret[1] = 0; + if (moveMode == "byWord") + { + focusCaret[1] = moveByWordInLine(lineText(focusCaret[0]), focusCaret[1], true); + } + } + } + savedFocusColumn = 0; + } + } + + var newSelFocusAtStart = ((focusCaret[0] < anchorCaret[0]) || (focusCaret[0] == anchorCaret[0] && focusCaret[1] < anchorCaret[1])); + var newSelStart = (newSelFocusAtStart ? focusCaret : anchorCaret); + var newSelEnd = (newSelFocusAtStart ? anchorCaret : focusCaret); + updatingSelectionNow = true; + performSelectionChange(markerfulLineAndChar(newSelStart[0], newSelStart[1]), markerfulLineAndChar(newSelEnd[0], newSelEnd[1]), newSelFocusAtStart); + updatingSelectionNow = false; + currentCallStack.userChangedSelection = true; + return true; + } + }; + })()); + + + // 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 sideDivInner; + + function initLineNumbers() + { + lineNumbersShown = 1; + sideDiv.innerHTML = '<table border="0" cellpadding="0" cellspacing="0" align="right">' + '<tr><td id="sidedivinner"><div>1</div></td></tr></table>'; + sideDivInner = outerWin.document.getElementById("sidedivinner"); + } + + function updateLineNumbers() + { + var newNumLines = rep.lines.length(); + if (newNumLines < 1) newNumLines = 1; + //update height of all current line numbers + + var a = sideDivInner.firstChild; + var b = doc.body.firstChild; + var n = 0; + + if (currentCallStack && currentCallStack.domClean) + { + + while (a && b) + { + if(n > lineNumbersShown) //all updated, break + break; + + var h = (b.clientHeight || b.offsetHeight); + if (b.nextSibling) + { + // when text is zoomed in mozilla, divs have fractional + // heights (though the properties are always integers) + // and the line-numbers don't line up unless we pay + // attention to where the divs are actually placed... + // (also: padding on TTs/SPANs in IE...) + h = b.nextSibling.offsetTop - b.offsetTop; + } + if (h) + { + var hpx = h + "px"; + if (a.style.height != hpx) { + a.style.height = hpx; + } + } + a = a.nextSibling; + b = b.nextSibling; + n++; + } + } + + if (newNumLines != lineNumbersShown) + { + var container = sideDivInner; + var odoc = outerWin.document; + var fragment = odoc.createDocumentFragment(); + while (lineNumbersShown < newNumLines) + { + lineNumbersShown++; + var n = lineNumbersShown; + var div = odoc.createElement("DIV"); + //calculate height for new line number + var h = (b.clientHeight || b.offsetHeight); + + if (b.nextSibling) + h = b.nextSibling.offsetTop - b.offsetTop; + + if(h) // apply style to div + div.style.height = h +"px"; + + div.appendChild(odoc.createTextNode(String(n))); + fragment.appendChild(div); + b = b.nextSibling; + } + + container.appendChild(fragment); + while (lineNumbersShown > newNumLines) + { + container.removeChild(container.lastChild); + lineNumbersShown--; + } + } + } + +} + +exports.editor = new Ace2Inner(); diff --git a/src/static/js/broadcast.js b/src/static/js/broadcast.js new file mode 100644 index 00000000..485db44f --- /dev/null +++ b/src/static/js/broadcast.js @@ -0,0 +1,694 @@ +/** + * This code is mostly from the old Etherpad. Please help us to comment this code. + * This helps other people to understand this code better and helps them to improve it. + * TL;DR COMMENTS ON THIS FILE ARE HIGHLY APPRECIATED + */ + +/** + * 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. + */ + +var makeCSSManager = require('./cssmanager').makeCSSManager; +var domline = require('./domline').domline; +var AttribPool = require('./AttributePoolFactory').createAttributePool; +var Changeset = require('./Changeset'); +var linestylefilter = require('./linestylefilter').linestylefilter; +var colorutils = require('./colorutils').colorutils; +var Ace2Common = require('./ace2_common'); + +var map = Ace2Common.map; +var forEach = Ace2Common.forEach; + +// These parameters were global, now they are injected. A reference to the +// Timeslider controller would probably be more appropriate. +function loadBroadcastJS(socket, sendSocketMsg, fireWhenAllScriptsAreLoaded, BroadcastSlider) +{ + var changesetLoader = undefined; + + // Below Array#indexOf code was direct pasted by AppJet/Etherpad, licence unknown. Possible source: http://www.tutorialspoint.com/javascript/array_indexof.htm + if (!Array.prototype.indexOf) + { + Array.prototype.indexOf = function(elt /*, from*/ ) + { + var len = this.length >>> 0; + + var from = Number(arguments[1]) || 0; + from = (from < 0) ? Math.ceil(from) : Math.floor(from); + if (from < 0) from += len; + + for (; from < len; from++) + { + if (from in this && this[from] === elt) return from; + } + return -1; + }; + } + + function debugLog() + { + try + { + if (window.console) console.log.apply(console, arguments); + } + catch (e) + { + if (window.console) console.log("error printing: ", e); + } + } + + // for IE + if ($.browser.msie) + { + try + { + document.execCommand("BackgroundImageCache", false, true); + } + catch (e) + {} + } + + + var socketId; + //var socket; + var channelState = "DISCONNECTED"; + + var appLevelDisconnectReason = null; + + var padContents = { + currentRevision: clientVars.revNum, + currentTime: clientVars.currentTime, + currentLines: Changeset.splitTextLines(clientVars.initialStyledContents.atext.text), + currentDivs: null, + // to be filled in once the dom loads + apool: (new AttribPool()).fromJsonable(clientVars.initialStyledContents.apool), + alines: Changeset.splitAttributionLines( + clientVars.initialStyledContents.atext.attribs, clientVars.initialStyledContents.atext.text), + + // generates a jquery element containing HTML for a line + lineToElement: function(line, aline) + { + var element = document.createElement("div"); + var emptyLine = (line == '\n'); + var domInfo = domline.createDomLine(!emptyLine, true); + linestylefilter.populateDomLine(line, aline, this.apool, domInfo); + domInfo.prepareForAdd(); + element.className = domInfo.node.className; + element.innerHTML = domInfo.node.innerHTML; + element.id = Math.random(); + return $(element); + }, + + applySpliceToDivs: function(start, numRemoved, newLines) + { + // remove spliced-out lines from DOM + for (var i = start; i < start + numRemoved && i < this.currentDivs.length; i++) + { + debugLog("removing", this.currentDivs[i].attr('id')); + this.currentDivs[i].remove(); + } + + // remove spliced-out line divs from currentDivs array + this.currentDivs.splice(start, numRemoved); + + var newDivs = []; + for (var i = 0; i < newLines.length; i++) + { + newDivs.push(this.lineToElement(newLines[i], this.alines[start + i])); + } + + // grab the div just before the first one + var startDiv = this.currentDivs[start - 1] || null; + + // insert the div elements into the correct place, in the correct order + for (var i = 0; i < newDivs.length; i++) + { + if (startDiv) + { + startDiv.after(newDivs[i]); + } + else + { + $("#padcontent").prepend(newDivs[i]); + } + startDiv = newDivs[i]; + } + + // insert new divs into currentDivs array + newDivs.unshift(0); // remove 0 elements + newDivs.unshift(start); + this.currentDivs.splice.apply(this.currentDivs, newDivs); + return this; + }, + + // splice the lines + splice: function(start, numRemoved, newLinesVA) + { + var newLines = map(Array.prototype.slice.call(arguments, 2), function(s) { + return s; + }); + + // apply this splice to the divs + this.applySpliceToDivs(start, numRemoved, newLines); + + // call currentLines.splice, to keep the currentLines array up to date + newLines.unshift(numRemoved); + newLines.unshift(start); + this.currentLines.splice.apply(this.currentLines, arguments); + }, + // returns the contents of the specified line I + get: function(i) + { + return this.currentLines[i]; + }, + // returns the number of lines in the document + length: function() + { + return this.currentLines.length; + }, + + getActiveAuthors: function() + { + var self = this; + var authors = []; + var seenNums = {}; + var alines = self.alines; + for (var i = 0; i < alines.length; i++) + { + Changeset.eachAttribNumber(alines[i], function(n) + { + if (!seenNums[n]) + { + seenNums[n] = true; + if (self.apool.getAttribKey(n) == 'author') + { + var a = self.apool.getAttribValue(n); + if (a) + { + authors.push(a); + } + } + } + }); + } + authors.sort(); + return authors; + } + }; + + function callCatchingErrors(catcher, func) + { + try + { + wrapRecordingErrors(catcher, func)(); + } + catch (e) + { /*absorb*/ + } + } + + function wrapRecordingErrors(catcher, func) + { + return function() + { + try + { + return func.apply(this, Array.prototype.slice.call(arguments)); + } + catch (e) + { + // caughtErrors.push(e); + // caughtErrorCatchers.push(catcher); + // caughtErrorTimes.push(+new Date()); + // console.dir({catcher: catcher, e: e}); + debugLog(e); // TODO(kroo): added temporary, to catch errors + throw e; + } + }; + } + + function loadedNewChangeset(changesetForward, changesetBackward, revision, timeDelta) + { + var broadcasting = (BroadcastSlider.getSliderPosition() == revisionInfo.latest); + debugLog("broadcasting:", broadcasting, BroadcastSlider.getSliderPosition(), revisionInfo.latest, revision); + revisionInfo.addChangeset(revision, revision + 1, changesetForward, changesetBackward, timeDelta); + BroadcastSlider.setSliderLength(revisionInfo.latest); + if (broadcasting) applyChangeset(changesetForward, revision + 1, false, timeDelta); + } + +/* + At this point, we must be certain that the changeset really does map from + the current revision to the specified revision. Any mistakes here will + cause the whole slider to get out of sync. + */ + + function applyChangeset(changeset, revision, preventSliderMovement, timeDelta) + { + // disable the next 'gotorevision' call handled by a timeslider update + if (!preventSliderMovement) + { + goToRevisionIfEnabledCount++; + BroadcastSlider.setSliderPosition(revision); + } + + try + { + // must mutate attribution lines before text lines + Changeset.mutateAttributionLines(changeset, padContents.alines, padContents.apool); + } + catch (e) + { + debugLog(e); + } + + Changeset.mutateTextLines(changeset, padContents); + padContents.currentRevision = revision; + padContents.currentTime += timeDelta * 1000; + debugLog('Time Delta: ', timeDelta) + updateTimer(); + + var authors = map(padContents.getActiveAuthors(), function(name) + { + return authorData[name]; + }); + + BroadcastSlider.setAuthors(authors); + } + + function updateTimer() + { + var zpad = function(str, length) + { + str = str + ""; + while (str.length < length) + str = '0' + str; + return str; + } + + + + var date = new Date(padContents.currentTime); + var dateFormat = function() + { + var month = zpad(date.getMonth() + 1, 2); + var day = zpad(date.getDate(), 2); + var year = (date.getFullYear()); + var hours = zpad(date.getHours(), 2); + var minutes = zpad(date.getMinutes(), 2); + var seconds = zpad(date.getSeconds(), 2); + return ([month, '/', day, '/', year, ' ', hours, ':', minutes, ':', seconds].join("")); + } + + + + + + $('#timer').html(dateFormat()); + + var revisionDate = ["Saved", ["Jan", "Feb", "March", "April", "May", "June", "July", "Aug", "Sept", "Oct", "Nov", "Dec"][date.getMonth()], date.getDate() + ",", date.getFullYear()].join(" ") + $('#revision_date').html(revisionDate) + + } + + updateTimer(); + + function goToRevision(newRevision) + { + padContents.targetRevision = newRevision; + var self = this; + var path = revisionInfo.getPath(padContents.currentRevision, newRevision); + debugLog('newRev: ', padContents.currentRevision, path); + if (path.status == 'complete') + { + var cs = path.changesets; + debugLog("status: complete, changesets: ", cs, "path:", path); + var changeset = cs[0]; + var timeDelta = path.times[0]; + for (var i = 1; i < cs.length; i++) + { + changeset = Changeset.compose(changeset, cs[i], padContents.apool); + timeDelta += path.times[i]; + } + if (changeset) applyChangeset(changeset, path.rev, true, timeDelta); + } + else if (path.status == "partial") + { + debugLog('partial'); + var sliderLocation = padContents.currentRevision; + // callback is called after changeset information is pulled from server + // this may never get called, if the changeset has already been loaded + var update = function(start, end) + { + // if we've called goToRevision in the time since, don't goToRevision + goToRevision(padContents.targetRevision); + }; + + // do our best with what we have... + var cs = path.changesets; + + var changeset = cs[0]; + var timeDelta = path.times[0]; + for (var i = 1; i < cs.length; i++) + { + changeset = Changeset.compose(changeset, cs[i], padContents.apool); + timeDelta += path.times[i]; + } + if (changeset) applyChangeset(changeset, path.rev, true, timeDelta); + + + if (BroadcastSlider.getSliderLength() > 10000) + { + var start = (Math.floor((newRevision) / 10000) * 10000); // revision 0 to 10 + changesetLoader.queueUp(start, 100); + } + + if (BroadcastSlider.getSliderLength() > 1000) + { + var start = (Math.floor((newRevision) / 1000) * 1000); // (start from -1, go to 19) + 1 + changesetLoader.queueUp(start, 10); + } + + start = (Math.floor((newRevision) / 100) * 100); + + changesetLoader.queueUp(start, 1, update); + } + + var authors = map(padContents.getActiveAuthors(), function(name){ + return authorData[name]; + }); + BroadcastSlider.setAuthors(authors); + } + + changesetLoader = { + running: false, + resolved: [], + requestQueue1: [], + requestQueue2: [], + requestQueue3: [], + reqCallbacks: [], + queueUp: function(revision, width, callback) + { + if (revision < 0) revision = 0; + // if(changesetLoader.requestQueue.indexOf(revision) != -1) + // return; // already in the queue. + if (changesetLoader.resolved.indexOf(revision + "_" + width) != -1) return; // already loaded from the server + changesetLoader.resolved.push(revision + "_" + width); + + var requestQueue = width == 1 ? changesetLoader.requestQueue3 : width == 10 ? changesetLoader.requestQueue2 : changesetLoader.requestQueue1; + requestQueue.push( + { + 'rev': revision, + 'res': width, + 'callback': callback + }); + if (!changesetLoader.running) + { + changesetLoader.running = true; + setTimeout(changesetLoader.loadFromQueue, 10); + } + }, + loadFromQueue: function() + { + var self = changesetLoader; + var requestQueue = self.requestQueue1.length > 0 ? self.requestQueue1 : self.requestQueue2.length > 0 ? self.requestQueue2 : self.requestQueue3.length > 0 ? self.requestQueue3 : null; + + if (!requestQueue) + { + self.running = false; + return; + } + + var request = requestQueue.pop(); + var granularity = request.res; + var callback = request.callback; + var start = request.rev; + var requestID = Math.floor(Math.random() * 100000); + +/*var msg = { "component" : "timeslider", + "type":"CHANGESET_REQ", + "padId": padId, + "token": token, + "protocolVersion": 2, + "data" + { + "start": start, + "granularity": granularity + }}; + + socket.send(msg);*/ + + sendSocketMsg("CHANGESET_REQ", { + "start": start, + "granularity": granularity, + "requestID": requestID + }); + + self.reqCallbacks[requestID] = callback; + +/*debugLog("loadinging revision", start, "through ajax"); + $.getJSON("/ep/pad/changes/" + clientVars.padIdForUrl + "?s=" + start + "&g=" + granularity, function (data, textStatus) + { + if (textStatus !== "success") + { + console.log(textStatus); + BroadcastSlider.showReconnectUI(); + } + self.handleResponse(data, start, granularity, callback); + + setTimeout(self.loadFromQueue, 10); // load the next ajax function + });*/ + }, + handleSocketResponse: function(message) + { + var self = changesetLoader; + + var start = message.data.start; + var granularity = message.data.granularity; + var callback = self.reqCallbacks[message.data.requestID]; + delete self.reqCallbacks[message.data.requestID]; + + self.handleResponse(message.data, start, granularity, callback); + setTimeout(self.loadFromQueue, 10); + }, + handleResponse: function(data, start, granularity, callback) + { + debugLog("response: ", data); + var pool = (new AttribPool()).fromJsonable(data.apool); + for (var i = 0; i < data.forwardsChangesets.length; i++) + { + var astart = start + i * granularity - 1; // rev -1 is a blank single line + var aend = start + (i + 1) * granularity - 1; // totalRevs is the most recent revision + if (aend > data.actualEndNum - 1) aend = data.actualEndNum - 1; + debugLog("adding changeset:", astart, aend); + var forwardcs = Changeset.moveOpsToNewPool(data.forwardsChangesets[i], pool, padContents.apool); + var backwardcs = Changeset.moveOpsToNewPool(data.backwardsChangesets[i], pool, padContents.apool); + revisionInfo.addChangeset(astart, aend, forwardcs, backwardcs, data.timeDeltas[i]); + } + if (callback) callback(start - 1, start + data.forwardsChangesets.length * granularity - 1); + } + }; + + function handleMessageFromServer() + { + debugLog("handleMessage:", arguments); + var obj = arguments[0]['data']; + var expectedType = "COLLABROOM"; + + obj = JSON.parse(obj); + if (obj['type'] == expectedType) + { + obj = obj['data']; + + if (obj['type'] == "NEW_CHANGES") + { + debugLog(obj); + var changeset = Changeset.moveOpsToNewPool( + obj.changeset, (new AttribPool()).fromJsonable(obj.apool), padContents.apool); + + var changesetBack = Changeset.moveOpsToNewPool( + obj.changesetBack, (new AttribPool()).fromJsonable(obj.apool), padContents.apool); + + loadedNewChangeset(changeset, changesetBack, obj.newRev - 1, obj.timeDelta); + } + else if (obj['type'] == "NEW_AUTHORDATA") + { + var authorMap = {}; + authorMap[obj.author] = obj.data; + receiveAuthorData(authorMap); + + var authors = map(padContents.getActiveAuthors(),function(name) { + return authorData[name]; + }); + + BroadcastSlider.setAuthors(authors); + } + else if (obj['type'] == "NEW_SAVEDREV") + { + var savedRev = obj.savedRev; + BroadcastSlider.addSavedRevision(savedRev.revNum, savedRev); + } + } + else + { + debugLog("incorrect message type: " + obj['type'] + ", expected " + expectedType); + } + } + + function handleSocketClosed(params) + { + debugLog("socket closed!", params); + socket = null; + + BroadcastSlider.showReconnectUI(); + // var reason = appLevelDisconnectReason || params.reason; + // var shouldReconnect = params.reconnect; + // if (shouldReconnect) { + // // determine if this is a tight reconnect loop due to weird connectivity problems + // // reconnectTimes.push(+new Date()); + // var TOO_MANY_RECONNECTS = 8; + // var TOO_SHORT_A_TIME_MS = 10000; + // if (reconnectTimes.length >= TOO_MANY_RECONNECTS && + // ((+new Date()) - reconnectTimes[reconnectTimes.length-TOO_MANY_RECONNECTS]) < + // TOO_SHORT_A_TIME_MS) { + // setChannelState("DISCONNECTED", "looping"); + // } + // else { + // setChannelState("RECONNECTING", reason); + // setUpSocket(); + // } + // } + // else { + // BroadcastSlider.showReconnectUI(); + // setChannelState("DISCONNECTED", reason); + // } + } + + function sendMessage(msg) + { + socket.postMessage(JSON.stringify( + { + type: "COLLABROOM", + data: msg + })); + } + + + function setChannelState(newChannelState, moreInfo) + { + if (newChannelState != channelState) + { + channelState = newChannelState; + // callbacks.onChannelStateChange(channelState, moreInfo); + } + } + + function abandonConnection(reason) + { + if (socket) + { + socket.onclosed = function() + {}; + socket.onhiccup = function() + {}; + socket.disconnect(); + } + socket = null; + setChannelState("DISCONNECTED", reason); + } + +/*window['onloadFuncts'] = []; + window.onload = function () + { + window['isloaded'] = true; + forEach(window['onloadFuncts'],function (funct) + { + funct(); + }); + };*/ + + // to start upon window load, just push a function onto this array + //window['onloadFuncts'].push(setUpSocket); + //window['onloadFuncts'].push(function () + fireWhenAllScriptsAreLoaded.push(function() + { + // set up the currentDivs and DOM + padContents.currentDivs = []; + $("#padcontent").html(""); + for (var i = 0; i < padContents.currentLines.length; i++) + { + var div = padContents.lineToElement(padContents.currentLines[i], padContents.alines[i]); + padContents.currentDivs.push(div); + $("#padcontent").append(div); + } + debugLog(padContents.currentDivs); + }); + + // this is necessary to keep infinite loops of events firing, + // since goToRevision changes the slider position + var goToRevisionIfEnabledCount = 0; + var goToRevisionIfEnabled = function() + { + if (goToRevisionIfEnabledCount > 0) + { + goToRevisionIfEnabledCount--; + } + else + { + goToRevision.apply(goToRevision, arguments); + } + } + + + + + + BroadcastSlider.onSlider(goToRevisionIfEnabled); + + (function() + { + for (var i = 0; i < clientVars.initialChangesets.length; i++) + { + var csgroup = clientVars.initialChangesets[i]; + var start = clientVars.initialChangesets[i].start; + var granularity = clientVars.initialChangesets[i].granularity; + debugLog("loading changest on startup: ", start, granularity, csgroup); + changesetLoader.handleResponse(csgroup, start, granularity, null); + } + })(); + + var dynamicCSS = makeCSSManager('dynamicsyntax'); + var authorData = {}; + + function receiveAuthorData(newAuthorData) + { + for (var author in newAuthorData) + { + var data = newAuthorData[author]; + var bgcolor = typeof data.colorId == "number" ? clientVars.colorPalette[data.colorId] : data.colorId; + if (bgcolor && dynamicCSS) + { + var selector = dynamicCSS.selectorStyle('.' + linestylefilter.getAuthorClassName(author)); + selector.backgroundColor = bgcolor + selector.color = (colorutils.luminosity(colorutils.css2triple(bgcolor)) < 0.5) ? '#ffffff' : '#000000'; //see ace2_inner.js for the other part + } + authorData[author] = data; + } + } + + receiveAuthorData(clientVars.historicalAuthorData); + + return changesetLoader; +} + +exports.loadBroadcastJS = loadBroadcastJS; diff --git a/src/static/js/broadcast_revisions.js b/src/static/js/broadcast_revisions.js new file mode 100644 index 00000000..19f3f5ff --- /dev/null +++ b/src/static/js/broadcast_revisions.js @@ -0,0 +1,128 @@ +/** + * This code is mostly from the old Etherpad. Please help us to comment this code. + * This helps other people to understand this code better and helps them to improve it. + * TL;DR COMMENTS ON THIS FILE ARE HIGHLY APPRECIATED + */ + +/** + * 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. + */ +// revision info is a skip list whos entries represent a particular revision +// of the document. These revisions are connected together by various +// changesets, or deltas, between any two revisions. + +function loadBroadcastRevisionsJS() +{ + function Revision(revNum) + { + this.rev = revNum; + this.changesets = []; + } + + Revision.prototype.addChangeset = function(destIndex, changeset, timeDelta) + { + var changesetWrapper = { + deltaRev: destIndex - this.rev, + deltaTime: timeDelta, + getValue: function() + { + return changeset; + } + }; + this.changesets.push(changesetWrapper); + this.changesets.sort(function(a, b) + { + return (b.deltaRev - a.deltaRev) + }); + } + + revisionInfo = {}; + revisionInfo.addChangeset = function(fromIndex, toIndex, changeset, backChangeset, timeDelta) + { + var startRevision = revisionInfo[fromIndex] || revisionInfo.createNew(fromIndex); + var endRevision = revisionInfo[toIndex] || revisionInfo.createNew(toIndex); + startRevision.addChangeset(toIndex, changeset, timeDelta); + endRevision.addChangeset(fromIndex, backChangeset, -1 * timeDelta); + } + + revisionInfo.latest = clientVars.totalRevs || -1; + + revisionInfo.createNew = function(index) + { + revisionInfo[index] = new Revision(index); + if (index > revisionInfo.latest) + { + revisionInfo.latest = index; + } + + return revisionInfo[index]; + } + + // assuming that there is a path from fromIndex to toIndex, and that the links + // are laid out in a skip-list format + revisionInfo.getPath = function(fromIndex, toIndex) + { + var changesets = []; + var spans = []; + var times = []; + var elem = revisionInfo[fromIndex] || revisionInfo.createNew(fromIndex); + if (elem.changesets.length != 0 && fromIndex != toIndex) + { + var reverse = !(fromIndex < toIndex) + while (((elem.rev < toIndex) && !reverse) || ((elem.rev > toIndex) && reverse)) + { + var couldNotContinue = false; + var oldRev = elem.rev; + + for (var i = reverse ? elem.changesets.length - 1 : 0; + reverse ? i >= 0 : i < elem.changesets.length; + i += reverse ? -1 : 1) + { + if (((elem.changesets[i].deltaRev < 0) && !reverse) || ((elem.changesets[i].deltaRev > 0) && reverse)) + { + couldNotContinue = true; + break; + } + + if (((elem.rev + elem.changesets[i].deltaRev <= toIndex) && !reverse) || ((elem.rev + elem.changesets[i].deltaRev >= toIndex) && reverse)) + { + var topush = elem.changesets[i]; + changesets.push(topush.getValue()); + spans.push(elem.changesets[i].deltaRev); + times.push(topush.deltaTime); + elem = revisionInfo[elem.rev + elem.changesets[i].deltaRev]; + break; + } + } + + if (couldNotContinue || oldRev == elem.rev) break; + } + } + + var status = 'partial'; + if (elem.rev == toIndex) status = 'complete'; + + return { + 'fromRev': fromIndex, + 'rev': elem.rev, + 'status': status, + 'changesets': changesets, + 'spans': spans, + 'times': times + }; + } +} + +exports.loadBroadcastRevisionsJS = loadBroadcastRevisionsJS; diff --git a/src/static/js/broadcast_slider.js b/src/static/js/broadcast_slider.js new file mode 100644 index 00000000..2b67ac73 --- /dev/null +++ b/src/static/js/broadcast_slider.js @@ -0,0 +1,506 @@ +/** + * This code is mostly from the old Etherpad. Please help us to comment this code. + * This helps other people to understand this code better and helps them to improve it. + * TL;DR COMMENTS ON THIS FILE ARE HIGHLY APPRECIATED + */ + +/** + * 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. + */ + + // These parameters were global, now they are injected. A reference to the + // Timeslider controller would probably be more appropriate. +var forEach = require('./ace2_common').forEach; + +function loadBroadcastSliderJS(fireWhenAllScriptsAreLoaded) +{ + var BroadcastSlider; + + (function() + { // wrap this code in its own namespace + var sliderLength = 1000; + var sliderPos = 0; + var sliderActive = false; + var slidercallbacks = []; + var savedRevisions = []; + var sliderPlaying = false; + + function disableSelection(element) + { + element.onselectstart = function() + { + return false; + }; + element.unselectable = "on"; + element.style.MozUserSelect = "none"; + element.style.cursor = "default"; + } + var _callSliderCallbacks = function(newval) + { + sliderPos = newval; + for (var i = 0; i < slidercallbacks.length; i++) + { + slidercallbacks[i](newval); + } + } + + + + + + var updateSliderElements = function() + { + for (var i = 0; i < savedRevisions.length; i++) + { + var position = parseInt(savedRevisions[i].attr('pos')); + savedRevisions[i].css('left', (position * ($("#ui-slider-bar").width() - 2) / (sliderLength * 1.0)) - 1); + } + $("#ui-slider-handle").css('left', sliderPos * ($("#ui-slider-bar").width() - 2) / (sliderLength * 1.0)); + } + + + + + + var addSavedRevision = function(position, info) + { + var newSavedRevision = $('<div></div>'); + newSavedRevision.addClass("star"); + + newSavedRevision.attr('pos', position); + newSavedRevision.css('position', 'absolute'); + newSavedRevision.css('left', (position * ($("#ui-slider-bar").width() - 2) / (sliderLength * 1.0)) - 1); + $("#timeslider-slider").append(newSavedRevision); + newSavedRevision.mouseup(function(evt) + { + BroadcastSlider.setSliderPosition(position); + }); + savedRevisions.push(newSavedRevision); + }; + + var removeSavedRevision = function(position) + { + var element = $("div.star [pos=" + position + "]"); + savedRevisions.remove(element); + element.remove(); + return element; + }; + + /* Begin small 'API' */ + + function onSlider(callback) + { + slidercallbacks.push(callback); + } + + function getSliderPosition() + { + return sliderPos; + } + + function setSliderPosition(newpos) + { + newpos = Number(newpos); + if (newpos < 0 || newpos > sliderLength) return; + $("#ui-slider-handle").css('left', newpos * ($("#ui-slider-bar").width() - 2) / (sliderLength * 1.0)); + $("a.tlink").map(function() + { + $(this).attr('href', $(this).attr('thref').replace("%revision%", newpos)); + }); + $("#revision_label").html("Version " + newpos); + + if (newpos == 0) + { + $("#leftstar").css('opacity', .5); + $("#leftstep").css('opacity', .5); + } + else + { + $("#leftstar").css('opacity', 1); + $("#leftstep").css('opacity', 1); + } + + if (newpos == sliderLength) + { + $("#rightstar").css('opacity', .5); + $("#rightstep").css('opacity', .5); + } + else + { + $("#rightstar").css('opacity', 1); + $("#rightstep").css('opacity', 1); + } + + sliderPos = newpos; + _callSliderCallbacks(newpos); + } + + function getSliderLength() + { + return sliderLength; + } + + function setSliderLength(newlength) + { + sliderLength = newlength; + updateSliderElements(); + } + + // just take over the whole slider screen with a reconnect message + + function showReconnectUI() + { + if (!clientVars.sliderEnabled || !clientVars.supportsSlider) + { + $("#padmain, #rightbars").css('top', "130px"); + $("#timeslider").show(); + } + $('#error').show(); + } + + function setAuthors(authors) + { + $("#authorstable").empty(); + var numAnonymous = 0; + var numNamed = 0; + forEach(authors, function(author) + { + if (author.name) + { + 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 + { + numAnonymous++; + } + }); + 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>"; + $("#authorstable").append($(html)); + } + if (authors.length == 0) + { + $("#authorstable").append($("<tr><td colspan=\"2\" style=\"color:#999; padding-left: 10px\">No Authors</td></tr>")) + } + } + + BroadcastSlider = { + onSlider: onSlider, + getSliderPosition: getSliderPosition, + setSliderPosition: setSliderPosition, + getSliderLength: getSliderLength, + setSliderLength: setSliderLength, + isSliderActive: function() + { + return sliderActive; + }, + playpause: playpause, + addSavedRevision: addSavedRevision, + showReconnectUI: showReconnectUI, + setAuthors: setAuthors + } + + function playButtonUpdater() + { + if (sliderPlaying) + { + if (getSliderPosition() + 1 > sliderLength) + { + $("#playpause_button_icon").toggleClass('pause'); + sliderPlaying = false; + return; + } + setSliderPosition(getSliderPosition() + 1); + + setTimeout(playButtonUpdater, 100); + } + } + + function playpause() + { + $("#playpause_button_icon").toggleClass('pause'); + + if (!sliderPlaying) + { + if (getSliderPosition() == sliderLength) setSliderPosition(0); + sliderPlaying = true; + playButtonUpdater(); + } + else + { + sliderPlaying = false; + } + } + + // assign event handlers to html UI elements after page load + //$(window).load(function () + fireWhenAllScriptsAreLoaded.push(function() + { + disableSelection($("#playpause_button")[0]); + disableSelection($("#timeslider")[0]); + + if (clientVars.sliderEnabled && clientVars.supportsSlider) + { + $(document).keyup(function(e) + { + var code = -1; + if (!e) var e = window.event; + if (e.keyCode) code = e.keyCode; + else if (e.which) code = e.which; + + if (code == 37) + { // left + if (!e.shiftKey) + { + setSliderPosition(getSliderPosition() - 1); + } + else + { + var nextStar = 0; // default to first revision in document + for (var i = 0; i < savedRevisions.length; i++) + { + var pos = parseInt(savedRevisions[i].attr('pos')); + if (pos < getSliderPosition() && nextStar < pos) nextStar = pos; + } + setSliderPosition(nextStar); + } + } + else if (code == 39) + { + if (!e.shiftKey) + { + setSliderPosition(getSliderPosition() + 1); + } + else + { + var nextStar = sliderLength; // default to last revision in document + for (var i = 0; i < savedRevisions.length; i++) + { + var pos = parseInt(savedRevisions[i].attr('pos')); + if (pos > getSliderPosition() && nextStar > pos) nextStar = pos; + } + setSliderPosition(nextStar); + } + } + else if (code == 32) playpause(); + + }); + } + + $(window).resize(function() + { + updateSliderElements(); + }); + + $("#ui-slider-bar").mousedown(function(evt) + { + setSliderPosition(Math.floor((evt.clientX - $("#ui-slider-bar").offset().left) * sliderLength / 742)); + $("#ui-slider-handle").css('left', (evt.clientX - $("#ui-slider-bar").offset().left)); + $("#ui-slider-handle").trigger(evt); + }); + + // Slider dragging + $("#ui-slider-handle").mousedown(function(evt) + { + this.startLoc = evt.clientX; + this.currentLoc = parseInt($(this).css('left')); + var self = this; + sliderActive = true; + $(document).mousemove(function(evt2) + { + $(self).css('pointer', 'move') + var newloc = self.currentLoc + (evt2.clientX - self.startLoc); + if (newloc < 0) newloc = 0; + if (newloc > ($("#ui-slider-bar").width() - 2)) newloc = ($("#ui-slider-bar").width() - 2); + $("#revision_label").html("Version " + Math.floor(newloc * sliderLength / ($("#ui-slider-bar").width() - 2))); + $(self).css('left', newloc); + if (getSliderPosition() != Math.floor(newloc * sliderLength / ($("#ui-slider-bar").width() - 2))) _callSliderCallbacks(Math.floor(newloc * sliderLength / ($("#ui-slider-bar").width() - 2))) + }); + $(document).mouseup(function(evt2) + { + $(document).unbind('mousemove'); + $(document).unbind('mouseup'); + sliderActive = false; + var newloc = self.currentLoc + (evt2.clientX - self.startLoc); + if (newloc < 0) newloc = 0; + if (newloc > ($("#ui-slider-bar").width() - 2)) newloc = ($("#ui-slider-bar").width() - 2); + $(self).css('left', newloc); + // if(getSliderPosition() != Math.floor(newloc * sliderLength / ($("#ui-slider-bar").width()-2))) + setSliderPosition(Math.floor(newloc * sliderLength / ($("#ui-slider-bar").width() - 2))) + self.currentLoc = parseInt($(self).css('left')); + }); + }) + + // play/pause toggling + $("#playpause_button").mousedown(function(evt) + { + var self = this; + + $(self).css('background-image', 'url(/static/img/crushed_button_depressed.png)'); + $(self).mouseup(function(evt2) + { + $(self).css('background-image', 'url(/static/img/crushed_button_undepressed.png)'); + $(self).unbind('mouseup'); + BroadcastSlider.playpause(); + }); + $(document).mouseup(function(evt2) + { + $(self).css('background-image', 'url(/static/img/crushed_button_undepressed.png)'); + $(document).unbind('mouseup'); + }); + }); + + // next/prev saved revision and changeset + $('.stepper').mousedown(function(evt) + { + var self = this; + var origcss = $(self).css('background-position'); + if (!origcss) + { + origcss = $(self).css('background-position-x') + " " + $(self).css('background-position-y'); + } + var origpos = parseInt(origcss.split(" ")[1]); + var newpos = (origpos - 43); + if (newpos < 0) newpos += 87; + + var newcss = (origcss.split(" ")[0] + " " + newpos + "px"); + if ($(self).css('opacity') != 1.0) newcss = origcss; + + $(self).css('background-position', newcss) + + $(self).mouseup(function(evt2) + { + $(self).css('background-position', origcss); + $(self).unbind('mouseup'); + $(document).unbind('mouseup'); + if ($(self).attr("id") == ("leftstep")) + { + setSliderPosition(getSliderPosition() - 1); + } + else if ($(self).attr("id") == ("rightstep")) + { + setSliderPosition(getSliderPosition() + 1); + } + else if ($(self).attr("id") == ("leftstar")) + { + var nextStar = 0; // default to first revision in document + for (var i = 0; i < savedRevisions.length; i++) + { + var pos = parseInt(savedRevisions[i].attr('pos')); + if (pos < getSliderPosition() && nextStar < pos) nextStar = pos; + } + setSliderPosition(nextStar); + } + else if ($(self).attr("id") == ("rightstar")) + { + var nextStar = sliderLength; // default to last revision in document + for (var i = 0; i < savedRevisions.length; i++) + { + var pos = parseInt(savedRevisions[i].attr('pos')); + if (pos > getSliderPosition() && nextStar > pos) nextStar = pos; + } + setSliderPosition(nextStar); + } + }); + $(document).mouseup(function(evt2) + { + $(self).css('background-position', origcss); + $(self).unbind('mouseup'); + $(document).unbind('mouseup'); + }); + }) + + if (clientVars) + { + if (clientVars.fullWidth) + { + $("#padpage").css('width', '100%'); + $("#revision").css('position', "absolute") + $("#revision").css('right', "20px") + $("#revision").css('top', "20px") + $("#padmain").css('left', '0px'); + $("#padmain").css('right', '197px'); + $("#padmain").css('width', 'auto'); + $("#rightbars").css('right', '7px'); + $("#rightbars").css('margin-right', '0px'); + $("#timeslider").css('width', 'auto'); + } + + if (clientVars.disableRightBar) + { + $("#rightbars").css('display', 'none'); + $('#padmain').css('width', 'auto'); + if (clientVars.fullWidth) $("#padmain").css('right', '7px'); + else $("#padmain").css('width', '860px'); + $("#revision").css('position', "absolute"); + $("#revision").css('right', "20px"); + $("#revision").css('top', "20px"); + } + + + if (clientVars.sliderEnabled) + { + if (clientVars.supportsSlider) + { + $("#padmain, #rightbars").css('top', "130px"); + $("#timeslider").show(); + setSliderLength(clientVars.totalRevs); + setSliderPosition(clientVars.revNum); + forEach(clientVars.savedRevisions, function(revision) + { + addSavedRevision(revision.revNum, revision); + }) + } + else + { + // slider is not supported + $("#padmain, #rightbars").css('top', "130px"); + $("#timeslider").show(); + $("#error").html("The timeslider feature is not supported on this pad. <a href=\"/ep/about/faq#disabledslider\">Why not?</a>"); + $("#error").show(); + } + } + else + { + if (clientVars.supportsSlider) + { + setSliderLength(clientVars.totalRevs); + setSliderPosition(clientVars.revNum); + } + } + } + }); + })(); + + BroadcastSlider.onSlider(function(loc) + { + $("#viewlatest").html(loc == BroadcastSlider.getSliderLength() ? "Viewing latest content" : "View latest content"); + }) + + return BroadcastSlider; +} + +exports.loadBroadcastSliderJS = loadBroadcastSliderJS; diff --git a/src/static/js/changesettracker.js b/src/static/js/changesettracker.js new file mode 100644 index 00000000..b0219852 --- /dev/null +++ b/src/static/js/changesettracker.js @@ -0,0 +1,213 @@ +/** + * This code is mostly from the old Etherpad. Please help us to comment this code. + * This helps other people to understand this code better and helps them to improve it. + * TL;DR COMMENTS ON THIS FILE ARE HIGHLY APPRECIATED + */ + +/** + * 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. + */ + +var AttribPool = require('./AttributePoolFactory').createAttributePool; +var Changeset = require('./Changeset'); + +function makeChangesetTracker(scheduler, apool, aceCallbacksProvider) +{ + + // latest official text from server + var baseAText = Changeset.makeAText("\n"); + // changes applied to baseText that have been submitted + var submittedChangeset = null; + // changes applied to submittedChangeset since it was prepared + var userChangeset = Changeset.identity(1); + // is the changesetTracker enabled + var tracking = false; + // stack state flag so that when we change the rep we don't + // handle the notification recursively. When setting, always + // unset in a "finally" block. When set to true, the setter + // takes change of userChangeset. + var applyingNonUserChanges = false; + + var changeCallback = null; + + var changeCallbackTimeout = null; + + function setChangeCallbackTimeout() + { + // can call this multiple times per call-stack, because + // we only schedule a call to changeCallback if it exists + // and if there isn't a timeout already scheduled. + if (changeCallback && changeCallbackTimeout === null) + { + changeCallbackTimeout = scheduler.setTimeout(function() + { + try + { + changeCallback(); + } + finally + { + changeCallbackTimeout = null; + } + }, 0); + } + } + + var self; + return self = { + isTracking: function() + { + return tracking; + }, + setBaseText: function(text) + { + self.setBaseAttributedText(Changeset.makeAText(text), null); + }, + setBaseAttributedText: function(atext, apoolJsonObj) + { + aceCallbacksProvider.withCallbacks("setBaseText", function(callbacks) + { + tracking = true; + baseAText = Changeset.cloneAText(atext); + if (apoolJsonObj) + { + var wireApool = (new AttribPool()).fromJsonable(apoolJsonObj); + baseAText.attribs = Changeset.moveOpsToNewPool(baseAText.attribs, wireApool, apool); + } + submittedChangeset = null; + userChangeset = Changeset.identity(atext.text.length); + applyingNonUserChanges = true; + try + { + callbacks.setDocumentAttributedText(atext); + } + finally + { + applyingNonUserChanges = false; + } + }); + }, + composeUserChangeset: function(c) + { + if (!tracking) return; + if (applyingNonUserChanges) return; + if (Changeset.isIdentity(c)) return; + userChangeset = Changeset.compose(userChangeset, c, apool); + + setChangeCallbackTimeout(); + }, + applyChangesToBase: function(c, optAuthor, apoolJsonObj) + { + if (!tracking) return; + + aceCallbacksProvider.withCallbacks("applyChangesToBase", function(callbacks) + { + + if (apoolJsonObj) + { + var wireApool = (new AttribPool()).fromJsonable(apoolJsonObj); + c = Changeset.moveOpsToNewPool(c, wireApool, apool); + } + + baseAText = Changeset.applyToAText(c, baseAText, apool); + + var c2 = c; + if (submittedChangeset) + { + var oldSubmittedChangeset = submittedChangeset; + submittedChangeset = Changeset.follow(c, oldSubmittedChangeset, false, apool); + c2 = Changeset.follow(oldSubmittedChangeset, c, true, apool); + } + + var preferInsertingAfterUserChanges = true; + var oldUserChangeset = userChangeset; + userChangeset = Changeset.follow(c2, oldUserChangeset, preferInsertingAfterUserChanges, apool); + var postChange = Changeset.follow(oldUserChangeset, c2, !preferInsertingAfterUserChanges, apool); + + var preferInsertionAfterCaret = true; //(optAuthor && optAuthor > thisAuthor); + applyingNonUserChanges = true; + try + { + callbacks.applyChangesetToDocument(postChange, preferInsertionAfterCaret); + } + finally + { + applyingNonUserChanges = false; + } + }); + }, + prepareUserChangeset: function() + { + // If there are user changes to submit, 'changeset' will be the + // changeset, else it will be null. + var toSubmit; + if (submittedChangeset) + { + // submission must have been canceled, prepare new changeset + // that includes old submittedChangeset + toSubmit = Changeset.compose(submittedChangeset, userChangeset, apool); + } + else + { + if (Changeset.isIdentity(userChangeset)) toSubmit = null; + else toSubmit = userChangeset; + } + + var cs = null; + if (toSubmit) + { + submittedChangeset = toSubmit; + userChangeset = Changeset.identity(Changeset.newLen(toSubmit)); + + cs = toSubmit; + } + var wireApool = null; + if (cs) + { + var forWire = Changeset.prepareForWire(cs, apool); + wireApool = forWire.pool.toJsonable(); + cs = forWire.translated; + } + + var data = { + changeset: cs, + apool: wireApool + }; + return data; + }, + applyPreparedChangesetToBase: function() + { + if (!submittedChangeset) + { + // violation of protocol; use prepareUserChangeset first + throw new Error("applySubmittedChangesToBase: no submitted changes to apply"); + } + //bumpDebug("applying committed changeset: "+submittedChangeset.encodeToString(false)); + baseAText = Changeset.applyToAText(submittedChangeset, baseAText, apool); + submittedChangeset = null; + }, + setUserChangeNotificationCallback: function(callback) + { + changeCallback = callback; + }, + hasUncommittedChanges: function() + { + return !!(submittedChangeset || (!Changeset.isIdentity(userChangeset))); + } + }; + +} + +exports.makeChangesetTracker = makeChangesetTracker; diff --git a/src/static/js/chat.js b/src/static/js/chat.js new file mode 100644 index 00000000..b8f22e9e --- /dev/null +++ b/src/static/js/chat.js @@ -0,0 +1,162 @@ +/** + * This code is mostly from the old Etherpad. Please help us to comment this code. + * This helps other people to understand this code better and helps them to improve it. + * TL;DR COMMENTS ON THIS FILE ARE HIGHLY APPRECIATED + */ + +/** + * 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. + */ + +var padutils = require('./pad_utils').padutils; +var padcookie = require('./pad_cookie').padcookie; + +var chat = (function() +{ + var isStuck = false; + var chatMentions = 0; + var title = document.title; + var self = { + show: function () + { + $("#chaticon").hide(); + $("#chatbox").show(); + self.scrollDown(); + chatMentions = 0; + document.title = title; + }, + stickToScreen: function(fromInitialCall) // Make chat stick to right hand side of screen + { + chat.show(); + if(!isStuck || fromInitialCall) { // Stick it to + padcookie.setPref("chatAlwaysVisible", true); + $('#chatbox').addClass("stickyChat"); + $('#chattext').css({"top":"0px"}); + $('#editorcontainer').css({"right":"192px", "width":"auto"}); + isStuck = true; + } else { // Unstick it + padcookie.setPref("chatAlwaysVisible", false); + $('#chatbox').removeClass("stickyChat"); + $('#chattext').css({"top":"25px"}); + $('#editorcontainer').css({"right":"0px", "width":"100%"}); + isStuck = false; + } + }, + hide: function () + { + $("#chatcounter").text("0"); + $("#chaticon").show(); + $("#chatbox").hide(); + }, + scrollDown: function() + { + if($('#chatbox').css("display") != "none") + $('#chattext').animate({scrollTop: $('#chattext')[0].scrollHeight}, "slow"); + }, + send: function() + { + var text = $("#chatinput").val(); + this._pad.collabClient.sendMessage({"type": "CHAT_MESSAGE", "text": text}); + $("#chatinput").val(""); + }, + addMessage: function(msg, increment) + { + //correct the time + msg.time += this._pad.clientTimeOffset; + + //create the time string + var minutes = "" + new Date(msg.time).getMinutes(); + var hours = "" + new Date(msg.time).getHours(); + if(minutes.length == 1) + minutes = "0" + minutes ; + if(hours.length == 1) + hours = "0" + hours ; + var timeStr = hours + ":" + minutes; + + //create the authorclass + var authorClass = "author-" + msg.userId.replace(/[^a-y0-9]/g, function(c) + { + if (c == ".") return "-"; + return 'z' + c.charCodeAt(0) + 'z'; + }); + + var text = padutils.escapeHtmlWithClickableLinks(msg.text, "_blank"); + + /* Performs an action if your name is mentioned */ + var myName = $('#myusernameedit').val(); + myName = myName.toLowerCase(); + var chatText = text.toLowerCase(); + var wasMentioned = false; + if (chatText.indexOf(myName) !== -1 && myName != "undefined"){ + wasMentioned = true; + } + /* End of new action */ + + var authorName = msg.userName == null ? "unnamed" : padutils.escapeHtml(msg.userName); + + var html = "<p class='" + authorClass + "'><b>" + authorName + ":</b><span class='time " + authorClass + "'>" + timeStr + "</span> " + text + "</p>"; + $("#chattext").append(html); + + //should we increment the counter?? + if(increment) + { + var count = Number($("#chatcounter").text()); + count++; + $("#chatcounter").text(count); + // chat throb stuff -- Just make it throw for twice as long + if(wasMentioned) + { // If the user was mentioned show for twice as long and flash the browser window + if (chatMentions == 0){ + title = document.title; + } + $('#chatthrob').html("<b>"+authorName+"</b>" + ": " + text).show().delay(4000).hide(400); + chatMentions++; + document.title = "("+chatMentions+") " + title; + } + else + { + $('#chatthrob').html("<b>"+authorName+"</b>" + ": " + text).show().delay(2000).hide(400); + } + } + + self.scrollDown(); + + }, + init: function(pad) + { + this._pad = pad; + $("#chatinput").keypress(function(evt) + { + //if the user typed enter, fire the send + if(evt.which == 13) + { + evt.preventDefault(); + self.send(); + } + }); + + for(var i in clientVars.chatHistory) + { + this.addMessage(clientVars.chatHistory[i], false); + } + $("#chatcounter").text(clientVars.chatHistory.length); + } + } + + return self; +}()); + +exports.chat = chat; + diff --git a/src/static/js/collab_client.js b/src/static/js/collab_client.js new file mode 100644 index 00000000..18e3616b --- /dev/null +++ b/src/static/js/collab_client.js @@ -0,0 +1,706 @@ +/** + * This code is mostly from the old Etherpad. Please help us to comment this code. + * This helps other people to understand this code better and helps them to improve it. + * TL;DR COMMENTS ON THIS FILE ARE HIGHLY APPRECIATED + */ + +/** + * 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. + */ + +var chat = require('./chat').chat; + +// Dependency fill on init. This exists for `pad.socket` only. +// TODO: bind directly to the socket. +var pad = undefined; +function getSocket() { + return pad && pad.socket; +} + +/** Call this when the document is ready, and a new Ace2Editor() has been created and inited. + ACE's ready callback does not need to have fired yet. + "serverVars" are from calling doc.getCollabClientVars() on the server. */ +function getCollabClient(ace2editor, serverVars, initialUserInfo, options, _pad) +{ + var editor = ace2editor; + pad = _pad; // Inject pad to avoid a circular dependency. + + var rev = serverVars.rev; + var padId = serverVars.padId; + var globalPadId = serverVars.globalPadId; + + var state = "IDLE"; + var stateMessage; + var stateMessageSocketId; + var channelState = "CONNECTING"; + var appLevelDisconnectReason = null; + + var lastCommitTime = 0; + var initialStartConnectTime = 0; + + var userId = initialUserInfo.userId; + var socketId; + //var socket; + var userSet = {}; // userId -> userInfo + userSet[userId] = initialUserInfo; + + var reconnectTimes = []; + var caughtErrors = []; + var caughtErrorCatchers = []; + var caughtErrorTimes = []; + var debugMessages = []; + + tellAceAboutHistoricalAuthors(serverVars.historicalAuthorData); + tellAceActiveAuthorInfo(initialUserInfo); + + var callbacks = { + onUserJoin: function() + {}, + onUserLeave: function() + {}, + onUpdateUserInfo: function() + {}, + onChannelStateChange: function() + {}, + onClientMessage: function() + {}, + onInternalAction: function() + {}, + onConnectionTrouble: function() + {}, + onServerMessage: function() + {} + }; + + if ($.browser.mozilla) + { + // Prevent "escape" from taking effect and canceling a comet connection; + // doesn't work if focus is on an iframe. + $(window).bind("keydown", function(evt) + { + if (evt.which == 27) + { + evt.preventDefault() + } + }); + } + + editor.setProperty("userAuthor", userId); + editor.setBaseAttributedText(serverVars.initialAttributedText, serverVars.apool); + editor.setUserChangeNotificationCallback(wrapRecordingErrors("handleUserChanges", handleUserChanges)); + + function dmesg(str) + { + if (typeof window.ajlog == "string") window.ajlog += str + '\n'; + debugMessages.push(str); + } + + function handleUserChanges() + { + if ((!getSocket()) || channelState == "CONNECTING") + { + if (channelState == "CONNECTING" && (((+new Date()) - initialStartConnectTime) > 20000)) + { + setChannelState("DISCONNECTED", "initsocketfail"); + } + else + { + // check again in a bit + setTimeout(wrapRecordingErrors("setTimeout(handleUserChanges)", handleUserChanges), 1000); + } + return; + } + + var t = (+new Date()); + + if (state != "IDLE") + { + if (state == "COMMITTING" && (t - lastCommitTime) > 20000) + { + // a commit is taking too long + setChannelState("DISCONNECTED", "slowcommit"); + } + else if (state == "COMMITTING" && (t - lastCommitTime) > 5000) + { + callbacks.onConnectionTrouble("SLOW"); + } + else + { + // run again in a few seconds, to detect a disconnect + setTimeout(wrapRecordingErrors("setTimeout(handleUserChanges)", handleUserChanges), 3000); + } + return; + } + + var earliestCommit = lastCommitTime + 500; + if (t < earliestCommit) + { + setTimeout(wrapRecordingErrors("setTimeout(handleUserChanges)", handleUserChanges), earliestCommit - t); + return; + } + + var sentMessage = false; + var userChangesData = editor.prepareUserChangeset(); + if (userChangesData.changeset) + { + lastCommitTime = t; + state = "COMMITTING"; + stateMessage = { + type: "USER_CHANGES", + baseRev: rev, + changeset: userChangesData.changeset, + apool: userChangesData.apool + }; + stateMessageSocketId = socketId; + sendMessage(stateMessage); + sentMessage = true; + callbacks.onInternalAction("commitPerformed"); + } + + if (sentMessage) + { + // run again in a few seconds, to detect a disconnect + setTimeout(wrapRecordingErrors("setTimeout(handleUserChanges)", handleUserChanges), 3000); + } + } + + function getStats() + { + var stats = {}; + + stats.screen = [$(window).width(), $(window).height(), window.screen.availWidth, window.screen.availHeight, window.screen.width, window.screen.height].join(','); + stats.ip = serverVars.clientIp; + stats.useragent = serverVars.clientAgent; + + return stats; + } + + function setUpSocket() + { + //oldSocketId = String(Math.floor(Math.random()*1e12)); + //socketId = String(Math.floor(Math.random()*1e12)); +/*socket = new io.Socket(); + socket.connect();*/ + + //socket.on('connect', function(){ + hiccupCount = 0; + setChannelState("CONNECTED"); +/*var msg = { type:"CLIENT_READY", roomType:'padpage', + roomName:'padpage/'+globalPadId, + data: { + lastRev:rev, + userInfo:userSet[userId], + stats: getStats() } }; + if (oldSocketId) { + msg.data.isReconnectOf = oldSocketId; + msg.data.isCommitPending = (state == "COMMITTING"); + } + sendMessage(msg);*/ + doDeferredActions(); + + initialStartConnectTime = +new Date(); + // }); +/*socket.on('message', function(obj){ + if(window.console) + console.log(obj); + handleMessageFromServer(obj); + });*/ + +/*var success = false; + callCatchingErrors("setUpSocket", function() { + appLevelDisconnectReason = null; + + var oldSocketId = socketId; + socketId = String(Math.floor(Math.random()*1e12)); + socket = new WebSocket(socketId); + socket.onmessage = wrapRecordingErrors("socket.onmessage", handleMessageFromServer); + socket.onclosed = wrapRecordingErrors("socket.onclosed", handleSocketClosed); + socket.onopen = wrapRecordingErrors("socket.onopen", function() { + hiccupCount = 0; + setChannelState("CONNECTED"); + var msg = { type:"CLIENT_READY", roomType:'padpage', + roomName:'padpage/'+globalPadId, + data: { + lastRev:rev, + userInfo:userSet[userId], + stats: getStats() } }; + if (oldSocketId) { + msg.data.isReconnectOf = oldSocketId; + msg.data.isCommitPending = (state == "COMMITTING"); + } + sendMessage(msg); + doDeferredActions(); + }); + socket.onhiccup = wrapRecordingErrors("socket.onhiccup", handleCometHiccup); + socket.onlogmessage = dmesg; + socket.connect(); + success = true; + }); + if (success) { + initialStartConnectTime = +new Date(); + } + else { + abandonConnection("initsocketfail"); + }*/ + } + + var hiccupCount = 0; + + function handleCometHiccup(params) + { + dmesg("HICCUP (connected:" + ( !! params.connected) + ")"); + var connectedNow = params.connected; + if (!connectedNow) + { + hiccupCount++; + // skip first "cut off from server" notification + if (hiccupCount > 1) + { + setChannelState("RECONNECTING"); + } + } + else + { + hiccupCount = 0; + setChannelState("CONNECTED"); + } + } + + function sendMessage(msg) + { + getSocket().json.send( + { + type: "COLLABROOM", + component: "pad", + data: msg + }); + } + + function wrapRecordingErrors(catcher, func) + { + return function() + { + try + { + return func.apply(this, Array.prototype.slice.call(arguments)); + } + catch (e) + { + caughtErrors.push(e); + caughtErrorCatchers.push(catcher); + caughtErrorTimes.push(+new Date()); + //console.dir({catcher: catcher, e: e}); + throw e; + } + }; + } + + function callCatchingErrors(catcher, func) + { + try + { + wrapRecordingErrors(catcher, func)(); + } + catch (e) + { /*absorb*/ + } + } + + function handleMessageFromServer(evt) + { + if (window.console) console.log(evt); + + if (!getSocket()) return; + if (!evt.data) return; + var wrapper = evt; + if (wrapper.type != "COLLABROOM") return; + var msg = wrapper.data; + if (msg.type == "NEW_CHANGES") + { + var newRev = msg.newRev; + var changeset = msg.changeset; + var author = (msg.author || ''); + var apool = msg.apool; + if (newRev != (rev + 1)) + { + dmesg("bad message revision on NEW_CHANGES: " + newRev + " not " + (rev + 1)); + setChannelState("DISCONNECTED", "badmessage_newchanges"); + return; + } + rev = newRev; + editor.applyChangesToBase(changeset, author, apool); + } + else if (msg.type == "ACCEPT_COMMIT") + { + var newRev = msg.newRev; + if (newRev != (rev + 1)) + { + dmesg("bad message revision on ACCEPT_COMMIT: " + newRev + " not " + (rev + 1)); + setChannelState("DISCONNECTED", "badmessage_acceptcommit"); + return; + } + rev = newRev; + editor.applyPreparedChangesetToBase(); + setStateIdle(); + callCatchingErrors("onInternalAction", function() + { + callbacks.onInternalAction("commitAcceptedByServer"); + }); + callCatchingErrors("onConnectionTrouble", function() + { + callbacks.onConnectionTrouble("OK"); + }); + handleUserChanges(); + } + else if (msg.type == "NO_COMMIT_PENDING") + { + if (state == "COMMITTING") + { + // server missed our commit message; abort that commit + setStateIdle(); + handleUserChanges(); + } + } + else if (msg.type == "USER_NEWINFO") + { + var userInfo = msg.userInfo; + var id = userInfo.userId; + + if (userSet[id]) + { + userSet[id] = userInfo; + callbacks.onUpdateUserInfo(userInfo); + dmesgUsers(); + } + else + { + userSet[id] = userInfo; + callbacks.onUserJoin(userInfo); + dmesgUsers(); + } + tellAceActiveAuthorInfo(userInfo); + } + else if (msg.type == "USER_LEAVE") + { + var userInfo = msg.userInfo; + var id = userInfo.userId; + if (userSet[id]) + { + delete userSet[userInfo.userId]; + fadeAceAuthorInfo(userInfo); + callbacks.onUserLeave(userInfo); + dmesgUsers(); + } + } + else if (msg.type == "DISCONNECT_REASON") + { + appLevelDisconnectReason = msg.reason; + } + else if (msg.type == "CLIENT_MESSAGE") + { + callbacks.onClientMessage(msg.payload); + } + else if (msg.type == "CHAT_MESSAGE") + { + chat.addMessage(msg, true); + } + else if (msg.type == "SERVER_MESSAGE") + { + callbacks.onServerMessage(msg.payload); + } + } + + function updateUserInfo(userInfo) + { + userInfo.userId = userId; + userSet[userId] = userInfo; + tellAceActiveAuthorInfo(userInfo); + if (!getSocket()) return; + sendMessage( + { + type: "USERINFO_UPDATE", + userInfo: userInfo + }); + } + + function tellAceActiveAuthorInfo(userInfo) + { + tellAceAuthorInfo(userInfo.userId, userInfo.colorId); + } + + function tellAceAuthorInfo(userId, colorId, inactive) + { + if(typeof colorId == "number") + { + colorId = clientVars.colorPalette[colorId]; + } + + var cssColor = colorId; + if (inactive) + { + editor.setAuthorInfo(userId, { + bgcolor: cssColor, + fade: 0.5 + }); + } + else + { + editor.setAuthorInfo(userId, { + bgcolor: cssColor + }); + } + } + + function fadeAceAuthorInfo(userInfo) + { + tellAceAuthorInfo(userInfo.userId, userInfo.colorId, true); + } + + function getConnectedUsers() + { + return valuesArray(userSet); + } + + function tellAceAboutHistoricalAuthors(hadata) + { + for (var author in hadata) + { + var data = hadata[author]; + if (!userSet[author]) + { + tellAceAuthorInfo(author, data.colorId, true); + } + } + } + + function dmesgUsers() + { + //pad.dmesg($.map(getConnectedUsers(), function(u) { return u.userId.slice(-2); }).join(',')); + } + + function setChannelState(newChannelState, moreInfo) + { + if (newChannelState != channelState) + { + channelState = newChannelState; + callbacks.onChannelStateChange(channelState, moreInfo); + } + } + + function keys(obj) + { + var array = []; + $.each(obj, function(k, v) + { + array.push(k); + }); + return array; + } + + function valuesArray(obj) + { + var array = []; + $.each(obj, function(k, v) + { + array.push(v); + }); + return array; + } + + // We need to present a working interface even before the socket + // is connected for the first time. + var deferredActions = []; + + function defer(func, tag) + { + return function() + { + var that = this; + var args = arguments; + + function action() + { + func.apply(that, args); + } + action.tag = tag; + if (channelState == "CONNECTING") + { + deferredActions.push(action); + } + else + { + action(); + } + } + } + + function doDeferredActions(tag) + { + var newArray = []; + for (var i = 0; i < deferredActions.length; i++) + { + var a = deferredActions[i]; + if ((!tag) || (tag == a.tag)) + { + a(); + } + else + { + newArray.push(a); + } + } + deferredActions = newArray; + } + + function sendClientMessage(msg) + { + sendMessage( + { + type: "CLIENT_MESSAGE", + payload: msg + }); + } + + function getCurrentRevisionNumber() + { + return rev; + } + + function getMissedChanges() + { + var obj = {}; + obj.userInfo = userSet[userId]; + obj.baseRev = rev; + if (state == "COMMITTING" && stateMessage) + { + obj.committedChangeset = stateMessage.changeset; + obj.committedChangesetAPool = stateMessage.apool; + obj.committedChangesetSocketId = stateMessageSocketId; + editor.applyPreparedChangesetToBase(); + } + var userChangesData = editor.prepareUserChangeset(); + if (userChangesData.changeset) + { + obj.furtherChangeset = userChangesData.changeset; + obj.furtherChangesetAPool = userChangesData.apool; + } + return obj; + } + + function setStateIdle() + { + state = "IDLE"; + callbacks.onInternalAction("newlyIdle"); + schedulePerhapsCallIdleFuncs(); + } + + function callWhenNotCommitting(func) + { + idleFuncs.push(func); + schedulePerhapsCallIdleFuncs(); + } + + var idleFuncs = []; + + function schedulePerhapsCallIdleFuncs() + { + setTimeout(function() + { + if (state == "IDLE") + { + while (idleFuncs.length > 0) + { + var f = idleFuncs.shift(); + f(); + } + } + }, 0); + } + + var self = { + setOnUserJoin: function(cb) + { + callbacks.onUserJoin = cb; + }, + setOnUserLeave: function(cb) + { + callbacks.onUserLeave = cb; + }, + setOnUpdateUserInfo: function(cb) + { + callbacks.onUpdateUserInfo = cb; + }, + setOnChannelStateChange: function(cb) + { + callbacks.onChannelStateChange = cb; + }, + setOnClientMessage: function(cb) + { + callbacks.onClientMessage = cb; + }, + setOnInternalAction: function(cb) + { + callbacks.onInternalAction = cb; + }, + setOnConnectionTrouble: function(cb) + { + callbacks.onConnectionTrouble = cb; + }, + setOnServerMessage: function(cb) + { + callbacks.onServerMessage = cb; + }, + updateUserInfo: defer(updateUserInfo), + handleMessageFromServer: handleMessageFromServer, + getConnectedUsers: getConnectedUsers, + sendClientMessage: sendClientMessage, + sendMessage: sendMessage, + getCurrentRevisionNumber: getCurrentRevisionNumber, + getMissedChanges: getMissedChanges, + callWhenNotCommitting: callWhenNotCommitting, + addHistoricalAuthors: tellAceAboutHistoricalAuthors, + setChannelState: setChannelState + }; + + $(document).ready(setUpSocket); + return self; +} + +function selectElementContents(elem) +{ + if ($.browser.msie) + { + var range = document.body.createTextRange(); + range.moveToElementText(elem); + range.select(); + } + else + { + if (window.getSelection) + { + var browserSelection = window.getSelection(); + if (browserSelection) + { + var range = document.createRange(); + range.selectNodeContents(elem); + browserSelection.removeAllRanges(); + browserSelection.addRange(range); + } + } + } +} + +exports.getCollabClient = getCollabClient; +exports.selectElementContents = selectElementContents; diff --git a/src/static/js/colorutils.js b/src/static/js/colorutils.js new file mode 100644 index 00000000..5fbefb4d --- /dev/null +++ b/src/static/js/colorutils.js @@ -0,0 +1,138 @@ +/** + * This code is mostly from the old Etherpad. Please help us to comment this code. + * This helps other people to understand this code better and helps them to improve it. + * TL;DR COMMENTS ON THIS FILE ARE HIGHLY APPRECIATED + */ + +// DO NOT EDIT THIS FILE, edit infrastructure/ace/www/colorutils.js +// THIS FILE IS ALSO SERVED AS CLIENT-SIDE JS +/** + * 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. + */ + +var colorutils = {}; + +// "#ffffff" or "#fff" or "ffffff" or "fff" to [1.0, 1.0, 1.0] +colorutils.css2triple = function(cssColor) +{ + var sixHex = colorutils.css2sixhex(cssColor); + + function hexToFloat(hh) + { + return Number("0x" + hh) / 255; + } + return [hexToFloat(sixHex.substr(0, 2)), hexToFloat(sixHex.substr(2, 2)), hexToFloat(sixHex.substr(4, 2))]; +} + +// "#ffffff" or "#fff" or "ffffff" or "fff" to "ffffff" +colorutils.css2sixhex = function(cssColor) +{ + var h = /[0-9a-fA-F]+/.exec(cssColor)[0]; + if (h.length != 6) + { + var a = h.charAt(0); + var b = h.charAt(1); + var c = h.charAt(2); + h = a + a + b + b + c + c; + } + return h; +} + +// [1.0, 1.0, 1.0] -> "#ffffff" +colorutils.triple2css = function(triple) +{ + function floatToHex(n) + { + var n2 = colorutils.clamp(Math.round(n * 255), 0, 255); + return ("0" + n2.toString(16)).slice(-2); + } + return "#" + floatToHex(triple[0]) + floatToHex(triple[1]) + floatToHex(triple[2]); +} + + +colorutils.clamp = function(v, bot, top) +{ + return v < bot ? bot : (v > top ? top : v); +}; +colorutils.min3 = function(a, b, c) +{ + return (a < b) ? (a < c ? a : c) : (b < c ? b : c); +}; +colorutils.max3 = function(a, b, c) +{ + return (a > b) ? (a > c ? a : c) : (b > c ? b : c); +}; +colorutils.colorMin = function(c) +{ + return colorutils.min3(c[0], c[1], c[2]); +}; +colorutils.colorMax = function(c) +{ + return colorutils.max3(c[0], c[1], c[2]); +}; +colorutils.scale = function(v, bot, top) +{ + return colorutils.clamp(bot + v * (top - bot), 0, 1); +}; +colorutils.unscale = function(v, bot, top) +{ + return colorutils.clamp((v - bot) / (top - bot), 0, 1); +}; + +colorutils.scaleColor = function(c, bot, top) +{ + return [colorutils.scale(c[0], bot, top), colorutils.scale(c[1], bot, top), colorutils.scale(c[2], bot, top)]; +} + +colorutils.unscaleColor = function(c, bot, top) +{ + return [colorutils.unscale(c[0], bot, top), colorutils.unscale(c[1], bot, top), colorutils.unscale(c[2], bot, top)]; +} + +colorutils.luminosity = function(c) +{ + // rule of thumb for RGB brightness; 1.0 is white + return c[0] * 0.30 + c[1] * 0.59 + c[2] * 0.11; +} + +colorutils.saturate = function(c) +{ + var min = colorutils.colorMin(c); + var max = colorutils.colorMax(c); + if (max - min <= 0) return [1.0, 1.0, 1.0]; + return colorutils.unscaleColor(c, min, max); +} + +colorutils.blend = function(c1, c2, t) +{ + return [colorutils.scale(t, c1[0], c2[0]), colorutils.scale(t, c1[1], c2[1]), colorutils.scale(t, c1[2], c2[2])]; +} + +colorutils.invert = function(c) +{ + return [1 - c[0], 1 - c[1], 1- c[2]]; +} + +colorutils.complementary = function(c) +{ + var inv = colorutils.invert(c); + return [ + (inv[0] >= c[0]) ? Math.min(inv[0] * 1.30, 1) : (c[0] * 0.30), + (inv[1] >= c[1]) ? Math.min(inv[1] * 1.59, 1) : (c[1] * 0.59), + (inv[2] >= c[2]) ? Math.min(inv[2] * 1.11, 1) : (c[2] * 0.11) + ]; +} + +exports.colorutils = colorutils; diff --git a/src/static/js/contentcollector.js b/src/static/js/contentcollector.js new file mode 100644 index 00000000..e04fbb43 --- /dev/null +++ b/src/static/js/contentcollector.js @@ -0,0 +1,690 @@ +/** + * This code is mostly from the old Etherpad. Please help us to comment this code. + * This helps other people to understand this code better and helps them to improve it. + * TL;DR COMMENTS ON THIS FILE ARE HIGHLY APPRECIATED + */ + +// THIS FILE IS ALSO AN APPJET MODULE: etherpad.collab.ace.contentcollector +// %APPJET%: import("etherpad.collab.ace.easysync2.Changeset"); +// %APPJET%: import("etherpad.admin.plugins"); +/** + * 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. + */ + +var _MAX_LIST_LEVEL = 8; + +var Changeset = require('./Changeset'); +var hooks = require('./pluginfw/hooks'); + +function sanitizeUnicode(s) +{ + return s.replace(/[\uffff\ufffe\ufeff\ufdd0-\ufdef\ud800-\udfff]/g, '?'); +} + +function makeContentCollector(collectStyles, browser, apool, domInterface, className2Author) +{ + browser = browser || {}; + + var dom = domInterface || { + isNodeText: function(n) + { + return (n.nodeType == 3); + }, + nodeTagName: function(n) + { + return n.tagName; + }, + nodeValue: function(n) + { + return n.nodeValue; + }, + nodeNumChildren: function(n) + { + return n.childNodes.length; + }, + nodeChild: function(n, i) + { + return n.childNodes.item(i); + }, + nodeProp: function(n, p) + { + return n[p]; + }, + nodeAttr: function(n, a) + { + return n.getAttribute(a); + }, + optNodeInnerHTML: function(n) + { + return n.innerHTML; + } + }; + + var _blockElems = { + "div": 1, + "p": 1, + "pre": 1, + "li": 1 + }; + + function isBlockElement(n) + { + return !!_blockElems[(dom.nodeTagName(n) || "").toLowerCase()]; + } + + function textify(str) + { + return sanitizeUnicode( + str.replace(/[\n\r ]/g, ' ').replace(/\xa0/g, ' ').replace(/\t/g, ' ')); + } + + function getAssoc(node, name) + { + return dom.nodeProp(node, "_magicdom_" + name); + } + + var lines = (function() + { + var textArray = []; + var attribsArray = []; + var attribsBuilder = null; + var op = Changeset.newOp('+'); + var self = { + length: function() + { + return textArray.length; + }, + atColumnZero: function() + { + return textArray[textArray.length - 1] === ""; + }, + startNew: function() + { + textArray.push(""); + self.flush(true); + attribsBuilder = Changeset.smartOpAssembler(); + }, + textOfLine: function(i) + { + return textArray[i]; + }, + appendText: function(txt, attrString) + { + textArray[textArray.length - 1] += txt; + //dmesg(txt+" / "+attrString); + op.attribs = attrString; + op.chars = txt.length; + attribsBuilder.append(op); + }, + textLines: function() + { + return textArray.slice(); + }, + attribLines: function() + { + return attribsArray; + }, + // call flush only when you're done + flush: function(withNewline) + { + if (attribsBuilder) + { + attribsArray.push(attribsBuilder.toString()); + attribsBuilder = null; + } + } + }; + self.startNew(); + return self; + }()); + var cc = {}; + + function _ensureColumnZero(state) + { + if (!lines.atColumnZero()) + { + cc.startNewLine(state); + } + } + var selection, startPoint, endPoint; + var selStart = [-1, -1], + selEnd = [-1, -1]; + var blockElems = { + "div": 1, + "p": 1, + "pre": 1 + }; + + function _isEmpty(node, state) + { + // consider clean blank lines pasted in IE to be empty + if (dom.nodeNumChildren(node) == 0) return true; + if (dom.nodeNumChildren(node) == 1 && getAssoc(node, "shouldBeEmpty") && dom.optNodeInnerHTML(node) == " " && !getAssoc(node, "unpasted")) + { + if (state) + { + var child = dom.nodeChild(node, 0); + _reachPoint(child, 0, state); + _reachPoint(child, 1, state); + } + return true; + } + return false; + } + + function _pointHere(charsAfter, state) + { + var ln = lines.length() - 1; + var chr = lines.textOfLine(ln).length; + if (chr == 0 && state.listType && state.listType != 'none') + { + chr += 1; // listMarker + } + chr += charsAfter; + return [ln, chr]; + } + + function _reachBlockPoint(nd, idx, state) + { + if (!dom.isNodeText(nd)) _reachPoint(nd, idx, state); + } + + function _reachPoint(nd, idx, state) + { + if (startPoint && nd == startPoint.node && startPoint.index == idx) + { + selStart = _pointHere(0, state); + } + if (endPoint && nd == endPoint.node && endPoint.index == idx) + { + selEnd = _pointHere(0, state); + } + } + cc.incrementFlag = function(state, flagName) + { + state.flags[flagName] = (state.flags[flagName] || 0) + 1; + } + cc.decrementFlag = function(state, flagName) + { + state.flags[flagName]--; + } + cc.incrementAttrib = function(state, attribName) + { + if (!state.attribs[attribName]) + { + state.attribs[attribName] = 1; + } + else + { + state.attribs[attribName]++; + } + _recalcAttribString(state); + } + cc.decrementAttrib = function(state, attribName) + { + state.attribs[attribName]--; + _recalcAttribString(state); + } + + function _enterList(state, listType) + { + var oldListType = state.listType; + state.listLevel = (state.listLevel || 0) + 1; + if (listType != 'none') + { + state.listNesting = (state.listNesting || 0) + 1; + } + state.listType = listType; + _recalcAttribString(state); + return oldListType; + } + + function _exitList(state, oldListType) + { + state.listLevel--; + if (state.listType != 'none') + { + state.listNesting--; + } + state.listType = oldListType; + _recalcAttribString(state); + } + + function _enterAuthor(state, author) + { + var oldAuthor = state.author; + state.authorLevel = (state.authorLevel || 0) + 1; + state.author = author; + _recalcAttribString(state); + return oldAuthor; + } + + function _exitAuthor(state, oldAuthor) + { + state.authorLevel--; + state.author = oldAuthor; + _recalcAttribString(state); + } + + function _recalcAttribString(state) + { + var lst = []; + for (var a in state.attribs) + { + if (state.attribs[a]) + { + lst.push([a, 'true']); + } + } + if (state.authorLevel > 0) + { + var authorAttrib = ['author', state.author]; + if (apool.putAttrib(authorAttrib, true) >= 0) + { + // require that author already be in pool + // (don't add authors from other documents, etc.) + lst.push(authorAttrib); + } + } + state.attribString = Changeset.makeAttribsString('+', lst, apool); + } + + function _produceListMarker(state) + { + lines.appendText('*', Changeset.makeAttribsString('+', [ + ['list', state.listType], + ['insertorder', 'first'] + ], apool)); + } + cc.startNewLine = function(state) + { + if (state) + { + var atBeginningOfLine = lines.textOfLine(lines.length() - 1).length == 0; + if (atBeginningOfLine && state.listType && state.listType != 'none') + { + _produceListMarker(state); + } + } + lines.startNew(); + } + cc.notifySelection = function(sel) + { + if (sel) + { + selection = sel; + startPoint = selection.startPoint; + endPoint = selection.endPoint; + } + }; + cc.doAttrib = function(state, na) + { + state.localAttribs = (state.localAttribs || []); + state.localAttribs.push(na); + cc.incrementAttrib(state, na); + }; + cc.collectContent = function(node, state) + { + if (!state) + { + state = { + flags: { /*name -> nesting counter*/ + }, + localAttribs: null, + attribs: { /*name -> nesting counter*/ + }, + attribString: '' + }; + } + var localAttribs = state.localAttribs; + state.localAttribs = null; + var isBlock = isBlockElement(node); + var isEmpty = _isEmpty(node, state); + if (isBlock) _ensureColumnZero(state); + var startLine = lines.length() - 1; + _reachBlockPoint(node, 0, state); + if (dom.isNodeText(node)) + { + var txt = dom.nodeValue(node); + var rest = ''; + var x = 0; // offset into original text + if (txt.length == 0) + { + if (startPoint && node == startPoint.node) + { + selStart = _pointHere(0, state); + } + if (endPoint && node == endPoint.node) + { + selEnd = _pointHere(0, state); + } + } + while (txt.length > 0) + { + var consumed = 0; + if (state.flags.preMode) + { + var firstLine = txt.split('\n', 1)[0]; + consumed = firstLine.length + 1; + rest = txt.substring(consumed); + txt = firstLine; + } + else + { /* will only run this loop body once */ + } + if (startPoint && node == startPoint.node && startPoint.index - x <= txt.length) + { + selStart = _pointHere(startPoint.index - x, state); + } + if (endPoint && node == endPoint.node && endPoint.index - x <= txt.length) + { + selEnd = _pointHere(endPoint.index - x, state); + } + var txt2 = txt; + if ((!state.flags.preMode) && /^[\r\n]*$/.exec(txt)) + { + // prevents textnodes containing just "\n" from being significant + // in safari when pasting text, now that we convert them to + // spaces instead of removing them, because in other cases + // removing "\n" from pasted HTML will collapse words together. + txt2 = ""; + } + var atBeginningOfLine = lines.textOfLine(lines.length() - 1).length == 0; + if (atBeginningOfLine) + { + // newlines in the source mustn't become spaces at beginning of line box + txt2 = txt2.replace(/^\n*/, ''); + } + if (atBeginningOfLine && state.listType && state.listType != 'none') + { + _produceListMarker(state); + } + lines.appendText(textify(txt2), state.attribString); + x += consumed; + txt = rest; + if (txt.length > 0) + { + cc.startNewLine(state); + } + } + } + else + { + var tname = (dom.nodeTagName(node) || "").toLowerCase(); + if (tname == "br") + { + cc.startNewLine(state); + } + else if (tname == "script" || tname == "style") + { + // ignore + } + else if (!isEmpty) + { + var styl = dom.nodeAttr(node, "style"); + var cls = dom.nodeProp(node, "className"); + + var isPre = (tname == "pre"); + if ((!isPre) && browser.safari) + { + isPre = (styl && /\bwhite-space:\s*pre\b/i.exec(styl)); + } + if (isPre) cc.incrementFlag(state, 'preMode'); + var oldListTypeOrNull = null; + var oldAuthorOrNull = null; + if (collectStyles) + { + hooks.callAll('collectContentPre', { + cc: cc, + state: state, + tname: tname, + styl: styl, + cls: cls + }); + if (tname == "b" || (styl && /\bfont-weight:\s*bold\b/i.exec(styl)) || tname == "strong") + { + cc.doAttrib(state, "bold"); + } + if (tname == "i" || (styl && /\bfont-style:\s*italic\b/i.exec(styl)) || tname == "em") + { + cc.doAttrib(state, "italic"); + } + if (tname == "u" || (styl && /\btext-decoration:\s*underline\b/i.exec(styl)) || tname == "ins") + { + cc.doAttrib(state, "underline"); + } + if (tname == "s" || (styl && /\btext-decoration:\s*line-through\b/i.exec(styl)) || tname == "del") + { + cc.doAttrib(state, "strikethrough"); + } + if (tname == "ul" || tname == "ol") + { + var type; + var rr = cls && /(?:^| )list-([a-z]+[12345678])\b/.exec(cls); + type = rr && rr[1] || "bullet" + String(Math.min(_MAX_LIST_LEVEL, (state.listNesting || 0) + 1)); + oldListTypeOrNull = (_enterList(state, type) || 'none'); + } + else if ((tname == "div" || tname == "p") && cls && cls.match(/(?:^| )ace-line\b/)) + { + oldListTypeOrNull = (_enterList(state, type) || 'none'); + } + if (className2Author && cls) + { + var classes = cls.match(/\S+/g); + if (classes && classes.length > 0) + { + for (var i = 0; i < classes.length; i++) + { + var c = classes[i]; + var a = className2Author(c); + if (a) + { + oldAuthorOrNull = (_enterAuthor(state, a) || 'none'); + break; + } + } + } + } + } + + var nc = dom.nodeNumChildren(node); + for (var i = 0; i < nc; i++) + { + var c = dom.nodeChild(node, i); + cc.collectContent(c, state); + } + + if (collectStyles) + { + hooks.callAll('collectContentPost', { + cc: cc, + state: state, + tname: tname, + styl: styl, + cls: cls + }); + } + + if (isPre) cc.decrementFlag(state, 'preMode'); + if (state.localAttribs) + { + for (var i = 0; i < state.localAttribs.length; i++) + { + cc.decrementAttrib(state, state.localAttribs[i]); + } + } + if (oldListTypeOrNull) + { + _exitList(state, oldListTypeOrNull); + } + if (oldAuthorOrNull) + { + _exitAuthor(state, oldAuthorOrNull); + } + } + } + if (!browser.msie) + { + _reachBlockPoint(node, 1, state); + } + if (isBlock) + { + if (lines.length() - 1 == startLine) + { + cc.startNewLine(state); + } + else + { + _ensureColumnZero(state); + } + } + + if (browser.msie) + { + // in IE, a point immediately after a DIV appears on the next line + _reachBlockPoint(node, 1, state); + } + + state.localAttribs = localAttribs; + }; + // can pass a falsy value for end of doc + cc.notifyNextNode = function(node) + { + // an "empty block" won't end a line; this addresses an issue in IE with + // typing into a blank line at the end of the document. typed text + // goes into the body, and the empty line div still looks clean. + // it is incorporated as dirty by the rule that a dirty region has + // to end a line. + if ((!node) || (isBlockElement(node) && !_isEmpty(node))) + { + _ensureColumnZero(null); + } + }; + // each returns [line, char] or [-1,-1] + var getSelectionStart = function() + { + return selStart; + }; + var getSelectionEnd = function() + { + return selEnd; + }; + + // returns array of strings for lines found, last entry will be "" if + // last line is complete (i.e. if a following span should be on a new line). + // can be called at any point + cc.getLines = function() + { + return lines.textLines(); + }; + + cc.finish = function() + { + lines.flush(); + var lineAttribs = lines.attribLines(); + var lineStrings = cc.getLines(); + + lineStrings.length--; + lineAttribs.length--; + + var ss = getSelectionStart(); + var se = getSelectionEnd(); + + function fixLongLines() + { + // design mode does not deal with with really long lines! + var lineLimit = 2000; // chars + var buffer = 10; // chars allowed over before wrapping + var linesWrapped = 0; + var numLinesAfter = 0; + for (var i = lineStrings.length - 1; i >= 0; i--) + { + var oldString = lineStrings[i]; + var oldAttribString = lineAttribs[i]; + if (oldString.length > lineLimit + buffer) + { + var newStrings = []; + var newAttribStrings = []; + while (oldString.length > lineLimit) + { + //var semiloc = oldString.lastIndexOf(';', lineLimit-1); + //var lengthToTake = (semiloc >= 0 ? (semiloc+1) : lineLimit); + lengthToTake = lineLimit; + newStrings.push(oldString.substring(0, lengthToTake)); + oldString = oldString.substring(lengthToTake); + newAttribStrings.push(Changeset.subattribution(oldAttribString, 0, lengthToTake)); + oldAttribString = Changeset.subattribution(oldAttribString, lengthToTake); + } + if (oldString.length > 0) + { + newStrings.push(oldString); + newAttribStrings.push(oldAttribString); + } + + function fixLineNumber(lineChar) + { + if (lineChar[0] < 0) return; + var n = lineChar[0]; + var c = lineChar[1]; + if (n > i) + { + n += (newStrings.length - 1); + } + else if (n == i) + { + var a = 0; + while (c > newStrings[a].length) + { + c -= newStrings[a].length; + a++; + } + n += a; + } + lineChar[0] = n; + lineChar[1] = c; + } + fixLineNumber(ss); + fixLineNumber(se); + linesWrapped++; + numLinesAfter += newStrings.length; + + newStrings.unshift(i, 1); + lineStrings.splice.apply(lineStrings, newStrings); + newAttribStrings.unshift(i, 1); + lineAttribs.splice.apply(lineAttribs, newAttribStrings); + } + } + return { + linesWrapped: linesWrapped, + numLinesAfter: numLinesAfter + }; + } + var wrapData = fixLongLines(); + + return { + selStart: ss, + selEnd: se, + linesWrapped: wrapData.linesWrapped, + numLinesAfter: wrapData.numLinesAfter, + lines: lineStrings, + lineAttribs: lineAttribs + }; + } + + return cc; +} + +exports.sanitizeUnicode = sanitizeUnicode; +exports.makeContentCollector = makeContentCollector; diff --git a/src/static/js/cssmanager.js b/src/static/js/cssmanager.js new file mode 100644 index 00000000..46075e57 --- /dev/null +++ b/src/static/js/cssmanager.js @@ -0,0 +1,122 @@ +/** + * This code is mostly from the old Etherpad. Please help us to comment this code. + * This helps other people to understand this code better and helps them to improve it. + * TL;DR COMMENTS ON THIS FILE ARE HIGHLY APPRECIATED + */ + +/** + * 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. + */ + +function makeCSSManager(emptyStylesheetTitle, top) +{ + + function getSheetByTitle(title, top) + { + if(top) + var allSheets = window.parent.parent.document.styleSheets; + else + var allSheets = document.styleSheets; + + for (var i = 0; i < allSheets.length; i++) + { + var s = allSheets[i]; + if (s.title == title) + { + return s; + } + } + return null; + } + +/*function getSheetTagByTitle(title) { + var allStyleTags = document.getElementsByTagName("style"); + for(var i=0;i<allStyleTags.length;i++) { + var t = allStyleTags[i]; + if (t.title == title) { + return t; + } + } + return null; + }*/ + + var browserSheet = getSheetByTitle(emptyStylesheetTitle, top); + //var browserTag = getSheetTagByTitle(emptyStylesheetTitle); + + + function browserRules() + { + return (browserSheet.cssRules || browserSheet.rules); + } + + function browserDeleteRule(i) + { + if (browserSheet.deleteRule) browserSheet.deleteRule(i); + else browserSheet.removeRule(i); + } + + function browserInsertRule(i, selector) + { + if (browserSheet.insertRule) browserSheet.insertRule(selector + ' {}', i); + else browserSheet.addRule(selector, null, i); + } + var selectorList = []; + + function indexOfSelector(selector) + { + for (var i = 0; i < selectorList.length; i++) + { + if (selectorList[i] == selector) + { + return i; + } + } + return -1; + } + + function selectorStyle(selector) + { + var i = indexOfSelector(selector); + if (i < 0) + { + // add selector + browserInsertRule(0, selector); + selectorList.splice(0, 0, selector); + i = 0; + } + return browserRules().item(i).style; + } + + function removeSelectorStyle(selector) + { + var i = indexOfSelector(selector); + if (i >= 0) + { + browserDeleteRule(i); + selectorList.splice(i, 1); + } + } + + return { + selectorStyle: selectorStyle, + removeSelectorStyle: removeSelectorStyle, + info: function() + { + return selectorList.length + ":" + browserRules().length; + } + }; +} + +exports.makeCSSManager = makeCSSManager; diff --git a/src/static/js/domline.js b/src/static/js/domline.js new file mode 100644 index 00000000..d6dbb74a --- /dev/null +++ b/src/static/js/domline.js @@ -0,0 +1,285 @@ +/** + * This code is mostly from the old Etherpad. Please help us to comment this code. + * This helps other people to understand this code better and helps them to improve it. + * TL;DR COMMENTS ON THIS FILE ARE HIGHLY APPRECIATED + */ + +// THIS FILE IS ALSO AN APPJET MODULE: etherpad.collab.ace.domline +// %APPJET%: import("etherpad.admin.plugins"); +/** + * 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. + */ + +// requires: top +// requires: plugins +// requires: undefined + +var Security = require('./security'); +var hooks = require('./pluginfw/hooks'); +var Ace2Common = require('./ace2_common'); +var map = Ace2Common.map; +var noop = Ace2Common.noop; +var identity = Ace2Common.identity; + +var domline = {}; + +domline.addToLineClass = function(lineClass, cls) +{ + // an "empty span" at any point can be used to add classes to + // the line, using line:className. otherwise, we ignore + // the span. + cls.replace(/\S+/g, function(c) + { + if (c.indexOf("line:") == 0) + { + // add class to line + lineClass = (lineClass ? lineClass + ' ' : '') + c.substring(5); + } + }); + return lineClass; +} + +// if "document" is falsy we don't create a DOM node, just +// an object with innerHTML and className +domline.createDomLine = function(nonEmpty, doesWrap, optBrowser, optDocument) +{ + var result = { + node: null, + appendSpan: noop, + prepareForAdd: noop, + notifyAdded: noop, + clearSpans: noop, + finishUpdate: noop, + lineMarker: 0 + }; + + var browser = (optBrowser || {}); + var document = optDocument; + + if (document) + { + result.node = document.createElement("div"); + } + else + { + result.node = { + innerHTML: '', + className: '' + }; + } + + var html = []; + var preHtml, postHtml; + var curHTML = null; + + function processSpaces(s) + { + return domline.processSpaces(s, doesWrap); + } + + var perTextNodeProcess = (doesWrap ? identity : processSpaces); + var perHtmlLineProcess = (doesWrap ? processSpaces : identity); + var lineClass = 'ace-line'; + result.appendSpan = function(txt, cls) + { + if (cls.indexOf('list') >= 0) + { + var listType = /(?:^| )list:(\S+)/.exec(cls); + var start = /(?:^| )start:(\S+)/.exec(cls); + if (listType) + { + listType = listType[1]; + start = start?'start="'+Security.escapeHTMLAttribute(start[1])+'"':''; + if (listType) + { + if(listType.indexOf("number") < 0) + { + preHtml = '<ul class="list-' + Security.escapeHTMLAttribute(listType) + '"><li>'; + postHtml = '</li></ul>'; + } + else + { + preHtml = '<ol '+start+' class="list-' + Security.escapeHTMLAttribute(listType) + '"><li>'; + postHtml = '</li></ol>'; + } + } + result.lineMarker += txt.length; + return; // don't append any text + } + } + var href = null; + var simpleTags = null; + if (cls.indexOf('url') >= 0) + { + cls = cls.replace(/(^| )url:(\S+)/g, function(x0, space, url) + { + href = url; + return space + "url"; + }); + } + if (cls.indexOf('tag') >= 0) + { + cls = cls.replace(/(^| )tag:(\S+)/g, function(x0, space, tag) + { + if (!simpleTags) simpleTags = []; + simpleTags.push(tag.toLowerCase()); + return space + tag; + }); + } + + var extraOpenTags = ""; + var extraCloseTags = ""; + + map(hooks.callAll("aceCreateDomLine", { + domline: domline, + cls: cls + }), function(modifier) + { + cls = modifier.cls; + extraOpenTags = extraOpenTags + modifier.extraOpenTags; + extraCloseTags = modifier.extraCloseTags + extraCloseTags; + }); + + if ((!txt) && cls) + { + lineClass = domline.addToLineClass(lineClass, cls); + } + else if (txt) + { + if (href) + { + if(!~href.indexOf("http")) // if the url doesn't include http or https etc prefix it. + { + href = "http://"+href; + } + extraOpenTags = extraOpenTags + '<a href="' + Security.escapeHTMLAttribute(href) + '">'; + extraCloseTags = '</a>' + extraCloseTags; + } + if (simpleTags) + { + simpleTags.sort(); + extraOpenTags = extraOpenTags + '<' + simpleTags.join('><') + '>'; + simpleTags.reverse(); + extraCloseTags = '</' + simpleTags.join('></') + '>' + extraCloseTags; + } + html.push('<span class="', Security.escapeHTMLAttribute(cls || ''), '">', extraOpenTags, perTextNodeProcess(Security.escapeHTML(txt)), extraCloseTags, '</span>'); + } + }; + result.clearSpans = function() + { + html = []; + lineClass = ''; // non-null to cause update + result.lineMarker = 0; + }; + + function writeHTML() + { + var newHTML = perHtmlLineProcess(html.join('')); + if (!newHTML) + { + if ((!document) || (!optBrowser)) + { + newHTML += ' '; + } + else if (!browser.msie) + { + newHTML += '<br/>'; + } + } + if (nonEmpty) + { + newHTML = (preHtml || '') + newHTML + (postHtml || ''); + } + html = preHtml = postHtml = null; // free memory + if (newHTML !== curHTML) + { + curHTML = newHTML; + result.node.innerHTML = curHTML; + } + if (lineClass !== null) result.node.className = lineClass; + } + result.prepareForAdd = writeHTML; + result.finishUpdate = writeHTML; + result.getInnerHTML = function() + { + return curHTML || ''; + }; + + return result; +}; + +domline.processSpaces = function(s, doesWrap) +{ + if (s.indexOf("<") < 0 && !doesWrap) + { + // short-cut + return s.replace(/ /g, ' '); + } + var parts = []; + s.replace(/<[^>]*>?| |[^ <]+/g, function(m) + { + parts.push(m); + }); + if (doesWrap) + { + var endOfLine = true; + var beforeSpace = false; + // last space in a run is normal, others are nbsp, + // end of line is nbsp + for (var i = parts.length - 1; i >= 0; i--) + { + var p = parts[i]; + if (p == " ") + { + if (endOfLine || beforeSpace) parts[i] = ' '; + endOfLine = false; + beforeSpace = true; + } + else if (p.charAt(0) != "<") + { + endOfLine = false; + beforeSpace = false; + } + } + // beginning of line is nbsp + for (var i = 0; i < parts.length; i++) + { + var p = parts[i]; + if (p == " ") + { + parts[i] = ' '; + break; + } + else if (p.charAt(0) != "<") + { + break; + } + } + } + else + { + for (var i = 0; i < parts.length; i++) + { + var p = parts[i]; + if (p == " ") + { + parts[i] = ' '; + } + } + } + return parts.join(''); +}; + +exports.domline = domline; diff --git a/src/static/js/draggable.js b/src/static/js/draggable.js new file mode 100644 index 00000000..8d197545 --- /dev/null +++ b/src/static/js/draggable.js @@ -0,0 +1,197 @@ +/** + * This code is mostly from the old Etherpad. Please help us to comment this code. + * This helps other people to understand this code better and helps them to improve it. + * TL;DR COMMENTS ON THIS FILE ARE HIGHLY APPRECIATED + */ + +/** + * 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. + */ + +function makeDraggable(jqueryNodes, eventHandler) +{ + jqueryNodes.each(function() + { + var node = $(this); + var state = {}; + var inDrag = false; + + function dragStart(evt) + { + if (inDrag) + { + return; + } + inDrag = true; + if (eventHandler('dragstart', evt, state) !== false) + { + $(document).bind('mousemove', dragUpdate); + $(document).bind('mouseup', dragEnd); + } + evt.preventDefault(); + return false; + } + + function dragUpdate(evt) + { + if (!inDrag) + { + return; + } + eventHandler('dragupdate', evt, state); + evt.preventDefault(); + return false; + } + + function dragEnd(evt) + { + if (!inDrag) + { + return; + } + inDrag = false; + try + { + eventHandler('dragend', evt, state); + } + finally + { + $(document).unbind('mousemove', dragUpdate); + $(document).unbind('mouseup', dragEnd); + evt.preventDefault(); + } + return false; + } + node.bind('mousedown', dragStart); + }); +} + +function makeResizableVPane(top, sep, bottom, minTop, minBottom, callback) +{ + if (minTop === undefined) minTop = 0; + if (minBottom === undefined) minBottom = 0; + + makeDraggable($(sep), function(eType, evt, state) + { + if (eType == 'dragstart') + { + state.startY = evt.pageY; + state.topHeight = $(top).height(); + state.bottomHeight = $(bottom).height(); + state.minTop = minTop; + state.maxTop = (state.topHeight + state.bottomHeight) - minBottom; + } + else if (eType == 'dragupdate') + { + var change = evt.pageY - state.startY; + + var topHeight = state.topHeight + change; + if (topHeight < state.minTop) + { + topHeight = state.minTop; + } + if (topHeight > state.maxTop) + { + topHeight = state.maxTop; + } + change = topHeight - state.topHeight; + + var bottomHeight = state.bottomHeight - change; + var sepHeight = $(sep).height(); + + var totalHeight = topHeight + sepHeight + bottomHeight; + topHeight = 100.0 * topHeight / totalHeight; + sepHeight = 100.0 * sepHeight / totalHeight; + bottomHeight = 100.0 * bottomHeight / totalHeight; + + $(top).css('bottom', 'auto'); + $(top).css('height', topHeight + "%"); + $(sep).css('top', topHeight + "%"); + $(bottom).css('top', (topHeight + sepHeight) + '%'); + $(bottom).css('height', 'auto'); + if (callback) callback(); + } + }); +} + +function makeResizableHPane(left, sep, right, minLeft, minRight, sepWidth, sepOffset, callback) +{ + if (minLeft === undefined) minLeft = 0; + if (minRight === undefined) minRight = 0; + + makeDraggable($(sep), function(eType, evt, state) + { + if (eType == 'dragstart') + { + state.startX = evt.pageX; + state.leftWidth = $(left).width(); + state.rightWidth = $(right).width(); + state.minLeft = minLeft; + state.maxLeft = (state.leftWidth + state.rightWidth) - minRight; + } + else if (eType == 'dragend' || eType == 'dragupdate') + { + var change = evt.pageX - state.startX; + + var leftWidth = state.leftWidth + change; + if (leftWidth < state.minLeft) + { + leftWidth = state.minLeft; + } + if (leftWidth > state.maxLeft) + { + leftWidth = state.maxLeft; + } + change = leftWidth - state.leftWidth; + + var rightWidth = state.rightWidth - change; + newSepWidth = sepWidth; + if (newSepWidth == undefined) newSepWidth = $(sep).width(); + newSepOffset = sepOffset; + if (newSepOffset == undefined) newSepOffset = 0; + + if (change == 0) + { + if (rightWidth != minRight || state.lastRightWidth == undefined) + { + state.lastRightWidth = rightWidth; + rightWidth = minRight; + } + else + { + rightWidth = state.lastRightWidth; + state.lastRightWidth = minRight; + } + change = state.rightWidth - rightWidth; + leftWidth = change + state.leftWidth; + } + + var totalWidth = leftWidth + newSepWidth + rightWidth; + leftWidth = 100.0 * leftWidth / totalWidth; + newSepWidth = 100.0 * newSepWidth / totalWidth; + newSepOffset = 100.0 * newSepOffset / totalWidth; + rightWidth = 100.0 * rightWidth / totalWidth; + + $(left).css('right', 'auto'); + $(left).css('width', leftWidth + "%"); + $(sep).css('left', (leftWidth + newSepOffset) + "%"); + $(right).css('left', (leftWidth + newSepWidth) + '%'); + $(right).css('width', 'auto'); + if (callback) callback(); + } + }); +} + +exports.makeDraggable = makeDraggable; diff --git a/src/static/js/excanvas.js b/src/static/js/excanvas.js new file mode 100644 index 00000000..a34ca1da --- /dev/null +++ b/src/static/js/excanvas.js @@ -0,0 +1,35 @@ +// Copyright 2006 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. +document.createElement("canvas").getContext||(function(){var s=Math,j=s.round,F=s.sin,G=s.cos,V=s.abs,W=s.sqrt,k=10,v=k/2;function X(){return this.context_||(this.context_=new H(this))}var L=Array.prototype.slice;function Y(b,a){var c=L.call(arguments,2);return function(){return b.apply(a,c.concat(L.call(arguments)))}}var M={init:function(b){if(/MSIE/.test(navigator.userAgent)&&!window.opera){var a=b||document;a.createElement("canvas");a.attachEvent("onreadystatechange",Y(this.init_,this,a))}},init_:function(b){b.namespaces.g_vml_|| +b.namespaces.add("g_vml_","urn:schemas-microsoft-com:vml","#default#VML");b.namespaces.g_o_||b.namespaces.add("g_o_","urn:schemas-microsoft-com:office:office","#default#VML");if(!b.styleSheets.ex_canvas_){var a=b.createStyleSheet();a.owningElement.id="ex_canvas_";a.cssText="canvas{display:inline-block;overflow:hidden;text-align:left;width:300px;height:150px}g_vml_\\:*{behavior:url(#default#VML)}g_o_\\:*{behavior:url(#default#VML)}"}var c=b.getElementsByTagName("canvas"),d=0;for(;d<c.length;d++)this.initElement(c[d])}, +initElement:function(b){if(!b.getContext){b.getContext=X;b.innerHTML="";b.attachEvent("onpropertychange",Z);b.attachEvent("onresize",$);var a=b.attributes;if(a.width&&a.width.specified)b.style.width=a.width.nodeValue+"px";else b.width=b.clientWidth;if(a.height&&a.height.specified)b.style.height=a.height.nodeValue+"px";else b.height=b.clientHeight}return b}};function Z(b){var a=b.srcElement;switch(b.propertyName){case "width":a.style.width=a.attributes.width.nodeValue+"px";a.getContext().clearRect(); +break;case "height":a.style.height=a.attributes.height.nodeValue+"px";a.getContext().clearRect();break}}function $(b){var a=b.srcElement;if(a.firstChild){a.firstChild.style.width=a.clientWidth+"px";a.firstChild.style.height=a.clientHeight+"px"}}M.init();var N=[],B=0;for(;B<16;B++){var C=0;for(;C<16;C++)N[B*16+C]=B.toString(16)+C.toString(16)}function I(){return[[1,0,0],[0,1,0],[0,0,1]]}function y(b,a){var c=I(),d=0;for(;d<3;d++){var f=0;for(;f<3;f++){var h=0,g=0;for(;g<3;g++)h+=b[d][g]*a[g][f];c[d][f]= +h}}return c}function O(b,a){a.fillStyle=b.fillStyle;a.lineCap=b.lineCap;a.lineJoin=b.lineJoin;a.lineWidth=b.lineWidth;a.miterLimit=b.miterLimit;a.shadowBlur=b.shadowBlur;a.shadowColor=b.shadowColor;a.shadowOffsetX=b.shadowOffsetX;a.shadowOffsetY=b.shadowOffsetY;a.strokeStyle=b.strokeStyle;a.globalAlpha=b.globalAlpha;a.arcScaleX_=b.arcScaleX_;a.arcScaleY_=b.arcScaleY_;a.lineScale_=b.lineScale_}function P(b){var a,c=1;b=String(b);if(b.substring(0,3)=="rgb"){var d=b.indexOf("(",3),f=b.indexOf(")",d+ +1),h=b.substring(d+1,f).split(",");a="#";var g=0;for(;g<3;g++)a+=N[Number(h[g])];if(h.length==4&&b.substr(3,1)=="a")c=h[3]}else a=b;return{color:a,alpha:c}}function aa(b){switch(b){case "butt":return"flat";case "round":return"round";case "square":default:return"square"}}function H(b){this.m_=I();this.mStack_=[];this.aStack_=[];this.currentPath_=[];this.fillStyle=this.strokeStyle="#000";this.lineWidth=1;this.lineJoin="miter";this.lineCap="butt";this.miterLimit=k*1;this.globalAlpha=1;this.canvas=b; +var a=b.ownerDocument.createElement("div");a.style.width=b.clientWidth+"px";a.style.height=b.clientHeight+"px";a.style.overflow="hidden";a.style.position="absolute";b.appendChild(a);this.element_=a;this.lineScale_=this.arcScaleY_=this.arcScaleX_=1}var i=H.prototype;i.clearRect=function(){this.element_.innerHTML=""};i.beginPath=function(){this.currentPath_=[]};i.moveTo=function(b,a){var c=this.getCoords_(b,a);this.currentPath_.push({type:"moveTo",x:c.x,y:c.y});this.currentX_=c.x;this.currentY_=c.y}; +i.lineTo=function(b,a){var c=this.getCoords_(b,a);this.currentPath_.push({type:"lineTo",x:c.x,y:c.y});this.currentX_=c.x;this.currentY_=c.y};i.bezierCurveTo=function(b,a,c,d,f,h){var g=this.getCoords_(f,h),l=this.getCoords_(b,a),e=this.getCoords_(c,d);Q(this,l,e,g)};function Q(b,a,c,d){b.currentPath_.push({type:"bezierCurveTo",cp1x:a.x,cp1y:a.y,cp2x:c.x,cp2y:c.y,x:d.x,y:d.y});b.currentX_=d.x;b.currentY_=d.y}i.quadraticCurveTo=function(b,a,c,d){var f=this.getCoords_(b,a),h=this.getCoords_(c,d),g={x:this.currentX_+ +0.6666666666666666*(f.x-this.currentX_),y:this.currentY_+0.6666666666666666*(f.y-this.currentY_)};Q(this,g,{x:g.x+(h.x-this.currentX_)/3,y:g.y+(h.y-this.currentY_)/3},h)};i.arc=function(b,a,c,d,f,h){c*=k;var g=h?"at":"wa",l=b+G(d)*c-v,e=a+F(d)*c-v,m=b+G(f)*c-v,r=a+F(f)*c-v;if(l==m&&!h)l+=0.125;var n=this.getCoords_(b,a),o=this.getCoords_(l,e),q=this.getCoords_(m,r);this.currentPath_.push({type:g,x:n.x,y:n.y,radius:c,xStart:o.x,yStart:o.y,xEnd:q.x,yEnd:q.y})};i.rect=function(b,a,c,d){this.moveTo(b, +a);this.lineTo(b+c,a);this.lineTo(b+c,a+d);this.lineTo(b,a+d);this.closePath()};i.strokeRect=function(b,a,c,d){var f=this.currentPath_;this.beginPath();this.moveTo(b,a);this.lineTo(b+c,a);this.lineTo(b+c,a+d);this.lineTo(b,a+d);this.closePath();this.stroke();this.currentPath_=f};i.fillRect=function(b,a,c,d){var f=this.currentPath_;this.beginPath();this.moveTo(b,a);this.lineTo(b+c,a);this.lineTo(b+c,a+d);this.lineTo(b,a+d);this.closePath();this.fill();this.currentPath_=f};i.createLinearGradient=function(b, +a,c,d){var f=new D("gradient");f.x0_=b;f.y0_=a;f.x1_=c;f.y1_=d;return f};i.createRadialGradient=function(b,a,c,d,f,h){var g=new D("gradientradial");g.x0_=b;g.y0_=a;g.r0_=c;g.x1_=d;g.y1_=f;g.r1_=h;return g};i.drawImage=function(b){var a,c,d,f,h,g,l,e,m=b.runtimeStyle.width,r=b.runtimeStyle.height;b.runtimeStyle.width="auto";b.runtimeStyle.height="auto";var n=b.width,o=b.height;b.runtimeStyle.width=m;b.runtimeStyle.height=r;if(arguments.length==3){a=arguments[1];c=arguments[2];h=g=0;l=d=n;e=f=o}else if(arguments.length== +5){a=arguments[1];c=arguments[2];d=arguments[3];f=arguments[4];h=g=0;l=n;e=o}else if(arguments.length==9){h=arguments[1];g=arguments[2];l=arguments[3];e=arguments[4];a=arguments[5];c=arguments[6];d=arguments[7];f=arguments[8]}else throw Error("Invalid number of arguments");var q=this.getCoords_(a,c),t=[];t.push(" <g_vml_:group",' coordsize="',k*10,",",k*10,'"',' coordorigin="0,0"',' style="width:',10,"px;height:",10,"px;position:absolute;");if(this.m_[0][0]!=1||this.m_[0][1]){var E=[];E.push("M11=", +this.m_[0][0],",","M12=",this.m_[1][0],",","M21=",this.m_[0][1],",","M22=",this.m_[1][1],",","Dx=",j(q.x/k),",","Dy=",j(q.y/k),"");var p=q,z=this.getCoords_(a+d,c),w=this.getCoords_(a,c+f),x=this.getCoords_(a+d,c+f);p.x=s.max(p.x,z.x,w.x,x.x);p.y=s.max(p.y,z.y,w.y,x.y);t.push("padding:0 ",j(p.x/k),"px ",j(p.y/k),"px 0;filter:progid:DXImageTransform.Microsoft.Matrix(",E.join(""),", sizingmethod='clip');")}else t.push("top:",j(q.y/k),"px;left:",j(q.x/k),"px;");t.push(' ">','<g_vml_:image src="',b.src, +'"',' style="width:',k*d,"px;"," height:",k*f,'px;"',' cropleft="',h/n,'"',' croptop="',g/o,'"',' cropright="',(n-h-l)/n,'"',' cropbottom="',(o-g-e)/o,'"'," />","</g_vml_:group>");this.element_.insertAdjacentHTML("BeforeEnd",t.join(""))};i.stroke=function(b){var a=[],c=P(b?this.fillStyle:this.strokeStyle),d=c.color,f=c.alpha*this.globalAlpha;a.push("<g_vml_:shape",' filled="',!!b,'"',' style="position:absolute;width:',10,"px;height:",10,'px;"',' coordorigin="0 0" coordsize="',k*10," ",k*10,'"',' stroked="', +!b,'"',' path="');var h={x:null,y:null},g={x:null,y:null},l=0;for(;l<this.currentPath_.length;l++){var e=this.currentPath_[l];switch(e.type){case "moveTo":a.push(" m ",j(e.x),",",j(e.y));break;case "lineTo":a.push(" l ",j(e.x),",",j(e.y));break;case "close":a.push(" x ");e=null;break;case "bezierCurveTo":a.push(" c ",j(e.cp1x),",",j(e.cp1y),",",j(e.cp2x),",",j(e.cp2y),",",j(e.x),",",j(e.y));break;case "at":case "wa":a.push(" ",e.type," ",j(e.x-this.arcScaleX_*e.radius),",",j(e.y-this.arcScaleY_*e.radius), +" ",j(e.x+this.arcScaleX_*e.radius),",",j(e.y+this.arcScaleY_*e.radius)," ",j(e.xStart),",",j(e.yStart)," ",j(e.xEnd),",",j(e.yEnd));break}if(e){if(h.x==null||e.x<h.x)h.x=e.x;if(g.x==null||e.x>g.x)g.x=e.x;if(h.y==null||e.y<h.y)h.y=e.y;if(g.y==null||e.y>g.y)g.y=e.y}}a.push(' ">');if(b)if(typeof this.fillStyle=="object"){var m=this.fillStyle,r=0,n={x:0,y:0},o=0,q=1;if(m.type_=="gradient"){var t=m.x1_/this.arcScaleX_,E=m.y1_/this.arcScaleY_,p=this.getCoords_(m.x0_/this.arcScaleX_,m.y0_/this.arcScaleY_), +z=this.getCoords_(t,E);r=Math.atan2(z.x-p.x,z.y-p.y)*180/Math.PI;if(r<0)r+=360;if(r<1.0E-6)r=0}else{var p=this.getCoords_(m.x0_,m.y0_),w=g.x-h.x,x=g.y-h.y;n={x:(p.x-h.x)/w,y:(p.y-h.y)/x};w/=this.arcScaleX_*k;x/=this.arcScaleY_*k;var R=s.max(w,x);o=2*m.r0_/R;q=2*m.r1_/R-o}var u=m.colors_;u.sort(function(ba,ca){return ba.offset-ca.offset});var J=u.length,da=u[0].color,ea=u[J-1].color,fa=u[0].alpha*this.globalAlpha,ga=u[J-1].alpha*this.globalAlpha,S=[],l=0;for(;l<J;l++){var T=u[l];S.push(T.offset*q+ +o+" "+T.color)}a.push('<g_vml_:fill type="',m.type_,'"',' method="none" focus="100%"',' color="',da,'"',' color2="',ea,'"',' colors="',S.join(","),'"',' opacity="',ga,'"',' g_o_:opacity2="',fa,'"',' angle="',r,'"',' focusposition="',n.x,",",n.y,'" />')}else a.push('<g_vml_:fill color="',d,'" opacity="',f,'" />');else{var K=this.lineScale_*this.lineWidth;if(K<1)f*=K;a.push("<g_vml_:stroke",' opacity="',f,'"',' joinstyle="',this.lineJoin,'"',' miterlimit="',this.miterLimit,'"',' endcap="',aa(this.lineCap), +'"',' weight="',K,'px"',' color="',d,'" />')}a.push("</g_vml_:shape>");this.element_.insertAdjacentHTML("beforeEnd",a.join(""))};i.fill=function(){this.stroke(true)};i.closePath=function(){this.currentPath_.push({type:"close"})};i.getCoords_=function(b,a){var c=this.m_;return{x:k*(b*c[0][0]+a*c[1][0]+c[2][0])-v,y:k*(b*c[0][1]+a*c[1][1]+c[2][1])-v}};i.save=function(){var b={};O(this,b);this.aStack_.push(b);this.mStack_.push(this.m_);this.m_=y(I(),this.m_)};i.restore=function(){O(this.aStack_.pop(), +this);this.m_=this.mStack_.pop()};function ha(b){var a=0;for(;a<3;a++){var c=0;for(;c<2;c++)if(!isFinite(b[a][c])||isNaN(b[a][c]))return false}return true}function A(b,a,c){if(!!ha(a)){b.m_=a;if(c)b.lineScale_=W(V(a[0][0]*a[1][1]-a[0][1]*a[1][0]))}}i.translate=function(b,a){A(this,y([[1,0,0],[0,1,0],[b,a,1]],this.m_),false)};i.rotate=function(b){var a=G(b),c=F(b);A(this,y([[a,c,0],[-c,a,0],[0,0,1]],this.m_),false)};i.scale=function(b,a){this.arcScaleX_*=b;this.arcScaleY_*=a;A(this,y([[b,0,0],[0,a, +0],[0,0,1]],this.m_),true)};i.transform=function(b,a,c,d,f,h){A(this,y([[b,a,0],[c,d,0],[f,h,1]],this.m_),true)};i.setTransform=function(b,a,c,d,f,h){A(this,[[b,a,0],[c,d,0],[f,h,1]],true)};i.clip=function(){};i.arcTo=function(){};i.createPattern=function(){return new U};function D(b){this.type_=b;this.r1_=this.y1_=this.x1_=this.r0_=this.y0_=this.x0_=0;this.colors_=[]}D.prototype.addColorStop=function(b,a){a=P(a);this.colors_.push({offset:b,color:a.color,alpha:a.alpha})};function U(){}G_vmlCanvasManager= +M;CanvasRenderingContext2D=H;CanvasGradient=D;CanvasPattern=U})(); diff --git a/src/static/js/farbtastic.js b/src/static/js/farbtastic.js new file mode 100644 index 00000000..0045703d --- /dev/null +++ b/src/static/js/farbtastic.js @@ -0,0 +1,524 @@ +// Farbtastic 2.0 alpha +(function ($) { + +var __debug = false; +var __factor = 0.5; + +$.fn.farbtastic = function (options) { + $.farbtastic(this, options); + return this; +}; + +$.farbtastic = function (container, options) { + var container = $(container)[0]; + return container.farbtastic || (container.farbtastic = new $._farbtastic(container, options)); +} + +$._farbtastic = function (container, options) { + var fb = this; + + ///////////////////////////////////////////////////// + + /** + * Link to the given element(s) or callback. + */ + fb.linkTo = function (callback) { + // Unbind previous nodes + if (typeof fb.callback == 'object') { + $(fb.callback).unbind('keyup', fb.updateValue); + } + + // Reset color + fb.color = null; + + // Bind callback or elements + if (typeof callback == 'function') { + fb.callback = callback; + } + else if (typeof callback == 'object' || typeof callback == 'string') { + fb.callback = $(callback); + fb.callback.bind('keyup', fb.updateValue); + if (fb.callback[0].value) { + fb.setColor(fb.callback[0].value); + } + } + return this; + } + fb.updateValue = function (event) { + if (this.value && this.value != fb.color) { + fb.setColor(this.value); + } + } + + /** + * Change color with HTML syntax #123456 + */ + fb.setColor = function (color) { + var unpack = fb.unpack(color); + if (fb.color != color && unpack) { + fb.color = color; + fb.rgb = unpack; + fb.hsl = fb.RGBToHSL(fb.rgb); + fb.updateDisplay(); + } + return this; + } + + /** + * Change color with HSL triplet [0..1, 0..1, 0..1] + */ + fb.setHSL = function (hsl) { + fb.hsl = hsl; + + var convertedHSL = [hsl[0]] + convertedHSL[1] = hsl[1]*__factor+((1-__factor)/2); + convertedHSL[2] = hsl[2]*__factor+((1-__factor)/2); + + fb.rgb = fb.HSLToRGB(convertedHSL); + fb.color = fb.pack(fb.rgb); + fb.updateDisplay(); + return this; + } + + ///////////////////////////////////////////////////// + //excanvas-compatible building of canvases + fb._makeCanvas = function(className){ + var c = document.createElement('canvas'); + if (!c.getContext) { // excanvas hack + c = window.G_vmlCanvasManager.initElement(c); + c.getContext(); //this creates the excanvas children + } + $(c).addClass(className); + return c; + } + + /** + * Initialize the color picker widget. + */ + fb.initWidget = function () { + + // Insert markup and size accordingly. + var dim = { + width: options.width, + height: options.width + }; + $(container) + .html( + '<div class="farbtastic" style="position: relative">' + + '<div class="farbtastic-solid"></div>' + + '</div>' + ) + .children('.farbtastic') + .append(fb._makeCanvas('farbtastic-mask')) + .append(fb._makeCanvas('farbtastic-overlay')) + .end() + .find('*').attr(dim).css(dim).end() + .find('div>*').css('position', 'absolute'); + + // Determine layout + fb.radius = (options.width - options.wheelWidth) / 2 - 1; + fb.square = Math.floor((fb.radius - options.wheelWidth / 2) * 0.7) - 1; + fb.mid = Math.floor(options.width / 2); + fb.markerSize = options.wheelWidth * 0.3; + fb.solidFill = $('.farbtastic-solid', container).css({ + width: fb.square * 2 - 1, + height: fb.square * 2 - 1, + left: fb.mid - fb.square, + top: fb.mid - fb.square + }); + + // Set up drawing context. + fb.cnvMask = $('.farbtastic-mask', container); + fb.ctxMask = fb.cnvMask[0].getContext('2d'); + fb.cnvOverlay = $('.farbtastic-overlay', container); + fb.ctxOverlay = fb.cnvOverlay[0].getContext('2d'); + fb.ctxMask.translate(fb.mid, fb.mid); + fb.ctxOverlay.translate(fb.mid, fb.mid); + + // Draw widget base layers. + fb.drawCircle(); + fb.drawMask(); + } + + /** + * Draw the color wheel. + */ + fb.drawCircle = function () { + var tm = +(new Date()); + // Draw a hue circle with a bunch of gradient-stroked beziers. + // Have to use beziers, as gradient-stroked arcs don't work. + var n = 24, + r = fb.radius, + w = options.wheelWidth, + nudge = 8 / r / n * Math.PI, // Fudge factor for seams. + m = fb.ctxMask, + angle1 = 0, color1, d1; + m.save(); + m.lineWidth = w / r; + m.scale(r, r); + // Each segment goes from angle1 to angle2. + for (var i = 0; i <= n; ++i) { + var d2 = i / n, + angle2 = d2 * Math.PI * 2, + // Endpoints + x1 = Math.sin(angle1), y1 = -Math.cos(angle1); + x2 = Math.sin(angle2), y2 = -Math.cos(angle2), + // Midpoint chosen so that the endpoints are tangent to the circle. + am = (angle1 + angle2) / 2, + tan = 1 / Math.cos((angle2 - angle1) / 2), + xm = Math.sin(am) * tan, ym = -Math.cos(am) * tan, + // New color + color2 = fb.pack(fb.HSLToRGB([d2, 1, 0.5])); + if (i > 0) { + if ($.browser.msie) { + // IE's gradient calculations mess up the colors. Correct along the diagonals. + var corr = (1 + Math.min(Math.abs(Math.tan(angle1)), Math.abs(Math.tan(Math.PI / 2 - angle1)))) / n; + color1 = fb.pack(fb.HSLToRGB([d1 - 0.15 * corr, 1, 0.5])); + color2 = fb.pack(fb.HSLToRGB([d2 + 0.15 * corr, 1, 0.5])); + // Create gradient fill between the endpoints. + var grad = m.createLinearGradient(x1, y1, x2, y2); + grad.addColorStop(0, color1); + grad.addColorStop(1, color2); + m.fillStyle = grad; + // Draw quadratic curve segment as a fill. + var r1 = (r + w / 2) / r, r2 = (r - w / 2) / r; // inner/outer radius. + m.beginPath(); + m.moveTo(x1 * r1, y1 * r1); + m.quadraticCurveTo(xm * r1, ym * r1, x2 * r1, y2 * r1); + m.lineTo(x2 * r2, y2 * r2); + m.quadraticCurveTo(xm * r2, ym * r2, x1 * r2, y1 * r2); + m.fill(); + } + else { + // Create gradient fill between the endpoints. + var grad = m.createLinearGradient(x1, y1, x2, y2); + grad.addColorStop(0, color1); + grad.addColorStop(1, color2); + m.strokeStyle = grad; + // Draw quadratic curve segment. + m.beginPath(); + m.moveTo(x1, y1); + m.quadraticCurveTo(xm, ym, x2, y2); + m.stroke(); + } + } + // Prevent seams where curves join. + angle1 = angle2 - nudge; color1 = color2; d1 = d2; + } + m.restore(); + __debug && $('body').append('<div>drawCircle '+ (+(new Date()) - tm) +'ms'); + }; + + /** + * Draw the saturation/luminance mask. + */ + fb.drawMask = function () { + var tm = +(new Date()); + + // Iterate over sat/lum space and calculate appropriate mask pixel values. + var size = fb.square * 2, sq = fb.square; + function calculateMask(sizex, sizey, outputPixel) { + var isx = 1 / sizex, isy = 1 / sizey; + for (var y = 0; y <= sizey; ++y) { + var l = 1 - y * isy; + for (var x = 0; x <= sizex; ++x) { + var s = 1 - x * isx; + // From sat/lum to alpha and color (grayscale) + var a = 1 - 2 * Math.min(l * s, (1 - l) * s); + var c = (a > 0) ? ((2 * l - 1 + a) * .5 / a) : 0; + + a = a*__factor+(1-__factor)/2; + c = c*__factor+(1-__factor)/2; + + outputPixel(x, y, c, a); + } + } + } + + // Method #1: direct pixel access (new Canvas). + if (fb.ctxMask.getImageData) { + // Create half-resolution buffer. + var sz = Math.floor(size / 2); + var buffer = document.createElement('canvas'); + buffer.width = buffer.height = sz + 1; + var ctx = buffer.getContext('2d'); + var frame = ctx.getImageData(0, 0, sz + 1, sz + 1); + + var i = 0; + calculateMask(sz, sz, function (x, y, c, a) { + frame.data[i++] = frame.data[i++] = frame.data[i++] = c * 255; + frame.data[i++] = a * 255; + }); + + ctx.putImageData(frame, 0, 0); + fb.ctxMask.drawImage(buffer, 0, 0, sz + 1, sz + 1, -sq, -sq, sq * 2, sq * 2); + } + // Method #2: drawing commands (old Canvas). + else if (!$.browser.msie) { + // Render directly at half-resolution + var sz = Math.floor(size / 2); + calculateMask(sz, sz, function (x, y, c, a) { + c = Math.round(c * 255); + fb.ctxMask.fillStyle = 'rgba(' + c + ', ' + c + ', ' + c + ', ' + a +')'; + fb.ctxMask.fillRect(x * 2 - sq - 1, y * 2 - sq - 1, 2, 2); + }); + } + // Method #3: vertical DXImageTransform gradient strips (IE). + else { + var cache_last, cache, w = 6; // Each strip is 6 pixels wide. + var sizex = Math.floor(size / w); + // 6 vertical pieces of gradient per strip. + calculateMask(sizex, 6, function (x, y, c, a) { + if (x == 0) { + cache_last = cache; + cache = []; + } + c = Math.round(c * 255); + a = Math.round(a * 255); + // We can only start outputting gradients once we have two rows of pixels. + if (y > 0) { + var c_last = cache_last[x][0], + a_last = cache_last[x][1], + color1 = fb.packDX(c_last, a_last), + color2 = fb.packDX(c, a), + y1 = Math.round(fb.mid + ((y - 1) * .333 - 1) * sq), + y2 = Math.round(fb.mid + (y * .333 - 1) * sq); + $('<div>').css({ + position: 'absolute', + filter: "progid:DXImageTransform.Microsoft.Gradient(StartColorStr="+ color1 +", EndColorStr="+ color2 +", GradientType=0)", + top: y1, + height: y2 - y1, + // Avoid right-edge sticking out. + left: fb.mid + (x * w - sq - 1), + width: w - (x == sizex ? Math.round(w / 2) : 0) + }).appendTo(fb.cnvMask); + } + cache.push([c, a]); + }); + } + __debug && $('body').append('<div>drawMask '+ (+(new Date()) - tm) +'ms'); + } + + /** + * Draw the selection markers. + */ + fb.drawMarkers = function () { + // Determine marker dimensions + var sz = options.width, lw = Math.ceil(fb.markerSize / 4), r = fb.markerSize - lw + 1; + var angle = fb.hsl[0] * 6.28, + x1 = Math.sin(angle) * fb.radius, + y1 = -Math.cos(angle) * fb.radius, + x2 = 2 * fb.square * (.5 - fb.hsl[1]), + y2 = 2 * fb.square * (.5 - fb.hsl[2]), + c1 = fb.invert ? '#fff' : '#000', + c2 = fb.invert ? '#000' : '#fff'; + var circles = [ + { x: x1, y: y1, r: r, c: '#000', lw: lw + 1 }, + { x: x1, y: y1, r: fb.markerSize, c: '#fff', lw: lw }, + { x: x2, y: y2, r: r, c: c2, lw: lw + 1 }, + { x: x2, y: y2, r: fb.markerSize, c: c1, lw: lw }, + ]; + + // Update the overlay canvas. + fb.ctxOverlay.clearRect(-fb.mid, -fb.mid, sz, sz); + for (i in circles) { + var c = circles[i]; + fb.ctxOverlay.lineWidth = c.lw; + fb.ctxOverlay.strokeStyle = c.c; + fb.ctxOverlay.beginPath(); + fb.ctxOverlay.arc(c.x, c.y, c.r, 0, Math.PI * 2, true); + fb.ctxOverlay.stroke(); + } + } + + /** + * Update the markers and styles + */ + fb.updateDisplay = function () { + // Determine whether labels/markers should invert. + fb.invert = (fb.rgb[0] * 0.3 + fb.rgb[1] * .59 + fb.rgb[2] * .11) <= 0.6; + + // Update the solid background fill. + fb.solidFill.css('backgroundColor', fb.pack(fb.HSLToRGB([fb.hsl[0], 1, 0.5]))); + + // Draw markers + fb.drawMarkers(); + + // Linked elements or callback + if (typeof fb.callback == 'object') { + // Set background/foreground color + $(fb.callback).css({ + backgroundColor: fb.color, + color: fb.invert ? '#fff' : '#000' + }); + + // Change linked value + $(fb.callback).each(function() { + if ((typeof this.value == 'string') && this.value != fb.color) { + this.value = fb.color; + } + }); + } + else if (typeof fb.callback == 'function') { + fb.callback.call(fb, fb.color); + } + } + + /** + * Helper for returning coordinates relative to the center. + */ + fb.widgetCoords = function (event) { + return { + x: event.pageX - fb.offset.left - fb.mid, + y: event.pageY - fb.offset.top - fb.mid + }; + } + + /** + * Mousedown handler + */ + fb.mousedown = function (event) { + // Capture mouse + if (!$._farbtastic.dragging) { + $(document).bind('mousemove', fb.mousemove).bind('mouseup', fb.mouseup); + $._farbtastic.dragging = true; + } + + // Update the stored offset for the widget. + fb.offset = $(container).offset(); + + // Check which area is being dragged + var pos = fb.widgetCoords(event); + fb.circleDrag = Math.max(Math.abs(pos.x), Math.abs(pos.y)) > (fb.square + 2); + + // Process + fb.mousemove(event); + return false; + } + + /** + * Mousemove handler + */ + fb.mousemove = function (event) { + // Get coordinates relative to color picker center + var pos = fb.widgetCoords(event); + + // Set new HSL parameters + if (fb.circleDrag) { + var hue = Math.atan2(pos.x, -pos.y) / 6.28; + fb.setHSL([(hue + 1) % 1, fb.hsl[1], fb.hsl[2]]); + } + else { + var sat = Math.max(0, Math.min(1, -(pos.x / fb.square / 2) + .5)); + var lum = Math.max(0, Math.min(1, -(pos.y / fb.square / 2) + .5)); + fb.setHSL([fb.hsl[0], sat, lum]); + } + return false; + } + + /** + * Mouseup handler + */ + fb.mouseup = function () { + // Uncapture mouse + $(document).unbind('mousemove', fb.mousemove); + $(document).unbind('mouseup', fb.mouseup); + $._farbtastic.dragging = false; + } + + /* Various color utility functions */ + fb.dec2hex = function (x) { + return (x < 16 ? '0' : '') + x.toString(16); + } + + fb.packDX = function (c, a) { + return '#' + fb.dec2hex(a) + fb.dec2hex(c) + fb.dec2hex(c) + fb.dec2hex(c); + }; + + fb.pack = function (rgb) { + var r = Math.round(rgb[0] * 255); + var g = Math.round(rgb[1] * 255); + var b = Math.round(rgb[2] * 255); + return '#' + fb.dec2hex(r) + fb.dec2hex(g) + fb.dec2hex(b); + }; + + fb.unpack = function (color) { + if (color.length == 7) { + function x(i) { + return parseInt(color.substring(i, i + 2), 16) / 255; + } + return [ x(1), x(3), x(5) ]; + } + else if (color.length == 4) { + function x(i) { + return parseInt(color.substring(i, i + 1), 16) / 15; + } + return [ x(1), x(2), x(3) ]; + } + }; + + fb.HSLToRGB = function (hsl) { + var m1, m2, r, g, b; + var h = hsl[0], s = hsl[1], l = hsl[2]; + m2 = (l <= 0.5) ? l * (s + 1) : l + s - l * s; + m1 = l * 2 - m2; + return [ + this.hueToRGB(m1, m2, h + 0.33333), + this.hueToRGB(m1, m2, h), + this.hueToRGB(m1, m2, h - 0.33333) + ]; + }; + + fb.hueToRGB = function (m1, m2, h) { + h = (h + 1) % 1; + if (h * 6 < 1) return m1 + (m2 - m1) * h * 6; + if (h * 2 < 1) return m2; + if (h * 3 < 2) return m1 + (m2 - m1) * (0.66666 - h) * 6; + return m1; + }; + + fb.RGBToHSL = function (rgb) { + var r = rgb[0], g = rgb[1], b = rgb[2], + min = Math.min(r, g, b), + max = Math.max(r, g, b), + delta = max - min, + h = 0, + s = 0, + l = (min + max) / 2; + if (l > 0 && l < 1) { + s = delta / (l < 0.5 ? (2 * l) : (2 - 2 * l)); + } + if (delta > 0) { + if (max == r && max != g) h += (g - b) / delta; + if (max == g && max != b) h += (2 + (b - r) / delta); + if (max == b && max != r) h += (4 + (r - g) / delta); + h /= 6; + } + return [h, s, l]; + }; + + // Parse options. + if (!options.callback) { + options = { callback: options }; + } + options = $.extend({ + width: 300, + wheelWidth: (options.width || 300) / 10, + callback: null + }, options); + + // Initialize. + fb.initWidget(); + + // Install mousedown handler (the others are set on the document on-demand) + $('canvas.farbtastic-overlay', container).mousedown(fb.mousedown); + + // Set linked elements/callback + if (options.callback) { + fb.linkTo(options.callback); + } + // Set to gray. + fb.setColor('#808080'); +} + +})(jQuery); diff --git a/src/static/js/jquery.js b/src/static/js/jquery.js new file mode 100644 index 00000000..8ccd0ea7 --- /dev/null +++ b/src/static/js/jquery.js @@ -0,0 +1,9266 @@ +/*! + * jQuery JavaScript Library v1.7.1 + * http://jquery.com/ + * + * Copyright 2011, John Resig + * Dual licensed under the MIT or GPL Version 2 licenses. + * http://jquery.org/license + * + * Includes Sizzle.js + * http://sizzlejs.com/ + * Copyright 2011, The Dojo Foundation + * Released under the MIT, BSD, and GPL Licenses. + * + * Date: Mon Nov 21 21:11:03 2011 -0500 + */ +(function( window, undefined ) { + +// Use the correct document accordingly with window argument (sandbox) +var document = window.document, + navigator = window.navigator, + location = window.location; +var jQuery = (function() { + +// Define a local copy of jQuery +var jQuery = function( selector, context ) { + // The jQuery object is actually just the init constructor 'enhanced' + return new jQuery.fn.init( selector, context, rootjQuery ); + }, + + // Map over jQuery in case of overwrite + _jQuery = window.jQuery, + + // Map over the $ in case of overwrite + _$ = window.$, + + // A central reference to the root jQuery(document) + rootjQuery, + + // A simple way to check for HTML strings or ID strings + // Prioritize #id over <tag> to avoid XSS via location.hash (#9521) + quickExpr = /^(?:[^#<]*(<[\w\W]+>)[^>]*$|#([\w\-]*)$)/, + + // Check if a string has a non-whitespace character in it + rnotwhite = /\S/, + + // Used for trimming whitespace + trimLeft = /^\s+/, + trimRight = /\s+$/, + + // Match a standalone tag + rsingleTag = /^<(\w+)\s*\/?>(?:<\/\1>)?$/, + + // JSON RegExp + rvalidchars = /^[\],:{}\s]*$/, + rvalidescape = /\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})/g, + rvalidtokens = /"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g, + rvalidbraces = /(?:^|:|,)(?:\s*\[)+/g, + + // Useragent RegExp + rwebkit = /(webkit)[ \/]([\w.]+)/, + ropera = /(opera)(?:.*version)?[ \/]([\w.]+)/, + rmsie = /(msie) ([\w.]+)/, + rmozilla = /(mozilla)(?:.*? rv:([\w.]+))?/, + + // Matches dashed string for camelizing + rdashAlpha = /-([a-z]|[0-9])/ig, + rmsPrefix = /^-ms-/, + + // Used by jQuery.camelCase as callback to replace() + fcamelCase = function( all, letter ) { + return ( letter + "" ).toUpperCase(); + }, + + // Keep a UserAgent string for use with jQuery.browser + userAgent = navigator.userAgent, + + // For matching the engine and version of the browser + browserMatch, + + // The deferred used on DOM ready + readyList, + + // The ready event handler + DOMContentLoaded, + + // Save a reference to some core methods + toString = Object.prototype.toString, + hasOwn = Object.prototype.hasOwnProperty, + push = Array.prototype.push, + slice = Array.prototype.slice, + trim = String.prototype.trim, + indexOf = Array.prototype.indexOf, + + // [[Class]] -> type pairs + class2type = {}; + +jQuery.fn = jQuery.prototype = { + constructor: jQuery, + init: function( selector, context, rootjQuery ) { + var match, elem, ret, doc; + + // Handle $(""), $(null), or $(undefined) + if ( !selector ) { + return this; + } + + // Handle $(DOMElement) + if ( selector.nodeType ) { + this.context = this[0] = selector; + this.length = 1; + return this; + } + + // The body element only exists once, optimize finding it + if ( selector === "body" && !context && document.body ) { + this.context = document; + this[0] = document.body; + this.selector = selector; + this.length = 1; + return this; + } + + // Handle HTML strings + if ( typeof selector === "string" ) { + // Are we dealing with HTML string or an ID? + if ( selector.charAt(0) === "<" && selector.charAt( selector.length - 1 ) === ">" && selector.length >= 3 ) { + // Assume that strings that start and end with <> are HTML and skip the regex check + match = [ null, selector, null ]; + + } else { + match = quickExpr.exec( selector ); + } + + // Verify a match, and that no context was specified for #id + if ( match && (match[1] || !context) ) { + + // HANDLE: $(html) -> $(array) + if ( match[1] ) { + context = context instanceof jQuery ? context[0] : context; + doc = ( context ? context.ownerDocument || context : document ); + + // If a single string is passed in and it's a single tag + // just do a createElement and skip the rest + ret = rsingleTag.exec( selector ); + + if ( ret ) { + if ( jQuery.isPlainObject( context ) ) { + selector = [ document.createElement( ret[1] ) ]; + jQuery.fn.attr.call( selector, context, true ); + + } else { + selector = [ doc.createElement( ret[1] ) ]; + } + + } else { + ret = jQuery.buildFragment( [ match[1] ], [ doc ] ); + selector = ( ret.cacheable ? jQuery.clone(ret.fragment) : ret.fragment ).childNodes; + } + + return jQuery.merge( this, selector ); + + // HANDLE: $("#id") + } else { + elem = document.getElementById( match[2] ); + + // Check parentNode to catch when Blackberry 4.6 returns + // nodes that are no longer in the document #6963 + if ( elem && elem.parentNode ) { + // Handle the case where IE and Opera return items + // by name instead of ID + if ( elem.id !== match[2] ) { + return rootjQuery.find( selector ); + } + + // Otherwise, we inject the element directly into the jQuery object + this.length = 1; + this[0] = elem; + } + + this.context = document; + this.selector = selector; + return this; + } + + // HANDLE: $(expr, $(...)) + } else if ( !context || context.jquery ) { + return ( context || rootjQuery ).find( selector ); + + // HANDLE: $(expr, context) + // (which is just equivalent to: $(context).find(expr) + } else { + return this.constructor( context ).find( selector ); + } + + // HANDLE: $(function) + // Shortcut for document ready + } else if ( jQuery.isFunction( selector ) ) { + return rootjQuery.ready( selector ); + } + + if ( selector.selector !== undefined ) { + this.selector = selector.selector; + this.context = selector.context; + } + + return jQuery.makeArray( selector, this ); + }, + + // Start with an empty selector + selector: "", + + // The current version of jQuery being used + jquery: "1.7.1", + + // The default length of a jQuery object is 0 + length: 0, + + // The number of elements contained in the matched element set + size: function() { + return this.length; + }, + + toArray: function() { + return slice.call( this, 0 ); + }, + + // Get the Nth element in the matched element set OR + // Get the whole matched element set as a clean array + get: function( num ) { + return num == null ? + + // Return a 'clean' array + this.toArray() : + + // Return just the object + ( num < 0 ? this[ this.length + num ] : this[ num ] ); + }, + + // Take an array of elements and push it onto the stack + // (returning the new matched element set) + pushStack: function( elems, name, selector ) { + // Build a new jQuery matched element set + var ret = this.constructor(); + + if ( jQuery.isArray( elems ) ) { + push.apply( ret, elems ); + + } else { + jQuery.merge( ret, elems ); + } + + // Add the old object onto the stack (as a reference) + ret.prevObject = this; + + ret.context = this.context; + + if ( name === "find" ) { + ret.selector = this.selector + ( this.selector ? " " : "" ) + selector; + } else if ( name ) { + ret.selector = this.selector + "." + name + "(" + selector + ")"; + } + + // Return the newly-formed element set + return ret; + }, + + // Execute a callback for every element in the matched set. + // (You can seed the arguments with an array of args, but this is + // only used internally.) + each: function( callback, args ) { + return jQuery.each( this, callback, args ); + }, + + ready: function( fn ) { + // Attach the listeners + jQuery.bindReady(); + + // Add the callback + readyList.add( fn ); + + return this; + }, + + eq: function( i ) { + i = +i; + return i === -1 ? + this.slice( i ) : + this.slice( i, i + 1 ); + }, + + first: function() { + return this.eq( 0 ); + }, + + last: function() { + return this.eq( -1 ); + }, + + slice: function() { + return this.pushStack( slice.apply( this, arguments ), + "slice", slice.call(arguments).join(",") ); + }, + + map: function( callback ) { + return this.pushStack( jQuery.map(this, function( elem, i ) { + return callback.call( elem, i, elem ); + })); + }, + + end: function() { + return this.prevObject || this.constructor(null); + }, + + // For internal use only. + // Behaves like an Array's method, not like a jQuery method. + push: push, + sort: [].sort, + splice: [].splice +}; + +// Give the init function the jQuery prototype for later instantiation +jQuery.fn.init.prototype = jQuery.fn; + +jQuery.extend = jQuery.fn.extend = function() { + var options, name, src, copy, copyIsArray, clone, + target = arguments[0] || {}, + i = 1, + length = arguments.length, + deep = false; + + // Handle a deep copy situation + if ( typeof target === "boolean" ) { + deep = target; + target = arguments[1] || {}; + // skip the boolean and the target + i = 2; + } + + // Handle case when target is a string or something (possible in deep copy) + if ( typeof target !== "object" && !jQuery.isFunction(target) ) { + target = {}; + } + + // extend jQuery itself if only one argument is passed + if ( length === i ) { + target = this; + --i; + } + + for ( ; i < length; i++ ) { + // Only deal with non-null/undefined values + if ( (options = arguments[ i ]) != null ) { + // Extend the base object + for ( name in options ) { + src = target[ name ]; + copy = options[ name ]; + + // Prevent never-ending loop + if ( target === copy ) { + continue; + } + + // Recurse if we're merging plain objects or arrays + if ( deep && copy && ( jQuery.isPlainObject(copy) || (copyIsArray = jQuery.isArray(copy)) ) ) { + if ( copyIsArray ) { + copyIsArray = false; + clone = src && jQuery.isArray(src) ? src : []; + + } else { + clone = src && jQuery.isPlainObject(src) ? src : {}; + } + + // Never move original objects, clone them + target[ name ] = jQuery.extend( deep, clone, copy ); + + // Don't bring in undefined values + } else if ( copy !== undefined ) { + target[ name ] = copy; + } + } + } + } + + // Return the modified object + return target; +}; + +jQuery.extend({ + noConflict: function( deep ) { + if ( window.$ === jQuery ) { + window.$ = _$; + } + + if ( deep && window.jQuery === jQuery ) { + window.jQuery = _jQuery; + } + + return jQuery; + }, + + // Is the DOM ready to be used? Set to true once it occurs. + isReady: false, + + // A counter to track how many items to wait for before + // the ready event fires. See #6781 + readyWait: 1, + + // Hold (or release) the ready event + holdReady: function( hold ) { + if ( hold ) { + jQuery.readyWait++; + } else { + jQuery.ready( true ); + } + }, + + // Handle when the DOM is ready + ready: function( wait ) { + // Either a released hold or an DOMready/load event and not yet ready + if ( (wait === true && !--jQuery.readyWait) || (wait !== true && !jQuery.isReady) ) { + // Make sure body exists, at least, in case IE gets a little overzealous (ticket #5443). + if ( !document.body ) { + return setTimeout( jQuery.ready, 1 ); + } + + // Remember that the DOM is ready + jQuery.isReady = true; + + // If a normal DOM Ready event fired, decrement, and wait if need be + if ( wait !== true && --jQuery.readyWait > 0 ) { + return; + } + + // If there are functions bound, to execute + readyList.fireWith( document, [ jQuery ] ); + + // Trigger any bound ready events + if ( jQuery.fn.trigger ) { + jQuery( document ).trigger( "ready" ).off( "ready" ); + } + } + }, + + bindReady: function() { + if ( readyList ) { + return; + } + + readyList = jQuery.Callbacks( "once memory" ); + + // Catch cases where $(document).ready() is called after the + // browser event has already occurred. + if ( document.readyState === "complete" ) { + // Handle it asynchronously to allow scripts the opportunity to delay ready + return setTimeout( jQuery.ready, 1 ); + } + + // Mozilla, Opera and webkit nightlies currently support this event + if ( document.addEventListener ) { + // Use the handy event callback + document.addEventListener( "DOMContentLoaded", DOMContentLoaded, false ); + + // A fallback to window.onload, that will always work + window.addEventListener( "load", jQuery.ready, false ); + + // If IE event model is used + } else if ( document.attachEvent ) { + // ensure firing before onload, + // maybe late but safe also for iframes + document.attachEvent( "onreadystatechange", DOMContentLoaded ); + + // A fallback to window.onload, that will always work + window.attachEvent( "onload", jQuery.ready ); + + // If IE and not a frame + // continually check to see if the document is ready + var toplevel = false; + + try { + toplevel = window.frameElement == null; + } catch(e) {} + + if ( document.documentElement.doScroll && toplevel ) { + doScrollCheck(); + } + } + }, + + // See test/unit/core.js for details concerning isFunction. + // Since version 1.3, DOM methods and functions like alert + // aren't supported. They return false on IE (#2968). + isFunction: function( obj ) { + return jQuery.type(obj) === "function"; + }, + + isArray: Array.isArray || function( obj ) { + return jQuery.type(obj) === "array"; + }, + + // A crude way of determining if an object is a window + isWindow: function( obj ) { + return obj && typeof obj === "object" && "setInterval" in obj; + }, + + isNumeric: function( obj ) { + return !isNaN( parseFloat(obj) ) && isFinite( obj ); + }, + + type: function( obj ) { + return obj == null ? + String( obj ) : + class2type[ toString.call(obj) ] || "object"; + }, + + isPlainObject: function( obj ) { + // Must be an Object. + // Because of IE, we also have to check the presence of the constructor property. + // Make sure that DOM nodes and window objects don't pass through, as well + if ( !obj || jQuery.type(obj) !== "object" || obj.nodeType || jQuery.isWindow( obj ) ) { + return false; + } + + try { + // Not own constructor property must be Object + if ( obj.constructor && + !hasOwn.call(obj, "constructor") && + !hasOwn.call(obj.constructor.prototype, "isPrototypeOf") ) { + return false; + } + } catch ( e ) { + // IE8,9 Will throw exceptions on certain host objects #9897 + return false; + } + + // Own properties are enumerated firstly, so to speed up, + // if last one is own, then all properties are own. + + var key; + for ( key in obj ) {} + + return key === undefined || hasOwn.call( obj, key ); + }, + + isEmptyObject: function( obj ) { + for ( var name in obj ) { + return false; + } + return true; + }, + + error: function( msg ) { + throw new Error( msg ); + }, + + parseJSON: function( data ) { + if ( typeof data !== "string" || !data ) { + return null; + } + + // Make sure leading/trailing whitespace is removed (IE can't handle it) + data = jQuery.trim( data ); + + // Attempt to parse using the native JSON parser first + if ( window.JSON && window.JSON.parse ) { + return window.JSON.parse( data ); + } + + // Make sure the incoming data is actual JSON + // Logic borrowed from http://json.org/json2.js + if ( rvalidchars.test( data.replace( rvalidescape, "@" ) + .replace( rvalidtokens, "]" ) + .replace( rvalidbraces, "")) ) { + + return ( new Function( "return " + data ) )(); + + } + jQuery.error( "Invalid JSON: " + data ); + }, + + // Cross-browser xml parsing + parseXML: function( data ) { + var xml, tmp; + try { + if ( window.DOMParser ) { // Standard + tmp = new DOMParser(); + xml = tmp.parseFromString( data , "text/xml" ); + } else { // IE + xml = new ActiveXObject( "Microsoft.XMLDOM" ); + xml.async = "false"; + xml.loadXML( data ); + } + } catch( e ) { + xml = undefined; + } + if ( !xml || !xml.documentElement || xml.getElementsByTagName( "parsererror" ).length ) { + jQuery.error( "Invalid XML: " + data ); + } + return xml; + }, + + noop: function() {}, + + // Evaluates a script in a global context + // Workarounds based on findings by Jim Driscoll + // http://weblogs.java.net/blog/driscoll/archive/2009/09/08/eval-javascript-global-context + globalEval: function( data ) { + if ( data && rnotwhite.test( data ) ) { + // We use execScript on Internet Explorer + // We use an anonymous function so that context is window + // rather than jQuery in Firefox + ( window.execScript || function( data ) { + window[ "eval" ].call( window, data ); + } )( data ); + } + }, + + // Convert dashed to camelCase; used by the css and data modules + // Microsoft forgot to hump their vendor prefix (#9572) + camelCase: function( string ) { + return string.replace( rmsPrefix, "ms-" ).replace( rdashAlpha, fcamelCase ); + }, + + nodeName: function( elem, name ) { + return elem.nodeName && elem.nodeName.toUpperCase() === name.toUpperCase(); + }, + + // args is for internal usage only + each: function( object, callback, args ) { + var name, i = 0, + length = object.length, + isObj = length === undefined || jQuery.isFunction( object ); + + if ( args ) { + if ( isObj ) { + for ( name in object ) { + if ( callback.apply( object[ name ], args ) === false ) { + break; + } + } + } else { + for ( ; i < length; ) { + if ( callback.apply( object[ i++ ], args ) === false ) { + break; + } + } + } + + // A special, fast, case for the most common use of each + } else { + if ( isObj ) { + for ( name in object ) { + if ( callback.call( object[ name ], name, object[ name ] ) === false ) { + break; + } + } + } else { + for ( ; i < length; ) { + if ( callback.call( object[ i ], i, object[ i++ ] ) === false ) { + break; + } + } + } + } + + return object; + }, + + // Use native String.trim function wherever possible + trim: trim ? + function( text ) { + return text == null ? + "" : + trim.call( text ); + } : + + // Otherwise use our own trimming functionality + function( text ) { + return text == null ? + "" : + text.toString().replace( trimLeft, "" ).replace( trimRight, "" ); + }, + + // results is for internal usage only + makeArray: function( array, results ) { + var ret = results || []; + + if ( array != null ) { + // The window, strings (and functions) also have 'length' + // Tweaked logic slightly to handle Blackberry 4.7 RegExp issues #6930 + var type = jQuery.type( array ); + + if ( array.length == null || type === "string" || type === "function" || type === "regexp" || jQuery.isWindow( array ) ) { + push.call( ret, array ); + } else { + jQuery.merge( ret, array ); + } + } + + return ret; + }, + + inArray: function( elem, array, i ) { + var len; + + if ( array ) { + if ( indexOf ) { + return indexOf.call( array, elem, i ); + } + + len = array.length; + i = i ? i < 0 ? Math.max( 0, len + i ) : i : 0; + + for ( ; i < len; i++ ) { + // Skip accessing in sparse arrays + if ( i in array && array[ i ] === elem ) { + return i; + } + } + } + + return -1; + }, + + merge: function( first, second ) { + var i = first.length, + j = 0; + + if ( typeof second.length === "number" ) { + for ( var l = second.length; j < l; j++ ) { + first[ i++ ] = second[ j ]; + } + + } else { + while ( second[j] !== undefined ) { + first[ i++ ] = second[ j++ ]; + } + } + + first.length = i; + + return first; + }, + + grep: function( elems, callback, inv ) { + var ret = [], retVal; + inv = !!inv; + + // Go through the array, only saving the items + // that pass the validator function + for ( var i = 0, length = elems.length; i < length; i++ ) { + retVal = !!callback( elems[ i ], i ); + if ( inv !== retVal ) { + ret.push( elems[ i ] ); + } + } + + return ret; + }, + + // arg is for internal usage only + map: function( elems, callback, arg ) { + var value, key, ret = [], + i = 0, + length = elems.length, + // jquery objects are treated as arrays + isArray = elems instanceof jQuery || length !== undefined && typeof length === "number" && ( ( length > 0 && elems[ 0 ] && elems[ length -1 ] ) || length === 0 || jQuery.isArray( elems ) ) ; + + // Go through the array, translating each of the items to their + if ( isArray ) { + for ( ; i < length; i++ ) { + value = callback( elems[ i ], i, arg ); + + if ( value != null ) { + ret[ ret.length ] = value; + } + } + + // Go through every key on the object, + } else { + for ( key in elems ) { + value = callback( elems[ key ], key, arg ); + + if ( value != null ) { + ret[ ret.length ] = value; + } + } + } + + // Flatten any nested arrays + return ret.concat.apply( [], ret ); + }, + + // A global GUID counter for objects + guid: 1, + + // Bind a function to a context, optionally partially applying any + // arguments. + proxy: function( fn, context ) { + if ( typeof context === "string" ) { + var tmp = fn[ context ]; + context = fn; + fn = tmp; + } + + // Quick check to determine if target is callable, in the spec + // this throws a TypeError, but we will just return undefined. + if ( !jQuery.isFunction( fn ) ) { + return undefined; + } + + // Simulated bind + var args = slice.call( arguments, 2 ), + proxy = function() { + return fn.apply( context, args.concat( slice.call( arguments ) ) ); + }; + + // Set the guid of unique handler to the same of original handler, so it can be removed + proxy.guid = fn.guid = fn.guid || proxy.guid || jQuery.guid++; + + return proxy; + }, + + // Mutifunctional method to get and set values to a collection + // The value/s can optionally be executed if it's a function + access: function( elems, key, value, exec, fn, pass ) { + var length = elems.length; + + // Setting many attributes + if ( typeof key === "object" ) { + for ( var k in key ) { + jQuery.access( elems, k, key[k], exec, fn, value ); + } + return elems; + } + + // Setting one attribute + if ( value !== undefined ) { + // Optionally, function values get executed if exec is true + exec = !pass && exec && jQuery.isFunction(value); + + for ( var i = 0; i < length; i++ ) { + fn( elems[i], key, exec ? value.call( elems[i], i, fn( elems[i], key ) ) : value, pass ); + } + + return elems; + } + + // Getting an attribute + return length ? fn( elems[0], key ) : undefined; + }, + + now: function() { + return ( new Date() ).getTime(); + }, + + // Use of jQuery.browser is frowned upon. + // More details: http://docs.jquery.com/Utilities/jQuery.browser + uaMatch: function( ua ) { + ua = ua.toLowerCase(); + + var match = rwebkit.exec( ua ) || + ropera.exec( ua ) || + rmsie.exec( ua ) || + ua.indexOf("compatible") < 0 && rmozilla.exec( ua ) || + []; + + return { browser: match[1] || "", version: match[2] || "0" }; + }, + + sub: function() { + function jQuerySub( selector, context ) { + return new jQuerySub.fn.init( selector, context ); + } + jQuery.extend( true, jQuerySub, this ); + jQuerySub.superclass = this; + jQuerySub.fn = jQuerySub.prototype = this(); + jQuerySub.fn.constructor = jQuerySub; + jQuerySub.sub = this.sub; + jQuerySub.fn.init = function init( selector, context ) { + if ( context && context instanceof jQuery && !(context instanceof jQuerySub) ) { + context = jQuerySub( context ); + } + + return jQuery.fn.init.call( this, selector, context, rootjQuerySub ); + }; + jQuerySub.fn.init.prototype = jQuerySub.fn; + var rootjQuerySub = jQuerySub(document); + return jQuerySub; + }, + + browser: {} +}); + +// Populate the class2type map +jQuery.each("Boolean Number String Function Array Date RegExp Object".split(" "), function(i, name) { + class2type[ "[object " + name + "]" ] = name.toLowerCase(); +}); + +browserMatch = jQuery.uaMatch( userAgent ); +if ( browserMatch.browser ) { + jQuery.browser[ browserMatch.browser ] = true; + jQuery.browser.version = browserMatch.version; +} + +// Deprecated, use jQuery.browser.webkit instead +if ( jQuery.browser.webkit ) { + jQuery.browser.safari = true; +} + +// IE doesn't match non-breaking spaces with \s +if ( rnotwhite.test( "\xA0" ) ) { + trimLeft = /^[\s\xA0]+/; + trimRight = /[\s\xA0]+$/; +} + +// All jQuery objects should point back to these +rootjQuery = jQuery(document); + +// Cleanup functions for the document ready method +if ( document.addEventListener ) { + DOMContentLoaded = function() { + document.removeEventListener( "DOMContentLoaded", DOMContentLoaded, false ); + jQuery.ready(); + }; + +} else if ( document.attachEvent ) { + DOMContentLoaded = function() { + // Make sure body exists, at least, in case IE gets a little overzealous (ticket #5443). + if ( document.readyState === "complete" ) { + document.detachEvent( "onreadystatechange", DOMContentLoaded ); + jQuery.ready(); + } + }; +} + +// The DOM ready check for Internet Explorer +function doScrollCheck() { + if ( jQuery.isReady ) { + return; + } + + try { + // If IE is used, use the trick by Diego Perini + // http://javascript.nwbox.com/IEContentLoaded/ + document.documentElement.doScroll("left"); + } catch(e) { + setTimeout( doScrollCheck, 1 ); + return; + } + + // and execute any waiting functions + jQuery.ready(); +} + +return jQuery; + +})(); + + +// String to Object flags format cache +var flagsCache = {}; + +// Convert String-formatted flags into Object-formatted ones and store in cache +function createFlags( flags ) { + var object = flagsCache[ flags ] = {}, + i, length; + flags = flags.split( /\s+/ ); + for ( i = 0, length = flags.length; i < length; i++ ) { + object[ flags[i] ] = true; + } + return object; +} + +/* + * Create a callback list using the following parameters: + * + * flags: an optional list of space-separated flags that will change how + * the callback list behaves + * + * By default a callback list will act like an event callback list and can be + * "fired" multiple times. + * + * Possible flags: + * + * once: will ensure the callback list can only be fired once (like a Deferred) + * + * memory: will keep track of previous values and will call any callback added + * after the list has been fired right away with the latest "memorized" + * values (like a Deferred) + * + * unique: will ensure a callback can only be added once (no duplicate in the list) + * + * stopOnFalse: interrupt callings when a callback returns false + * + */ +jQuery.Callbacks = function( flags ) { + + // Convert flags from String-formatted to Object-formatted + // (we check in cache first) + flags = flags ? ( flagsCache[ flags ] || createFlags( flags ) ) : {}; + + var // Actual callback list + list = [], + // Stack of fire calls for repeatable lists + stack = [], + // Last fire value (for non-forgettable lists) + memory, + // Flag to know if list is currently firing + firing, + // First callback to fire (used internally by add and fireWith) + firingStart, + // End of the loop when firing + firingLength, + // Index of currently firing callback (modified by remove if needed) + firingIndex, + // Add one or several callbacks to the list + add = function( args ) { + var i, + length, + elem, + type, + actual; + for ( i = 0, length = args.length; i < length; i++ ) { + elem = args[ i ]; + type = jQuery.type( elem ); + if ( type === "array" ) { + // Inspect recursively + add( elem ); + } else if ( type === "function" ) { + // Add if not in unique mode and callback is not in + if ( !flags.unique || !self.has( elem ) ) { + list.push( elem ); + } + } + } + }, + // Fire callbacks + fire = function( context, args ) { + args = args || []; + memory = !flags.memory || [ context, args ]; + firing = true; + firingIndex = firingStart || 0; + firingStart = 0; + firingLength = list.length; + for ( ; list && firingIndex < firingLength; firingIndex++ ) { + if ( list[ firingIndex ].apply( context, args ) === false && flags.stopOnFalse ) { + memory = true; // Mark as halted + break; + } + } + firing = false; + if ( list ) { + if ( !flags.once ) { + if ( stack && stack.length ) { + memory = stack.shift(); + self.fireWith( memory[ 0 ], memory[ 1 ] ); + } + } else if ( memory === true ) { + self.disable(); + } else { + list = []; + } + } + }, + // Actual Callbacks object + self = { + // Add a callback or a collection of callbacks to the list + add: function() { + if ( list ) { + var length = list.length; + add( arguments ); + // Do we need to add the callbacks to the + // current firing batch? + if ( firing ) { + firingLength = list.length; + // With memory, if we're not firing then + // we should call right away, unless previous + // firing was halted (stopOnFalse) + } else if ( memory && memory !== true ) { + firingStart = length; + fire( memory[ 0 ], memory[ 1 ] ); + } + } + return this; + }, + // Remove a callback from the list + remove: function() { + if ( list ) { + var args = arguments, + argIndex = 0, + argLength = args.length; + for ( ; argIndex < argLength ; argIndex++ ) { + for ( var i = 0; i < list.length; i++ ) { + if ( args[ argIndex ] === list[ i ] ) { + // Handle firingIndex and firingLength + if ( firing ) { + if ( i <= firingLength ) { + firingLength--; + if ( i <= firingIndex ) { + firingIndex--; + } + } + } + // Remove the element + list.splice( i--, 1 ); + // If we have some unicity property then + // we only need to do this once + if ( flags.unique ) { + break; + } + } + } + } + } + return this; + }, + // Control if a given callback is in the list + has: function( fn ) { + if ( list ) { + var i = 0, + length = list.length; + for ( ; i < length; i++ ) { + if ( fn === list[ i ] ) { + return true; + } + } + } + return false; + }, + // Remove all callbacks from the list + empty: function() { + list = []; + return this; + }, + // Have the list do nothing anymore + disable: function() { + list = stack = memory = undefined; + return this; + }, + // Is it disabled? + disabled: function() { + return !list; + }, + // Lock the list in its current state + lock: function() { + stack = undefined; + if ( !memory || memory === true ) { + self.disable(); + } + return this; + }, + // Is it locked? + locked: function() { + return !stack; + }, + // Call all callbacks with the given context and arguments + fireWith: function( context, args ) { + if ( stack ) { + if ( firing ) { + if ( !flags.once ) { + stack.push( [ context, args ] ); + } + } else if ( !( flags.once && memory ) ) { + fire( context, args ); + } + } + return this; + }, + // Call all the callbacks with the given arguments + fire: function() { + self.fireWith( this, arguments ); + return this; + }, + // To know if the callbacks have already been called at least once + fired: function() { + return !!memory; + } + }; + + return self; +}; + + + + +var // Static reference to slice + sliceDeferred = [].slice; + +jQuery.extend({ + + Deferred: function( func ) { + var doneList = jQuery.Callbacks( "once memory" ), + failList = jQuery.Callbacks( "once memory" ), + progressList = jQuery.Callbacks( "memory" ), + state = "pending", + lists = { + resolve: doneList, + reject: failList, + notify: progressList + }, + promise = { + done: doneList.add, + fail: failList.add, + progress: progressList.add, + + state: function() { + return state; + }, + + // Deprecated + isResolved: doneList.fired, + isRejected: failList.fired, + + then: function( doneCallbacks, failCallbacks, progressCallbacks ) { + deferred.done( doneCallbacks ).fail( failCallbacks ).progress( progressCallbacks ); + return this; + }, + always: function() { + deferred.done.apply( deferred, arguments ).fail.apply( deferred, arguments ); + return this; + }, + pipe: function( fnDone, fnFail, fnProgress ) { + return jQuery.Deferred(function( newDefer ) { + jQuery.each( { + done: [ fnDone, "resolve" ], + fail: [ fnFail, "reject" ], + progress: [ fnProgress, "notify" ] + }, function( handler, data ) { + var fn = data[ 0 ], + action = data[ 1 ], + returned; + if ( jQuery.isFunction( fn ) ) { + deferred[ handler ](function() { + returned = fn.apply( this, arguments ); + if ( returned && jQuery.isFunction( returned.promise ) ) { + returned.promise().then( newDefer.resolve, newDefer.reject, newDefer.notify ); + } else { + newDefer[ action + "With" ]( this === deferred ? newDefer : this, [ returned ] ); + } + }); + } else { + deferred[ handler ]( newDefer[ action ] ); + } + }); + }).promise(); + }, + // Get a promise for this deferred + // If obj is provided, the promise aspect is added to the object + promise: function( obj ) { + if ( obj == null ) { + obj = promise; + } else { + for ( var key in promise ) { + obj[ key ] = promise[ key ]; + } + } + return obj; + } + }, + deferred = promise.promise({}), + key; + + for ( key in lists ) { + deferred[ key ] = lists[ key ].fire; + deferred[ key + "With" ] = lists[ key ].fireWith; + } + + // Handle state + deferred.done( function() { + state = "resolved"; + }, failList.disable, progressList.lock ).fail( function() { + state = "rejected"; + }, doneList.disable, progressList.lock ); + + // Call given func if any + if ( func ) { + func.call( deferred, deferred ); + } + + // All done! + return deferred; + }, + + // Deferred helper + when: function( firstParam ) { + var args = sliceDeferred.call( arguments, 0 ), + i = 0, + length = args.length, + pValues = new Array( length ), + count = length, + pCount = length, + deferred = length <= 1 && firstParam && jQuery.isFunction( firstParam.promise ) ? + firstParam : + jQuery.Deferred(), + promise = deferred.promise(); + function resolveFunc( i ) { + return function( value ) { + args[ i ] = arguments.length > 1 ? sliceDeferred.call( arguments, 0 ) : value; + if ( !( --count ) ) { + deferred.resolveWith( deferred, args ); + } + }; + } + function progressFunc( i ) { + return function( value ) { + pValues[ i ] = arguments.length > 1 ? sliceDeferred.call( arguments, 0 ) : value; + deferred.notifyWith( promise, pValues ); + }; + } + if ( length > 1 ) { + for ( ; i < length; i++ ) { + if ( args[ i ] && args[ i ].promise && jQuery.isFunction( args[ i ].promise ) ) { + args[ i ].promise().then( resolveFunc(i), deferred.reject, progressFunc(i) ); + } else { + --count; + } + } + if ( !count ) { + deferred.resolveWith( deferred, args ); + } + } else if ( deferred !== firstParam ) { + deferred.resolveWith( deferred, length ? [ firstParam ] : [] ); + } + return promise; + } +}); + + + + +jQuery.support = (function() { + + var support, + all, + a, + select, + opt, + input, + marginDiv, + fragment, + tds, + events, + eventName, + i, + isSupported, + div = document.createElement( "div" ), + documentElement = document.documentElement; + + // Preliminary tests + div.setAttribute("className", "t"); + div.innerHTML = " <link/><table></table><a href='/a' style='top:1px;float:left;opacity:.55;'>a</a><input type='checkbox'/>"; + + all = div.getElementsByTagName( "*" ); + a = div.getElementsByTagName( "a" )[ 0 ]; + + // Can't get basic test support + if ( !all || !all.length || !a ) { + return {}; + } + + // First batch of supports tests + select = document.createElement( "select" ); + opt = select.appendChild( document.createElement("option") ); + input = div.getElementsByTagName( "input" )[ 0 ]; + + support = { + // IE strips leading whitespace when .innerHTML is used + leadingWhitespace: ( div.firstChild.nodeType === 3 ), + + // Make sure that tbody elements aren't automatically inserted + // IE will insert them into empty tables + tbody: !div.getElementsByTagName("tbody").length, + + // Make sure that link elements get serialized correctly by innerHTML + // This requires a wrapper element in IE + htmlSerialize: !!div.getElementsByTagName("link").length, + + // Get the style information from getAttribute + // (IE uses .cssText instead) + style: /top/.test( a.getAttribute("style") ), + + // Make sure that URLs aren't manipulated + // (IE normalizes it by default) + hrefNormalized: ( a.getAttribute("href") === "/a" ), + + // Make sure that element opacity exists + // (IE uses filter instead) + // Use a regex to work around a WebKit issue. See #5145 + opacity: /^0.55/.test( a.style.opacity ), + + // Verify style float existence + // (IE uses styleFloat instead of cssFloat) + cssFloat: !!a.style.cssFloat, + + // Make sure that if no value is specified for a checkbox + // that it defaults to "on". + // (WebKit defaults to "" instead) + checkOn: ( input.value === "on" ), + + // Make sure that a selected-by-default option has a working selected property. + // (WebKit defaults to false instead of true, IE too, if it's in an optgroup) + optSelected: opt.selected, + + // Test setAttribute on camelCase class. If it works, we need attrFixes when doing get/setAttribute (ie6/7) + getSetAttribute: div.className !== "t", + + // Tests for enctype support on a form(#6743) + enctype: !!document.createElement("form").enctype, + + // Makes sure cloning an html5 element does not cause problems + // Where outerHTML is undefined, this still works + html5Clone: document.createElement("nav").cloneNode( true ).outerHTML !== "<:nav></:nav>", + + // Will be defined later + submitBubbles: true, + changeBubbles: true, + focusinBubbles: false, + deleteExpando: true, + noCloneEvent: true, + inlineBlockNeedsLayout: false, + shrinkWrapBlocks: false, + reliableMarginRight: true + }; + + // Make sure checked status is properly cloned + input.checked = true; + support.noCloneChecked = input.cloneNode( true ).checked; + + // Make sure that the options inside disabled selects aren't marked as disabled + // (WebKit marks them as disabled) + select.disabled = true; + support.optDisabled = !opt.disabled; + + // Test to see if it's possible to delete an expando from an element + // Fails in Internet Explorer + try { + delete div.test; + } catch( e ) { + support.deleteExpando = false; + } + + if ( !div.addEventListener && div.attachEvent && div.fireEvent ) { + div.attachEvent( "onclick", function() { + // Cloning a node shouldn't copy over any + // bound event handlers (IE does this) + support.noCloneEvent = false; + }); + div.cloneNode( true ).fireEvent( "onclick" ); + } + + // Check if a radio maintains its value + // after being appended to the DOM + input = document.createElement("input"); + input.value = "t"; + input.setAttribute("type", "radio"); + support.radioValue = input.value === "t"; + + input.setAttribute("checked", "checked"); + div.appendChild( input ); + fragment = document.createDocumentFragment(); + fragment.appendChild( div.lastChild ); + + // WebKit doesn't clone checked state correctly in fragments + support.checkClone = fragment.cloneNode( true ).cloneNode( true ).lastChild.checked; + + // Check if a disconnected checkbox will retain its checked + // value of true after appended to the DOM (IE6/7) + support.appendChecked = input.checked; + + fragment.removeChild( input ); + fragment.appendChild( div ); + + div.innerHTML = ""; + + // Check if div with explicit width and no margin-right incorrectly + // gets computed margin-right based on width of container. For more + // info see bug #3333 + // Fails in WebKit before Feb 2011 nightlies + // WebKit Bug 13343 - getComputedStyle returns wrong value for margin-right + if ( window.getComputedStyle ) { + marginDiv = document.createElement( "div" ); + marginDiv.style.width = "0"; + marginDiv.style.marginRight = "0"; + div.style.width = "2px"; + div.appendChild( marginDiv ); + support.reliableMarginRight = + ( parseInt( ( window.getComputedStyle( marginDiv, null ) || { marginRight: 0 } ).marginRight, 10 ) || 0 ) === 0; + } + + // Technique from Juriy Zaytsev + // http://perfectionkills.com/detecting-event-support-without-browser-sniffing/ + // We only care about the case where non-standard event systems + // are used, namely in IE. Short-circuiting here helps us to + // avoid an eval call (in setAttribute) which can cause CSP + // to go haywire. See: https://developer.mozilla.org/en/Security/CSP + if ( div.attachEvent ) { + for( i in { + submit: 1, + change: 1, + focusin: 1 + }) { + eventName = "on" + i; + isSupported = ( eventName in div ); + if ( !isSupported ) { + div.setAttribute( eventName, "return;" ); + isSupported = ( typeof div[ eventName ] === "function" ); + } + support[ i + "Bubbles" ] = isSupported; + } + } + + fragment.removeChild( div ); + + // Null elements to avoid leaks in IE + fragment = select = opt = marginDiv = div = input = null; + + // Run tests that need a body at doc ready + jQuery(function() { + var container, outer, inner, table, td, offsetSupport, + conMarginTop, ptlm, vb, style, html, + body = document.getElementsByTagName("body")[0]; + + if ( !body ) { + // Return for frameset docs that don't have a body + return; + } + + conMarginTop = 1; + ptlm = "position:absolute;top:0;left:0;width:1px;height:1px;margin:0;"; + vb = "visibility:hidden;border:0;"; + style = "style='" + ptlm + "border:5px solid #000;padding:0;'"; + html = "<div " + style + "><div></div></div>" + + "<table " + style + " cellpadding='0' cellspacing='0'>" + + "<tr><td></td></tr></table>"; + + container = document.createElement("div"); + container.style.cssText = vb + "width:0;height:0;position:static;top:0;margin-top:" + conMarginTop + "px"; + body.insertBefore( container, body.firstChild ); + + // Construct the test element + div = document.createElement("div"); + container.appendChild( div ); + + // Check if table cells still have offsetWidth/Height when they are set + // to display:none and there are still other visible table cells in a + // table row; if so, offsetWidth/Height are not reliable for use when + // determining if an element has been hidden directly using + // display:none (it is still safe to use offsets if a parent element is + // hidden; don safety goggles and see bug #4512 for more information). + // (only IE 8 fails this test) + div.innerHTML = "<table><tr><td style='padding:0;border:0;display:none'></td><td>t</td></tr></table>"; + tds = div.getElementsByTagName( "td" ); + isSupported = ( tds[ 0 ].offsetHeight === 0 ); + + tds[ 0 ].style.display = ""; + tds[ 1 ].style.display = "none"; + + // Check if empty table cells still have offsetWidth/Height + // (IE <= 8 fail this test) + support.reliableHiddenOffsets = isSupported && ( tds[ 0 ].offsetHeight === 0 ); + + // Figure out if the W3C box model works as expected + div.innerHTML = ""; + div.style.width = div.style.paddingLeft = "1px"; + jQuery.boxModel = support.boxModel = div.offsetWidth === 2; + + if ( typeof div.style.zoom !== "undefined" ) { + // Check if natively block-level elements act like inline-block + // elements when setting their display to 'inline' and giving + // them layout + // (IE < 8 does this) + div.style.display = "inline"; + div.style.zoom = 1; + support.inlineBlockNeedsLayout = ( div.offsetWidth === 2 ); + + // Check if elements with layout shrink-wrap their children + // (IE 6 does this) + div.style.display = ""; + div.innerHTML = "<div style='width:4px;'></div>"; + support.shrinkWrapBlocks = ( div.offsetWidth !== 2 ); + } + + div.style.cssText = ptlm + vb; + div.innerHTML = html; + + outer = div.firstChild; + inner = outer.firstChild; + td = outer.nextSibling.firstChild.firstChild; + + offsetSupport = { + doesNotAddBorder: ( inner.offsetTop !== 5 ), + doesAddBorderForTableAndCells: ( td.offsetTop === 5 ) + }; + + inner.style.position = "fixed"; + inner.style.top = "20px"; + + // safari subtracts parent border width here which is 5px + offsetSupport.fixedPosition = ( inner.offsetTop === 20 || inner.offsetTop === 15 ); + inner.style.position = inner.style.top = ""; + + outer.style.overflow = "hidden"; + outer.style.position = "relative"; + + offsetSupport.subtractsBorderForOverflowNotVisible = ( inner.offsetTop === -5 ); + offsetSupport.doesNotIncludeMarginInBodyOffset = ( body.offsetTop !== conMarginTop ); + + body.removeChild( container ); + div = container = null; + + jQuery.extend( support, offsetSupport ); + }); + + return support; +})(); + + + + +var rbrace = /^(?:\{.*\}|\[.*\])$/, + rmultiDash = /([A-Z])/g; + +jQuery.extend({ + cache: {}, + + // Please use with caution + uuid: 0, + + // Unique for each copy of jQuery on the page + // Non-digits removed to match rinlinejQuery + expando: "jQuery" + ( jQuery.fn.jquery + Math.random() ).replace( /\D/g, "" ), + + // The following elements throw uncatchable exceptions if you + // attempt to add expando properties to them. + noData: { + "embed": true, + // Ban all objects except for Flash (which handle expandos) + "object": "clsid:D27CDB6E-AE6D-11cf-96B8-444553540000", + "applet": true + }, + + hasData: function( elem ) { + elem = elem.nodeType ? jQuery.cache[ elem[jQuery.expando] ] : elem[ jQuery.expando ]; + return !!elem && !isEmptyDataObject( elem ); + }, + + data: function( elem, name, data, pvt /* Internal Use Only */ ) { + if ( !jQuery.acceptData( elem ) ) { + return; + } + + var privateCache, thisCache, ret, + internalKey = jQuery.expando, + getByName = typeof name === "string", + + // We have to handle DOM nodes and JS objects differently because IE6-7 + // can't GC object references properly across the DOM-JS boundary + isNode = elem.nodeType, + + // Only DOM nodes need the global jQuery cache; JS object data is + // attached directly to the object so GC can occur automatically + cache = isNode ? jQuery.cache : elem, + + // Only defining an ID for JS objects if its cache already exists allows + // the code to shortcut on the same path as a DOM node with no cache + id = isNode ? elem[ internalKey ] : elem[ internalKey ] && internalKey, + isEvents = name === "events"; + + // Avoid doing any more work than we need to when trying to get data on an + // object that has no data at all + if ( (!id || !cache[id] || (!isEvents && !pvt && !cache[id].data)) && getByName && data === undefined ) { + return; + } + + if ( !id ) { + // Only DOM nodes need a new unique ID for each element since their data + // ends up in the global cache + if ( isNode ) { + elem[ internalKey ] = id = ++jQuery.uuid; + } else { + id = internalKey; + } + } + + if ( !cache[ id ] ) { + cache[ id ] = {}; + + // Avoids exposing jQuery metadata on plain JS objects when the object + // is serialized using JSON.stringify + if ( !isNode ) { + cache[ id ].toJSON = jQuery.noop; + } + } + + // An object can be passed to jQuery.data instead of a key/value pair; this gets + // shallow copied over onto the existing cache + if ( typeof name === "object" || typeof name === "function" ) { + if ( pvt ) { + cache[ id ] = jQuery.extend( cache[ id ], name ); + } else { + cache[ id ].data = jQuery.extend( cache[ id ].data, name ); + } + } + + privateCache = thisCache = cache[ id ]; + + // jQuery data() is stored in a separate object inside the object's internal data + // cache in order to avoid key collisions between internal data and user-defined + // data. + if ( !pvt ) { + if ( !thisCache.data ) { + thisCache.data = {}; + } + + thisCache = thisCache.data; + } + + if ( data !== undefined ) { + thisCache[ jQuery.camelCase( name ) ] = data; + } + + // Users should not attempt to inspect the internal events object using jQuery.data, + // it is undocumented and subject to change. But does anyone listen? No. + if ( isEvents && !thisCache[ name ] ) { + return privateCache.events; + } + + // Check for both converted-to-camel and non-converted data property names + // If a data property was specified + if ( getByName ) { + + // First Try to find as-is property data + ret = thisCache[ name ]; + + // Test for null|undefined property data + if ( ret == null ) { + + // Try to find the camelCased property + ret = thisCache[ jQuery.camelCase( name ) ]; + } + } else { + ret = thisCache; + } + + return ret; + }, + + removeData: function( elem, name, pvt /* Internal Use Only */ ) { + if ( !jQuery.acceptData( elem ) ) { + return; + } + + var thisCache, i, l, + + // Reference to internal data cache key + internalKey = jQuery.expando, + + isNode = elem.nodeType, + + // See jQuery.data for more information + cache = isNode ? jQuery.cache : elem, + + // See jQuery.data for more information + id = isNode ? elem[ internalKey ] : internalKey; + + // If there is already no cache entry for this object, there is no + // purpose in continuing + if ( !cache[ id ] ) { + return; + } + + if ( name ) { + + thisCache = pvt ? cache[ id ] : cache[ id ].data; + + if ( thisCache ) { + + // Support array or space separated string names for data keys + if ( !jQuery.isArray( name ) ) { + + // try the string as a key before any manipulation + if ( name in thisCache ) { + name = [ name ]; + } else { + + // split the camel cased version by spaces unless a key with the spaces exists + name = jQuery.camelCase( name ); + if ( name in thisCache ) { + name = [ name ]; + } else { + name = name.split( " " ); + } + } + } + + for ( i = 0, l = name.length; i < l; i++ ) { + delete thisCache[ name[i] ]; + } + + // If there is no data left in the cache, we want to continue + // and let the cache object itself get destroyed + if ( !( pvt ? isEmptyDataObject : jQuery.isEmptyObject )( thisCache ) ) { + return; + } + } + } + + // See jQuery.data for more information + if ( !pvt ) { + delete cache[ id ].data; + + // Don't destroy the parent cache unless the internal data object + // had been the only thing left in it + if ( !isEmptyDataObject(cache[ id ]) ) { + return; + } + } + + // Browsers that fail expando deletion also refuse to delete expandos on + // the window, but it will allow it on all other JS objects; other browsers + // don't care + // Ensure that `cache` is not a window object #10080 + if ( jQuery.support.deleteExpando || !cache.setInterval ) { + delete cache[ id ]; + } else { + cache[ id ] = null; + } + + // We destroyed the cache and need to eliminate the expando on the node to avoid + // false lookups in the cache for entries that no longer exist + if ( isNode ) { + // IE does not allow us to delete expando properties from nodes, + // nor does it have a removeAttribute function on Document nodes; + // we must handle all of these cases + if ( jQuery.support.deleteExpando ) { + delete elem[ internalKey ]; + } else if ( elem.removeAttribute ) { + elem.removeAttribute( internalKey ); + } else { + elem[ internalKey ] = null; + } + } + }, + + // For internal use only. + _data: function( elem, name, data ) { + return jQuery.data( elem, name, data, true ); + }, + + // A method for determining if a DOM node can handle the data expando + acceptData: function( elem ) { + if ( elem.nodeName ) { + var match = jQuery.noData[ elem.nodeName.toLowerCase() ]; + + if ( match ) { + return !(match === true || elem.getAttribute("classid") !== match); + } + } + + return true; + } +}); + +jQuery.fn.extend({ + data: function( key, value ) { + var parts, attr, name, + data = null; + + if ( typeof key === "undefined" ) { + if ( this.length ) { + data = jQuery.data( this[0] ); + + if ( this[0].nodeType === 1 && !jQuery._data( this[0], "parsedAttrs" ) ) { + attr = this[0].attributes; + for ( var i = 0, l = attr.length; i < l; i++ ) { + name = attr[i].name; + + if ( name.indexOf( "data-" ) === 0 ) { + name = jQuery.camelCase( name.substring(5) ); + + dataAttr( this[0], name, data[ name ] ); + } + } + jQuery._data( this[0], "parsedAttrs", true ); + } + } + + return data; + + } else if ( typeof key === "object" ) { + return this.each(function() { + jQuery.data( this, key ); + }); + } + + parts = key.split("."); + parts[1] = parts[1] ? "." + parts[1] : ""; + + if ( value === undefined ) { + data = this.triggerHandler("getData" + parts[1] + "!", [parts[0]]); + + // Try to fetch any internally stored data first + if ( data === undefined && this.length ) { + data = jQuery.data( this[0], key ); + data = dataAttr( this[0], key, data ); + } + + return data === undefined && parts[1] ? + this.data( parts[0] ) : + data; + + } else { + return this.each(function() { + var self = jQuery( this ), + args = [ parts[0], value ]; + + self.triggerHandler( "setData" + parts[1] + "!", args ); + jQuery.data( this, key, value ); + self.triggerHandler( "changeData" + parts[1] + "!", args ); + }); + } + }, + + removeData: function( key ) { + return this.each(function() { + jQuery.removeData( this, key ); + }); + } +}); + +function dataAttr( elem, key, data ) { + // If nothing was found internally, try to fetch any + // data from the HTML5 data-* attribute + if ( data === undefined && elem.nodeType === 1 ) { + + var name = "data-" + key.replace( rmultiDash, "-$1" ).toLowerCase(); + + data = elem.getAttribute( name ); + + if ( typeof data === "string" ) { + try { + data = data === "true" ? true : + data === "false" ? false : + data === "null" ? null : + jQuery.isNumeric( data ) ? parseFloat( data ) : + rbrace.test( data ) ? jQuery.parseJSON( data ) : + data; + } catch( e ) {} + + // Make sure we set the data so it isn't changed later + jQuery.data( elem, key, data ); + + } else { + data = undefined; + } + } + + return data; +} + +// checks a cache object for emptiness +function isEmptyDataObject( obj ) { + for ( var name in obj ) { + + // if the public data object is empty, the private is still empty + if ( name === "data" && jQuery.isEmptyObject( obj[name] ) ) { + continue; + } + if ( name !== "toJSON" ) { + return false; + } + } + + return true; +} + + + + +function handleQueueMarkDefer( elem, type, src ) { + var deferDataKey = type + "defer", + queueDataKey = type + "queue", + markDataKey = type + "mark", + defer = jQuery._data( elem, deferDataKey ); + if ( defer && + ( src === "queue" || !jQuery._data(elem, queueDataKey) ) && + ( src === "mark" || !jQuery._data(elem, markDataKey) ) ) { + // Give room for hard-coded callbacks to fire first + // and eventually mark/queue something else on the element + setTimeout( function() { + if ( !jQuery._data( elem, queueDataKey ) && + !jQuery._data( elem, markDataKey ) ) { + jQuery.removeData( elem, deferDataKey, true ); + defer.fire(); + } + }, 0 ); + } +} + +jQuery.extend({ + + _mark: function( elem, type ) { + if ( elem ) { + type = ( type || "fx" ) + "mark"; + jQuery._data( elem, type, (jQuery._data( elem, type ) || 0) + 1 ); + } + }, + + _unmark: function( force, elem, type ) { + if ( force !== true ) { + type = elem; + elem = force; + force = false; + } + if ( elem ) { + type = type || "fx"; + var key = type + "mark", + count = force ? 0 : ( (jQuery._data( elem, key ) || 1) - 1 ); + if ( count ) { + jQuery._data( elem, key, count ); + } else { + jQuery.removeData( elem, key, true ); + handleQueueMarkDefer( elem, type, "mark" ); + } + } + }, + + queue: function( elem, type, data ) { + var q; + if ( elem ) { + type = ( type || "fx" ) + "queue"; + q = jQuery._data( elem, type ); + + // Speed up dequeue by getting out quickly if this is just a lookup + if ( data ) { + if ( !q || jQuery.isArray(data) ) { + q = jQuery._data( elem, type, jQuery.makeArray(data) ); + } else { + q.push( data ); + } + } + return q || []; + } + }, + + dequeue: function( elem, type ) { + type = type || "fx"; + + var queue = jQuery.queue( elem, type ), + fn = queue.shift(), + hooks = {}; + + // If the fx queue is dequeued, always remove the progress sentinel + if ( fn === "inprogress" ) { + fn = queue.shift(); + } + + if ( fn ) { + // Add a progress sentinel to prevent the fx queue from being + // automatically dequeued + if ( type === "fx" ) { + queue.unshift( "inprogress" ); + } + + jQuery._data( elem, type + ".run", hooks ); + fn.call( elem, function() { + jQuery.dequeue( elem, type ); + }, hooks ); + } + + if ( !queue.length ) { + jQuery.removeData( elem, type + "queue " + type + ".run", true ); + handleQueueMarkDefer( elem, type, "queue" ); + } + } +}); + +jQuery.fn.extend({ + queue: function( type, data ) { + if ( typeof type !== "string" ) { + data = type; + type = "fx"; + } + + if ( data === undefined ) { + return jQuery.queue( this[0], type ); + } + return this.each(function() { + var queue = jQuery.queue( this, type, data ); + + if ( type === "fx" && queue[0] !== "inprogress" ) { + jQuery.dequeue( this, type ); + } + }); + }, + dequeue: function( type ) { + return this.each(function() { + jQuery.dequeue( this, type ); + }); + }, + // Based off of the plugin by Clint Helfers, with permission. + // http://blindsignals.com/index.php/2009/07/jquery-delay/ + delay: function( time, type ) { + time = jQuery.fx ? jQuery.fx.speeds[ time ] || time : time; + type = type || "fx"; + + return this.queue( type, function( next, hooks ) { + var timeout = setTimeout( next, time ); + hooks.stop = function() { + clearTimeout( timeout ); + }; + }); + }, + clearQueue: function( type ) { + return this.queue( type || "fx", [] ); + }, + // Get a promise resolved when queues of a certain type + // are emptied (fx is the type by default) + promise: function( type, object ) { + if ( typeof type !== "string" ) { + object = type; + type = undefined; + } + type = type || "fx"; + var defer = jQuery.Deferred(), + elements = this, + i = elements.length, + count = 1, + deferDataKey = type + "defer", + queueDataKey = type + "queue", + markDataKey = type + "mark", + tmp; + function resolve() { + if ( !( --count ) ) { + defer.resolveWith( elements, [ elements ] ); + } + } + while( i-- ) { + if (( tmp = jQuery.data( elements[ i ], deferDataKey, undefined, true ) || + ( jQuery.data( elements[ i ], queueDataKey, undefined, true ) || + jQuery.data( elements[ i ], markDataKey, undefined, true ) ) && + jQuery.data( elements[ i ], deferDataKey, jQuery.Callbacks( "once memory" ), true ) )) { + count++; + tmp.add( resolve ); + } + } + resolve(); + return defer.promise(); + } +}); + + + + +var rclass = /[\n\t\r]/g, + rspace = /\s+/, + rreturn = /\r/g, + rtype = /^(?:button|input)$/i, + rfocusable = /^(?:button|input|object|select|textarea)$/i, + rclickable = /^a(?:rea)?$/i, + rboolean = /^(?:autofocus|autoplay|async|checked|controls|defer|disabled|hidden|loop|multiple|open|readonly|required|scoped|selected)$/i, + getSetAttribute = jQuery.support.getSetAttribute, + nodeHook, boolHook, fixSpecified; + +jQuery.fn.extend({ + attr: function( name, value ) { + return jQuery.access( this, name, value, true, jQuery.attr ); + }, + + removeAttr: function( name ) { + return this.each(function() { + jQuery.removeAttr( this, name ); + }); + }, + + prop: function( name, value ) { + return jQuery.access( this, name, value, true, jQuery.prop ); + }, + + removeProp: function( name ) { + name = jQuery.propFix[ name ] || name; + return this.each(function() { + // try/catch handles cases where IE balks (such as removing a property on window) + try { + this[ name ] = undefined; + delete this[ name ]; + } catch( e ) {} + }); + }, + + addClass: function( value ) { + var classNames, i, l, elem, + setClass, c, cl; + + if ( jQuery.isFunction( value ) ) { + return this.each(function( j ) { + jQuery( this ).addClass( value.call(this, j, this.className) ); + }); + } + + if ( value && typeof value === "string" ) { + classNames = value.split( rspace ); + + for ( i = 0, l = this.length; i < l; i++ ) { + elem = this[ i ]; + + if ( elem.nodeType === 1 ) { + if ( !elem.className && classNames.length === 1 ) { + elem.className = value; + + } else { + setClass = " " + elem.className + " "; + + for ( c = 0, cl = classNames.length; c < cl; c++ ) { + if ( !~setClass.indexOf( " " + classNames[ c ] + " " ) ) { + setClass += classNames[ c ] + " "; + } + } + elem.className = jQuery.trim( setClass ); + } + } + } + } + + return this; + }, + + removeClass: function( value ) { + var classNames, i, l, elem, className, c, cl; + + if ( jQuery.isFunction( value ) ) { + return this.each(function( j ) { + jQuery( this ).removeClass( value.call(this, j, this.className) ); + }); + } + + if ( (value && typeof value === "string") || value === undefined ) { + classNames = ( value || "" ).split( rspace ); + + for ( i = 0, l = this.length; i < l; i++ ) { + elem = this[ i ]; + + if ( elem.nodeType === 1 && elem.className ) { + if ( value ) { + className = (" " + elem.className + " ").replace( rclass, " " ); + for ( c = 0, cl = classNames.length; c < cl; c++ ) { + className = className.replace(" " + classNames[ c ] + " ", " "); + } + elem.className = jQuery.trim( className ); + + } else { + elem.className = ""; + } + } + } + } + + return this; + }, + + toggleClass: function( value, stateVal ) { + var type = typeof value, + isBool = typeof stateVal === "boolean"; + + if ( jQuery.isFunction( value ) ) { + return this.each(function( i ) { + jQuery( this ).toggleClass( value.call(this, i, this.className, stateVal), stateVal ); + }); + } + + return this.each(function() { + if ( type === "string" ) { + // toggle individual class names + var className, + i = 0, + self = jQuery( this ), + state = stateVal, + classNames = value.split( rspace ); + + while ( (className = classNames[ i++ ]) ) { + // check each className given, space seperated list + state = isBool ? state : !self.hasClass( className ); + self[ state ? "addClass" : "removeClass" ]( className ); + } + + } else if ( type === "undefined" || type === "boolean" ) { + if ( this.className ) { + // store className if set + jQuery._data( this, "__className__", this.className ); + } + + // toggle whole className + this.className = this.className || value === false ? "" : jQuery._data( this, "__className__" ) || ""; + } + }); + }, + + hasClass: function( selector ) { + var className = " " + selector + " ", + i = 0, + l = this.length; + for ( ; i < l; i++ ) { + if ( this[i].nodeType === 1 && (" " + this[i].className + " ").replace(rclass, " ").indexOf( className ) > -1 ) { + return true; + } + } + + return false; + }, + + val: function( value ) { + var hooks, ret, isFunction, + elem = this[0]; + + if ( !arguments.length ) { + if ( elem ) { + hooks = jQuery.valHooks[ elem.nodeName.toLowerCase() ] || jQuery.valHooks[ elem.type ]; + + if ( hooks && "get" in hooks && (ret = hooks.get( elem, "value" )) !== undefined ) { + return ret; + } + + ret = elem.value; + + return typeof ret === "string" ? + // handle most common string cases + ret.replace(rreturn, "") : + // handle cases where value is null/undef or number + ret == null ? "" : ret; + } + + return; + } + + isFunction = jQuery.isFunction( value ); + + return this.each(function( i ) { + var self = jQuery(this), val; + + if ( this.nodeType !== 1 ) { + return; + } + + if ( isFunction ) { + val = value.call( this, i, self.val() ); + } else { + val = value; + } + + // Treat null/undefined as ""; convert numbers to string + if ( val == null ) { + val = ""; + } else if ( typeof val === "number" ) { + val += ""; + } else if ( jQuery.isArray( val ) ) { + val = jQuery.map(val, function ( value ) { + return value == null ? "" : value + ""; + }); + } + + hooks = jQuery.valHooks[ this.nodeName.toLowerCase() ] || jQuery.valHooks[ this.type ]; + + // If set returns undefined, fall back to normal setting + if ( !hooks || !("set" in hooks) || hooks.set( this, val, "value" ) === undefined ) { + this.value = val; + } + }); + } +}); + +jQuery.extend({ + valHooks: { + option: { + get: function( elem ) { + // attributes.value is undefined in Blackberry 4.7 but + // uses .value. See #6932 + var val = elem.attributes.value; + return !val || val.specified ? elem.value : elem.text; + } + }, + select: { + get: function( elem ) { + var value, i, max, option, + index = elem.selectedIndex, + values = [], + options = elem.options, + one = elem.type === "select-one"; + + // Nothing was selected + if ( index < 0 ) { + return null; + } + + // Loop through all the selected options + i = one ? index : 0; + max = one ? index + 1 : options.length; + for ( ; i < max; i++ ) { + option = options[ i ]; + + // Don't return options that are disabled or in a disabled optgroup + if ( option.selected && (jQuery.support.optDisabled ? !option.disabled : option.getAttribute("disabled") === null) && + (!option.parentNode.disabled || !jQuery.nodeName( option.parentNode, "optgroup" )) ) { + + // Get the specific value for the option + value = jQuery( option ).val(); + + // We don't need an array for one selects + if ( one ) { + return value; + } + + // Multi-Selects return an array + values.push( value ); + } + } + + // Fixes Bug #2551 -- select.val() broken in IE after form.reset() + if ( one && !values.length && options.length ) { + return jQuery( options[ index ] ).val(); + } + + return values; + }, + + set: function( elem, value ) { + var values = jQuery.makeArray( value ); + + jQuery(elem).find("option").each(function() { + this.selected = jQuery.inArray( jQuery(this).val(), values ) >= 0; + }); + + if ( !values.length ) { + elem.selectedIndex = -1; + } + return values; + } + } + }, + + attrFn: { + val: true, + css: true, + html: true, + text: true, + data: true, + width: true, + height: true, + offset: true + }, + + attr: function( elem, name, value, pass ) { + var ret, hooks, notxml, + nType = elem.nodeType; + + // don't get/set attributes on text, comment and attribute nodes + if ( !elem || nType === 3 || nType === 8 || nType === 2 ) { + return; + } + + if ( pass && name in jQuery.attrFn ) { + return jQuery( elem )[ name ]( value ); + } + + // Fallback to prop when attributes are not supported + if ( typeof elem.getAttribute === "undefined" ) { + return jQuery.prop( elem, name, value ); + } + + notxml = nType !== 1 || !jQuery.isXMLDoc( elem ); + + // All attributes are lowercase + // Grab necessary hook if one is defined + if ( notxml ) { + name = name.toLowerCase(); + hooks = jQuery.attrHooks[ name ] || ( rboolean.test( name ) ? boolHook : nodeHook ); + } + + if ( value !== undefined ) { + + if ( value === null ) { + jQuery.removeAttr( elem, name ); + return; + + } else if ( hooks && "set" in hooks && notxml && (ret = hooks.set( elem, value, name )) !== undefined ) { + return ret; + + } else { + elem.setAttribute( name, "" + value ); + return value; + } + + } else if ( hooks && "get" in hooks && notxml && (ret = hooks.get( elem, name )) !== null ) { + return ret; + + } else { + + ret = elem.getAttribute( name ); + + // Non-existent attributes return null, we normalize to undefined + return ret === null ? + undefined : + ret; + } + }, + + removeAttr: function( elem, value ) { + var propName, attrNames, name, l, + i = 0; + + if ( value && elem.nodeType === 1 ) { + attrNames = value.toLowerCase().split( rspace ); + l = attrNames.length; + + for ( ; i < l; i++ ) { + name = attrNames[ i ]; + + if ( name ) { + propName = jQuery.propFix[ name ] || name; + + // See #9699 for explanation of this approach (setting first, then removal) + jQuery.attr( elem, name, "" ); + elem.removeAttribute( getSetAttribute ? name : propName ); + + // Set corresponding property to false for boolean attributes + if ( rboolean.test( name ) && propName in elem ) { + elem[ propName ] = false; + } + } + } + } + }, + + attrHooks: { + type: { + set: function( elem, value ) { + // We can't allow the type property to be changed (since it causes problems in IE) + if ( rtype.test( elem.nodeName ) && elem.parentNode ) { + jQuery.error( "type property can't be changed" ); + } else if ( !jQuery.support.radioValue && value === "radio" && jQuery.nodeName(elem, "input") ) { + // Setting the type on a radio button after the value resets the value in IE6-9 + // Reset value to it's default in case type is set after value + // This is for element creation + var val = elem.value; + elem.setAttribute( "type", value ); + if ( val ) { + elem.value = val; + } + return value; + } + } + }, + // Use the value property for back compat + // Use the nodeHook for button elements in IE6/7 (#1954) + value: { + get: function( elem, name ) { + if ( nodeHook && jQuery.nodeName( elem, "button" ) ) { + return nodeHook.get( elem, name ); + } + return name in elem ? + elem.value : + null; + }, + set: function( elem, value, name ) { + if ( nodeHook && jQuery.nodeName( elem, "button" ) ) { + return nodeHook.set( elem, value, name ); + } + // Does not return so that setAttribute is also used + elem.value = value; + } + } + }, + + propFix: { + tabindex: "tabIndex", + readonly: "readOnly", + "for": "htmlFor", + "class": "className", + maxlength: "maxLength", + cellspacing: "cellSpacing", + cellpadding: "cellPadding", + rowspan: "rowSpan", + colspan: "colSpan", + usemap: "useMap", + frameborder: "frameBorder", + contenteditable: "contentEditable" + }, + + prop: function( elem, name, value ) { + var ret, hooks, notxml, + nType = elem.nodeType; + + // don't get/set properties on text, comment and attribute nodes + if ( !elem || nType === 3 || nType === 8 || nType === 2 ) { + return; + } + + notxml = nType !== 1 || !jQuery.isXMLDoc( elem ); + + if ( notxml ) { + // Fix name and attach hooks + name = jQuery.propFix[ name ] || name; + hooks = jQuery.propHooks[ name ]; + } + + if ( value !== undefined ) { + if ( hooks && "set" in hooks && (ret = hooks.set( elem, value, name )) !== undefined ) { + return ret; + + } else { + return ( elem[ name ] = value ); + } + + } else { + if ( hooks && "get" in hooks && (ret = hooks.get( elem, name )) !== null ) { + return ret; + + } else { + return elem[ name ]; + } + } + }, + + propHooks: { + tabIndex: { + get: function( elem ) { + // elem.tabIndex doesn't always return the correct value when it hasn't been explicitly set + // http://fluidproject.org/blog/2008/01/09/getting-setting-and-removing-tabindex-values-with-javascript/ + var attributeNode = elem.getAttributeNode("tabindex"); + + return attributeNode && attributeNode.specified ? + parseInt( attributeNode.value, 10 ) : + rfocusable.test( elem.nodeName ) || rclickable.test( elem.nodeName ) && elem.href ? + 0 : + undefined; + } + } + } +}); + +// Add the tabIndex propHook to attrHooks for back-compat (different case is intentional) +jQuery.attrHooks.tabindex = jQuery.propHooks.tabIndex; + +// Hook for boolean attributes +boolHook = { + get: function( elem, name ) { + // Align boolean attributes with corresponding properties + // Fall back to attribute presence where some booleans are not supported + var attrNode, + property = jQuery.prop( elem, name ); + return property === true || typeof property !== "boolean" && ( attrNode = elem.getAttributeNode(name) ) && attrNode.nodeValue !== false ? + name.toLowerCase() : + undefined; + }, + set: function( elem, value, name ) { + var propName; + if ( value === false ) { + // Remove boolean attributes when set to false + jQuery.removeAttr( elem, name ); + } else { + // value is true since we know at this point it's type boolean and not false + // Set boolean attributes to the same name and set the DOM property + propName = jQuery.propFix[ name ] || name; + if ( propName in elem ) { + // Only set the IDL specifically if it already exists on the element + elem[ propName ] = true; + } + + elem.setAttribute( name, name.toLowerCase() ); + } + return name; + } +}; + +// IE6/7 do not support getting/setting some attributes with get/setAttribute +if ( !getSetAttribute ) { + + fixSpecified = { + name: true, + id: true + }; + + // Use this for any attribute in IE6/7 + // This fixes almost every IE6/7 issue + nodeHook = jQuery.valHooks.button = { + get: function( elem, name ) { + var ret; + ret = elem.getAttributeNode( name ); + return ret && ( fixSpecified[ name ] ? ret.nodeValue !== "" : ret.specified ) ? + ret.nodeValue : + undefined; + }, + set: function( elem, value, name ) { + // Set the existing or create a new attribute node + var ret = elem.getAttributeNode( name ); + if ( !ret ) { + ret = document.createAttribute( name ); + elem.setAttributeNode( ret ); + } + return ( ret.nodeValue = value + "" ); + } + }; + + // Apply the nodeHook to tabindex + jQuery.attrHooks.tabindex.set = nodeHook.set; + + // Set width and height to auto instead of 0 on empty string( Bug #8150 ) + // This is for removals + jQuery.each([ "width", "height" ], function( i, name ) { + jQuery.attrHooks[ name ] = jQuery.extend( jQuery.attrHooks[ name ], { + set: function( elem, value ) { + if ( value === "" ) { + elem.setAttribute( name, "auto" ); + return value; + } + } + }); + }); + + // Set contenteditable to false on removals(#10429) + // Setting to empty string throws an error as an invalid value + jQuery.attrHooks.contenteditable = { + get: nodeHook.get, + set: function( elem, value, name ) { + if ( value === "" ) { + value = "false"; + } + nodeHook.set( elem, value, name ); + } + }; +} + + +// Some attributes require a special call on IE +if ( !jQuery.support.hrefNormalized ) { + jQuery.each([ "href", "src", "width", "height" ], function( i, name ) { + jQuery.attrHooks[ name ] = jQuery.extend( jQuery.attrHooks[ name ], { + get: function( elem ) { + var ret = elem.getAttribute( name, 2 ); + return ret === null ? undefined : ret; + } + }); + }); +} + +if ( !jQuery.support.style ) { + jQuery.attrHooks.style = { + get: function( elem ) { + // Return undefined in the case of empty string + // Normalize to lowercase since IE uppercases css property names + return elem.style.cssText.toLowerCase() || undefined; + }, + set: function( elem, value ) { + return ( elem.style.cssText = "" + value ); + } + }; +} + +// Safari mis-reports the default selected property of an option +// Accessing the parent's selectedIndex property fixes it +if ( !jQuery.support.optSelected ) { + jQuery.propHooks.selected = jQuery.extend( jQuery.propHooks.selected, { + get: function( elem ) { + var parent = elem.parentNode; + + if ( parent ) { + parent.selectedIndex; + + // Make sure that it also works with optgroups, see #5701 + if ( parent.parentNode ) { + parent.parentNode.selectedIndex; + } + } + return null; + } + }); +} + +// IE6/7 call enctype encoding +if ( !jQuery.support.enctype ) { + jQuery.propFix.enctype = "encoding"; +} + +// Radios and checkboxes getter/setter +if ( !jQuery.support.checkOn ) { + jQuery.each([ "radio", "checkbox" ], function() { + jQuery.valHooks[ this ] = { + get: function( elem ) { + // Handle the case where in Webkit "" is returned instead of "on" if a value isn't specified + return elem.getAttribute("value") === null ? "on" : elem.value; + } + }; + }); +} +jQuery.each([ "radio", "checkbox" ], function() { + jQuery.valHooks[ this ] = jQuery.extend( jQuery.valHooks[ this ], { + set: function( elem, value ) { + if ( jQuery.isArray( value ) ) { + return ( elem.checked = jQuery.inArray( jQuery(elem).val(), value ) >= 0 ); + } + } + }); +}); + + + + +var rformElems = /^(?:textarea|input|select)$/i, + rtypenamespace = /^([^\.]*)?(?:\.(.+))?$/, + rhoverHack = /\bhover(\.\S+)?\b/, + rkeyEvent = /^key/, + rmouseEvent = /^(?:mouse|contextmenu)|click/, + rfocusMorph = /^(?:focusinfocus|focusoutblur)$/, + rquickIs = /^(\w*)(?:#([\w\-]+))?(?:\.([\w\-]+))?$/, + quickParse = function( selector ) { + var quick = rquickIs.exec( selector ); + if ( quick ) { + // 0 1 2 3 + // [ _, tag, id, class ] + quick[1] = ( quick[1] || "" ).toLowerCase(); + quick[3] = quick[3] && new RegExp( "(?:^|\\s)" + quick[3] + "(?:\\s|$)" ); + } + return quick; + }, + quickIs = function( elem, m ) { + var attrs = elem.attributes || {}; + return ( + (!m[1] || elem.nodeName.toLowerCase() === m[1]) && + (!m[2] || (attrs.id || {}).value === m[2]) && + (!m[3] || m[3].test( (attrs[ "class" ] || {}).value )) + ); + }, + hoverHack = function( events ) { + return jQuery.event.special.hover ? events : events.replace( rhoverHack, "mouseenter$1 mouseleave$1" ); + }; + +/* + * Helper functions for managing events -- not part of the public interface. + * Props to Dean Edwards' addEvent library for many of the ideas. + */ +jQuery.event = { + + add: function( elem, types, handler, data, selector ) { + + var elemData, eventHandle, events, + t, tns, type, namespaces, handleObj, + handleObjIn, quick, handlers, special; + + // Don't attach events to noData or text/comment nodes (allow plain objects tho) + if ( elem.nodeType === 3 || elem.nodeType === 8 || !types || !handler || !(elemData = jQuery._data( elem )) ) { + return; + } + + // Caller can pass in an object of custom data in lieu of the handler + if ( handler.handler ) { + handleObjIn = handler; + handler = handleObjIn.handler; + } + + // Make sure that the handler has a unique ID, used to find/remove it later + if ( !handler.guid ) { + handler.guid = jQuery.guid++; + } + + // Init the element's event structure and main handler, if this is the first + events = elemData.events; + if ( !events ) { + elemData.events = events = {}; + } + eventHandle = elemData.handle; + if ( !eventHandle ) { + elemData.handle = eventHandle = function( e ) { + // Discard the second event of a jQuery.event.trigger() and + // when an event is called after a page has unloaded + return typeof jQuery !== "undefined" && (!e || jQuery.event.triggered !== e.type) ? + jQuery.event.dispatch.apply( eventHandle.elem, arguments ) : + undefined; + }; + // Add elem as a property of the handle fn to prevent a memory leak with IE non-native events + eventHandle.elem = elem; + } + + // Handle multiple events separated by a space + // jQuery(...).bind("mouseover mouseout", fn); + types = jQuery.trim( hoverHack(types) ).split( " " ); + for ( t = 0; t < types.length; t++ ) { + + tns = rtypenamespace.exec( types[t] ) || []; + type = tns[1]; + namespaces = ( tns[2] || "" ).split( "." ).sort(); + + // If event changes its type, use the special event handlers for the changed type + special = jQuery.event.special[ type ] || {}; + + // If selector defined, determine special event api type, otherwise given type + type = ( selector ? special.delegateType : special.bindType ) || type; + + // Update special based on newly reset type + special = jQuery.event.special[ type ] || {}; + + // handleObj is passed to all event handlers + handleObj = jQuery.extend({ + type: type, + origType: tns[1], + data: data, + handler: handler, + guid: handler.guid, + selector: selector, + quick: quickParse( selector ), + namespace: namespaces.join(".") + }, handleObjIn ); + + // Init the event handler queue if we're the first + handlers = events[ type ]; + if ( !handlers ) { + handlers = events[ type ] = []; + handlers.delegateCount = 0; + + // Only use addEventListener/attachEvent if the special events handler returns false + if ( !special.setup || special.setup.call( elem, data, namespaces, eventHandle ) === false ) { + // Bind the global event handler to the element + if ( elem.addEventListener ) { + elem.addEventListener( type, eventHandle, false ); + + } else if ( elem.attachEvent ) { + elem.attachEvent( "on" + type, eventHandle ); + } + } + } + + if ( special.add ) { + special.add.call( elem, handleObj ); + + if ( !handleObj.handler.guid ) { + handleObj.handler.guid = handler.guid; + } + } + + // Add to the element's handler list, delegates in front + if ( selector ) { + handlers.splice( handlers.delegateCount++, 0, handleObj ); + } else { + handlers.push( handleObj ); + } + + // Keep track of which events have ever been used, for event optimization + jQuery.event.global[ type ] = true; + } + + // Nullify elem to prevent memory leaks in IE + elem = null; + }, + + global: {}, + + // Detach an event or set of events from an element + remove: function( elem, types, handler, selector, mappedTypes ) { + + var elemData = jQuery.hasData( elem ) && jQuery._data( elem ), + t, tns, type, origType, namespaces, origCount, + j, events, special, handle, eventType, handleObj; + + if ( !elemData || !(events = elemData.events) ) { + return; + } + + // Once for each type.namespace in types; type may be omitted + types = jQuery.trim( hoverHack( types || "" ) ).split(" "); + for ( t = 0; t < types.length; t++ ) { + tns = rtypenamespace.exec( types[t] ) || []; + type = origType = tns[1]; + namespaces = tns[2]; + + // Unbind all events (on this namespace, if provided) for the element + if ( !type ) { + for ( type in events ) { + jQuery.event.remove( elem, type + types[ t ], handler, selector, true ); + } + continue; + } + + special = jQuery.event.special[ type ] || {}; + type = ( selector? special.delegateType : special.bindType ) || type; + eventType = events[ type ] || []; + origCount = eventType.length; + namespaces = namespaces ? new RegExp("(^|\\.)" + namespaces.split(".").sort().join("\\.(?:.*\\.)?") + "(\\.|$)") : null; + + // Remove matching events + for ( j = 0; j < eventType.length; j++ ) { + handleObj = eventType[ j ]; + + if ( ( mappedTypes || origType === handleObj.origType ) && + ( !handler || handler.guid === handleObj.guid ) && + ( !namespaces || namespaces.test( handleObj.namespace ) ) && + ( !selector || selector === handleObj.selector || selector === "**" && handleObj.selector ) ) { + eventType.splice( j--, 1 ); + + if ( handleObj.selector ) { + eventType.delegateCount--; + } + if ( special.remove ) { + special.remove.call( elem, handleObj ); + } + } + } + + // Remove generic event handler if we removed something and no more handlers exist + // (avoids potential for endless recursion during removal of special event handlers) + if ( eventType.length === 0 && origCount !== eventType.length ) { + if ( !special.teardown || special.teardown.call( elem, namespaces ) === false ) { + jQuery.removeEvent( elem, type, elemData.handle ); + } + + delete events[ type ]; + } + } + + // Remove the expando if it's no longer used + if ( jQuery.isEmptyObject( events ) ) { + handle = elemData.handle; + if ( handle ) { + handle.elem = null; + } + + // removeData also checks for emptiness and clears the expando if empty + // so use it instead of delete + jQuery.removeData( elem, [ "events", "handle" ], true ); + } + }, + + // Events that are safe to short-circuit if no handlers are attached. + // Native DOM events should not be added, they may have inline handlers. + customEvent: { + "getData": true, + "setData": true, + "changeData": true + }, + + trigger: function( event, data, elem, onlyHandlers ) { + // Don't do events on text and comment nodes + if ( elem && (elem.nodeType === 3 || elem.nodeType === 8) ) { + return; + } + + // Event object or event type + var type = event.type || event, + namespaces = [], + cache, exclusive, i, cur, old, ontype, special, handle, eventPath, bubbleType; + + // focus/blur morphs to focusin/out; ensure we're not firing them right now + if ( rfocusMorph.test( type + jQuery.event.triggered ) ) { + return; + } + + if ( type.indexOf( "!" ) >= 0 ) { + // Exclusive events trigger only for the exact event (no namespaces) + type = type.slice(0, -1); + exclusive = true; + } + + if ( type.indexOf( "." ) >= 0 ) { + // Namespaced trigger; create a regexp to match event type in handle() + namespaces = type.split("."); + type = namespaces.shift(); + namespaces.sort(); + } + + if ( (!elem || jQuery.event.customEvent[ type ]) && !jQuery.event.global[ type ] ) { + // No jQuery handlers for this event type, and it can't have inline handlers + return; + } + + // Caller can pass in an Event, Object, or just an event type string + event = typeof event === "object" ? + // jQuery.Event object + event[ jQuery.expando ] ? event : + // Object literal + new jQuery.Event( type, event ) : + // Just the event type (string) + new jQuery.Event( type ); + + event.type = type; + event.isTrigger = true; + event.exclusive = exclusive; + event.namespace = namespaces.join( "." ); + event.namespace_re = event.namespace? new RegExp("(^|\\.)" + namespaces.join("\\.(?:.*\\.)?") + "(\\.|$)") : null; + ontype = type.indexOf( ":" ) < 0 ? "on" + type : ""; + + // Handle a global trigger + if ( !elem ) { + + // TODO: Stop taunting the data cache; remove global events and always attach to document + cache = jQuery.cache; + for ( i in cache ) { + if ( cache[ i ].events && cache[ i ].events[ type ] ) { + jQuery.event.trigger( event, data, cache[ i ].handle.elem, true ); + } + } + return; + } + + // Clean up the event in case it is being reused + event.result = undefined; + if ( !event.target ) { + event.target = elem; + } + + // Clone any incoming data and prepend the event, creating the handler arg list + data = data != null ? jQuery.makeArray( data ) : []; + data.unshift( event ); + + // Allow special events to draw outside the lines + special = jQuery.event.special[ type ] || {}; + if ( special.trigger && special.trigger.apply( elem, data ) === false ) { + return; + } + + // Determine event propagation path in advance, per W3C events spec (#9951) + // Bubble up to document, then to window; watch for a global ownerDocument var (#9724) + eventPath = [[ elem, special.bindType || type ]]; + if ( !onlyHandlers && !special.noBubble && !jQuery.isWindow( elem ) ) { + + bubbleType = special.delegateType || type; + cur = rfocusMorph.test( bubbleType + type ) ? elem : elem.parentNode; + old = null; + for ( ; cur; cur = cur.parentNode ) { + eventPath.push([ cur, bubbleType ]); + old = cur; + } + + // Only add window if we got to document (e.g., not plain obj or detached DOM) + if ( old && old === elem.ownerDocument ) { + eventPath.push([ old.defaultView || old.parentWindow || window, bubbleType ]); + } + } + + // Fire handlers on the event path + for ( i = 0; i < eventPath.length && !event.isPropagationStopped(); i++ ) { + + cur = eventPath[i][0]; + event.type = eventPath[i][1]; + + handle = ( jQuery._data( cur, "events" ) || {} )[ event.type ] && jQuery._data( cur, "handle" ); + if ( handle ) { + handle.apply( cur, data ); + } + // Note that this is a bare JS function and not a jQuery handler + handle = ontype && cur[ ontype ]; + if ( handle && jQuery.acceptData( cur ) && handle.apply( cur, data ) === false ) { + event.preventDefault(); + } + } + event.type = type; + + // If nobody prevented the default action, do it now + if ( !onlyHandlers && !event.isDefaultPrevented() ) { + + if ( (!special._default || special._default.apply( elem.ownerDocument, data ) === false) && + !(type === "click" && jQuery.nodeName( elem, "a" )) && jQuery.acceptData( elem ) ) { + + // Call a native DOM method on the target with the same name name as the event. + // Can't use an .isFunction() check here because IE6/7 fails that test. + // Don't do default actions on window, that's where global variables be (#6170) + // IE<9 dies on focus/blur to hidden element (#1486) + if ( ontype && elem[ type ] && ((type !== "focus" && type !== "blur") || event.target.offsetWidth !== 0) && !jQuery.isWindow( elem ) ) { + + // Don't re-trigger an onFOO event when we call its FOO() method + old = elem[ ontype ]; + + if ( old ) { + elem[ ontype ] = null; + } + + // Prevent re-triggering of the same event, since we already bubbled it above + jQuery.event.triggered = type; + elem[ type ](); + jQuery.event.triggered = undefined; + + if ( old ) { + elem[ ontype ] = old; + } + } + } + } + + return event.result; + }, + + dispatch: function( event ) { + + // Make a writable jQuery.Event from the native event object + event = jQuery.event.fix( event || window.event ); + + var handlers = ( (jQuery._data( this, "events" ) || {} )[ event.type ] || []), + delegateCount = handlers.delegateCount, + args = [].slice.call( arguments, 0 ), + run_all = !event.exclusive && !event.namespace, + handlerQueue = [], + i, j, cur, jqcur, ret, selMatch, matched, matches, handleObj, sel, related; + + // Use the fix-ed jQuery.Event rather than the (read-only) native event + args[0] = event; + event.delegateTarget = this; + + // Determine handlers that should run if there are delegated events + // Avoid disabled elements in IE (#6911) and non-left-click bubbling in Firefox (#3861) + if ( delegateCount && !event.target.disabled && !(event.button && event.type === "click") ) { + + // Pregenerate a single jQuery object for reuse with .is() + jqcur = jQuery(this); + jqcur.context = this.ownerDocument || this; + + for ( cur = event.target; cur != this; cur = cur.parentNode || this ) { + selMatch = {}; + matches = []; + jqcur[0] = cur; + for ( i = 0; i < delegateCount; i++ ) { + handleObj = handlers[ i ]; + sel = handleObj.selector; + + if ( selMatch[ sel ] === undefined ) { + selMatch[ sel ] = ( + handleObj.quick ? quickIs( cur, handleObj.quick ) : jqcur.is( sel ) + ); + } + if ( selMatch[ sel ] ) { + matches.push( handleObj ); + } + } + if ( matches.length ) { + handlerQueue.push({ elem: cur, matches: matches }); + } + } + } + + // Add the remaining (directly-bound) handlers + if ( handlers.length > delegateCount ) { + handlerQueue.push({ elem: this, matches: handlers.slice( delegateCount ) }); + } + + // Run delegates first; they may want to stop propagation beneath us + for ( i = 0; i < handlerQueue.length && !event.isPropagationStopped(); i++ ) { + matched = handlerQueue[ i ]; + event.currentTarget = matched.elem; + + for ( j = 0; j < matched.matches.length && !event.isImmediatePropagationStopped(); j++ ) { + handleObj = matched.matches[ j ]; + + // Triggered event must either 1) be non-exclusive and have no namespace, or + // 2) have namespace(s) a subset or equal to those in the bound event (both can have no namespace). + if ( run_all || (!event.namespace && !handleObj.namespace) || event.namespace_re && event.namespace_re.test( handleObj.namespace ) ) { + + event.data = handleObj.data; + event.handleObj = handleObj; + + ret = ( (jQuery.event.special[ handleObj.origType ] || {}).handle || handleObj.handler ) + .apply( matched.elem, args ); + + if ( ret !== undefined ) { + event.result = ret; + if ( ret === false ) { + event.preventDefault(); + event.stopPropagation(); + } + } + } + } + } + + return event.result; + }, + + // Includes some event props shared by KeyEvent and MouseEvent + // *** attrChange attrName relatedNode srcElement are not normalized, non-W3C, deprecated, will be removed in 1.8 *** + props: "attrChange attrName relatedNode srcElement altKey bubbles cancelable ctrlKey currentTarget eventPhase metaKey relatedTarget shiftKey target timeStamp view which".split(" "), + + fixHooks: {}, + + keyHooks: { + props: "char charCode key keyCode".split(" "), + filter: function( event, original ) { + + // Add which for key events + if ( event.which == null ) { + event.which = original.charCode != null ? original.charCode : original.keyCode; + } + + return event; + } + }, + + mouseHooks: { + props: "button buttons clientX clientY fromElement offsetX offsetY pageX pageY screenX screenY toElement".split(" "), + filter: function( event, original ) { + var eventDoc, doc, body, + button = original.button, + fromElement = original.fromElement; + + // Calculate pageX/Y if missing and clientX/Y available + if ( event.pageX == null && original.clientX != null ) { + eventDoc = event.target.ownerDocument || document; + doc = eventDoc.documentElement; + body = eventDoc.body; + + event.pageX = original.clientX + ( doc && doc.scrollLeft || body && body.scrollLeft || 0 ) - ( doc && doc.clientLeft || body && body.clientLeft || 0 ); + event.pageY = original.clientY + ( doc && doc.scrollTop || body && body.scrollTop || 0 ) - ( doc && doc.clientTop || body && body.clientTop || 0 ); + } + + // Add relatedTarget, if necessary + if ( !event.relatedTarget && fromElement ) { + event.relatedTarget = fromElement === event.target ? original.toElement : fromElement; + } + + // Add which for click: 1 === left; 2 === middle; 3 === right + // Note: button is not normalized, so don't use it + if ( !event.which && button !== undefined ) { + event.which = ( button & 1 ? 1 : ( button & 2 ? 3 : ( button & 4 ? 2 : 0 ) ) ); + } + + return event; + } + }, + + fix: function( event ) { + if ( event[ jQuery.expando ] ) { + return event; + } + + // Create a writable copy of the event object and normalize some properties + var i, prop, + originalEvent = event, + fixHook = jQuery.event.fixHooks[ event.type ] || {}, + copy = fixHook.props ? this.props.concat( fixHook.props ) : this.props; + + event = jQuery.Event( originalEvent ); + + for ( i = copy.length; i; ) { + prop = copy[ --i ]; + event[ prop ] = originalEvent[ prop ]; + } + + // Fix target property, if necessary (#1925, IE 6/7/8 & Safari2) + if ( !event.target ) { + event.target = originalEvent.srcElement || document; + } + + // Target should not be a text node (#504, Safari) + if ( event.target.nodeType === 3 ) { + event.target = event.target.parentNode; + } + + // For mouse/key events; add metaKey if it's not there (#3368, IE6/7/8) + if ( event.metaKey === undefined ) { + event.metaKey = event.ctrlKey; + } + + return fixHook.filter? fixHook.filter( event, originalEvent ) : event; + }, + + special: { + ready: { + // Make sure the ready event is setup + setup: jQuery.bindReady + }, + + load: { + // Prevent triggered image.load events from bubbling to window.load + noBubble: true + }, + + focus: { + delegateType: "focusin" + }, + blur: { + delegateType: "focusout" + }, + + beforeunload: { + setup: function( data, namespaces, eventHandle ) { + // We only want to do this special case on windows + if ( jQuery.isWindow( this ) ) { + this.onbeforeunload = eventHandle; + } + }, + + teardown: function( namespaces, eventHandle ) { + if ( this.onbeforeunload === eventHandle ) { + this.onbeforeunload = null; + } + } + } + }, + + simulate: function( type, elem, event, bubble ) { + // Piggyback on a donor event to simulate a different one. + // Fake originalEvent to avoid donor's stopPropagation, but if the + // simulated event prevents default then we do the same on the donor. + var e = jQuery.extend( + new jQuery.Event(), + event, + { type: type, + isSimulated: true, + originalEvent: {} + } + ); + if ( bubble ) { + jQuery.event.trigger( e, null, elem ); + } else { + jQuery.event.dispatch.call( elem, e ); + } + if ( e.isDefaultPrevented() ) { + event.preventDefault(); + } + } +}; + +// Some plugins are using, but it's undocumented/deprecated and will be removed. +// The 1.7 special event interface should provide all the hooks needed now. +jQuery.event.handle = jQuery.event.dispatch; + +jQuery.removeEvent = document.removeEventListener ? + function( elem, type, handle ) { + if ( elem.removeEventListener ) { + elem.removeEventListener( type, handle, false ); + } + } : + function( elem, type, handle ) { + if ( elem.detachEvent ) { + elem.detachEvent( "on" + type, handle ); + } + }; + +jQuery.Event = function( src, props ) { + // Allow instantiation without the 'new' keyword + if ( !(this instanceof jQuery.Event) ) { + return new jQuery.Event( src, props ); + } + + // Event object + if ( src && src.type ) { + this.originalEvent = src; + this.type = src.type; + + // Events bubbling up the document may have been marked as prevented + // by a handler lower down the tree; reflect the correct value. + this.isDefaultPrevented = ( src.defaultPrevented || src.returnValue === false || + src.getPreventDefault && src.getPreventDefault() ) ? returnTrue : returnFalse; + + // Event type + } else { + this.type = src; + } + + // Put explicitly provided properties onto the event object + if ( props ) { + jQuery.extend( this, props ); + } + + // Create a timestamp if incoming event doesn't have one + this.timeStamp = src && src.timeStamp || jQuery.now(); + + // Mark it as fixed + this[ jQuery.expando ] = true; +}; + +function returnFalse() { + return false; +} +function returnTrue() { + return true; +} + +// jQuery.Event is based on DOM3 Events as specified by the ECMAScript Language Binding +// http://www.w3.org/TR/2003/WD-DOM-Level-3-Events-20030331/ecma-script-binding.html +jQuery.Event.prototype = { + preventDefault: function() { + this.isDefaultPrevented = returnTrue; + + var e = this.originalEvent; + if ( !e ) { + return; + } + + // if preventDefault exists run it on the original event + if ( e.preventDefault ) { + e.preventDefault(); + + // otherwise set the returnValue property of the original event to false (IE) + } else { + e.returnValue = false; + } + }, + stopPropagation: function() { + this.isPropagationStopped = returnTrue; + + var e = this.originalEvent; + if ( !e ) { + return; + } + // if stopPropagation exists run it on the original event + if ( e.stopPropagation ) { + e.stopPropagation(); + } + // otherwise set the cancelBubble property of the original event to true (IE) + e.cancelBubble = true; + }, + stopImmediatePropagation: function() { + this.isImmediatePropagationStopped = returnTrue; + this.stopPropagation(); + }, + isDefaultPrevented: returnFalse, + isPropagationStopped: returnFalse, + isImmediatePropagationStopped: returnFalse +}; + +// Create mouseenter/leave events using mouseover/out and event-time checks +jQuery.each({ + mouseenter: "mouseover", + mouseleave: "mouseout" +}, function( orig, fix ) { + jQuery.event.special[ orig ] = { + delegateType: fix, + bindType: fix, + + handle: function( event ) { + var target = this, + related = event.relatedTarget, + handleObj = event.handleObj, + selector = handleObj.selector, + ret; + + // For mousenter/leave call the handler if related is outside the target. + // NB: No relatedTarget if the mouse left/entered the browser window + if ( !related || (related !== target && !jQuery.contains( target, related )) ) { + event.type = handleObj.origType; + ret = handleObj.handler.apply( this, arguments ); + event.type = fix; + } + return ret; + } + }; +}); + +// IE submit delegation +if ( !jQuery.support.submitBubbles ) { + + jQuery.event.special.submit = { + setup: function() { + // Only need this for delegated form submit events + if ( jQuery.nodeName( this, "form" ) ) { + return false; + } + + // Lazy-add a submit handler when a descendant form may potentially be submitted + jQuery.event.add( this, "click._submit keypress._submit", function( e ) { + // Node name check avoids a VML-related crash in IE (#9807) + var elem = e.target, + form = jQuery.nodeName( elem, "input" ) || jQuery.nodeName( elem, "button" ) ? elem.form : undefined; + if ( form && !form._submit_attached ) { + jQuery.event.add( form, "submit._submit", function( event ) { + // If form was submitted by the user, bubble the event up the tree + if ( this.parentNode && !event.isTrigger ) { + jQuery.event.simulate( "submit", this.parentNode, event, true ); + } + }); + form._submit_attached = true; + } + }); + // return undefined since we don't need an event listener + }, + + teardown: function() { + // Only need this for delegated form submit events + if ( jQuery.nodeName( this, "form" ) ) { + return false; + } + + // Remove delegated handlers; cleanData eventually reaps submit handlers attached above + jQuery.event.remove( this, "._submit" ); + } + }; +} + +// IE change delegation and checkbox/radio fix +if ( !jQuery.support.changeBubbles ) { + + jQuery.event.special.change = { + + setup: function() { + + if ( rformElems.test( this.nodeName ) ) { + // IE doesn't fire change on a check/radio until blur; trigger it on click + // after a propertychange. Eat the blur-change in special.change.handle. + // This still fires onchange a second time for check/radio after blur. + if ( this.type === "checkbox" || this.type === "radio" ) { + jQuery.event.add( this, "propertychange._change", function( event ) { + if ( event.originalEvent.propertyName === "checked" ) { + this._just_changed = true; + } + }); + jQuery.event.add( this, "click._change", function( event ) { + if ( this._just_changed && !event.isTrigger ) { + this._just_changed = false; + jQuery.event.simulate( "change", this, event, true ); + } + }); + } + return false; + } + // Delegated event; lazy-add a change handler on descendant inputs + jQuery.event.add( this, "beforeactivate._change", function( e ) { + var elem = e.target; + + if ( rformElems.test( elem.nodeName ) && !elem._change_attached ) { + jQuery.event.add( elem, "change._change", function( event ) { + if ( this.parentNode && !event.isSimulated && !event.isTrigger ) { + jQuery.event.simulate( "change", this.parentNode, event, true ); + } + }); + elem._change_attached = true; + } + }); + }, + + handle: function( event ) { + var elem = event.target; + + // Swallow native change events from checkbox/radio, we already triggered them above + if ( this !== elem || event.isSimulated || event.isTrigger || (elem.type !== "radio" && elem.type !== "checkbox") ) { + return event.handleObj.handler.apply( this, arguments ); + } + }, + + teardown: function() { + jQuery.event.remove( this, "._change" ); + + return rformElems.test( this.nodeName ); + } + }; +} + +// Create "bubbling" focus and blur events +if ( !jQuery.support.focusinBubbles ) { + jQuery.each({ focus: "focusin", blur: "focusout" }, function( orig, fix ) { + + // Attach a single capturing handler while someone wants focusin/focusout + var attaches = 0, + handler = function( event ) { + jQuery.event.simulate( fix, event.target, jQuery.event.fix( event ), true ); + }; + + jQuery.event.special[ fix ] = { + setup: function() { + if ( attaches++ === 0 ) { + document.addEventListener( orig, handler, true ); + } + }, + teardown: function() { + if ( --attaches === 0 ) { + document.removeEventListener( orig, handler, true ); + } + } + }; + }); +} + +jQuery.fn.extend({ + + on: function( types, selector, data, fn, /*INTERNAL*/ one ) { + var origFn, type; + + // Types can be a map of types/handlers + if ( typeof types === "object" ) { + // ( types-Object, selector, data ) + if ( typeof selector !== "string" ) { + // ( types-Object, data ) + data = selector; + selector = undefined; + } + for ( type in types ) { + this.on( type, selector, data, types[ type ], one ); + } + return this; + } + + if ( data == null && fn == null ) { + // ( types, fn ) + fn = selector; + data = selector = undefined; + } else if ( fn == null ) { + if ( typeof selector === "string" ) { + // ( types, selector, fn ) + fn = data; + data = undefined; + } else { + // ( types, data, fn ) + fn = data; + data = selector; + selector = undefined; + } + } + if ( fn === false ) { + fn = returnFalse; + } else if ( !fn ) { + return this; + } + + if ( one === 1 ) { + origFn = fn; + fn = function( event ) { + // Can use an empty set, since event contains the info + jQuery().off( event ); + return origFn.apply( this, arguments ); + }; + // Use same guid so caller can remove using origFn + fn.guid = origFn.guid || ( origFn.guid = jQuery.guid++ ); + } + return this.each( function() { + jQuery.event.add( this, types, fn, data, selector ); + }); + }, + one: function( types, selector, data, fn ) { + return this.on.call( this, types, selector, data, fn, 1 ); + }, + off: function( types, selector, fn ) { + if ( types && types.preventDefault && types.handleObj ) { + // ( event ) dispatched jQuery.Event + var handleObj = types.handleObj; + jQuery( types.delegateTarget ).off( + handleObj.namespace? handleObj.type + "." + handleObj.namespace : handleObj.type, + handleObj.selector, + handleObj.handler + ); + return this; + } + if ( typeof types === "object" ) { + // ( types-object [, selector] ) + for ( var type in types ) { + this.off( type, selector, types[ type ] ); + } + return this; + } + if ( selector === false || typeof selector === "function" ) { + // ( types [, fn] ) + fn = selector; + selector = undefined; + } + if ( fn === false ) { + fn = returnFalse; + } + return this.each(function() { + jQuery.event.remove( this, types, fn, selector ); + }); + }, + + bind: function( types, data, fn ) { + return this.on( types, null, data, fn ); + }, + unbind: function( types, fn ) { + return this.off( types, null, fn ); + }, + + live: function( types, data, fn ) { + jQuery( this.context ).on( types, this.selector, data, fn ); + return this; + }, + die: function( types, fn ) { + jQuery( this.context ).off( types, this.selector || "**", fn ); + return this; + }, + + delegate: function( selector, types, data, fn ) { + return this.on( types, selector, data, fn ); + }, + undelegate: function( selector, types, fn ) { + // ( namespace ) or ( selector, types [, fn] ) + return arguments.length == 1? this.off( selector, "**" ) : this.off( types, selector, fn ); + }, + + trigger: function( type, data ) { + return this.each(function() { + jQuery.event.trigger( type, data, this ); + }); + }, + triggerHandler: function( type, data ) { + if ( this[0] ) { + return jQuery.event.trigger( type, data, this[0], true ); + } + }, + + toggle: function( fn ) { + // Save reference to arguments for access in closure + var args = arguments, + guid = fn.guid || jQuery.guid++, + i = 0, + toggler = function( event ) { + // Figure out which function to execute + var lastToggle = ( jQuery._data( this, "lastToggle" + fn.guid ) || 0 ) % i; + jQuery._data( this, "lastToggle" + fn.guid, lastToggle + 1 ); + + // Make sure that clicks stop + event.preventDefault(); + + // and execute the function + return args[ lastToggle ].apply( this, arguments ) || false; + }; + + // link all the functions, so any of them can unbind this click handler + toggler.guid = guid; + while ( i < args.length ) { + args[ i++ ].guid = guid; + } + + return this.click( toggler ); + }, + + hover: function( fnOver, fnOut ) { + return this.mouseenter( fnOver ).mouseleave( fnOut || fnOver ); + } +}); + +jQuery.each( ("blur focus focusin focusout load resize scroll unload click dblclick " + + "mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave " + + "change select submit keydown keypress keyup error contextmenu").split(" "), function( i, name ) { + + // Handle event binding + jQuery.fn[ name ] = function( data, fn ) { + if ( fn == null ) { + fn = data; + data = null; + } + + return arguments.length > 0 ? + this.on( name, null, data, fn ) : + this.trigger( name ); + }; + + if ( jQuery.attrFn ) { + jQuery.attrFn[ name ] = true; + } + + if ( rkeyEvent.test( name ) ) { + jQuery.event.fixHooks[ name ] = jQuery.event.keyHooks; + } + + if ( rmouseEvent.test( name ) ) { + jQuery.event.fixHooks[ name ] = jQuery.event.mouseHooks; + } +}); + + + +/*! + * Sizzle CSS Selector Engine + * Copyright 2011, The Dojo Foundation + * Released under the MIT, BSD, and GPL Licenses. + * More information: http://sizzlejs.com/ + */ +(function(){ + +var chunker = /((?:\((?:\([^()]+\)|[^()]+)+\)|\[(?:\[[^\[\]]*\]|['"][^'"]*['"]|[^\[\]'"]+)+\]|\\.|[^ >+~,(\[\\]+)+|[>+~])(\s*,\s*)?((?:.|\r|\n)*)/g, + expando = "sizcache" + (Math.random() + '').replace('.', ''), + done = 0, + toString = Object.prototype.toString, + hasDuplicate = false, + baseHasDuplicate = true, + rBackslash = /\\/g, + rReturn = /\r\n/g, + rNonWord = /\W/; + +// Here we check if the JavaScript engine is using some sort of +// optimization where it does not always call our comparision +// function. If that is the case, discard the hasDuplicate value. +// Thus far that includes Google Chrome. +[0, 0].sort(function() { + baseHasDuplicate = false; + return 0; +}); + +var Sizzle = function( selector, context, results, seed ) { + results = results || []; + context = context || document; + + var origContext = context; + + if ( context.nodeType !== 1 && context.nodeType !== 9 ) { + return []; + } + + if ( !selector || typeof selector !== "string" ) { + return results; + } + + var m, set, checkSet, extra, ret, cur, pop, i, + prune = true, + contextXML = Sizzle.isXML( context ), + parts = [], + soFar = selector; + + // Reset the position of the chunker regexp (start from head) + do { + chunker.exec( "" ); + m = chunker.exec( soFar ); + + if ( m ) { + soFar = m[3]; + + parts.push( m[1] ); + + if ( m[2] ) { + extra = m[3]; + break; + } + } + } while ( m ); + + if ( parts.length > 1 && origPOS.exec( selector ) ) { + + if ( parts.length === 2 && Expr.relative[ parts[0] ] ) { + set = posProcess( parts[0] + parts[1], context, seed ); + + } else { + set = Expr.relative[ parts[0] ] ? + [ context ] : + Sizzle( parts.shift(), context ); + + while ( parts.length ) { + selector = parts.shift(); + + if ( Expr.relative[ selector ] ) { + selector += parts.shift(); + } + + set = posProcess( selector, set, seed ); + } + } + + } else { + // Take a shortcut and set the context if the root selector is an ID + // (but not if it'll be faster if the inner selector is an ID) + if ( !seed && parts.length > 1 && context.nodeType === 9 && !contextXML && + Expr.match.ID.test(parts[0]) && !Expr.match.ID.test(parts[parts.length - 1]) ) { + + ret = Sizzle.find( parts.shift(), context, contextXML ); + context = ret.expr ? + Sizzle.filter( ret.expr, ret.set )[0] : + ret.set[0]; + } + + if ( context ) { + ret = seed ? + { expr: parts.pop(), set: makeArray(seed) } : + Sizzle.find( parts.pop(), parts.length === 1 && (parts[0] === "~" || parts[0] === "+") && context.parentNode ? context.parentNode : context, contextXML ); + + set = ret.expr ? + Sizzle.filter( ret.expr, ret.set ) : + ret.set; + + if ( parts.length > 0 ) { + checkSet = makeArray( set ); + + } else { + prune = false; + } + + while ( parts.length ) { + cur = parts.pop(); + pop = cur; + + if ( !Expr.relative[ cur ] ) { + cur = ""; + } else { + pop = parts.pop(); + } + + if ( pop == null ) { + pop = context; + } + + Expr.relative[ cur ]( checkSet, pop, contextXML ); + } + + } else { + checkSet = parts = []; + } + } + + if ( !checkSet ) { + checkSet = set; + } + + if ( !checkSet ) { + Sizzle.error( cur || selector ); + } + + if ( toString.call(checkSet) === "[object Array]" ) { + if ( !prune ) { + results.push.apply( results, checkSet ); + + } else if ( context && context.nodeType === 1 ) { + for ( i = 0; checkSet[i] != null; i++ ) { + if ( checkSet[i] && (checkSet[i] === true || checkSet[i].nodeType === 1 && Sizzle.contains(context, checkSet[i])) ) { + results.push( set[i] ); + } + } + + } else { + for ( i = 0; checkSet[i] != null; i++ ) { + if ( checkSet[i] && checkSet[i].nodeType === 1 ) { + results.push( set[i] ); + } + } + } + + } else { + makeArray( checkSet, results ); + } + + if ( extra ) { + Sizzle( extra, origContext, results, seed ); + Sizzle.uniqueSort( results ); + } + + return results; +}; + +Sizzle.uniqueSort = function( results ) { + if ( sortOrder ) { + hasDuplicate = baseHasDuplicate; + results.sort( sortOrder ); + + if ( hasDuplicate ) { + for ( var i = 1; i < results.length; i++ ) { + if ( results[i] === results[ i - 1 ] ) { + results.splice( i--, 1 ); + } + } + } + } + + return results; +}; + +Sizzle.matches = function( expr, set ) { + return Sizzle( expr, null, null, set ); +}; + +Sizzle.matchesSelector = function( node, expr ) { + return Sizzle( expr, null, null, [node] ).length > 0; +}; + +Sizzle.find = function( expr, context, isXML ) { + var set, i, len, match, type, left; + + if ( !expr ) { + return []; + } + + for ( i = 0, len = Expr.order.length; i < len; i++ ) { + type = Expr.order[i]; + + if ( (match = Expr.leftMatch[ type ].exec( expr )) ) { + left = match[1]; + match.splice( 1, 1 ); + + if ( left.substr( left.length - 1 ) !== "\\" ) { + match[1] = (match[1] || "").replace( rBackslash, "" ); + set = Expr.find[ type ]( match, context, isXML ); + + if ( set != null ) { + expr = expr.replace( Expr.match[ type ], "" ); + break; + } + } + } + } + + if ( !set ) { + set = typeof context.getElementsByTagName !== "undefined" ? + context.getElementsByTagName( "*" ) : + []; + } + + return { set: set, expr: expr }; +}; + +Sizzle.filter = function( expr, set, inplace, not ) { + var match, anyFound, + type, found, item, filter, left, + i, pass, + old = expr, + result = [], + curLoop = set, + isXMLFilter = set && set[0] && Sizzle.isXML( set[0] ); + + while ( expr && set.length ) { + for ( type in Expr.filter ) { + if ( (match = Expr.leftMatch[ type ].exec( expr )) != null && match[2] ) { + filter = Expr.filter[ type ]; + left = match[1]; + + anyFound = false; + + match.splice(1,1); + + if ( left.substr( left.length - 1 ) === "\\" ) { + continue; + } + + if ( curLoop === result ) { + result = []; + } + + if ( Expr.preFilter[ type ] ) { + match = Expr.preFilter[ type ]( match, curLoop, inplace, result, not, isXMLFilter ); + + if ( !match ) { + anyFound = found = true; + + } else if ( match === true ) { + continue; + } + } + + if ( match ) { + for ( i = 0; (item = curLoop[i]) != null; i++ ) { + if ( item ) { + found = filter( item, match, i, curLoop ); + pass = not ^ found; + + if ( inplace && found != null ) { + if ( pass ) { + anyFound = true; + + } else { + curLoop[i] = false; + } + + } else if ( pass ) { + result.push( item ); + anyFound = true; + } + } + } + } + + if ( found !== undefined ) { + if ( !inplace ) { + curLoop = result; + } + + expr = expr.replace( Expr.match[ type ], "" ); + + if ( !anyFound ) { + return []; + } + + break; + } + } + } + + // Improper expression + if ( expr === old ) { + if ( anyFound == null ) { + Sizzle.error( expr ); + + } else { + break; + } + } + + old = expr; + } + + return curLoop; +}; + +Sizzle.error = function( msg ) { + throw new Error( "Syntax error, unrecognized expression: " + msg ); +}; + +/** + * Utility function for retreiving the text value of an array of DOM nodes + * @param {Array|Element} elem + */ +var getText = Sizzle.getText = function( elem ) { + var i, node, + nodeType = elem.nodeType, + ret = ""; + + if ( nodeType ) { + if ( nodeType === 1 || nodeType === 9 ) { + // Use textContent || innerText for elements + if ( typeof elem.textContent === 'string' ) { + return elem.textContent; + } else if ( typeof elem.innerText === 'string' ) { + // Replace IE's carriage returns + return elem.innerText.replace( rReturn, '' ); + } else { + // Traverse it's children + for ( elem = elem.firstChild; elem; elem = elem.nextSibling) { + ret += getText( elem ); + } + } + } else if ( nodeType === 3 || nodeType === 4 ) { + return elem.nodeValue; + } + } else { + + // If no nodeType, this is expected to be an array + for ( i = 0; (node = elem[i]); i++ ) { + // Do not traverse comment nodes + if ( node.nodeType !== 8 ) { + ret += getText( node ); + } + } + } + return ret; +}; + +var Expr = Sizzle.selectors = { + order: [ "ID", "NAME", "TAG" ], + + match: { + ID: /#((?:[\w\u00c0-\uFFFF\-]|\\.)+)/, + CLASS: /\.((?:[\w\u00c0-\uFFFF\-]|\\.)+)/, + NAME: /\[name=['"]*((?:[\w\u00c0-\uFFFF\-]|\\.)+)['"]*\]/, + ATTR: /\[\s*((?:[\w\u00c0-\uFFFF\-]|\\.)+)\s*(?:(\S?=)\s*(?:(['"])(.*?)\3|(#?(?:[\w\u00c0-\uFFFF\-]|\\.)*)|)|)\s*\]/, + TAG: /^((?:[\w\u00c0-\uFFFF\*\-]|\\.)+)/, + CHILD: /:(only|nth|last|first)-child(?:\(\s*(even|odd|(?:[+\-]?\d+|(?:[+\-]?\d*)?n\s*(?:[+\-]\s*\d+)?))\s*\))?/, + POS: /:(nth|eq|gt|lt|first|last|even|odd)(?:\((\d*)\))?(?=[^\-]|$)/, + PSEUDO: /:((?:[\w\u00c0-\uFFFF\-]|\\.)+)(?:\((['"]?)((?:\([^\)]+\)|[^\(\)]*)+)\2\))?/ + }, + + leftMatch: {}, + + attrMap: { + "class": "className", + "for": "htmlFor" + }, + + attrHandle: { + href: function( elem ) { + return elem.getAttribute( "href" ); + }, + type: function( elem ) { + return elem.getAttribute( "type" ); + } + }, + + relative: { + "+": function(checkSet, part){ + var isPartStr = typeof part === "string", + isTag = isPartStr && !rNonWord.test( part ), + isPartStrNotTag = isPartStr && !isTag; + + if ( isTag ) { + part = part.toLowerCase(); + } + + for ( var i = 0, l = checkSet.length, elem; i < l; i++ ) { + if ( (elem = checkSet[i]) ) { + while ( (elem = elem.previousSibling) && elem.nodeType !== 1 ) {} + + checkSet[i] = isPartStrNotTag || elem && elem.nodeName.toLowerCase() === part ? + elem || false : + elem === part; + } + } + + if ( isPartStrNotTag ) { + Sizzle.filter( part, checkSet, true ); + } + }, + + ">": function( checkSet, part ) { + var elem, + isPartStr = typeof part === "string", + i = 0, + l = checkSet.length; + + if ( isPartStr && !rNonWord.test( part ) ) { + part = part.toLowerCase(); + + for ( ; i < l; i++ ) { + elem = checkSet[i]; + + if ( elem ) { + var parent = elem.parentNode; + checkSet[i] = parent.nodeName.toLowerCase() === part ? parent : false; + } + } + + } else { + for ( ; i < l; i++ ) { + elem = checkSet[i]; + + if ( elem ) { + checkSet[i] = isPartStr ? + elem.parentNode : + elem.parentNode === part; + } + } + + if ( isPartStr ) { + Sizzle.filter( part, checkSet, true ); + } + } + }, + + "": function(checkSet, part, isXML){ + var nodeCheck, + doneName = done++, + checkFn = dirCheck; + + if ( typeof part === "string" && !rNonWord.test( part ) ) { + part = part.toLowerCase(); + nodeCheck = part; + checkFn = dirNodeCheck; + } + + checkFn( "parentNode", part, doneName, checkSet, nodeCheck, isXML ); + }, + + "~": function( checkSet, part, isXML ) { + var nodeCheck, + doneName = done++, + checkFn = dirCheck; + + if ( typeof part === "string" && !rNonWord.test( part ) ) { + part = part.toLowerCase(); + nodeCheck = part; + checkFn = dirNodeCheck; + } + + checkFn( "previousSibling", part, doneName, checkSet, nodeCheck, isXML ); + } + }, + + find: { + ID: function( match, context, isXML ) { + if ( typeof context.getElementById !== "undefined" && !isXML ) { + var m = context.getElementById(match[1]); + // Check parentNode to catch when Blackberry 4.6 returns + // nodes that are no longer in the document #6963 + return m && m.parentNode ? [m] : []; + } + }, + + NAME: function( match, context ) { + if ( typeof context.getElementsByName !== "undefined" ) { + var ret = [], + results = context.getElementsByName( match[1] ); + + for ( var i = 0, l = results.length; i < l; i++ ) { + if ( results[i].getAttribute("name") === match[1] ) { + ret.push( results[i] ); + } + } + + return ret.length === 0 ? null : ret; + } + }, + + TAG: function( match, context ) { + if ( typeof context.getElementsByTagName !== "undefined" ) { + return context.getElementsByTagName( match[1] ); + } + } + }, + preFilter: { + CLASS: function( match, curLoop, inplace, result, not, isXML ) { + match = " " + match[1].replace( rBackslash, "" ) + " "; + + if ( isXML ) { + return match; + } + + for ( var i = 0, elem; (elem = curLoop[i]) != null; i++ ) { + if ( elem ) { + if ( not ^ (elem.className && (" " + elem.className + " ").replace(/[\t\n\r]/g, " ").indexOf(match) >= 0) ) { + if ( !inplace ) { + result.push( elem ); + } + + } else if ( inplace ) { + curLoop[i] = false; + } + } + } + + return false; + }, + + ID: function( match ) { + return match[1].replace( rBackslash, "" ); + }, + + TAG: function( match, curLoop ) { + return match[1].replace( rBackslash, "" ).toLowerCase(); + }, + + CHILD: function( match ) { + if ( match[1] === "nth" ) { + if ( !match[2] ) { + Sizzle.error( match[0] ); + } + + match[2] = match[2].replace(/^\+|\s*/g, ''); + + // parse equations like 'even', 'odd', '5', '2n', '3n+2', '4n-1', '-n+6' + var test = /(-?)(\d*)(?:n([+\-]?\d*))?/.exec( + match[2] === "even" && "2n" || match[2] === "odd" && "2n+1" || + !/\D/.test( match[2] ) && "0n+" + match[2] || match[2]); + + // calculate the numbers (first)n+(last) including if they are negative + match[2] = (test[1] + (test[2] || 1)) - 0; + match[3] = test[3] - 0; + } + else if ( match[2] ) { + Sizzle.error( match[0] ); + } + + // TODO: Move to normal caching system + match[0] = done++; + + return match; + }, + + ATTR: function( match, curLoop, inplace, result, not, isXML ) { + var name = match[1] = match[1].replace( rBackslash, "" ); + + if ( !isXML && Expr.attrMap[name] ) { + match[1] = Expr.attrMap[name]; + } + + // Handle if an un-quoted value was used + match[4] = ( match[4] || match[5] || "" ).replace( rBackslash, "" ); + + if ( match[2] === "~=" ) { + match[4] = " " + match[4] + " "; + } + + return match; + }, + + PSEUDO: function( match, curLoop, inplace, result, not ) { + if ( match[1] === "not" ) { + // If we're dealing with a complex expression, or a simple one + if ( ( chunker.exec(match[3]) || "" ).length > 1 || /^\w/.test(match[3]) ) { + match[3] = Sizzle(match[3], null, null, curLoop); + + } else { + var ret = Sizzle.filter(match[3], curLoop, inplace, true ^ not); + + if ( !inplace ) { + result.push.apply( result, ret ); + } + + return false; + } + + } else if ( Expr.match.POS.test( match[0] ) || Expr.match.CHILD.test( match[0] ) ) { + return true; + } + + return match; + }, + + POS: function( match ) { + match.unshift( true ); + + return match; + } + }, + + filters: { + enabled: function( elem ) { + return elem.disabled === false && elem.type !== "hidden"; + }, + + disabled: function( elem ) { + return elem.disabled === true; + }, + + checked: function( elem ) { + return elem.checked === true; + }, + + selected: function( elem ) { + // Accessing this property makes selected-by-default + // options in Safari work properly + if ( elem.parentNode ) { + elem.parentNode.selectedIndex; + } + + return elem.selected === true; + }, + + parent: function( elem ) { + return !!elem.firstChild; + }, + + empty: function( elem ) { + return !elem.firstChild; + }, + + has: function( elem, i, match ) { + return !!Sizzle( match[3], elem ).length; + }, + + header: function( elem ) { + return (/h\d/i).test( elem.nodeName ); + }, + + text: function( elem ) { + var attr = elem.getAttribute( "type" ), type = elem.type; + // IE6 and 7 will map elem.type to 'text' for new HTML5 types (search, etc) + // use getAttribute instead to test this case + return elem.nodeName.toLowerCase() === "input" && "text" === type && ( attr === type || attr === null ); + }, + + radio: function( elem ) { + return elem.nodeName.toLowerCase() === "input" && "radio" === elem.type; + }, + + checkbox: function( elem ) { + return elem.nodeName.toLowerCase() === "input" && "checkbox" === elem.type; + }, + + file: function( elem ) { + return elem.nodeName.toLowerCase() === "input" && "file" === elem.type; + }, + + password: function( elem ) { + return elem.nodeName.toLowerCase() === "input" && "password" === elem.type; + }, + + submit: function( elem ) { + var name = elem.nodeName.toLowerCase(); + return (name === "input" || name === "button") && "submit" === elem.type; + }, + + image: function( elem ) { + return elem.nodeName.toLowerCase() === "input" && "image" === elem.type; + }, + + reset: function( elem ) { + var name = elem.nodeName.toLowerCase(); + return (name === "input" || name === "button") && "reset" === elem.type; + }, + + button: function( elem ) { + var name = elem.nodeName.toLowerCase(); + return name === "input" && "button" === elem.type || name === "button"; + }, + + input: function( elem ) { + return (/input|select|textarea|button/i).test( elem.nodeName ); + }, + + focus: function( elem ) { + return elem === elem.ownerDocument.activeElement; + } + }, + setFilters: { + first: function( elem, i ) { + return i === 0; + }, + + last: function( elem, i, match, array ) { + return i === array.length - 1; + }, + + even: function( elem, i ) { + return i % 2 === 0; + }, + + odd: function( elem, i ) { + return i % 2 === 1; + }, + + lt: function( elem, i, match ) { + return i < match[3] - 0; + }, + + gt: function( elem, i, match ) { + return i > match[3] - 0; + }, + + nth: function( elem, i, match ) { + return match[3] - 0 === i; + }, + + eq: function( elem, i, match ) { + return match[3] - 0 === i; + } + }, + filter: { + PSEUDO: function( elem, match, i, array ) { + var name = match[1], + filter = Expr.filters[ name ]; + + if ( filter ) { + return filter( elem, i, match, array ); + + } else if ( name === "contains" ) { + return (elem.textContent || elem.innerText || getText([ elem ]) || "").indexOf(match[3]) >= 0; + + } else if ( name === "not" ) { + var not = match[3]; + + for ( var j = 0, l = not.length; j < l; j++ ) { + if ( not[j] === elem ) { + return false; + } + } + + return true; + + } else { + Sizzle.error( name ); + } + }, + + CHILD: function( elem, match ) { + var first, last, + doneName, parent, cache, + count, diff, + type = match[1], + node = elem; + + switch ( type ) { + case "only": + case "first": + while ( (node = node.previousSibling) ) { + if ( node.nodeType === 1 ) { + return false; + } + } + + if ( type === "first" ) { + return true; + } + + node = elem; + + case "last": + while ( (node = node.nextSibling) ) { + if ( node.nodeType === 1 ) { + return false; + } + } + + return true; + + case "nth": + first = match[2]; + last = match[3]; + + if ( first === 1 && last === 0 ) { + return true; + } + + doneName = match[0]; + parent = elem.parentNode; + + if ( parent && (parent[ expando ] !== doneName || !elem.nodeIndex) ) { + count = 0; + + for ( node = parent.firstChild; node; node = node.nextSibling ) { + if ( node.nodeType === 1 ) { + node.nodeIndex = ++count; + } + } + + parent[ expando ] = doneName; + } + + diff = elem.nodeIndex - last; + + if ( first === 0 ) { + return diff === 0; + + } else { + return ( diff % first === 0 && diff / first >= 0 ); + } + } + }, + + ID: function( elem, match ) { + return elem.nodeType === 1 && elem.getAttribute("id") === match; + }, + + TAG: function( elem, match ) { + return (match === "*" && elem.nodeType === 1) || !!elem.nodeName && elem.nodeName.toLowerCase() === match; + }, + + CLASS: function( elem, match ) { + return (" " + (elem.className || elem.getAttribute("class")) + " ") + .indexOf( match ) > -1; + }, + + ATTR: function( elem, match ) { + var name = match[1], + result = Sizzle.attr ? + Sizzle.attr( elem, name ) : + Expr.attrHandle[ name ] ? + Expr.attrHandle[ name ]( elem ) : + elem[ name ] != null ? + elem[ name ] : + elem.getAttribute( name ), + value = result + "", + type = match[2], + check = match[4]; + + return result == null ? + type === "!=" : + !type && Sizzle.attr ? + result != null : + type === "=" ? + value === check : + type === "*=" ? + value.indexOf(check) >= 0 : + type === "~=" ? + (" " + value + " ").indexOf(check) >= 0 : + !check ? + value && result !== false : + type === "!=" ? + value !== check : + type === "^=" ? + value.indexOf(check) === 0 : + type === "$=" ? + value.substr(value.length - check.length) === check : + type === "|=" ? + value === check || value.substr(0, check.length + 1) === check + "-" : + false; + }, + + POS: function( elem, match, i, array ) { + var name = match[2], + filter = Expr.setFilters[ name ]; + + if ( filter ) { + return filter( elem, i, match, array ); + } + } + } +}; + +var origPOS = Expr.match.POS, + fescape = function(all, num){ + return "\\" + (num - 0 + 1); + }; + +for ( var type in Expr.match ) { + Expr.match[ type ] = new RegExp( Expr.match[ type ].source + (/(?![^\[]*\])(?![^\(]*\))/.source) ); + Expr.leftMatch[ type ] = new RegExp( /(^(?:.|\r|\n)*?)/.source + Expr.match[ type ].source.replace(/\\(\d+)/g, fescape) ); +} + +var makeArray = function( array, results ) { + array = Array.prototype.slice.call( array, 0 ); + + if ( results ) { + results.push.apply( results, array ); + return results; + } + + return array; +}; + +// Perform a simple check to determine if the browser is capable of +// converting a NodeList to an array using builtin methods. +// Also verifies that the returned array holds DOM nodes +// (which is not the case in the Blackberry browser) +try { + Array.prototype.slice.call( document.documentElement.childNodes, 0 )[0].nodeType; + +// Provide a fallback method if it does not work +} catch( e ) { + makeArray = function( array, results ) { + var i = 0, + ret = results || []; + + if ( toString.call(array) === "[object Array]" ) { + Array.prototype.push.apply( ret, array ); + + } else { + if ( typeof array.length === "number" ) { + for ( var l = array.length; i < l; i++ ) { + ret.push( array[i] ); + } + + } else { + for ( ; array[i]; i++ ) { + ret.push( array[i] ); + } + } + } + + return ret; + }; +} + +var sortOrder, siblingCheck; + +if ( document.documentElement.compareDocumentPosition ) { + sortOrder = function( a, b ) { + if ( a === b ) { + hasDuplicate = true; + return 0; + } + + if ( !a.compareDocumentPosition || !b.compareDocumentPosition ) { + return a.compareDocumentPosition ? -1 : 1; + } + + return a.compareDocumentPosition(b) & 4 ? -1 : 1; + }; + +} else { + sortOrder = function( a, b ) { + // The nodes are identical, we can exit early + if ( a === b ) { + hasDuplicate = true; + return 0; + + // Fallback to using sourceIndex (in IE) if it's available on both nodes + } else if ( a.sourceIndex && b.sourceIndex ) { + return a.sourceIndex - b.sourceIndex; + } + + var al, bl, + ap = [], + bp = [], + aup = a.parentNode, + bup = b.parentNode, + cur = aup; + + // If the nodes are siblings (or identical) we can do a quick check + if ( aup === bup ) { + return siblingCheck( a, b ); + + // If no parents were found then the nodes are disconnected + } else if ( !aup ) { + return -1; + + } else if ( !bup ) { + return 1; + } + + // Otherwise they're somewhere else in the tree so we need + // to build up a full list of the parentNodes for comparison + while ( cur ) { + ap.unshift( cur ); + cur = cur.parentNode; + } + + cur = bup; + + while ( cur ) { + bp.unshift( cur ); + cur = cur.parentNode; + } + + al = ap.length; + bl = bp.length; + + // Start walking down the tree looking for a discrepancy + for ( var i = 0; i < al && i < bl; i++ ) { + if ( ap[i] !== bp[i] ) { + return siblingCheck( ap[i], bp[i] ); + } + } + + // We ended someplace up the tree so do a sibling check + return i === al ? + siblingCheck( a, bp[i], -1 ) : + siblingCheck( ap[i], b, 1 ); + }; + + siblingCheck = function( a, b, ret ) { + if ( a === b ) { + return ret; + } + + var cur = a.nextSibling; + + while ( cur ) { + if ( cur === b ) { + return -1; + } + + cur = cur.nextSibling; + } + + return 1; + }; +} + +// Check to see if the browser returns elements by name when +// querying by getElementById (and provide a workaround) +(function(){ + // We're going to inject a fake input element with a specified name + var form = document.createElement("div"), + id = "script" + (new Date()).getTime(), + root = document.documentElement; + + form.innerHTML = "<a name='" + id + "'/>"; + + // Inject it into the root element, check its status, and remove it quickly + root.insertBefore( form, root.firstChild ); + + // The workaround has to do additional checks after a getElementById + // Which slows things down for other browsers (hence the branching) + if ( document.getElementById( id ) ) { + Expr.find.ID = function( match, context, isXML ) { + if ( typeof context.getElementById !== "undefined" && !isXML ) { + var m = context.getElementById(match[1]); + + return m ? + m.id === match[1] || typeof m.getAttributeNode !== "undefined" && m.getAttributeNode("id").nodeValue === match[1] ? + [m] : + undefined : + []; + } + }; + + Expr.filter.ID = function( elem, match ) { + var node = typeof elem.getAttributeNode !== "undefined" && elem.getAttributeNode("id"); + + return elem.nodeType === 1 && node && node.nodeValue === match; + }; + } + + root.removeChild( form ); + + // release memory in IE + root = form = null; +})(); + +(function(){ + // Check to see if the browser returns only elements + // when doing getElementsByTagName("*") + + // Create a fake element + var div = document.createElement("div"); + div.appendChild( document.createComment("") ); + + // Make sure no comments are found + if ( div.getElementsByTagName("*").length > 0 ) { + Expr.find.TAG = function( match, context ) { + var results = context.getElementsByTagName( match[1] ); + + // Filter out possible comments + if ( match[1] === "*" ) { + var tmp = []; + + for ( var i = 0; results[i]; i++ ) { + if ( results[i].nodeType === 1 ) { + tmp.push( results[i] ); + } + } + + results = tmp; + } + + return results; + }; + } + + // Check to see if an attribute returns normalized href attributes + div.innerHTML = "<a href='#'></a>"; + + if ( div.firstChild && typeof div.firstChild.getAttribute !== "undefined" && + div.firstChild.getAttribute("href") !== "#" ) { + + Expr.attrHandle.href = function( elem ) { + return elem.getAttribute( "href", 2 ); + }; + } + + // release memory in IE + div = null; +})(); + +if ( document.querySelectorAll ) { + (function(){ + var oldSizzle = Sizzle, + div = document.createElement("div"), + id = "__sizzle__"; + + div.innerHTML = "<p class='TEST'></p>"; + + // Safari can't handle uppercase or unicode characters when + // in quirks mode. + if ( div.querySelectorAll && div.querySelectorAll(".TEST").length === 0 ) { + return; + } + + Sizzle = function( query, context, extra, seed ) { + context = context || document; + + // Only use querySelectorAll on non-XML documents + // (ID selectors don't work in non-HTML documents) + if ( !seed && !Sizzle.isXML(context) ) { + // See if we find a selector to speed up + var match = /^(\w+$)|^\.([\w\-]+$)|^#([\w\-]+$)/.exec( query ); + + if ( match && (context.nodeType === 1 || context.nodeType === 9) ) { + // Speed-up: Sizzle("TAG") + if ( match[1] ) { + return makeArray( context.getElementsByTagName( query ), extra ); + + // Speed-up: Sizzle(".CLASS") + } else if ( match[2] && Expr.find.CLASS && context.getElementsByClassName ) { + return makeArray( context.getElementsByClassName( match[2] ), extra ); + } + } + + if ( context.nodeType === 9 ) { + // Speed-up: Sizzle("body") + // The body element only exists once, optimize finding it + if ( query === "body" && context.body ) { + return makeArray( [ context.body ], extra ); + + // Speed-up: Sizzle("#ID") + } else if ( match && match[3] ) { + var elem = context.getElementById( match[3] ); + + // Check parentNode to catch when Blackberry 4.6 returns + // nodes that are no longer in the document #6963 + if ( elem && elem.parentNode ) { + // Handle the case where IE and Opera return items + // by name instead of ID + if ( elem.id === match[3] ) { + return makeArray( [ elem ], extra ); + } + + } else { + return makeArray( [], extra ); + } + } + + try { + return makeArray( context.querySelectorAll(query), extra ); + } catch(qsaError) {} + + // qSA works strangely on Element-rooted queries + // We can work around this by specifying an extra ID on the root + // and working up from there (Thanks to Andrew Dupont for the technique) + // IE 8 doesn't work on object elements + } else if ( context.nodeType === 1 && context.nodeName.toLowerCase() !== "object" ) { + var oldContext = context, + old = context.getAttribute( "id" ), + nid = old || id, + hasParent = context.parentNode, + relativeHierarchySelector = /^\s*[+~]/.test( query ); + + if ( !old ) { + context.setAttribute( "id", nid ); + } else { + nid = nid.replace( /'/g, "\\$&" ); + } + if ( relativeHierarchySelector && hasParent ) { + context = context.parentNode; + } + + try { + if ( !relativeHierarchySelector || hasParent ) { + return makeArray( context.querySelectorAll( "[id='" + nid + "'] " + query ), extra ); + } + + } catch(pseudoError) { + } finally { + if ( !old ) { + oldContext.removeAttribute( "id" ); + } + } + } + } + + return oldSizzle(query, context, extra, seed); + }; + + for ( var prop in oldSizzle ) { + Sizzle[ prop ] = oldSizzle[ prop ]; + } + + // release memory in IE + div = null; + })(); +} + +(function(){ + var html = document.documentElement, + matches = html.matchesSelector || html.mozMatchesSelector || html.webkitMatchesSelector || html.msMatchesSelector; + + if ( matches ) { + // Check to see if it's possible to do matchesSelector + // on a disconnected node (IE 9 fails this) + var disconnectedMatch = !matches.call( document.createElement( "div" ), "div" ), + pseudoWorks = false; + + try { + // This should fail with an exception + // Gecko does not error, returns false instead + matches.call( document.documentElement, "[test!='']:sizzle" ); + + } catch( pseudoError ) { + pseudoWorks = true; + } + + Sizzle.matchesSelector = function( node, expr ) { + // Make sure that attribute selectors are quoted + expr = expr.replace(/\=\s*([^'"\]]*)\s*\]/g, "='$1']"); + + if ( !Sizzle.isXML( node ) ) { + try { + if ( pseudoWorks || !Expr.match.PSEUDO.test( expr ) && !/!=/.test( expr ) ) { + var ret = matches.call( node, expr ); + + // IE 9's matchesSelector returns false on disconnected nodes + if ( ret || !disconnectedMatch || + // As well, disconnected nodes are said to be in a document + // fragment in IE 9, so check for that + node.document && node.document.nodeType !== 11 ) { + return ret; + } + } + } catch(e) {} + } + + return Sizzle(expr, null, null, [node]).length > 0; + }; + } +})(); + +(function(){ + var div = document.createElement("div"); + + div.innerHTML = "<div class='test e'></div><div class='test'></div>"; + + // Opera can't find a second classname (in 9.6) + // Also, make sure that getElementsByClassName actually exists + if ( !div.getElementsByClassName || div.getElementsByClassName("e").length === 0 ) { + return; + } + + // Safari caches class attributes, doesn't catch changes (in 3.2) + div.lastChild.className = "e"; + + if ( div.getElementsByClassName("e").length === 1 ) { + return; + } + + Expr.order.splice(1, 0, "CLASS"); + Expr.find.CLASS = function( match, context, isXML ) { + if ( typeof context.getElementsByClassName !== "undefined" && !isXML ) { + return context.getElementsByClassName(match[1]); + } + }; + + // release memory in IE + div = null; +})(); + +function dirNodeCheck( dir, cur, doneName, checkSet, nodeCheck, isXML ) { + for ( var i = 0, l = checkSet.length; i < l; i++ ) { + var elem = checkSet[i]; + + if ( elem ) { + var match = false; + + elem = elem[dir]; + + while ( elem ) { + if ( elem[ expando ] === doneName ) { + match = checkSet[elem.sizset]; + break; + } + + if ( elem.nodeType === 1 && !isXML ){ + elem[ expando ] = doneName; + elem.sizset = i; + } + + if ( elem.nodeName.toLowerCase() === cur ) { + match = elem; + break; + } + + elem = elem[dir]; + } + + checkSet[i] = match; + } + } +} + +function dirCheck( dir, cur, doneName, checkSet, nodeCheck, isXML ) { + for ( var i = 0, l = checkSet.length; i < l; i++ ) { + var elem = checkSet[i]; + + if ( elem ) { + var match = false; + + elem = elem[dir]; + + while ( elem ) { + if ( elem[ expando ] === doneName ) { + match = checkSet[elem.sizset]; + break; + } + + if ( elem.nodeType === 1 ) { + if ( !isXML ) { + elem[ expando ] = doneName; + elem.sizset = i; + } + + if ( typeof cur !== "string" ) { + if ( elem === cur ) { + match = true; + break; + } + + } else if ( Sizzle.filter( cur, [elem] ).length > 0 ) { + match = elem; + break; + } + } + + elem = elem[dir]; + } + + checkSet[i] = match; + } + } +} + +if ( document.documentElement.contains ) { + Sizzle.contains = function( a, b ) { + return a !== b && (a.contains ? a.contains(b) : true); + }; + +} else if ( document.documentElement.compareDocumentPosition ) { + Sizzle.contains = function( a, b ) { + return !!(a.compareDocumentPosition(b) & 16); + }; + +} else { + Sizzle.contains = function() { + return false; + }; +} + +Sizzle.isXML = function( elem ) { + // documentElement is verified for cases where it doesn't yet exist + // (such as loading iframes in IE - #4833) + var documentElement = (elem ? elem.ownerDocument || elem : 0).documentElement; + + return documentElement ? documentElement.nodeName !== "HTML" : false; +}; + +var posProcess = function( selector, context, seed ) { + var match, + tmpSet = [], + later = "", + root = context.nodeType ? [context] : context; + + // Position selectors must be done after the filter + // And so must :not(positional) so we move all PSEUDOs to the end + while ( (match = Expr.match.PSEUDO.exec( selector )) ) { + later += match[0]; + selector = selector.replace( Expr.match.PSEUDO, "" ); + } + + selector = Expr.relative[selector] ? selector + "*" : selector; + + for ( var i = 0, l = root.length; i < l; i++ ) { + Sizzle( selector, root[i], tmpSet, seed ); + } + + return Sizzle.filter( later, tmpSet ); +}; + +// EXPOSE +// Override sizzle attribute retrieval +Sizzle.attr = jQuery.attr; +Sizzle.selectors.attrMap = {}; +jQuery.find = Sizzle; +jQuery.expr = Sizzle.selectors; +jQuery.expr[":"] = jQuery.expr.filters; +jQuery.unique = Sizzle.uniqueSort; +jQuery.text = Sizzle.getText; +jQuery.isXMLDoc = Sizzle.isXML; +jQuery.contains = Sizzle.contains; + + +})(); + + +var runtil = /Until$/, + rparentsprev = /^(?:parents|prevUntil|prevAll)/, + // Note: This RegExp should be improved, or likely pulled from Sizzle + rmultiselector = /,/, + isSimple = /^.[^:#\[\.,]*$/, + slice = Array.prototype.slice, + POS = jQuery.expr.match.POS, + // methods guaranteed to produce a unique set when starting from a unique set + guaranteedUnique = { + children: true, + contents: true, + next: true, + prev: true + }; + +jQuery.fn.extend({ + find: function( selector ) { + var self = this, + i, l; + + if ( typeof selector !== "string" ) { + return jQuery( selector ).filter(function() { + for ( i = 0, l = self.length; i < l; i++ ) { + if ( jQuery.contains( self[ i ], this ) ) { + return true; + } + } + }); + } + + var ret = this.pushStack( "", "find", selector ), + length, n, r; + + for ( i = 0, l = this.length; i < l; i++ ) { + length = ret.length; + jQuery.find( selector, this[i], ret ); + + if ( i > 0 ) { + // Make sure that the results are unique + for ( n = length; n < ret.length; n++ ) { + for ( r = 0; r < length; r++ ) { + if ( ret[r] === ret[n] ) { + ret.splice(n--, 1); + break; + } + } + } + } + } + + return ret; + }, + + has: function( target ) { + var targets = jQuery( target ); + return this.filter(function() { + for ( var i = 0, l = targets.length; i < l; i++ ) { + if ( jQuery.contains( this, targets[i] ) ) { + return true; + } + } + }); + }, + + not: function( selector ) { + return this.pushStack( winnow(this, selector, false), "not", selector); + }, + + filter: function( selector ) { + return this.pushStack( winnow(this, selector, true), "filter", selector ); + }, + + is: function( selector ) { + return !!selector && ( + typeof selector === "string" ? + // If this is a positional selector, check membership in the returned set + // so $("p:first").is("p:last") won't return true for a doc with two "p". + POS.test( selector ) ? + jQuery( selector, this.context ).index( this[0] ) >= 0 : + jQuery.filter( selector, this ).length > 0 : + this.filter( selector ).length > 0 ); + }, + + closest: function( selectors, context ) { + var ret = [], i, l, cur = this[0]; + + // Array (deprecated as of jQuery 1.7) + if ( jQuery.isArray( selectors ) ) { + var level = 1; + + while ( cur && cur.ownerDocument && cur !== context ) { + for ( i = 0; i < selectors.length; i++ ) { + + if ( jQuery( cur ).is( selectors[ i ] ) ) { + ret.push({ selector: selectors[ i ], elem: cur, level: level }); + } + } + + cur = cur.parentNode; + level++; + } + + return ret; + } + + // String + var pos = POS.test( selectors ) || typeof selectors !== "string" ? + jQuery( selectors, context || this.context ) : + 0; + + for ( i = 0, l = this.length; i < l; i++ ) { + cur = this[i]; + + while ( cur ) { + if ( pos ? pos.index(cur) > -1 : jQuery.find.matchesSelector(cur, selectors) ) { + ret.push( cur ); + break; + + } else { + cur = cur.parentNode; + if ( !cur || !cur.ownerDocument || cur === context || cur.nodeType === 11 ) { + break; + } + } + } + } + + ret = ret.length > 1 ? jQuery.unique( ret ) : ret; + + return this.pushStack( ret, "closest", selectors ); + }, + + // Determine the position of an element within + // the matched set of elements + index: function( elem ) { + + // No argument, return index in parent + if ( !elem ) { + return ( this[0] && this[0].parentNode ) ? this.prevAll().length : -1; + } + + // index in selector + if ( typeof elem === "string" ) { + return jQuery.inArray( this[0], jQuery( elem ) ); + } + + // Locate the position of the desired element + return jQuery.inArray( + // If it receives a jQuery object, the first element is used + elem.jquery ? elem[0] : elem, this ); + }, + + add: function( selector, context ) { + var set = typeof selector === "string" ? + jQuery( selector, context ) : + jQuery.makeArray( selector && selector.nodeType ? [ selector ] : selector ), + all = jQuery.merge( this.get(), set ); + + return this.pushStack( isDisconnected( set[0] ) || isDisconnected( all[0] ) ? + all : + jQuery.unique( all ) ); + }, + + andSelf: function() { + return this.add( this.prevObject ); + } +}); + +// A painfully simple check to see if an element is disconnected +// from a document (should be improved, where feasible). +function isDisconnected( node ) { + return !node || !node.parentNode || node.parentNode.nodeType === 11; +} + +jQuery.each({ + parent: function( elem ) { + var parent = elem.parentNode; + return parent && parent.nodeType !== 11 ? parent : null; + }, + parents: function( elem ) { + return jQuery.dir( elem, "parentNode" ); + }, + parentsUntil: function( elem, i, until ) { + return jQuery.dir( elem, "parentNode", until ); + }, + next: function( elem ) { + return jQuery.nth( elem, 2, "nextSibling" ); + }, + prev: function( elem ) { + return jQuery.nth( elem, 2, "previousSibling" ); + }, + nextAll: function( elem ) { + return jQuery.dir( elem, "nextSibling" ); + }, + prevAll: function( elem ) { + return jQuery.dir( elem, "previousSibling" ); + }, + nextUntil: function( elem, i, until ) { + return jQuery.dir( elem, "nextSibling", until ); + }, + prevUntil: function( elem, i, until ) { + return jQuery.dir( elem, "previousSibling", until ); + }, + siblings: function( elem ) { + return jQuery.sibling( elem.parentNode.firstChild, elem ); + }, + children: function( elem ) { + return jQuery.sibling( elem.firstChild ); + }, + contents: function( elem ) { + return jQuery.nodeName( elem, "iframe" ) ? + elem.contentDocument || elem.contentWindow.document : + jQuery.makeArray( elem.childNodes ); + } +}, function( name, fn ) { + jQuery.fn[ name ] = function( until, selector ) { + var ret = jQuery.map( this, fn, until ); + + if ( !runtil.test( name ) ) { + selector = until; + } + + if ( selector && typeof selector === "string" ) { + ret = jQuery.filter( selector, ret ); + } + + ret = this.length > 1 && !guaranteedUnique[ name ] ? jQuery.unique( ret ) : ret; + + if ( (this.length > 1 || rmultiselector.test( selector )) && rparentsprev.test( name ) ) { + ret = ret.reverse(); + } + + return this.pushStack( ret, name, slice.call( arguments ).join(",") ); + }; +}); + +jQuery.extend({ + filter: function( expr, elems, not ) { + if ( not ) { + expr = ":not(" + expr + ")"; + } + + return elems.length === 1 ? + jQuery.find.matchesSelector(elems[0], expr) ? [ elems[0] ] : [] : + jQuery.find.matches(expr, elems); + }, + + dir: function( elem, dir, until ) { + var matched = [], + cur = elem[ dir ]; + + while ( cur && cur.nodeType !== 9 && (until === undefined || cur.nodeType !== 1 || !jQuery( cur ).is( until )) ) { + if ( cur.nodeType === 1 ) { + matched.push( cur ); + } + cur = cur[dir]; + } + return matched; + }, + + nth: function( cur, result, dir, elem ) { + result = result || 1; + var num = 0; + + for ( ; cur; cur = cur[dir] ) { + if ( cur.nodeType === 1 && ++num === result ) { + break; + } + } + + return cur; + }, + + sibling: function( n, elem ) { + var r = []; + + for ( ; n; n = n.nextSibling ) { + if ( n.nodeType === 1 && n !== elem ) { + r.push( n ); + } + } + + return r; + } +}); + +// Implement the identical functionality for filter and not +function winnow( elements, qualifier, keep ) { + + // Can't pass null or undefined to indexOf in Firefox 4 + // Set to 0 to skip string check + qualifier = qualifier || 0; + + if ( jQuery.isFunction( qualifier ) ) { + return jQuery.grep(elements, function( elem, i ) { + var retVal = !!qualifier.call( elem, i, elem ); + return retVal === keep; + }); + + } else if ( qualifier.nodeType ) { + return jQuery.grep(elements, function( elem, i ) { + return ( elem === qualifier ) === keep; + }); + + } else if ( typeof qualifier === "string" ) { + var filtered = jQuery.grep(elements, function( elem ) { + return elem.nodeType === 1; + }); + + if ( isSimple.test( qualifier ) ) { + return jQuery.filter(qualifier, filtered, !keep); + } else { + qualifier = jQuery.filter( qualifier, filtered ); + } + } + + return jQuery.grep(elements, function( elem, i ) { + return ( jQuery.inArray( elem, qualifier ) >= 0 ) === keep; + }); +} + + + + +function createSafeFragment( document ) { + var list = nodeNames.split( "|" ), + safeFrag = document.createDocumentFragment(); + + if ( safeFrag.createElement ) { + while ( list.length ) { + safeFrag.createElement( + list.pop() + ); + } + } + return safeFrag; +} + +var nodeNames = "abbr|article|aside|audio|canvas|datalist|details|figcaption|figure|footer|" + + "header|hgroup|mark|meter|nav|output|progress|section|summary|time|video", + rinlinejQuery = / jQuery\d+="(?:\d+|null)"/g, + rleadingWhitespace = /^\s+/, + rxhtmlTag = /<(?!area|br|col|embed|hr|img|input|link|meta|param)(([\w:]+)[^>]*)\/>/ig, + rtagName = /<([\w:]+)/, + rtbody = /<tbody/i, + rhtml = /<|&#?\w+;/, + rnoInnerhtml = /<(?:script|style)/i, + rnocache = /<(?:script|object|embed|option|style)/i, + rnoshimcache = new RegExp("<(?:" + nodeNames + ")", "i"), + // checked="checked" or checked + rchecked = /checked\s*(?:[^=]|=\s*.checked.)/i, + rscriptType = /\/(java|ecma)script/i, + rcleanScript = /^\s*<!(?:\[CDATA\[|\-\-)/, + wrapMap = { + option: [ 1, "<select multiple='multiple'>", "</select>" ], + legend: [ 1, "<fieldset>", "</fieldset>" ], + thead: [ 1, "<table>", "</table>" ], + tr: [ 2, "<table><tbody>", "</tbody></table>" ], + td: [ 3, "<table><tbody><tr>", "</tr></tbody></table>" ], + col: [ 2, "<table><tbody></tbody><colgroup>", "</colgroup></table>" ], + area: [ 1, "<map>", "</map>" ], + _default: [ 0, "", "" ] + }, + safeFragment = createSafeFragment( document ); + +wrapMap.optgroup = wrapMap.option; +wrapMap.tbody = wrapMap.tfoot = wrapMap.colgroup = wrapMap.caption = wrapMap.thead; +wrapMap.th = wrapMap.td; + +// IE can't serialize <link> and <script> tags normally +if ( !jQuery.support.htmlSerialize ) { + wrapMap._default = [ 1, "div<div>", "</div>" ]; +} + +jQuery.fn.extend({ + text: function( text ) { + if ( jQuery.isFunction(text) ) { + return this.each(function(i) { + var self = jQuery( this ); + + self.text( text.call(this, i, self.text()) ); + }); + } + + if ( typeof text !== "object" && text !== undefined ) { + return this.empty().append( (this[0] && this[0].ownerDocument || document).createTextNode( text ) ); + } + + return jQuery.text( this ); + }, + + wrapAll: function( html ) { + if ( jQuery.isFunction( html ) ) { + return this.each(function(i) { + jQuery(this).wrapAll( html.call(this, i) ); + }); + } + + if ( this[0] ) { + // The elements to wrap the target around + var wrap = jQuery( html, this[0].ownerDocument ).eq(0).clone(true); + + if ( this[0].parentNode ) { + wrap.insertBefore( this[0] ); + } + + wrap.map(function() { + var elem = this; + + while ( elem.firstChild && elem.firstChild.nodeType === 1 ) { + elem = elem.firstChild; + } + + return elem; + }).append( this ); + } + + return this; + }, + + wrapInner: function( html ) { + if ( jQuery.isFunction( html ) ) { + return this.each(function(i) { + jQuery(this).wrapInner( html.call(this, i) ); + }); + } + + return this.each(function() { + var self = jQuery( this ), + contents = self.contents(); + + if ( contents.length ) { + contents.wrapAll( html ); + + } else { + self.append( html ); + } + }); + }, + + wrap: function( html ) { + var isFunction = jQuery.isFunction( html ); + + return this.each(function(i) { + jQuery( this ).wrapAll( isFunction ? html.call(this, i) : html ); + }); + }, + + unwrap: function() { + return this.parent().each(function() { + if ( !jQuery.nodeName( this, "body" ) ) { + jQuery( this ).replaceWith( this.childNodes ); + } + }).end(); + }, + + append: function() { + return this.domManip(arguments, true, function( elem ) { + if ( this.nodeType === 1 ) { + this.appendChild( elem ); + } + }); + }, + + prepend: function() { + return this.domManip(arguments, true, function( elem ) { + if ( this.nodeType === 1 ) { + this.insertBefore( elem, this.firstChild ); + } + }); + }, + + before: function() { + if ( this[0] && this[0].parentNode ) { + return this.domManip(arguments, false, function( elem ) { + this.parentNode.insertBefore( elem, this ); + }); + } else if ( arguments.length ) { + var set = jQuery.clean( arguments ); + set.push.apply( set, this.toArray() ); + return this.pushStack( set, "before", arguments ); + } + }, + + after: function() { + if ( this[0] && this[0].parentNode ) { + return this.domManip(arguments, false, function( elem ) { + this.parentNode.insertBefore( elem, this.nextSibling ); + }); + } else if ( arguments.length ) { + var set = this.pushStack( this, "after", arguments ); + set.push.apply( set, jQuery.clean(arguments) ); + return set; + } + }, + + // keepData is for internal use only--do not document + remove: function( selector, keepData ) { + for ( var i = 0, elem; (elem = this[i]) != null; i++ ) { + if ( !selector || jQuery.filter( selector, [ elem ] ).length ) { + if ( !keepData && elem.nodeType === 1 ) { + jQuery.cleanData( elem.getElementsByTagName("*") ); + jQuery.cleanData( [ elem ] ); + } + + if ( elem.parentNode ) { + elem.parentNode.removeChild( elem ); + } + } + } + + return this; + }, + + empty: function() { + for ( var i = 0, elem; (elem = this[i]) != null; i++ ) { + // Remove element nodes and prevent memory leaks + if ( elem.nodeType === 1 ) { + jQuery.cleanData( elem.getElementsByTagName("*") ); + } + + // Remove any remaining nodes + while ( elem.firstChild ) { + elem.removeChild( elem.firstChild ); + } + } + + return this; + }, + + clone: function( dataAndEvents, deepDataAndEvents ) { + dataAndEvents = dataAndEvents == null ? false : dataAndEvents; + deepDataAndEvents = deepDataAndEvents == null ? dataAndEvents : deepDataAndEvents; + + return this.map( function () { + return jQuery.clone( this, dataAndEvents, deepDataAndEvents ); + }); + }, + + html: function( value ) { + if ( value === undefined ) { + return this[0] && this[0].nodeType === 1 ? + this[0].innerHTML.replace(rinlinejQuery, "") : + null; + + // See if we can take a shortcut and just use innerHTML + } else if ( typeof value === "string" && !rnoInnerhtml.test( value ) && + (jQuery.support.leadingWhitespace || !rleadingWhitespace.test( value )) && + !wrapMap[ (rtagName.exec( value ) || ["", ""])[1].toLowerCase() ] ) { + + value = value.replace(rxhtmlTag, "<$1></$2>"); + + try { + for ( var i = 0, l = this.length; i < l; i++ ) { + // Remove element nodes and prevent memory leaks + if ( this[i].nodeType === 1 ) { + jQuery.cleanData( this[i].getElementsByTagName("*") ); + this[i].innerHTML = value; + } + } + + // If using innerHTML throws an exception, use the fallback method + } catch(e) { + this.empty().append( value ); + } + + } else if ( jQuery.isFunction( value ) ) { + this.each(function(i){ + var self = jQuery( this ); + + self.html( value.call(this, i, self.html()) ); + }); + + } else { + this.empty().append( value ); + } + + return this; + }, + + replaceWith: function( value ) { + if ( this[0] && this[0].parentNode ) { + // Make sure that the elements are removed from the DOM before they are inserted + // this can help fix replacing a parent with child elements + if ( jQuery.isFunction( value ) ) { + return this.each(function(i) { + var self = jQuery(this), old = self.html(); + self.replaceWith( value.call( this, i, old ) ); + }); + } + + if ( typeof value !== "string" ) { + value = jQuery( value ).detach(); + } + + return this.each(function() { + var next = this.nextSibling, + parent = this.parentNode; + + jQuery( this ).remove(); + + if ( next ) { + jQuery(next).before( value ); + } else { + jQuery(parent).append( value ); + } + }); + } else { + return this.length ? + this.pushStack( jQuery(jQuery.isFunction(value) ? value() : value), "replaceWith", value ) : + this; + } + }, + + detach: function( selector ) { + return this.remove( selector, true ); + }, + + domManip: function( args, table, callback ) { + var results, first, fragment, parent, + value = args[0], + scripts = []; + + // We can't cloneNode fragments that contain checked, in WebKit + if ( !jQuery.support.checkClone && arguments.length === 3 && typeof value === "string" && rchecked.test( value ) ) { + return this.each(function() { + jQuery(this).domManip( args, table, callback, true ); + }); + } + + if ( jQuery.isFunction(value) ) { + return this.each(function(i) { + var self = jQuery(this); + args[0] = value.call(this, i, table ? self.html() : undefined); + self.domManip( args, table, callback ); + }); + } + + if ( this[0] ) { + parent = value && value.parentNode; + + // If we're in a fragment, just use that instead of building a new one + if ( jQuery.support.parentNode && parent && parent.nodeType === 11 && parent.childNodes.length === this.length ) { + results = { fragment: parent }; + + } else { + results = jQuery.buildFragment( args, this, scripts ); + } + + fragment = results.fragment; + + if ( fragment.childNodes.length === 1 ) { + first = fragment = fragment.firstChild; + } else { + first = fragment.firstChild; + } + + if ( first ) { + table = table && jQuery.nodeName( first, "tr" ); + + for ( var i = 0, l = this.length, lastIndex = l - 1; i < l; i++ ) { + callback.call( + table ? + root(this[i], first) : + this[i], + // Make sure that we do not leak memory by inadvertently discarding + // the original fragment (which might have attached data) instead of + // using it; in addition, use the original fragment object for the last + // item instead of first because it can end up being emptied incorrectly + // in certain situations (Bug #8070). + // Fragments from the fragment cache must always be cloned and never used + // in place. + results.cacheable || ( l > 1 && i < lastIndex ) ? + jQuery.clone( fragment, true, true ) : + fragment + ); + } + } + + if ( scripts.length ) { + jQuery.each( scripts, evalScript ); + } + } + + return this; + } +}); + +function root( elem, cur ) { + return jQuery.nodeName(elem, "table") ? + (elem.getElementsByTagName("tbody")[0] || + elem.appendChild(elem.ownerDocument.createElement("tbody"))) : + elem; +} + +function cloneCopyEvent( src, dest ) { + + if ( dest.nodeType !== 1 || !jQuery.hasData( src ) ) { + return; + } + + var type, i, l, + oldData = jQuery._data( src ), + curData = jQuery._data( dest, oldData ), + events = oldData.events; + + if ( events ) { + delete curData.handle; + curData.events = {}; + + for ( type in events ) { + for ( i = 0, l = events[ type ].length; i < l; i++ ) { + jQuery.event.add( dest, type + ( events[ type ][ i ].namespace ? "." : "" ) + events[ type ][ i ].namespace, events[ type ][ i ], events[ type ][ i ].data ); + } + } + } + + // make the cloned public data object a copy from the original + if ( curData.data ) { + curData.data = jQuery.extend( {}, curData.data ); + } +} + +function cloneFixAttributes( src, dest ) { + var nodeName; + + // We do not need to do anything for non-Elements + if ( dest.nodeType !== 1 ) { + return; + } + + // clearAttributes removes the attributes, which we don't want, + // but also removes the attachEvent events, which we *do* want + if ( dest.clearAttributes ) { + dest.clearAttributes(); + } + + // mergeAttributes, in contrast, only merges back on the + // original attributes, not the events + if ( dest.mergeAttributes ) { + dest.mergeAttributes( src ); + } + + nodeName = dest.nodeName.toLowerCase(); + + // IE6-8 fail to clone children inside object elements that use + // the proprietary classid attribute value (rather than the type + // attribute) to identify the type of content to display + if ( nodeName === "object" ) { + dest.outerHTML = src.outerHTML; + + } else if ( nodeName === "input" && (src.type === "checkbox" || src.type === "radio") ) { + // IE6-8 fails to persist the checked state of a cloned checkbox + // or radio button. Worse, IE6-7 fail to give the cloned element + // a checked appearance if the defaultChecked value isn't also set + if ( src.checked ) { + dest.defaultChecked = dest.checked = src.checked; + } + + // IE6-7 get confused and end up setting the value of a cloned + // checkbox/radio button to an empty string instead of "on" + if ( dest.value !== src.value ) { + dest.value = src.value; + } + + // IE6-8 fails to return the selected option to the default selected + // state when cloning options + } else if ( nodeName === "option" ) { + dest.selected = src.defaultSelected; + + // IE6-8 fails to set the defaultValue to the correct value when + // cloning other types of input fields + } else if ( nodeName === "input" || nodeName === "textarea" ) { + dest.defaultValue = src.defaultValue; + } + + // Event data gets referenced instead of copied if the expando + // gets copied too + dest.removeAttribute( jQuery.expando ); +} + +jQuery.buildFragment = function( args, nodes, scripts ) { + var fragment, cacheable, cacheresults, doc, + first = args[ 0 ]; + + // nodes may contain either an explicit document object, + // a jQuery collection or context object. + // If nodes[0] contains a valid object to assign to doc + if ( nodes && nodes[0] ) { + doc = nodes[0].ownerDocument || nodes[0]; + } + + // Ensure that an attr object doesn't incorrectly stand in as a document object + // Chrome and Firefox seem to allow this to occur and will throw exception + // Fixes #8950 + if ( !doc.createDocumentFragment ) { + doc = document; + } + + // Only cache "small" (1/2 KB) HTML strings that are associated with the main document + // Cloning options loses the selected state, so don't cache them + // IE 6 doesn't like it when you put <object> or <embed> elements in a fragment + // Also, WebKit does not clone 'checked' attributes on cloneNode, so don't cache + // Lastly, IE6,7,8 will not correctly reuse cached fragments that were created from unknown elems #10501 + if ( args.length === 1 && typeof first === "string" && first.length < 512 && doc === document && + first.charAt(0) === "<" && !rnocache.test( first ) && + (jQuery.support.checkClone || !rchecked.test( first )) && + (jQuery.support.html5Clone || !rnoshimcache.test( first )) ) { + + cacheable = true; + + cacheresults = jQuery.fragments[ first ]; + if ( cacheresults && cacheresults !== 1 ) { + fragment = cacheresults; + } + } + + if ( !fragment ) { + fragment = doc.createDocumentFragment(); + jQuery.clean( args, doc, fragment, scripts ); + } + + if ( cacheable ) { + jQuery.fragments[ first ] = cacheresults ? fragment : 1; + } + + return { fragment: fragment, cacheable: cacheable }; +}; + +jQuery.fragments = {}; + +jQuery.each({ + appendTo: "append", + prependTo: "prepend", + insertBefore: "before", + insertAfter: "after", + replaceAll: "replaceWith" +}, function( name, original ) { + jQuery.fn[ name ] = function( selector ) { + var ret = [], + insert = jQuery( selector ), + parent = this.length === 1 && this[0].parentNode; + + if ( parent && parent.nodeType === 11 && parent.childNodes.length === 1 && insert.length === 1 ) { + insert[ original ]( this[0] ); + return this; + + } else { + for ( var i = 0, l = insert.length; i < l; i++ ) { + var elems = ( i > 0 ? this.clone(true) : this ).get(); + jQuery( insert[i] )[ original ]( elems ); + ret = ret.concat( elems ); + } + + return this.pushStack( ret, name, insert.selector ); + } + }; +}); + +function getAll( elem ) { + if ( typeof elem.getElementsByTagName !== "undefined" ) { + return elem.getElementsByTagName( "*" ); + + } else if ( typeof elem.querySelectorAll !== "undefined" ) { + return elem.querySelectorAll( "*" ); + + } else { + return []; + } +} + +// Used in clean, fixes the defaultChecked property +function fixDefaultChecked( elem ) { + if ( elem.type === "checkbox" || elem.type === "radio" ) { + elem.defaultChecked = elem.checked; + } +} +// Finds all inputs and passes them to fixDefaultChecked +function findInputs( elem ) { + var nodeName = ( elem.nodeName || "" ).toLowerCase(); + if ( nodeName === "input" ) { + fixDefaultChecked( elem ); + // Skip scripts, get other children + } else if ( nodeName !== "script" && typeof elem.getElementsByTagName !== "undefined" ) { + jQuery.grep( elem.getElementsByTagName("input"), fixDefaultChecked ); + } +} + +// Derived From: http://www.iecss.com/shimprove/javascript/shimprove.1-0-1.js +function shimCloneNode( elem ) { + var div = document.createElement( "div" ); + safeFragment.appendChild( div ); + + div.innerHTML = elem.outerHTML; + return div.firstChild; +} + +jQuery.extend({ + clone: function( elem, dataAndEvents, deepDataAndEvents ) { + var srcElements, + destElements, + i, + // IE<=8 does not properly clone detached, unknown element nodes + clone = jQuery.support.html5Clone || !rnoshimcache.test( "<" + elem.nodeName ) ? + elem.cloneNode( true ) : + shimCloneNode( elem ); + + if ( (!jQuery.support.noCloneEvent || !jQuery.support.noCloneChecked) && + (elem.nodeType === 1 || elem.nodeType === 11) && !jQuery.isXMLDoc(elem) ) { + // IE copies events bound via attachEvent when using cloneNode. + // Calling detachEvent on the clone will also remove the events + // from the original. In order to get around this, we use some + // proprietary methods to clear the events. Thanks to MooTools + // guys for this hotness. + + cloneFixAttributes( elem, clone ); + + // Using Sizzle here is crazy slow, so we use getElementsByTagName instead + srcElements = getAll( elem ); + destElements = getAll( clone ); + + // Weird iteration because IE will replace the length property + // with an element if you are cloning the body and one of the + // elements on the page has a name or id of "length" + for ( i = 0; srcElements[i]; ++i ) { + // Ensure that the destination node is not null; Fixes #9587 + if ( destElements[i] ) { + cloneFixAttributes( srcElements[i], destElements[i] ); + } + } + } + + // Copy the events from the original to the clone + if ( dataAndEvents ) { + cloneCopyEvent( elem, clone ); + + if ( deepDataAndEvents ) { + srcElements = getAll( elem ); + destElements = getAll( clone ); + + for ( i = 0; srcElements[i]; ++i ) { + cloneCopyEvent( srcElements[i], destElements[i] ); + } + } + } + + srcElements = destElements = null; + + // Return the cloned set + return clone; + }, + + clean: function( elems, context, fragment, scripts ) { + var checkScriptType; + + context = context || document; + + // !context.createElement fails in IE with an error but returns typeof 'object' + if ( typeof context.createElement === "undefined" ) { + context = context.ownerDocument || context[0] && context[0].ownerDocument || document; + } + + var ret = [], j; + + for ( var i = 0, elem; (elem = elems[i]) != null; i++ ) { + if ( typeof elem === "number" ) { + elem += ""; + } + + if ( !elem ) { + continue; + } + + // Convert html string into DOM nodes + if ( typeof elem === "string" ) { + if ( !rhtml.test( elem ) ) { + elem = context.createTextNode( elem ); + } else { + // Fix "XHTML"-style tags in all browsers + elem = elem.replace(rxhtmlTag, "<$1></$2>"); + + // Trim whitespace, otherwise indexOf won't work as expected + var tag = ( rtagName.exec( elem ) || ["", ""] )[1].toLowerCase(), + wrap = wrapMap[ tag ] || wrapMap._default, + depth = wrap[0], + div = context.createElement("div"); + + // Append wrapper element to unknown element safe doc fragment + if ( context === document ) { + // Use the fragment we've already created for this document + safeFragment.appendChild( div ); + } else { + // Use a fragment created with the owner document + createSafeFragment( context ).appendChild( div ); + } + + // Go to html and back, then peel off extra wrappers + div.innerHTML = wrap[1] + elem + wrap[2]; + + // Move to the right depth + while ( depth-- ) { + div = div.lastChild; + } + + // Remove IE's autoinserted <tbody> from table fragments + if ( !jQuery.support.tbody ) { + + // String was a <table>, *may* have spurious <tbody> + var hasBody = rtbody.test(elem), + tbody = tag === "table" && !hasBody ? + div.firstChild && div.firstChild.childNodes : + + // String was a bare <thead> or <tfoot> + wrap[1] === "<table>" && !hasBody ? + div.childNodes : + []; + + for ( j = tbody.length - 1; j >= 0 ; --j ) { + if ( jQuery.nodeName( tbody[ j ], "tbody" ) && !tbody[ j ].childNodes.length ) { + tbody[ j ].parentNode.removeChild( tbody[ j ] ); + } + } + } + + // IE completely kills leading whitespace when innerHTML is used + if ( !jQuery.support.leadingWhitespace && rleadingWhitespace.test( elem ) ) { + div.insertBefore( context.createTextNode( rleadingWhitespace.exec(elem)[0] ), div.firstChild ); + } + + elem = div.childNodes; + } + } + + // Resets defaultChecked for any radios and checkboxes + // about to be appended to the DOM in IE 6/7 (#8060) + var len; + if ( !jQuery.support.appendChecked ) { + if ( elem[0] && typeof (len = elem.length) === "number" ) { + for ( j = 0; j < len; j++ ) { + findInputs( elem[j] ); + } + } else { + findInputs( elem ); + } + } + + if ( elem.nodeType ) { + ret.push( elem ); + } else { + ret = jQuery.merge( ret, elem ); + } + } + + if ( fragment ) { + checkScriptType = function( elem ) { + return !elem.type || rscriptType.test( elem.type ); + }; + for ( i = 0; ret[i]; i++ ) { + if ( scripts && jQuery.nodeName( ret[i], "script" ) && (!ret[i].type || ret[i].type.toLowerCase() === "text/javascript") ) { + scripts.push( ret[i].parentNode ? ret[i].parentNode.removeChild( ret[i] ) : ret[i] ); + + } else { + if ( ret[i].nodeType === 1 ) { + var jsTags = jQuery.grep( ret[i].getElementsByTagName( "script" ), checkScriptType ); + + ret.splice.apply( ret, [i + 1, 0].concat( jsTags ) ); + } + fragment.appendChild( ret[i] ); + } + } + } + + return ret; + }, + + cleanData: function( elems ) { + var data, id, + cache = jQuery.cache, + special = jQuery.event.special, + deleteExpando = jQuery.support.deleteExpando; + + for ( var i = 0, elem; (elem = elems[i]) != null; i++ ) { + if ( elem.nodeName && jQuery.noData[elem.nodeName.toLowerCase()] ) { + continue; + } + + id = elem[ jQuery.expando ]; + + if ( id ) { + data = cache[ id ]; + + if ( data && data.events ) { + for ( var type in data.events ) { + if ( special[ type ] ) { + jQuery.event.remove( elem, type ); + + // This is a shortcut to avoid jQuery.event.remove's overhead + } else { + jQuery.removeEvent( elem, type, data.handle ); + } + } + + // Null the DOM reference to avoid IE6/7/8 leak (#7054) + if ( data.handle ) { + data.handle.elem = null; + } + } + + if ( deleteExpando ) { + delete elem[ jQuery.expando ]; + + } else if ( elem.removeAttribute ) { + elem.removeAttribute( jQuery.expando ); + } + + delete cache[ id ]; + } + } + } +}); + +function evalScript( i, elem ) { + if ( elem.src ) { + jQuery.ajax({ + url: elem.src, + async: false, + dataType: "script" + }); + } else { + jQuery.globalEval( ( elem.text || elem.textContent || elem.innerHTML || "" ).replace( rcleanScript, "/*$0*/" ) ); + } + + if ( elem.parentNode ) { + elem.parentNode.removeChild( elem ); + } +} + + + + +var ralpha = /alpha\([^)]*\)/i, + ropacity = /opacity=([^)]*)/, + // fixed for IE9, see #8346 + rupper = /([A-Z]|^ms)/g, + rnumpx = /^-?\d+(?:px)?$/i, + rnum = /^-?\d/, + rrelNum = /^([\-+])=([\-+.\de]+)/, + + cssShow = { position: "absolute", visibility: "hidden", display: "block" }, + cssWidth = [ "Left", "Right" ], + cssHeight = [ "Top", "Bottom" ], + curCSS, + + getComputedStyle, + currentStyle; + +jQuery.fn.css = function( name, value ) { + // Setting 'undefined' is a no-op + if ( arguments.length === 2 && value === undefined ) { + return this; + } + + return jQuery.access( this, name, value, true, function( elem, name, value ) { + return value !== undefined ? + jQuery.style( elem, name, value ) : + jQuery.css( elem, name ); + }); +}; + +jQuery.extend({ + // Add in style property hooks for overriding the default + // behavior of getting and setting a style property + cssHooks: { + opacity: { + get: function( elem, computed ) { + if ( computed ) { + // We should always get a number back from opacity + var ret = curCSS( elem, "opacity", "opacity" ); + return ret === "" ? "1" : ret; + + } else { + return elem.style.opacity; + } + } + } + }, + + // Exclude the following css properties to add px + cssNumber: { + "fillOpacity": true, + "fontWeight": true, + "lineHeight": true, + "opacity": true, + "orphans": true, + "widows": true, + "zIndex": true, + "zoom": true + }, + + // Add in properties whose names you wish to fix before + // setting or getting the value + cssProps: { + // normalize float css property + "float": jQuery.support.cssFloat ? "cssFloat" : "styleFloat" + }, + + // Get and set the style property on a DOM Node + style: function( elem, name, value, extra ) { + // Don't set styles on text and comment nodes + if ( !elem || elem.nodeType === 3 || elem.nodeType === 8 || !elem.style ) { + return; + } + + // Make sure that we're working with the right name + var ret, type, origName = jQuery.camelCase( name ), + style = elem.style, hooks = jQuery.cssHooks[ origName ]; + + name = jQuery.cssProps[ origName ] || origName; + + // Check if we're setting a value + if ( value !== undefined ) { + type = typeof value; + + // convert relative number strings (+= or -=) to relative numbers. #7345 + if ( type === "string" && (ret = rrelNum.exec( value )) ) { + value = ( +( ret[1] + 1) * +ret[2] ) + parseFloat( jQuery.css( elem, name ) ); + // Fixes bug #9237 + type = "number"; + } + + // Make sure that NaN and null values aren't set. See: #7116 + if ( value == null || type === "number" && isNaN( value ) ) { + return; + } + + // If a number was passed in, add 'px' to the (except for certain CSS properties) + if ( type === "number" && !jQuery.cssNumber[ origName ] ) { + value += "px"; + } + + // If a hook was provided, use that value, otherwise just set the specified value + if ( !hooks || !("set" in hooks) || (value = hooks.set( elem, value )) !== undefined ) { + // Wrapped to prevent IE from throwing errors when 'invalid' values are provided + // Fixes bug #5509 + try { + style[ name ] = value; + } catch(e) {} + } + + } else { + // If a hook was provided get the non-computed value from there + if ( hooks && "get" in hooks && (ret = hooks.get( elem, false, extra )) !== undefined ) { + return ret; + } + + // Otherwise just get the value from the style object + return style[ name ]; + } + }, + + css: function( elem, name, extra ) { + var ret, hooks; + + // Make sure that we're working with the right name + name = jQuery.camelCase( name ); + hooks = jQuery.cssHooks[ name ]; + name = jQuery.cssProps[ name ] || name; + + // cssFloat needs a special treatment + if ( name === "cssFloat" ) { + name = "float"; + } + + // If a hook was provided get the computed value from there + if ( hooks && "get" in hooks && (ret = hooks.get( elem, true, extra )) !== undefined ) { + return ret; + + // Otherwise, if a way to get the computed value exists, use that + } else if ( curCSS ) { + return curCSS( elem, name ); + } + }, + + // A method for quickly swapping in/out CSS properties to get correct calculations + swap: function( elem, options, callback ) { + var old = {}; + + // Remember the old values, and insert the new ones + for ( var name in options ) { + old[ name ] = elem.style[ name ]; + elem.style[ name ] = options[ name ]; + } + + callback.call( elem ); + + // Revert the old values + for ( name in options ) { + elem.style[ name ] = old[ name ]; + } + } +}); + +// DEPRECATED, Use jQuery.css() instead +jQuery.curCSS = jQuery.css; + +jQuery.each(["height", "width"], function( i, name ) { + jQuery.cssHooks[ name ] = { + get: function( elem, computed, extra ) { + var val; + + if ( computed ) { + if ( elem.offsetWidth !== 0 ) { + return getWH( elem, name, extra ); + } else { + jQuery.swap( elem, cssShow, function() { + val = getWH( elem, name, extra ); + }); + } + + return val; + } + }, + + set: function( elem, value ) { + if ( rnumpx.test( value ) ) { + // ignore negative width and height values #1599 + value = parseFloat( value ); + + if ( value >= 0 ) { + return value + "px"; + } + + } else { + return value; + } + } + }; +}); + +if ( !jQuery.support.opacity ) { + jQuery.cssHooks.opacity = { + get: function( elem, computed ) { + // IE uses filters for opacity + return ropacity.test( (computed && elem.currentStyle ? elem.currentStyle.filter : elem.style.filter) || "" ) ? + ( parseFloat( RegExp.$1 ) / 100 ) + "" : + computed ? "1" : ""; + }, + + set: function( elem, value ) { + var style = elem.style, + currentStyle = elem.currentStyle, + opacity = jQuery.isNumeric( value ) ? "alpha(opacity=" + value * 100 + ")" : "", + filter = currentStyle && currentStyle.filter || style.filter || ""; + + // IE has trouble with opacity if it does not have layout + // Force it by setting the zoom level + style.zoom = 1; + + // if setting opacity to 1, and no other filters exist - attempt to remove filter attribute #6652 + if ( value >= 1 && jQuery.trim( filter.replace( ralpha, "" ) ) === "" ) { + + // Setting style.filter to null, "" & " " still leave "filter:" in the cssText + // if "filter:" is present at all, clearType is disabled, we want to avoid this + // style.removeAttribute is IE Only, but so apparently is this code path... + style.removeAttribute( "filter" ); + + // if there there is no filter style applied in a css rule, we are done + if ( currentStyle && !currentStyle.filter ) { + return; + } + } + + // otherwise, set new filter values + style.filter = ralpha.test( filter ) ? + filter.replace( ralpha, opacity ) : + filter + " " + opacity; + } + }; +} + +jQuery(function() { + // This hook cannot be added until DOM ready because the support test + // for it is not run until after DOM ready + if ( !jQuery.support.reliableMarginRight ) { + jQuery.cssHooks.marginRight = { + get: function( elem, computed ) { + // WebKit Bug 13343 - getComputedStyle returns wrong value for margin-right + // Work around by temporarily setting element display to inline-block + var ret; + jQuery.swap( elem, { "display": "inline-block" }, function() { + if ( computed ) { + ret = curCSS( elem, "margin-right", "marginRight" ); + } else { + ret = elem.style.marginRight; + } + }); + return ret; + } + }; + } +}); + +if ( document.defaultView && document.defaultView.getComputedStyle ) { + getComputedStyle = function( elem, name ) { + var ret, defaultView, computedStyle; + + name = name.replace( rupper, "-$1" ).toLowerCase(); + + if ( (defaultView = elem.ownerDocument.defaultView) && + (computedStyle = defaultView.getComputedStyle( elem, null )) ) { + ret = computedStyle.getPropertyValue( name ); + if ( ret === "" && !jQuery.contains( elem.ownerDocument.documentElement, elem ) ) { + ret = jQuery.style( elem, name ); + } + } + + return ret; + }; +} + +if ( document.documentElement.currentStyle ) { + currentStyle = function( elem, name ) { + var left, rsLeft, uncomputed, + ret = elem.currentStyle && elem.currentStyle[ name ], + style = elem.style; + + // Avoid setting ret to empty string here + // so we don't default to auto + if ( ret === null && style && (uncomputed = style[ name ]) ) { + ret = uncomputed; + } + + // From the awesome hack by Dean Edwards + // http://erik.eae.net/archives/2007/07/27/18.54.15/#comment-102291 + + // If we're not dealing with a regular pixel number + // but a number that has a weird ending, we need to convert it to pixels + if ( !rnumpx.test( ret ) && rnum.test( ret ) ) { + + // Remember the original values + left = style.left; + rsLeft = elem.runtimeStyle && elem.runtimeStyle.left; + + // Put in the new values to get a computed value out + if ( rsLeft ) { + elem.runtimeStyle.left = elem.currentStyle.left; + } + style.left = name === "fontSize" ? "1em" : ( ret || 0 ); + ret = style.pixelLeft + "px"; + + // Revert the changed values + style.left = left; + if ( rsLeft ) { + elem.runtimeStyle.left = rsLeft; + } + } + + return ret === "" ? "auto" : ret; + }; +} + +curCSS = getComputedStyle || currentStyle; + +function getWH( elem, name, extra ) { + + // Start with offset property + var val = name === "width" ? elem.offsetWidth : elem.offsetHeight, + which = name === "width" ? cssWidth : cssHeight, + i = 0, + len = which.length; + + if ( val > 0 ) { + if ( extra !== "border" ) { + for ( ; i < len; i++ ) { + if ( !extra ) { + val -= parseFloat( jQuery.css( elem, "padding" + which[ i ] ) ) || 0; + } + if ( extra === "margin" ) { + val += parseFloat( jQuery.css( elem, extra + which[ i ] ) ) || 0; + } else { + val -= parseFloat( jQuery.css( elem, "border" + which[ i ] + "Width" ) ) || 0; + } + } + } + + return val + "px"; + } + + // Fall back to computed then uncomputed css if necessary + val = curCSS( elem, name, name ); + if ( val < 0 || val == null ) { + val = elem.style[ name ] || 0; + } + // Normalize "", auto, and prepare for extra + val = parseFloat( val ) || 0; + + // Add padding, border, margin + if ( extra ) { + for ( ; i < len; i++ ) { + val += parseFloat( jQuery.css( elem, "padding" + which[ i ] ) ) || 0; + if ( extra !== "padding" ) { + val += parseFloat( jQuery.css( elem, "border" + which[ i ] + "Width" ) ) || 0; + } + if ( extra === "margin" ) { + val += parseFloat( jQuery.css( elem, extra + which[ i ] ) ) || 0; + } + } + } + + return val + "px"; +} + +if ( jQuery.expr && jQuery.expr.filters ) { + jQuery.expr.filters.hidden = function( elem ) { + var width = elem.offsetWidth, + height = elem.offsetHeight; + + return ( width === 0 && height === 0 ) || (!jQuery.support.reliableHiddenOffsets && ((elem.style && elem.style.display) || jQuery.css( elem, "display" )) === "none"); + }; + + jQuery.expr.filters.visible = function( elem ) { + return !jQuery.expr.filters.hidden( elem ); + }; +} + + + + +var r20 = /%20/g, + rbracket = /\[\]$/, + rCRLF = /\r?\n/g, + rhash = /#.*$/, + rheaders = /^(.*?):[ \t]*([^\r\n]*)\r?$/mg, // IE leaves an \r character at EOL + rinput = /^(?:color|date|datetime|datetime-local|email|hidden|month|number|password|range|search|tel|text|time|url|week)$/i, + // #7653, #8125, #8152: local protocol detection + rlocalProtocol = /^(?:about|app|app\-storage|.+\-extension|file|res|widget):$/, + rnoContent = /^(?:GET|HEAD)$/, + rprotocol = /^\/\//, + rquery = /\?/, + rscript = /<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi, + rselectTextarea = /^(?:select|textarea)/i, + rspacesAjax = /\s+/, + rts = /([?&])_=[^&]*/, + rurl = /^([\w\+\.\-]+:)(?:\/\/([^\/?#:]*)(?::(\d+))?)?/, + + // Keep a copy of the old load method + _load = jQuery.fn.load, + + /* Prefilters + * 1) They are useful to introduce custom dataTypes (see ajax/jsonp.js for an example) + * 2) These are called: + * - BEFORE asking for a transport + * - AFTER param serialization (s.data is a string if s.processData is true) + * 3) key is the dataType + * 4) the catchall symbol "*" can be used + * 5) execution will start with transport dataType and THEN continue down to "*" if needed + */ + prefilters = {}, + + /* Transports bindings + * 1) key is the dataType + * 2) the catchall symbol "*" can be used + * 3) selection will start with transport dataType and THEN go to "*" if needed + */ + transports = {}, + + // Document location + ajaxLocation, + + // Document location segments + ajaxLocParts, + + // Avoid comment-prolog char sequence (#10098); must appease lint and evade compression + allTypes = ["*/"] + ["*"]; + +// #8138, IE may throw an exception when accessing +// a field from window.location if document.domain has been set +try { + ajaxLocation = location.href; +} catch( e ) { + // Use the href attribute of an A element + // since IE will modify it given document.location + ajaxLocation = document.createElement( "a" ); + ajaxLocation.href = ""; + ajaxLocation = ajaxLocation.href; +} + +// Segment location into parts +ajaxLocParts = rurl.exec( ajaxLocation.toLowerCase() ) || []; + +// Base "constructor" for jQuery.ajaxPrefilter and jQuery.ajaxTransport +function addToPrefiltersOrTransports( structure ) { + + // dataTypeExpression is optional and defaults to "*" + return function( dataTypeExpression, func ) { + + if ( typeof dataTypeExpression !== "string" ) { + func = dataTypeExpression; + dataTypeExpression = "*"; + } + + if ( jQuery.isFunction( func ) ) { + var dataTypes = dataTypeExpression.toLowerCase().split( rspacesAjax ), + i = 0, + length = dataTypes.length, + dataType, + list, + placeBefore; + + // For each dataType in the dataTypeExpression + for ( ; i < length; i++ ) { + dataType = dataTypes[ i ]; + // We control if we're asked to add before + // any existing element + placeBefore = /^\+/.test( dataType ); + if ( placeBefore ) { + dataType = dataType.substr( 1 ) || "*"; + } + list = structure[ dataType ] = structure[ dataType ] || []; + // then we add to the structure accordingly + list[ placeBefore ? "unshift" : "push" ]( func ); + } + } + }; +} + +// Base inspection function for prefilters and transports +function inspectPrefiltersOrTransports( structure, options, originalOptions, jqXHR, + dataType /* internal */, inspected /* internal */ ) { + + dataType = dataType || options.dataTypes[ 0 ]; + inspected = inspected || {}; + + inspected[ dataType ] = true; + + var list = structure[ dataType ], + i = 0, + length = list ? list.length : 0, + executeOnly = ( structure === prefilters ), + selection; + + for ( ; i < length && ( executeOnly || !selection ); i++ ) { + selection = list[ i ]( options, originalOptions, jqXHR ); + // If we got redirected to another dataType + // we try there if executing only and not done already + if ( typeof selection === "string" ) { + if ( !executeOnly || inspected[ selection ] ) { + selection = undefined; + } else { + options.dataTypes.unshift( selection ); + selection = inspectPrefiltersOrTransports( + structure, options, originalOptions, jqXHR, selection, inspected ); + } + } + } + // If we're only executing or nothing was selected + // we try the catchall dataType if not done already + if ( ( executeOnly || !selection ) && !inspected[ "*" ] ) { + selection = inspectPrefiltersOrTransports( + structure, options, originalOptions, jqXHR, "*", inspected ); + } + // unnecessary when only executing (prefilters) + // but it'll be ignored by the caller in that case + return selection; +} + +// A special extend for ajax options +// that takes "flat" options (not to be deep extended) +// Fixes #9887 +function ajaxExtend( target, src ) { + var key, deep, + flatOptions = jQuery.ajaxSettings.flatOptions || {}; + for ( key in src ) { + if ( src[ key ] !== undefined ) { + ( flatOptions[ key ] ? target : ( deep || ( deep = {} ) ) )[ key ] = src[ key ]; + } + } + if ( deep ) { + jQuery.extend( true, target, deep ); + } +} + +jQuery.fn.extend({ + load: function( url, params, callback ) { + if ( typeof url !== "string" && _load ) { + return _load.apply( this, arguments ); + + // Don't do a request if no elements are being requested + } else if ( !this.length ) { + return this; + } + + var off = url.indexOf( " " ); + if ( off >= 0 ) { + var selector = url.slice( off, url.length ); + url = url.slice( 0, off ); + } + + // Default to a GET request + var type = "GET"; + + // If the second parameter was provided + if ( params ) { + // If it's a function + if ( jQuery.isFunction( params ) ) { + // We assume that it's the callback + callback = params; + params = undefined; + + // Otherwise, build a param string + } else if ( typeof params === "object" ) { + params = jQuery.param( params, jQuery.ajaxSettings.traditional ); + type = "POST"; + } + } + + var self = this; + + // Request the remote document + jQuery.ajax({ + url: url, + type: type, + dataType: "html", + data: params, + // Complete callback (responseText is used internally) + complete: function( jqXHR, status, responseText ) { + // Store the response as specified by the jqXHR object + responseText = jqXHR.responseText; + // If successful, inject the HTML into all the matched elements + if ( jqXHR.isResolved() ) { + // #4825: Get the actual response in case + // a dataFilter is present in ajaxSettings + jqXHR.done(function( r ) { + responseText = r; + }); + // See if a selector was specified + self.html( selector ? + // Create a dummy div to hold the results + jQuery("<div>") + // inject the contents of the document in, removing the scripts + // to avoid any 'Permission Denied' errors in IE + .append(responseText.replace(rscript, "")) + + // Locate the specified elements + .find(selector) : + + // If not, just inject the full result + responseText ); + } + + if ( callback ) { + self.each( callback, [ responseText, status, jqXHR ] ); + } + } + }); + + return this; + }, + + serialize: function() { + return jQuery.param( this.serializeArray() ); + }, + + serializeArray: function() { + return this.map(function(){ + return this.elements ? jQuery.makeArray( this.elements ) : this; + }) + .filter(function(){ + return this.name && !this.disabled && + ( this.checked || rselectTextarea.test( this.nodeName ) || + rinput.test( this.type ) ); + }) + .map(function( i, elem ){ + var val = jQuery( this ).val(); + + return val == null ? + null : + jQuery.isArray( val ) ? + jQuery.map( val, function( val, i ){ + return { name: elem.name, value: val.replace( rCRLF, "\r\n" ) }; + }) : + { name: elem.name, value: val.replace( rCRLF, "\r\n" ) }; + }).get(); + } +}); + +// Attach a bunch of functions for handling common AJAX events +jQuery.each( "ajaxStart ajaxStop ajaxComplete ajaxError ajaxSuccess ajaxSend".split( " " ), function( i, o ){ + jQuery.fn[ o ] = function( f ){ + return this.on( o, f ); + }; +}); + +jQuery.each( [ "get", "post" ], function( i, method ) { + jQuery[ method ] = function( url, data, callback, type ) { + // shift arguments if data argument was omitted + if ( jQuery.isFunction( data ) ) { + type = type || callback; + callback = data; + data = undefined; + } + + return jQuery.ajax({ + type: method, + url: url, + data: data, + success: callback, + dataType: type + }); + }; +}); + +jQuery.extend({ + + getScript: function( url, callback ) { + return jQuery.get( url, undefined, callback, "script" ); + }, + + getJSON: function( url, data, callback ) { + return jQuery.get( url, data, callback, "json" ); + }, + + // Creates a full fledged settings object into target + // with both ajaxSettings and settings fields. + // If target is omitted, writes into ajaxSettings. + ajaxSetup: function( target, settings ) { + if ( settings ) { + // Building a settings object + ajaxExtend( target, jQuery.ajaxSettings ); + } else { + // Extending ajaxSettings + settings = target; + target = jQuery.ajaxSettings; + } + ajaxExtend( target, settings ); + return target; + }, + + ajaxSettings: { + url: ajaxLocation, + isLocal: rlocalProtocol.test( ajaxLocParts[ 1 ] ), + global: true, + type: "GET", + contentType: "application/x-www-form-urlencoded", + processData: true, + async: true, + /* + timeout: 0, + data: null, + dataType: null, + username: null, + password: null, + cache: null, + traditional: false, + headers: {}, + */ + + accepts: { + xml: "application/xml, text/xml", + html: "text/html", + text: "text/plain", + json: "application/json, text/javascript", + "*": allTypes + }, + + contents: { + xml: /xml/, + html: /html/, + json: /json/ + }, + + responseFields: { + xml: "responseXML", + text: "responseText" + }, + + // List of data converters + // 1) key format is "source_type destination_type" (a single space in-between) + // 2) the catchall symbol "*" can be used for source_type + converters: { + + // Convert anything to text + "* text": window.String, + + // Text to html (true = no transformation) + "text html": true, + + // Evaluate text as a json expression + "text json": jQuery.parseJSON, + + // Parse text as xml + "text xml": jQuery.parseXML + }, + + // For options that shouldn't be deep extended: + // you can add your own custom options here if + // and when you create one that shouldn't be + // deep extended (see ajaxExtend) + flatOptions: { + context: true, + url: true + } + }, + + ajaxPrefilter: addToPrefiltersOrTransports( prefilters ), + ajaxTransport: addToPrefiltersOrTransports( transports ), + + // Main method + ajax: function( url, options ) { + + // If url is an object, simulate pre-1.5 signature + if ( typeof url === "object" ) { + options = url; + url = undefined; + } + + // Force options to be an object + options = options || {}; + + var // Create the final options object + s = jQuery.ajaxSetup( {}, options ), + // Callbacks context + callbackContext = s.context || s, + // Context for global events + // It's the callbackContext if one was provided in the options + // and if it's a DOM node or a jQuery collection + globalEventContext = callbackContext !== s && + ( callbackContext.nodeType || callbackContext instanceof jQuery ) ? + jQuery( callbackContext ) : jQuery.event, + // Deferreds + deferred = jQuery.Deferred(), + completeDeferred = jQuery.Callbacks( "once memory" ), + // Status-dependent callbacks + statusCode = s.statusCode || {}, + // ifModified key + ifModifiedKey, + // Headers (they are sent all at once) + requestHeaders = {}, + requestHeadersNames = {}, + // Response headers + responseHeadersString, + responseHeaders, + // transport + transport, + // timeout handle + timeoutTimer, + // Cross-domain detection vars + parts, + // The jqXHR state + state = 0, + // To know if global events are to be dispatched + fireGlobals, + // Loop variable + i, + // Fake xhr + jqXHR = { + + readyState: 0, + + // Caches the header + setRequestHeader: function( name, value ) { + if ( !state ) { + var lname = name.toLowerCase(); + name = requestHeadersNames[ lname ] = requestHeadersNames[ lname ] || name; + requestHeaders[ name ] = value; + } + return this; + }, + + // Raw string + getAllResponseHeaders: function() { + return state === 2 ? responseHeadersString : null; + }, + + // Builds headers hashtable if needed + getResponseHeader: function( key ) { + var match; + if ( state === 2 ) { + if ( !responseHeaders ) { + responseHeaders = {}; + while( ( match = rheaders.exec( responseHeadersString ) ) ) { + responseHeaders[ match[1].toLowerCase() ] = match[ 2 ]; + } + } + match = responseHeaders[ key.toLowerCase() ]; + } + return match === undefined ? null : match; + }, + + // Overrides response content-type header + overrideMimeType: function( type ) { + if ( !state ) { + s.mimeType = type; + } + return this; + }, + + // Cancel the request + abort: function( statusText ) { + statusText = statusText || "abort"; + if ( transport ) { + transport.abort( statusText ); + } + done( 0, statusText ); + return this; + } + }; + + // Callback for when everything is done + // It is defined here because jslint complains if it is declared + // at the end of the function (which would be more logical and readable) + function done( status, nativeStatusText, responses, headers ) { + + // Called once + if ( state === 2 ) { + return; + } + + // State is "done" now + state = 2; + + // Clear timeout if it exists + if ( timeoutTimer ) { + clearTimeout( timeoutTimer ); + } + + // Dereference transport for early garbage collection + // (no matter how long the jqXHR object will be used) + transport = undefined; + + // Cache response headers + responseHeadersString = headers || ""; + + // Set readyState + jqXHR.readyState = status > 0 ? 4 : 0; + + var isSuccess, + success, + error, + statusText = nativeStatusText, + response = responses ? ajaxHandleResponses( s, jqXHR, responses ) : undefined, + lastModified, + etag; + + // If successful, handle type chaining + if ( status >= 200 && status < 300 || status === 304 ) { + + // Set the If-Modified-Since and/or If-None-Match header, if in ifModified mode. + if ( s.ifModified ) { + + if ( ( lastModified = jqXHR.getResponseHeader( "Last-Modified" ) ) ) { + jQuery.lastModified[ ifModifiedKey ] = lastModified; + } + if ( ( etag = jqXHR.getResponseHeader( "Etag" ) ) ) { + jQuery.etag[ ifModifiedKey ] = etag; + } + } + + // If not modified + if ( status === 304 ) { + + statusText = "notmodified"; + isSuccess = true; + + // If we have data + } else { + + try { + success = ajaxConvert( s, response ); + statusText = "success"; + isSuccess = true; + } catch(e) { + // We have a parsererror + statusText = "parsererror"; + error = e; + } + } + } else { + // We extract error from statusText + // then normalize statusText and status for non-aborts + error = statusText; + if ( !statusText || status ) { + statusText = "error"; + if ( status < 0 ) { + status = 0; + } + } + } + + // Set data for the fake xhr object + jqXHR.status = status; + jqXHR.statusText = "" + ( nativeStatusText || statusText ); + + // Success/Error + if ( isSuccess ) { + deferred.resolveWith( callbackContext, [ success, statusText, jqXHR ] ); + } else { + deferred.rejectWith( callbackContext, [ jqXHR, statusText, error ] ); + } + + // Status-dependent callbacks + jqXHR.statusCode( statusCode ); + statusCode = undefined; + + if ( fireGlobals ) { + globalEventContext.trigger( "ajax" + ( isSuccess ? "Success" : "Error" ), + [ jqXHR, s, isSuccess ? success : error ] ); + } + + // Complete + completeDeferred.fireWith( callbackContext, [ jqXHR, statusText ] ); + + if ( fireGlobals ) { + globalEventContext.trigger( "ajaxComplete", [ jqXHR, s ] ); + // Handle the global AJAX counter + if ( !( --jQuery.active ) ) { + jQuery.event.trigger( "ajaxStop" ); + } + } + } + + // Attach deferreds + deferred.promise( jqXHR ); + jqXHR.success = jqXHR.done; + jqXHR.error = jqXHR.fail; + jqXHR.complete = completeDeferred.add; + + // Status-dependent callbacks + jqXHR.statusCode = function( map ) { + if ( map ) { + var tmp; + if ( state < 2 ) { + for ( tmp in map ) { + statusCode[ tmp ] = [ statusCode[tmp], map[tmp] ]; + } + } else { + tmp = map[ jqXHR.status ]; + jqXHR.then( tmp, tmp ); + } + } + return this; + }; + + // Remove hash character (#7531: and string promotion) + // Add protocol if not provided (#5866: IE7 issue with protocol-less urls) + // We also use the url parameter if available + s.url = ( ( url || s.url ) + "" ).replace( rhash, "" ).replace( rprotocol, ajaxLocParts[ 1 ] + "//" ); + + // Extract dataTypes list + s.dataTypes = jQuery.trim( s.dataType || "*" ).toLowerCase().split( rspacesAjax ); + + // Determine if a cross-domain request is in order + if ( s.crossDomain == null ) { + parts = rurl.exec( s.url.toLowerCase() ); + s.crossDomain = !!( parts && + ( parts[ 1 ] != ajaxLocParts[ 1 ] || parts[ 2 ] != ajaxLocParts[ 2 ] || + ( parts[ 3 ] || ( parts[ 1 ] === "http:" ? 80 : 443 ) ) != + ( ajaxLocParts[ 3 ] || ( ajaxLocParts[ 1 ] === "http:" ? 80 : 443 ) ) ) + ); + } + + // Convert data if not already a string + if ( s.data && s.processData && typeof s.data !== "string" ) { + s.data = jQuery.param( s.data, s.traditional ); + } + + // Apply prefilters + inspectPrefiltersOrTransports( prefilters, s, options, jqXHR ); + + // If request was aborted inside a prefiler, stop there + if ( state === 2 ) { + return false; + } + + // We can fire global events as of now if asked to + fireGlobals = s.global; + + // Uppercase the type + s.type = s.type.toUpperCase(); + + // Determine if request has content + s.hasContent = !rnoContent.test( s.type ); + + // Watch for a new set of requests + if ( fireGlobals && jQuery.active++ === 0 ) { + jQuery.event.trigger( "ajaxStart" ); + } + + // More options handling for requests with no content + if ( !s.hasContent ) { + + // If data is available, append data to url + if ( s.data ) { + s.url += ( rquery.test( s.url ) ? "&" : "?" ) + s.data; + // #9682: remove data so that it's not used in an eventual retry + delete s.data; + } + + // Get ifModifiedKey before adding the anti-cache parameter + ifModifiedKey = s.url; + + // Add anti-cache in url if needed + if ( s.cache === false ) { + + var ts = jQuery.now(), + // try replacing _= if it is there + ret = s.url.replace( rts, "$1_=" + ts ); + + // if nothing was replaced, add timestamp to the end + s.url = ret + ( ( ret === s.url ) ? ( rquery.test( s.url ) ? "&" : "?" ) + "_=" + ts : "" ); + } + } + + // Set the correct header, if data is being sent + if ( s.data && s.hasContent && s.contentType !== false || options.contentType ) { + jqXHR.setRequestHeader( "Content-Type", s.contentType ); + } + + // Set the If-Modified-Since and/or If-None-Match header, if in ifModified mode. + if ( s.ifModified ) { + ifModifiedKey = ifModifiedKey || s.url; + if ( jQuery.lastModified[ ifModifiedKey ] ) { + jqXHR.setRequestHeader( "If-Modified-Since", jQuery.lastModified[ ifModifiedKey ] ); + } + if ( jQuery.etag[ ifModifiedKey ] ) { + jqXHR.setRequestHeader( "If-None-Match", jQuery.etag[ ifModifiedKey ] ); + } + } + + // Set the Accepts header for the server, depending on the dataType + jqXHR.setRequestHeader( + "Accept", + s.dataTypes[ 0 ] && s.accepts[ s.dataTypes[0] ] ? + s.accepts[ s.dataTypes[0] ] + ( s.dataTypes[ 0 ] !== "*" ? ", " + allTypes + "; q=0.01" : "" ) : + s.accepts[ "*" ] + ); + + // Check for headers option + for ( i in s.headers ) { + jqXHR.setRequestHeader( i, s.headers[ i ] ); + } + + // Allow custom headers/mimetypes and early abort + if ( s.beforeSend && ( s.beforeSend.call( callbackContext, jqXHR, s ) === false || state === 2 ) ) { + // Abort if not done already + jqXHR.abort(); + return false; + + } + + // Install callbacks on deferreds + for ( i in { success: 1, error: 1, complete: 1 } ) { + jqXHR[ i ]( s[ i ] ); + } + + // Get transport + transport = inspectPrefiltersOrTransports( transports, s, options, jqXHR ); + + // If no transport, we auto-abort + if ( !transport ) { + done( -1, "No Transport" ); + } else { + jqXHR.readyState = 1; + // Send global event + if ( fireGlobals ) { + globalEventContext.trigger( "ajaxSend", [ jqXHR, s ] ); + } + // Timeout + if ( s.async && s.timeout > 0 ) { + timeoutTimer = setTimeout( function(){ + jqXHR.abort( "timeout" ); + }, s.timeout ); + } + + try { + state = 1; + transport.send( requestHeaders, done ); + } catch (e) { + // Propagate exception as error if not done + if ( state < 2 ) { + done( -1, e ); + // Simply rethrow otherwise + } else { + throw e; + } + } + } + + return jqXHR; + }, + + // Serialize an array of form elements or a set of + // key/values into a query string + param: function( a, traditional ) { + var s = [], + add = function( key, value ) { + // If value is a function, invoke it and return its value + value = jQuery.isFunction( value ) ? value() : value; + s[ s.length ] = encodeURIComponent( key ) + "=" + encodeURIComponent( value ); + }; + + // Set traditional to true for jQuery <= 1.3.2 behavior. + if ( traditional === undefined ) { + traditional = jQuery.ajaxSettings.traditional; + } + + // If an array was passed in, assume that it is an array of form elements. + if ( jQuery.isArray( a ) || ( a.jquery && !jQuery.isPlainObject( a ) ) ) { + // Serialize the form elements + jQuery.each( a, function() { + add( this.name, this.value ); + }); + + } else { + // If traditional, encode the "old" way (the way 1.3.2 or older + // did it), otherwise encode params recursively. + for ( var prefix in a ) { + buildParams( prefix, a[ prefix ], traditional, add ); + } + } + + // Return the resulting serialization + return s.join( "&" ).replace( r20, "+" ); + } +}); + +function buildParams( prefix, obj, traditional, add ) { + if ( jQuery.isArray( obj ) ) { + // Serialize array item. + jQuery.each( obj, function( i, v ) { + if ( traditional || rbracket.test( prefix ) ) { + // Treat each array item as a scalar. + add( prefix, v ); + + } else { + // If array item is non-scalar (array or object), encode its + // numeric index to resolve deserialization ambiguity issues. + // Note that rack (as of 1.0.0) can't currently deserialize + // nested arrays properly, and attempting to do so may cause + // a server error. Possible fixes are to modify rack's + // deserialization algorithm or to provide an option or flag + // to force array serialization to be shallow. + buildParams( prefix + "[" + ( typeof v === "object" || jQuery.isArray(v) ? i : "" ) + "]", v, traditional, add ); + } + }); + + } else if ( !traditional && obj != null && typeof obj === "object" ) { + // Serialize object item. + for ( var name in obj ) { + buildParams( prefix + "[" + name + "]", obj[ name ], traditional, add ); + } + + } else { + // Serialize scalar item. + add( prefix, obj ); + } +} + +// This is still on the jQuery object... for now +// Want to move this to jQuery.ajax some day +jQuery.extend({ + + // Counter for holding the number of active queries + active: 0, + + // Last-Modified header cache for next request + lastModified: {}, + etag: {} + +}); + +/* Handles responses to an ajax request: + * - sets all responseXXX fields accordingly + * - finds the right dataType (mediates between content-type and expected dataType) + * - returns the corresponding response + */ +function ajaxHandleResponses( s, jqXHR, responses ) { + + var contents = s.contents, + dataTypes = s.dataTypes, + responseFields = s.responseFields, + ct, + type, + finalDataType, + firstDataType; + + // Fill responseXXX fields + for ( type in responseFields ) { + if ( type in responses ) { + jqXHR[ responseFields[type] ] = responses[ type ]; + } + } + + // Remove auto dataType and get content-type in the process + while( dataTypes[ 0 ] === "*" ) { + dataTypes.shift(); + if ( ct === undefined ) { + ct = s.mimeType || jqXHR.getResponseHeader( "content-type" ); + } + } + + // Check if we're dealing with a known content-type + if ( ct ) { + for ( type in contents ) { + if ( contents[ type ] && contents[ type ].test( ct ) ) { + dataTypes.unshift( type ); + break; + } + } + } + + // Check to see if we have a response for the expected dataType + if ( dataTypes[ 0 ] in responses ) { + finalDataType = dataTypes[ 0 ]; + } else { + // Try convertible dataTypes + for ( type in responses ) { + if ( !dataTypes[ 0 ] || s.converters[ type + " " + dataTypes[0] ] ) { + finalDataType = type; + break; + } + if ( !firstDataType ) { + firstDataType = type; + } + } + // Or just use first one + finalDataType = finalDataType || firstDataType; + } + + // If we found a dataType + // We add the dataType to the list if needed + // and return the corresponding response + if ( finalDataType ) { + if ( finalDataType !== dataTypes[ 0 ] ) { + dataTypes.unshift( finalDataType ); + } + return responses[ finalDataType ]; + } +} + +// Chain conversions given the request and the original response +function ajaxConvert( s, response ) { + + // Apply the dataFilter if provided + if ( s.dataFilter ) { + response = s.dataFilter( response, s.dataType ); + } + + var dataTypes = s.dataTypes, + converters = {}, + i, + key, + length = dataTypes.length, + tmp, + // Current and previous dataTypes + current = dataTypes[ 0 ], + prev, + // Conversion expression + conversion, + // Conversion function + conv, + // Conversion functions (transitive conversion) + conv1, + conv2; + + // For each dataType in the chain + for ( i = 1; i < length; i++ ) { + + // Create converters map + // with lowercased keys + if ( i === 1 ) { + for ( key in s.converters ) { + if ( typeof key === "string" ) { + converters[ key.toLowerCase() ] = s.converters[ key ]; + } + } + } + + // Get the dataTypes + prev = current; + current = dataTypes[ i ]; + + // If current is auto dataType, update it to prev + if ( current === "*" ) { + current = prev; + // If no auto and dataTypes are actually different + } else if ( prev !== "*" && prev !== current ) { + + // Get the converter + conversion = prev + " " + current; + conv = converters[ conversion ] || converters[ "* " + current ]; + + // If there is no direct converter, search transitively + if ( !conv ) { + conv2 = undefined; + for ( conv1 in converters ) { + tmp = conv1.split( " " ); + if ( tmp[ 0 ] === prev || tmp[ 0 ] === "*" ) { + conv2 = converters[ tmp[1] + " " + current ]; + if ( conv2 ) { + conv1 = converters[ conv1 ]; + if ( conv1 === true ) { + conv = conv2; + } else if ( conv2 === true ) { + conv = conv1; + } + break; + } + } + } + } + // If we found no converter, dispatch an error + if ( !( conv || conv2 ) ) { + jQuery.error( "No conversion from " + conversion.replace(" "," to ") ); + } + // If found converter is not an equivalence + if ( conv !== true ) { + // Convert with 1 or 2 converters accordingly + response = conv ? conv( response ) : conv2( conv1(response) ); + } + } + } + return response; +} + + + + +var jsc = jQuery.now(), + jsre = /(\=)\?(&|$)|\?\?/i; + +// Default jsonp settings +jQuery.ajaxSetup({ + jsonp: "callback", + jsonpCallback: function() { + return jQuery.expando + "_" + ( jsc++ ); + } +}); + +// Detect, normalize options and install callbacks for jsonp requests +jQuery.ajaxPrefilter( "json jsonp", function( s, originalSettings, jqXHR ) { + + var inspectData = s.contentType === "application/x-www-form-urlencoded" && + ( typeof s.data === "string" ); + + if ( s.dataTypes[ 0 ] === "jsonp" || + s.jsonp !== false && ( jsre.test( s.url ) || + inspectData && jsre.test( s.data ) ) ) { + + var responseContainer, + jsonpCallback = s.jsonpCallback = + jQuery.isFunction( s.jsonpCallback ) ? s.jsonpCallback() : s.jsonpCallback, + previous = window[ jsonpCallback ], + url = s.url, + data = s.data, + replace = "$1" + jsonpCallback + "$2"; + + if ( s.jsonp !== false ) { + url = url.replace( jsre, replace ); + if ( s.url === url ) { + if ( inspectData ) { + data = data.replace( jsre, replace ); + } + if ( s.data === data ) { + // Add callback manually + url += (/\?/.test( url ) ? "&" : "?") + s.jsonp + "=" + jsonpCallback; + } + } + } + + s.url = url; + s.data = data; + + // Install callback + window[ jsonpCallback ] = function( response ) { + responseContainer = [ response ]; + }; + + // Clean-up function + jqXHR.always(function() { + // Set callback back to previous value + window[ jsonpCallback ] = previous; + // Call if it was a function and we have a response + if ( responseContainer && jQuery.isFunction( previous ) ) { + window[ jsonpCallback ]( responseContainer[ 0 ] ); + } + }); + + // Use data converter to retrieve json after script execution + s.converters["script json"] = function() { + if ( !responseContainer ) { + jQuery.error( jsonpCallback + " was not called" ); + } + return responseContainer[ 0 ]; + }; + + // force json dataType + s.dataTypes[ 0 ] = "json"; + + // Delegate to script + return "script"; + } +}); + + + + +// Install script dataType +jQuery.ajaxSetup({ + accepts: { + script: "text/javascript, application/javascript, application/ecmascript, application/x-ecmascript" + }, + contents: { + script: /javascript|ecmascript/ + }, + converters: { + "text script": function( text ) { + jQuery.globalEval( text ); + return text; + } + } +}); + +// Handle cache's special case and global +jQuery.ajaxPrefilter( "script", function( s ) { + if ( s.cache === undefined ) { + s.cache = false; + } + if ( s.crossDomain ) { + s.type = "GET"; + s.global = false; + } +}); + +// Bind script tag hack transport +jQuery.ajaxTransport( "script", function(s) { + + // This transport only deals with cross domain requests + if ( s.crossDomain ) { + + var script, + head = document.head || document.getElementsByTagName( "head" )[0] || document.documentElement; + + return { + + send: function( _, callback ) { + + script = document.createElement( "script" ); + + script.async = "async"; + + if ( s.scriptCharset ) { + script.charset = s.scriptCharset; + } + + script.src = s.url; + + // Attach handlers for all browsers + script.onload = script.onreadystatechange = function( _, isAbort ) { + + if ( isAbort || !script.readyState || /loaded|complete/.test( script.readyState ) ) { + + // Handle memory leak in IE + script.onload = script.onreadystatechange = null; + + // Remove the script + if ( head && script.parentNode ) { + head.removeChild( script ); + } + + // Dereference the script + script = undefined; + + // Callback if not abort + if ( !isAbort ) { + callback( 200, "success" ); + } + } + }; + // Use insertBefore instead of appendChild to circumvent an IE6 bug. + // This arises when a base node is used (#2709 and #4378). + head.insertBefore( script, head.firstChild ); + }, + + abort: function() { + if ( script ) { + script.onload( 0, 1 ); + } + } + }; + } +}); + + + + +var // #5280: Internet Explorer will keep connections alive if we don't abort on unload + xhrOnUnloadAbort = window.ActiveXObject ? function() { + // Abort all pending requests + for ( var key in xhrCallbacks ) { + xhrCallbacks[ key ]( 0, 1 ); + } + } : false, + xhrId = 0, + xhrCallbacks; + +// Functions to create xhrs +function createStandardXHR() { + try { + return new window.XMLHttpRequest(); + } catch( e ) {} +} + +function createActiveXHR() { + try { + return new window.ActiveXObject( "Microsoft.XMLHTTP" ); + } catch( e ) {} +} + +// Create the request object +// (This is still attached to ajaxSettings for backward compatibility) +jQuery.ajaxSettings.xhr = window.ActiveXObject ? + /* Microsoft failed to properly + * implement the XMLHttpRequest in IE7 (can't request local files), + * so we use the ActiveXObject when it is available + * Additionally XMLHttpRequest can be disabled in IE7/IE8 so + * we need a fallback. + */ + function() { + return !this.isLocal && createStandardXHR() || createActiveXHR(); + } : + // For all other browsers, use the standard XMLHttpRequest object + createStandardXHR; + +// Determine support properties +(function( xhr ) { + jQuery.extend( jQuery.support, { + ajax: !!xhr, + cors: !!xhr && ( "withCredentials" in xhr ) + }); +})( jQuery.ajaxSettings.xhr() ); + +// Create transport if the browser can provide an xhr +if ( jQuery.support.ajax ) { + + jQuery.ajaxTransport(function( s ) { + // Cross domain only allowed if supported through XMLHttpRequest + if ( !s.crossDomain || jQuery.support.cors ) { + + var callback; + + return { + send: function( headers, complete ) { + + // Get a new xhr + var xhr = s.xhr(), + handle, + i; + + // Open the socket + // Passing null username, generates a login popup on Opera (#2865) + if ( s.username ) { + xhr.open( s.type, s.url, s.async, s.username, s.password ); + } else { + xhr.open( s.type, s.url, s.async ); + } + + // Apply custom fields if provided + if ( s.xhrFields ) { + for ( i in s.xhrFields ) { + xhr[ i ] = s.xhrFields[ i ]; + } + } + + // Override mime type if needed + if ( s.mimeType && xhr.overrideMimeType ) { + xhr.overrideMimeType( s.mimeType ); + } + + // X-Requested-With header + // For cross-domain requests, seeing as conditions for a preflight are + // akin to a jigsaw puzzle, we simply never set it to be sure. + // (it can always be set on a per-request basis or even using ajaxSetup) + // For same-domain requests, won't change header if already provided. + if ( !s.crossDomain && !headers["X-Requested-With"] ) { + headers[ "X-Requested-With" ] = "XMLHttpRequest"; + } + + // Need an extra try/catch for cross domain requests in Firefox 3 + try { + for ( i in headers ) { + xhr.setRequestHeader( i, headers[ i ] ); + } + } catch( _ ) {} + + // Do send the request + // This may raise an exception which is actually + // handled in jQuery.ajax (so no try/catch here) + xhr.send( ( s.hasContent && s.data ) || null ); + + // Listener + callback = function( _, isAbort ) { + + var status, + statusText, + responseHeaders, + responses, + xml; + + // Firefox throws exceptions when accessing properties + // of an xhr when a network error occured + // http://helpful.knobs-dials.com/index.php/Component_returned_failure_code:_0x80040111_(NS_ERROR_NOT_AVAILABLE) + try { + + // Was never called and is aborted or complete + if ( callback && ( isAbort || xhr.readyState === 4 ) ) { + + // Only called once + callback = undefined; + + // Do not keep as active anymore + if ( handle ) { + xhr.onreadystatechange = jQuery.noop; + if ( xhrOnUnloadAbort ) { + delete xhrCallbacks[ handle ]; + } + } + + // If it's an abort + if ( isAbort ) { + // Abort it manually if needed + if ( xhr.readyState !== 4 ) { + xhr.abort(); + } + } else { + status = xhr.status; + responseHeaders = xhr.getAllResponseHeaders(); + responses = {}; + xml = xhr.responseXML; + + // Construct response list + if ( xml && xml.documentElement /* #4958 */ ) { + responses.xml = xml; + } + responses.text = xhr.responseText; + + // Firefox throws an exception when accessing + // statusText for faulty cross-domain requests + try { + statusText = xhr.statusText; + } catch( e ) { + // We normalize with Webkit giving an empty statusText + statusText = ""; + } + + // Filter status for non standard behaviors + + // If the request is local and we have data: assume a success + // (success with no data won't get notified, that's the best we + // can do given current implementations) + if ( !status && s.isLocal && !s.crossDomain ) { + status = responses.text ? 200 : 404; + // IE - #1450: sometimes returns 1223 when it should be 204 + } else if ( status === 1223 ) { + status = 204; + } + } + } + } catch( firefoxAccessException ) { + if ( !isAbort ) { + complete( -1, firefoxAccessException ); + } + } + + // Call complete if needed + if ( responses ) { + complete( status, statusText, responses, responseHeaders ); + } + }; + + // if we're in sync mode or it's in cache + // and has been retrieved directly (IE6 & IE7) + // we need to manually fire the callback + if ( !s.async || xhr.readyState === 4 ) { + callback(); + } else { + handle = ++xhrId; + if ( xhrOnUnloadAbort ) { + // Create the active xhrs callbacks list if needed + // and attach the unload handler + if ( !xhrCallbacks ) { + xhrCallbacks = {}; + jQuery( window ).unload( xhrOnUnloadAbort ); + } + // Add to list of active xhrs callbacks + xhrCallbacks[ handle ] = callback; + } + xhr.onreadystatechange = callback; + } + }, + + abort: function() { + if ( callback ) { + callback(0,1); + } + } + }; + } + }); +} + + + + +var elemdisplay = {}, + iframe, iframeDoc, + rfxtypes = /^(?:toggle|show|hide)$/, + rfxnum = /^([+\-]=)?([\d+.\-]+)([a-z%]*)$/i, + timerId, + fxAttrs = [ + // height animations + [ "height", "marginTop", "marginBottom", "paddingTop", "paddingBottom" ], + // width animations + [ "width", "marginLeft", "marginRight", "paddingLeft", "paddingRight" ], + // opacity animations + [ "opacity" ] + ], + fxNow; + +jQuery.fn.extend({ + show: function( speed, easing, callback ) { + var elem, display; + + if ( speed || speed === 0 ) { + return this.animate( genFx("show", 3), speed, easing, callback ); + + } else { + for ( var i = 0, j = this.length; i < j; i++ ) { + elem = this[ i ]; + + if ( elem.style ) { + display = elem.style.display; + + // Reset the inline display of this element to learn if it is + // being hidden by cascaded rules or not + if ( !jQuery._data(elem, "olddisplay") && display === "none" ) { + display = elem.style.display = ""; + } + + // Set elements which have been overridden with display: none + // in a stylesheet to whatever the default browser style is + // for such an element + if ( display === "" && jQuery.css(elem, "display") === "none" ) { + jQuery._data( elem, "olddisplay", defaultDisplay(elem.nodeName) ); + } + } + } + + // Set the display of most of the elements in a second loop + // to avoid the constant reflow + for ( i = 0; i < j; i++ ) { + elem = this[ i ]; + + if ( elem.style ) { + display = elem.style.display; + + if ( display === "" || display === "none" ) { + elem.style.display = jQuery._data( elem, "olddisplay" ) || ""; + } + } + } + + return this; + } + }, + + hide: function( speed, easing, callback ) { + if ( speed || speed === 0 ) { + return this.animate( genFx("hide", 3), speed, easing, callback); + + } else { + var elem, display, + i = 0, + j = this.length; + + for ( ; i < j; i++ ) { + elem = this[i]; + if ( elem.style ) { + display = jQuery.css( elem, "display" ); + + if ( display !== "none" && !jQuery._data( elem, "olddisplay" ) ) { + jQuery._data( elem, "olddisplay", display ); + } + } + } + + // Set the display of the elements in a second loop + // to avoid the constant reflow + for ( i = 0; i < j; i++ ) { + if ( this[i].style ) { + this[i].style.display = "none"; + } + } + + return this; + } + }, + + // Save the old toggle function + _toggle: jQuery.fn.toggle, + + toggle: function( fn, fn2, callback ) { + var bool = typeof fn === "boolean"; + + if ( jQuery.isFunction(fn) && jQuery.isFunction(fn2) ) { + this._toggle.apply( this, arguments ); + + } else if ( fn == null || bool ) { + this.each(function() { + var state = bool ? fn : jQuery(this).is(":hidden"); + jQuery(this)[ state ? "show" : "hide" ](); + }); + + } else { + this.animate(genFx("toggle", 3), fn, fn2, callback); + } + + return this; + }, + + fadeTo: function( speed, to, easing, callback ) { + return this.filter(":hidden").css("opacity", 0).show().end() + .animate({opacity: to}, speed, easing, callback); + }, + + animate: function( prop, speed, easing, callback ) { + var optall = jQuery.speed( speed, easing, callback ); + + if ( jQuery.isEmptyObject( prop ) ) { + return this.each( optall.complete, [ false ] ); + } + + // Do not change referenced properties as per-property easing will be lost + prop = jQuery.extend( {}, prop ); + + function doAnimation() { + // XXX 'this' does not always have a nodeName when running the + // test suite + + if ( optall.queue === false ) { + jQuery._mark( this ); + } + + var opt = jQuery.extend( {}, optall ), + isElement = this.nodeType === 1, + hidden = isElement && jQuery(this).is(":hidden"), + name, val, p, e, + parts, start, end, unit, + method; + + // will store per property easing and be used to determine when an animation is complete + opt.animatedProperties = {}; + + for ( p in prop ) { + + // property name normalization + name = jQuery.camelCase( p ); + if ( p !== name ) { + prop[ name ] = prop[ p ]; + delete prop[ p ]; + } + + val = prop[ name ]; + + // easing resolution: per property > opt.specialEasing > opt.easing > 'swing' (default) + if ( jQuery.isArray( val ) ) { + opt.animatedProperties[ name ] = val[ 1 ]; + val = prop[ name ] = val[ 0 ]; + } else { + opt.animatedProperties[ name ] = opt.specialEasing && opt.specialEasing[ name ] || opt.easing || 'swing'; + } + + if ( val === "hide" && hidden || val === "show" && !hidden ) { + return opt.complete.call( this ); + } + + if ( isElement && ( name === "height" || name === "width" ) ) { + // Make sure that nothing sneaks out + // Record all 3 overflow attributes because IE does not + // change the overflow attribute when overflowX and + // overflowY are set to the same value + opt.overflow = [ this.style.overflow, this.style.overflowX, this.style.overflowY ]; + + // Set display property to inline-block for height/width + // animations on inline elements that are having width/height animated + if ( jQuery.css( this, "display" ) === "inline" && + jQuery.css( this, "float" ) === "none" ) { + + // inline-level elements accept inline-block; + // block-level elements need to be inline with layout + if ( !jQuery.support.inlineBlockNeedsLayout || defaultDisplay( this.nodeName ) === "inline" ) { + this.style.display = "inline-block"; + + } else { + this.style.zoom = 1; + } + } + } + } + + if ( opt.overflow != null ) { + this.style.overflow = "hidden"; + } + + for ( p in prop ) { + e = new jQuery.fx( this, opt, p ); + val = prop[ p ]; + + if ( rfxtypes.test( val ) ) { + + // Tracks whether to show or hide based on private + // data attached to the element + method = jQuery._data( this, "toggle" + p ) || ( val === "toggle" ? hidden ? "show" : "hide" : 0 ); + if ( method ) { + jQuery._data( this, "toggle" + p, method === "show" ? "hide" : "show" ); + e[ method ](); + } else { + e[ val ](); + } + + } else { + parts = rfxnum.exec( val ); + start = e.cur(); + + if ( parts ) { + end = parseFloat( parts[2] ); + unit = parts[3] || ( jQuery.cssNumber[ p ] ? "" : "px" ); + + // We need to compute starting value + if ( unit !== "px" ) { + jQuery.style( this, p, (end || 1) + unit); + start = ( (end || 1) / e.cur() ) * start; + jQuery.style( this, p, start + unit); + } + + // If a +=/-= token was provided, we're doing a relative animation + if ( parts[1] ) { + end = ( (parts[ 1 ] === "-=" ? -1 : 1) * end ) + start; + } + + e.custom( start, end, unit ); + + } else { + e.custom( start, val, "" ); + } + } + } + + // For JS strict compliance + return true; + } + + return optall.queue === false ? + this.each( doAnimation ) : + this.queue( optall.queue, doAnimation ); + }, + + stop: function( type, clearQueue, gotoEnd ) { + if ( typeof type !== "string" ) { + gotoEnd = clearQueue; + clearQueue = type; + type = undefined; + } + if ( clearQueue && type !== false ) { + this.queue( type || "fx", [] ); + } + + return this.each(function() { + var index, + hadTimers = false, + timers = jQuery.timers, + data = jQuery._data( this ); + + // clear marker counters if we know they won't be + if ( !gotoEnd ) { + jQuery._unmark( true, this ); + } + + function stopQueue( elem, data, index ) { + var hooks = data[ index ]; + jQuery.removeData( elem, index, true ); + hooks.stop( gotoEnd ); + } + + if ( type == null ) { + for ( index in data ) { + if ( data[ index ] && data[ index ].stop && index.indexOf(".run") === index.length - 4 ) { + stopQueue( this, data, index ); + } + } + } else if ( data[ index = type + ".run" ] && data[ index ].stop ){ + stopQueue( this, data, index ); + } + + for ( index = timers.length; index--; ) { + if ( timers[ index ].elem === this && (type == null || timers[ index ].queue === type) ) { + if ( gotoEnd ) { + + // force the next step to be the last + timers[ index ]( true ); + } else { + timers[ index ].saveState(); + } + hadTimers = true; + timers.splice( index, 1 ); + } + } + + // start the next in the queue if the last step wasn't forced + // timers currently will call their complete callbacks, which will dequeue + // but only if they were gotoEnd + if ( !( gotoEnd && hadTimers ) ) { + jQuery.dequeue( this, type ); + } + }); + } + +}); + +// Animations created synchronously will run synchronously +function createFxNow() { + setTimeout( clearFxNow, 0 ); + return ( fxNow = jQuery.now() ); +} + +function clearFxNow() { + fxNow = undefined; +} + +// Generate parameters to create a standard animation +function genFx( type, num ) { + var obj = {}; + + jQuery.each( fxAttrs.concat.apply([], fxAttrs.slice( 0, num )), function() { + obj[ this ] = type; + }); + + return obj; +} + +// Generate shortcuts for custom animations +jQuery.each({ + slideDown: genFx( "show", 1 ), + slideUp: genFx( "hide", 1 ), + slideToggle: genFx( "toggle", 1 ), + fadeIn: { opacity: "show" }, + fadeOut: { opacity: "hide" }, + fadeToggle: { opacity: "toggle" } +}, function( name, props ) { + jQuery.fn[ name ] = function( speed, easing, callback ) { + return this.animate( props, speed, easing, callback ); + }; +}); + +jQuery.extend({ + speed: function( speed, easing, fn ) { + var opt = speed && typeof speed === "object" ? jQuery.extend( {}, speed ) : { + complete: fn || !fn && easing || + jQuery.isFunction( speed ) && speed, + duration: speed, + easing: fn && easing || easing && !jQuery.isFunction( easing ) && easing + }; + + opt.duration = jQuery.fx.off ? 0 : typeof opt.duration === "number" ? opt.duration : + opt.duration in jQuery.fx.speeds ? jQuery.fx.speeds[ opt.duration ] : jQuery.fx.speeds._default; + + // normalize opt.queue - true/undefined/null -> "fx" + if ( opt.queue == null || opt.queue === true ) { + opt.queue = "fx"; + } + + // Queueing + opt.old = opt.complete; + + opt.complete = function( noUnmark ) { + if ( jQuery.isFunction( opt.old ) ) { + opt.old.call( this ); + } + + if ( opt.queue ) { + jQuery.dequeue( this, opt.queue ); + } else if ( noUnmark !== false ) { + jQuery._unmark( this ); + } + }; + + return opt; + }, + + easing: { + linear: function( p, n, firstNum, diff ) { + return firstNum + diff * p; + }, + swing: function( p, n, firstNum, diff ) { + return ( ( -Math.cos( p*Math.PI ) / 2 ) + 0.5 ) * diff + firstNum; + } + }, + + timers: [], + + fx: function( elem, options, prop ) { + this.options = options; + this.elem = elem; + this.prop = prop; + + options.orig = options.orig || {}; + } + +}); + +jQuery.fx.prototype = { + // Simple function for setting a style value + update: function() { + if ( this.options.step ) { + this.options.step.call( this.elem, this.now, this ); + } + + ( jQuery.fx.step[ this.prop ] || jQuery.fx.step._default )( this ); + }, + + // Get the current size + cur: function() { + if ( this.elem[ this.prop ] != null && (!this.elem.style || this.elem.style[ this.prop ] == null) ) { + return this.elem[ this.prop ]; + } + + var parsed, + r = jQuery.css( this.elem, this.prop ); + // Empty strings, null, undefined and "auto" are converted to 0, + // complex values such as "rotate(1rad)" are returned as is, + // simple values such as "10px" are parsed to Float. + return isNaN( parsed = parseFloat( r ) ) ? !r || r === "auto" ? 0 : r : parsed; + }, + + // Start an animation from one number to another + custom: function( from, to, unit ) { + var self = this, + fx = jQuery.fx; + + this.startTime = fxNow || createFxNow(); + this.end = to; + this.now = this.start = from; + this.pos = this.state = 0; + this.unit = unit || this.unit || ( jQuery.cssNumber[ this.prop ] ? "" : "px" ); + + function t( gotoEnd ) { + return self.step( gotoEnd ); + } + + t.queue = this.options.queue; + t.elem = this.elem; + t.saveState = function() { + if ( self.options.hide && jQuery._data( self.elem, "fxshow" + self.prop ) === undefined ) { + jQuery._data( self.elem, "fxshow" + self.prop, self.start ); + } + }; + + if ( t() && jQuery.timers.push(t) && !timerId ) { + timerId = setInterval( fx.tick, fx.interval ); + } + }, + + // Simple 'show' function + show: function() { + var dataShow = jQuery._data( this.elem, "fxshow" + this.prop ); + + // Remember where we started, so that we can go back to it later + this.options.orig[ this.prop ] = dataShow || jQuery.style( this.elem, this.prop ); + this.options.show = true; + + // Begin the animation + // Make sure that we start at a small width/height to avoid any flash of content + if ( dataShow !== undefined ) { + // This show is picking up where a previous hide or show left off + this.custom( this.cur(), dataShow ); + } else { + this.custom( this.prop === "width" || this.prop === "height" ? 1 : 0, this.cur() ); + } + + // Start by showing the element + jQuery( this.elem ).show(); + }, + + // Simple 'hide' function + hide: function() { + // Remember where we started, so that we can go back to it later + this.options.orig[ this.prop ] = jQuery._data( this.elem, "fxshow" + this.prop ) || jQuery.style( this.elem, this.prop ); + this.options.hide = true; + + // Begin the animation + this.custom( this.cur(), 0 ); + }, + + // Each step of an animation + step: function( gotoEnd ) { + var p, n, complete, + t = fxNow || createFxNow(), + done = true, + elem = this.elem, + options = this.options; + + if ( gotoEnd || t >= options.duration + this.startTime ) { + this.now = this.end; + this.pos = this.state = 1; + this.update(); + + options.animatedProperties[ this.prop ] = true; + + for ( p in options.animatedProperties ) { + if ( options.animatedProperties[ p ] !== true ) { + done = false; + } + } + + if ( done ) { + // Reset the overflow + if ( options.overflow != null && !jQuery.support.shrinkWrapBlocks ) { + + jQuery.each( [ "", "X", "Y" ], function( index, value ) { + elem.style[ "overflow" + value ] = options.overflow[ index ]; + }); + } + + // Hide the element if the "hide" operation was done + if ( options.hide ) { + jQuery( elem ).hide(); + } + + // Reset the properties, if the item has been hidden or shown + if ( options.hide || options.show ) { + for ( p in options.animatedProperties ) { + jQuery.style( elem, p, options.orig[ p ] ); + jQuery.removeData( elem, "fxshow" + p, true ); + // Toggle data is no longer needed + jQuery.removeData( elem, "toggle" + p, true ); + } + } + + // Execute the complete function + // in the event that the complete function throws an exception + // we must ensure it won't be called twice. #5684 + + complete = options.complete; + if ( complete ) { + + options.complete = false; + complete.call( elem ); + } + } + + return false; + + } else { + // classical easing cannot be used with an Infinity duration + if ( options.duration == Infinity ) { + this.now = t; + } else { + n = t - this.startTime; + this.state = n / options.duration; + + // Perform the easing function, defaults to swing + this.pos = jQuery.easing[ options.animatedProperties[this.prop] ]( this.state, n, 0, 1, options.duration ); + this.now = this.start + ( (this.end - this.start) * this.pos ); + } + // Perform the next step of the animation + this.update(); + } + + return true; + } +}; + +jQuery.extend( jQuery.fx, { + tick: function() { + var timer, + timers = jQuery.timers, + i = 0; + + for ( ; i < timers.length; i++ ) { + timer = timers[ i ]; + // Checks the timer has not already been removed + if ( !timer() && timers[ i ] === timer ) { + timers.splice( i--, 1 ); + } + } + + if ( !timers.length ) { + jQuery.fx.stop(); + } + }, + + interval: 13, + + stop: function() { + clearInterval( timerId ); + timerId = null; + }, + + speeds: { + slow: 600, + fast: 200, + // Default speed + _default: 400 + }, + + step: { + opacity: function( fx ) { + jQuery.style( fx.elem, "opacity", fx.now ); + }, + + _default: function( fx ) { + if ( fx.elem.style && fx.elem.style[ fx.prop ] != null ) { + fx.elem.style[ fx.prop ] = fx.now + fx.unit; + } else { + fx.elem[ fx.prop ] = fx.now; + } + } + } +}); + +// Adds width/height step functions +// Do not set anything below 0 +jQuery.each([ "width", "height" ], function( i, prop ) { + jQuery.fx.step[ prop ] = function( fx ) { + jQuery.style( fx.elem, prop, Math.max(0, fx.now) + fx.unit ); + }; +}); + +if ( jQuery.expr && jQuery.expr.filters ) { + jQuery.expr.filters.animated = function( elem ) { + return jQuery.grep(jQuery.timers, function( fn ) { + return elem === fn.elem; + }).length; + }; +} + +// Try to restore the default display value of an element +function defaultDisplay( nodeName ) { + + if ( !elemdisplay[ nodeName ] ) { + + var body = document.body, + elem = jQuery( "<" + nodeName + ">" ).appendTo( body ), + display = elem.css( "display" ); + elem.remove(); + + // If the simple way fails, + // get element's real default display by attaching it to a temp iframe + if ( display === "none" || display === "" ) { + // No iframe to use yet, so create it + if ( !iframe ) { + iframe = document.createElement( "iframe" ); + iframe.frameBorder = iframe.width = iframe.height = 0; + } + + body.appendChild( iframe ); + + // Create a cacheable copy of the iframe document on first call. + // IE and Opera will allow us to reuse the iframeDoc without re-writing the fake HTML + // document to it; WebKit & Firefox won't allow reusing the iframe document. + if ( !iframeDoc || !iframe.createElement ) { + iframeDoc = ( iframe.contentWindow || iframe.contentDocument ).document; + iframeDoc.write( ( document.compatMode === "CSS1Compat" ? "<!doctype html>" : "" ) + "<html><body>" ); + iframeDoc.close(); + } + + elem = iframeDoc.createElement( nodeName ); + + iframeDoc.body.appendChild( elem ); + + display = jQuery.css( elem, "display" ); + body.removeChild( iframe ); + } + + // Store the correct default display + elemdisplay[ nodeName ] = display; + } + + return elemdisplay[ nodeName ]; +} + + + + +var rtable = /^t(?:able|d|h)$/i, + rroot = /^(?:body|html)$/i; + +if ( "getBoundingClientRect" in document.documentElement ) { + jQuery.fn.offset = function( options ) { + var elem = this[0], box; + + if ( options ) { + return this.each(function( i ) { + jQuery.offset.setOffset( this, options, i ); + }); + } + + if ( !elem || !elem.ownerDocument ) { + return null; + } + + if ( elem === elem.ownerDocument.body ) { + return jQuery.offset.bodyOffset( elem ); + } + + try { + box = elem.getBoundingClientRect(); + } catch(e) {} + + var doc = elem.ownerDocument, + docElem = doc.documentElement; + + // Make sure we're not dealing with a disconnected DOM node + if ( !box || !jQuery.contains( docElem, elem ) ) { + return box ? { top: box.top, left: box.left } : { top: 0, left: 0 }; + } + + var body = doc.body, + win = getWindow(doc), + clientTop = docElem.clientTop || body.clientTop || 0, + clientLeft = docElem.clientLeft || body.clientLeft || 0, + scrollTop = win.pageYOffset || jQuery.support.boxModel && docElem.scrollTop || body.scrollTop, + scrollLeft = win.pageXOffset || jQuery.support.boxModel && docElem.scrollLeft || body.scrollLeft, + top = box.top + scrollTop - clientTop, + left = box.left + scrollLeft - clientLeft; + + return { top: top, left: left }; + }; + +} else { + jQuery.fn.offset = function( options ) { + var elem = this[0]; + + if ( options ) { + return this.each(function( i ) { + jQuery.offset.setOffset( this, options, i ); + }); + } + + if ( !elem || !elem.ownerDocument ) { + return null; + } + + if ( elem === elem.ownerDocument.body ) { + return jQuery.offset.bodyOffset( elem ); + } + + var computedStyle, + offsetParent = elem.offsetParent, + prevOffsetParent = elem, + doc = elem.ownerDocument, + docElem = doc.documentElement, + body = doc.body, + defaultView = doc.defaultView, + prevComputedStyle = defaultView ? defaultView.getComputedStyle( elem, null ) : elem.currentStyle, + top = elem.offsetTop, + left = elem.offsetLeft; + + while ( (elem = elem.parentNode) && elem !== body && elem !== docElem ) { + if ( jQuery.support.fixedPosition && prevComputedStyle.position === "fixed" ) { + break; + } + + computedStyle = defaultView ? defaultView.getComputedStyle(elem, null) : elem.currentStyle; + top -= elem.scrollTop; + left -= elem.scrollLeft; + + if ( elem === offsetParent ) { + top += elem.offsetTop; + left += elem.offsetLeft; + + if ( jQuery.support.doesNotAddBorder && !(jQuery.support.doesAddBorderForTableAndCells && rtable.test(elem.nodeName)) ) { + top += parseFloat( computedStyle.borderTopWidth ) || 0; + left += parseFloat( computedStyle.borderLeftWidth ) || 0; + } + + prevOffsetParent = offsetParent; + offsetParent = elem.offsetParent; + } + + if ( jQuery.support.subtractsBorderForOverflowNotVisible && computedStyle.overflow !== "visible" ) { + top += parseFloat( computedStyle.borderTopWidth ) || 0; + left += parseFloat( computedStyle.borderLeftWidth ) || 0; + } + + prevComputedStyle = computedStyle; + } + + if ( prevComputedStyle.position === "relative" || prevComputedStyle.position === "static" ) { + top += body.offsetTop; + left += body.offsetLeft; + } + + if ( jQuery.support.fixedPosition && prevComputedStyle.position === "fixed" ) { + top += Math.max( docElem.scrollTop, body.scrollTop ); + left += Math.max( docElem.scrollLeft, body.scrollLeft ); + } + + return { top: top, left: left }; + }; +} + +jQuery.offset = { + + bodyOffset: function( body ) { + var top = body.offsetTop, + left = body.offsetLeft; + + if ( jQuery.support.doesNotIncludeMarginInBodyOffset ) { + top += parseFloat( jQuery.css(body, "marginTop") ) || 0; + left += parseFloat( jQuery.css(body, "marginLeft") ) || 0; + } + + return { top: top, left: left }; + }, + + setOffset: function( elem, options, i ) { + var position = jQuery.css( elem, "position" ); + + // set position first, in-case top/left are set even on static elem + if ( position === "static" ) { + elem.style.position = "relative"; + } + + var curElem = jQuery( elem ), + curOffset = curElem.offset(), + curCSSTop = jQuery.css( elem, "top" ), + curCSSLeft = jQuery.css( elem, "left" ), + calculatePosition = ( position === "absolute" || position === "fixed" ) && jQuery.inArray("auto", [curCSSTop, curCSSLeft]) > -1, + props = {}, curPosition = {}, curTop, curLeft; + + // need to be able to calculate position if either top or left is auto and position is either absolute or fixed + if ( calculatePosition ) { + curPosition = curElem.position(); + curTop = curPosition.top; + curLeft = curPosition.left; + } else { + curTop = parseFloat( curCSSTop ) || 0; + curLeft = parseFloat( curCSSLeft ) || 0; + } + + if ( jQuery.isFunction( options ) ) { + options = options.call( elem, i, curOffset ); + } + + if ( options.top != null ) { + props.top = ( options.top - curOffset.top ) + curTop; + } + if ( options.left != null ) { + props.left = ( options.left - curOffset.left ) + curLeft; + } + + if ( "using" in options ) { + options.using.call( elem, props ); + } else { + curElem.css( props ); + } + } +}; + + +jQuery.fn.extend({ + + position: function() { + if ( !this[0] ) { + return null; + } + + var elem = this[0], + + // Get *real* offsetParent + offsetParent = this.offsetParent(), + + // Get correct offsets + offset = this.offset(), + parentOffset = rroot.test(offsetParent[0].nodeName) ? { top: 0, left: 0 } : offsetParent.offset(); + + // Subtract element margins + // note: when an element has margin: auto the offsetLeft and marginLeft + // are the same in Safari causing offset.left to incorrectly be 0 + offset.top -= parseFloat( jQuery.css(elem, "marginTop") ) || 0; + offset.left -= parseFloat( jQuery.css(elem, "marginLeft") ) || 0; + + // Add offsetParent borders + parentOffset.top += parseFloat( jQuery.css(offsetParent[0], "borderTopWidth") ) || 0; + parentOffset.left += parseFloat( jQuery.css(offsetParent[0], "borderLeftWidth") ) || 0; + + // Subtract the two offsets + return { + top: offset.top - parentOffset.top, + left: offset.left - parentOffset.left + }; + }, + + offsetParent: function() { + return this.map(function() { + var offsetParent = this.offsetParent || document.body; + while ( offsetParent && (!rroot.test(offsetParent.nodeName) && jQuery.css(offsetParent, "position") === "static") ) { + offsetParent = offsetParent.offsetParent; + } + return offsetParent; + }); + } +}); + + +// Create scrollLeft and scrollTop methods +jQuery.each( ["Left", "Top"], function( i, name ) { + var method = "scroll" + name; + + jQuery.fn[ method ] = function( val ) { + var elem, win; + + if ( val === undefined ) { + elem = this[ 0 ]; + + if ( !elem ) { + return null; + } + + win = getWindow( elem ); + + // Return the scroll offset + return win ? ("pageXOffset" in win) ? win[ i ? "pageYOffset" : "pageXOffset" ] : + jQuery.support.boxModel && win.document.documentElement[ method ] || + win.document.body[ method ] : + elem[ method ]; + } + + // Set the scroll offset + return this.each(function() { + win = getWindow( this ); + + if ( win ) { + win.scrollTo( + !i ? val : jQuery( win ).scrollLeft(), + i ? val : jQuery( win ).scrollTop() + ); + + } else { + this[ method ] = val; + } + }); + }; +}); + +function getWindow( elem ) { + return jQuery.isWindow( elem ) ? + elem : + elem.nodeType === 9 ? + elem.defaultView || elem.parentWindow : + false; +} + + + + +// Create width, height, innerHeight, innerWidth, outerHeight and outerWidth methods +jQuery.each([ "Height", "Width" ], function( i, name ) { + + var type = name.toLowerCase(); + + // innerHeight and innerWidth + jQuery.fn[ "inner" + name ] = function() { + var elem = this[0]; + return elem ? + elem.style ? + parseFloat( jQuery.css( elem, type, "padding" ) ) : + this[ type ]() : + null; + }; + + // outerHeight and outerWidth + jQuery.fn[ "outer" + name ] = function( margin ) { + var elem = this[0]; + return elem ? + elem.style ? + parseFloat( jQuery.css( elem, type, margin ? "margin" : "border" ) ) : + this[ type ]() : + null; + }; + + jQuery.fn[ type ] = function( size ) { + // Get window width or height + var elem = this[0]; + if ( !elem ) { + return size == null ? null : this; + } + + if ( jQuery.isFunction( size ) ) { + return this.each(function( i ) { + var self = jQuery( this ); + self[ type ]( size.call( this, i, self[ type ]() ) ); + }); + } + + if ( jQuery.isWindow( elem ) ) { + // Everyone else use document.documentElement or document.body depending on Quirks vs Standards mode + // 3rd condition allows Nokia support, as it supports the docElem prop but not CSS1Compat + var docElemProp = elem.document.documentElement[ "client" + name ], + body = elem.document.body; + return elem.document.compatMode === "CSS1Compat" && docElemProp || + body && body[ "client" + name ] || docElemProp; + + // Get document width or height + } else if ( elem.nodeType === 9 ) { + // Either scroll[Width/Height] or offset[Width/Height], whichever is greater + return Math.max( + elem.documentElement["client" + name], + elem.body["scroll" + name], elem.documentElement["scroll" + name], + elem.body["offset" + name], elem.documentElement["offset" + name] + ); + + // Get or set width or height on the element + } else if ( size === undefined ) { + var orig = jQuery.css( elem, type ), + ret = parseFloat( orig ); + + return jQuery.isNumeric( ret ) ? ret : orig; + + // Set the width or height on the element (default to pixels if value is unitless) + } else { + return this.css( type, typeof size === "string" ? size : size + "px" ); + } + }; + +}); + + + + +// Expose jQuery to the global object +window.jQuery = window.$ = jQuery; + +// Expose jQuery as an AMD module, but only for AMD loaders that +// understand the issues with loading multiple versions of jQuery +// in a page that all might call define(). The loader will indicate +// they have special allowances for multiple jQuery versions by +// specifying define.amd.jQuery = true. Register as a named module, +// since jQuery can be concatenated with other files that may use define, +// but not use a proper concatenation script that understands anonymous +// AMD modules. A named AMD is safest and most robust way to register. +// Lowercase jquery is used because AMD module names are derived from +// file names, and jQuery is normally delivered in a lowercase file name. +// Do this after creating the global so that if an AMD module wants to call +// noConflict to hide this version of jQuery, it will work. +if ( typeof define === "function" && define.amd && define.amd.jQuery ) { + define( "jquery", [], function () { return jQuery; } ); +} + + + +})( window ); diff --git a/src/static/js/json2.js b/src/static/js/json2.js new file mode 100644 index 00000000..663f932c --- /dev/null +++ b/src/static/js/json2.js @@ -0,0 +1,473 @@ +/* + http://www.JSON.org/json2.js + 2011-02-23 + + Public Domain. + + NO WARRANTY EXPRESSED OR IMPLIED. USE AT YOUR OWN RISK. + + See http://www.JSON.org/js.html + + + This code should be minified before deployment. + See http://javascript.crockford.com/jsmin.html + + USE YOUR OWN COPY. IT IS EXTREMELY UNWISE TO LOAD CODE FROM SERVERS YOU DO + NOT CONTROL. + + + This file creates a global JSON object containing two methods: stringify + and parse. + + JSON.stringify(value, replacer, space) + value any JavaScript value, usually an object or array. + + replacer an optional parameter that determines how object + values are stringified for objects. It can be a + function or an array of strings. + + space an optional parameter that specifies the indentation + of nested structures. If it is omitted, the text will + be packed without extra whitespace. If it is a number, + it will specify the number of spaces to indent at each + level. If it is a string (such as '\t' or ' '), + it contains the characters used to indent at each level. + + This method produces a JSON text from a JavaScript value. + + When an object value is found, if the object contains a toJSON + method, its toJSON method will be called and the result will be + stringified. A toJSON method does not serialize: it returns the + value represented by the name/value pair that should be serialized, + or undefined if nothing should be serialized. The toJSON method + will be passed the key associated with the value, and this will be + bound to the value + + For example, this would serialize Dates as ISO strings. + + Date.prototype.toJSON = function (key) { + function f(n) { + // Format integers to have at least two digits. + return n < 10 ? '0' + n : n; + } + + return this.getUTCFullYear() + '-' + + f(this.getUTCMonth() + 1) + '-' + + f(this.getUTCDate()) + 'T' + + f(this.getUTCHours()) + ':' + + f(this.getUTCMinutes()) + ':' + + f(this.getUTCSeconds()) + 'Z'; + }; + + You can provide an optional replacer method. It will be passed the + key and value of each member, with this bound to the containing + object. The value that is returned from your method will be + serialized. If your method returns undefined, then the member will + be excluded from the serialization. + + If the replacer parameter is an array of strings, then it will be + used to select the members to be serialized. It filters the results + such that only members with keys listed in the replacer array are + stringified. + + Values that do not have JSON representations, such as undefined or + functions, will not be serialized. Such values in objects will be + dropped; in arrays they will be replaced with null. You can use + a replacer function to replace those with JSON values. + JSON.stringify(undefined) returns undefined. + + The optional space parameter produces a stringification of the + value that is filled with line breaks and indentation to make it + easier to read. + + If the space parameter is a non-empty string, then that string will + be used for indentation. If the space parameter is a number, then + the indentation will be that many spaces. + + Example: + + text = JSON.stringify(['e', {pluribus: 'unum'}]); + // text is '["e",{"pluribus":"unum"}]' + + + text = JSON.stringify(['e', {pluribus: 'unum'}], null, '\t'); + // text is '[\n\t"e",\n\t{\n\t\t"pluribus": "unum"\n\t}\n]' + + text = JSON.stringify([new Date()], function (key, value) { + return this[key] instanceof Date ? + 'Date(' + this[key] + ')' : value; + }); + // text is '["Date(---current time---)"]' + + + JSON.parse(text, reviver) + This method parses a JSON text to produce an object or array. + It can throw a SyntaxError exception. + + The optional reviver parameter is a function that can filter and + transform the results. It receives each of the keys and values, + and its return value is used instead of the original value. + If it returns what it received, then the structure is not modified. + If it returns undefined then the member is deleted. + + Example: + + // Parse the text. Values that look like ISO date strings will + // be converted to Date objects. + + myData = JSON.parse(text, function (key, value) { + var a; + if (typeof value === 'string') { + a = +/^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2}(?:\.\d*)?)Z$/.exec(value); + if (a) { + return new Date(Date.UTC(+a[1], +a[2] - 1, +a[3], +a[4], + +a[5], +a[6])); + } + } + return value; + }); + + myData = JSON.parse('["Date(09/09/2001)"]', function (key, value) { + var d; + if (typeof value === 'string' && + value.slice(0, 5) === 'Date(' && + value.slice(-1) === ')') { + d = new Date(value.slice(5, -1)); + if (d) { + return d; + } + } + return value; + }); + + + This is a reference implementation. You are free to copy, modify, or + redistribute. +*/ + +/*jslint evil: true, strict: false, regexp: false */ + +/*members "", "\b", "\t", "\n", "\f", "\r", "\"", JSON, "\\", apply, + call, charCodeAt, getUTCDate, getUTCFullYear, getUTCHours, + getUTCMinutes, getUTCMonth, getUTCSeconds, hasOwnProperty, join, + lastIndex, length, parse, prototype, push, replace, slice, stringify, + test, toJSON, toString, valueOf +*/ + + +// Create a JSON object only if one does not already exist. We create the +// methods in a closure to avoid creating global variables. +var JSON; +if (!JSON) +{ + JSON = {}; +} + +(function() +{ + "use strict"; + + function f(n) + { + // Format integers to have at least two digits. + return n < 10 ? '0' + n : n; + } + + if (typeof Date.prototype.toJSON !== 'function') + { + + Date.prototype.toJSON = function(key) + { + + return isFinite(this.valueOf()) ? this.getUTCFullYear() + '-' + f(this.getUTCMonth() + 1) + '-' + f(this.getUTCDate()) + 'T' + f(this.getUTCHours()) + ':' + f(this.getUTCMinutes()) + ':' + f(this.getUTCSeconds()) + 'Z' : null; + }; + + String.prototype.toJSON = Number.prototype.toJSON = Boolean.prototype.toJSON = function(key) + { + return this.valueOf(); + }; + } + + var cx = /[\u0000\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g, + escapable = /[\\\"\x00-\x1f\x7f-\x9f\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g, + gap, indent, meta = { // table of character substitutions + '\b': '\\b', + '\t': '\\t', + '\n': '\\n', + '\f': '\\f', + '\r': '\\r', + '"': '\\"', + '\\': '\\\\' + }, + rep; + + + function quote(string) + { + + // If the string contains no control characters, no quote characters, and no + // backslash characters, then we can safely slap some quotes around it. + // Otherwise we must also replace the offending characters with safe escape + // sequences. + escapable.lastIndex = 0; + return escapable.test(string) ? '"' + string.replace(escapable, function(a) + { + var c = meta[a]; + return typeof c === 'string' ? c : '\\u' + ('0000' + a.charCodeAt(0).toString(16)).slice(-4); + }) + '"' : '"' + string + '"'; + } + + + function str(key, holder) + { + + // Produce a string from holder[key]. + var i, // The loop counter. + k, // The member key. + v, // The member value. + length, mind = gap, + partial, value = holder[key]; + + // If the value has a toJSON method, call it to obtain a replacement value. + if (value && typeof value === 'object' && typeof value.toJSON === 'function') + { + value = value.toJSON(key); + } + + // If we were called with a replacer function, then call the replacer to + // obtain a replacement value. + if (typeof rep === 'function') + { + value = rep.call(holder, key, value); + } + + // What happens next depends on the value's type. + switch (typeof value) + { + case 'string': + return quote(value); + + case 'number': + + // JSON numbers must be finite. Encode non-finite numbers as null. + return isFinite(value) ? String(value) : 'null'; + + case 'boolean': + case 'null': + + // If the value is a boolean or null, convert it to a string. Note: + // typeof null does not produce 'null'. The case is included here in + // the remote chance that this gets fixed someday. + return String(value); + + // If the type is 'object', we might be dealing with an object or an array or + // null. + case 'object': + + // Due to a specification blunder in ECMAScript, typeof null is 'object', + // so watch out for that case. + if (!value) + { + return 'null'; + } + + // Make an array to hold the partial results of stringifying this object value. + gap += indent; + partial = []; + + // Is the value an array? + if (Object.prototype.toString.apply(value) === '[object Array]') + { + + // The value is an array. Stringify every element. Use null as a placeholder + // for non-JSON values. + length = value.length; + for (i = 0; i < length; i += 1) + { + partial[i] = str(i, value) || 'null'; + } + + // Join all of the elements together, separated with commas, and wrap them in + // brackets. + v = partial.length === 0 ? '[]' : gap ? '[\n' + gap + partial.join(',\n' + gap) + '\n' + mind + ']' : '[' + partial.join(',') + ']'; + gap = mind; + return v; + } + + // If the replacer is an array, use it to select the members to be stringified. + if (rep && typeof rep === 'object') + { + length = rep.length; + for (i = 0; i < length; i += 1) + { + if (typeof rep[i] === 'string') + { + k = rep[i]; + v = str(k, value); + if (v) + { + partial.push(quote(k) + (gap ? ': ' : ':') + v); + } + } + } + } + else + { + + // Otherwise, iterate through all of the keys in the object. + for (k in value) + { + if (Object.prototype.hasOwnProperty.call(value, k)) + { + v = str(k, value); + if (v) + { + partial.push(quote(k) + (gap ? ': ' : ':') + v); + } + } + } + } + + // Join all of the member texts together, separated with commas, + // and wrap them in braces. + v = partial.length === 0 ? '{}' : gap ? '{\n' + gap + partial.join(',\n' + gap) + '\n' + mind + '}' : '{' + partial.join(',') + '}'; + gap = mind; + return v; + } + } + + // If the JSON object does not yet have a stringify method, give it one. + if (typeof JSON.stringify !== 'function') + { + JSON.stringify = function(value, replacer, space) + { + + // The stringify method takes a value and an optional replacer, and an optional + // space parameter, and returns a JSON text. The replacer can be a function + // that can replace values, or an array of strings that will select the keys. + // A default replacer method can be provided. Use of the space parameter can + // produce text that is more easily readable. + var i; + gap = ''; + indent = ''; + + // If the space parameter is a number, make an indent string containing that + // many spaces. + if (typeof space === 'number') + { + for (i = 0; i < space; i += 1) + { + indent += ' '; + } + + // If the space parameter is a string, it will be used as the indent string. + } + else if (typeof space === 'string') + { + indent = space; + } + + // If there is a replacer, it must be a function or an array. + // Otherwise, throw an error. + rep = replacer; + if (replacer && typeof replacer !== 'function' && (typeof replacer !== 'object' || typeof replacer.length !== 'number')) + { + throw new Error('JSON.stringify'); + } + + // Make a fake root object containing our value under the key of ''. + // Return the result of stringifying the value. + return str('', { + '': value + }); + }; + } + + + // If the JSON object does not yet have a parse method, give it one. + if (typeof JSON.parse !== 'function') + { + JSON.parse = function(text, reviver) + { + + // The parse method takes a text and an optional reviver function, and returns + // a JavaScript value if the text is a valid JSON text. + var j; + + function walk(holder, key) + { + + // The walk method is used to recursively walk the resulting structure so + // that modifications can be made. + var k, v, value = holder[key]; + if (value && typeof value === 'object') + { + for (k in value) + { + if (Object.prototype.hasOwnProperty.call(value, k)) + { + v = walk(value, k); + if (v !== undefined) + { + value[k] = v; + } + else + { + delete value[k]; + } + } + } + } + return reviver.call(holder, key, value); + } + + + // Parsing happens in four stages. In the first stage, we replace certain + // Unicode characters with escape sequences. JavaScript handles many characters + // incorrectly, either silently deleting them, or treating them as line endings. + text = String(text); + cx.lastIndex = 0; + if (cx.test(text)) + { + text = text.replace(cx, function(a) + { + return '\\u' + ('0000' + a.charCodeAt(0).toString(16)).slice(-4); + }); + } + + // In the second stage, we run the text against regular expressions that look + // for non-JSON patterns. We are especially concerned with '()' and 'new' + // because they can cause invocation, and '=' because it can cause mutation. + // But just to be safe, we want to reject all unexpected forms. + // We split the second stage into 4 regexp operations in order to work around + // crippling inefficiencies in IE's and Safari's regexp engines. First we + // replace the JSON backslash pairs with '@' (a non-JSON character). Second, we + // replace all simple value tokens with ']' characters. Third, we delete all + // open brackets that follow a colon or comma or that begin the text. Finally, + // we look to see that the remaining characters are only whitespace or ']' or + // ',' or ':' or '{' or '}'. If that is so, then the text is safe for eval. + if (/^[\],:{}\s]*$/.test(text.replace(/\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})/g, '@').replace(/"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g, ']').replace(/(?:^|:|,)(?:\s*\[)+/g, ''))) + { + + // In the third stage we use the eval function to compile the text into a + // JavaScript structure. The '{' operator is subject to a syntactic ambiguity + // in JavaScript: it can begin a block or an object literal. We wrap the text + // in parens to eliminate the ambiguity. + j = eval('(' + text + ')'); + + // In the optional fourth stage, we recursively walk the new structure, passing + // each name/value pair to a reviver function for possible transformation. + return typeof reviver === 'function' ? walk( + { + '': j + }, '') : j; + } + + // If the text is not JSON parseable, then a SyntaxError is thrown. + throw new SyntaxError('JSON.parse'); + }; + } +}()); + +module.exports = JSON; diff --git a/src/static/js/linestylefilter.js b/src/static/js/linestylefilter.js new file mode 100644 index 00000000..04b8fb72 --- /dev/null +++ b/src/static/js/linestylefilter.js @@ -0,0 +1,341 @@ +/** + * This code is mostly from the old Etherpad. Please help us to comment this code. + * This helps other people to understand this code better and helps them to improve it. + * TL;DR COMMENTS ON THIS FILE ARE HIGHLY APPRECIATED + */ + +// THIS FILE IS ALSO AN APPJET MODULE: etherpad.collab.ace.linestylefilter +// %APPJET%: import("etherpad.collab.ace.easysync2.Changeset"); +// %APPJET%: import("etherpad.admin.plugins"); +/** + * 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. + */ + +// requires: easysync2.Changeset +// requires: top +// requires: plugins +// requires: undefined + +var Changeset = require('./Changeset'); +var hooks = require('./pluginfw/hooks'); +var map = require('./ace2_common').map; + +var linestylefilter = {}; + +linestylefilter.ATTRIB_CLASSES = { + 'bold': 'tag:b', + 'italic': 'tag:i', + 'underline': 'tag:u', + 'strikethrough': 'tag:s' +}; + +linestylefilter.getAuthorClassName = function(author) +{ + return "author-" + author.replace(/[^a-y0-9]/g, function(c) + { + if (c == ".") return "-"; + return 'z' + c.charCodeAt(0) + 'z'; + }); +}; + +// lineLength is without newline; aline includes newline, +// but may be falsy if lineLength == 0 +linestylefilter.getLineStyleFilter = function(lineLength, aline, textAndClassFunc, apool) +{ + + if (lineLength == 0) return textAndClassFunc; + + var nextAfterAuthorColors = textAndClassFunc; + + var authorColorFunc = (function() + { + var lineEnd = lineLength; + var curIndex = 0; + var extraClasses; + var leftInAuthor; + + function attribsToClasses(attribs) + { + var classes = ''; + Changeset.eachAttribNumber(attribs, function(n) + { + var key = apool.getAttribKey(n); + if (key) + { + var value = apool.getAttribValue(n); + if (value) + { + if (key == 'author') + { + classes += ' ' + linestylefilter.getAuthorClassName(value); + } + else if (key == 'list') + { + classes += ' list:' + value; + } + else if (key == 'start') + { + classes += ' start:' + value; + } + else if (linestylefilter.ATTRIB_CLASSES[key]) + { + classes += ' ' + linestylefilter.ATTRIB_CLASSES[key]; + } + else + { + classes += hooks.callAllStr("aceAttribsToClasses", { + linestylefilter: linestylefilter, + key: key, + value: value + }, " ", " ", ""); + } + } + } + }); + return classes.substring(1); + } + + var attributionIter = Changeset.opIterator(aline); + var nextOp, nextOpClasses; + + function goNextOp() + { + nextOp = attributionIter.next(); + nextOpClasses = (nextOp.opcode && attribsToClasses(nextOp.attribs)); + } + goNextOp(); + + function nextClasses() + { + if (curIndex < lineEnd) + { + extraClasses = nextOpClasses; + leftInAuthor = nextOp.chars; + goNextOp(); + while (nextOp.opcode && nextOpClasses == extraClasses) + { + leftInAuthor += nextOp.chars; + goNextOp(); + } + } + } + nextClasses(); + + return function(txt, cls) + { + while (txt.length > 0) + { + if (leftInAuthor <= 0) + { + // prevent infinite loop if something funny's going on + return nextAfterAuthorColors(txt, cls); + } + var spanSize = txt.length; + if (spanSize > leftInAuthor) + { + spanSize = leftInAuthor; + } + var curTxt = txt.substring(0, spanSize); + txt = txt.substring(spanSize); + nextAfterAuthorColors(curTxt, (cls && cls + " ") + extraClasses); + curIndex += spanSize; + leftInAuthor -= spanSize; + if (leftInAuthor == 0) + { + nextClasses(); + } + } + }; + })(); + return authorColorFunc; +}; + +linestylefilter.getAtSignSplitterFilter = function(lineText, textAndClassFunc) +{ + var at = /@/g; + at.lastIndex = 0; + var splitPoints = null; + var execResult; + while ((execResult = at.exec(lineText))) + { + if (!splitPoints) + { + splitPoints = []; + } + splitPoints.push(execResult.index); + } + + if (!splitPoints) return textAndClassFunc; + + return linestylefilter.textAndClassFuncSplitter(textAndClassFunc, splitPoints); +}; + +linestylefilter.getRegexpFilter = function(regExp, tag) +{ + return function(lineText, textAndClassFunc) + { + regExp.lastIndex = 0; + var regExpMatchs = null; + var splitPoints = null; + var execResult; + while ((execResult = regExp.exec(lineText))) + { + if (!regExpMatchs) + { + regExpMatchs = []; + splitPoints = []; + } + var startIndex = execResult.index; + var regExpMatch = execResult[0]; + regExpMatchs.push([startIndex, regExpMatch]); + splitPoints.push(startIndex, startIndex + regExpMatch.length); + } + + if (!regExpMatchs) return textAndClassFunc; + + function regExpMatchForIndex(idx) + { + for (var k = 0; k < regExpMatchs.length; k++) + { + var u = regExpMatchs[k]; + if (idx >= u[0] && idx < u[0] + u[1].length) + { + return u[1]; + } + } + return false; + } + + var handleRegExpMatchsAfterSplit = (function() + { + var curIndex = 0; + return function(txt, cls) + { + var txtlen = txt.length; + var newCls = cls; + var regExpMatch = regExpMatchForIndex(curIndex); + if (regExpMatch) + { + newCls += " " + tag + ":" + regExpMatch; + } + textAndClassFunc(txt, newCls); + curIndex += txtlen; + }; + })(); + + return linestylefilter.textAndClassFuncSplitter(handleRegExpMatchsAfterSplit, splitPoints); + }; +}; + + +linestylefilter.REGEX_WORDCHAR = /[\u0030-\u0039\u0041-\u005A\u0061-\u007A\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u00FF\u0100-\u1FFF\u3040-\u9FFF\uF900-\uFDFF\uFE70-\uFEFE\uFF10-\uFF19\uFF21-\uFF3A\uFF41-\uFF5A\uFF66-\uFFDC]/; +linestylefilter.REGEX_URLCHAR = new RegExp('(' + /[-:@a-zA-Z0-9_.,~%+\/\\?=&#;()$]/.source + '|' + linestylefilter.REGEX_WORDCHAR.source + ')'); +linestylefilter.REGEX_URL = new RegExp(/(?:(?:https?|s?ftp|ftps|file|smb|afp|nfs|(x-)?man|gopher|txmt):\/\/|mailto:|www\.)/.source + linestylefilter.REGEX_URLCHAR.source + '*(?![:.,;])' + linestylefilter.REGEX_URLCHAR.source, 'g'); +linestylefilter.getURLFilter = linestylefilter.getRegexpFilter( +linestylefilter.REGEX_URL, 'url'); + +linestylefilter.textAndClassFuncSplitter = function(func, splitPointsOpt) +{ + var nextPointIndex = 0; + var idx = 0; + + // don't split at 0 + while (splitPointsOpt && nextPointIndex < splitPointsOpt.length && splitPointsOpt[nextPointIndex] == 0) + { + nextPointIndex++; + } + + function spanHandler(txt, cls) + { + if ((!splitPointsOpt) || nextPointIndex >= splitPointsOpt.length) + { + func(txt, cls); + idx += txt.length; + } + else + { + var splitPoints = splitPointsOpt; + var pointLocInSpan = splitPoints[nextPointIndex] - idx; + var txtlen = txt.length; + if (pointLocInSpan >= txtlen) + { + func(txt, cls); + idx += txt.length; + if (pointLocInSpan == txtlen) + { + nextPointIndex++; + } + } + else + { + if (pointLocInSpan > 0) + { + func(txt.substring(0, pointLocInSpan), cls); + idx += pointLocInSpan; + } + nextPointIndex++; + // recurse + spanHandler(txt.substring(pointLocInSpan), cls); + } + } + } + return spanHandler; +}; + +linestylefilter.getFilterStack = function(lineText, textAndClassFunc, browser) +{ + var func = linestylefilter.getURLFilter(lineText, textAndClassFunc); + + var hookFilters = hooks.callAll("aceGetFilterStack", { + linestylefilter: linestylefilter, + browser: browser + }); + map(hookFilters, function(hookFilter) + { + func = hookFilter(lineText, func); + }); + + if (browser !== undefined && browser.msie) + { + // IE7+ will take an e-mail address like <foo@bar.com> and linkify it to foo@bar.com. + // We then normalize it back to text with no angle brackets. It's weird. So always + // break spans at an "at" sign. + func = linestylefilter.getAtSignSplitterFilter( + lineText, func); + } + return func; +}; + +// domLineObj is like that returned by domline.createDomLine +linestylefilter.populateDomLine = function(textLine, aline, apool, domLineObj) +{ + // remove final newline from text if any + var text = textLine; + if (text.slice(-1) == '\n') + { + text = text.substring(0, text.length - 1); + } + + function textAndClassFunc(tokenText, tokenClass) + { + domLineObj.appendSpan(tokenText, tokenClass); + } + + var func = linestylefilter.getFilterStack(text, textAndClassFunc); + func = linestylefilter.getLineStyleFilter(text.length, aline, func, apool); + func(text, ''); +}; + +exports.linestylefilter = linestylefilter; diff --git a/src/static/js/pad.js b/src/static/js/pad.js new file mode 100644 index 00000000..d19cface --- /dev/null +++ b/src/static/js/pad.js @@ -0,0 +1,1000 @@ +/** + * This code is mostly from the old Etherpad. Please help us to comment this code. + * This helps other people to understand this code better and helps them to improve it. + * TL;DR COMMENTS ON THIS FILE ARE HIGHLY APPRECIATED + */ + +/** + * 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. + */ + +/* global $, window */ + +var socket; + +// These jQuery things should create local references, but for now `require()` +// assigns to the global `$` and augments it with plugins. +require('./jquery'); +require('./farbtastic'); +require('./excanvas'); +JSON = require('./json2'); +require('./undo-xpopup'); +require('./prefixfree'); + +var chat = require('./chat').chat; +var getCollabClient = require('./collab_client').getCollabClient; +var padconnectionstatus = require('./pad_connectionstatus').padconnectionstatus; +var padcookie = require('./pad_cookie').padcookie; +var paddocbar = require('./pad_docbar').paddocbar; +var padeditbar = require('./pad_editbar').padeditbar; +var padeditor = require('./pad_editor').padeditor; +var padimpexp = require('./pad_impexp').padimpexp; +var padmodals = require('./pad_modals').padmodals; +var padsavedrevs = require('./pad_savedrevs').padsavedrevs; +var paduserlist = require('./pad_userlist').paduserlist; +var padutils = require('./pad_utils').padutils; + +var createCookie = require('./pad_utils').createCookie; +var readCookie = require('./pad_utils').readCookie; +var randomString = require('./pad_utils').randomString; + +function getParams() +{ + var params = getUrlVars() + var showControls = params["showControls"]; + var showChat = params["showChat"]; + var userName = params["userName"]; + var showLineNumbers = params["showLineNumbers"]; + var useMonospaceFont = params["useMonospaceFont"]; + var IsnoColors = params["noColors"]; + var hideQRCode = params["hideQRCode"]; + var rtl = params["rtl"]; + var alwaysShowChat = params["alwaysShowChat"]; + + if(IsnoColors) + { + if(IsnoColors == "true") + { + settings.noColors = true; + $('#clearAuthorship').hide(); + } + } + if(showControls) + { + if(showControls == "false") + { + $('#editbar').hide(); + $('#editorcontainer').css({"top":"0px"}); + } + } + if(showChat) + { + if(showChat == "false") + { + $('#chaticon').hide(); + } + } + if(showLineNumbers) + { + if(showLineNumbers == "false") + { + settings.LineNumbersDisabled = true; + } + } + if(useMonospaceFont) + { + if(useMonospaceFont == "true") + { + settings.useMonospaceFontGlobal = true; + } + } + if(userName) + { + // If the username is set as a parameter we should set a global value that we can call once we have initiated the pad. + settings.globalUserName = decodeURIComponent(userName); + } + if(hideQRCode) + { + $('#qrcode').hide(); + } + if(rtl) + { + if(rtl == "true") + { + settings.rtlIsTrue = true + } + } + if(alwaysShowChat) + { + if(alwaysShowChat == "true") + { + chat.stickToScreen(); + } + } +} + +function getUrlVars() +{ + var vars = [], hash; + var hashes = window.location.href.slice(window.location.href.indexOf('?') + 1).split('&'); + for(var i = 0; i < hashes.length; i++) + { + hash = hashes[i].split('='); + vars.push(hash[0]); + vars[hash[0]] = hash[1]; + } + return vars; +} + +function savePassword() +{ + //set the password cookie + createCookie("password",$("#passwordinput").val(),null,document.location.pathname); + //reload + document.location=document.location; +} + +function ieTestXMLHTTP(){ + // Test for IE known XML HTTP issue + if ($.browser.msie && !window.XMLHttpRequest){ + $("#editorloadingbox").html("You do not have XML HTTP enabled in your browser. <a target='_blank' href='https://github.com/Pita/etherpad-lite/wiki/How-to-enable-native-XMLHTTP-support-in-IE'>Fix this issue</a>"); + } +} +function handshake() +{ + var loc = document.location; + //get the correct port + var port = loc.port == "" ? (loc.protocol == "https:" ? 443 : 80) : loc.port; + //create the url + var url = loc.protocol + "//" + loc.hostname + ":" + port + "/"; + //find out in which subfolder we are + var resource = loc.pathname.substr(1, loc.pathname.indexOf("/p/")) + "socket.io"; + //connect + socket = pad.socket = io.connect(url, { + resource: resource, + 'max reconnection attempts': 3, + 'sync disconnect on unload' : false + }); + + function sendClientReady(isReconnect) + { + var padId = document.location.pathname.substring(document.location.pathname.lastIndexOf("/") + 1); + padId = decodeURIComponent(padId); // unescape neccesary due to Safari and Opera interpretation of spaces + + if(!isReconnect) + document.title = padId.replace(/_+/g, ' ') + " | " + document.title; + + var token = readCookie("token"); + if (token == null) + { + token = "t." + randomString(); + createCookie("token", token, 60); + } + + var sessionID = readCookie("sessionID"); + var password = readCookie("password"); + + var msg = { + "component": "pad", + "type": "CLIENT_READY", + "padId": padId, + "sessionID": sessionID, + "password": password, + "token": token, + "protocolVersion": 2 + }; + + //this is a reconnect, lets tell the server our revisionnumber + if(isReconnect == true) + { + msg.client_rev=pad.collabClient.getCurrentRevisionNumber(); + msg.reconnect=true; + } + + socket.json.send(msg); + }; + + var disconnectTimeout; + + socket.once('connect', function () { + sendClientReady(false); + }); + + socket.on('reconnect', function () { + //reconnect is before the timeout, lets stop the timeout + if(disconnectTimeout) + { + clearTimeout(disconnectTimeout); + } + + pad.collabClient.setChannelState("CONNECTED"); + sendClientReady(true); + }); + + socket.on('disconnect', function (reason) { + if(reason == "booted"){ + pad.collabClient.setChannelState("DISCONNECTED"); + } else { + function disconnectEvent() + { + pad.collabClient.setChannelState("DISCONNECTED", "reconnect_timeout"); + } + + pad.collabClient.setChannelState("RECONNECTING"); + + disconnectTimeout = setTimeout(disconnectEvent, 10000); + } + }); + + var receivedClientVars = false; + var initalized = false; + + socket.on('message', function(obj) + { + //the access was not granted, give the user a message + if(!receivedClientVars && obj.accessStatus) + { + if(obj.accessStatus == "deny") + { + $("#editorloadingbox").html("<b>You do not have permission to access this pad</b>"); + } + else if(obj.accessStatus == "needPassword") + { + $("#editorloadingbox").html("<b>You need a password to access this pad</b><br>" + + "<input id='passwordinput' type='password' name='password'>"+ + "<button type='button' onclick=\"" + padutils.escapeHtml('require('+JSON.stringify(module.id)+").savePassword()") + "\">ok</button>"); + } + else if(obj.accessStatus == "wrongPassword") + { + $("#editorloadingbox").html("<b>You're password was wrong</b><br>" + + "<input id='passwordinput' type='password' name='password'>"+ + "<button type='button' onclick=\"" + padutils.escapeHtml('require('+JSON.stringify(module.id)+").savePassword()") + "\">ok</button>"); + } + } + + //if we haven't recieved the clientVars yet, then this message should it be + else if (!receivedClientVars) + { + //log the message + if (window.console) console.log(obj); + + receivedClientVars = true; + + //set some client vars + clientVars = obj; + clientVars.userAgent = "Anonymous"; + clientVars.collab_client_vars.clientAgent = "Anonymous"; + + //initalize the pad + pad._afterHandshake(); + initalized = true; + + // If the LineNumbersDisabled value is set to true then we need to hide the Line Numbers + if (settings.LineNumbersDisabled == true) + { + pad.changeViewOption('showLineNumbers', false); + } + + // If the noColors value is set to true then we need to hide the background colors on the ace spans + if (settings.noColors == true) + { + pad.changeViewOption('noColors', true); + } + + if (settings.rtlIsTrue == true) + { + pad.changeViewOption('rtl', true); + } + + // If the Monospacefont value is set to true then change it to monospace. + if (settings.useMonospaceFontGlobal == true) + { + pad.changeViewOption('useMonospaceFont', true); + } + // if the globalUserName value is set we need to tell the server and the client about the new authorname + if (settings.globalUserName !== false) + { + pad.notifyChangeName(settings.globalUserName); // Notifies the server + pad.myUserInfo.name = settings.globalUserName; + $('#myusernameedit').attr({"value":settings.globalUserName}); // Updates the current users UI + } + } + //This handles every Message after the clientVars + else + { + //this message advices the client to disconnect + if (obj.disconnect) + { + padconnectionstatus.disconnected(obj.disconnect); + socket.disconnect(); + return; + } + else + { + pad.collabClient.handleMessageFromServer(obj); + } + } + }); + // Bind the colorpicker + var fb = $('#colorpicker').farbtastic({ callback: '#mycolorpickerpreview', width: 220}); +} + +var pad = { + // don't access these directly from outside this file, except + // for debugging + collabClient: null, + myUserInfo: null, + diagnosticInfo: {}, + initTime: 0, + clientTimeOffset: null, + preloadedImages: false, + padOptions: {}, + + // these don't require init; clientVars should all go through here + getPadId: function() + { + return clientVars.padId; + }, + getClientIp: function() + { + return clientVars.clientIp; + }, + getIsProPad: function() + { + return clientVars.isProPad; + }, + getColorPalette: function() + { + return clientVars.colorPalette; + }, + getDisplayUserAgent: function() + { + return padutils.uaDisplay(clientVars.userAgent); + }, + getIsDebugEnabled: function() + { + return clientVars.debugEnabled; + }, + getPrivilege: function(name) + { + return clientVars.accountPrivs[name]; + }, + getUserIsGuest: function() + { + return clientVars.userIsGuest; + }, + getUserId: function() + { + return pad.myUserInfo.userId; + }, + getUserName: function() + { + return pad.myUserInfo.name; + }, + sendClientMessage: function(msg) + { + pad.collabClient.sendClientMessage(msg); + }, + + init: function() + { + padutils.setupGlobalExceptionHandler(); + + $(document).ready(function() + { + // test for XML HTTP capabiites + ieTestXMLHTTP(); + // start the custom js + if (typeof customStart == "function") customStart(); + getParams(); + handshake(); + }); + }, + _afterHandshake: function() + { + pad.clientTimeOffset = new Date().getTime() - clientVars.serverTimestamp; + + //initialize the chat + chat.init(this); + pad.initTime = +(new Date()); + pad.padOptions = clientVars.initialOptions; + + if ((!$.browser.msie) && (!($.browser.mozilla && $.browser.version.indexOf("1.8.") == 0))) + { + document.domain = document.domain; // for comet + } + + // for IE + if ($.browser.msie) + { + try + { + doc.execCommand("BackgroundImageCache", false, true); + } + catch (e) + {} + } + + // order of inits is important here: + padcookie.init(clientVars.cookiePrefsToSet, this); + + $("#widthprefcheck").click(pad.toggleWidthPref); + // $("#sidebarcheck").click(pad.togglewSidebar); + + pad.myUserInfo = { + userId: clientVars.userId, + name: clientVars.userName, + ip: pad.getClientIp(), + colorId: clientVars.userColor, + userAgent: pad.getDisplayUserAgent() + }; + + if (clientVars.specialKey) + { + pad.myUserInfo.specialKey = clientVars.specialKey; + if (clientVars.specialKeyTranslation) + { + $("#specialkeyarea").html("mode: " + String(clientVars.specialKeyTranslation).toUpperCase()); + } + } + paddocbar.init( + { + isTitleEditable: pad.getIsProPad(), + initialTitle: clientVars.initialTitle, + initialPassword: clientVars.initialPassword, + guestPolicy: pad.padOptions.guestPolicy + }, this); + padimpexp.init(this); + padsavedrevs.init(clientVars.initialRevisionList, this); + + padeditor.init(postAceInit, pad.padOptions.view || {}, this); + + paduserlist.init(pad.myUserInfo, this); + // padchat.init(clientVars.chatHistory, pad.myUserInfo); + padconnectionstatus.init(); + padmodals.init(this); + + pad.collabClient = getCollabClient(padeditor.ace, clientVars.collab_client_vars, pad.myUserInfo, { + colorPalette: pad.getColorPalette() + }, pad); + pad.collabClient.setOnUserJoin(pad.handleUserJoin); + pad.collabClient.setOnUpdateUserInfo(pad.handleUserUpdate); + pad.collabClient.setOnUserLeave(pad.handleUserLeave); + pad.collabClient.setOnClientMessage(pad.handleClientMessage); + pad.collabClient.setOnServerMessage(pad.handleServerMessage); + pad.collabClient.setOnChannelStateChange(pad.handleChannelStateChange); + pad.collabClient.setOnInternalAction(pad.handleCollabAction); + + function postAceInit() + { + padeditbar.init(); + setTimeout(function() + { + padeditor.ace.focus(); + }, 0); + if(padcookie.getPref("chatAlwaysVisible")){ // if we have a cookie for always showing chat then show it + chat.stickToScreen(true); // stick it to the screen + $('#options-stickychat').prop("checked", true); // set the checkbox to on + } + if(padcookie.getPref("showAuthorshipColors") == false){ + pad.changeViewOption('showAuthorColors', false); + } + } + }, + dispose: function() + { + padeditor.dispose(); + }, + notifyChangeName: function(newName) + { + pad.myUserInfo.name = newName; + pad.collabClient.updateUserInfo(pad.myUserInfo); + //padchat.handleUserJoinOrUpdate(pad.myUserInfo); + }, + notifyChangeColor: function(newColorId) + { + pad.myUserInfo.colorId = newColorId; + pad.collabClient.updateUserInfo(pad.myUserInfo); + //padchat.handleUserJoinOrUpdate(pad.myUserInfo); + }, + notifyChangeTitle: function(newTitle) + { + pad.collabClient.sendClientMessage( + { + type: 'padtitle', + title: newTitle, + changedBy: pad.myUserInfo.name || "unnamed" + }); + }, + notifyChangePassword: function(newPass) + { + pad.collabClient.sendClientMessage( + { + type: 'padpassword', + password: newPass, + changedBy: pad.myUserInfo.name || "unnamed" + }); + }, + changePadOption: function(key, value) + { + var options = {}; + options[key] = value; + pad.handleOptionsChange(options); + pad.collabClient.sendClientMessage( + { + type: 'padoptions', + options: options, + changedBy: pad.myUserInfo.name || "unnamed" + }); + }, + changeViewOption: function(key, value) + { + var options = { + view: {} + }; + options.view[key] = value; + pad.handleOptionsChange(options); + }, + handleOptionsChange: function(opts) + { + // opts object is a full set of options or just + // some options to change + if (opts.view) + { + if (!pad.padOptions.view) + { + pad.padOptions.view = {}; + } + for (var k in opts.view) + { + pad.padOptions.view[k] = opts.view[k]; + } + padeditor.setViewOptions(pad.padOptions.view); + } + if (opts.guestPolicy) + { + // order important here + pad.padOptions.guestPolicy = opts.guestPolicy; + paddocbar.setGuestPolicy(opts.guestPolicy); + } + }, + getPadOptions: function() + { + // caller shouldn't mutate the object + return pad.padOptions; + }, + isPadPublic: function() + { + return (!pad.getIsProPad()) || (pad.getPadOptions().guestPolicy == 'allow'); + }, + suggestUserName: function(userId, name) + { + pad.collabClient.sendClientMessage( + { + type: 'suggestUserName', + unnamedId: userId, + newName: name + }); + }, + handleUserJoin: function(userInfo) + { + paduserlist.userJoinOrUpdate(userInfo); + //padchat.handleUserJoinOrUpdate(userInfo); + }, + handleUserUpdate: function(userInfo) + { + paduserlist.userJoinOrUpdate(userInfo); + //padchat.handleUserJoinOrUpdate(userInfo); + }, + handleUserLeave: function(userInfo) + { + paduserlist.userLeave(userInfo); + //padchat.handleUserLeave(userInfo); + }, + handleClientMessage: function(msg) + { + if (msg.type == 'suggestUserName') + { + if (msg.unnamedId == pad.myUserInfo.userId && msg.newName && !pad.myUserInfo.name) + { + pad.notifyChangeName(msg.newName); + paduserlist.setMyUserInfo(pad.myUserInfo); + } + } + else if (msg.type == 'chat') + { + //padchat.receiveChat(msg); + } + else if (msg.type == 'padtitle') + { + paddocbar.changeTitle(msg.title); + } + else if (msg.type == 'padpassword') + { + paddocbar.changePassword(msg.password); + } + else if (msg.type == 'newRevisionList') + { + padsavedrevs.newRevisionList(msg.revisionList); + } + else if (msg.type == 'revisionLabel') + { + padsavedrevs.newRevisionList(msg.revisionList); + } + else if (msg.type == 'padoptions') + { + var opts = msg.options; + pad.handleOptionsChange(opts); + } + else if (msg.type == 'guestanswer') + { + // someone answered a prompt, remove it + paduserlist.removeGuestPrompt(msg.guestId); + } + }, + editbarClick: function(cmd) + { + if (padeditbar) + { + padeditbar.toolbarClick(cmd); + } + }, + dmesg: function(m) + { + if (pad.getIsDebugEnabled()) + { + var djs = $('#djs').get(0); + var wasAtBottom = (djs.scrollTop - (djs.scrollHeight - $(djs).height()) >= -20); + $('#djs').append('<p>' + m + '</p>'); + if (wasAtBottom) + { + djs.scrollTop = djs.scrollHeight; + } + } + }, + handleServerMessage: function(m) + { + if (m.type == 'NOTICE') + { + if (m.text) + { + alertBar.displayMessage(function(abar) + { + abar.find("#servermsgdate").html(" (" + padutils.simpleDateTime(new Date) + ")"); + abar.find("#servermsgtext").html(m.text); + }); + } + if (m.js) + { + window['ev' + 'al'](m.js); + } + } + else if (m.type == 'GUEST_PROMPT') + { + paduserlist.showGuestPrompt(m.userId, m.displayName); + } + }, + handleChannelStateChange: function(newState, message) + { + var oldFullyConnected = !! padconnectionstatus.isFullyConnected(); + var wasConnecting = (padconnectionstatus.getStatus().what == 'connecting'); + if (newState == "CONNECTED") + { + padconnectionstatus.connected(); + } + else if (newState == "RECONNECTING") + { + padconnectionstatus.reconnecting(); + } + else if (newState == "DISCONNECTED") + { + pad.diagnosticInfo.disconnectedMessage = message; + pad.diagnosticInfo.padId = pad.getPadId(); + pad.diagnosticInfo.socket = {}; + + //we filter non objects from the socket object and put them in the diagnosticInfo + //this ensures we have no cyclic data - this allows us to stringify the data + for(var i in socket.socket) + { + var value = socket.socket[i]; + var type = typeof value; + + if(type == "string" || type == "number") + { + pad.diagnosticInfo.socket[i] = value; + } + } + + pad.asyncSendDiagnosticInfo(); + if (typeof window.ajlog == "string") + { + window.ajlog += ("Disconnected: " + message + '\n'); + } + padeditor.disable(); + padeditbar.disable(); + paddocbar.disable(); + padimpexp.disable(); + + padconnectionstatus.disconnected(message); + } + var newFullyConnected = !! padconnectionstatus.isFullyConnected(); + if (newFullyConnected != oldFullyConnected) + { + pad.handleIsFullyConnected(newFullyConnected, wasConnecting); + } + }, + handleIsFullyConnected: function(isConnected, isInitialConnect) + { + // load all images referenced from CSS, one at a time, + // starting one second after connection is first established. + if (isConnected && !pad.preloadedImages) + { + window.setTimeout(function() + { + if (!pad.preloadedImages) + { + pad.preloadImages(); + pad.preloadedImages = true; + } + }, 1000); + } + + padsavedrevs.handleIsFullyConnected(isConnected); + + // pad.determineSidebarVisibility(isConnected && !isInitialConnect); + pad.determineChatVisibility(isConnected && !isInitialConnect); + pad.determineAuthorshipColorsVisibility(); + + }, +/* determineSidebarVisibility: function(asNowConnectedFeedback) + { + if (pad.isFullyConnected()) + { + var setSidebarVisibility = padutils.getCancellableAction("set-sidebar-visibility", function() + { + // $("body").toggleClass('hidesidebar', !! padcookie.getPref('hideSidebar')); + }); + window.setTimeout(setSidebarVisibility, asNowConnectedFeedback ? 3000 : 0); + } + else + { + padutils.cancelActions("set-sidebar-visibility"); + $("body").removeClass('hidesidebar'); + } + }, +*/ + determineChatVisibility: function(asNowConnectedFeedback){ + var chatVisCookie = padcookie.getPref('chatAlwaysVisible'); + if(chatVisCookie){ // if the cookie is set for chat always visible + chat.stickToScreen(true); // stick it to the screen + $('#options-stickychat').prop("checked", true); // set the checkbox to on + } + else{ + $('#options-stickychat').prop("checked", false); // set the checkbox for off + } + }, + determineAuthorshipColorsVisibility: function(){ + var authColCookie = padcookie.getPref('showAuthorshipColors'); + if (authColCookie){ + pad.changeViewOption('showAuthorColors', true); + $('#options-colorscheck').prop("checked", true); + } + else { + $('#options-colorscheck').prop("checked", false); + } + }, + handleCollabAction: function(action) + { + if (action == "commitPerformed") + { + padeditbar.setSyncStatus("syncing"); + } + else if (action == "newlyIdle") + { + padeditbar.setSyncStatus("done"); + } + }, + hideServerMessage: function() + { + alertBar.hideMessage(); + }, + asyncSendDiagnosticInfo: function() + { + window.setTimeout(function() + { + $.ajax( + { + type: 'post', + url: '/ep/pad/connection-diagnostic-info', + data: { + diagnosticInfo: JSON.stringify(pad.diagnosticInfo) + }, + success: function() + {}, + error: function() + {} + }); + }, 0); + }, + forceReconnect: function() + { + $('form#reconnectform input.padId').val(pad.getPadId()); + pad.diagnosticInfo.collabDiagnosticInfo = pad.collabClient.getDiagnosticInfo(); + $('form#reconnectform input.diagnosticInfo').val(JSON.stringify(pad.diagnosticInfo)); + $('form#reconnectform input.missedChanges').val(JSON.stringify(pad.collabClient.getMissedChanges())); + $('form#reconnectform').submit(); + }, + toggleWidthPref: function() + { + var newValue = !padcookie.getPref('fullWidth'); + padcookie.setPref('fullWidth', newValue); + $("#widthprefcheck").toggleClass('widthprefchecked', !! newValue).toggleClass('widthprefunchecked', !newValue); + pad.handleWidthChange(); + }, +/* + toggleSidebar: function() + { + var newValue = !padcookie.getPref('hideSidebar'); + padcookie.setPref('hideSidebar', newValue); + $("#sidebarcheck").toggleClass('sidebarchecked', !newValue).toggleClass('sidebarunchecked', !! newValue); + pad.determineSidebarVisibility(); + }, +*/ + handleWidthChange: function() + { + var isFullWidth = padcookie.getPref('fullWidth'); + if (isFullWidth) + { + $("body").addClass('fullwidth').removeClass('limwidth').removeClass('squish1width').removeClass('squish2width'); + } + else + { + $("body").addClass('limwidth').removeClass('fullwidth'); + + var pageWidth = $(window).width(); + $("body").toggleClass('squish1width', (pageWidth < 912 && pageWidth > 812)).toggleClass('squish2width', (pageWidth <= 812)); + } + }, + // this is called from code put into a frame from the server: + handleImportExportFrameCall: function(callName, varargs) + { + padimpexp.handleFrameCall.call(padimpexp, callName, Array.prototype.slice.call(arguments, 1)); + }, + callWhenNotCommitting: function(f) + { + pad.collabClient.callWhenNotCommitting(f); + }, + getCollabRevisionNumber: function() + { + return pad.collabClient.getCurrentRevisionNumber(); + }, + isFullyConnected: function() + { + return padconnectionstatus.isFullyConnected(); + }, + addHistoricalAuthors: function(data) + { + if (!pad.collabClient) + { + window.setTimeout(function() + { + pad.addHistoricalAuthors(data); + }, 1000); + } + else + { + pad.collabClient.addHistoricalAuthors(data); + } + }, + preloadImages: function() + { + var images = ["../static/img/connectingbar.gif"]; + + function loadNextImage() + { + if (images.length == 0) + { + return; + } + var img = new Image(); + img.src = images.shift(); + if (img.complete) + { + scheduleLoadNextImage(); + } + else + { + $(img).bind('error load onreadystatechange', scheduleLoadNextImage); + } + } + + function scheduleLoadNextImage() + { + window.setTimeout(loadNextImage, 0); + } + scheduleLoadNextImage(); + } +}; + +var alertBar = (function() +{ + + var animator = padutils.makeShowHideAnimator(arriveAtAnimationState, false, 25, 400); + + function arriveAtAnimationState(state) + { + if (state == -1) + { + $("#alertbar").css('opacity', 0).css('display', 'block'); + } + else if (state == 0) + { + $("#alertbar").css('opacity', 1); + } + else if (state == 1) + { + $("#alertbar").css('opacity', 0).css('display', 'none'); + } + else if (state < 0) + { + $("#alertbar").css('opacity', state + 1); + } + else if (state > 0) + { + $("#alertbar").css('opacity', 1 - state); + } + } + + var self = { + displayMessage: function(setupFunc) + { + animator.show(); + setupFunc($("#alertbar")); + }, + hideMessage: function() + { + animator.hide(); + } + }; + return self; +}()); + +function init() { + return pad.init(); +} + +var settings = { + LineNumbersDisabled: false +, noColors: false +, useMonospaceFontGlobal: false +, globalUserName: false +, hideQRCode: false +, rtlIsTrue: false +}; + +pad.settings = settings; + +exports.settings = settings; +exports.createCookie = createCookie; +exports.readCookie = readCookie; +exports.randomString = randomString; +exports.getParams = getParams; +exports.getUrlVars = getUrlVars; +exports.savePassword = savePassword; +exports.handshake = handshake; +exports.pad = pad; +exports.init = init; +exports.alertBar = alertBar; + diff --git a/src/static/js/pad_connectionstatus.js b/src/static/js/pad_connectionstatus.js new file mode 100644 index 00000000..bb0f0521 --- /dev/null +++ b/src/static/js/pad_connectionstatus.js @@ -0,0 +1,91 @@ +/** + * This code is mostly from the old Etherpad. Please help us to comment this code. + * This helps other people to understand this code better and helps them to improve it. + * TL;DR COMMENTS ON THIS FILE ARE HIGHLY APPRECIATED + */ + +/** + * 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. + */ + +var padmodals = require('./pad_modals').padmodals; + +var padconnectionstatus = (function() +{ + + var status = { + what: 'connecting' + }; + + var self = { + init: function() + { + $('button#forcereconnect').click(function() + { + window.location.reload(); + }); + }, + connected: function() + { + status = { + what: 'connected' + }; + padmodals.hideModal(500); + }, + reconnecting: function() + { + status = { + what: 'reconnecting' + }; + $("#connectionbox").get(0).className = 'modaldialog cboxreconnecting'; + padmodals.showModal("#connectionbox", 500); + }, + disconnected: function(msg) + { + if(status.what == "disconnected") + return; + + status = { + what: 'disconnected', + why: msg + }; + var k = String(msg).toLowerCase(); // known reason why + if (!(k == 'userdup' || k == 'deleted' || k == 'looping' || k == 'slowcommit' || k == 'initsocketfail' || k == 'unauth')) + { + k = 'unknown'; + } + + var cls = 'modaldialog cboxdisconnected cboxdisconnected_' + k; + $("#connectionbox").get(0).className = cls; + padmodals.showModal("#connectionbox", 500); + + $('button#forcereconnect').click(function() + { + window.location.reload(); + }); + }, + isFullyConnected: function() + { + return status.what == 'connected'; + }, + getStatus: function() + { + return status; + } + }; + return self; +}()); + +exports.padconnectionstatus = padconnectionstatus; diff --git a/src/static/js/pad_cookie.js b/src/static/js/pad_cookie.js new file mode 100644 index 00000000..1bb5700a --- /dev/null +++ b/src/static/js/pad_cookie.js @@ -0,0 +1,133 @@ +/** + * This code is mostly from the old Etherpad. Please help us to comment this code. + * This helps other people to understand this code better and helps them to improve it. + * TL;DR COMMENTS ON THIS FILE ARE HIGHLY APPRECIATED + */ + +/** + * 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. + */ + + +var padcookie = (function() +{ + function getRawCookie() + { + // returns null if can't get cookie text + if (!document.cookie) + { + return null; + } + // look for (start of string OR semicolon) followed by whitespace followed by prefs=(something); + var regexResult = document.cookie.match(/(?:^|;)\s*prefs=([^;]*)(?:;|$)/); + if ((!regexResult) || (!regexResult[1])) + { + return null; + } + return regexResult[1]; + } + + function setRawCookie(safeText) + { + var expiresDate = new Date(); + expiresDate.setFullYear(3000); + document.cookie = ('prefs=' + safeText + ';expires=' + expiresDate.toGMTString()); + } + + function parseCookie(text) + { + // returns null if can't parse cookie. + try + { + var cookieData = JSON.parse(unescape(text)); + return cookieData; + } + catch (e) + { + return null; + } + } + + function stringifyCookie(data) + { + return escape(JSON.stringify(data)); + } + + function saveCookie() + { + if (!inited) + { + return; + } + setRawCookie(stringifyCookie(cookieData)); + + if (pad.getIsProPad() && (!getRawCookie()) && (!alreadyWarnedAboutNoCookies)) + { + alert("Warning: it appears that your browser does not have cookies enabled." + " EtherPad uses cookies to keep track of unique users for the purpose" + " of putting a quota on the number of active users. Using EtherPad without " + " cookies may fill up your server's user quota faster than expected."); + alreadyWarnedAboutNoCookies = true; + } + } + + var wasNoCookie = true; + var cookieData = {}; + var alreadyWarnedAboutNoCookies = false; + var inited = false; + + var pad = undefined; + var self = { + init: function(prefsToSet, _pad) + { + pad = _pad; + + var rawCookie = getRawCookie(); + if (rawCookie) + { + var cookieObj = parseCookie(rawCookie); + if (cookieObj) + { + wasNoCookie = false; // there was a cookie + delete cookieObj.userId; + delete cookieObj.name; + delete cookieObj.colorId; + cookieData = cookieObj; + } + } + + for (var k in prefsToSet) + { + cookieData[k] = prefsToSet[k]; + } + + inited = true; + saveCookie(); + }, + wasNoCookie: function() + { + return wasNoCookie; + }, + getPref: function(prefName) + { + return cookieData[prefName]; + }, + setPref: function(prefName, value) + { + cookieData[prefName] = value; + saveCookie(); + } + }; + return self; +}()); + +exports.padcookie = padcookie; diff --git a/src/static/js/pad_docbar.js b/src/static/js/pad_docbar.js new file mode 100644 index 00000000..08bbb0c4 --- /dev/null +++ b/src/static/js/pad_docbar.js @@ -0,0 +1,466 @@ +/** + * This code is mostly from the old Etherpad. Please help us to comment this code. + * This helps other people to understand this code better and helps them to improve it. + * TL;DR COMMENTS ON THIS FILE ARE HIGHLY APPRECIATED + */ + +/** + * 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. + */ + +var padutils = require('./pad_utils').padutils; + +var paddocbar = (function() +{ + var isTitleEditable = false; + var isEditingTitle = false; + var isEditingPassword = false; + var enabled = false; + + function getPanelOpenCloseAnimator(panelName, panelHeight) + { + var wrapper = $("#" + panelName + "-wrapper"); + var openingClass = "docbar" + panelName + "-opening"; + var openClass = "docbar" + panelName + "-open"; + var closingClass = "docbar" + panelName + "-closing"; + + function setPanelState(action) + { + $("#docbar").removeClass(openingClass).removeClass(openClass). + removeClass(closingClass); + if (action != "closed") + { + $("#docbar").addClass("docbar" + panelName + "-" + action); + } + } + + function openCloseAnimate(state) + { + function pow(x) + { + x = 1 - x; + x *= x * x; + return 1 - x; + } + + if (state == -1) + { + // startng to open + setPanelState("opening"); + wrapper.css('height', '0'); + } + else if (state < 0) + { + // opening + var height = Math.round(pow(state + 1) * (panelHeight - 1)) + 'px'; + wrapper.css('height', height); + } + else if (state == 0) + { + // open + setPanelState("open"); + wrapper.css('height', panelHeight - 1); + } + else if (state < 1) + { + // closing + setPanelState("closing"); + var height = Math.round((1 - pow(state)) * (panelHeight - 1)) + 'px'; + wrapper.css('height', height); + } + else if (state == 1) + { + // closed + setPanelState("closed"); + wrapper.css('height', '0'); + } + } + + return padutils.makeShowHideAnimator(openCloseAnimate, false, 25, 500); + } + + + var currentPanel = null; + + function setCurrentPanel(newCurrentPanel) + { + if (currentPanel != newCurrentPanel) + { + currentPanel = newCurrentPanel; + padutils.cancelActions("hide-docbar-panel"); + } + } + var panels; + + function changePassword(newPass) + { + if ((newPass || null) != (self.password || null)) + { + self.password = (newPass || null); + pad.notifyChangePassword(newPass); + } + self.renderPassword(); + } + + var pad = undefined; + var self = { + title: null, + password: null, + init: function(opts, _pad) + { + pad = _pad; + + panels = { + impexp: { + animator: getPanelOpenCloseAnimator("impexp", 160) + }, + savedrevs: { + animator: getPanelOpenCloseAnimator("savedrevs", 79) + }, + options: { + animator: getPanelOpenCloseAnimator("options", 114) + }, + security: { + animator: getPanelOpenCloseAnimator("security", 130) + } + }; + + isTitleEditable = opts.isTitleEditable; + self.title = opts.initialTitle; + self.password = opts.initialPassword; + + $("#docbarimpexp").click(function() + { + self.togglePanel("impexp"); + }); + $("#docbarsavedrevs").click(function() + { + self.togglePanel("savedrevs"); + }); + $("#docbaroptions").click(function() + { + self.togglePanel("options"); + }); + $("#docbarsecurity").click(function() + { + self.togglePanel("security"); + }); + + $("#docbarrenamelink").click(self.editTitle); + $("#padtitlesave").click(function() + { + self.closeTitleEdit(true); + }); + $("#padtitlecancel").click(function() + { + self.closeTitleEdit(false); + }); + padutils.bindEnterAndEscape($("#padtitleedit"), function() + { + $("#padtitlesave").trigger('click'); + }, function() + { + $("#padtitlecancel").trigger('click'); + }); + + $("#options-close").click(function() + { + self.setShownPanel(null); + }); + $("#security-close").click(function() + { + self.setShownPanel(null); + }); + + if (pad.getIsProPad()) + { + self.initPassword(); + } + + enabled = true; + self.render(); + + // public/private + $("#security-access input").bind("change click", function(evt) + { + pad.changePadOption('guestPolicy', $("#security-access input[name='padaccess']:checked").val()); + }); + self.setGuestPolicy(opts.guestPolicy); + }, + setGuestPolicy: function(newPolicy) + { + $("#security-access input[value='" + newPolicy + "']").attr("checked", "checked"); + self.render(); + }, + initPassword: function() + { + self.renderPassword(); + $("#password-clearlink").click(function() + { + changePassword(null); + }); + $("#password-setlink, #password-display").click(function() + { + self.enterPassword(); + }); + $("#password-cancellink").click(function() + { + self.exitPassword(false); + }); + $("#password-savelink").click(function() + { + self.exitPassword(true); + }); + padutils.bindEnterAndEscape($("#security-passwordedit"), function() + { + self.exitPassword(true); + }, function() + { + self.exitPassword(false); + }); + }, + enterPassword: function() + { + isEditingPassword = true; + $("#security-passwordedit").val(self.password || ''); + self.renderPassword(); + $("#security-passwordedit").focus().select(); + }, + exitPassword: function(accept) + { + isEditingPassword = false; + if (accept) + { + changePassword($("#security-passwordedit").val()); + } + else + { + self.renderPassword(); + } + }, + renderPassword: function() + { + if (isEditingPassword) + { + $("#password-nonedit").hide(); + $("#password-inedit").show(); + } + else + { + $("#password-nonedit").toggleClass('nopassword', !self.password); + $("#password-setlink").html(self.password ? "Change..." : "Set..."); + if (self.password) + { + $("#password-display").html(self.password.replace(/./g, '•')); + } + else + { + $("#password-display").html("None"); + } + $("#password-inedit").hide(); + $("#password-nonedit").show(); + } + }, + togglePanel: function(panelName) + { + if (panelName in panels) + { + if (currentPanel == panelName) + { + self.setShownPanel(null); + } + else + { + self.setShownPanel(panelName); + } + } + }, + setShownPanel: function(panelName) + { + function animateHidePanel(panelName, next) + { + var delay = 0; + if (panelName == 'options' && isEditingPassword) + { + // give user feedback that the password they've + // typed in won't actually take effect + self.exitPassword(false); + delay = 500; + } + + window.setTimeout(function() + { + panels[panelName].animator.hide(); + if (next) + { + next(); + } + }, delay); + } + + if (!panelName) + { + if (currentPanel) + { + animateHidePanel(currentPanel); + setCurrentPanel(null); + } + } + else if (panelName in panels) + { + if (currentPanel != panelName) + { + if (currentPanel) + { + animateHidePanel(currentPanel, function() + { + panels[panelName].animator.show(); + setCurrentPanel(panelName); + }); + } + else + { + panels[panelName].animator.show(); + setCurrentPanel(panelName); + } + } + } + }, + isPanelShown: function(panelName) + { + if (!panelName) + { + return !currentPanel; + } + else + { + return (panelName == currentPanel); + } + }, + changeTitle: function(newTitle) + { + self.title = newTitle; + self.render(); + }, + editTitle: function() + { + if (!enabled) + { + return; + } + $("#padtitleedit").val(self.title); + isEditingTitle = true; + self.render(); + $("#padtitleedit").focus().select(); + }, + closeTitleEdit: function(accept) + { + if (!enabled) + { + return; + } + if (accept) + { + var newTitle = $("#padtitleedit").val(); + if (newTitle) + { + newTitle = newTitle.substring(0, 80); + self.title = newTitle; + + pad.notifyChangeTitle(newTitle); + } + } + + isEditingTitle = false; + self.render(); + }, + changePassword: function(newPass) + { + if (newPass) + { + self.password = newPass; + } + else + { + self.password = null; + } + self.renderPassword(); + }, + render: function() + { + if (isEditingTitle) + { + $("#docbarpadtitle").hide(); + $("#docbarrenamelink").hide(); + $("#padtitleedit").show(); + $("#padtitlebuttons").show(); + if (!enabled) + { + $("#padtitleedit").attr('disabled', 'disabled'); + } + else + { + $("#padtitleedit").removeAttr('disabled'); + } + } + else + { + $("#padtitleedit").hide(); + $("#padtitlebuttons").hide(); + + var titleSpan = $("#docbarpadtitle span"); + titleSpan.html(padutils.escapeHtml(self.title)); + $("#docbarpadtitle").attr('title', (pad.isPadPublic() ? "Public Pad: " : "") + self.title); + $("#docbarpadtitle").show(); + + if (isTitleEditable) + { + var titleRight = $("#docbarpadtitle").position().left + $("#docbarpadtitle span").position().left + Math.min($("#docbarpadtitle").width(), $("#docbarpadtitle span").width()); + $("#docbarrenamelink").css('left', titleRight + 10).show(); + } + + if (pad.isPadPublic()) + { + $("#docbar").addClass("docbar-public"); + } + else + { + $("#docbar").removeClass("docbar-public"); + } + } + }, + disable: function() + { + enabled = false; + self.render(); + }, + handleResizePage: function() + { + // Side-step circular reference. This should be injected. + var padsavedrevs = require('./pad_savedrevs').padsavedrevs; + padsavedrevs.handleResizePage(); + }, + hideLaterIfNoOtherInteraction: function() + { + return padutils.getCancellableAction('hide-docbar-panel', function() + { + self.setShownPanel(null); + }); + } + }; + return self; +}()); + +exports.paddocbar = paddocbar; diff --git a/src/static/js/pad_editbar.js b/src/static/js/pad_editbar.js new file mode 100644 index 00000000..0cfd1f20 --- /dev/null +++ b/src/static/js/pad_editbar.js @@ -0,0 +1,256 @@ +/** + * This code is mostly from the old Etherpad. Please help us to comment this code. + * This helps other people to understand this code better and helps them to improve it. + * TL;DR COMMENTS ON THIS FILE ARE HIGHLY APPRECIATED + */ + +/** + * 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. + */ + +var padutils = require('./pad_utils').padutils; +var padeditor = require('./pad_editor').padeditor; +var padsavedrevs = require('./pad_savedrevs').padsavedrevs; + +function indexOf(array, value) { + for (var i = 0, ii = array.length; i < ii; i++) { + if (array[i] == value) { + return i; + } + } + return -1; +} + +var padeditbar = (function() +{ + + var syncAnimation = (function() + { + var SYNCING = -100; + var DONE = 100; + var state = DONE; + var fps = 25; + var step = 1 / fps; + var T_START = -0.5; + var T_FADE = 1.0; + var T_GONE = 1.5; + var animator = padutils.makeAnimationScheduler(function() + { + if (state == SYNCING || state == DONE) + { + return false; + } + else if (state >= T_GONE) + { + state = DONE; + $("#syncstatussyncing").css('display', 'none'); + $("#syncstatusdone").css('display', 'none'); + return false; + } + else if (state < 0) + { + state += step; + if (state >= 0) + { + $("#syncstatussyncing").css('display', 'none'); + $("#syncstatusdone").css('display', 'block').css('opacity', 1); + } + return true; + } + else + { + state += step; + if (state >= T_FADE) + { + $("#syncstatusdone").css('opacity', (T_GONE - state) / (T_GONE - T_FADE)); + } + return true; + } + }, step * 1000); + return { + syncing: function() + { + state = SYNCING; + $("#syncstatussyncing").css('display', 'block'); + $("#syncstatusdone").css('display', 'none'); + }, + done: function() + { + state = T_START; + animator.scheduleAnimation(); + } + }; + }()); + + var self = { + init: function() + { + $("#editbar .editbarbutton").attr("unselectable", "on"); // for IE + $("#editbar").removeClass("disabledtoolbar").addClass("enabledtoolbar"); + }, + isEnabled: function() + { +// return !$("#editbar").hasClass('disabledtoolbar'); + return true; + }, + disable: function() + { + $("#editbar").addClass('disabledtoolbar').removeClass("enabledtoolbar"); + }, + toolbarClick: function(cmd) + { + if (self.isEnabled()) + { + if(cmd == "showusers") + { + self.toogleDropDown("users"); + } + else if (cmd == 'settings') + { + self.toogleDropDown("settingsmenu"); + } + else if (cmd == 'embed') + { + self.setEmbedLinks(); + $('#linkinput').focus().select(); + self.toogleDropDown("embed"); + } + else if (cmd == 'import_export') + { + self.toogleDropDown("importexport"); + } + else if (cmd == 'save') + { + padsavedrevs.saveNow(); + } + else + { + padeditor.ace.callWithAce(function(ace) + { + if (cmd == 'bold' || cmd == 'italic' || cmd == 'underline' || cmd == 'strikethrough') ace.ace_toggleAttributeOnSelection(cmd); + else if (cmd == 'undo' || cmd == 'redo') ace.ace_doUndoRedo(cmd); + else if (cmd == 'insertunorderedlist') ace.ace_doInsertUnorderedList(); + else if (cmd == 'insertorderedlist') ace.ace_doInsertOrderedList(); + else if (cmd == 'indent') + { + if (!ace.ace_doIndentOutdent(false)) + { + ace.ace_doInsertUnorderedList(); + } + } + else if (cmd == 'outdent') + { + ace.ace_doIndentOutdent(true); + } + else if (cmd == 'clearauthorship') + { + if ((!(ace.ace_getRep().selStart && ace.ace_getRep().selEnd)) || ace.ace_isCaret()) + { + if (window.confirm("Clear authorship colors on entire document?")) + { + ace.ace_performDocumentApplyAttributesToCharRange(0, ace.ace_getRep().alltext.length, [ + ['author', ''] + ]); + } + } + else + { + ace.ace_setAttributeOnSelection('author', ''); + } + } + }, cmd, true); + } + } + if(padeditor.ace) padeditor.ace.focus(); + }, + toogleDropDown: function(moduleName) + { + var modules = ["settingsmenu", "importexport", "embed", "users"]; + + //hide all modules + if(moduleName == "none") + { + $("#editbar ul#menu_right > li").removeClass("selected"); + for(var i=0;i<modules.length;i++) + { + //skip the userlist + if(modules[i] == "users") + continue; + + var module = $("#" + modules[i]); + + if(module.css('display') != "none") + { + module.slideUp("fast"); + } + } + } + else + { + var nth_child = indexOf(modules, moduleName) + 1; + if (nth_child > 0 && nth_child <= 3) { + $("#editbar ul#menu_right li:not(:nth-child(" + nth_child + "))").removeClass("selected"); + $("#editbar ul#menu_right li:nth-child(" + nth_child + ")").toggleClass("selected"); + } + //hide all modules that are not selected and show the selected one + for(var i=0;i<modules.length;i++) + { + var module = $("#" + modules[i]); + + if(module.css('display') != "none") + { + module.slideUp("fast"); + } + else if(modules[i]==moduleName) + { + module.slideDown("fast"); + } + } + } + }, + setSyncStatus: function(status) + { + if (status == "syncing") + { + syncAnimation.syncing(); + } + else if (status == "done") + { + syncAnimation.done(); + } + }, + setEmbedLinks: function() + { + if ($('#readonlyinput').is(':checked')) + { + var basePath = document.location.href.substring(0, document.location.href.indexOf("/p/")); + var readonlyLink = basePath + "/ro/" + clientVars.readOnlyId; + $('#embedinput').val("<iframe name='embed_readonly' src='" + readonlyLink + "?showControls=true&showChat=true&showLineNumbers=true&useMonospaceFont=false' width=600 height=400>"); + $('#linkinput').val(readonlyLink); + $('#embedreadonlyqr').attr("src","https://chart.googleapis.com/chart?chs=200x200&cht=qr&chld=|0&chl=" + readonlyLink); + } + else + { + var padurl = window.location.href.split("?")[0]; + $('#embedinput').val("<iframe name='embed_readwrite' src='" + padurl + "?showControls=true&showChat=true&showLineNumbers=true&useMonospaceFont=false' width=600 height=400>"); + $('#linkinput').val(padurl); + $('#embedreadonlyqr').attr("src","https://chart.googleapis.com/chart?chs=200x200&cht=qr&chld=|0&chl=" + padurl); + } + } + }; + return self; +}()); + +exports.padeditbar = padeditbar; diff --git a/src/static/js/pad_editor.js b/src/static/js/pad_editor.js new file mode 100644 index 00000000..12f83aeb --- /dev/null +++ b/src/static/js/pad_editor.js @@ -0,0 +1,163 @@ +/** + * This code is mostly from the old Etherpad. Please help us to comment this code. + * This helps other people to understand this code better and helps them to improve it. + * TL;DR COMMENTS ON THIS FILE ARE HIGHLY APPRECIATED + */ + +/** + * 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. + */ + +var padcookie = require('./pad_cookie').padcookie; +var padutils = require('./pad_utils').padutils; + +var padeditor = (function() +{ + var Ace2Editor = undefined; + var pad = undefined; + var settings = undefined; + var self = { + ace: null, + // this is accessed directly from other files + viewZoom: 100, + init: function(readyFunc, initialViewOptions, _pad) + { + Ace2Editor = require('./ace').Ace2Editor; + pad = _pad; + settings = pad.settings; + + function aceReady() + { + $("#editorloadingbox").hide(); + if (readyFunc) + { + readyFunc(); + } + } + + self.ace = new Ace2Editor(); + self.ace.init("editorcontainer", "", aceReady); + self.ace.setProperty("wraps", true); + if (pad.getIsDebugEnabled()) + { + self.ace.setProperty("dmesg", pad.dmesg); + } + self.initViewOptions(); + self.setViewOptions(initialViewOptions); + + // view bar + self.initViewZoom(); + $("#viewbarcontents").show(); + }, + initViewOptions: function() + { + padutils.bindCheckboxChange($("#options-linenoscheck"), function() + { + pad.changeViewOption('showLineNumbers', padutils.getCheckbox($("#options-linenoscheck"))); + }); + padutils.bindCheckboxChange($("#options-colorscheck"), function() + { + padcookie.setPref('showAuthorshipColors', padutils.getCheckbox("#options-colorscheck")); + pad.changeViewOption('showAuthorColors', padutils.getCheckbox("#options-colorscheck")); + }); + $("#viewfontmenu").change(function() + { + pad.changeViewOption('useMonospaceFont', $("#viewfontmenu").val() == 'monospace'); + }); + }, + setViewOptions: function(newOptions) + { + function getOption(key, defaultValue) + { + var value = String(newOptions[key]); + if (value == "true") return true; + if (value == "false") return false; + return defaultValue; + } + + self.ace.setProperty("showsauthorcolors", !settings.noColors); + + self.ace.setProperty("rtlIsTrue", settings.rtlIsTrue); + + var v; + + v = getOption('showLineNumbers', true); + self.ace.setProperty("showslinenumbers", v); + padutils.setCheckbox($("#options-linenoscheck"), v); + + v = getOption('showAuthorColors', true); + self.ace.setProperty("showsauthorcolors", v); + padutils.setCheckbox($("#options-colorscheck"), v); + + v = getOption('useMonospaceFont', false); + self.ace.setProperty("textface", (v ? "monospace" : "Arial, sans-serif")); + $("#viewfontmenu").val(v ? "monospace" : "normal"); + }, + initViewZoom: function() + { + var viewZoom = Number(padcookie.getPref('viewZoom')); + if ((!viewZoom) || isNaN(viewZoom)) + { + viewZoom = 100; + } + self.setViewZoom(viewZoom); + $("#viewzoommenu").change(function(evt) + { + // strip initial 'z' from val + self.setViewZoom(Number($("#viewzoommenu").val().substring(1))); + }); + }, + setViewZoom: function(percent) + { + if (!(percent >= 50 && percent <= 1000)) + { + // percent is out of sane range or NaN (which fails comparisons) + return; + } + + self.viewZoom = percent; + $("#viewzoommenu").val('z' + percent); + + var baseSize = 13; + self.ace.setProperty('textsize', Math.round(baseSize * self.viewZoom / 100)); + + padcookie.setPref('viewZoom', percent); + }, + dispose: function() + { + if (self.ace) + { + self.ace.destroy(); + self.ace = null; + } + }, + disable: function() + { + if (self.ace) + { + self.ace.setProperty("grayedOut", true); + self.ace.setEditable(false); + } + }, + restoreRevisionText: function(dataFromServer) + { + pad.addHistoricalAuthors(dataFromServer.historicalAuthorData); + self.ace.importAText(dataFromServer.atext, dataFromServer.apool, true); + } + }; + return self; +}()); + +exports.padeditor = padeditor; diff --git a/src/static/js/pad_impexp.js b/src/static/js/pad_impexp.js new file mode 100644 index 00000000..23655ba4 --- /dev/null +++ b/src/static/js/pad_impexp.js @@ -0,0 +1,299 @@ +/** + * This code is mostly from the old Etherpad. Please help us to comment this code. + * This helps other people to understand this code better and helps them to improve it. + * TL;DR COMMENTS ON THIS FILE ARE HIGHLY APPRECIATED + */ + +/** + * 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. + */ + +var paddocbar = require('./pad_docbar').paddocbar; + +var padimpexp = (function() +{ + + ///// import + var currentImportTimer = null; + var hidePanelCall = null; + + function addImportFrames() + { + $("#import .importframe").remove(); + var iframe = $('<iframe style="display: none;" name="importiframe" class="importframe"></iframe>'); + $('#import').append(iframe); + } + + function fileInputUpdated() + { + $('#importformfilediv').addClass('importformenabled'); + $('#importsubmitinput').removeAttr('disabled'); + $('#importmessagefail').fadeOut("fast"); + $('#importarrow').show(); + $('#importarrow').animate( + { + paddingLeft: "0px" + }, 500).animate( + { + paddingLeft: "10px" + }, 150, 'swing').animate( + { + paddingLeft: "0px" + }, 150, 'swing').animate( + { + paddingLeft: "10px" + }, 150, 'swing').animate( + { + paddingLeft: "0px" + }, 150, 'swing').animate( + { + paddingLeft: "10px" + }, 150, 'swing').animate( + { + paddingLeft: "0px" + }, 150, 'swing'); + } + + function fileInputSubmit() + { + $('#importmessagefail').fadeOut("fast"); + var ret = window.confirm("Importing a file will overwrite the current text of the pad." + " Are you sure you want to proceed?"); + if (ret) + { + hidePanelCall = paddocbar.hideLaterIfNoOtherInteraction(); + currentImportTimer = window.setTimeout(function() + { + if (!currentImportTimer) + { + return; + } + currentImportTimer = null; + importFailed("Request timed out."); + }, 25000); // time out after some number of seconds + $('#importsubmitinput').attr( + { + disabled: true + }).val("Importing..."); + window.setTimeout(function() + { + $('#importfileinput').attr( + { + disabled: true + }); + }, 0); + $('#importarrow').stop(true, true).hide(); + $('#importstatusball').show(); + } + return ret; + } + + function importFailed(msg) + { + importErrorMessage(msg); + } + + function importDone() + { + $('#importsubmitinput').removeAttr('disabled').val("Import Now"); + window.setTimeout(function() + { + $('#importfileinput').removeAttr('disabled'); + }, 0); + $('#importstatusball').hide(); + importClearTimeout(); + addImportFrames(); + } + + function importClearTimeout() + { + if (currentImportTimer) + { + window.clearTimeout(currentImportTimer); + currentImportTimer = null; + } + } + + function importErrorMessage(status) + { + var msg=""; + + if(status === "convertFailed"){ + msg = "We were not able to import this file. Please use a different document format or copy paste manually"; + } else if(status === "uploadFailed"){ + msg = "The upload failed, please try again"; + } + + function showError(fade) + { + $('#importmessagefail').html('<strong style="color: red">Import failed:</strong> ' + (msg || 'Please copy paste'))[(fade ? "fadeIn" : "show")](); + } + + if ($('#importexport .importmessage').is(':visible')) + { + $('#importmessagesuccess').fadeOut("fast"); + $('#importmessagefail').fadeOut("fast", function() + { + showError(true); + }); + } + else + { + showError(); + } + } + + function importSuccessful(token) + { + $.ajax( + { + type: 'post', + url: '/ep/pad/impexp/import2', + data: { + token: token, + padId: pad.getPadId() + }, + success: importApplicationSuccessful, + error: importApplicationFailed, + timeout: 25000 + }); + addImportFrames(); + } + + function importApplicationFailed(xhr, textStatus, errorThrown) + { + importErrorMessage("Error during conversion."); + importDone(); + } + + ///// export + + function cantExport() + { + var type = $(this); + if (type.hasClass("exporthrefpdf")) + { + type = "PDF"; + } + else if (type.hasClass("exporthrefdoc")) + { + type = "Microsoft Word"; + } + else if (type.hasClass("exporthrefodt")) + { + type = "OpenDocument"; + } + else + { + type = "this file"; + } + alert("Exporting as " + type + " format is disabled. Please contact your" + " system administrator for details."); + return false; + } + + ///// + var pad = undefined; + var self = { + init: function(_pad) + { + pad = _pad; + + //get /p/padname + var pad_root_path = new RegExp(/.*\/p\/[^\/]+/).exec(document.location.pathname) + //get http://example.com/p/padname + var pad_root_url = document.location.href.replace(document.location.pathname, pad_root_path) + + // build the export links + $("#exporthtmla").attr("href", pad_root_path + "/export/html"); + $("#exportplaina").attr("href", pad_root_path + "/export/txt"); + $("#exportwordlea").attr("href", pad_root_path + "/export/wordle"); + $("#exportdokuwikia").attr("href", pad_root_path + "/export/dokuwiki"); + + //hide stuff thats not avaible if abiword is disabled + if(clientVars.abiwordAvailable == "no") + { + $("#exportworda").remove(); + $("#exportpdfa").remove(); + $("#exportopena").remove(); + $(".importformdiv").remove(); + $("#import").html("Import is not available. To enable import please install abiword"); + } + else if(clientVars.abiwordAvailable == "withoutPDF") + { + $("#exportpdfa").remove(); + + $("#exportworda").attr("href", pad_root_path + "/export/doc"); + $("#exportopena").attr("href", pad_root_path + "/export/odt"); + + $("#importexport").css({"height":"142px"}); + $("#importexportline").css({"height":"142px"}); + + $("#importform").attr('action', pad_root_url + "/import"); + } + else + { + $("#exportworda").attr("href", pad_root_path + "/export/doc"); + $("#exportpdfa").attr("href", pad_root_path + "/export/pdf"); + $("#exportopena").attr("href", pad_root_path + "/export/odt"); + + $("#importform").attr('action', pad_root_path + "/import"); + } + + $("#impexp-close").click(function() + { + paddocbar.setShownPanel(null); + }); + + addImportFrames(); + $("#importfileinput").change(fileInputUpdated); + $('#importform').submit(fileInputSubmit); + $('.disabledexport').click(cantExport); + }, + handleFrameCall: function(status) + { + if (status !== "ok") + { + importFailed(status); + } + + importDone(); + }, + disable: function() + { + $("#impexp-disabled-clickcatcher").show(); + $("#import").css('opacity', 0.5); + $("#impexp-export").css('opacity', 0.5); + }, + enable: function() + { + $("#impexp-disabled-clickcatcher").hide(); + $("#import").css('opacity', 1); + $("#impexp-export").css('opacity', 1); + }, + export2Wordle: function() + { + var padUrl = $('#exportwordlea').attr('href').replace(/\/wordle$/, '/txt') + + $.get(padUrl, function(data) + { + $('.result').html(data); + $('#text').html(data); + $('#wordlepost').submit(); + }); + } + }; + return self; +}()); + +exports.padimpexp = padimpexp; diff --git a/src/static/js/pad_modals.js b/src/static/js/pad_modals.js new file mode 100644 index 00000000..0dd281bb --- /dev/null +++ b/src/static/js/pad_modals.js @@ -0,0 +1,374 @@ +/** + * This code is mostly from the old Etherpad. Please help us to comment this code. + * This helps other people to understand this code better and helps them to improve it. + * TL;DR COMMENTS ON THIS FILE ARE HIGHLY APPRECIATED + */ + +/** + * 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. + */ + +var padutils = require('./pad_utils').padutils; +var paddocbar = require('./pad_docbar').paddocbar; + +var padmodals = (function() +{ + +/*var clearFeedbackEmail = function() {}; + function clearFeedback() { + clearFeedbackEmail(); + $("#feedbackbox-message").val(''); + } + + var sendingFeedback = false; + function setSendingFeedback(v) { + v = !! v; + if (sendingFeedback != v) { + sendingFeedback = v; + if (v) { + $("#feedbackbox-send").css('opacity', 0.75); + } + else { + $("#feedbackbox-send").css('opacity', 1); + } + } + }*/ + + var sendingInvite = false; + + function setSendingInvite(v) + { + v = !! v; + if (sendingInvite != v) + { + sendingInvite = v; + if (v) + { + $(".sharebox-send").css('opacity', 0.75); + } + else + { + $("#sharebox-send").css('opacity', 1); + } + } + } + + var clearShareBoxTo = function() + {}; + + function clearShareBox() + { + clearShareBoxTo(); + } + + var pad = undefined; + var self = { + init: function(_pad) + { + pad = _pad; + + self.initFeedback(); + self.initShareBox(); + }, + initFeedback: function() + { +/*var emailField = $("#feedbackbox-email"); + clearFeedbackEmail = + padutils.makeFieldLabeledWhenEmpty(emailField, '(your email address)').clear; + clearFeedback();*/ + + $("#feedbackbox-hide").click(function() + { + self.hideModal(); + }); +/*$("#feedbackbox-send").click(function() { + self.sendFeedbackEmail(); + });*/ + + $("#feedbackbutton").click(function() + { + self.showFeedback(); + }); + }, + initShareBox: function() + { + $("#sharebutton").click(function() + { + self.showShareBox(); + }); + $("#sharebox-hide").click(function() + { + self.hideModal(); + }); + $("#sharebox-send").click(function() + { + self.sendInvite(); + }); + + $("#sharebox-url").click(function() + { + $("#sharebox-url").focus().select(); + }); + + clearShareBoxTo = padutils.makeFieldLabeledWhenEmpty($("#sharebox-to"), "(email addresses)").clear; + clearShareBox(); + + $("#sharebox-subject").val(self.getDefaultShareBoxSubjectForName(pad.getUserName())); + $("#sharebox-message").val(self.getDefaultShareBoxMessageForName(pad.getUserName())); + + $("#sharebox-stripe .setsecurity").click(function() + { + self.hideModal(); + paddocbar.setShownPanel('security'); + }); + }, + getDefaultShareBoxMessageForName: function(name) + { + return (name || "Somebody") + " has shared an EtherPad document with you." + "\n\n" + "View it here:\n\n" + padutils.escapeHtml($(".sharebox-url").val() + "\n"); + }, + getDefaultShareBoxSubjectForName: function(name) + { + return (name || "Somebody") + " invited you to an EtherPad document"; + }, + relayoutWithBottom: function(px) + { + $("#modaloverlay").height(px); + $("#sharebox").css('left', Math.floor(($(window).width() - $("#sharebox").outerWidth()) / 2)); + $("#feedbackbox").css('left', Math.floor(($(window).width() - $("#feedbackbox").outerWidth()) / 2)); + }, + showFeedback: function() + { + self.showModal("#feedbackbox"); + }, + showShareBox: function() + { + // when showing the dialog, if it still says "Somebody" invited you + // then we fill in the updated username if there is one; + // otherwise, we don't touch it, perhaps the user is happy with it + var msgbox = $("#sharebox-message"); + if (msgbox.val() == self.getDefaultShareBoxMessageForName(null)) + { + msgbox.val(self.getDefaultShareBoxMessageForName(pad.getUserName())); + } + var subjBox = $("#sharebox-subject"); + if (subjBox.val() == self.getDefaultShareBoxSubjectForName(null)) + { + subjBox.val(self.getDefaultShareBoxSubjectForName(pad.getUserName())); + } + + if (pad.isPadPublic()) + { + $("#sharebox-stripe").get(0).className = 'sharebox-stripe-public'; + } + else + { + $("#sharebox-stripe").get(0).className = 'sharebox-stripe-private'; + } + + self.showModal("#sharebox", 500); + $("#sharebox-url").focus().select(); + }, + showModal: function(modalId, duration) + { + $(".modaldialog").hide(); + $(modalId).show().css( + { + 'opacity': 0 + }).animate( + { + 'opacity': 1 + }, duration); + $("#modaloverlay").show().css( + { + 'opacity': 0 + }).animate( + { + 'opacity': 1 + }, duration); + }, + hideModal: function(duration) + { + padutils.cancelActions('hide-feedbackbox'); + padutils.cancelActions('hide-sharebox'); + $("#sharebox-response").hide(); + $(".modaldialog").animate( + { + 'opacity': 0 + }, duration, function() + { + $("#modaloverlay").hide(); + }); + $("#modaloverlay").animate( + { + 'opacity': 0 + }, duration, function() + { + $("#modaloverlay").hide(); + }); + }, + hideFeedbackLaterIfNoOtherInteraction: function() + { + return padutils.getCancellableAction('hide-feedbackbox', function() + { + self.hideModal(); + }); + }, + hideShareboxLaterIfNoOtherInteraction: function() + { + return padutils.getCancellableAction('hide-sharebox', function() + { + self.hideModal(); + }); + }, +/* sendFeedbackEmail: function() { + if (sendingFeedback) { + return; + } + var message = $("#feedbackbox-message").val(); + if (! message) { + return; + } + var email = ($("#feedbackbox-email").hasClass('editempty') ? '' : + $("#feedbackbox-email").val()); + var padId = pad.getPadId(); + var username = pad.getUserName(); + setSendingFeedback(true); + $("#feedbackbox-response").html("Sending...").get(0).className = ''; + $("#feedbackbox-response").show(); + $.ajax({ + type: 'post', + url: '/ep/pad/feedback', + data: { + feedback: message, + padId: padId, + username: username, + email: email + }, + success: success, + error: error + }); + var hideCall = self.hideFeedbackLaterIfNoOtherInteraction(); + function success(msg) { + setSendingFeedback(false); + clearFeedback(); + $("#feedbackbox-response").html("Thanks for your feedback").get(0).className = 'goodresponse'; + $("#feedbackbox-response").show(); + window.setTimeout(function() { + $("#feedbackbox-response").fadeOut('slow', function() { + hideCall(); + }); + }, 1500); + } + function error(e) { + setSendingFeedback(false); + $("#feedbackbox-response").html("Could not send feedback. Please email us at feedback"+"@"+"etherpad.com instead.").get(0).className = 'badresponse'; + $("#feedbackbox-response").show(); + } + },*/ + sendInvite: function() + { + if (sendingInvite) + { + return; + } + if (!pad.isFullyConnected()) + { + displayErrorMessage("Error: Connection to the server is down or flaky."); + return; + } + var message = $("#sharebox-message").val(); + if (!message) + { + displayErrorMessage("Please enter a message body before sending."); + return; + } + var emails = ($("#sharebox-to").hasClass('editempty') ? '' : $("#sharebox-to").val()) || ''; + // find runs of characters that aren't obviously non-email punctuation + var emailArray = emails.match(/[^\s,:;<>\"\'\/\(\)\[\]{}]+/g) || []; + if (emailArray.length == 0) + { + displayErrorMessage('Please enter at least one "To:" address.'); + $("#sharebox-to").focus().select(); + return; + } + for (var i = 0; i < emailArray.length; i++) + { + var addr = emailArray[i]; + if (!addr.match(/^[\w\.\_\+\-]+\@[\w\_\-]+\.[\w\_\-\.]+$/)) + { + displayErrorMessage('"' + padutils.escapeHtml(addr) + '" does not appear to be a valid email address.'); + return; + } + } + var subject = $("#sharebox-subject").val(); + if (!subject) + { + subject = self.getDefaultShareBoxSubjectForName(pad.getUserName()); + $("#sharebox-subject").val(subject); // force the default subject + } + + var padId = pad.getPadId(); + var username = pad.getUserName(); + setSendingInvite(true); + $("#sharebox-response").html("Sending...").get(0).className = ''; + $("#sharebox-response").show(); + $.ajax( + { + type: 'post', + url: '/ep/pad/emailinvite', + data: { + message: message, + toEmails: emailArray.join(','), + subject: subject, + username: username, + padId: padId + }, + success: success, + error: error + }); + var hideCall = self.hideShareboxLaterIfNoOtherInteraction(); + + function success(msg) + { + setSendingInvite(false); + $("#sharebox-response").html("Email invitation sent!").get(0).className = 'goodresponse'; + $("#sharebox-response").show(); + window.setTimeout(function() + { + $("#sharebox-response").fadeOut('slow', function() + { + hideCall(); + }); + }, 1500); + } + + function error(e) + { + setSendingFeedback(false); + $("#sharebox-response").html("An error occurred; no email was sent.").get(0).className = 'badresponse'; + $("#sharebox-response").show(); + } + + function displayErrorMessage(msgHtml) + { + $("#sharebox-response").html(msgHtml).get(0).className = 'badresponse'; + $("#sharebox-response").show(); + } + } + }; + return self; +}()); + +exports.padmodals = padmodals; diff --git a/src/static/js/pad_savedrevs.js b/src/static/js/pad_savedrevs.js new file mode 100644 index 00000000..2a0f4fde --- /dev/null +++ b/src/static/js/pad_savedrevs.js @@ -0,0 +1,526 @@ +/** + * This code is mostly from the old Etherpad. Please help us to comment this code. + * This helps other people to understand this code better and helps them to improve it. + * TL;DR COMMENTS ON THIS FILE ARE HIGHLY APPRECIATED + */ + +/** + * 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. + */ + +var padutils = require('./pad_utils').padutils; +var paddocbar = require('./pad_docbar').paddocbar; + +var padsavedrevs = (function() +{ + + function reversedCopy(L) + { + var L2 = L.slice(); + L2.reverse(); + return L2; + } + + function makeRevisionBox(revisionInfo, rnum) + { + var box = $('<div class="srouterbox">' + '<div class="srinnerbox">' + '<a href="javascript:void(0)" class="srname"><!-- --></a>' + '<div class="sractions"><a class="srview" href="javascript:void(0)" target="_blank">view</a> | <a class="srrestore" href="javascript:void(0)">restore</a></div>' + '<div class="srtime"><!-- --></div>' + '<div class="srauthor"><!-- --></div>' + '<img class="srtwirly" src="static/img/misc/status-ball.gif">' + '</div></div>'); + setBoxLabel(box, revisionInfo.label); + setBoxTimestamp(box, revisionInfo.timestamp); + box.find(".srauthor").html("by " + padutils.escapeHtml(revisionInfo.savedBy)); + var viewLink = '/ep/pad/view/' + pad.getPadId() + '/' + revisionInfo.id; + box.find(".srview").attr('href', viewLink); + var restoreLink = 'javascript:void(require('+JSON.stringify(module.id)+').padsavedrevs.restoreRevision(' + JSON.stringify(rnum) + ');'; + box.find(".srrestore").attr('href', restoreLink); + box.find(".srname").click(function(evt) + { + editRevisionLabel(rnum, box); + }); + return box; + } + + function setBoxLabel(box, label) + { + box.find(".srname").html(padutils.escapeHtml(label)).attr('title', label); + } + + function setBoxTimestamp(box, timestamp) + { + box.find(".srtime").html(padutils.escapeHtml( + padutils.timediff(new Date(timestamp)))); + } + + function getNthBox(n) + { + return $("#savedrevisions .srouterbox").eq(n); + } + + function editRevisionLabel(rnum, box) + { + var input = $('<input type="text" class="srnameedit"/>'); + box.find(".srnameedit").remove(); // just in case + var label = box.find(".srname"); + input.width(label.width()); + input.height(label.height()); + input.css('top', label.position().top); + input.css('left', label.position().left); + label.after(input); + label.css('opacity', 0); + + function endEdit() + { + input.remove(); + label.css('opacity', 1); + } + var rev = currentRevisionList[rnum]; + var oldLabel = rev.label; + input.blur(function() + { + var newLabel = input.val(); + if (newLabel && newLabel != oldLabel) + { + relabelRevision(rnum, newLabel); + } + endEdit(); + }); + input.val(rev.label).focus().select(); + padutils.bindEnterAndEscape(input, function onEnter() + { + input.blur(); + }, function onEscape() + { + input.val('').blur(); + }); + } + + function relabelRevision(rnum, newLabel) + { + var rev = currentRevisionList[rnum]; + $.ajax( + { + type: 'post', + url: '/ep/pad/saverevisionlabel', + data: { + userId: pad.getUserId(), + padId: pad.getPadId(), + revId: rev.id, + newLabel: newLabel + }, + success: success, + error: error + }); + + function success(text) + { + var newRevisionList = JSON.parse(text); + self.newRevisionList(newRevisionList); + pad.sendClientMessage( + { + type: 'revisionLabel', + revisionList: reversedCopy(currentRevisionList), + savedBy: pad.getUserName(), + newLabel: newLabel + }); + } + + function error(e) + { + alert("Oops! There was an error saving that revision label. Please try again later."); + } + } + + var currentRevisionList = []; + + function setRevisionList(newRevisionList, noAnimation) + { + // deals with changed labels and new added revisions + for (var i = 0; i < currentRevisionList.length; i++) + { + var a = currentRevisionList[i]; + var b = newRevisionList[i]; + if (b.label != a.label) + { + setBoxLabel(getNthBox(i), b.label); + } + } + for (var j = currentRevisionList.length; j < newRevisionList.length; j++) + { + var newBox = makeRevisionBox(newRevisionList[j], j); + $("#savedrevs-scrollinner").append(newBox); + newBox.css('left', j * REVISION_BOX_WIDTH); + } + var newOnes = (newRevisionList.length > currentRevisionList.length); + currentRevisionList = newRevisionList; + if (newOnes) + { + setDesiredScroll(getMaxScroll()); + if (noAnimation) + { + setScroll(desiredScroll); + } + + if (!noAnimation) + { + var nameOfLast = currentRevisionList[currentRevisionList.length - 1].label; + displaySavedTip(nameOfLast); + } + } + } + + function refreshRevisionList() + { + for (var i = 0; i < currentRevisionList.length; i++) + { + var r = currentRevisionList[i]; + var box = getNthBox(i); + setBoxTimestamp(box, r.timestamp); + } + } + + var savedTipAnimator = padutils.makeShowHideAnimator(function(state) + { + if (state == -1) + { + $("#revision-notifier").css('opacity', 0).css('display', 'block'); + } + else if (state == 0) + { + $("#revision-notifier").css('opacity', 1); + } + else if (state == 1) + { + $("#revision-notifier").css('opacity', 0).css('display', 'none'); + } + else if (state < 0) + { + $("#revision-notifier").css('opacity', 1); + } + else if (state > 0) + { + $("#revision-notifier").css('opacity', 1 - state); + } + }, false, 25, 300); + + function displaySavedTip(text) + { + $("#revision-notifier .name").html(padutils.escapeHtml(text)); + savedTipAnimator.show(); + padutils.cancelActions("hide-revision-notifier"); + var hideLater = padutils.getCancellableAction("hide-revision-notifier", function() + { + savedTipAnimator.hide(); + }); + window.setTimeout(hideLater, 3000); + } + + var REVISION_BOX_WIDTH = 120; + var curScroll = 0; // distance between left of revisions and right of view + var desiredScroll = 0; + + function getScrollWidth() + { + return REVISION_BOX_WIDTH * currentRevisionList.length; + } + + function getViewportWidth() + { + return $("#savedrevs-scrollouter").width(); + } + + function getMinScroll() + { + return Math.min(getViewportWidth(), getScrollWidth()); + } + + function getMaxScroll() + { + return getScrollWidth(); + } + + function setScroll(newScroll) + { + curScroll = newScroll; + $("#savedrevs-scrollinner").css('right', newScroll); + updateScrollArrows(); + } + + function setDesiredScroll(newDesiredScroll, dontUpdate) + { + desiredScroll = Math.min(getMaxScroll(), Math.max(getMinScroll(), newDesiredScroll)); + if (!dontUpdate) + { + updateScroll(); + } + } + + function updateScroll() + { + updateScrollArrows(); + scrollAnimator.scheduleAnimation(); + } + + function updateScrollArrows() + { + $("#savedrevs-scrollleft").toggleClass("disabledscrollleft", desiredScroll <= getMinScroll()); + $("#savedrevs-scrollright").toggleClass("disabledscrollright", desiredScroll >= getMaxScroll()); + } + var scrollAnimator = padutils.makeAnimationScheduler(function() + { + setDesiredScroll(desiredScroll, true); // re-clamp + if (Math.abs(desiredScroll - curScroll) < 1) + { + setScroll(desiredScroll); + return false; + } + else + { + setScroll(curScroll + (desiredScroll - curScroll) * 0.5); + return true; + } + }, 50, 2); + + var isSaving = false; + + function setIsSaving(v) + { + isSaving = v; + rerenderButton(); + } + + function haveReachedRevLimit() + { + var mv = pad.getPrivilege('maxRevisions'); + return (!(mv < 0 || mv > currentRevisionList.length)); + } + + function rerenderButton() + { + if (isSaving || (!pad.isFullyConnected()) || haveReachedRevLimit()) + { + $("#savedrevs-savenow").css('opacity', 0.75); + } + else + { + $("#savedrevs-savenow").css('opacity', 1); + } + } + + var scrollRepeatTimer = null; + var scrollStartTime = 0; + + function setScrollRepeatTimer(dir) + { + clearScrollRepeatTimer(); + scrollStartTime = +new Date; + scrollRepeatTimer = window.setTimeout(function f() + { + if (!scrollRepeatTimer) + { + return; + } + self.scroll(dir); + var scrollTime = (+new Date) - scrollStartTime; + var delay = (scrollTime > 2000 ? 50 : 300); + scrollRepeatTimer = window.setTimeout(f, delay); + }, 300); + $(document).bind('mouseup', clearScrollRepeatTimer); + } + + function clearScrollRepeatTimer() + { + if (scrollRepeatTimer) + { + window.clearTimeout(scrollRepeatTimer); + scrollRepeatTimer = null; + } + $(document).unbind('mouseup', clearScrollRepeatTimer); + } + + var pad = undefined; + var self = { + init: function(initialRevisions, _pad) + { + pad = _pad; + self.newRevisionList(initialRevisions, true); + + $("#savedrevs-savenow").click(function() + { + self.saveNow(); + }); + $("#savedrevs-scrollleft").mousedown(function() + { + self.scroll('left'); + setScrollRepeatTimer('left'); + }); + $("#savedrevs-scrollright").mousedown(function() + { + self.scroll('right'); + setScrollRepeatTimer('right'); + }); + $("#savedrevs-close").click(function() + { + paddocbar.setShownPanel(null); + }); + + // update "saved n minutes ago" times + window.setInterval(function() + { + refreshRevisionList(); + }, 60 * 1000); + }, + restoreRevision: function(rnum) + { + var rev = currentRevisionList[rnum]; + var warning = ("Restoring this revision will overwrite the current" + " text of the pad. " + "Are you sure you want to continue?"); + var hidePanel = paddocbar.hideLaterIfNoOtherInteraction(); + var box = getNthBox(rnum); + if (confirm(warning)) + { + box.find(".srtwirly").show(); + $.ajax( + { + type: 'get', + url: '/ep/pad/getrevisionatext', + data: { + padId: pad.getPadId(), + revId: rev.id + }, + success: success, + error: error + }); + } + + function success(resultJson) + { + untwirl(); + var result = JSON.parse(resultJson); + padeditor.restoreRevisionText(result); + window.setTimeout(function() + { + hidePanel(); + }, 0); + } + + function error(e) + { + untwirl(); + alert("Oops! There was an error retreiving the text (revNum= " + rev.revNum + "; padId=" + pad.getPadId()); + } + + function untwirl() + { + box.find(".srtwirly").hide(); + } + }, + showReachedLimit: function() + { + alert("Sorry, you do not have privileges to save more than " + pad.getPrivilege('maxRevisions') + " revisions."); + }, + newRevisionList: function(lst, noAnimation) + { + // server gives us list with newest first; + // we want chronological order + var L = reversedCopy(lst); + setRevisionList(L, noAnimation); + rerenderButton(); + }, + saveNow: function() + { + if (isSaving) + { + return; + } + if (!pad.isFullyConnected()) + { + return; + } + if (haveReachedRevLimit()) + { + self.showReachedLimit(); + return; + } + setIsSaving(true); + var savedBy = pad.getUserName() || "unnamed"; + pad.callWhenNotCommitting(submitSave); + + function submitSave() + { + $.ajax( + { + type: 'post', + url: '/ep/pad/saverevision', + data: { + padId: pad.getPadId(), + savedBy: savedBy, + savedById: pad.getUserId(), + revNum: pad.getCollabRevisionNumber() + }, + success: success, + error: error + }); + } + + function success(text) + { + setIsSaving(false); + var newRevisionList = JSON.parse(text); + self.newRevisionList(newRevisionList); + pad.sendClientMessage( + { + type: 'newRevisionList', + revisionList: newRevisionList, + savedBy: savedBy + }); + } + + function error(e) + { + setIsSaving(false); + alert("Oops! The server failed to save the revision. Please try again later."); + } + }, + handleResizePage: function() + { + updateScrollArrows(); + }, + handleIsFullyConnected: function(isConnected) + { + rerenderButton(); + }, + scroll: function(dir) + { + var minScroll = getMinScroll(); + var maxScroll = getMaxScroll(); + if (dir == 'left') + { + if (desiredScroll > minScroll) + { + var n = Math.floor((desiredScroll - 1 - minScroll) / REVISION_BOX_WIDTH); + setDesiredScroll(Math.max(0, n) * REVISION_BOX_WIDTH + minScroll); + } + } + else if (dir == 'right') + { + if (desiredScroll < maxScroll) + { + var n = Math.floor((maxScroll - desiredScroll - 1) / REVISION_BOX_WIDTH); + setDesiredScroll(maxScroll - Math.max(0, n) * REVISION_BOX_WIDTH); + } + } + } + }; + return self; +}()); + +exports.padsavedrevs = padsavedrevs; diff --git a/src/static/js/pad_userlist.js b/src/static/js/pad_userlist.js new file mode 100644 index 00000000..5a3f9b35 --- /dev/null +++ b/src/static/js/pad_userlist.js @@ -0,0 +1,814 @@ +/** + * This code is mostly from the old Etherpad. Please help us to comment this code. + * This helps other people to understand this code better and helps them to improve it. + * TL;DR COMMENTS ON THIS FILE ARE HIGHLY APPRECIATED + */ + +/** + * 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. + */ + +var padutils = require('./pad_utils').padutils; + +var myUserInfo = {}; + +var colorPickerOpen = false; +var colorPickerSetup = false; +var previousColorId = 0; + + +var paduserlist = (function() +{ + + var rowManager = (function() + { + // The row manager handles rendering rows of the user list and animating + // their insertion, removal, and reordering. It manipulates TD height + // and TD opacity. + + function nextRowId() + { + return "usertr" + (nextRowId.counter++); + } + nextRowId.counter = 1; + // objects are shared; fields are "domId","data","animationStep" + var rowsFadingOut = []; // unordered set + var rowsFadingIn = []; // unordered set + var rowsPresent = []; // in order + var ANIMATION_START = -12; // just starting to fade in + var ANIMATION_END = 12; // just finishing fading out + + + function getAnimationHeight(step, power) + { + var a = Math.abs(step / 12); + if (power == 2) a = a * a; + else if (power == 3) a = a * a * a; + else if (power == 4) a = a * a * a * a; + else if (power >= 5) a = a * a * a * a * a; + return Math.round(26 * (1 - a)); + } + var OPACITY_STEPS = 6; + + var ANIMATION_STEP_TIME = 20; + var LOWER_FRAMERATE_FACTOR = 2; + var scheduleAnimation = padutils.makeAnimationScheduler(animateStep, ANIMATION_STEP_TIME, LOWER_FRAMERATE_FACTOR).scheduleAnimation; + + var NUMCOLS = 4; + + // we do lots of manipulation of table rows and stuff that JQuery makes ok, despite + // IE's poor handling when manipulating the DOM directly. + + function getEmptyRowHtml(height) + { + return '<td colspan="' + NUMCOLS + '" style="border:0;height:' + height + 'px"><!-- --></td>'; + } + + function isNameEditable(data) + { + return (!data.name) && (data.status != 'Disconnected'); + } + + function replaceUserRowContents(tr, height, data) + { + var tds = getUserRowHtml(height, data).match(/<td.*?<\/td>/gi); + if (isNameEditable(data) && tr.find("td.usertdname input:enabled").length > 0) + { + // preserve input field node + for (var i = 0; i < tds.length; i++) + { + var oldTd = $(tr.find("td").get(i)); + if (!oldTd.hasClass('usertdname')) + { + oldTd.replaceWith(tds[i]); + } + } + } + else + { + tr.html(tds.join('')); + } + return tr; + } + + function getUserRowHtml(height, data) + { + var nameHtml; + var isGuest = (data.id.charAt(0) != 'p'); + if (data.name) + { + nameHtml = padutils.escapeHtml(data.name); + if (isGuest && pad.getIsProPad()) + { + nameHtml += ' (Guest)'; + } + } + else + { + nameHtml = '<input type="text" class="editempty newinput" value="unnamed" ' + (isNameEditable(data) ? '' : 'disabled="disabled" ') + '/>'; + } + + return ['<td style="height:', height, 'px" class="usertdswatch"><div class="swatch" style="background:' + data.color + '"> </div></td>', '<td style="height:', height, 'px" class="usertdname">', nameHtml, '</td>', '<td style="height:', height, 'px" class="usertdstatus">', padutils.escapeHtml(data.status), '</td>', '<td style="height:', height, 'px" class="activity">', padutils.escapeHtml(data.activity), '</td>'].join(''); + } + + function getRowHtml(id, innerHtml) + { + return '<tr id="' + id + '">' + innerHtml + '</tr>'; + } + + function rowNode(row) + { + return $("#" + row.domId); + } + + function handleRowData(row) + { + if (row.data && row.data.status == 'Disconnected') + { + row.opacity = 0.5; + } + else + { + delete row.opacity; + } + } + + function handleRowNode(tr, data) + { + if (data.titleText) + { + var titleText = data.titleText; + window.setTimeout(function() + { + /* tr.attr('title', titleText)*/ + }, 0); + } + else + { + tr.removeAttr('title'); + } + } + + function handleOtherUserInputs() + { + // handle 'INPUT' elements for naming other unnamed users + $("#otheruserstable input.newinput").each(function() + { + var input = $(this); + var tr = input.closest("tr"); + if (tr.length > 0) + { + var index = tr.parent().children().index(tr); + if (index >= 0) + { + var userId = rowsPresent[index].data.id; + rowManagerMakeNameEditor($(this), userId); + } + } + }).removeClass('newinput'); + } + + // animationPower is 0 to skip animation, 1 for linear, 2 for quadratic, etc. + + + function insertRow(position, data, animationPower) + { + position = Math.max(0, Math.min(rowsPresent.length, position)); + animationPower = (animationPower === undefined ? 4 : animationPower); + + var domId = nextRowId(); + var row = { + data: data, + animationStep: ANIMATION_START, + domId: domId, + animationPower: animationPower + }; + handleRowData(row); + rowsPresent.splice(position, 0, row); + var tr; + if (animationPower == 0) + { + tr = $(getRowHtml(domId, getUserRowHtml(getAnimationHeight(0), data))); + row.animationStep = 0; + } + else + { + rowsFadingIn.push(row); + tr = $(getRowHtml(domId, getEmptyRowHtml(getAnimationHeight(ANIMATION_START)))); + } + handleRowNode(tr, data); + if (position == 0) + { + $("table#otheruserstable").prepend(tr); + } + else + { + rowNode(rowsPresent[position - 1]).after(tr); + } + + if (animationPower != 0) + { + scheduleAnimation(); + } + + handleOtherUserInputs(); + + return row; + } + + function updateRow(position, data) + { + var row = rowsPresent[position]; + if (row) + { + row.data = data; + handleRowData(row); + if (row.animationStep == 0) + { + // not currently animating + var tr = rowNode(row); + replaceUserRowContents(tr, getAnimationHeight(0), row.data).find("td").css('opacity', (row.opacity === undefined ? 1 : row.opacity)); + handleRowNode(tr, data); + handleOtherUserInputs(); + } + } + } + + function removeRow(position, animationPower) + { + animationPower = (animationPower === undefined ? 4 : animationPower); + var row = rowsPresent[position]; + if (row) + { + rowsPresent.splice(position, 1); // remove + if (animationPower == 0) + { + rowNode(row).remove(); + } + else + { + row.animationStep = -row.animationStep; // use symmetry + row.animationPower = animationPower; + rowsFadingOut.push(row); + scheduleAnimation(); + } + } + } + + // newPosition is position after the row has been removed + + + function moveRow(oldPosition, newPosition, animationPower) + { + animationPower = (animationPower === undefined ? 1 : animationPower); // linear is best + var row = rowsPresent[oldPosition]; + if (row && oldPosition != newPosition) + { + var rowData = row.data; + removeRow(oldPosition, animationPower); + insertRow(newPosition, rowData, animationPower); + } + } + + function animateStep() + { + // animation must be symmetrical + for (var i = rowsFadingIn.length - 1; i >= 0; i--) + { // backwards to allow removal + var row = rowsFadingIn[i]; + var step = ++row.animationStep; + var animHeight = getAnimationHeight(step, row.animationPower); + var node = rowNode(row); + var baseOpacity = (row.opacity === undefined ? 1 : row.opacity); + if (step <= -OPACITY_STEPS) + { + node.find("td").height(animHeight); + } + else if (step == -OPACITY_STEPS + 1) + { + node.html(getUserRowHtml(animHeight, row.data)).find("td").css('opacity', baseOpacity * 1 / OPACITY_STEPS); + handleRowNode(node, row.data); + } + else if (step < 0) + { + node.find("td").css('opacity', baseOpacity * (OPACITY_STEPS - (-step)) / OPACITY_STEPS).height(animHeight); + } + else if (step == 0) + { + // set HTML in case modified during animation + node.html(getUserRowHtml(animHeight, row.data)).find("td").css('opacity', baseOpacity * 1).height(animHeight); + handleRowNode(node, row.data); + rowsFadingIn.splice(i, 1); // remove from set + } + } + for (var i = rowsFadingOut.length - 1; i >= 0; i--) + { // backwards to allow removal + var row = rowsFadingOut[i]; + var step = ++row.animationStep; + var node = rowNode(row); + var animHeight = getAnimationHeight(step, row.animationPower); + var baseOpacity = (row.opacity === undefined ? 1 : row.opacity); + if (step < OPACITY_STEPS) + { + node.find("td").css('opacity', baseOpacity * (OPACITY_STEPS - step) / OPACITY_STEPS).height(animHeight); + } + else if (step == OPACITY_STEPS) + { + node.html(getEmptyRowHtml(animHeight)); + } + else if (step <= ANIMATION_END) + { + node.find("td").height(animHeight); + } + else + { + rowsFadingOut.splice(i, 1); // remove from set + node.remove(); + } + } + + handleOtherUserInputs(); + + return (rowsFadingIn.length > 0) || (rowsFadingOut.length > 0); // is more to do + } + + var self = { + insertRow: insertRow, + removeRow: removeRow, + moveRow: moveRow, + updateRow: updateRow + }; + return self; + }()); ////////// rowManager + var otherUsersInfo = []; + var otherUsersData = []; + + function rowManagerMakeNameEditor(jnode, userId) + { + setUpEditable(jnode, function() + { + var existingIndex = findExistingIndex(userId); + if (existingIndex >= 0) + { + return otherUsersInfo[existingIndex].name || ''; + } + else + { + return ''; + } + }, function(newName) + { + if (!newName) + { + jnode.addClass("editempty"); + jnode.val("unnamed"); + } + else + { + jnode.attr('disabled', 'disabled'); + pad.suggestUserName(userId, newName); + } + }); + } + + function findExistingIndex(userId) + { + var existingIndex = -1; + for (var i = 0; i < otherUsersInfo.length; i++) + { + if (otherUsersInfo[i].userId == userId) + { + existingIndex = i; + break; + } + } + return existingIndex; + } + + function setUpEditable(jqueryNode, valueGetter, valueSetter) + { + jqueryNode.bind('focus', function(evt) + { + var oldValue = valueGetter(); + if (jqueryNode.val() !== oldValue) + { + jqueryNode.val(oldValue); + } + jqueryNode.addClass("editactive").removeClass("editempty"); + }); + jqueryNode.bind('blur', function(evt) + { + var newValue = jqueryNode.removeClass("editactive").val(); + valueSetter(newValue); + }); + padutils.bindEnterAndEscape(jqueryNode, function onEnter() + { + jqueryNode.blur(); + }, function onEscape() + { + jqueryNode.val(valueGetter()).blur(); + }); + jqueryNode.removeAttr('disabled').addClass('editable'); + } + + function updateInviteNotice() + { + if (otherUsersInfo.length == 0) + { + $("#otheruserstable").hide(); + $("#nootherusers").show(); + } + else + { + $("#nootherusers").hide(); + $("#otheruserstable").show(); + } + } + + var knocksToIgnore = {}; + var guestPromptFlashState = 0; + var guestPromptFlash = padutils.makeAnimationScheduler( + + function() + { + var prompts = $("#guestprompts .guestprompt"); + if (prompts.length == 0) + { + return false; // no more to do + } + + guestPromptFlashState = 1 - guestPromptFlashState; + if (guestPromptFlashState) + { + prompts.css('background', '#ffa'); + } + else + { + prompts.css('background', '#ffe'); + } + + return true; + }, 1000); + + var pad = undefined; + var self = { + init: function(myInitialUserInfo, _pad) + { + pad = _pad; + + self.setMyUserInfo(myInitialUserInfo); + + $("#otheruserstable tr").remove(); + + if (pad.getUserIsGuest()) + { + $("#myusernameedit").addClass('myusernameedithoverable'); + setUpEditable($("#myusernameedit"), function() + { + return myUserInfo.name || ''; + }, function(newValue) + { + myUserInfo.name = newValue; + pad.notifyChangeName(newValue); + // wrap with setTimeout to do later because we get + // a double "blur" fire in IE... + window.setTimeout(function() + { + self.renderMyUserInfo(); + }, 0); + }); + } + + // color picker + $("#myswatchbox").click(showColorPicker); + $("#mycolorpicker .pickerswatchouter").click(function() + { + $("#mycolorpicker .pickerswatchouter").removeClass('picked'); + $(this).addClass('picked'); + }); + $("#mycolorpickersave").click(function() + { + closeColorPicker(true); + }); + $("#mycolorpickercancel").click(function() + { + closeColorPicker(false); + }); + // + }, + setMyUserInfo: function(info) + { + //translate the colorId + if(typeof info.colorId == "number") + { + info.colorId = clientVars.colorPalette[info.colorId]; + } + + myUserInfo = $.extend( + {}, info); + + self.renderMyUserInfo(); + }, + userJoinOrUpdate: function(info) + { + if ((!info.userId) || (info.userId == myUserInfo.userId)) + { + // not sure how this would happen + return; + } + + var userData = {}; + userData.color = typeof info.colorId == "number" ? clientVars.colorPalette[info.colorId] : info.colorId; + userData.name = info.name; + userData.status = ''; + userData.activity = ''; + userData.id = info.userId; + // Firefox ignores \n in title text; Safari does a linebreak + userData.titleText = [info.userAgent || '', info.ip || ''].join(' \n'); + + var existingIndex = findExistingIndex(info.userId); + + var numUsersBesides = otherUsersInfo.length; + if (existingIndex >= 0) + { + numUsersBesides--; + } + var newIndex = padutils.binarySearch(numUsersBesides, function(n) + { + if (existingIndex >= 0 && n >= existingIndex) + { + // pretend existingIndex isn't there + n++; + } + var infoN = otherUsersInfo[n]; + var nameN = (infoN.name || '').toLowerCase(); + var nameThis = (info.name || '').toLowerCase(); + var idN = infoN.userId; + var idThis = info.userId; + return (nameN > nameThis) || (nameN == nameThis && idN > idThis); + }); + + if (existingIndex >= 0) + { + // update + if (existingIndex == newIndex) + { + otherUsersInfo[existingIndex] = info; + otherUsersData[existingIndex] = userData; + rowManager.updateRow(existingIndex, userData); + } + else + { + otherUsersInfo.splice(existingIndex, 1); + otherUsersData.splice(existingIndex, 1); + otherUsersInfo.splice(newIndex, 0, info); + otherUsersData.splice(newIndex, 0, userData); + rowManager.updateRow(existingIndex, userData); + rowManager.moveRow(existingIndex, newIndex); + } + } + else + { + otherUsersInfo.splice(newIndex, 0, info); + otherUsersData.splice(newIndex, 0, userData); + rowManager.insertRow(newIndex, userData); + } + + updateInviteNotice(); + + self.updateNumberOfOnlineUsers(); + }, + updateNumberOfOnlineUsers: function() + { + var online = 1; // you are always online! + for (var i = 0; i < otherUsersData.length; i++) + { + if (otherUsersData[i].status == "") + { + online++; + } + } + $("#online_count").text(online); + + return online; + }, + userLeave: function(info) + { + var existingIndex = findExistingIndex(info.userId); + if (existingIndex >= 0) + { + var userData = otherUsersData[existingIndex]; + userData.status = 'Disconnected'; + rowManager.updateRow(existingIndex, userData); + if (userData.leaveTimer) + { + window.clearTimeout(userData.leaveTimer); + } + // set up a timer that will only fire if no leaves, + // joins, or updates happen for this user in the + // next N seconds, to remove the user from the list. + var thisUserId = info.userId; + var thisLeaveTimer = window.setTimeout(function() + { + var newExistingIndex = findExistingIndex(thisUserId); + if (newExistingIndex >= 0) + { + var newUserData = otherUsersData[newExistingIndex]; + if (newUserData.status == 'Disconnected' && newUserData.leaveTimer == thisLeaveTimer) + { + otherUsersInfo.splice(newExistingIndex, 1); + otherUsersData.splice(newExistingIndex, 1); + rowManager.removeRow(newExistingIndex); + updateInviteNotice(); + } + } + }, 8000); // how long to wait + userData.leaveTimer = thisLeaveTimer; + } + updateInviteNotice(); + + self.updateNumberOfOnlineUsers(); + }, + showGuestPrompt: function(userId, displayName) + { + if (knocksToIgnore[userId]) + { + return; + } + + var encodedUserId = padutils.encodeUserId(userId); + + var actionName = 'hide-guest-prompt-' + encodedUserId; + padutils.cancelActions(actionName); + + var box = $("#guestprompt-" + encodedUserId); + if (box.length == 0) + { + // make guest prompt box + box = $('<div id="'+padutils.escapeHtml('guestprompt-' + encodedUserId) + '" class="guestprompt"><div class="choices"><a href="' + padutils.escapeHtml('javascript:void(require('+JSON.stringify(module.id)+').paduserlist.answerGuestPrompt(' + JSON.stringify(encodedUserId) + ',false))')+'">Deny</a> <a href="' + padutils.escapeHtml('javascript:void(require('+JSON.stringify(module.id)+').paduserlist.answerGuestPrompt(' + JSON.stringify(encodedUserId) + ',true))') + '">Approve</a></div><div class="guestname"><strong>Guest:</strong> ' + padutils.escapeHtml(displayName) + '</div></div>'); + $("#guestprompts").append(box); + } + else + { + // update display name + box.find(".guestname").html('<strong>Guest:</strong> ' + padutils.escapeHtml(displayName)); + } + var hideLater = padutils.getCancellableAction(actionName, function() + { + self.removeGuestPrompt(userId); + }); + window.setTimeout(hideLater, 15000); // time-out with no knock + guestPromptFlash.scheduleAnimation(); + }, + removeGuestPrompt: function(userId) + { + var box = $("#guestprompt-" + padutils.encodeUserId(userId)); + // remove ID now so a new knock by same user gets new, unfaded box + box.removeAttr('id').fadeOut("fast", function() + { + box.remove(); + }); + + knocksToIgnore[userId] = true; + window.setTimeout(function() + { + delete knocksToIgnore[userId]; + }, 5000); + }, + answerGuestPrompt: function(encodedUserId, approve) + { + var guestId = padutils.decodeUserId(encodedUserId); + + var msg = { + type: 'guestanswer', + authId: pad.getUserId(), + guestId: guestId, + answer: (approve ? "approved" : "denied") + }; + pad.sendClientMessage(msg); + + self.removeGuestPrompt(guestId); + }, + renderMyUserInfo: function() + { + if (myUserInfo.name) + { + $("#myusernameedit").removeClass("editempty").val( + myUserInfo.name); + } + else + { + $("#myusernameedit").addClass("editempty").val("Enter your name"); + } + if (colorPickerOpen) + { + $("#myswatchbox").addClass('myswatchboxunhoverable').removeClass('myswatchboxhoverable'); + } + else + { + $("#myswatchbox").addClass('myswatchboxhoverable').removeClass('myswatchboxunhoverable'); + } + + $("#myswatch").css({'background-color': myUserInfo.colorId}); + + if ($.browser.msie && parseInt($.browser.version) <= 8) { + $("#usericon").css({'box-shadow': 'inset 0 0 30px ' + myUserInfo.colorId,'background-color': myUserInfo.colorId}); + } + else + { + $("#usericon").css({'box-shadow': 'inset 0 0 30px ' + myUserInfo.colorId}); + } + } + }; + return self; +}()); + +function getColorPickerSwatchIndex(jnode) +{ + // return Number(jnode.get(0).className.match(/\bn([0-9]+)\b/)[1])-1; + return $("#colorpickerswatches li").index(jnode); +} + +function closeColorPicker(accept) +{ + if (accept) + { + var newColor = $("#mycolorpickerpreview").css("background-color"); + var parts = newColor.match(/^rgb\((\d+),\s*(\d+),\s*(\d+)\)$/); + // parts now should be ["rgb(0, 70, 255", "0", "70", "255"] + delete (parts[0]); + for (var i = 1; i <= 3; ++i) { + parts[i] = parseInt(parts[i]).toString(16); + if (parts[i].length == 1) parts[i] = '0' + parts[i]; + } + var newColor = "#" +parts.join(''); // "0070ff" + + myUserInfo.colorId = newColor; + pad.notifyChangeColor(newColor); + paduserlist.renderMyUserInfo(); + } + else + { + //pad.notifyChangeColor(previousColorId); + //paduserlist.renderMyUserInfo(); + } + + colorPickerOpen = false; + $("#mycolorpicker").fadeOut("fast"); +} + +function showColorPicker() +{ + previousColorId = myUserInfo.colorId; + + if (!colorPickerOpen) + { + var palette = pad.getColorPalette(); + + if (!colorPickerSetup) + { + var colorsList = $("#colorpickerswatches") + for (var i = 0; i < palette.length; i++) + { + + var li = $('<li>', { + style: 'background: ' + palette[i] + ';' + }); + + li.appendTo(colorsList); + + li.bind('click', function(event) + { + $("#colorpickerswatches li").removeClass('picked'); + $(event.target).addClass("picked"); + + var newColorId = getColorPickerSwatchIndex($("#colorpickerswatches .picked")); + pad.notifyChangeColor(newColorId); + }); + + } + + colorPickerSetup = true; + } + + $("#mycolorpicker").fadeIn(); + colorPickerOpen = true; + + $("#colorpickerswatches li").removeClass('picked'); + $($("#colorpickerswatches li")[myUserInfo.colorId]).addClass("picked"); //seems weird + } +} + +exports.paduserlist = paduserlist; diff --git a/src/static/js/pad_utils.js b/src/static/js/pad_utils.js new file mode 100644 index 00000000..83ee9aae --- /dev/null +++ b/src/static/js/pad_utils.js @@ -0,0 +1,537 @@ +/** + * This code is mostly from the old Etherpad. Please help us to comment this code. + * This helps other people to understand this code better and helps them to improve it. + * TL;DR COMMENTS ON THIS FILE ARE HIGHLY APPRECIATED + */ + +/** + * 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. + */ + +var Security = require('./security'); + +/** + * Generates a random String with the given length. Is needed to generate the Author, Group, readonly, session Ids + */ + +function randomString(len) +{ + var chars = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"; + var randomstring = ''; + len = len || 20 + for (var i = 0; i < len; i++) + { + var rnum = Math.floor(Math.random() * chars.length); + randomstring += chars.substring(rnum, rnum + 1); + } + return randomstring; +} + +function createCookie(name, value, days, path) +{ + if (days) + { + var date = new Date(); + date.setTime(date.getTime() + (days * 24 * 60 * 60 * 1000)); + var expires = "; expires=" + date.toGMTString(); + } + else var expires = ""; + + if(!path) + path = "/"; + + document.cookie = name + "=" + value + expires + "; path=" + path; +} + +function readCookie(name) +{ + var nameEQ = name + "="; + var ca = document.cookie.split(';'); + for (var i = 0; i < ca.length; i++) + { + var c = ca[i]; + while (c.charAt(0) == ' ') c = c.substring(1, c.length); + if (c.indexOf(nameEQ) == 0) return c.substring(nameEQ.length, c.length); + } + return null; +} + +var padutils = { + escapeHtml: function(x) + { + return Security.escapeHTML(String(x)); + }, + uniqueId: function() + { + var pad = require('./pad').pad; // Sidestep circular dependency + function encodeNum(n, width) + { + // returns string that is exactly 'width' chars, padding with zeros + // and taking rightmost digits + return (Array(width + 1).join('0') + Number(n).toString(35)).slice(-width); + } + return [pad.getClientIp(), encodeNum(+new Date, 7), encodeNum(Math.floor(Math.random() * 1e9), 4)].join('.'); + }, + uaDisplay: function(ua) + { + var m; + + function clean(a) + { + var maxlen = 16; + a = a.replace(/[^a-zA-Z0-9\.]/g, ''); + if (a.length > maxlen) + { + a = a.substr(0, maxlen); + } + return a; + } + + function checkver(name) + { + var m = ua.match(RegExp(name + '\\/([\\d\\.]+)')); + if (m && m.length > 1) + { + return clean(name + m[1]); + } + return null; + } + + // firefox + if (checkver('Firefox')) + { + return checkver('Firefox'); + } + + // misc browsers, including IE + m = ua.match(/compatible; ([^;]+);/); + if (m && m.length > 1) + { + return clean(m[1]); + } + + // iphone + if (ua.match(/\(iPhone;/)) + { + return 'iPhone'; + } + + // chrome + if (checkver('Chrome')) + { + return checkver('Chrome'); + } + + // safari + m = ua.match(/Safari\/[\d\.]+/); + if (m) + { + var v = '?'; + m = ua.match(/Version\/([\d\.]+)/); + if (m && m.length > 1) + { + v = m[1]; + } + return clean('Safari' + v); + } + + // everything else + var x = ua.split(' ')[0]; + return clean(x); + }, + // e.g. "Thu Jun 18 2009 13:09" + simpleDateTime: function(date) + { + var d = new Date(+date); // accept either number or date + var dayOfWeek = (['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'])[d.getDay()]; + var month = (['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'])[d.getMonth()]; + var dayOfMonth = d.getDate(); + var year = d.getFullYear(); + var hourmin = d.getHours() + ":" + ("0" + d.getMinutes()).slice(-2); + return dayOfWeek + ' ' + month + ' ' + dayOfMonth + ' ' + year + ' ' + hourmin; + }, + findURLs: function(text) + { + // copied from ACE + var _REGEX_WORDCHAR = /[\u0030-\u0039\u0041-\u005A\u0061-\u007A\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u00FF\u0100-\u1FFF\u3040-\u9FFF\uF900-\uFDFF\uFE70-\uFEFE\uFF10-\uFF19\uFF21-\uFF3A\uFF41-\uFF5A\uFF66-\uFFDC]/; + var _REGEX_URLCHAR = new RegExp('(' + /[-:@a-zA-Z0-9_.,~%+\/?=&#;()$]/.source + '|' + _REGEX_WORDCHAR.source + ')'); + var _REGEX_URL = new RegExp(/(?:(?:https?|s?ftp|ftps|file|smb|afp|nfs|(x-)?man|gopher|txmt):\/\/|mailto:)/.source + _REGEX_URLCHAR.source + '*(?![:.,;])' + _REGEX_URLCHAR.source, 'g'); + + // returns null if no URLs, or [[startIndex1, url1], [startIndex2, url2], ...] + + + function _findURLs(text) + { + _REGEX_URL.lastIndex = 0; + var urls = null; + var execResult; + while ((execResult = _REGEX_URL.exec(text))) + { + urls = (urls || []); + var startIndex = execResult.index; + var url = execResult[0]; + urls.push([startIndex, url]); + } + + return urls; + } + + return _findURLs(text); + }, + escapeHtmlWithClickableLinks: function(text, target) + { + var idx = 0; + var pieces = []; + var urls = padutils.findURLs(text); + + function advanceTo(i) + { + if (i > idx) + { + pieces.push(Security.escapeHTML(text.substring(idx, i))); + idx = i; + } + } + if (urls) + { + for (var j = 0; j < urls.length; j++) + { + var startIndex = urls[j][0]; + var href = urls[j][1]; + advanceTo(startIndex); + pieces.push('<a ', (target ? 'target="' + Security.escapeHTMLAttribute(target) + '" ' : ''), 'href="', Security.escapeHTMLAttribute(href), '">'); + advanceTo(startIndex + href.length); + pieces.push('</a>'); + } + } + advanceTo(text.length); + return pieces.join(''); + }, + bindEnterAndEscape: function(node, onEnter, onEscape) + { + + // Use keypress instead of keyup in bindEnterAndEscape + // Keyup event is fired on enter in IME (Input Method Editor), But + // keypress is not. So, I changed to use keypress instead of keyup. + // It is work on Windows (IE8, Chrome 6.0.472), CentOs (Firefox 3.0) and Mac OSX (Firefox 3.6.10, Chrome 6.0.472, Safari 5.0). + if (onEnter) + { + node.keypress(function(evt) + { + if (evt.which == 13) + { + onEnter(evt); + } + }); + } + + if (onEscape) + { + node.keydown(function(evt) + { + if (evt.which == 27) + { + onEscape(evt); + } + }); + } + }, + timediff: function(d) + { + var pad = require('./pad').pad; // Sidestep circular dependency + function format(n, word) + { + n = Math.round(n); + return ('' + n + ' ' + word + (n != 1 ? 's' : '') + ' ago'); + } + d = Math.max(0, (+(new Date) - (+d) - pad.clientTimeOffset) / 1000); + if (d < 60) + { + return format(d, 'second'); + } + d /= 60; + if (d < 60) + { + return format(d, 'minute'); + } + d /= 60; + if (d < 24) + { + return format(d, 'hour'); + } + d /= 24; + return format(d, 'day'); + }, + makeAnimationScheduler: function(funcToAnimateOneStep, stepTime, stepsAtOnce) + { + if (stepsAtOnce === undefined) + { + stepsAtOnce = 1; + } + + var animationTimer = null; + + function scheduleAnimation() + { + if (!animationTimer) + { + animationTimer = window.setTimeout(function() + { + animationTimer = null; + var n = stepsAtOnce; + var moreToDo = true; + while (moreToDo && n > 0) + { + moreToDo = funcToAnimateOneStep(); + n--; + } + if (moreToDo) + { + // more to do + scheduleAnimation(); + } + }, stepTime * stepsAtOnce); + } + } + return { + scheduleAnimation: scheduleAnimation + }; + }, + makeShowHideAnimator: function(funcToArriveAtState, initiallyShown, fps, totalMs) + { + var animationState = (initiallyShown ? 0 : -2); // -2 hidden, -1 to 0 fade in, 0 to 1 fade out + var animationFrameDelay = 1000 / fps; + var animationStep = animationFrameDelay / totalMs; + + var scheduleAnimation = padutils.makeAnimationScheduler(animateOneStep, animationFrameDelay).scheduleAnimation; + + function doShow() + { + animationState = -1; + funcToArriveAtState(animationState); + scheduleAnimation(); + } + + function doQuickShow() + { // start showing without losing any fade-in progress + if (animationState < -1) + { + animationState = -1; + } + else if (animationState <= 0) + { + animationState = animationState; + } + else + { + animationState = Math.max(-1, Math.min(0, -animationState)); + } + funcToArriveAtState(animationState); + scheduleAnimation(); + } + + function doHide() + { + if (animationState >= -1 && animationState <= 0) + { + animationState = 1e-6; + scheduleAnimation(); + } + } + + function animateOneStep() + { + if (animationState < -1 || animationState == 0) + { + return false; + } + else if (animationState < 0) + { + // animate show + animationState += animationStep; + if (animationState >= 0) + { + animationState = 0; + funcToArriveAtState(animationState); + return false; + } + else + { + funcToArriveAtState(animationState); + return true; + } + } + else if (animationState > 0) + { + // animate hide + animationState += animationStep; + if (animationState >= 1) + { + animationState = 1; + funcToArriveAtState(animationState); + animationState = -2; + return false; + } + else + { + funcToArriveAtState(animationState); + return true; + } + } + } + + return { + show: doShow, + hide: doHide, + quickShow: doQuickShow + }; + }, + _nextActionId: 1, + uncanceledActions: {}, + getCancellableAction: function(actionType, actionFunc) + { + var o = padutils.uncanceledActions[actionType]; + if (!o) + { + o = {}; + padutils.uncanceledActions[actionType] = o; + } + var actionId = (padutils._nextActionId++); + o[actionId] = true; + return function() + { + var p = padutils.uncanceledActions[actionType]; + if (p && p[actionId]) + { + actionFunc(); + } + }; + }, + cancelActions: function(actionType) + { + var o = padutils.uncanceledActions[actionType]; + if (o) + { + // clear it + delete padutils.uncanceledActions[actionType]; + } + }, + makeFieldLabeledWhenEmpty: function(field, labelText) + { + field = $(field); + + function clear() + { + field.addClass('editempty'); + field.val(labelText); + } + field.focus(function() + { + if (field.hasClass('editempty')) + { + field.val(''); + } + field.removeClass('editempty'); + }); + field.blur(function() + { + if (!field.val()) + { + clear(); + } + }); + return { + clear: clear + }; + }, + getCheckbox: function(node) + { + return $(node).is(':checked'); + }, + setCheckbox: function(node, value) + { + if (value) + { + $(node).attr('checked', 'checked'); + } + else + { + $(node).removeAttr('checked'); + } + }, + bindCheckboxChange: function(node, func) + { + $(node).bind("click change", func); + }, + encodeUserId: function(userId) + { + return userId.replace(/[^a-y0-9]/g, function(c) + { + if (c == ".") return "-"; + return 'z' + c.charCodeAt(0) + 'z'; + }); + }, + decodeUserId: function(encodedUserId) + { + return encodedUserId.replace(/[a-y0-9]+|-|z.+?z/g, function(cc) + { + if (cc == '-') return '.'; + else if (cc.charAt(0) == 'z') + { + return String.fromCharCode(Number(cc.slice(1, -1))); + } + else + { + return cc; + } + }); + } +}; + +var globalExceptionHandler = undefined; +function setupGlobalExceptionHandler() { + if (!globalExceptionHandler) { + globalExceptionHandler = function test (msg, url, linenumber) + { + var errorId = randomString(20); + if ($("#editorloadingbox").attr("display") != "none"){ + //show javascript errors to the user + $("#editorloadingbox").css("padding", "10px"); + $("#editorloadingbox").css("padding-top", "45px"); + $("#editorloadingbox").html("<div style='text-align:left;color:red;font-size:16px;'><b>An error occured</b><br>The error was reported with the following id: '" + errorId + "'<br><br><span style='color:black;font-weight:bold;font-size:16px'>Please send this error message to us: </span><div style='color:black;font-size:14px'>'" + + "ErrorId: " + errorId + "<br>UserAgent: " + navigator.userAgent + "<br>" + msg + " in " + url + " at line " + linenumber + "'</div></div>"); + } + + //send javascript errors to the server + var errObj = {errorInfo: JSON.stringify({errorId: errorId, msg: msg, url: url, linenumber: linenumber, userAgent: navigator.userAgent})}; + var loc = document.location; + var url = loc.protocol + "//" + loc.hostname + ":" + loc.port + "/" + loc.pathname.substr(1, loc.pathname.indexOf("/p/")) + "jserror"; + + $.post(url, errObj); + + return false; + }; + window.onerror = globalExceptionHandler; + } +} + +padutils.setupGlobalExceptionHandler = setupGlobalExceptionHandler; + +padutils.binarySearch = require('./ace2_common').binarySearch; + +exports.randomString = randomString; +exports.createCookie = createCookie; +exports.readCookie = readCookie; +exports.padutils = padutils; diff --git a/src/static/js/pluginfw/async.js b/src/static/js/pluginfw/async.js new file mode 100644 index 00000000..c862008a --- /dev/null +++ b/src/static/js/pluginfw/async.js @@ -0,0 +1,690 @@ +/*global setTimeout: false, console: false */ +(function () { + + var async = {}; + + // global on the server, window in the browser + var root = this, + previous_async = root.async; + + if (typeof module !== 'undefined' && module.exports) { + module.exports = async; + } + else { + root.async = async; + } + + async.noConflict = function () { + root.async = previous_async; + return async; + }; + + //// cross-browser compatiblity functions //// + + var _forEach = function (arr, iterator) { + if (arr.forEach) { + return arr.forEach(iterator); + } + for (var i = 0; i < arr.length; i += 1) { + iterator(arr[i], i, arr); + } + }; + + var _map = function (arr, iterator) { + if (arr.map) { + return arr.map(iterator); + } + var results = []; + _forEach(arr, function (x, i, a) { + results.push(iterator(x, i, a)); + }); + return results; + }; + + var _reduce = function (arr, iterator, memo) { + if (arr.reduce) { + return arr.reduce(iterator, memo); + } + _forEach(arr, function (x, i, a) { + memo = iterator(memo, x, i, a); + }); + return memo; + }; + + var _keys = function (obj) { + if (Object.keys) { + return Object.keys(obj); + } + var keys = []; + for (var k in obj) { + if (obj.hasOwnProperty(k)) { + keys.push(k); + } + } + return keys; + }; + + var _indexOf = function (arr, item) { + if (arr.indexOf) { + return arr.indexOf(item); + } + for (var i = 0; i < arr.length; i += 1) { + if (arr[i] === item) { + return i; + } + } + return -1; + }; + + //// exported async module functions //// + + //// nextTick implementation with browser-compatible fallback //// + if (typeof process === 'undefined' || !(process.nextTick)) { + async.nextTick = function (fn) { + setTimeout(fn, 0); + }; + } + else { + async.nextTick = process.nextTick; + } + + async.forEach = function (arr, iterator, callback) { + if (!arr.length) { + return callback(); + } + var completed = 0; + _forEach(arr, function (x) { + iterator(x, function (err) { + if (err) { + callback(err); + callback = function () {}; + } + else { + completed += 1; + if (completed === arr.length) { + callback(); + } + } + }); + }); + }; + + async.forEachSeries = function (arr, iterator, callback) { + if (!arr.length) { + return callback(); + } + var completed = 0; + var iterate = function () { + iterator(arr[completed], function (err) { + if (err) { + callback(err); + callback = function () {}; + } + else { + completed += 1; + if (completed === arr.length) { + callback(); + } + else { + iterate(); + } + } + }); + }; + iterate(); + }; + + async.forEachLimit = function (arr, limit, iterator, callback) { + if (!arr.length || limit <= 0) { + return callback(); + } + var completed = 0; + var started = 0; + var running = 0; + + (function replenish () { + if (completed === arr.length) { + return callback(); + } + + while (running < limit && started < arr.length) { + iterator(arr[started], function (err) { + if (err) { + callback(err); + callback = function () {}; + } + else { + completed += 1; + running -= 1; + if (completed === arr.length) { + callback(); + } + else { + replenish(); + } + } + }); + started += 1; + running += 1; + } + })(); + }; + + + var doParallel = function (fn) { + return function () { + var args = Array.prototype.slice.call(arguments); + return fn.apply(null, [async.forEach].concat(args)); + }; + }; + var doSeries = function (fn) { + return function () { + var args = Array.prototype.slice.call(arguments); + return fn.apply(null, [async.forEachSeries].concat(args)); + }; + }; + + + var _asyncMap = function (eachfn, arr, iterator, callback) { + var results = []; + arr = _map(arr, function (x, i) { + return {index: i, value: x}; + }); + eachfn(arr, function (x, callback) { + iterator(x.value, function (err, v) { + results[x.index] = v; + callback(err); + }); + }, function (err) { + callback(err, results); + }); + }; + async.map = doParallel(_asyncMap); + async.mapSeries = doSeries(_asyncMap); + + + // reduce only has a series version, as doing reduce in parallel won't + // work in many situations. + async.reduce = function (arr, memo, iterator, callback) { + async.forEachSeries(arr, function (x, callback) { + iterator(memo, x, function (err, v) { + memo = v; + callback(err); + }); + }, function (err) { + callback(err, memo); + }); + }; + // inject alias + async.inject = async.reduce; + // foldl alias + async.foldl = async.reduce; + + async.reduceRight = function (arr, memo, iterator, callback) { + var reversed = _map(arr, function (x) { + return x; + }).reverse(); + async.reduce(reversed, memo, iterator, callback); + }; + // foldr alias + async.foldr = async.reduceRight; + + var _filter = function (eachfn, arr, iterator, callback) { + var results = []; + arr = _map(arr, function (x, i) { + return {index: i, value: x}; + }); + eachfn(arr, function (x, callback) { + iterator(x.value, function (v) { + if (v) { + results.push(x); + } + callback(); + }); + }, function (err) { + callback(_map(results.sort(function (a, b) { + return a.index - b.index; + }), function (x) { + return x.value; + })); + }); + }; + async.filter = doParallel(_filter); + async.filterSeries = doSeries(_filter); + // select alias + async.select = async.filter; + async.selectSeries = async.filterSeries; + + var _reject = function (eachfn, arr, iterator, callback) { + var results = []; + arr = _map(arr, function (x, i) { + return {index: i, value: x}; + }); + eachfn(arr, function (x, callback) { + iterator(x.value, function (v) { + if (!v) { + results.push(x); + } + callback(); + }); + }, function (err) { + callback(_map(results.sort(function (a, b) { + return a.index - b.index; + }), function (x) { + return x.value; + })); + }); + }; + async.reject = doParallel(_reject); + async.rejectSeries = doSeries(_reject); + + var _detect = function (eachfn, arr, iterator, main_callback) { + eachfn(arr, function (x, callback) { + iterator(x, function (result) { + if (result) { + main_callback(x); + main_callback = function () {}; + } + else { + callback(); + } + }); + }, function (err) { + main_callback(); + }); + }; + async.detect = doParallel(_detect); + async.detectSeries = doSeries(_detect); + + async.some = function (arr, iterator, main_callback) { + async.forEach(arr, function (x, callback) { + iterator(x, function (v) { + if (v) { + main_callback(true); + main_callback = function () {}; + } + callback(); + }); + }, function (err) { + main_callback(false); + }); + }; + // any alias + async.any = async.some; + + async.every = function (arr, iterator, main_callback) { + async.forEach(arr, function (x, callback) { + iterator(x, function (v) { + if (!v) { + main_callback(false); + main_callback = function () {}; + } + callback(); + }); + }, function (err) { + main_callback(true); + }); + }; + // all alias + async.all = async.every; + + async.sortBy = function (arr, iterator, callback) { + async.map(arr, function (x, callback) { + iterator(x, function (err, criteria) { + if (err) { + callback(err); + } + else { + callback(null, {value: x, criteria: criteria}); + } + }); + }, function (err, results) { + if (err) { + return callback(err); + } + else { + var fn = function (left, right) { + var a = left.criteria, b = right.criteria; + return a < b ? -1 : a > b ? 1 : 0; + }; + callback(null, _map(results.sort(fn), function (x) { + return x.value; + })); + } + }); + }; + + async.auto = function (tasks, callback) { + callback = callback || function () {}; + var keys = _keys(tasks); + if (!keys.length) { + return callback(null); + } + + var results = {}; + + var listeners = []; + var addListener = function (fn) { + listeners.unshift(fn); + }; + var removeListener = function (fn) { + for (var i = 0; i < listeners.length; i += 1) { + if (listeners[i] === fn) { + listeners.splice(i, 1); + return; + } + } + }; + var taskComplete = function () { + _forEach(listeners, function (fn) { + fn(); + }); + }; + + addListener(function () { + if (_keys(results).length === keys.length) { + callback(null, results); + } + }); + + _forEach(keys, function (k) { + var task = (tasks[k] instanceof Function) ? [tasks[k]]: tasks[k]; + var taskCallback = function (err) { + if (err) { + callback(err); + // stop subsequent errors hitting callback multiple times + callback = function () {}; + } + else { + var args = Array.prototype.slice.call(arguments, 1); + if (args.length <= 1) { + args = args[0]; + } + results[k] = args; + taskComplete(); + } + }; + var requires = task.slice(0, Math.abs(task.length - 1)) || []; + var ready = function () { + return _reduce(requires, function (a, x) { + return (a && results.hasOwnProperty(x)); + }, true); + }; + if (ready()) { + task[task.length - 1](taskCallback, results); + } + else { + var listener = function () { + if (ready()) { + removeListener(listener); + task[task.length - 1](taskCallback, results); + } + }; + addListener(listener); + } + }); + }; + + async.waterfall = function (tasks, callback) { + if (!tasks.length) { + return callback(); + } + callback = callback || function () {}; + var wrapIterator = function (iterator) { + return function (err) { + if (err) { + callback(err); + callback = function () {}; + } + else { + var args = Array.prototype.slice.call(arguments, 1); + var next = iterator.next(); + if (next) { + args.push(wrapIterator(next)); + } + else { + args.push(callback); + } + async.nextTick(function () { + iterator.apply(null, args); + }); + } + }; + }; + wrapIterator(async.iterator(tasks))(); + }; + + async.parallel = function (tasks, callback) { + callback = callback || function () {}; + if (tasks.constructor === Array) { + async.map(tasks, function (fn, callback) { + if (fn) { + fn(function (err) { + var args = Array.prototype.slice.call(arguments, 1); + if (args.length <= 1) { + args = args[0]; + } + callback.call(null, err, args); + }); + } + }, callback); + } + else { + var results = {}; + async.forEach(_keys(tasks), function (k, callback) { + tasks[k](function (err) { + var args = Array.prototype.slice.call(arguments, 1); + if (args.length <= 1) { + args = args[0]; + } + results[k] = args; + callback(err); + }); + }, function (err) { + callback(err, results); + }); + } + }; + + async.series = function (tasks, callback) { + callback = callback || function () {}; + if (tasks.constructor === Array) { + async.mapSeries(tasks, function (fn, callback) { + if (fn) { + fn(function (err) { + var args = Array.prototype.slice.call(arguments, 1); + if (args.length <= 1) { + args = args[0]; + } + callback.call(null, err, args); + }); + } + }, callback); + } + else { + var results = {}; + async.forEachSeries(_keys(tasks), function (k, callback) { + tasks[k](function (err) { + var args = Array.prototype.slice.call(arguments, 1); + if (args.length <= 1) { + args = args[0]; + } + results[k] = args; + callback(err); + }); + }, function (err) { + callback(err, results); + }); + } + }; + + async.iterator = function (tasks) { + var makeCallback = function (index) { + var fn = function () { + if (tasks.length) { + tasks[index].apply(null, arguments); + } + return fn.next(); + }; + fn.next = function () { + return (index < tasks.length - 1) ? makeCallback(index + 1): null; + }; + return fn; + }; + return makeCallback(0); + }; + + async.apply = function (fn) { + var args = Array.prototype.slice.call(arguments, 1); + return function () { + return fn.apply( + null, args.concat(Array.prototype.slice.call(arguments)) + ); + }; + }; + + var _concat = function (eachfn, arr, fn, callback) { + var r = []; + eachfn(arr, function (x, cb) { + fn(x, function (err, y) { + r = r.concat(y || []); + cb(err); + }); + }, function (err) { + callback(err, r); + }); + }; + async.concat = doParallel(_concat); + async.concatSeries = doSeries(_concat); + + async.whilst = function (test, iterator, callback) { + if (test()) { + iterator(function (err) { + if (err) { + return callback(err); + } + async.whilst(test, iterator, callback); + }); + } + else { + callback(); + } + }; + + async.until = function (test, iterator, callback) { + if (!test()) { + iterator(function (err) { + if (err) { + return callback(err); + } + async.until(test, iterator, callback); + }); + } + else { + callback(); + } + }; + + async.queue = function (worker, concurrency) { + var workers = 0; + var q = { + tasks: [], + concurrency: concurrency, + saturated: null, + empty: null, + drain: null, + push: function (data, callback) { + q.tasks.push({data: data, callback: callback}); + if(q.saturated && q.tasks.length == concurrency) q.saturated(); + async.nextTick(q.process); + }, + process: function () { + if (workers < q.concurrency && q.tasks.length) { + var task = q.tasks.shift(); + if(q.empty && q.tasks.length == 0) q.empty(); + workers += 1; + worker(task.data, function () { + workers -= 1; + if (task.callback) { + task.callback.apply(task, arguments); + } + if(q.drain && q.tasks.length + workers == 0) q.drain(); + q.process(); + }); + } + }, + length: function () { + return q.tasks.length; + }, + running: function () { + return workers; + } + }; + return q; + }; + + var _console_fn = function (name) { + return function (fn) { + var args = Array.prototype.slice.call(arguments, 1); + fn.apply(null, args.concat([function (err) { + var args = Array.prototype.slice.call(arguments, 1); + if (typeof console !== 'undefined') { + if (err) { + if (console.error) { + console.error(err); + } + } + else if (console[name]) { + _forEach(args, function (x) { + console[name](x); + }); + } + } + }])); + }; + }; + async.log = _console_fn('log'); + async.dir = _console_fn('dir'); + /*async.info = _console_fn('info'); + async.warn = _console_fn('warn'); + async.error = _console_fn('error');*/ + + async.memoize = function (fn, hasher) { + var memo = {}; + var queues = {}; + hasher = hasher || function (x) { + return x; + }; + var memoized = function () { + var args = Array.prototype.slice.call(arguments); + var callback = args.pop(); + var key = hasher.apply(null, args); + if (key in memo) { + callback.apply(null, memo[key]); + } + else if (key in queues) { + queues[key].push(callback); + } + else { + queues[key] = [callback]; + fn.apply(null, args.concat([function () { + memo[key] = arguments; + var q = queues[key]; + delete queues[key]; + for (var i = 0, l = q.length; i < l; i++) { + q[i].apply(null, arguments); + } + }])); + } + }; + memoized.unmemoized = fn; + return memoized; + }; + + async.unmemoize = function (fn) { + return function () { + return (fn.unmemoized || fn).apply(null, arguments); + } + }; + +}()); diff --git a/src/static/js/pluginfw/hooks.js b/src/static/js/pluginfw/hooks.js new file mode 100644 index 00000000..1b09a6e5 --- /dev/null +++ b/src/static/js/pluginfw/hooks.js @@ -0,0 +1,75 @@ +var plugins = require("ep_etherpad-lite/static/js/pluginfw/plugins"); + +/* FIXME: Ugly hack, in the future, use same code for server & client */ +if (plugins.isClient) { + var async = require("ep_etherpad-lite/static/js/pluginfw/async"); +} else { + var async = require("async"); +} + +var hookCallWrapper = function (hook, hook_name, args, cb) { + if (cb === undefined) cb = function (x) { return x; }; + try { + return hook.hook_fn(hook_name, args, cb); + } catch (ex) { + console.error([hook_name, hook.part.full_name, ex.stack || ex]); + } +} + + +/* Don't use Array.concat as it flatterns arrays within the array */ +exports.flatten = function (lst) { + var res = []; + if (lst != undefined && lst != null) { + for (var i = 0; i < lst.length; i++) { + if (lst[i] != undefined && lst[i] != null) { + for (var j = 0; j < lst[i].length; j++) { + res.push(lst[i][j]); + } + } + } + } + return res; +} + +exports.callAll = function (hook_name, args) { + if (plugins.hooks[hook_name] === undefined) return []; + return exports.flatten(plugins.hooks[hook_name].map(function (hook) { + return hookCallWrapper(hook, hook_name, args); + })); +} + +exports.aCallAll = function (hook_name, args, cb) { + if (plugins.hooks[hook_name] === undefined) cb([]); + async.map( + plugins.hooks[hook_name], + function (hook, cb) { + hookCallWrapper(hook, hook_name, args, function (res) { cb(null, res); }); + }, + function (err, res) { + cb(exports.flatten(res)); + } + ); +} + +exports.callFirst = function (hook_name, args) { + if (plugins.hooks[hook_name][0] === undefined) return []; + return exports.flatten(hookCallWrapper(plugins.hooks[hook_name][0], hook_name, args)); +} + +exports.aCallFirst = function (hook_name, args, cb) { + if (plugins.hooks[hook_name][0] === undefined) cb([]); + hookCallWrapper(plugins.hooks[hook_name][0], hook_name, args, function (res) { cb(exports.flatten(res)); }); +} + +exports.callAllStr = function(hook_name, args, sep, pre, post) { + if (sep == undefined) sep = ''; + if (pre == undefined) pre = ''; + if (post == undefined) post = ''; + var newCallhooks = []; + var callhooks = exports.callAll(hook_name, args); + for (var i = 0, ii = callhooks.length; i < ii; i++) { + newCallhooks[i] = pre + callhooks[i] + post; + } + return newCallhooks.join(sep || ""); +} diff --git a/src/static/js/pluginfw/plugins.js b/src/static/js/pluginfw/plugins.js new file mode 100644 index 00000000..c5c21903 --- /dev/null +++ b/src/static/js/pluginfw/plugins.js @@ -0,0 +1,179 @@ +exports.isClient = typeof global != "object"; + +if (!exports.isClient) { + var npm = require("npm/lib/npm.js"); + var readInstalled = require("npm/lib/utils/read-installed.js"); + var relativize = require("npm/lib/utils/relativize.js"); + var readJson = require("npm/lib/utils/read-json.js"); + var path = require("path"); + var async = require("async"); + var fs = require("fs"); + var tsort = require("./tsort"); + var util = require("util"); +} + +exports.prefix = 'ep_'; +exports.loaded = false; +exports.plugins = {}; +exports.parts = []; +exports.hooks = {}; + +exports.ensure = function (cb) { + if (!exports.loaded) + exports.update(cb); + else + cb(); +} + +exports.formatPlugins = function () { + return Object.keys(exports.plugins).join(", "); +} + +exports.formatParts = function () { + return exports.parts.map(function (part) { return part.full_name; }).join("\n"); +} + +exports.formatHooks = function () { + var res = []; + Object.keys(exports.hooks).forEach(function (hook_name) { + exports.hooks[hook_name].forEach(function (hook) { + res.push(hook.hook_name + ": " + hook.hook_fn_name + " from " + hook.part.full_name); + }); + }); + return res.join("\n"); +} + +exports.loadFn = function (path) { + var x = path.split(":"); + var fn = require(x[0]); + x[1].split(".").forEach(function (name) { + fn = fn[name]; + }); + return fn; +} + +exports.extractHooks = function (parts, hook_set_name) { + var hooks = {}; + parts.forEach(function (part) { + Object.keys(part[hook_set_name] || {}).forEach(function (hook_name) { + if (hooks[hook_name] === undefined) hooks[hook_name] = []; + var hook_fn_name = part[hook_set_name][hook_name]; + var hook_fn = exports.loadFn(part[hook_set_name][hook_name]); + if (hook_fn) { + hooks[hook_name].push({"hook_name": hook_name, "hook_fn": hook_fn, "hook_fn_name": hook_fn_name, "part": part}); + } else { + console.error("Unable to load hook function for " + part.full_name + " for hook " + hook_name + ": " + part.hooks[hook_name]); + } + }); + }); + return hooks; +} + + +if (exports.isClient) { + exports.update = function (cb) { + jQuery.getJSON('/pluginfw/plugin-definitions.json', function(data) { + exports.plugins = data.plugins; + exports.parts = data.parts; + exports.hooks = exports.extractHooks(exports.parts, "client_hooks"); + exports.loaded = true; + cb(); + }); + } +} else { + +exports.update = function (cb) { + exports.getPackages(function (er, packages) { + var parts = []; + var plugins = {}; + // Load plugin metadata ep.json + async.forEach( + Object.keys(packages), + function (plugin_name, cb) { + exports.loadPlugin(packages, plugin_name, plugins, parts, cb); + }, + function (err) { + exports.plugins = plugins; + exports.parts = exports.sortParts(parts); + exports.hooks = exports.extractHooks(exports.parts, "hooks"); + exports.loaded = true; + cb(err); + } + ); + }); +} + +exports.getPackages = function (cb) { + // Load list of installed NPM packages, flatten it to a list, and filter out only packages with names that + var dir = path.resolve(npm.dir, '..'); + readInstalled(dir, function (er, data) { + if (er) cb(er, null); + var packages = {}; + function flatten(deps) { + Object.keys(deps).forEach(function (name) { + if (name.indexOf(exports.prefix) == 0) { + packages[name] = deps[name]; + } + if (deps[name].dependencies !== undefined) + flatten(deps[name].dependencies); + delete deps[name].dependencies; + }); + } + flatten([data]); + cb(null, packages); + }); +} + +exports.loadPlugin = function (packages, plugin_name, plugins, parts, cb) { + var plugin_path = path.resolve(packages[plugin_name].path, "ep.json"); + fs.readFile( + plugin_path, + function (er, data) { + if (er) { + console.error("Unable to load plugin definition file " + plugin_path); + return cb(); + } + try { + var plugin = JSON.parse(data); + plugin.package = packages[plugin_name]; + plugins[plugin_name] = plugin; + plugin.parts.forEach(function (part) { + part.plugin = plugin_name; + part.full_name = plugin_name + "/" + part.name; + parts[part.full_name] = part; + }); + } catch (ex) { + console.error("Unable to parse plugin definition file " + plugin_path + ": " + ex.toString()); + } + cb(); + } + ); +} + +exports.partsToParentChildList = function (parts) { + var res = []; + Object.keys(parts).forEach(function (name) { + (parts[name].post || []).forEach(function (child_name) { + res.push([name, child_name]); + }); + (parts[name].pre || []).forEach(function (parent_name) { + res.push([parent_name, name]); + }); + if (!parts[name].pre && !parts[name].post) { + res.push([name, ":" + name]); // Include apps with no dependency info + } + }); + return res; +} + +exports.sortParts = function(parts) { + return tsort( + exports.partsToParentChildList(parts) + ).filter( + function (name) { return parts[name] !== undefined; } + ).map( + function (name) { return parts[name]; } + ); +}; + +}
\ No newline at end of file diff --git a/src/static/js/pluginfw/tsort.js b/src/static/js/pluginfw/tsort.js new file mode 100644 index 00000000..6591c51c --- /dev/null +++ b/src/static/js/pluginfw/tsort.js @@ -0,0 +1,112 @@ +/** + * general topological sort + * from https://gist.github.com/1232505 + * @author SHIN Suzuki (shinout310@gmail.com) + * @param Array<Array> edges : list of edges. each edge forms Array<ID,ID> e.g. [12 , 3] + * + * @returns Array : topological sorted list of IDs + **/ + +function tsort(edges) { + var nodes = {}, // hash: stringified id of the node => { id: id, afters: lisf of ids } + sorted = [], // sorted list of IDs ( returned value ) + visited = {}; // hash: id of already visited node => true + + var Node = function(id) { + this.id = id; + this.afters = []; + } + + // 1. build data structures + edges.forEach(function(v) { + var from = v[0], to = v[1]; + if (!nodes[from]) nodes[from] = new Node(from); + if (!nodes[to]) nodes[to] = new Node(to); + nodes[from].afters.push(to); + }); + + // 2. topological sort + Object.keys(nodes).forEach(function visit(idstr, ancestors) { + var node = nodes[idstr], + id = node.id; + + // if already exists, do nothing + if (visited[idstr]) return; + + if (!Array.isArray(ancestors)) ancestors = []; + + ancestors.push(id); + + visited[idstr] = true; + + node.afters.forEach(function(afterID) { + if (ancestors.indexOf(afterID) >= 0) // if already in ancestors, a closed chain exists. + throw new Error('closed chain : ' + afterID + ' is in ' + id); + + visit(afterID.toString(), ancestors.map(function(v) { return v })); // recursive call + }); + + sorted.unshift(id); + }); + + return sorted; +} + +/** + * TEST + **/ +function tsortTest() { + + // example 1: success + var edges = [ + [1, 2], + [1, 3], + [2, 4], + [3, 4] + ]; + + var sorted = tsort(edges); + console.log(sorted); + + // example 2: failure ( A > B > C > A ) + edges = [ + ['A', 'B'], + ['B', 'C'], + ['C', 'A'] + ]; + + try { + sorted = tsort(edges); + } + catch (e) { + console.log(e.message); + } + + // example 3: generate random edges + var max = 100, iteration = 30; + function randomInt(max) { + return Math.floor(Math.random() * max) + 1; + } + + edges = (function() { + var ret = [], i = 0; + while (i++ < iteration) ret.push( [randomInt(max), randomInt(max)] ); + return ret; + })(); + + try { + sorted = tsort(edges); + console.log("succeeded", sorted); + } + catch (e) { + console.log("failed", e.message); + } + +} + + +// for node.js +if (typeof exports == 'object' && exports === this) { + module.exports = tsort; + if (process.argv[1] === __filename) tsortTest(); +} diff --git a/src/static/js/prefixfree.js b/src/static/js/prefixfree.js new file mode 100644 index 00000000..b5b23466 --- /dev/null +++ b/src/static/js/prefixfree.js @@ -0,0 +1,419 @@ +/** + * StyleFix 1.0.2 + * @author Lea Verou + * MIT license + */ + +(function(){ + +if(!window.addEventListener) { + return; +} + +var self = window.StyleFix = { + link: function(link) { + try { + // Ignore stylesheets with data-noprefix attribute as well as alternate stylesheets + if(link.rel !== 'stylesheet' || link.hasAttribute('data-noprefix')) { + return; + } + } + catch(e) { + return; + } + + var url = link.href || link.getAttribute('data-href'), + base = url.replace(/[^\/]+$/, ''), + parent = link.parentNode, + xhr = new XMLHttpRequest(); + + xhr.open('GET', url); + + xhr.onreadystatechange = function() { + if(xhr.readyState === 4) { + var css = xhr.responseText; + + if(css && link.parentNode) { + css = self.fix(css, true, link); + + // Convert relative URLs to absolute, if needed + if(base) { + css = css.replace(/url\(('?|"?)(.+?)\1\)/gi, function($0, quote, url) { + if(!/^([a-z]{3,10}:|\/|#)/i.test(url)) { // If url not absolute & not a hash + // May contain sequences like /../ and /./ but those DO work + return 'url("' + base + url + '")'; + } + + return $0; + }); + + // behavior URLs shoudn’t be converted (Issue #19) + css = css.replace(RegExp('\\b(behavior:\\s*?url\\(\'?"?)' + base, 'gi'), '$1'); + } + + var style = document.createElement('style'); + style.textContent = css; + style.media = link.media; + style.disabled = link.disabled; + style.setAttribute('data-href', link.getAttribute('href')); + + parent.insertBefore(style, link); + parent.removeChild(link); + } + } + }; + + xhr.send(null); + + link.setAttribute('data-inprogress', ''); + }, + + styleElement: function(style) { + var disabled = style.disabled; + + style.textContent = self.fix(style.textContent, true, style); + + style.disabled = disabled; + }, + + styleAttribute: function(element) { + var css = element.getAttribute('style'); + + css = self.fix(css, false, element); + + element.setAttribute('style', css); + }, + + process: function() { + // Linked stylesheets + $('link[rel="stylesheet"]:not([data-inprogress])').forEach(StyleFix.link); + + // Inline stylesheets + $('style').forEach(StyleFix.styleElement); + + // Inline styles + $('[style]').forEach(StyleFix.styleAttribute); + }, + + register: function(fixer, index) { + (self.fixers = self.fixers || []) + .splice(index === undefined? self.fixers.length : index, 0, fixer); + }, + + fix: function(css, raw) { + for(var i=0; i<self.fixers.length; i++) { + css = self.fixers[i](css, raw) || css; + } + + return css; + }, + + camelCase: function(str) { + return str.replace(/-([a-z])/g, function($0, $1) { return $1.toUpperCase(); }).replace('-',''); + }, + + deCamelCase: function(str) { + return str.replace(/[A-Z]/g, function($0) { return '-' + $0.toLowerCase() }); + } +}; + +/************************************** + * Process styles + **************************************/ +(function(){ + setTimeout(function(){ + $('link[rel="stylesheet"]').forEach(StyleFix.link); + }, 10); + + document.addEventListener('DOMContentLoaded', StyleFix.process, false); +})(); + +function $(expr, con) { + return [].slice.call((con || document).querySelectorAll(expr)); +} + +})(); + +/** + * PrefixFree 1.0.4 + * @author Lea Verou + * MIT license + */ +(function(root, undefined){ + +if(!window.StyleFix || !window.getComputedStyle) { + return; +} + +var self = window.PrefixFree = { + prefixCSS: function(css, raw) { + var prefix = self.prefix; + + function fix(what, before, after, replacement) { + what = self[what]; + + if(what.length) { + var regex = RegExp(before + '(' + what.join('|') + ')' + after, 'gi'); + + css = css.replace(regex, replacement); + } + } + + fix('functions', '(\\s|:|,)', '\\s*\\(', '$1' + prefix + '$2('); + fix('keywords', '(\\s|:)', '(\\s|;|\\}|$)', '$1' + prefix + '$2$3'); + fix('properties', '(^|\\{|\\s|;)', '\\s*:', '$1' + prefix + '$2:'); + + // Prefix properties *inside* values (issue #8) + if (self.properties.length) { + var regex = RegExp('\\b(' + self.properties.join('|') + ')(?!:)', 'gi'); + + fix('valueProperties', '\\b', ':(.+?);', function($0) { + return $0.replace(regex, prefix + "$1") + }); + } + + if(raw) { + fix('selectors', '', '\\b', self.prefixSelector); + fix('atrules', '@', '\\b', '@' + prefix + '$1'); + } + + // Fix double prefixing + css = css.replace(RegExp('-' + prefix, 'g'), '-'); + + return css; + }, + + // Warning: prefixXXX functions prefix no matter what, even if the XXX is supported prefix-less + prefixSelector: function(selector) { + return selector.replace(/^:{1,2}/, function($0) { return $0 + self.prefix }) + }, + + prefixProperty: function(property, camelCase) { + var prefixed = self.prefix + property; + + return camelCase? StyleFix.camelCase(prefixed) : prefixed; + } +}; + +/************************************** + * Properties + **************************************/ +(function() { + var prefixes = {}, + properties = [], + shorthands = {}, + style = getComputedStyle(document.documentElement, null), + dummy = document.createElement('div').style; + + // Why are we doing this instead of iterating over properties in a .style object? Cause Webkit won't iterate over those. + var iterate = function(property) { + if(property.charAt(0) === '-') { + properties.push(property); + + var parts = property.split('-'), + prefix = parts[1]; + + // Count prefix uses + prefixes[prefix] = ++prefixes[prefix] || 1; + + // This helps determining shorthands + while(parts.length > 3) { + parts.pop(); + + var shorthand = parts.join('-'); + + if(supported(shorthand) && properties.indexOf(shorthand) === -1) { + properties.push(shorthand); + } + } + } + }, + supported = function(property) { + return StyleFix.camelCase(property) in dummy; + } + + // Some browsers have numerical indices for the properties, some don't + if(style.length > 0) { + for(var i=0; i<style.length; i++) { + iterate(style[i]) + } + } + else { + for(var property in style) { + iterate(StyleFix.deCamelCase(property)); + } + } + + // Find most frequently used prefix + var highest = {uses:0}; + for(var prefix in prefixes) { + var uses = prefixes[prefix]; + + if(highest.uses < uses) { + highest = {prefix: prefix, uses: uses}; + } + } + + self.prefix = '-' + highest.prefix + '-'; + self.Prefix = StyleFix.camelCase(self.prefix); + + self.properties = []; + + // Get properties ONLY supported with a prefix + for(var i=0; i<properties.length; i++) { + var property = properties[i]; + + if(property.indexOf(self.prefix) === 0) { // we might have multiple prefixes, like Opera + var unprefixed = property.slice(self.prefix.length); + + if(!supported(unprefixed)) { + self.properties.push(unprefixed); + } + } + } + + // IE fix + if(self.Prefix == 'Ms' + && !('transform' in dummy) + && !('MsTransform' in dummy) + && ('msTransform' in dummy)) { + self.properties.push('transform', 'transform-origin'); + } + + self.properties.sort(); +})(); + +/************************************** + * Values + **************************************/ +(function() { +// Values that might need prefixing +var functions = { + 'linear-gradient': { + property: 'backgroundImage', + params: 'red, teal' + }, + 'calc': { + property: 'width', + params: '1px + 5%' + }, + 'element': { + property: 'backgroundImage', + params: '#foo' + } +}; + + +functions['repeating-linear-gradient'] = +functions['repeating-radial-gradient'] = +functions['radial-gradient'] = +functions['linear-gradient']; + +var keywords = { + 'initial': 'color', + 'zoom-in': 'cursor', + 'zoom-out': 'cursor', + 'box': 'display', + 'flexbox': 'display', + 'inline-flexbox': 'display' +}; + +self.functions = []; +self.keywords = []; + +var style = document.createElement('div').style; + +function supported(value, property) { + style[property] = ''; + style[property] = value; + + return !!style[property]; +} + +for (var func in functions) { + var test = functions[func], + property = test.property, + value = func + '(' + test.params + ')'; + + if (!supported(value, property) + && supported(self.prefix + value, property)) { + // It's supported, but with a prefix + self.functions.push(func); + } +} + +for (var keyword in keywords) { + var property = keywords[keyword]; + + if (!supported(keyword, property) + && supported(self.prefix + keyword, property)) { + // It's supported, but with a prefix + self.keywords.push(keyword); + } +} + +})(); + +/************************************** + * Selectors and @-rules + **************************************/ +(function() { + +var +selectors = { + ':read-only': null, + ':read-write': null, + ':any-link': null, + '::selection': null +}, + +atrules = { + 'keyframes': 'name', + 'viewport': null, + 'document': 'regexp(".")' +}; + +self.selectors = []; +self.atrules = []; + +var style = root.appendChild(document.createElement('style')); + +function supported(selector) { + style.textContent = selector + '{}'; // Safari 4 has issues with style.innerHTML + + return !!style.sheet.cssRules.length; +} + +for(var selector in selectors) { + var test = selector + (selectors[selector]? '(' + selectors[selector] + ')' : ''); + + if(!supported(test) && supported(self.prefixSelector(test))) { + self.selectors.push(selector); + } +} + +for(var atrule in atrules) { + var test = atrule + ' ' + (atrules[atrule] || ''); + + if(!supported('@' + test) && supported('@' + self.prefix + test)) { + self.atrules.push(atrule); + } +} + +root.removeChild(style); + +})(); + +// Properties that accept properties as their value +self.valueProperties = [ + 'transition', + 'transition-property' +] + +// Add class for current prefix +root.className += ' ' + self.prefix; + +StyleFix.register(self.prefixCSS); + + +})(document.documentElement); diff --git a/src/static/js/security.js b/src/static/js/security.js new file mode 100644 index 00000000..6f42d051 --- /dev/null +++ b/src/static/js/security.js @@ -0,0 +1,54 @@ +/** + * 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. + */ + +var HTML_ENTITY_MAP = { + '&': '&' +, '<': '<' +, '>': '>' +, '"': '"' +, "'": ''' +, '/': '/' +}; + +// OSWASP Guidlines: &, <, >, ", ' plus forward slash. +var HTML_CHARACTERS_EXPRESSION = /[&"'<>\/]/g; +function escapeHTML(text) { + return text && text.replace(HTML_CHARACTERS_EXPRESSION, function (c) { + return HTML_ENTITY_MAP[c] || c; + }); +} + +// OSWASP Guidlines: escape all non alphanumeric characters in ASCII space. +var HTML_ATTRIBUTE_CHARACTERS_EXPRESSION = + /[\x00-\x2F\x3A-\x40\5B-\x60\x7B-\xFF]/g; +function escapeHTMLAttribute(text) { + return text && text.replace(HTML_ATTRIBUTE_CHARACTERS_EXPRESSION, function (c) { + return "&#x" + ('00' + c.charCodeAt(0).toString(16)).slice(-2) + ";"; + }); +}; + +// OSWASP Guidlines: escape all non alphanumeric characters in ASCII space. +var JAVASCRIPT_CHARACTERS_EXPRESSION = + /[\x00-\x2F\x3A-\x40\5B-\x60\x7B-\xFF]/g; +function escapeJavaScriptData(text) { + return text && text.replace(JAVASCRIPT_CHARACTERS_EXPRESSION, function (c) { + return "\\x" + ('00' + c.charCodeAt(0).toString(16)).slice(-2); + }); +} + +exports.escapeHTML = escapeHTML; +exports.escapeHTMLAttribute = escapeHTMLAttribute; +exports.escapeJavaScriptData = escapeJavaScriptData; diff --git a/src/static/js/skiplist.js b/src/static/js/skiplist.js new file mode 100644 index 00000000..190bc55b --- /dev/null +++ b/src/static/js/skiplist.js @@ -0,0 +1,489 @@ +/** + * This code is mostly from the old Etherpad. Please help us to comment this code. + * This helps other people to understand this code better and helps them to improve it. + * TL;DR COMMENTS ON THIS FILE ARE HIGHLY APPRECIATED + */ + +/** + * 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. + */ + + +var noop = require('./ace2_common').noop; + + +function newSkipList() +{ + var PROFILER = window.PROFILER; + if (!PROFILER) + { + PROFILER = function() + { + return { + start: noop, + mark: noop, + literal: noop, + end: noop, + cancel: noop + }; + }; + } + + // if there are N elements in the skiplist, "start" is element -1 and "end" is element N + var start = { + key: null, + levels: 1, + upPtrs: [null], + downPtrs: [null], + downSkips: [1], + downSkipWidths: [0] + }; + var end = { + key: null, + levels: 1, + upPtrs: [null], + downPtrs: [null], + downSkips: [null], + downSkipWidths: [null] + }; + var numNodes = 0; + var totalWidth = 0; + var keyToNodeMap = {}; + start.downPtrs[0] = end; + end.upPtrs[0] = start; + // a "point" object at location x allows modifications immediately after the first + // x elements of the skiplist, such as multiple inserts or deletes. + // After an insert or delete using point P, the point is still valid and points + // to the same index in the skiplist. Other operations with other points invalidate + // this point. + + + function _getPoint(targetLoc) + { + var numLevels = start.levels; + var lvl = numLevels - 1; + var i = -1, + ws = 0; + var nodes = new Array(numLevels); + var idxs = new Array(numLevels); + var widthSkips = new Array(numLevels); + nodes[lvl] = start; + idxs[lvl] = -1; + widthSkips[lvl] = 0; + while (lvl >= 0) + { + var n = nodes[lvl]; + while (n.downPtrs[lvl] && (i + n.downSkips[lvl] < targetLoc)) + { + i += n.downSkips[lvl]; + ws += n.downSkipWidths[lvl]; + n = n.downPtrs[lvl]; + } + nodes[lvl] = n; + idxs[lvl] = i; + widthSkips[lvl] = ws; + lvl--; + if (lvl >= 0) + { + nodes[lvl] = n; + } + } + return { + nodes: nodes, + idxs: idxs, + loc: targetLoc, + widthSkips: widthSkips, + toString: function() + { + return "getPoint(" + targetLoc + ")"; + } + }; + } + + function _getNodeAtOffset(targetOffset) + { + var i = 0; + var n = start; + var lvl = start.levels - 1; + while (lvl >= 0 && n.downPtrs[lvl]) + { + while (n.downPtrs[lvl] && (i + n.downSkipWidths[lvl] <= targetOffset)) + { + i += n.downSkipWidths[lvl]; + n = n.downPtrs[lvl]; + } + lvl--; + } + if (n === start) return (start.downPtrs[0] || null); + else if (n === end) return (targetOffset == totalWidth ? (end.upPtrs[0] || null) : null); + return n; + } + + function _entryWidth(e) + { + return (e && e.width) || 0; + } + + function _insertKeyAtPoint(point, newKey, entry) + { + var p = PROFILER("insertKey", false); + var newNode = { + key: newKey, + levels: 0, + upPtrs: [], + downPtrs: [], + downSkips: [], + downSkipWidths: [] + }; + p.mark("donealloc"); + var pNodes = point.nodes; + var pIdxs = point.idxs; + var pLoc = point.loc; + var widthLoc = point.widthSkips[0] + point.nodes[0].downSkipWidths[0]; + var newWidth = _entryWidth(entry); + p.mark("loop1"); + while (newNode.levels == 0 || Math.random() < 0.01) + { + var lvl = newNode.levels; + newNode.levels++; + if (lvl == pNodes.length) + { + // assume we have just passed the end of point.nodes, and reached one level greater + // than the skiplist currently supports + pNodes[lvl] = start; + pIdxs[lvl] = -1; + start.levels++; + end.levels++; + start.downPtrs[lvl] = end; + end.upPtrs[lvl] = start; + start.downSkips[lvl] = numNodes + 1; + start.downSkipWidths[lvl] = totalWidth; + point.widthSkips[lvl] = 0; + } + var me = newNode; + var up = pNodes[lvl]; + var down = up.downPtrs[lvl]; + var skip1 = pLoc - pIdxs[lvl]; + var skip2 = up.downSkips[lvl] + 1 - skip1; + up.downSkips[lvl] = skip1; + up.downPtrs[lvl] = me; + me.downSkips[lvl] = skip2; + me.upPtrs[lvl] = up; + me.downPtrs[lvl] = down; + down.upPtrs[lvl] = me; + var widthSkip1 = widthLoc - point.widthSkips[lvl]; + var widthSkip2 = up.downSkipWidths[lvl] + newWidth - widthSkip1; + up.downSkipWidths[lvl] = widthSkip1; + me.downSkipWidths[lvl] = widthSkip2; + } + p.mark("loop2"); + p.literal(pNodes.length, "PNL"); + for (var lvl = newNode.levels; lvl < pNodes.length; lvl++) + { + var up = pNodes[lvl]; + up.downSkips[lvl]++; + up.downSkipWidths[lvl] += newWidth; + } + p.mark("map"); + keyToNodeMap['$KEY$' + newKey] = newNode; + numNodes++; + totalWidth += newWidth; + p.end(); + } + + function _getNodeAtPoint(point) + { + return point.nodes[0].downPtrs[0]; + } + + function _incrementPoint(point) + { + point.loc++; + for (var i = 0; i < point.nodes.length; i++) + { + if (point.idxs[i] + point.nodes[i].downSkips[i] < point.loc) + { + point.idxs[i] += point.nodes[i].downSkips[i]; + point.widthSkips[i] += point.nodes[i].downSkipWidths[i]; + point.nodes[i] = point.nodes[i].downPtrs[i]; + } + } + } + + function _deleteKeyAtPoint(point) + { + var elem = point.nodes[0].downPtrs[0]; + var elemWidth = _entryWidth(elem.entry); + for (var i = 0; i < point.nodes.length; i++) + { + if (i < elem.levels) + { + var up = elem.upPtrs[i]; + var down = elem.downPtrs[i]; + var totalSkip = up.downSkips[i] + elem.downSkips[i] - 1; + up.downPtrs[i] = down; + down.upPtrs[i] = up; + up.downSkips[i] = totalSkip; + var totalWidthSkip = up.downSkipWidths[i] + elem.downSkipWidths[i] - elemWidth; + up.downSkipWidths[i] = totalWidthSkip; + } + else + { + var up = point.nodes[i]; + var down = up.downPtrs[i]; + up.downSkips[i]--; + up.downSkipWidths[i] -= elemWidth; + } + } + delete keyToNodeMap['$KEY$' + elem.key]; + numNodes--; + totalWidth -= elemWidth; + } + + function _propagateWidthChange(node) + { + var oldWidth = node.downSkipWidths[0]; + var newWidth = _entryWidth(node.entry); + var widthChange = newWidth - oldWidth; + var n = node; + var lvl = 0; + while (lvl < n.levels) + { + n.downSkipWidths[lvl] += widthChange; + lvl++; + while (lvl >= n.levels && n.upPtrs[lvl - 1]) + { + n = n.upPtrs[lvl - 1]; + } + } + totalWidth += widthChange; + } + + function _getNodeIndex(node, byWidth) + { + var dist = (byWidth ? 0 : -1); + var n = node; + while (n !== start) + { + var lvl = n.levels - 1; + n = n.upPtrs[lvl]; + if (byWidth) dist += n.downSkipWidths[lvl]; + else dist += n.downSkips[lvl]; + } + return dist; + } +/*function _debugToString() { + var array = [start]; + while (array[array.length-1] !== end) { + array[array.length] = array[array.length-1].downPtrs[0]; + } + function getIndex(node) { + if (!node) return null; + for(var i=0;i<array.length;i++) { + if (array[i] === node) + return i-1; + } + return false; + } + var processedArray = map(array, function(node) { + var x = {key:node.key, levels: node.levels, downSkips: node.downSkips, + upPtrs: map(node.upPtrs, getIndex), downPtrs: map(node.downPtrs, getIndex), + downSkipWidths: node.downSkipWidths}; + return x; + }); + return map(processedArray, function (x) { return x.toSource(); }).join("\n"); + }*/ + + function _getNodeByKey(key) + { + return keyToNodeMap['$KEY$' + key]; + } + + // Returns index of first entry such that entryFunc(entry) is truthy, + // or length() if no such entry. Assumes all falsy entries come before + // all truthy entries. + + + function _search(entryFunc) + { + var low = start; + var lvl = start.levels - 1; + var lowIndex = -1; + + function f(node) + { + if (node === start) return false; + else if (node === end) return true; + else return entryFunc(node.entry); + } + while (lvl >= 0) + { + var nextLow = low.downPtrs[lvl]; + while (!f(nextLow)) + { + lowIndex += low.downSkips[lvl]; + low = nextLow; + nextLow = low.downPtrs[lvl]; + } + lvl--; + } + return lowIndex + 1; + } + +/* +The skip-list contains "entries", JavaScript objects that each must have a unique "key" property +that is a string. +*/ + var self = { + length: function() + { + return numNodes; + }, + atIndex: function(i) + { + if (i < 0) console.warn("atIndex(" + i + ")"); + if (i >= numNodes) console.warn("atIndex(" + i + ">=" + numNodes + ")"); + return _getNodeAtPoint(_getPoint(i)).entry; + }, + // differs from Array.splice() in that new elements are in an array, not varargs + splice: function(start, deleteCount, newEntryArray) + { + if (start < 0) console.warn("splice(" + start + ", ...)"); + if (start + deleteCount > numNodes) + { + console.warn("splice(" + start + ", " + deleteCount + ", ...), N=" + numNodes); + console.warn("%s %s %s", typeof start, typeof deleteCount, typeof numNodes); + console.trace(); + } + + if (!newEntryArray) newEntryArray = []; + var pt = _getPoint(start); + for (var i = 0; i < deleteCount; i++) + { + _deleteKeyAtPoint(pt); + } + for (var i = (newEntryArray.length - 1); i >= 0; i--) + { + var entry = newEntryArray[i]; + _insertKeyAtPoint(pt, entry.key, entry); + var node = _getNodeByKey(entry.key); + node.entry = entry; + } + }, + next: function(entry) + { + return _getNodeByKey(entry.key).downPtrs[0].entry || null; + }, + prev: function(entry) + { + return _getNodeByKey(entry.key).upPtrs[0].entry || null; + }, + push: function(entry) + { + self.splice(numNodes, 0, [entry]); + }, + slice: function(start, end) + { + // act like Array.slice() + if (start === undefined) start = 0; + else if (start < 0) start += numNodes; + if (end === undefined) end = numNodes; + else if (end < 0) end += numNodes; + + if (start < 0) start = 0; + if (start > numNodes) start = numNodes; + if (end < 0) end = 0; + if (end > numNodes) end = numNodes; + + dmesg(String([start, end, numNodes])); + if (end <= start) return []; + var n = self.atIndex(start); + var array = [n]; + for (var i = 1; i < (end - start); i++) + { + n = self.next(n); + array.push(n); + } + return array; + }, + atKey: function(key) + { + return _getNodeByKey(key).entry; + }, + indexOfKey: function(key) + { + return _getNodeIndex(_getNodeByKey(key)); + }, + indexOfEntry: function(entry) + { + return self.indexOfKey(entry.key); + }, + containsKey: function(key) + { + return !!(_getNodeByKey(key)); + }, + // gets the last entry starting at or before the offset + atOffset: function(offset) + { + return _getNodeAtOffset(offset).entry; + }, + keyAtOffset: function(offset) + { + return self.atOffset(offset).key; + }, + offsetOfKey: function(key) + { + return _getNodeIndex(_getNodeByKey(key), true); + }, + offsetOfEntry: function(entry) + { + return self.offsetOfKey(entry.key); + }, + setEntryWidth: function(entry, width) + { + entry.width = width; + _propagateWidthChange(_getNodeByKey(entry.key)); + }, + totalWidth: function() + { + return totalWidth; + }, + offsetOfIndex: function(i) + { + if (i < 0) return 0; + if (i >= numNodes) return totalWidth; + return self.offsetOfEntry(self.atIndex(i)); + }, + indexOfOffset: function(offset) + { + if (offset <= 0) return 0; + if (offset >= totalWidth) return numNodes; + return self.indexOfEntry(self.atOffset(offset)); + }, + search: function(entryFunc) + { + return _search(entryFunc); + }, + //debugToString: _debugToString, + debugGetPoint: _getPoint, + debugDepth: function() + { + return start.levels; + } + } + return self; +} + +exports.newSkipList = newSkipList; diff --git a/src/static/js/timeslider.js b/src/static/js/timeslider.js new file mode 100644 index 00000000..579dcb60 --- /dev/null +++ b/src/static/js/timeslider.js @@ -0,0 +1,154 @@ +/** + * This code is mostly from the old Etherpad. Please help us to comment this code. + * This helps other people to understand this code better and helps them to improve it. + * TL;DR COMMENTS ON THIS FILE ARE HIGHLY APPRECIATED + */ + +/** + * 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. + */ + +// These jQuery things should create local references, but for now `require()` +// assigns to the global `$` and augments it with plugins. +require('./jquery'); +JSON = require('./json2'); +require('./undo-xpopup'); + +var createCookie = require('./pad_utils').createCookie; +var readCookie = require('./pad_utils').readCookie; +var randomString = require('./pad_utils').randomString; + +var socket, token, padId, export_links; + +function init() { + $(document).ready(function () + { + // start the custom js + if (typeof customStart == "function") customStart(); + + //get the padId out of the url + var urlParts= document.location.pathname.split("/"); + padId = decodeURIComponent(urlParts[urlParts.length-2]); + + //set the title + document.title = padId.replace(/_+/g, ' ') + " | " + document.title; + + //ensure we have a token + token = readCookie("token"); + if(token == null) + { + token = "t." + randomString(); + createCookie("token", token, 60); + } + + var loc = document.location; + //get the correct port + var port = loc.port == "" ? (loc.protocol == "https:" ? 443 : 80) : loc.port; + //create the url + var url = loc.protocol + "//" + loc.hostname + ":" + port + "/"; + //find out in which subfolder we are + var resource = loc.pathname.substr(1,loc.pathname.indexOf("/p/")) + "socket.io"; + + //build up the socket io connection + socket = io.connect(url, {resource: resource}); + + //send the ready message once we're connected + socket.on('connect', function() + { + sendSocketMsg("CLIENT_READY", {}); + }); + + //route the incoming messages + socket.on('message', function(message) + { + if(window.console) console.log(message); + + if(message.type == "CLIENT_VARS") + { + handleClientVars(message); + } + else if(message.type == "CHANGESET_REQ") + { + changesetLoader.handleSocketResponse(message); + } + else if(message.accessStatus) + { + $("body").html("<h2>You have no permission to access this pad</h2>") + } + }); + + //get all the export links + export_links = $('#export > .exportlink') + + if(document.referrer.length > 0 && document.referrer.substring(document.referrer.lastIndexOf("/")-1,document.referrer.lastIndexOf("/")) === "p") { + $("#returnbutton").attr("href", document.referrer); + } else { + $("#returnbutton").attr("href", document.location.href.substring(0,document.location.href.lastIndexOf("/"))); + } + }); +} + +//sends a message over the socket +function sendSocketMsg(type, data) +{ + var sessionID = readCookie("sessionID"); + var password = readCookie("password"); + + var msg = { "component" : "timeslider", + "type": type, + "data": data, + "padId": padId, + "token": token, + "sessionID": sessionID, + "password": password, + "protocolVersion": 2}; + + socket.json.send(msg); +} + +var fireWhenAllScriptsAreLoaded = []; + +var BroadcastSlider, changesetLoader; +function handleClientVars(message) +{ + //save the client Vars + clientVars = message.data; + + //load all script that doesn't work without the clientVars + BroadcastSlider = require('./broadcast_slider').loadBroadcastSliderJS(fireWhenAllScriptsAreLoaded); + require('./broadcast_revisions').loadBroadcastRevisionsJS(); + changesetLoader = require('./broadcast').loadBroadcastJS(socket, sendSocketMsg, fireWhenAllScriptsAreLoaded, BroadcastSlider); + + //initialize export ui + require('./pad_impexp').padimpexp.init(); + + //change export urls when the slider moves + var export_rev_regex = /(\/\d+)?\/export/ + BroadcastSlider.onSlider(function(revno) + { + export_links.each(function() + { + this.setAttribute('href', this.href.replace(export_rev_regex, '/' + revno + '/export')); + }); + }); + + //fire all start functions of these scripts, formerly fired with window.load + for(var i=0;i < fireWhenAllScriptsAreLoaded.length;i++) + { + fireWhenAllScriptsAreLoaded[i](); + } +} + +exports.init = init; diff --git a/src/static/js/undo-xpopup.js b/src/static/js/undo-xpopup.js new file mode 100644 index 00000000..e733f3ea --- /dev/null +++ b/src/static/js/undo-xpopup.js @@ -0,0 +1,34 @@ +/** + * This code is mostly from the old Etherpad. Please help us to comment this code. + * This helps other people to understand this code better and helps them to improve it. + * TL;DR COMMENTS ON THIS FILE ARE HIGHLY APPRECIATED + */ + +/** + * 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. + */ + +if (window._orig_windowOpen) +{ + window.open = _orig_windowOpen; +} +if (window._orig_windowSetTimeout) +{ + window.setTimeout = _orig_windowSetTimeout; +} +if (window._orig_windowSetInterval) +{ + window.setInterval = _orig_windowSetInterval; +} diff --git a/src/static/js/undomodule.js b/src/static/js/undomodule.js new file mode 100644 index 00000000..8b0c0909 --- /dev/null +++ b/src/static/js/undomodule.js @@ -0,0 +1,335 @@ +/** + * This code is mostly from the old Etherpad. Please help us to comment this code. + * This helps other people to understand this code better and helps them to improve it. + * TL;DR COMMENTS ON THIS FILE ARE HIGHLY APPRECIATED + */ + +/** + * 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. + */ + +var Changeset = require('./Changeset'); +var extend = require('./ace2_common').extend; + +var undoModule = (function() +{ + var stack = (function() + { + var stackElements = []; + // two types of stackElements: + // 1) { elementType: UNDOABLE_EVENT, eventType: "anything", [backset: <changeset>,] + // [selStart: <char number>, selEnd: <char number>, selFocusAtStart: <boolean>] } + // 2) { elementType: EXTERNAL_CHANGE, changeset: <changeset> } + // invariant: no two consecutive EXTERNAL_CHANGEs + var numUndoableEvents = 0; + + var UNDOABLE_EVENT = "undoableEvent"; + var EXTERNAL_CHANGE = "externalChange"; + + function clearStack() + { + stackElements.length = 0; + stackElements.push( + { + elementType: UNDOABLE_EVENT, + eventType: "bottom" + }); + numUndoableEvents = 1; + } + clearStack(); + + function pushEvent(event) + { + var e = extend( + {}, event); + e.elementType = UNDOABLE_EVENT; + stackElements.push(e); + numUndoableEvents++; + //dmesg("pushEvent backset: "+event.backset); + } + + function pushExternalChange(cs) + { + var idx = stackElements.length - 1; + if (stackElements[idx].elementType == EXTERNAL_CHANGE) + { + stackElements[idx].changeset = Changeset.compose(stackElements[idx].changeset, cs, getAPool()); + } + else + { + stackElements.push( + { + elementType: EXTERNAL_CHANGE, + changeset: cs + }); + } + } + + function _exposeEvent(nthFromTop) + { + // precond: 0 <= nthFromTop < numUndoableEvents + var targetIndex = stackElements.length - 1 - nthFromTop; + var idx = stackElements.length - 1; + while (idx > targetIndex || stackElements[idx].elementType == EXTERNAL_CHANGE) + { + if (stackElements[idx].elementType == EXTERNAL_CHANGE) + { + var ex = stackElements[idx]; + var un = stackElements[idx - 1]; + if (un.backset) + { + var excs = ex.changeset; + var unbs = un.backset; + un.backset = Changeset.follow(excs, un.backset, false, getAPool()); + ex.changeset = Changeset.follow(unbs, ex.changeset, true, getAPool()); + if ((typeof un.selStart) == "number") + { + var newSel = Changeset.characterRangeFollow(excs, un.selStart, un.selEnd); + un.selStart = newSel[0]; + un.selEnd = newSel[1]; + if (un.selStart == un.selEnd) + { + un.selFocusAtStart = false; + } + } + } + stackElements[idx - 1] = ex; + stackElements[idx] = un; + if (idx >= 2 && stackElements[idx - 2].elementType == EXTERNAL_CHANGE) + { + ex.changeset = Changeset.compose(stackElements[idx - 2].changeset, ex.changeset, getAPool()); + stackElements.splice(idx - 2, 1); + idx--; + } + } + else + { + idx--; + } + } + } + + function getNthFromTop(n) + { + // precond: 0 <= n < numEvents() + _exposeEvent(n); + return stackElements[stackElements.length - 1 - n]; + } + + function numEvents() + { + return numUndoableEvents; + } + + function popEvent() + { + // precond: numEvents() > 0 + _exposeEvent(0); + numUndoableEvents--; + return stackElements.pop(); + } + + return { + numEvents: numEvents, + popEvent: popEvent, + pushEvent: pushEvent, + pushExternalChange: pushExternalChange, + clearStack: clearStack, + getNthFromTop: getNthFromTop + }; + })(); + + // invariant: stack always has at least one undoable event + var undoPtr = 0; // zero-index from top of stack, 0 == top + + function clearHistory() + { + stack.clearStack(); + undoPtr = 0; + } + + function _charOccurrences(str, c) + { + var i = 0; + var count = 0; + while (i >= 0 && i < str.length) + { + i = str.indexOf(c, i); + if (i >= 0) + { + count++; + i++; + } + } + return count; + } + + function _opcodeOccurrences(cs, opcode) + { + return _charOccurrences(Changeset.unpack(cs).ops, opcode); + } + + function _mergeChangesets(cs1, cs2) + { + if (!cs1) return cs2; + if (!cs2) return cs1; + + // Rough heuristic for whether changesets should be considered one action: + // each does exactly one insertion, no dels, and the composition does also; or + // each does exactly one deletion, no ins, and the composition does also. + // A little weird in that it won't merge "make bold" with "insert char" + // but will merge "make bold and insert char" with "insert char", + // though that isn't expected to come up. + var plusCount1 = _opcodeOccurrences(cs1, '+'); + var plusCount2 = _opcodeOccurrences(cs2, '+'); + var minusCount1 = _opcodeOccurrences(cs1, '-'); + var minusCount2 = _opcodeOccurrences(cs2, '-'); + if (plusCount1 == 1 && plusCount2 == 1 && minusCount1 == 0 && minusCount2 == 0) + { + var merge = Changeset.compose(cs1, cs2, getAPool()); + var plusCount3 = _opcodeOccurrences(merge, '+'); + var minusCount3 = _opcodeOccurrences(merge, '-'); + if (plusCount3 == 1 && minusCount3 == 0) + { + return merge; + } + } + else if (plusCount1 == 0 && plusCount2 == 0 && minusCount1 == 1 && minusCount2 == 1) + { + var merge = Changeset.compose(cs1, cs2, getAPool()); + var plusCount3 = _opcodeOccurrences(merge, '+'); + var minusCount3 = _opcodeOccurrences(merge, '-'); + if (plusCount3 == 0 && minusCount3 == 1) + { + return merge; + } + } + return null; + } + + function reportEvent(event) + { + var topEvent = stack.getNthFromTop(0); + + function applySelectionToTop() + { + if ((typeof event.selStart) == "number") + { + topEvent.selStart = event.selStart; + topEvent.selEnd = event.selEnd; + topEvent.selFocusAtStart = event.selFocusAtStart; + } + } + + if ((!event.backset) || Changeset.isIdentity(event.backset)) + { + applySelectionToTop(); + } + else + { + var merged = false; + if (topEvent.eventType == event.eventType) + { + var merge = _mergeChangesets(event.backset, topEvent.backset); + if (merge) + { + topEvent.backset = merge; + //dmesg("reportEvent merge: "+merge); + applySelectionToTop(); + merged = true; + } + } + if (!merged) + { + stack.pushEvent(event); + } + undoPtr = 0; + } + + } + + function reportExternalChange(changeset) + { + if (changeset && !Changeset.isIdentity(changeset)) + { + stack.pushExternalChange(changeset); + } + } + + function _getSelectionInfo(event) + { + if ((typeof event.selStart) != "number") + { + return null; + } + else + { + return { + selStart: event.selStart, + selEnd: event.selEnd, + selFocusAtStart: event.selFocusAtStart + }; + } + } + + // For "undo" and "redo", the change event must be returned + // by eventFunc and NOT reported through the normal mechanism. + // "eventFunc" should take a changeset and an optional selection info object, + // or can be called with no arguments to mean that no undo is possible. + // "eventFunc" will be called exactly once. + + function performUndo(eventFunc) + { + if (undoPtr < stack.numEvents() - 1) + { + var backsetEvent = stack.getNthFromTop(undoPtr); + var selectionEvent = stack.getNthFromTop(undoPtr + 1); + var undoEvent = eventFunc(backsetEvent.backset, _getSelectionInfo(selectionEvent)); + stack.pushEvent(undoEvent); + undoPtr += 2; + } + else eventFunc(); + } + + function performRedo(eventFunc) + { + if (undoPtr >= 2) + { + var backsetEvent = stack.getNthFromTop(0); + var selectionEvent = stack.getNthFromTop(1); + eventFunc(backsetEvent.backset, _getSelectionInfo(selectionEvent)); + stack.popEvent(); + undoPtr -= 2; + } + else eventFunc(); + } + + function getAPool() + { + return undoModule.apool; + } + + return { + clearHistory: clearHistory, + reportEvent: reportEvent, + reportExternalChange: reportExternalChange, + performUndo: performUndo, + performRedo: performRedo, + enabled: true, + apool: null + }; // apool is filled in by caller +})(); + +exports.undoModule = undoModule; diff --git a/src/static/js/virtual_lines.js b/src/static/js/virtual_lines.js new file mode 100644 index 00000000..2bcf5ed6 --- /dev/null +++ b/src/static/js/virtual_lines.js @@ -0,0 +1,388 @@ +/** + * This code is mostly from the old Etherpad. Please help us to comment this code. + * This helps other people to understand this code better and helps them to improve it. + * TL;DR COMMENTS ON THIS FILE ARE HIGHLY APPRECIATED + */ + +/** + * 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. + */ + +function makeVirtualLineView(lineNode) +{ + + // how much to jump forward or backward at once in a charSeeker before + // constructing a DOM node and checking the coordinates (which takes a + // significant fraction of a millisecond). From the + // coordinates and the approximate line height we can estimate how + // many lines we have moved. We risk being off if the number of lines + // we move is on the order of the line height in pixels. Fortunately, + // when the user boosts the font-size they increase both. + var maxCharIncrement = 20; + var seekerAtEnd = null; + + function getNumChars() + { + return lineNode.textContent.length; + } + + function getNumVirtualLines() + { + if (!seekerAtEnd) + { + var seeker = makeCharSeeker(); + seeker.forwardByWhile(maxCharIncrement); + seekerAtEnd = seeker; + } + return seekerAtEnd.getVirtualLine() + 1; + } + + function getVLineAndOffsetForChar(lineChar) + { + var seeker = makeCharSeeker(); + seeker.forwardByWhile(maxCharIncrement, null, lineChar); + var theLine = seeker.getVirtualLine(); + seeker.backwardByWhile(8, function() + { + return seeker.getVirtualLine() == theLine; + }); + seeker.forwardByWhile(1, function() + { + return seeker.getVirtualLine() != theLine; + }); + var lineStartChar = seeker.getOffset(); + return { + vline: theLine, + offset: (lineChar - lineStartChar) + }; + } + + function getCharForVLineAndOffset(vline, offset) + { + // returns revised vline and offset as well as absolute char index within line. + // if offset is beyond end of line, for example, will give new offset at end of line. + var seeker = makeCharSeeker(); + // go to start of line + seeker.binarySearch(function() + { + return seeker.getVirtualLine() >= vline; + }); + var lineStart = seeker.getOffset(); + var theLine = seeker.getVirtualLine(); + // go to offset, overshooting the virtual line only if offset is too large for it + seeker.forwardByWhile(maxCharIncrement, null, lineStart + offset); + // get back into line + seeker.backwardByWhile(1, function() + { + return seeker.getVirtualLine() != theLine; + }, lineStart); + var lineChar = seeker.getOffset(); + var theOffset = lineChar - lineStart; + // handle case of last virtual line; should be able to be at end of it + if (theOffset < offset && theLine == (getNumVirtualLines() - 1)) + { + var lineLen = getNumChars(); + theOffset += lineLen - lineChar; + lineChar = lineLen; + } + + return { + vline: theLine, + offset: theOffset, + lineChar: lineChar + }; + } + + return { + getNumVirtualLines: getNumVirtualLines, + getVLineAndOffsetForChar: getVLineAndOffsetForChar, + getCharForVLineAndOffset: getCharForVLineAndOffset, + makeCharSeeker: function() + { + return makeCharSeeker(); + } + }; + + function deepFirstChildTextNode(nd) + { + nd = nd.firstChild; + while (nd && nd.firstChild) nd = nd.firstChild; + if (nd.data) return nd; + return null; + } + + function makeCharSeeker( /*lineNode*/ ) + { + + function charCoords(tnode, i) + { + var container = tnode.parentNode; + + // treat space specially; a space at the end of a virtual line + // will have weird coordinates + var isSpace = (tnode.nodeValue.charAt(i) === " "); + if (isSpace) + { + if (i == 0) + { + if (container.previousSibling && deepFirstChildTextNode(container.previousSibling)) + { + tnode = deepFirstChildTextNode(container.previousSibling); + i = tnode.length - 1; + container = tnode.parentNode; + } + else + { + return { + top: container.offsetTop, + left: container.offsetLeft + }; + } + } + else + { + i--; // use previous char + } + } + + + var charWrapper = document.createElement("SPAN"); + + // wrap the character + var tnodeText = tnode.nodeValue; + var frag = document.createDocumentFragment(); + frag.appendChild(document.createTextNode(tnodeText.substring(0, i))); + charWrapper.appendChild(document.createTextNode(tnodeText.substr(i, 1))); + frag.appendChild(charWrapper); + frag.appendChild(document.createTextNode(tnodeText.substring(i + 1))); + container.replaceChild(frag, tnode); + + var result = { + top: charWrapper.offsetTop, + left: charWrapper.offsetLeft + (isSpace ? charWrapper.offsetWidth : 0), + height: charWrapper.offsetHeight + }; + + while (container.firstChild) container.removeChild(container.firstChild); + container.appendChild(tnode); + + return result; + } + + var lineText = lineNode.textContent; + var lineLength = lineText.length; + + var curNode = null; + var curChar = 0; + var curCharWithinNode = 0 + var curTop; + var curLeft; + var approxLineHeight; + var whichLine = 0; + + function nextNode() + { + var n = curNode; + if (!n) n = lineNode.firstChild; + else n = n.nextSibling; + while (n && !deepFirstChildTextNode(n)) + { + n = n.nextSibling; + } + return n; + } + + function prevNode() + { + var n = curNode; + if (!n) n = lineNode.lastChild; + else n = n.previousSibling; + while (n && !deepFirstChildTextNode(n)) + { + n = n.previousSibling; + } + return n; + } + + var seeker; + if (lineLength > 0) + { + curNode = nextNode(); + var firstCharData = charCoords(deepFirstChildTextNode(curNode), 0); + approxLineHeight = firstCharData.height; + curTop = firstCharData.top; + curLeft = firstCharData.left; + + function updateCharData(tnode, i) + { + var coords = charCoords(tnode, i); + whichLine += Math.round((coords.top - curTop) / approxLineHeight); + curTop = coords.top; + curLeft = coords.left; + } + + seeker = { + forward: function(numChars) + { + var oldChar = curChar; + var newChar = curChar + numChars; + if (newChar > (lineLength - 1)) newChar = lineLength - 1; + while (curChar < newChar) + { + var curNodeLength = deepFirstChildTextNode(curNode).length; + var toGo = curNodeLength - curCharWithinNode; + if (curChar + toGo > newChar || !nextNode()) + { + // going to next node would be too far + var n = newChar - curChar; + if (n >= toGo) n = toGo - 1; + curChar += n; + curCharWithinNode += n; + break; + } + else + { + // go to next node + curChar += toGo; + curCharWithinNode = 0; + curNode = nextNode(); + } + } + updateCharData(deepFirstChildTextNode(curNode), curCharWithinNode); + return curChar - oldChar; + }, + backward: function(numChars) + { + var oldChar = curChar; + var newChar = curChar - numChars; + if (newChar < 0) newChar = 0; + while (curChar > newChar) + { + if (curChar - curCharWithinNode <= newChar || !prevNode()) + { + // going to prev node would be too far + var n = curChar - newChar; + if (n > curCharWithinNode) n = curCharWithinNode; + curChar -= n; + curCharWithinNode -= n; + break; + } + else + { + // go to prev node + curChar -= curCharWithinNode + 1; + curNode = prevNode(); + curCharWithinNode = deepFirstChildTextNode(curNode).length - 1; + } + } + updateCharData(deepFirstChildTextNode(curNode), curCharWithinNode); + return oldChar - curChar; + }, + getVirtualLine: function() + { + return whichLine; + }, + getLeftCoord: function() + { + return curLeft; + } + }; + } + else + { + curLeft = lineNode.offsetLeft; + seeker = { + forward: function(numChars) + { + return 0; + }, + backward: function(numChars) + { + return 0; + }, + getVirtualLine: function() + { + return 0; + }, + getLeftCoord: function() + { + return curLeft; + } + }; + } + seeker.getOffset = function() + { + return curChar; + }; + seeker.getLineLength = function() + { + return lineLength; + }; + seeker.toString = function() + { + return "seeker[curChar: " + curChar + "(" + lineText.charAt(curChar) + "), left: " + seeker.getLeftCoord() + ", vline: " + seeker.getVirtualLine() + "]"; + }; + + function moveByWhile(isBackward, amount, optCondFunc, optCharLimit) + { + var charsMovedLast = null; + var hasCondFunc = ((typeof optCondFunc) == "function"); + var condFunc = optCondFunc; + var hasCharLimit = ((typeof optCharLimit) == "number"); + var charLimit = optCharLimit; + while (charsMovedLast !== 0 && ((!hasCondFunc) || condFunc())) + { + var toMove = amount; + if (hasCharLimit) + { + var untilLimit = (isBackward ? curChar - charLimit : charLimit - curChar); + if (untilLimit < toMove) toMove = untilLimit; + } + if (toMove < 0) break; + charsMovedLast = (isBackward ? seeker.backward(toMove) : seeker.forward(toMove)); + } + } + + seeker.forwardByWhile = function(amount, optCondFunc, optCharLimit) + { + moveByWhile(false, amount, optCondFunc, optCharLimit); + } + seeker.backwardByWhile = function(amount, optCondFunc, optCharLimit) + { + moveByWhile(true, amount, optCondFunc, optCharLimit); + } + seeker.binarySearch = function(condFunc) + { + // returns index of boundary between false chars and true chars; + // positions seeker at first true char, or else last char + var trueFunc = condFunc; + var falseFunc = function() + { + return !condFunc(); + }; + seeker.forwardByWhile(20, falseFunc); + seeker.backwardByWhile(20, trueFunc); + seeker.forwardByWhile(10, falseFunc); + seeker.backwardByWhile(5, trueFunc); + seeker.forwardByWhile(1, falseFunc); + return seeker.getOffset() + (condFunc() ? 0 : 1); + } + + return seeker; + } + +} + +exports.makeVirtualLineView = makeVirtualLineView; diff --git a/src/static/pad.html b/src/static/pad.html new file mode 100644 index 00000000..95a5b98f --- /dev/null +++ b/src/static/pad.html @@ -0,0 +1,284 @@ +<!doctype html> +<html> + + <title>Etherpad Lite</title> + + <meta charset="utf-8"> + <meta name="robots" content="noindex, nofollow"> + <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=0"> + + <link href="../static/css/pad.css" rel="stylesheet"> + <link href="../static/custom/pad.css" rel="stylesheet"> + <style title="dynamicsyntax"></style> + + <!-- head and body had been removed intentionally --> + + <div id="editbar"> + <ul id="menu_left"> + <li id="bold" onClick="window.pad&&pad.editbarClick('bold');return false"> + <a class="buttonicon buttonicon-bold" title="Bold (ctrl-B)"></a> + </li> + <li id="italic" onClick="window.pad&&pad.editbarClick('italic'); return false;"> + <a class="buttonicon buttonicon-italic" title="Italics (ctrl-I)"></a> + </li> + <li id="underline" onClick="window.pad&&pad.editbarClick('underline');return false;" > + <a class="buttonicon buttonicon-underline" title="Underline (ctrl-U)"></a> + </li> + <li id="strikethrough" onClick="window.pad&&pad.editbarClick('strikethrough');return false;"> + <a class="buttonicon buttonicon-strikethrough" title="Strikethrough"></a> + </li> + <li class="separator"></li> + <li id="oderedlist" onClick="window.pad&&pad.editbarClick('insertorderedlist');return false;"> + <a class="buttonicon buttonicon-insertorderedlist" title="Toggle Ordered List"></a> + </li> + <li id="unoderedlist" onClick="window.pad&&pad.editbarClick('insertunorderedlist');return false;"> + <a class="buttonicon buttonicon-insertunorderedlist" title="Toggle Bullet List"></a> + </li> + <li id="indent" onClick="window.pad&&pad.editbarClick('indent');return false;"> + <a class="buttonicon buttonicon-indent" title="Indent"></a> + </li> + <li id="outdent" onClick="window.pad&&pad.editbarClick('outdent');return false;"> + <a class="buttonicon buttonicon-outdent" title="Unindent"></a> + </li> + <li class="separator"></li> + <li id="undo" onClick="window.pad&&pad.editbarClick('undo');return false;"> + <a class="buttonicon buttonicon-undo" title="Undo (ctrl-Z)"></a> + </li> + <li id="redo" onClick="window.pad&&pad.editbarClick('redo');return false;"> + <a class="buttonicon buttonicon-redo" title="Redo (ctrl-Y)"></a> + </li> + <li class="separator"></li> + <li id="clearAuthorship" onClick="window.pad&&pad.editbarClick('clearauthorship');return false;"> + <a class="buttonicon buttonicon-clearauthorship" title="Clear Authorship Colors"></a> + </li> + </ul> + <ul id="menu_right"> + <li id="settingslink" onClick="window.pad&&pad.editbarClick('settings');return false;"> + <a class="buttonicon buttonicon-settings" id="settingslink" title="Settings of this pad"></a> + </li> + <li id="importexportlink" onClick="window.pad&&pad.editbarClick('import_export');return false;"> + <a class="buttonicon buttonicon-import_export" id="exportlink" title="Import/Export from/to different document formats"></a> + </li> + <li id="embedlink" onClick="window.pad&&pad.editbarClick('embed');return false;" > + <a class="buttonicon buttonicon-embed" id="embedlink" title="Share and Embed this pad"></a> + </li> + <li class="separator"></li> + <li id="timesliderlink" onClick="document.location = document.location.pathname+ '/timeslider'"> + <a class="buttonicon buttonicon-history" title="Show the history of this pad"></a> + </li> + <li id="usericon" onClick="window.pad&&pad.editbarClick('showusers');return false;" title="Show connected users"> + <span class="buttonicon buttonicon-showusers" id="usericonback"></span> + <span id="online_count">1</span> + </li> + </ul> + </div> + + <div id="users"> + <div id="connectionstatus"></div> + <div id="myuser"> + <div id="mycolorpicker"> + <div id="colorpicker"></div> + <button id="mycolorpickersave">Save</button> + <button id="mycolorpickercancel">Cancel</button> + <span id="mycolorpickerpreview" class="myswatchboxhoverable"></span> + </div> + <div id="myswatchbox"><div id="myswatch"></div></div> + <div id="myusernameform"><input type="text" id="myusernameedit" disabled="disabled"></div> + <div id="mystatusform"><input type="text" id="mystatusedit" disabled="disabled"></div> + </div> + <div id="otherusers"> + <div id="guestprompts"></div> + <table id="otheruserstable" cellspacing="0" cellpadding="0" border="0"> + <tr><td></td></tr> + </table> + <div id="nootherusers"></div> + </div> + <div id="userlistbuttonarea"></div> + </div> + + <div id="editorcontainerbox"> + <div id="editorcontainer"></div> + <div id="editorloadingbox">Loading...</div> + </div> + + <div id="settingsmenu" class="popup"> + <h1>Pad settings</h1> + <div class="column"> + <h2>My view</h2> + <p> + <input type="checkbox" id="options-stickychat" onClick="chat.stickToScreen();"> + <label for="options-stickychat">Chat always on screen</label> + </p> + <p> + <input type="checkbox" id="options-colorscheck"> + <label for="options-colorscheck">Authorship colors</label> + </p> + <p> + <input type="checkbox" id="options-linenoscheck" checked> + <label for="options-linenoscheck">Line numbers</label> + </p> + <p> + Font type: + <select id="viewfontmenu"> + <option value="normal">Normal</option> + <option value="monospace">Monospaced</option> + </select> + </p> + </div> + <div class="column"> + <h2>Global view</h2> + <p>Currently nothing.</p> + <p class="note">These options affect everyone viewing this pad.</p> + </div> + </div> + + <div id="importexport" class="popup"> + <div class="column"> + <h2>Import from text file, HTML, PDF, Word, ODT or RTF</h2><br> + <form id="importform" method="post" action="" target="importiframe" enctype="multipart/form-data"> + <div class="importformdiv" id="importformfilediv"> + <input type="file" name="file" size="15" id="importfileinput"> + <div class="importmessage" id="importmessagefail"></div> + </div> + <div id="import"></div> + <div class="importmessage" id="importmessagesuccess">Successful!</div> + <div class="importformdiv" id="importformsubmitdiv"> + <input type="hidden" name="padId" value="blpmaXT35R"> + <span class="nowrap"> + <input type="submit" name="submit" value="Import Now" disabled="disabled" id="importsubmitinput"> + <img alt="" id="importstatusball" src="../static/img/loading.gif" align="top"> + <img alt="" id="importarrow" src="../static/img/leftarrow.png" align="top"> + </span> + </div> + </form> + </div> + <div class="column"> + <h2>Export current pad as</h2> + <a id="exporthtmla" target="_blank" class="exportlink"><div class="exporttype" id="exporthtml">HTML</div></a> + <a id="exportplaina" target="_blank" class="exportlink"><div class="exporttype" id="exportplain">Plain text</div></a> + <a id="exportworda" target="_blank" class="exportlink"><div class="exporttype" id="exportword">Microsoft Word</div></a> + <a id="exportpdfa" target="_blank" class="exportlink"><div class="exporttype" id="exportpdf">PDF</div></a> + <a id="exportopena" target="_blank" class="exportlink"><div class="exporttype" id="exportopen">OpenDocument</div></a> + <a id="exportdokuwikia" target="_blank" class="exportlink"><div class="exporttype" id="exportdokuwiki">DokuWiki text</div></a> + <a id="exportwordlea" target="_blank" onClick="padimpexp.export2Wordle();return false;" class="exportlink"><div class="exporttype" id="exportwordle">Wordle</div></a> + </div> + </div> + + <div id="embed" class="popup"> + <div id="embedreadonly" class="right"> + <input type="checkbox" id="readonlyinput" onClick="padeditbar.setEmbedLinks();"> + <label for="readonlyinput">Read only</label> + </div> + <h1>Share this pad</h1> + <div id="linkcode"> + <h2>Link</h2> + <input id="linkinput" type="text" value=""> + </div> + <br> + <div id="embedcode"> + <h2>Embed URL</h2> + <input id="embedinput" type="text" value=""> + </div> + <br> + <div id="qrcode"> + <h2>QR code</h2> + <div id="qr_center"><img id="embedreadonlyqr"></div> + </div> + </div> + + <div id="chatthrob"></div> + + <div id="chaticon" title="Open the chat for this pad" onclick="chat.show();return false;"> + <span id="chatlabel">Chat</span> + <span class="buttonicon buttonicon-chat"></span> + <span id="chatcounter">0</span> + </div> + + <div id="chatbox"> + <div id="titlebar"><span id ="titlelabel">Chat</span><a id="titlecross" onClick="chat.hide();return false;">- </a></div> + <div id="chattext" class="authorColors"></div> + <div id="chatinputbox"> + <form> + <input id="chatinput" type="text" maxlength="140"> + </form> + </div> + </div> + + <div id="focusprotector"> </div> + + <div id="modaloverlay"> + <div id="modaloverlay-inner"></div> + </div> + + <div id="mainmodals"> + <div id="connectionbox" class="modaldialog"> + <div id="connectionboxinner" class="modaldialog-inner"> + <div class="connecting">Connecting...</div> + <div class="reconnecting">Reestablishing connection...</div> + <div class="disconnected"> + <h2 class="h2_disconnect">Disconnected.</h2> + <h2 class="h2_userdup">Opened in another window.</h2> + <h2 class="h2_unauth">No Authorization.</h2> + <div id="disconnected_looping"> + <p><b>We're having trouble talking to the EtherPad lite synchronization server.</b> You may be connecting through an incompatible firewall or proxy server.</p> + </div> + <div id="disconnected_initsocketfail"> + <p><b>We were unable to connect to the EtherPad lite synchronization server.</b> This may be due to an incompatibility with your web browser or internet connection.</p> + </div> + <div id="disconnected_userdup"> + <p><b>You seem to have opened this pad in another browser window.</b> If you'd like to use this window instead, you can reconnect.</p> + </div> + <div id="disconnected_unknown"> + <p><b>Lost connection with the EtherPad lite synchronization server.</b> This may be due to a loss of network connectivity.</p> + </div> + <div id="disconnected_slowcommit"> + <p><b>Server not responding.</b> This may be due to network connectivity issues or high load on the server.</p> + </div> + <div id="disconnected_unauth"> + <p>Your browser's credentials or permissions have changed while viewing this pad. Try reconnecting.</p> + </div> + <div id="disconnected_deleted"> + <p>This pad was deleted.</p> + </div> + <div id="reconnect_advise"> + <p>If this continues to happen, please let us know</p> + </div> + <div id="reconnect_form"> + <button id="forcereconnect">Reconnect Now</button> + </div> + </div> + </div> + <form id="reconnectform" method="post" action="/ep/pad/reconnect" accept-charset="UTF-8" style="display: none;"> + <input type="hidden" class="padId" name="padId"> + <input type="hidden" class="diagnosticInfo" name="diagnosticInfo"> + <input type="hidden" class="missedChanges" name="missedChanges"> + </form> + </div> + + </div> + + <script type="text/javascript" src="../static/js/require-kernel.js"></script> + <script type="text/javascript" src="../static/js/jquery.js"></script> + <script type="text/javascript" src="../socket.io/socket.io.js"></script> + <script type="text/javascript" src="../javascripts/lib/ep_etherpad-lite/static/js/pad.js?callback=require.define"></script> + <script type="text/javascript"> + var clientVars = {}; + (function () { + require.setRootURI("../javascripts/src"); + require.setLibraryURI("../javascripts/lib"); + require.setGlobalKeyPath("require"); + + var plugins = require('ep_etherpad-lite/static/js/pluginfw/plugins'); + plugins.update(function () { + require('ep_etherpad-lite/static/js/pad').init(); + }); + + /* TODO: These globals shouldn't exist. */ + pad = require('ep_etherpad-lite/static/js/pad').pad; + chat = require('ep_etherpad-lite/static/js/chat').chat; + padeditbar = require('ep_etherpad-lite/static/js/pad_editbar').padeditbar; + padimpexp = require('ep_etherpad-lite/static/js/pad_impexp').padimpexp; + }()); + </script> + +</html> diff --git a/src/static/robots.txt b/src/static/robots.txt new file mode 100644 index 00000000..461bb6c9 --- /dev/null +++ b/src/static/robots.txt @@ -0,0 +1,3 @@ +User-agent: * +Disallow: /p/ +Disallow: /newpad diff --git a/src/static/tests.html b/src/static/tests.html new file mode 100644 index 00000000..bd5bc578 --- /dev/null +++ b/src/static/tests.html @@ -0,0 +1,165 @@ +<!doctype html> +<html lang="en"> +<head> + <meta charset="utf-8"> + <title>API Test and Examples Page</title> + <script type="text/javascript" src="js/jquery.min.js"></script> + <style type="text/css"> + body { + font-size:9pt; + background: rgba(0, 0, 0, .05); + color: #333; + text-shadow: 0 1px 0 #fff; + font: 14px helvetica,sans-serif; + background: #ccc; + background: -moz-radial-gradient(circle, #aaa, #eee) no-repeat center center fixed; + background: -webkit-radial-gradient(circle, #aaa, #eee) no-repeat center center fixed; + background: -ms-radial-gradient(circle, #aaa, #eee) no-repeat center center fixed; + background: -o-radial-gradient(circle, #aaa, #eee) no-repeat center center fixed; + width: 1000px; + } + .define, #template { + display: none; + } + .test_group { + overflow: auto; + width: 300px; + float:left; + color: #555; + + border-top: 1px solid #999; + margin: 4px; + padding: 4px 10px 4px 10px; + background: #eee; + background: -webkit-linear-gradient(#fff, #ccc); + background: -moz-linear-gradient(#fff, #ccc); + background: -ms-linear-gradient(#fff, #ccc); + background: -o-linear-gradient(#fff, #ccc); + opacity: .9; + box-shadow: 0px 1px 8px rgba(0, 0, 0, 0.3); + } + .test_group h2 { + font-size: 10pt; + } + .test_group table { + width: 100%; + } + + #apikeyDIV { + width: 100% + } + </style> + <script type="text/javascript"> + $(document).ready(function() { + $('input[type=button]').live('click', function() { + var $test_group = $(this).closest('.test_group'); + var name = parseName($test_group.find('h2').text()); + + var results_node = $test_group.find('.results'); + + var params = {}; + $test_group.find('input[type=text]').each(function() { + params[$(this).attr('name')] = $(this).val(); + }); + + callFunction(name, results_node, params); + }); + + $('.define').each(function() { + var functionName = parseName($(this).text()); + var parameters = parseParameters($(this).text()); + + var $template = $('#template').clone(); + + $template.find('h2').text(functionName + "()"); + + var $table = $template.find('table'); + + $(parameters).each(function(index, el) { + $table.prepend('<tr><td>' + el + ':</td>' + + '<td style="width:200px"><input type="text" size="10" name="' + el + '" /></td></tr>'); + }); + + $template.css({display: "block"}); + $template.appendTo('body'); + }); + }); + + function parseName(str) + { + return str.substring(0, str.indexOf('(')); + } + + function parseParameters(str) + { + // parse out the parameters by looking for parens + var parens = str.substring(str.indexOf("(")); + + // return empty array if there are no paremeters + if(parens.length < 3) + { + return []; + } + + // remove parens from string + parens = parens.substring(1); + parens = parens.substring(0, parens.length-1); + + return parens.split(','); + } + + function callFunction(memberName, results_node, params) + { + $('#result').text('Calling ' + memberName + "()..."); + + params["apikey"]=$("#apikey").val(); + + $.ajax({ + type: "GET", + url: "/api/1/" + memberName, + data: params, + success: function(json) { + results_node.text(json); + }, + error: function(jqXHR, textStatus, errorThrown) { + results_node.html("textStatus: " + textStatus + "<br />errorThrown: " + errorThrown); + } + }); + } + </script> +</head> +<body> + <div id="apikeyDIV" class="test_group"><b>APIKEY: </b><input type="text" id="apikey"></div> + <div class="test_group" id="template"> + <h2>createGroup()</h2> + <table> + <tr> + <td class="buttonBox" colspan="2" style="text-align:right;"><input type="button" value="Run" /></td> + </tr> + </table> + <div class="results"/> + </div> + <div class="define">createGroup()</div> + <div class="define">deleteGroup(groupID)</div> + <div class="define">createGroupIfNotExistsFor(groupMapper)</div> + <div class="define">listPads(groupID)</div> + <div class="define">createPad(padID,text)</div> + <div class="define">createGroupPad(groupID,padName,text)</div> + <div class="define">createAuthor(name)</div> + <div class="define">createAuthorIfNotExistsFor(authorMapper,name)</div> + <div class="define">createSession(groupID,authorID,validUntil)</div> + <div class="define">deleteSession(sessionID)</div> + <div class="define">getSessionInfo(sessionID)</div> + <div class="define">listSessionsOfGroup(groupID)</div> + <div class="define">listSessionsOfAuthor(authorID)</div> + <div class="define">getText(padID,rev)</div> + <div class="define">setText(padID,text)</div> + <div class="define">getRevisionsCount(padID)</div> + <div class="define">deletePad(padID)</div> + <div class="define">getReadOnlyID(padID)</div> + <div class="define">setPublicStatus(padID,publicStatus)</div> + <div class="define">getPublicStatus(padID)</div> + <div class="define">setPassword(padID,password)</div> + <div class="define">isPasswordProtected(padID)</div> +</body> +</html> diff --git a/src/static/timeslider.html b/src/static/timeslider.html new file mode 100644 index 00000000..413fbe80 --- /dev/null +++ b/src/static/timeslider.html @@ -0,0 +1,224 @@ +<!doctype html> +<html lang="en"> +<head> + <meta charset="utf-8"> + <meta name="robots" content="noindex, nofollow"> + + <title>Etherpad Lite Timeslider</title> + <link rel="stylesheet" href="../../static/css/pad.css"> + <link rel="stylesheet" href="../../static/css/timeslider.css"> + <link rel="stylesheet" href="../../static/custom/timeslider.css"> + <style type="text/css" title="dynamicsyntax"></style> +</head> + +<body id="padbody" class="timeslider limwidth nonpropad nonprouser"> + <div id="padpage"> + <div id="padtop"> + <div class="topbar"> + <div class="topbarleft"> + <!-- --> + </div> + + <div class="topbarright"> + <!-- --> + </div> + + <div class="topbarcenter"> + <a href="/" class="topbarBrand">Etherpad v1.1</a> <a href="http://etherpad.org" + class="EtherpadLink">Etherpad is</a> <a href="../../static/LICENSE" class= + "Licensing">free software</a> + + <div class="fullscreen" onclick="$('body').toggleClass('maximized');"> + Full screen + </div><a href="javascript:void(0);" onclick= + "$('body').toggleClass('maximized');" class="topbarmaximize" title= + "Toggle maximization"></a> + </div> + + <div class="specialkeyarea"> + <!-- --> + </div> + </div> + <div id="alertbar"> + <div id="servermsg"> + <h3>Server Notice<span id="servermsgdate"><!-- --></span>:</h3><a id= + "hidetopmsg" href="javascript:%20void%20pad.hideServerMessage()" name= + "hidetopmsg">hide</a> + + <p id="servermsgtext"><!-- --></p> + </div> + </div> + + <div id="navigation"></div> + + <div id="docbar" class="menu docbar"> + <table border="0" cellpadding="0" cellspacing="0" width="100%" id="docbartable" + class="docbartable"> + <tr> + <td><img src="../../static/img/roundcorner_left.gif" /></td> + + <td id="docbarpadtitle" class="docbarpadtitle" title= + "Public Pad: Public Pad"><span>Public Pad</span></td> + + <td width="100%"> </td> + + <td><img src="../../static/img/roundcorner_right.gif" /></td> + </tr> + </table> + </div><!-- /docbar --> + </div> + + <div id="timeslider-wrapper"> + <div id="error" style="display: none"> + It looks like you're having connection troubles. <a href= + "/ep/pad/view/test/latest">Reconnect now</a>. + </div> + + <div id="timeslider" unselectable="on" style="display: none"> + <div id="timeslider-left"></div> + + <div id="timeslider-right"></div> + + <div id="timer"></div> + + <div id="timeslider-slider"> + <div id="ui-slider-handle"></div> + + <div id="ui-slider-bar"></div> + </div> + + <div id="playpause_button"> + <div id="playpause_button_icon" class=""></div> + </div> + + <div id="steppers"> + <div class="stepper" id="leftstep"></div> + + <div class="stepper" id="rightstep"></div> + </div> + </div> + </div> + + <!--<div id="rightbars" style="top: 95px;"> + <div id="rightbar"><a href="/ep/pad/view/c6fg9GM51V/latest" id="viewlatest">Viewing latest content</a><br> + <a thref="/ep/pad/view/c6fg9GM51V/rev.%revision%" href="/ep/pad/view/c6fg9GM51V/rev.0" class="tlink">Link to this version</a> + <br><a thref="/ep/pad/view/ro.fw470Orpi4T/rev.%revision%" href="/ep/pad/view/ro.fw470Orpi4T/rev.0" class="tlink">Link to read-only page</a><br><a href="/c6fg9GM51V">Edit this pad</a> + <h2>Download as</h2> + <img src="../../static/img/may09/html.gif"><a thref="/ep/pad/export/c6fg9GM51V/rev.%revision%?format=html" href="/ep/pad/export/c6fg9GM51V/rev.0?format=html" class="tlink">HTML</a><br> + <img src="../../static/img/may09/txt.gif"><a thref="/ep/pad/export/c6fg9GM51V/rev.%revision%?format=txt" href="/ep/pad/export/c6fg9GM51V/rev.0?format=txt" class="tlink">Plain text</a><br> + <img src="../../static/img/may09/doc.gif"><a thref="/ep/pad/export/c6fg9GM51V/rev.%revision%?format=doc" href="/ep/pad/export/c6fg9GM51V/rev.0?format=doc" class="tlink">Microsoft Word</a><br> + <img src="../../static/img/may09/pdf.gif"><a thref="/ep/pad/export/c6fg9GM51V/rev.%revision%?format=pdf" href="/ep/pad/export/c6fg9GM51V/rev.0?format=pdf" class="tlink">PDF</a> + + + </div> + <div id="legend"> + <h2>Authors</h2> + <table cellspacing="0" cellpadding="0" border="0" id="authorstable"><tbody><tr><td style="color:#999; padding-left: 10px" colspan="2">No Authors</td></tr></tbody></table> + </div> + </div>--> + + <div id="padmain"> + <div id="padeditor"> + <div id="editbar" class="editbar disabledtoolbar"> + <div id="editbarinner" class="editbarinner"> + <div id="editbarleft" class="editbarleft"> + <!-- --> + </div> + + <div id="editbarright" class="editbarright"> + <!-- termporary place holder--> + <ul> + <li onClick="window.padeditbar.toolbarClick('import_export');return false;"> + <a id="exportlink" title="Export to different document formats"> + <div class="buttonicon buttonicon-import_export"></div> + </a> + </li> + </ul> + <a id = "returnbutton">Return to pad</a> + </div> + + <div id="editbarinner" class="editbarinner"> + <table cellpadding="0" cellspacing="0" border="0" id="editbartable" class= + "editbartable"> + <tr> + <td> + <h1> + <span id="revision_label"></span> + <span id="revision_date"></span> + </h1> + </td> + + <td width="100%"> </td> + </tr> + </table> + + <table cellpadding="0" cellspacing="0" border="0" id="editbarsavetable" + class="editbarsavetable"> + <tr> + <td></td> + </tr> + </table> + </div> + </div> + </div> + + <div id="editorcontainerbox"> + <div id="padcontent"> + + </div> + </div> + </div><!-- /padeditor --> + </div><!-- /padmain --> + </div><!-- /padpage --> + + <div id="modaloverlay"> + <div id="modaloverlay-inner"> + <!-- --> + </div> + </div> + + <div id="mainmodals"></div> + +<!-- export code --> +<div id="importexport"> + +<div id="export" class="popup"> + Export current version as: + <a id="exporthtmla" target="_blank" class="exportlink"><div class="exporttype" id="exporthtml">HTML</div></a> + <a id="exportplaina" target="_blank" class="exportlink"><div class="exporttype" id="exportplain">Plain text</div></a> + <a id="exportworda" target="_blank" class="exportlink"><div class="exporttype" id="exportword">Microsoft Word</div></a> + <a id="exportpdfa" target="_blank" class="exportlink"><div class="exporttype" id="exportpdf">PDF</div></a> + <a id="exportopena" target="_blank" class="exportlink"><div class="exporttype" id="exportopen">OpenDocument</div></a> + <a id="exportdokuwikia" target="_blank" class="exportlink"><div class="exporttype" id="exportdokuwiki">DokuWiki text</div></a> + <a id="exportwordlea" target="_blank" onClick="padimpexp.export2Wordle();return false;" class="exportlink"><div class="exporttype" id="exportwordle">Wordle</div></a> + <form id="wordlepost" name="wall" action="http://wordle.net/advanced" method="POST" style="margin-left:0px;"> + <div id="hidetext" style=""><textarea id="text" name="text" id="text" style="display:none;">Coming soon!</textarea></div> + </form> +</div> +</div> + +<script type="text/javascript" src="../../../static/js/require-kernel.js"></script> +<script type="text/javascript" src="../../../static/js/jquery.js"></script> +<script type="text/javascript" src="../../../socket.io/socket.io.js"></script> +<script type="text/javascript" src="../../../javascripts/lib/ep_etherpad-lite/static/js/timeslider.js?callback=require.define"></script> +<script type="text/javascript" src="../../../static/custom/timeslider.js"></script> +<script type="text/javascript" > + var clientVars = {}; + (function () { + require.setRootURI("../../../javascripts/src"); + require.setLibraryURI("../../../javascripts/lib"); + require.setGlobalKeyPath("require"); + + var plugins = require('ep_etherpad-lite/static/js/pluginfw/plugins'); + plugins.update(function () { + require('ep_etherpad-lite/static/js/timeslider').init(); + + /* TODO: These globals shouldn't exist. */ + padeditbar = require('ep_etherpad-lite/static/js/pad_editbar').padeditbar; + padimpexp = require('ep_etherpad-lite/static/js/pad_impexp').padimpexp; + }); + })(); +</script> +</body> +</html> + |