diff --git a/.gitignore b/.gitignore index 50cd6e212..31b7d2ea9 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ node_modules settings.json -static/js/jquery.min.js +static/js/jquery.js +static/js/prefixfree.js APIKEY.txt bin/abiword.exe bin/node.exe @@ -8,4 +9,5 @@ etherpad-lite-win.zip var/dirty.db bin/convertSettings.json *~ -*.patch \ No newline at end of file +*.patch +*.DS_Store \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 000000000..a35c25535 --- /dev/null +++ b/LICENSE @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2012 THE ETHERPAD FOUNDATION + + 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. diff --git a/README.md b/README.md index ab241333f..84e866585 100644 --- a/README.md +++ b/README.md @@ -58,7 +58,7 @@ Here is the **[FAQ](https://github.com/Pita/etherpad-lite/wiki/FAQ)**
  1. Install the dependencies. We need gzip, git, curl, libssl develop libraries, python and gcc.
    For Debian/Ubuntu apt-get install gzip git-core curl python libssl-dev build-essential
    - For Fedora/CentOS yum install gzip git-core curl python openssl-dev && yum groupinstall "Development Tools" + For Fedora/CentOS yum install gzip git-core curl python openssl-devel && yum groupinstall "Development Tools"

  2. Install node.js
      @@ -81,6 +81,8 @@ Here is the **[FAQ](https://github.com/Pita/etherpad-lite/wiki/FAQ)** ## Next Steps You can modify the settings in the file `settings.json` +If you have multiple settings files, you may pass one to `bin/run.sh` using the `-s|--settings` option. This allows you to run multiple Etherpad Lite instances from the same installation. + You should use a dedicated database such as "mysql" if you are planning on using etherpad-lite in a production environment, the "dirty" database driver is only for testing and/or development purposes. You can update to the latest version with `git pull origin`. The next start with bin/run.sh will update the dependencies @@ -98,7 +100,7 @@ Look at this wiki pages: You can find more information in the [wiki](https://github.com/Pita/etherpad-lite/wiki). Feel free to improve these wiki pages # Develop -If you're new to git and github, start here . +If you're new to git and github, start by watching [this video](http://youtu.be/67-Q26YH97E) then read this [git guide](http://learn.github.com/p/intro.html). If you're new to node.js, start with this video . @@ -111,11 +113,18 @@ You can join the [mailinglist](http://groups.google.com/group/etherpad-lite-dev) You also help the project, if you only host a Etherpad Lite instance and share your experience with us. +Please consider using [jshint](http://www.jshint.com/about/) if you plan to +contribute to Etherpad Lite. + # Modules created for this project * [ueberDB](https://github.com/Pita/ueberDB) "transforms every database into a object key value store" - manages all database access * [channels](https://github.com/Pita/channels) "Event channels in node.js" - ensures that ueberDB operations are atomic and in series for each key * [async-stacktrace](https://github.com/Pita/async-stacktrace) "Improves node.js stacktraces and makes it easier to handle errors" +# Donations +* [Etherpad Foundation Flattr] (http://flattr.com/thing/71378/Etherpad-Foundation) +* [Paypal] (http://etherpad.org) <-- Click the donate button + # License -[Apache License v2](http://www.apache.org/licenses/LICENSE-2.0.html) +[Apache License v2](http://www.apache.org/licenses/LICENSE-2.0.html) \ No newline at end of file diff --git a/bin/checkPad.js b/bin/checkPad.js index 9e0544415..356b07799 100644 --- a/bin/checkPad.js +++ b/bin/checkPad.js @@ -15,7 +15,8 @@ var log4js = require("log4js"); log4js.setGlobalLogLevel("INFO"); var async = require("async"); var db = require('../node/db/DB'); -var Changeset = require('../node/utils/Changeset'); +var CommonCode = require('../node/utils/common_code'); +var Changeset = CommonCode.require("/Changeset"); var padManager; async.series([ diff --git a/bin/convert.js b/bin/convert.js index d410f7a4a..c5dc535cd 100644 --- a/bin/convert.js +++ b/bin/convert.js @@ -1,10 +1,12 @@ +var CommonCode = require('../node/utils/common_code'); var startTime = new Date().getTime(); var fs = require("fs"); var ueberDB = require("ueberDB"); var mysql = require("mysql"); var async = require("async"); -var Changeset = require("../node/utils/Changeset"); -var AttributePoolFactory = require("../node/utils/AttributePoolFactory"); +var Changeset = CommonCode.require("/Changeset"); +var randomString = CommonCode.require('/pad_utils').randomString; +var AttributePoolFactory = CommonCode.require("/AttributePoolFactory"); var settingsFile = process.argv[2]; var sqlOutputFile = process.argv[3]; @@ -450,18 +452,3 @@ function parsePage(array, pageStart, offsets, data, json) start+=unitLength; } } - -/** - * Generates a random String with the given length. Is needed to generate the Author Ids - */ -function randomString(len) -{ - var chars = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"; - var randomstring = ''; - for (var i = 0; i < len; i++) - { - var rnum = Math.floor(Math.random() * chars.length); - randomstring += chars.substring(rnum, rnum + 1); - } - return randomstring; -} diff --git a/bin/installDeps.sh b/bin/installDeps.sh index a49c46b13..270ec98cd 100755 --- a/bin/installDeps.sh +++ b/bin/installDeps.sh @@ -16,7 +16,7 @@ hash curl > /dev/null 2>&1 || { #Is node installed? hash node > /dev/null 2>&1 || { - echo "Please install node.js ( http://nodesjs.org )" >&2 + echo "Please install node.js ( http://nodejs.org )" >&2 exit 1 } @@ -33,10 +33,25 @@ if [ ! $(echo $NPM_VERSION | cut -d "." -f 1) = "1" ]; then exit 1 fi -#Does a settings.json exist? if no copy the template -if [ ! -f "settings.json" ]; then - echo "Copy the settings template to settings.json..." - cp -v settings.json.template settings.json || exit 1 +#check node version +NODE_VERSION=$(node --version) +if [ ! $(echo $NODE_VERSION | cut -d "." -f 1-2) = "v0.6" ]; then + echo "You're running a wrong version of node, you're using $NODE_VERSION, we need v0.6.x" >&2 + exit 1 +fi + +#Get the name of the settings file +settings="settings.json" +a=''; +for arg in $*; do + if [ "$a" = "--settings" ] || [ "$a" = "-s" ]; then settings=$arg; fi + a=$arg +done + +#Does a $settings exist? if no copy the template +if [ ! -f $settings ]; then + echo "Copy the settings template to $settings..." + cp -v settings.json.template $settings || exit 1 fi echo "Ensure that all dependencies are up to date..." @@ -47,9 +62,9 @@ npm install || { echo "Ensure jQuery is downloaded and up to date..." DOWNLOAD_JQUERY="true" -NEEDED_VERSION="1.7" -if [ -f "static/js/jquery.min.js" ]; then - VERSION=$(cat static/js/jquery.min.js | head -n 3 | grep -o "v[0-9].[0-9]"); +NEEDED_VERSION="1.7.1" +if [ -f "static/js/jquery.js" ]; then + VERSION=$(cat static/js/jquery.js | head -n 3 | grep -o "v[0-9]\.[0-9]\(\.[0-9]\)\?"); if [ ${VERSION#v} = $NEEDED_VERSION ]; then DOWNLOAD_JQUERY="false" @@ -57,7 +72,22 @@ if [ -f "static/js/jquery.min.js" ]; then fi if [ $DOWNLOAD_JQUERY = "true" ]; then - curl -lo static/js/jquery.min.js http://code.jquery.com/jquery-$NEEDED_VERSION.min.js || exit 1 + curl -lo static/js/jquery.js http://code.jquery.com/jquery-$NEEDED_VERSION.js || exit 1 +fi + +echo "Ensure prefixfree is downloaded and up to date..." +DOWNLOAD_PREFIXFREE="true" +NEEDED_VERSION="1.0.4" +if [ -f "static/js/prefixfree.js" ]; then + VERSION=$(cat static/js/prefixfree.js | grep "PrefixFree" | grep -o "[0-9].[0-9].[0-9]"); + + if [ $VERSION = $NEEDED_VERSION ]; then + DOWNLOAD_PREFIXFREE="false" + fi +fi + +if [ $DOWNLOAD_PREFIXFREE = "true" ]; then + curl -lo static/js/prefixfree.js -k https://raw.github.com/LeaVerou/prefixfree/master/prefixfree.js || exit 1 fi #Remove all minified data to force node creating it new diff --git a/bin/jshint.sh b/bin/jshint.sh new file mode 100755 index 000000000..4dea73961 --- /dev/null +++ b/bin/jshint.sh @@ -0,0 +1,9 @@ +#!/bin/sh + +if [ -d "../bin" ]; then + cd "../" +fi + +JSHINT=./node_modules/jshint/bin/hint + +$JSHINT ./node/ diff --git a/bin/loadTesting/README b/bin/loadTesting/README new file mode 100644 index 000000000..297756f9d --- /dev/null +++ b/bin/loadTesting/README @@ -0,0 +1,75 @@ +This load tester is extremely useful for testing how many dormant clients can connect to etherpad lite. + +TODO: +Emulate characters being typed into a pad + +HOW TO USE (from @mjd75) proper formatting at: https://github.com/Pita/etherpad-lite/issues/360 + +Server 1: +Installed Node.js (etc), EtherPad Lite and MySQL + +Server 2: +Installed Xvfb and PhantomJS + +I installed Xvfb following (roughly) this guide: http://blog.martin-lyness.com/archives/installing-xvfb-on-ubuntu-9-10-karmic-koala + + #sudo apt-get install xvfb + #sudo apt-get install xfonts-100dpi xfonts-75dpi xfonts-scalable xfonts-cyrillic + +Launched two instances of Xvfb directly from the terminal: + + #Xvfb :0 -ac + #Xvfb :1 -ac + +I installed PhantomJS following this guide: http://code.google.com/p/phantomjs/wiki/Installation + + #sudo add-apt-repository ppa:jerome-etienne/neoip + #sudo apt-get update + #sudo apt-get install phantomjs + +I created a small JavaScript file for PhatomJS to use to control the browser instances: + +### BEGIN JAVASCRIPT ### + +var page = new WebPage(), + t, address; + +if (phantom.args.length === 0) { + console.log('Usage: loader.js '); + phantom.exit(); +} else { + t = Date.now(); + address = phantom.args[0]; + + var page = new WebPage(); + page.onResourceRequested = function (request) { + console.log('Request ' + JSON.stringify(request, undefined, 4)); + }; + page.onResourceReceived = function (response) { + console.log('Receive ' + JSON.stringify(response, undefined, 4)); + }; + page.open(address); + +} + +### END JAVASCRIPT ### + +And finally a launcher script that uses screen to run 400 instances of PhantomJS with the above script: + +### BEGIN SHELL SCRIPT ### + +#!/bin/bash + +# connect 200 instances to display :0 +for i in {1..200} +do + DISPLAY=:0 screen -d -m phantomjs loader.js http://ec2-50-17-168-xx.compute-1.amazonaws.com:9001/p/pad2 && sleep 2 +done + +# connect 200 instances to display :1 +for i in {1..200} +do + DISPLAY=:1 screen -d -m phantomjs loader.js http://ec2-50-17-168-xx.compute-1.amazonaws.com:9001/p/pad2 && sleep 2 +done + +### END SHELL SCRIPT ### diff --git a/bin/loadTesting/launcher.sh b/bin/loadTesting/launcher.sh new file mode 100755 index 000000000..375b15444 --- /dev/null +++ b/bin/loadTesting/launcher.sh @@ -0,0 +1,16 @@ +#!/bin/bash + +# connect 500 instances to display :0 +for i in {1..500} +do + echo $i + echo "Displaying Some shit" + DISPLAY=:0 screen -d -m /home/phantomjs/bin/phantomjs loader.js http://10.0.0.55:9001/p/pad2 && sleep 2 +done + +# connect 500 instances to display :1 +for i in {1..500} +do + echo $i + DISPLAY=:1 screen -d -m /home/phantomjs/bin/phantomjs loader.js http://10.0.0.55:9001/p/pad2 && sleep 2 +done diff --git a/bin/loadTesting/loader.js b/bin/loadTesting/loader.js new file mode 100644 index 000000000..ddcd0572d --- /dev/null +++ b/bin/loadTesting/loader.js @@ -0,0 +1,20 @@ +var page = new WebPage(), + t, address; + +if (phantom.args.length === 0) { + console.log('Usage: loader.js '); + phantom.exit(); +} else { + t = Date.now(); + address = phantom.args[0]; + + var page = new WebPage(); + page.onResourceRequested = function (request) { + console.log('Request ' + JSON.stringify(request, undefined, 4)); + }; + page.onResourceReceived = function (response) { + console.log('Receive ' + JSON.stringify(response, undefined, 4)); + }; + page.open(address); + +} diff --git a/bin/run.sh b/bin/run.sh index a5245ff77..c409920e7 100755 --- a/bin/run.sh +++ b/bin/run.sh @@ -21,9 +21,9 @@ if [ "$(id -u)" -eq 0 ]; then fi #prepare the enviroment -bin/installDeps.sh || exit 1 +bin/installDeps.sh $* || exit 1 #Move to the node folder and start echo "start..." cd "node" -node server.js +node server.js $* diff --git a/node/README.md b/node/README.md index 031810f6f..4b443289e 100644 --- a/node/README.md +++ b/node/README.md @@ -6,7 +6,7 @@ # Module name conventions -Module file names starts with a capital letter and uses camelCase +Module file names start with a capital letter and uses camelCase # Where does it start? diff --git a/node/db/API.js b/node/db/API.js index 90bfd8f66..09cc95afc 100644 --- a/node/db/API.js +++ b/node/db/API.js @@ -293,7 +293,7 @@ Example returns: exports.createPad = function(padID, text, callback) { //ensure there is no $ in the padID - if(padID.indexOf("$") != -1) + if(padID && padID.indexOf("$") != -1) { callback(new customError("createPad can't create group pads","apierror")); return; @@ -360,7 +360,7 @@ Example returns: exports.setPublicStatus = function(padID, publicStatus, callback) { //ensure this is a group pad - if(padID.indexOf("$") == -1) + if(padID && padID.indexOf("$") == -1) { callback(new customError("You can only get/set the publicStatus of pads that belong to a group","apierror")); return; @@ -393,7 +393,7 @@ Example returns: exports.getPublicStatus = function(padID, callback) { //ensure this is a group pad - if(padID.indexOf("$") == -1) + if(padID && padID.indexOf("$") == -1) { callback(new customError("You can only get/set the publicStatus of pads that belong to a group","apierror")); return; @@ -419,7 +419,7 @@ Example returns: exports.setPassword = function(padID, password, callback) { //ensure this is a group pad - if(padID.indexOf("$") == -1) + if(padID && padID.indexOf("$") == -1) { callback(new customError("You can only get/set the password of pads that belong to a group","apierror")); return; @@ -448,7 +448,7 @@ Example returns: exports.isPasswordProtected = function(padID, callback) { //ensure this is a group pad - if(padID.indexOf("$") == -1) + if(padID && padID.indexOf("$") == -1) { callback(new customError("You can only get/set the password of pads that belong to a group","apierror")); return; diff --git a/node/db/AuthorManager.js b/node/db/AuthorManager.js index f4f42d112..9baf63475 100644 --- a/node/db/AuthorManager.js +++ b/node/db/AuthorManager.js @@ -18,9 +18,11 @@ * limitations under the License. */ +var CommonCode = require('../utils/common_code'); var ERR = require("async-stacktrace"); var db = require("./DB").db; var async = require("async"); +var randomString = CommonCode.require('/pad_utils').randomString; /** * Checks if the author exists @@ -177,18 +179,3 @@ exports.setAuthorName = function (author, name, callback) { db.setSub("globalAuthor:" + author, ["name"], name, callback); } - -/** - * Generates a random String with the given length. Is needed to generate the Author Ids - */ -function randomString(len) -{ - var chars = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"; - var randomstring = ''; - for (var i = 0; i < len; i++) - { - var rnum = Math.floor(Math.random() * chars.length); - randomstring += chars.substring(rnum, rnum + 1); - } - return randomstring; -} diff --git a/node/db/GroupManager.js b/node/db/GroupManager.js index 473ea9b77..04c79cfae 100644 --- a/node/db/GroupManager.js +++ b/node/db/GroupManager.js @@ -18,8 +18,10 @@ * limitations under the License. */ +var CommonCode = require('../utils/common_code'); var ERR = require("async-stacktrace"); var customError = require("../utils/customError"); +var randomString = CommonCode.require('/pad_utils').randomString; var db = require("./DB").db; var async = require("async"); var padManager = require("./PadManager"); @@ -247,26 +249,15 @@ exports.listPads = function(groupID, callback) //group exists, let's get the pads else { - db.getSub("group:" + groupID, ["pads"], function(err, pads) + 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}); }); } }); } - -/** - * Generates a random String with the given length. Is needed to generate the Author Ids - */ -function randomString(len) -{ - var chars = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"; - var randomstring = ''; - for (var i = 0; i < len; i++) - { - var rnum = Math.floor(Math.random() * chars.length); - randomstring += chars.substring(rnum, rnum + 1); - } - return randomstring; -} diff --git a/node/db/Pad.js b/node/db/Pad.js index 7807d4643..40875effb 100644 --- a/node/db/Pad.js +++ b/node/db/Pad.js @@ -2,11 +2,11 @@ * The pad object, defined with joose */ -require('joose'); - +var CommonCode = require('../utils/common_code'); var ERR = require("async-stacktrace"); -var Changeset = require("../utils/Changeset"); -var AttributePoolFactory = require("../utils/AttributePoolFactory"); +var Changeset = CommonCode.require("/Changeset"); +var AttributePoolFactory = CommonCode.require("/AttributePoolFactory"); +var randomString = CommonCode.require('/pad_utils').randomString; var db = require("./DB").db; var async = require("async"); var settings = require('../utils/Settings'); @@ -22,490 +22,451 @@ var crypto = require("crypto"); */ exports.cleanText = function (txt) { return txt.replace(/\r\n/g,'\n').replace(/\r/g,'\n').replace(/\t/g, ' ').replace(/\xa0/g, ' '); -} +}; -Class('Pad', { - // these are the properties - has : { - - atext : { - is : 'rw', // readwrite - init : function() { return Changeset.makeAText("\n"); } // first value - }, // atext - - pool : { - is: 'rw', - init : function() { return AttributePoolFactory.createAttributePool(); }, - getterName : 'apool' // legacy - }, // pool - - head : { - is : 'rw', - init : -1, - getterName : 'getHeadRevisionNumber' - }, // head - - chatHead : { - is: 'rw', - init: -1 - }, // chatHead - - publicStatus : { - is: 'rw', - init: false, - getterName : 'getPublicStatus' - }, //publicStatus - - passwordHash : { - is: 'rw', - init: null - }, // passwordHash - - id : { is : 'r' } - }, +var Pad = function Pad(id) { - methods : { - - BUILD : function (id) - { - return { - 'id' : id, - } - }, - - appendRevision : function(aChangeset, author) - { - if(!author) - author = ''; + this.atext = Changeset.makeAText("\n"); + this.pool = AttributePoolFactory.createAttributePool(); + this.head = -1; + this.chatHead = -1; + this.publicStatus = false; + this.passwordHash = null; + this.id = id; - 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}); - }, //appendRevision - - getRevisionChangeset : function(revNum, callback) +}; + +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] != "") { - db.getSub("pad:"+this.id+":revs:"+revNum, ["changeset"], callback); - }, // getRevisionChangeset - - getRevisionAuthor : function(revNum, callback) - { - db.getSub("pad:"+this.id+":revs:"+revNum, ["meta", "author"], callback); - }, // getRevisionAuthor - - getRevisionDate : function(revNum, callback) - { - db.getSub("pad:"+this.id+":revs:"+revNum, ["meta", "timestamp"], callback); - }, // getRevisionAuthor - - getAllAuthors : function() + 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) { - var authors = []; - - for(key in this.pool.numToAttrib) - { - if(this.pool.numToAttrib[key][0] == "author" && this.pool.numToAttrib[key][1] != "") + async.parallel([ + //get the atext of the key revision + function (callback) { - authors.push(this.pool.numToAttrib[key][1]); + 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); } - } - - return authors; + ], callback); }, - - getInternalRevisionAText : function(targetRev, callback) + //apply all changesets to the key changeset + function(callback) { - var _this = this; - - var keyRev = this.getKeyRevisionNumber(targetRev); - var atext; - var changesets = []; - - //find out which changesets are needed - var neededChangesets = []; + var apool = _this.apool(); var curRev = keyRev; - while (curRev < targetRev) + + while (curRev < targetRev) { curRev++; - neededChangesets.push(curRev); + var cs = changesets[curRev]; + atext = Changeset.applyToAText(cs, atext, apool); } - - 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); - }); - }, - - getKeyRevisionNumber : function(revNum) - { - return Math.floor(revNum / 100) * 100; - }, - - text : function() - { - return this.atext.text; - }, - - setText : function(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); - }, - - appendChatMessage: function(text, userId, time) - { + + 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); - }, - - getChatMessage: function(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) +}; + +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; - callback(null, entry); + entry = _entry; + callback(); }); }, - - getLastChatMessages: function(count, callback) + //add the authorName + function(callback) { - //return an empty array if there are no chat messages - if(this.chatHead == -1) + //this chat message doesn't exist, return null + if(entry == null) { - callback(null, []); + callback(); 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 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) + //get the authorName + authorManager.getAuthorName(entry.userId, function(err, authorName) { if(ERR(err, callback)) return; + entry.userName = authorName; callback(); - }) - }, - //set in db - setPublicStatus: function(publicStatus) - { - this.publicStatus = publicStatus; - db.setSub("pad:"+this.id, ["publicStatus"], this.publicStatus); - }, - setPassword: function(password) - { - this.passwordHash = password == null ? null : hash(password, generateSalt()); - db.setSub("pad:"+this.id, ["passwordHash"], this.passwordHash); - }, - isCorrectPassword: function(password) - { - return compare(this.passwordHash, password) - }, - isPasswordProtected: function() - { - return this.passwordHash != null; + }); } - }, // methods -}); + ], 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 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 */ @@ -518,18 +479,10 @@ function hash(password, salt) function generateSalt() { - var len = 86; - var chars = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz./"; - var randomstring = ''; - for (var i = 0; i < len; i++) - { - var rnum = Math.floor(Math.random() * chars.length); - randomstring += chars.substring(rnum, rnum + 1); - } - return randomstring; + return randomString(86); } function compare(hashStr, password) { - return hash(password, hashStr.split("$")[1]) === hashStr; + return hash(password, hashStr.split("$")[1]) === hashStr; } diff --git a/node/db/PadManager.js b/node/db/PadManager.js index 36c272637..4e3a31999 100644 --- a/node/db/PadManager.js +++ b/node/db/PadManager.js @@ -20,7 +20,7 @@ var ERR = require("async-stacktrace"); var customError = require("../utils/customError"); -require("../db/Pad"); +var Pad = require("../db/Pad").Pad; var db = require("./DB").db; /** @@ -38,6 +38,15 @@ var globalPads = { 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 @@ -106,10 +115,43 @@ exports.doesPadExists = function(padId, callback) db.get("pad:"+padId, function(err, value) { if(ERR(err, callback)) return; - callback(null, value != null); + 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); diff --git a/node/db/ReadOnlyManager.js b/node/db/ReadOnlyManager.js index 73b3be9ec..e5dab99b4 100644 --- a/node/db/ReadOnlyManager.js +++ b/node/db/ReadOnlyManager.js @@ -18,9 +18,11 @@ * limitations under the License. */ +var CommonCode = require('../utils/common_code'); var ERR = require("async-stacktrace"); var db = require("./DB").db; var async = require("async"); +var randomString = CommonCode.require('/pad_utils').randomString; /** * returns a read only id for a pad @@ -70,18 +72,3 @@ exports.getPadId = function(readOnlyId, callback) { db.get("readonly2pad:" + readOnlyId, callback); } - -/** - * Generates a random String with the given length. Is needed to generate the read only ids - */ -function randomString(len) -{ - var chars = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"; - var randomstring = ''; - for (var i = 0; i < len; i++) - { - var rnum = Math.floor(Math.random() * chars.length); - randomstring += chars.substring(rnum, rnum + 1); - } - return randomstring; -} diff --git a/node/db/SecurityManager.js b/node/db/SecurityManager.js index 52d5afcbe..33ab37d44 100644 --- a/node/db/SecurityManager.js +++ b/node/db/SecurityManager.js @@ -18,6 +18,7 @@ * limitations under the License. */ +var CommonCode = require('../utils/common_code'); var ERR = require("async-stacktrace"); var db = require("./DB").db; var async = require("async"); @@ -25,6 +26,7 @@ var authorManager = require("./AuthorManager"); var padManager = require("./PadManager"); var sessionManager = require("./SessionManager"); var settings = require("../utils/Settings") +var randomString = CommonCode.require('/pad_utils').randomString; /** * This function controlls the access to a pad, it checks if the user can access a pad. diff --git a/node/db/SessionManager.js b/node/db/SessionManager.js index a394f5442..c5af33c68 100644 --- a/node/db/SessionManager.js +++ b/node/db/SessionManager.js @@ -18,8 +18,10 @@ * limitations under the License. */ +var CommonCode = require('../utils/common_code'); var ERR = require("async-stacktrace"); var customError = require("../utils/customError"); +var randomString = CommonCode.require('/pad_utils').randomString; var db = require("./DB").db; var async = require("async"); var groupMangager = require("./GroupManager"); @@ -358,21 +360,6 @@ function listSessionsWithDBKey (dbkey, callback) }); } -/** - * Generates a random String with the given length. Is needed to generate the SessionIDs - */ -function randomString(len) -{ - var chars = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"; - var randomstring = ''; - for (var i = 0; i < len; i++) - { - var rnum = Math.floor(Math.random() * chars.length); - randomstring += chars.substring(rnum, rnum + 1); - } - return randomstring; -} - //checks if a number is an int function is_int(value) { diff --git a/node/easysync_tests.js b/node/easysync_tests.js index 5b73b7170..8e7398bea 100644 --- a/node/easysync_tests.js +++ b/node/easysync_tests.js @@ -20,8 +20,9 @@ * limitations under the License. */ -var Changeset = require('./utils/Changeset'); -var AttributePoolFactory = require("./utils/AttributePoolFactory"); +var CommonCode = require('./utils/common_code'); +var Changeset = CommonCode.require("/Changeset"); +var AttributePoolFactory = CommonCode.require("/AttributePoolFactory"); function random() { this.nextInt = function (maxValue) { diff --git a/node/handler/APIHandler.js b/node/handler/APIHandler.js index 5c29f9e4c..a7f66151c 100644 --- a/node/handler/APIHandler.js +++ b/node/handler/APIHandler.js @@ -18,9 +18,12 @@ * limitations under the License. */ +var CommonCode = require('../utils/common_code'); var ERR = require("async-stacktrace"); var fs = require("fs"); var api = require("../db/API"); +var padManager = require("../db/PadManager"); +var randomString = CommonCode.require('/pad_utils').randomString; //ensure we have an apikey var apikey = null; @@ -95,7 +98,33 @@ exports.handle = function(functionName, fields, req, res) 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;idocument.domain = document.domain; var impexp = window.top.require('/pad_impexp').padimpexp.handleFrameCall('" + status + "'); ", 200); }); } diff --git a/node/handler/PadMessageHandler.js b/node/handler/PadMessageHandler.js index d86528498..48f0aa988 100644 --- a/node/handler/PadMessageHandler.js +++ b/node/handler/PadMessageHandler.js @@ -18,17 +18,17 @@ * limitations under the License. */ +var CommonCode = require('../utils/common_code'); var ERR = require("async-stacktrace"); var async = require("async"); var padManager = require("../db/PadManager"); -var Changeset = require("../utils/Changeset"); -var AttributePoolFactory = require("../utils/AttributePoolFactory"); +var Changeset = CommonCode.require("/Changeset"); +var AttributePoolFactory = CommonCode.require("/AttributePoolFactory"); var authorManager = require("../db/AuthorManager"); var readOnlyManager = require("../db/ReadOnlyManager"); var settings = require('../utils/Settings'); var securityManager = require("../db/SecurityManager"); var log4js = require('log4js'); -var os = require("os"); var messageLogger = log4js.getLogger("message"); /** @@ -517,7 +517,12 @@ exports.updatePadClients = function(pad, 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}}); @@ -539,7 +544,10 @@ exports.updatePadClients = function(pad, callback) callback ); - sessioninfos[session].rev = pad.getHeadRevisionNumber(); + if(sessioninfos[session] != null) + { + sessioninfos[session].rev = pad.getHeadRevisionNumber(); + } },callback); } @@ -755,13 +763,6 @@ function handleClientReady(client, message) var apool = attribsForWire.pool.toJsonable(); atext.attribs = attribsForWire.translated; - //check if abiword is avaiable - var abiwordAvailable = settings.abiword != null ? "yes" : "no"; - if(settings.abiword != null && os.type().indexOf("Windows") != -1) - { - abiwordAvailable = "withoutPDF"; - } - var clientVars = { "accountPrivs": { "maxRevisions": 100 @@ -798,7 +799,7 @@ function handleClientReady(client, message) "fullWidth": false, "hideSidebar": false }, - "abiwordAvailable": abiwordAvailable, + "abiwordAvailable": settings.abiwordAvailable(), "hooks": {} } @@ -852,8 +853,19 @@ function handleClientReady(client, message) //Run trough all sessions of this pad async.forEach(pad2sessions[message.padId], function(sessionID, callback) { - var sessionAuthorName, sessionAuthorColorId; - + 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) @@ -861,7 +873,7 @@ function handleClientReady(client, message) async.parallel([ function(callback) { - authorManager.getAuthorColorId(sessioninfos[sessionID].author, function(err, value) + authorManager.getAuthorColorId(author, function(err, value) { if(ERR(err, callback)) return; sessionAuthorColorId = value; @@ -870,7 +882,7 @@ function handleClientReady(client, message) }, function(callback) { - authorManager.getAuthorName(sessioninfos[sessionID].author, function(err, value) + authorManager.getAuthorName(author, function(err, value) { if(ERR(err, callback)) return; sessionAuthorName = value; @@ -885,7 +897,7 @@ function handleClientReady(client, message) if(sessionID != client.id) { //Send this Session the Notification about the new user - socketio.sockets.sockets[sessionID].json.send(messageToTheOtherUsers); + socket.json.send(messageToTheOtherUsers); //Send the new User a Notification about this other user var messageToNotifyTheClientAboutTheOthers = { @@ -897,7 +909,7 @@ function handleClientReady(client, message) "colorId": sessionAuthorColorId, "name": sessionAuthorName, "userAgent": "Anonymous", - "userId": sessioninfos[sessionID].author + "userId": author } } }; diff --git a/node/handler/TimesliderMessageHandler.js b/node/handler/TimesliderMessageHandler.js index b3493d8cb..188068430 100644 --- a/node/handler/TimesliderMessageHandler.js +++ b/node/handler/TimesliderMessageHandler.js @@ -18,11 +18,13 @@ * limitations under the License. */ +var CommonCode = require('../utils/common_code'); var ERR = require("async-stacktrace"); var async = require("async"); var padManager = require("../db/PadManager"); -var Changeset = require("../utils/Changeset"); -var AttributePoolFactory = require("../utils/AttributePoolFactory"); +var Changeset = CommonCode.require("/Changeset"); +var AttributePoolFactory = CommonCode.require("/AttributePoolFactory"); +var settings = require('../utils/Settings'); var authorManager = require("../db/AuthorManager"); var log4js = require('log4js'); var messageLogger = log4js.getLogger("message"); @@ -160,6 +162,7 @@ function createTimesliderClientVars (padId, callback) fullWidth: false, disableRightBar: false, initialChangesets: [], + abiwordAvailable: settings.abiwordAvailable(), hooks: [], initialStyledContents: {} }; diff --git a/node/server.js b/node/server.js index 461e82693..c5377d81b 100644 --- a/node/server.js +++ b/node/server.js @@ -31,6 +31,8 @@ var async = require('async'); var express = require('express'); var path = require('path'); var minify = require('./utils/Minify'); +var CachingMiddleware = require('./utils/caching_middleware'); +var Yajsml = require('yajsml'); var formidable = require('formidable'); var apiHandler; var exportHandler; @@ -45,8 +47,9 @@ var socketIORouter; var version = ""; try { - var ref = fs.readFileSync("../.git/HEAD", "utf-8"); - var refPath = "../.git/" + ref.substring(5, ref.indexOf("\n")); + var rootPath = path.normalize(__dirname + "/../") + var ref = fs.readFileSync(rootPath + ".git/HEAD", "utf-8"); + var refPath = rootPath + ".git/" + ref.substring(5, ref.indexOf("\n")); version = fs.readFileSync(refPath, "utf-8"); version = version.substring(0, 7); console.log("Your Etherpad Lite git version is " + version); @@ -60,8 +63,7 @@ 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; +exports.maxAge = settings.maxAge; //set loglevel log4js.setGlobalLogLevel(settings.loglevel); @@ -77,7 +79,39 @@ async.waterfall([ { //create server var app = express.createServer(); + + app.use(function (req, res, next) { + res.header("Server", serverName); + next(); + }); + + //redirects browser to the pad's sanitized url if needed. otherwise, renders the html + app.param('pad', function (req, res, next, padId) { + //ensure the padname is valid and the url doesn't end with a / + if(!padManager.isValidPadId(padId) || /\/$/.test(req.url)) + { + res.send('Such a padname is forbidden', 404); + } + else + { + padManager.sanitizePadId(padId, function(sanitizedPadId) { + //the pad id was sanitized, so we redirect to the sanitized version + if(sanitizedPadId != padId) + { + var real_path = req.path.replace(/^\/p\/[^\/]+/, './' + sanitizedPadId); + res.header('Location', real_path); + res.send('You should be redirected to ' + real_path + '', 302); + } + //the pad id was fine, so just render it + else + { + next(); + } + }); + } + }); + //load modules that needs a initalized db readOnlyManager = require("./db/ReadOnlyManager"); exporthtml = require("./utils/ExportHtml"); @@ -108,31 +142,26 @@ async.waterfall([ gracefulShutdown(); }); - //serve static files - app.get('/static/*', function(req, res) - { - res.header("Server", serverName); - var filePath = path.normalize(__dirname + "/.." + - req.url.replace(/\.\./g, '').split("?")[0]); - res.sendfile(filePath, { maxAge: exports.maxAge }); - }); - - //serve minified files - app.get('/minified/:id', function(req, res, next) - { - res.header("Server", serverName); - - var id = req.params.id; - - if(id == "pad.js" || id == "timeslider.js") - { - minify.minifyJS(req,res,id); - } - else - { - next(); - } + // Cache both minified and static. + var assetCache = new CachingMiddleware; + app.all('/(minified|static)/*', assetCache.handle); + + // Minify will serve static files compressed (minify enabled). It also has + // file-specific hacks for ace/require-kernel/etc. + app.all('/static/:filename(*)', minify.minify); + + // Setup middleware that will package JavaScript files served by minify for + // CommonJS loader on the client-side. + var jsServer = new (Yajsml.Server)({ + rootPath: 'minified/' + , rootURI: 'http://localhost:' + settings.port + '/static/js/' }); + var StaticAssociator = Yajsml.associators.StaticAssociator; + var associations = + Yajsml.associators.associationsForSimpleMapping(minify.tar); + var associator = new StaticAssociator(associations); + jsServer.setAssociator(associator); + app.use(jsServer); //checks for padAccess function hasPadAccess(req, res, callback) @@ -177,8 +206,6 @@ async.waterfall([ //serve read only pad app.get('/ro/:id', function(req, res) { - res.header("Server", serverName); - var html; var padId; var pad; @@ -232,18 +259,10 @@ async.waterfall([ res.send(html); }); }); - + //serve pad.html under /p app.get('/p/:pad', function(req, res, next) { - //ensure the padname is valid and the url doesn't end with a / - if(!padManager.isValidPadId(req.params.pad) || /\/$/.test(req.url)) - { - res.send('Such a padname is forbidden', 404); - return; - } - - res.header("Server", serverName); var filePath = path.normalize(__dirname + "/../static/pad.html"); res.sendfile(filePath, { maxAge: exports.maxAge }); }); @@ -251,28 +270,13 @@ async.waterfall([ //serve timeslider.html under /p/$padname/timeslider app.get('/p/:pad/timeslider', function(req, res, next) { - //ensure the padname is valid and the url doesn't end with a / - if(!padManager.isValidPadId(req.params.pad) || /\/$/.test(req.url)) - { - res.send('Such a padname is forbidden', 404); - return; - } - - res.header("Server", serverName); var filePath = path.normalize(__dirname + "/../static/timeslider.html"); res.sendfile(filePath, { maxAge: exports.maxAge }); }); //serve timeslider.html under /p/$padname/timeslider - app.get('/p/:pad/export/:type', function(req, res, next) + app.get('/p/:pad/:rev?/export/:type', function(req, res, next) { - //ensure the padname is valid and the url doesn't end with a / - if(!padManager.isValidPadId(req.params.pad) || /\/$/.test(req.url)) - { - res.send('Such a padname is forbidden', 404); - return; - } - 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) @@ -290,7 +294,6 @@ async.waterfall([ } res.header("Access-Control-Allow-Origin", "*"); - res.header("Server", serverName); hasPadAccess(req, res, function() { @@ -301,22 +304,13 @@ async.waterfall([ //handle import requests app.post('/p/:pad/import', function(req, res, next) { - //ensure the padname is valid and the url doesn't end with a / - if(!padManager.isValidPadId(req.params.pad) || /\/$/.test(req.url)) - { - res.send('Such a padname is forbidden', 404); - return; - } - //if abiword is disabled, skip handling this request if(settings.abiword == null) { next(); return; } - - res.header("Server", serverName); - + hasPadAccess(req, res, function() { importHandler.doImport(req, res, req.params.pad); @@ -326,8 +320,8 @@ async.waterfall([ var apiLogger = log4js.getLogger("API"); //This is for making an api call, collecting all post information and passing it to the apiHandler - var apiCaller = function(req, res, fields) { - res.header("Server", serverName); + var apiCaller = function(req, res, fields) + { res.header("Content-Type", "application/json; charset=utf-8"); apiLogger.info("REQUEST, " + req.params.func + ", " + JSON.stringify(fields)); @@ -388,7 +382,6 @@ async.waterfall([ //serve index.html under / app.get('/', function(req, res) { - res.header("Server", serverName); var filePath = path.normalize(__dirname + "/../static/index.html"); res.sendfile(filePath, { maxAge: exports.maxAge }); }); @@ -396,7 +389,6 @@ async.waterfall([ //serve robots.txt app.get('/robots.txt', function(req, res) { - res.header("Server", serverName); var filePath = path.normalize(__dirname + "/../static/robots.txt"); res.sendfile(filePath, { maxAge: exports.maxAge }); }); @@ -404,7 +396,6 @@ async.waterfall([ //serve favicon.ico app.get('/favicon.ico', function(req, res) { - res.header("Server", serverName); var filePath = path.normalize(__dirname + "/../static/custom/favicon.ico"); res.sendfile(filePath, { maxAge: exports.maxAge }, function(err) { diff --git a/node/utils/Abiword.js b/node/utils/Abiword.js index c53a6ab37..27138e648 100644 --- a/node/utils/Abiword.js +++ b/node/utils/Abiword.js @@ -53,7 +53,7 @@ if(os.type().indexOf("Windows") > -1) abiword.on('exit', function (code) { if(code != 0) { - throw "Abiword died with exit code " + code; + return callback("Abiword died with exit code " + code); } if(stdoutBuffer != "") @@ -75,52 +75,54 @@ if(os.type().indexOf("Windows") > -1) else { //spawn the abiword process - var abiword = spawn(settings.abiword, ["--plugin", "AbiCommand"]); - - //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) - { - throw "Abiword died with exit code " + code; - }); - - //delegate the processing of stdout to a other function - abiword.stdout.on('data',onAbiwordStdout); - + var abiword; var stdoutCallback = null; - var stdoutBuffer = ""; - var firstPrompt = true; + var spawnAbiword = function (){ + abiword = spawn(settings.abiword, ["--plugin", "AbiCommand"]); + var stdoutBuffer = ""; + var firstPrompt = true; - function onAbiwordStdout(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) + //append error messages to the buffer + abiword.stderr.on('data', function (data) { - //filter the feedback message - var err = stdoutBuffer.search("OK") != -1 ? null : stdoutBuffer; + 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(); - //reset the buffer - stdoutBuffer = ""; - - //call the callback with the error message - //skip the first prompt - if(stdoutCallback != null && !firstPrompt) + //we're searching for the prompt, cause this means everything we need is in the buffer + if(stdoutBuffer.search("AbiWord:>") != -1) { - stdoutCallback(err); - stdoutCallback = null; + //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; } - - firstPrompt = false; - } + }); } + spawnAbiword(); doConvertTask = function(task, callback) { @@ -130,6 +132,7 @@ else stdoutCallback = function (err) { callback(); + console.log("queue continue"); task.callback(err); }; } diff --git a/node/utils/Cli.js b/node/utils/Cli.js new file mode 100644 index 000000000..0c7947e91 --- /dev/null +++ b/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/node/utils/ExportDokuWiki.js b/node/utils/ExportDokuWiki.js index 48e4b2915..abe6d3471 100644 --- a/node/utils/ExportDokuWiki.js +++ b/node/utils/ExportDokuWiki.js @@ -15,7 +15,8 @@ */ var async = require("async"); -var Changeset = require("./Changeset"); +var CommonCode = require('./common_code'); +var Changeset = CommonCode.require("/Changeset"); var padManager = require("../db/PadManager"); function getPadDokuWiki(pad, revNum, callback) diff --git a/node/utils/ExportHtml.js b/node/utils/ExportHtml.js index 46ed980a5..afeafd3a9 100644 --- a/node/utils/ExportHtml.js +++ b/node/utils/ExportHtml.js @@ -14,10 +14,12 @@ * limitations under the License. */ +var CommonCode = require('./common_code'); var async = require("async"); -var Changeset = require("./Changeset"); +var Changeset = CommonCode.require("/Changeset"); var padManager = require("../db/PadManager"); var ERR = require("async-stacktrace"); +var Security = CommonCode.require('/security'); function getPadPlainText(pad, revNum) { @@ -269,7 +271,7 @@ function getHTMLFromAtext(pad, atext) //from but they break the abiword parser and are completly useless s = s.replace(String.fromCharCode(12), ""); - assem.append(_escapeHTML(s)); + assem.append(_encodeWhitespace(Security.escapeHTML(s))); } // end iteration over spans in line var tags2close = []; @@ -292,7 +294,7 @@ function getHTMLFromAtext(pad, atext) var url = urlData[1]; var urlLength = url.length; processNextChars(startIndex - idx); - assem.append(''); + assem.append(''); processNextChars(urlLength); assem.append(''); }); @@ -309,13 +311,14 @@ function getHTMLFromAtext(pad, atext) // 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 || lists.length > 0) + + if (line.listLevel)//If we are inside a list { // do list stuff var whichList = -1; // index into lists or -1 @@ -331,41 +334,89 @@ function getHTMLFromAtext(pad, atext) } } - if (whichList >= lists.length) + if (whichList >= lists.length)//means we are on a deeper level of indentation than the previous line { lists.push([line.listLevel, line.listTypeName]); - pieces.push('
      • ', lineContent || '
        '); + if(line.listTypeName == "number") + { + pieces.push('
        1. ', lineContent || '
          '); + } + else + { + pieces.push('
          • ', lineContent || '
            '); + } } - else if (whichList == -1) + //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 - pieces.push(new Array(lists.length + 1).join('
          • ')); + if(line.listTypeName == "number") + { + pieces.push(new Array(lists.length + 1).join('
        ')); + } + else + { + pieces.push(new Array(lists.length + 1).join('
      ')); + } lists.length = 0; pieces.push(lineContent, '
      '); } else { + console.log('trace 2'); pieces.push('

      '); } - } - else + }*/ + else//means we are getting closer to the lowest level of indentation { while (whichList < lists.length - 1) { - pieces.push(''); + if(lists[lists.length - 1][1] == "number") + { + pieces.push('
    '); + } + else + { + pieces.push('
  3. '); + } lists.length--; } pieces.push('
  4. ', lineContent || '
    '); } } - else + 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('
'); + } + else + { + pieces.push(''); + } + lists.length--; + } pieces.push(lineContent, '
'); } } - pieces.push(new Array(lists.length + 1).join('')); + + for (var k = lists.length - 1; k >= 0; k--) + { + if(lists[k][1] == "number") + { + pieces.push(''); + } + else + { + pieces.push(''); + } + } return pieces.join(''); } @@ -415,7 +466,24 @@ exports.getPadHTMLDocument = function (padId, revNum, noDocType, callback) { if(ERR(err, callback)) return; - var head = (noDocType ? '' : '\n') + '\n' + (noDocType ? '' : '\n' + '\n' + '\n' + '\n') + ''; + var head = + (noDocType ? '' : '\n') + + '\n' + (noDocType ? '' : '\n' + + '\n' + + '\n' + '\n') + + ''; var foot = '\n\n'; @@ -427,24 +495,7 @@ exports.getPadHTMLDocument = function (padId, revNum, noDocType, callback) }); } -function _escapeHTML(s) -{ - var re = /[&<>]/g; - if (!re.MAP) - { - // persisted across function calls! - re.MAP = { - '&': '&', - '<': '<', - '>': '>', - }; - } - - s = s.replace(re, function (c) - { - return re.MAP[c]; - }); - +function _encodeWhitespace(s) { return s.replace(/[^\x21-\x7E\s\t\n\r]/g, function(c) { return "&#" +c.charCodeAt(0) + ";" diff --git a/node/utils/ImportHtml.js b/node/utils/ImportHtml.js index 1b0bcaea7..ce8663697 100644 --- a/node/utils/ImportHtml.js +++ b/node/utils/ImportHtml.js @@ -17,9 +17,10 @@ var jsdom = require('jsdom-nocontextifiy').jsdom; var log4js = require('log4js'); -var Changeset = require("./Changeset"); -var contentcollector = require("./contentcollector"); -var map = require("../../static/js/ace2_common.js").map; +var CommonCode = require('../utils/common_code'); +var Changeset = CommonCode.require("/Changeset"); +var contentcollector = CommonCode.require("/contentcollector"); +var map = CommonCode.require("/ace2_common").map; function setPadHTML(pad, html, callback) { diff --git a/node/utils/Minify.js b/node/utils/Minify.js index fd1dd0793..a49195a7b 100644 --- a/node/utils/Minify.js +++ b/node/utils/Minify.js @@ -27,285 +27,257 @@ var cleanCSS = require('clean-css'); var jsp = require("uglify-js").parser; var pro = require("uglify-js").uglify; var path = require('path'); -var Buffer = require('buffer').Buffer; -var gzip = require('gzip'); +var RequireKernel = require('require-kernel'); var server = require('../server'); -var os = require('os'); -var padJS = ["jquery.min.js", "pad_utils.js", "plugins.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", "pad2.js", "jquery-ui.js", "chat.js", "excanvas.js", "farbtastic.js"]; +var ROOT_DIR = path.normalize(__dirname + "/../../static/"); +var TAR_PATH = path.join(__dirname, 'tar.json'); +var tar = JSON.parse(fs.readFileSync(TAR_PATH, 'utf8')); -var timesliderJS = ["jquery.min.js", "plugins.js", "undo-xpopup.js", "json2.js", "colorutils.js", "draggable.js", "pad_utils.js", "pad_cookie.js", "pad_editor.js", "pad_editbar.js", "pad_docbar.js", "pad_modals.js", "easysync2_client.js", "domline_client.js", "linestylefilter_client.js", "cssmanager_client.js", "broadcast.js", "broadcast_slider.js", "broadcast_revisions.js"]; +// 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.minifyJS = function(req, res, jsFilename) +exports.minify = function(req, res, next) { - res.header("Content-Type","text/javascript"); - - //choose the js files we need - if(jsFilename == "pad.js") - { - jsFiles = padJS; + 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; } - else if(jsFilename == "timeslider.js") - { - jsFiles = timesliderJS; + + // 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"; } - else - { - throw new Error("there is no profile for creating " + name); - } - - //minifying is enabled - if(settings.minify) - { - var fileValues = {}; - var embeds = {}; - var latestModification = 0; - - async.series([ - //find out the highest modification date - function(callback) - { - var folders2check = ["../static/css","../static/js"]; - - //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("../var/minified_" + jsFilename, 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"); - } + + 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(); }); - }, - //load all js files - function (callback) - { - async.forEach(jsFiles, function (item, callback) - { - fs.readFile("../static/js/" + item, "utf-8", function(err, data) - { - if(ERR(err, callback)) return; - fileValues[item] = data; - callback(); - }); - }, callback); - }, - //find all includes in ace.js and embed them - function(callback) - { - //if this is not the creation of pad.js, skip this part - if(jsFilename != "pad.js") - { - callback(); - return; + } 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? } - - var founds = fileValues["ace.js"].match(/\$\$INCLUDE_[a-zA-Z_]+\([a-zA-Z0-9.\/_"]+\)/gi); - - //go trough all includes - async.forEach(founds, function (item, callback) - { - var filename = item.match(/"[^"]*"/g)[0].substr(1); - filename = filename.substr(0,filename.length-1); - - var type = item.match(/INCLUDE_[A-Z]+/g)[0].substr("INCLUDE_".length); - - var quote = item.search("_Q") != -1; - - //read the included file - fs.readFile(filename, "utf-8", function(err, data) - { - if(ERR(err, callback)) return; - - //compress the file - if(type == "JS") - { - embeds[item] = " -
-
New Pad

or create/open a Pad with the name
-
- - -
-
+======= + // start the custom js + if (typeof customStart == "function") customStart(); + + +>>>>>>> 6fd73ecfda27fd6639c30a4c05c6149d66f669ee diff --git a/node/utils/AttributePoolFactory.js b/static/js/AttributePoolFactory.js similarity index 97% rename from node/utils/AttributePoolFactory.js rename to static/js/AttributePoolFactory.js index 807c2b393..00b58dbb3 100644 --- a/node/utils/AttributePoolFactory.js +++ b/static/js/AttributePoolFactory.js @@ -1,8 +1,8 @@ /** - * This code represents the Attribute Pool Object of the original Etherpad. + * 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: + * You can find a explanation what a attribute pool is here: * https://github.com/Pita/etherpad-lite/blob/master/doc/easysync/easysync-notes.txt */ diff --git a/node/utils/Changeset.js b/static/js/Changeset.js similarity index 86% rename from node/utils/Changeset.js rename to static/js/Changeset.js index 9e1b60ebe..81c0c81b2 100644 --- a/node/utils/Changeset.js +++ b/static/js/Changeset.js @@ -1,10 +1,10 @@ /* * 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 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 */ @@ -25,16 +25,30 @@ * limitations under the License. */ -var AttributePoolFactory = require("./AttributePoolFactory"); +var AttributePoolFactory = require("/AttributePoolFactory"); var _opt = null; -//var exports = {}; +/** + * ==================== 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(''); @@ -42,12 +56,30 @@ exports.assert = function assert(b, msgParts) { } }; +/** + * 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); @@ -57,13 +89,34 @@ exports.toBaseTen = function (cs) { }) + 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; @@ -129,12 +182,21 @@ exports.opIterator = function (opsStr, optStartIndex) { }; }; +/** + * 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 || ''), @@ -143,6 +205,11 @@ exports.newOp = function (optOpcode) { attribs: '' }; }; + +/** + * Clones an Op + * @param op Op to be cloned + */ exports.cloneOp = function (op) { return { opcode: op.opcode, @@ -151,12 +218,22 @@ exports.cloneOp = function (op) { 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'; @@ -164,11 +241,19 @@ exports.opString = function (op) { 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) @@ -218,6 +303,15 @@ exports.checkRep = function (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. @@ -474,6 +568,10 @@ if (_opt) { }; } +/** + * A custom made String Iterator + * @param str {string} String to be iterated over + */ exports.stringIterator = function (str) { var curIndex = 0; @@ -510,6 +608,9 @@ exports.stringIterator = function (str) { }; }; +/** + * A custom made StringBuffer + */ exports.stringAssembler = function () { var pieces = []; @@ -526,7 +627,11 @@ exports.stringAssembler = function () { }; }; -// "lines" need not be an array as long as it supports certain calls (lines_foo inside). +/** + * 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 @@ -781,6 +886,21 @@ exports.textLinesMutator = function (lines) { 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); @@ -802,6 +922,11 @@ exports.applyZip = function (in1, idx1, in2, idx2, func) { 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); @@ -823,6 +948,14 @@ exports.unpack = function (cs) { }; }; +/** + * 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)); @@ -831,6 +964,11 @@ exports.pack = function (oldLen, newLen, 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); @@ -856,6 +994,11 @@ exports.applyToText = function (cs, str) { 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); @@ -878,6 +1021,13 @@ exports.mutateTextLines = function (cs, lines) { 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 @@ -935,6 +1085,10 @@ exports.composeAttributes = function (att1, att2, resultIsMutation, pool) { 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. @@ -1021,6 +1175,12 @@ exports._slicerZipperFunc = function (attOp, csOp, opOut, pool) { } }; +/** + * 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); @@ -1129,6 +1289,11 @@ exports.mutateAttributionLines = function (cs, lines, pool) { //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++) { @@ -1179,10 +1344,20 @@ exports.splitAttributionLines = function (attrOps, text) { 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); @@ -1225,10 +1400,14 @@ exports.compose = function (cs1, cs2, pool) { 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) { - // 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. if (!pool) { return never; } @@ -1247,10 +1426,27 @@ exports.attributeTester = function (attribPair, pool) { } }; +/** + * 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; @@ -1271,8 +1467,14 @@ exports.makeSplice = function (oldFullText, spliceStart, numRemoved, newText, op 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) { - // get a list of splices, [startChar, endChar, newText] + // var unpacked = exports.unpack(cs); var splices = []; @@ -1302,6 +1504,9 @@ exports.toSplices = function (cs) { return splices; }; +/** + * + */ exports.characterRangeFollow = function (cs, startChar, endChar, insertionsAfter) { var newStartChar = startChar; var newEndChar = endChar; @@ -1346,6 +1551,14 @@ exports.characterRangeFollow = function (cs, startChar, endChar, insertionsAfter 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('$'); @@ -1363,13 +1576,22 @@ exports.moveOpsToNewPool = function (cs, oldPool, newPool) { }) + 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(); }; -// callable on a exports, attribution string, or attribs property of an op +/** + * 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) { @@ -1383,12 +1605,21 @@ exports.eachAttribNumber = function (cs, func) { }); }; -// callable on a exports, attribution string, or attribs property of an op, -// though it may easily create adjacent ops that can be merged. +/** + * 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) { @@ -1410,6 +1641,12 @@ exports.mapAttribNumbers = function (cs, func) { 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, @@ -1417,6 +1654,12 @@ exports.makeAText = function (text, attribs) { }; }; +/** + * 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), @@ -1424,6 +1667,10 @@ exports.applyToAText = function (cs, atext, pool) { }; }; +/** + * Clones a AText structure + * @param atext {AText} + */ exports.cloneAText = function (atext) { return { text: atext.text, @@ -1431,11 +1678,20 @@ exports.cloneAText = function (atext) { }; }; +/** + * 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); @@ -1469,6 +1725,11 @@ exports.appendATextToAssembler = function (atext, assem) { } }; +/** + * 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); @@ -1478,15 +1739,32 @@ exports.prepareForWire = function (cs, pool) { }; }; +/** + * 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) { @@ -1499,6 +1777,11 @@ exports.attribsAttributeValue = function (attribs, key, pool) { 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(); diff --git a/static/js/ace.js b/static/js/ace.js index 69d3cf09b..22d4eaa6e 100644 --- a/static/js/ace.js +++ b/static/js/ace.js @@ -28,9 +28,10 @@ Ace2Editor.registry = { nextId: 1 }; +var plugins = require('/plugins').plugins; + function Ace2Editor() { - var thisFunctionsName = "Ace2Editor"; var ace2 = Ace2Editor; var editor = {}; @@ -48,8 +49,7 @@ function Ace2Editor() { var that = this; var args = arguments; - - function action() + var action = function() { func.apply(that, args); } @@ -70,78 +70,47 @@ function Ace2Editor() function doActionsPendingInit() { - for (var i = 0; i < actionsPendingInit.length; i++) - { - actionsPendingInit[i](); - } + $.each(actionsPendingInit, function(i,fn){ + fn() + }); actionsPendingInit = []; } - + ace2.registry[info.id] = info; - editor.importText = pendingInit(function(newCode, undoable) - { - info.ace_importText(newCode, undoable); - }); - editor.importAText = pendingInit(function(newCode, apoolJsonObj, undoable) - { - info.ace_importAText(newCode, apoolJsonObj, undoable); + // 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.focus = pendingInit(function() - { - info.ace_focus(); - }); - editor.setEditable = pendingInit(function(newVal) - { - info.ace_setEditable(newVal); - }); - editor.getFormattedCode = function() - { - return info.ace_getFormattedCode(); - }; - editor.setOnKeyPress = pendingInit(function(handler) - { - info.ace_setOnKeyPress(handler); - }); - editor.setOnKeyDown = pendingInit(function(handler) - { - info.ace_setOnKeyDown(handler); - }); - editor.setNotifyDirty = pendingInit(function(handler) - { - info.ace_setNotifyDirty(handler); - }); - - editor.setProperty = pendingInit(function(key, value) - { - info.ace_setProperty(key, value); - }); + editor.getDebugProperty = function(prop) { return info.ace_getDebugProperty(prop); }; - editor.setBaseText = pendingInit(function(txt) - { - info.ace_setBaseText(txt); - }); - editor.setBaseAttributedText = pendingInit(function(atxt, apoolJsonObj) - { - info.ace_setBaseAttributedText(atxt, apoolJsonObj); - }); - editor.applyChangesToBase = pendingInit(function(changes, optAuthor, apoolJsonObj) - { - info.ace_applyChangesToBase(changes, optAuthor, apoolJsonObj); - }); // 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). @@ -156,24 +125,6 @@ function Ace2Editor() if (!loaded) return null; return info.ace_prepareUserChangeset(); }; - editor.applyPreparedChangesetToBase = pendingInit( - - function() - { - info.ace_applyPreparedChangesetToBase(); - }); - editor.setUserChangeNotificationCallback = pendingInit(function(callback) - { - info.ace_setUserChangeNotificationCallback(callback); - }); - editor.setAuthorInfo = pendingInit(function(author, authorInfo) - { - info.ace_setAuthorInfo(author, authorInfo); - }); - editor.setAuthorSelectionRange = pendingInit(function(author, start, end) - { - info.ace_setAuthorSelectionRange(author, start, end); - }); editor.getUnhandledErrors = function() { @@ -182,45 +133,71 @@ function Ace2Editor() return info.ace_getUnhandledErrors(); }; - editor.callWithAce = pendingInit(function(fn, callStack, normalize) - { - return info.ace_callWithAce(fn, callStack, normalize); - }); - - editor.execCommand = pendingInit(function(cmd, arg1) - { - info.ace_execCommand(cmd, arg1); - }); - editor.replaceRange = pendingInit(function(start, end, text) - { - info.ace_replaceRange(start, end, text); - }); - // 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. - var $$INCLUDE_CSS = function(fileName) - { - return ''; - }; - var $$INCLUDE_JS = function(fileName) - { - return '\x3cscript type="text/javascript" src="' + fileName + '">\x3c/script>'; - }; - var $$INCLUDE_JS_DEV = $$INCLUDE_JS; - var $$INCLUDE_CSS_DEV = $$INCLUDE_CSS; + function sortFilesByEmbeded(files) { + var embededFiles = []; + var remoteFiles = []; - var $$INCLUDE_CSS_Q = function(fileName) - { - return '\'\''; - }; - var $$INCLUDE_JS_Q = function(fileName) - { - return '\'\\x3cscript type="text/javascript" src="' + fileName + '">\\x3c/script>\''; - }; - var $$INCLUDE_JS_Q_DEV = $$INCLUDE_JS_Q; - var $$INCLUDE_CSS_Q_DEV = $$INCLUDE_CSS_Q; + 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("../minified/");\nrequire.setGlobalKeyPath("require");' + if (Ace2Editor.EMBEDED && Ace2Editor.EMBEDED[KERNEL_SOURCE]) { + buffer.push('\ +'); + pushScriptsTo(iframeHTML); + + iframeHTML.push(''); + iframeHTML.push(' '); + + // 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.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 = (' + iframeHTML.join('+') + ').replace(/\\\\x3c/g, \'<\');doc.write(text); doc.close(); ' + '}, 0); }'; + 'outerdocbody.insertBefore(iframe, outerdocbody.firstChild); ' + 'iframe.ace_outerWin = window; ' + 'readyFunc = function() { editorInfo.onEditorReady(); readyFunc = null; editorInfo = null; }; ' + 'var doc = iframe.contentWindow.document; doc.open(); var text = (' + JSON.stringify(iframeHTML.join('\n')) + ');doc.write(text); doc.close(); ' + '}, 0); }'; + + var outerHTML = [doctype, ''] + + 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); - var outerHTML = [doctype, '', $$INCLUDE_CSS("../static/css/iframe_editor.css"), $$INCLUDE_CSS("../static/css/pad.css"), $$INCLUDE_CSS("../static/custom/pad.css"), // bizarrely, in FF2, a file with no "external" dependencies won't finish loading properly // (throbs busy while typing) - '', '\x3cscript>\n', outerScript, '\n\x3c/script>', '
x
']; - - if (!Array.prototype.map) Array.prototype.map = function(fun) - { //needed for IE - if (typeof fun != "function") throw new TypeError(); - var len = this.length; - var res = new Array(len); - var thisp = arguments[1]; - for (var i = 0; i < len; i++) - { - if (i in this) res[i] = fun.call(thisp, this[i], i, this); - } - return res; - }; + outerHTML.push('', '\x3cscript>\n', outerScript.replace(/<\//g, '<\\/'), '\n\x3c/script>', '
x
'); var outerFrame = document.createElement("IFRAME"); outerFrame.frameBorder = 0; // for IE @@ -308,3 +302,5 @@ function Ace2Editor() return editor; } + +exports.Ace2Editor = Ace2Editor; diff --git a/static/js/ace2_common.js b/static/js/ace2_common.js index 1246a16ec..0f8195fa7 100644 --- a/static/js/ace2_common.js +++ b/static/js/ace2_common.js @@ -20,6 +20,7 @@ * limitations under the License. */ +var Security = require('/security'); function isNodeText(node) { @@ -80,14 +81,8 @@ function isArray(testObject) return testObject && typeof testObject === 'object' && !(testObject.propertyIsEnumerable('length')) && typeof testObject.length === 'number'; } -if (typeof exports !== "undefined") -{ - userAgent = "node-js"; -} -else -{ - userAgent = navigator.userAgent.toLowerCase(); -} +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], @@ -95,7 +90,8 @@ var browser = { opera: /opera/.test(userAgent), msie: /msie/.test(userAgent) && !/opera/.test(userAgent), mozilla: /mozilla/.test(userAgent) && !/(compatible|webkit)/.test(userAgent), - windows: /windows/.test(userAgent) // dgreensp + windows: /windows/.test(userAgent), + mobile: /mobile/.test(userAgent) || /android/.test(userAgent) }; @@ -142,10 +138,25 @@ function binarySearchInfinite(expectedLength, func) function htmlPrettyEscape(str) { - return str.replace(/&/g, '&').replace(//g, '>').replace(/\r?\n/g, '\\n'); + return Security.escapeHTML(str).replace(/\r?\n/g, '\\n'); } -if (typeof exports !== "undefined") -{ - exports.map = map; -} +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/static/js/ace2_inner.js b/static/js/ace2_inner.js index 65f8ec7e7..17d02036f 100644 --- a/static/js/ace2_inner.js +++ b/static/js/ace2_inner.js @@ -20,9 +20,38 @@ * limitations under the License. */ -function OUTER(gscope) -{ +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; @@ -85,7 +114,7 @@ function OUTER(gscope) 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; @@ -93,13 +122,13 @@ function OUTER(gscope) 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]] = function() - {}; + console[names[i]] = noop; //console.error = function(str) { alert(str); }; } @@ -118,14 +147,6 @@ function OUTER(gscope) }; } - function noop() - {} - - function identity(x) - { - return x; - } - // "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 @@ -207,12 +228,35 @@ function OUTER(gscope) { bgcolor = fadeColor(bgcolor, info.fade); } - - dynamicCSS.selectorStyle(getAuthorColorClassSelector( - getAuthorClassName(author))).backgroundColor = bgcolor; - dynamicCSSTop.selectorStyle(getAuthorColorClassSelector( - getAuthorClassName(author))).backgroundColor = bgcolor; + 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; + } } } } @@ -302,7 +346,7 @@ function OUTER(gscope) editorInfo.ace_getRep = function() { return rep; - } + }; var currentCallStack = null; @@ -424,12 +468,11 @@ function OUTER(gscope) submitOldEvent(cs.editEvent); if (cs.domClean && cs.type != "setup") { - if (cs.isUserChange) - { - if (cs.repChanged) parenModule.notifyChange(); - else parenModule.notifyTick(); - } - recolorModule.recolorLines(); + // if (cs.isUserChange) + // { + // if (cs.repChanged) parenModule.notifyChange(); + // else parenModule.notifyTick(); + // } if (cs.selectionAffected) { updateBrowserSelectionFromRep(); @@ -486,230 +529,7 @@ function OUTER(gscope) { return rep.lines.atOffset(charOffset).key; } - - var recolorModule = (function() - { - var dirtyLineKeys = {}; - - var module = {}; - module.setCharNeedsRecoloring = function(offset) - { - if (offset >= rep.alltext.length) - { - offset = rep.alltext.length - 1; - } - dirtyLineKeys[getLineKeyForOffset(offset)] = true; - } - - module.setCharRangeNeedsRecoloring = function(offset1, offset2) - { - if (offset1 >= rep.alltext.length) - { - offset1 = rep.alltext.length - 1; - } - if (offset2 >= rep.alltext.length) - { - offset2 = rep.alltext.length - 1; - } - var firstEntry = rep.lines.atOffset(offset1); - var lastKey = rep.lines.atOffset(offset2).key; - dirtyLineKeys[lastKey] = true; - var entry = firstEntry; - while (entry && entry.key != lastKey) - { - dirtyLineKeys[entry.key] = true; - entry = rep.lines.next(entry); - } - } - - module.recolorLines = function() - { - for (var k in dirtyLineKeys) - { - recolorLineByKey(k); - } - dirtyLineKeys = {}; - } - - return module; - })(); - - var parenModule = (function() - { - var module = {}; - module.notifyTick = function() - { - handleFlashing(false); - }; - module.notifyChange = function() - { - handleFlashing(true); - }; - module.shouldNormalizeOnChar = function(c) - { - if (parenFlashRep.active) - { - // avoid highlight style from carrying on to typed text - return true; - } - c = String.fromCharCode(c); - return !!(bracketMap[c]); - } - - var parenFlashRep = { - active: false, - whichChars: null, - whichLineKeys: null, - expireTime: null - }; - var bracketMap = { - '(': 1, - ')': -1, - '[': 2, - ']': -2, - '{': 3, - '}': -3 - }; - var bracketRegex = /[{}\[\]()]/g; - - function handleFlashing(docChanged) - { - function getSearchRange(aroundLoc) - { - var rng = getVisibleCharRange(); - var d = 100; // minimum radius - var e = 3000; // maximum radius; - if (rng[0] > aroundLoc - d) rng[0] = aroundLoc - d; - if (rng[0] < aroundLoc - e) rng[0] = aroundLoc - e; - if (rng[0] < 0) rng[0] = 0; - if (rng[1] < aroundLoc + d) rng[1] = aroundLoc + d; - if (rng[1] > aroundLoc + e) rng[1] = aroundLoc + e; - if (rng[1] > rep.lines.totalWidth()) rng[1] = rep.lines.totalWidth(); - return rng; - } - - function findMatchingVisibleBracket(startLoc, forwards) - { - var rng = getSearchRange(startLoc); - var str = rep.alltext.substring(rng[0], rng[1]); - var bstr = str.replace(bracketRegex, '('); // handy for searching - var loc = startLoc - rng[0]; - var bracketState = []; - var foundParen = false; - var goodParen = false; - - function nextLoc() - { - if (loc < 0) return; - if (forwards) loc++; - else loc--; - if (loc < 0 || loc >= str.length) loc = -1; - if (loc >= 0) - { - if (forwards) loc = bstr.indexOf('(', loc); - else loc = bstr.lastIndexOf('(', loc); - } - } - while ((!foundParen) && (loc >= 0)) - { - if (getCharType(loc + rng[0]) == "p") - { - var b = bracketMap[str.charAt(loc)]; // -1, 1, -2, 2, -3, 3 - var into = forwards; - var typ = b; - if (typ < 0) - { - into = !into; - typ = -typ; - } - if (into) bracketState.push(typ); - else - { - var recent = bracketState.pop(); - if (recent != typ) - { - foundParen = true; - goodParen = false; - } - else if (bracketState.length == 0) - { - foundParen = true; - goodParen = true; - } - } - } - //console.log(bracketState.toSource()); - if ((!foundParen) && (loc >= 0)) nextLoc(); - } - if (!foundParen) return null; - return { - chr: (loc + rng[0]), - good: goodParen - }; - } - - var r = parenFlashRep; - var charsToHighlight = null; - var linesToUnhighlight = null; - if (r.active && (docChanged || (now() > r.expireTime))) - { - linesToUnhighlight = r.whichLineKeys; - r.active = false; - } - if ((!r.active) && docChanged && isCaret() && caretColumn() > 0) - { - var caret = caretDocChar(); - if (caret > 0 && getCharType(caret - 1) == "p") - { - var charBefore = rep.alltext.charAt(caret - 1); - if (bracketMap[charBefore]) - { - var lookForwards = (bracketMap[charBefore] > 0); - var findResult = findMatchingVisibleBracket(caret - 1, lookForwards); - if (findResult) - { - var mateLoc = findResult.chr; - var mateGood = findResult.good; - r.active = true; - charsToHighlight = {}; - charsToHighlight[caret - 1] = 'flash'; - charsToHighlight[mateLoc] = (mateGood ? 'flash' : 'flashbad'); - r.whichLineKeys = []; - r.whichLineKeys.push(getLineKeyForOffset(caret - 1)); - r.whichLineKeys.push(getLineKeyForOffset(mateLoc)); - r.expireTime = now() + 4000; - newlyActive = true; - } - } - } - - } - if (linesToUnhighlight) - { - recolorLineByKey(linesToUnhighlight[0]); - recolorLineByKey(linesToUnhighlight[1]); - } - if (r.active && charsToHighlight) - { - function f(txt, cls, next, ofst) - { - var flashClass = charsToHighlight[ofst]; - if (cls) - { - next(txt, cls + " " + flashClass); - } - else next(txt, cls); - } - for (var c in charsToHighlight) - { - recolorLinesInRange((+c), (+c) + 1, null, f); - } - } - } - - return module; - })(); - + function dispose() { disposed = true; @@ -743,7 +563,7 @@ function OUTER(gscope) alineLength += o.chars; if (opIter.hasNext()) { - if (o.lines != 0) error(); + if (o.lines !== 0) error(); } else { @@ -1061,9 +881,9 @@ function OUTER(gscope) editorInfo.ace_callWithAce = function(fn, callStack, normalize) { var wrapper = function() - { - return fn(editorInfo); - } + { + return fn(editorInfo); + }; @@ -1074,7 +894,7 @@ function OUTER(gscope) { editorInfo.ace_fastIncorp(9); wrapper1(); - } + }; } if (callStack !== undefined) @@ -1085,59 +905,49 @@ function OUTER(gscope) { 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) { - var k = key.toLowerCase(); - if (k == "wraps") - { - setWraps(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) } - else if (k == "showsauthorcolors") - { - setClassPresence(root, "authorColors", !! value); - } - else if (k == "showsuserselections") - { - setClassPresence(root, "userSelections", !! value); - } - else if (k == "showslinenumbers") - { - hasLineNumbers = !! value; - setClassPresence(sideDiv, "sidedivhidden", !hasLineNumbers); - fixView(); - } - else if (k == "grayedout") - { - setClassPresence(outerWin.document.body, "grayedout", !! value); - } - else if (k == "dmesg") - { - dmesg = value; - window.dmesg = value; - } - else if (k == 'userauthor') - { - thisAuthor = String(value); - } - else if (k == 'styled') - { - setStyled(value); - } - else if (k == 'textface') - { - setTextFace(value); - } - else if (k == 'textsize') - { - setTextSize(value); - } - else if (k == 'rtlistrue') - { - setClassPresence(root, "rtl", !! value); - } - } + }; editorInfo.ace_setBaseText = function(txt) { @@ -1236,12 +1046,12 @@ function OUTER(gscope) lastElapsed = elapsed; return false; } - } + }; isTimeUp.elapsed = function() { return now() - startTime; - } + }; return isTimeUp; } @@ -1299,7 +1109,7 @@ function OUTER(gscope) { unschedule(); } - } + }; } function fastIncorp(n) @@ -1491,7 +1301,7 @@ function OUTER(gscope) } var text = lineEntry.text; var width = lineEntry.width; // text.length+1 - if (text.length == 0) + if (text.length === 0) { // allow getLineStyleFilter to set line-div styles var func = linestylefilter.getLineStyleFilter( @@ -1510,12 +1320,6 @@ function OUTER(gscope) } } - - function getCharType(charIndex) - { - return ''; - } - var observedChanges; function clearObservedChanges() @@ -1624,17 +1428,19 @@ function OUTER(gscope) 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) { - function topLevel(n) - { - if ((!n) || n == root) return null; - while (n.parentNode != root) - { - n = n.parentNode; - } - return n; - } var node1 = topLevel(selection.startPoint.node); var node2 = topLevel(selection.endPoint.node); if (node1) observeChangesAroundNode(node1); @@ -1707,7 +1513,7 @@ function OUTER(gscope) { 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)))) + if (!((a === 0 || getCleanNodeByKey(rep.lines.atIndex(a - 1).key)) && (b == rep.lines.length() || getCleanNodeByKey(rep.lines.atIndex(b).key)))) { dirtyRangesCheckOut = false; break; @@ -1748,7 +1554,7 @@ function OUTER(gscope) var range = dirtyRanges[i]; a = range[0]; b = range[1]; - var firstDirtyNode = (((a == 0) && root.firstChild) || getCleanNodeByKey(rep.lines.atIndex(a - 1).key).nextSibling); + 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); @@ -2046,7 +1852,7 @@ function OUTER(gscope) function handleReturnIndentation() { // on return, indent to level of previous line - if (isCaret() && caretColumn() == 0 && caretLine() > 0) + if (isCaret() && caretColumn() === 0 && caretLine() > 0) { var lineNum = caretLine(); var thisLine = rep.lines.atIndex(lineNum); @@ -2128,10 +1934,10 @@ function OUTER(gscope) var lineNode = lineEntry.lineNode; var n = lineNode; var after = false; - if (charsLeft == 0) + if (charsLeft === 0) { var index = 0; - if (browser.msie && line == (rep.lines.length() - 1) && lineNode.childNodes.length == 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; @@ -2195,7 +2001,7 @@ function OUTER(gscope) // assuming the point is not in a dirty node. if (point.node == root) { - if (point.index == 0) + if (point.index === 0) { return [0, 0]; } @@ -2233,7 +2039,7 @@ function OUTER(gscope) n = parNode; } } - if (n.id == "") console.debug("BAD"); + if (n.id === "") console.debug("BAD"); if (n.firstChild && isBlockElement(n.firstChild)) { col += 1; // lineMarker @@ -2458,7 +2264,7 @@ function OUTER(gscope) function performDocumentReplaceCharRange(startChar, endChar, newText) { - if (startChar == endChar && newText.length == 0) + if (startChar == endChar && newText.length === 0) { return; } @@ -2475,7 +2281,7 @@ function OUTER(gscope) endChar--; newText = '\n' + newText.substring(0, newText.length - 1); } - else if (newText.length == 0) + else if (newText.length === 0) { // a delete at end startChar--; @@ -2493,8 +2299,8 @@ function OUTER(gscope) function performDocumentReplaceRange(start, end, newText) { - if (start == undefined) start = rep.selStart; - if (end == undefined) end = rep.selEnd; + 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 @@ -2734,7 +2540,7 @@ function OUTER(gscope) spliceEnd--; commonEnd++; } - if (shortOldText.length == 0 && spliceStart == rep.alltext.length && shortNewText.length > 0) + if (shortOldText.length === 0 && spliceStart == rep.alltext.length && shortNewText.length > 0) { // inserting after final newline, bad spliceStart--; @@ -2742,7 +2548,7 @@ function OUTER(gscope) shortNewText = '\n' + shortNewText.slice(0, -1); shiftFinalNewlineToBeforeNewText = true; } - if (spliceEnd == rep.alltext.length && shortOldText.length > 0 && shortNewText.length == 0) + if (spliceEnd == rep.alltext.length && shortOldText.length > 0 && shortNewText.length === 0) { // deletion at end of rep.alltext if (rep.alltext.charAt(spliceStart - 1) == '\n') @@ -2754,7 +2560,7 @@ function OUTER(gscope) } } - if (!(shortOldText.length == 0 && shortNewText.length == 0)) + if (!(shortOldText.length === 0 && shortNewText.length === 0)) { var oldDocText = rep.alltext; var oldLen = oldDocText.length; @@ -2762,15 +2568,15 @@ function OUTER(gscope) var spliceStartLine = rep.lines.indexOfOffset(spliceStart); var spliceStartLineStart = rep.lines.offsetOfIndex(spliceStartLine); - function startBuilder() + var startBuilder = function() { var builder = Changeset.builder(oldLen); builder.keep(spliceStartLineStart, spliceStartLine); builder.keep(spliceStart - spliceStartLineStart); return builder; - } + }; - function eachAttribRun(attribs, func /*(startInNewText, endInNewText, attribs)*/ ) + var eachAttribRun = function(attribs, func /*(startInNewText, endInNewText, attribs)*/ ) { var attribsIter = Changeset.opIterator(attribs); var textIndex = 0; @@ -2786,7 +2592,7 @@ function OUTER(gscope) } textIndex = nextIndex; } - } + }; var justApplyStyles = (shortNewText == shortOldText); var theChangeset; @@ -2986,7 +2792,7 @@ function OUTER(gscope) var newEndIter = attribIterator(newARuns, true); while (commonEnd < minLen) { - if (commonEnd == 0) + if (commonEnd === 0) { // assume newline in common oldEndIter(); @@ -3107,11 +2913,12 @@ function OUTER(gscope) // 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 = ""; + 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) @@ -3128,10 +2935,11 @@ function OUTER(gscope) lineClass = ''; // non-null to cause update }; - function writeClass() + var writeClass = function() { if (lineClass !== null) lineElem.className = lineClass; - } + }; + result.prepareForAdd = writeClass; result.finishUpdate = writeClass; result.getInnerHTML = function() @@ -3358,7 +3166,7 @@ function OUTER(gscope) } } - if (N == 0) + if (N === 0) { p.cancel(); if (!isConsecutive(0)) @@ -3480,24 +3288,20 @@ function OUTER(gscope) function handleClick(evt) { - //hide the dropdowns - window.top.padeditbar.toogleDropDown("none"); - 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 - - - function isLink(n) - { - return (n.tagName || '').toLowerCase() == "a" && n.href; - } var n = evt.target; while (n && n.parentNode && !isLink(n)) { @@ -3517,6 +3321,10 @@ function OUTER(gscope) 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() @@ -3528,16 +3336,36 @@ function OUTER(gscope) var lineNum = rep.selStart[0]; var listType = getLineListType(lineNum); - performDocumentReplaceSelection('\n'); if (listType) { - if (lineNum + 1 < rep.lines.length()) + 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") { - setLineListType(lineNum + 1, listType); + //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(); } } @@ -3552,7 +3380,7 @@ function OUTER(gscope) var firstLine, lastLine; firstLine = rep.selStart[0]; - lastLine = Math.max(firstLine, rep.selEnd[0] - ((rep.selEnd[1] == 0) ? 1 : 0)); + lastLine = Math.max(firstLine, rep.selEnd[0] - ((rep.selEnd[1] === 0) ? 1 : 0)); var mods = []; for (var n = firstLine; n <= lastLine; n++) @@ -3681,6 +3509,15 @@ function OUTER(gscope) } } } + //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 @@ -3829,7 +3666,7 @@ function OUTER(gscope) //scrollSelectionIntoView(); specialHandled = true; } - if ((!specialHandled) && isTypeForCmdKey && String.fromCharCode(which).toLowerCase() == "z" && (evt.metaKey || evt.ctrlKey)) + if ((!specialHandled) && isTypeForCmdKey && String.fromCharCode(which).toLowerCase() == "z" && (evt.metaKey || evt.ctrlKey) && !evt.altKey) { // cmd-Z (undo) fastIncorp(6); @@ -3898,7 +3735,7 @@ function OUTER(gscope) } else if (type == "keypress") { - if ((!specialHandled) && parenModule.shouldNormalizeOnChar(charCode)) + if ((!specialHandled) && false /*parenModule.shouldNormalizeOnChar(charCode)*/) { idleWorkTimer.atMost(0); } @@ -3915,7 +3752,7 @@ function OUTER(gscope) } // Is part of multi-keystroke international character on Firefox Mac - var isFirefoxHalfCharacter = (browser.mozilla && evt.altKey && charCode == 0 && keyCode == 0); + 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); @@ -4014,7 +3851,7 @@ function OUTER(gscope) { var text = entry.text; var content; - if (text.length == 0) + if (text.length === 0) { content = '--'; } @@ -4050,8 +3887,6 @@ function OUTER(gscope) catch (e) {} if (!origSelectionRange) return false; - var selectionParent = origSelectionRange.parentElement(); - if (selectionParent.ownerDocument != doc) return false; return true; } @@ -4082,27 +3917,27 @@ function OUTER(gscope) var selectionParent = origSelectionRange.parentElement(); if (selectionParent.ownerDocument != doc) return null; - function newRange() + var newRange = function() { return doc.body.createTextRange(); - } + }; - function rangeForElementNode(nd) + var rangeForElementNode = function(nd) { var rng = newRange(); // doesn't work on text nodes rng.moveToElementText(nd); return rng; - } + }; - function pointFromCollapsedRange(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) + if (rng.compareEndPoints("StartToStart", rangeWithin) === 0) { return { node: parNode, @@ -4110,7 +3945,7 @@ function OUTER(gscope) maxIndex: 1 }; } - else if (rng.compareEndPoints("EndToEnd", rangeWithin) == 0) + else if (rng.compareEndPoints("EndToEnd", rangeWithin) === 0) { if (isBlockElement(parNode) && parNode.nextSibling) { @@ -4128,7 +3963,7 @@ function OUTER(gscope) maxIndex: 1 }; } - else if (parNode.childNodes.length == 0) + else if (parNode.childNodes.length === 0) { return { node: parNode, @@ -4237,9 +4072,10 @@ function OUTER(gscope) index: tn.nodeValue.length, maxIndex: tn.nodeValue.length }; - } + }; + var selection = {}; - if (origSelectionRange.compareEndPoints("StartToEnd", origSelectionRange) == 0) + if (origSelectionRange.compareEndPoints("StartToEnd", origSelectionRange) === 0) { // collapsed var pnt = pointFromCollapsedRange(origSelectionRange); @@ -4259,10 +4095,10 @@ function OUTER(gscope) 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); + console.log(selection.startPoint.node.uniqueId()+","+ + selection.startPoint.index+" / "+ + selection.endPoint.node.uniqueId()+","+ + selection.endPoint.index); }*/ } return selection; @@ -4305,7 +4141,7 @@ function OUTER(gscope) maxIndex: n.nodeValue.length }; } - else if (childCount == 0) + else if (childCount === 0) { return { node: n, @@ -4431,7 +4267,7 @@ function OUTER(gscope) setCollapsedBefore(s, n); s.move("character", point.index); } - else if (point.index == 0) + else if (point.index === 0) { setCollapsedBefore(s, n); } @@ -4519,7 +4355,7 @@ function OUTER(gscope) while (p.node.childNodes.length > 0) { //&& (p.node == root || p.node.parentNode == root)) { - if (p.index == 0) + if (p.index === 0) { p.node = p.node.firstChild; p.maxIndex = nodeMaxIndex(p.node); @@ -4622,7 +4458,7 @@ function OUTER(gscope) function fixView() { // calling this method repeatedly should be fast - if (getInnerWidth() == 0 || getInnerHeight() == 0) + if (getInnerWidth() === 0 || getInnerHeight() === 0) { return; } @@ -4834,7 +4670,6 @@ function OUTER(gscope) function bindTheEventHandlers() { - bindEventHandler(window, "unload", teardown); bindEventHandler(document, "keydown", handleKeyEvent); bindEventHandler(document, "keypress", handleKeyEvent); bindEventHandler(document, "keyup", handleKeyEvent); @@ -5041,7 +4876,7 @@ function OUTER(gscope) } if (!isNodeText(node)) { - if (index == 0) return leftOf(node); + if (index === 0) return leftOf(node); else return rightOf(node); } else @@ -5179,7 +5014,83 @@ function OUTER(gscope) [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]; @@ -5226,9 +5137,18 @@ function OUTER(gscope) { 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 doInsertUnorderedList() + function doInsertList(type) { if (!(rep.selStart && rep.selEnd)) { @@ -5237,13 +5157,13 @@ function OUTER(gscope) var firstLine, lastLine; firstLine = rep.selStart[0]; - lastLine = Math.max(firstLine, rep.selEnd[0] - ((rep.selEnd[1] == 0) ? 1 : 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, 'bullet'.length) != 'bullet') + if (!listType || listType.slice(0, type.length) != type) { allLinesAreList = false; break; @@ -5262,11 +5182,19 @@ function OUTER(gscope) level = Number(listType[2]); } var t = getLineListType(n); - mods.push([n, allLinesAreList ? 'indent' + level : (t ? 'bullet' + level : 'bullet1')]); + 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() { @@ -5439,7 +5367,7 @@ function OUTER(gscope) // move by "paragraph", a feature that Firefox lacks but IE and Safari both have if (up) { - if (focusCaret[1] == 0 && canChangeLines) + if (focusCaret[1] === 0 && canChangeLines) { focusCaret[0]--; focusCaret[1] = 0; @@ -5667,16 +5595,19 @@ function OUTER(gscope) { var newNumLines = rep.lines.length(); if (newNumLines < 1) newNumLines = 1; - //update height of all current line numbers + //update height of all current line numbers + + var a = sideDivInner.firstChild; + var b = doc.body.firstChild; + var n = 0; + if (currentCallStack && currentCallStack.domClean) { - var a = sideDivInner.firstChild; - var b = doc.body.firstChild; - var n = 0; + while (a && b) { - if(n > lineNumbersShown) //all updated, break - break; + if(n > lineNumbersShown) //all updated, break + break; var h = (b.clientHeight || b.offsetHeight); if (b.nextSibling) @@ -5692,12 +5623,12 @@ function OUTER(gscope) { var hpx = h + "px"; if (a.style.height != hpx) { - a.style.height = hpx; - } + a.style.height = hpx; + } } a = a.nextSibling; b = b.nextSibling; - n++; + n++; } } @@ -5711,17 +5642,20 @@ function OUTER(gscope) 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"; + //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; + fragment.appendChild(div); + b = b.nextSibling; } + container.appendChild(fragment); while (lineNumbersShown > newNumLines) { @@ -5731,6 +5665,6 @@ function OUTER(gscope) } } -}; +} -OUTER(this); +exports.editor = new Ace2Inner(); diff --git a/static/js/broadcast.js b/static/js/broadcast.js index 865574abd..97256bbfd 100644 --- a/static/js/broadcast.js +++ b/static/js/broadcast.js @@ -20,45 +20,22 @@ * limitations under the License. */ -var global = this; +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'); -function loadBroadcastJS() +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) { - // just in case... (todo: this must be somewhere else in the client code.) - // Below Array#map code was direct pasted by AppJet/Etherpad, licence unknown. Possible source: http://www.tutorialspoint.com/javascript/array_map.htm - if (!Array.prototype.map) - { - Array.prototype.map = function(fun /*, thisp*/ ) - { - var len = this.length >>> 0; - if (typeof fun != "function") throw new TypeError(); - - var res = new Array(len); - var thisp = arguments[1]; - for (var i = 0; i < len; i++) - { - if (i in this) res[i] = fun.call(thisp, this[i], i, this); - } - - return res; - }; - } - - // Below Array#forEach code was direct pasted by AppJet/Etherpad, licence unknown. Possible source: http://www.tutorialspoint.com/javascript/array_foreach.htm - if (!Array.prototype.forEach) - { - Array.prototype.forEach = function(fun /*, thisp*/ ) - { - var len = this.length >>> 0; - if (typeof fun != "function") throw new TypeError(); - - var thisp = arguments[1]; - for (var i = 0; i < len; i++) - { - if (i in this) fun.call(thisp, this[i], i, this); - } - }; - } + 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) @@ -91,11 +68,6 @@ function loadBroadcastJS() } } - function randomString() - { - return "_" + Math.floor(Math.random() * 1000000); - } - // for IE if ($.browser.msie) { @@ -107,7 +79,7 @@ function loadBroadcastJS() {} } - var userId = "hiddenUser" + randomString(); + var socketId; //var socket; var channelState = "DISCONNECTED"; @@ -183,10 +155,7 @@ function loadBroadcastJS() // splice the lines splice: function(start, numRemoved, newLinesVA) { - var newLines = Array.prototype.slice.call(arguments, 2).map( - - function(s) - { + var newLines = map(Array.prototype.slice.call(arguments, 2), function(s) { return s; }); @@ -308,10 +277,13 @@ function loadBroadcastJS() padContents.currentTime += timeDelta * 1000; debugLog('Time Delta: ', timeDelta) updateTimer(); - BroadcastSlider.setAuthors(padContents.getActiveAuthors().map(function(name) + + var authors = map(padContents.getActiveAuthors(), function(name) { return authorData[name]; - })); + }); + + BroadcastSlider.setAuthors(authors); } function updateTimer() @@ -411,13 +383,14 @@ function loadBroadcastJS() changesetLoader.queueUp(start, 1, update); } - BroadcastSlider.setAuthors(padContents.getActiveAuthors().map(function(name) - { + + var authors = map(padContents.getActiveAuthors(), function(name){ return authorData[name]; - })); + }); + BroadcastSlider.setAuthors(authors); } - global.changesetLoader = { + changesetLoader = { running: false, resolved: [], requestQueue1: [], @@ -553,10 +526,12 @@ function loadBroadcastJS() var authorMap = {}; authorMap[obj.author] = obj.data; receiveAuthorData(authorMap); - BroadcastSlider.setAuthors(padContents.getActiveAuthors().map(function(name) - { + + var authors = map(padContents.getActiveAuthors(),function(name) { return authorData[name]; - })); + }); + + BroadcastSlider.setAuthors(authors); } else if (obj['type'] == "NEW_SAVEDREV") { @@ -608,53 +583,6 @@ function loadBroadcastJS() })); } -/*function setUpSocket() - { - // required for Comet - if ((!$.browser.msie) && (!($.browser.mozilla && $.browser.version.indexOf("1.8.") == 0))) - { - document.domain = document.domain; // for comet - } - - var success = false; - callCatchingErrors("setUpSocket", function () - { - appLevelDisconnectReason = null; - - 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 () - { - setChannelState("CONNECTED"); - var msg = { - type: "CLIENT_READY", - roomType: 'padview', - roomName: 'padview/' + clientVars.viewId, - data: { - lastRev: clientVars.revNum, - userInfo: { - userId: userId - } - } - }; - sendMessage(msg); - }); - // socket.onhiccup = wrapRecordingErrors("socket.onhiccup", handleCometHiccup); - // socket.onlogmessage = function(x) {debugLog(x); }; - socket.connect(); - success = true; - }); - if (success) - { - //initialStartConnectTime = +new Date(); - } - else - { - abandonConnection("initsocketfail"); - } - }*/ function setChannelState(newChannelState, moreInfo) { @@ -683,7 +611,7 @@ function loadBroadcastJS() window.onload = function () { window['isloaded'] = true; - window['onloadFuncts'].forEach(function (funct) + forEach(window['onloadFuncts'],function (funct) { funct(); }); @@ -750,11 +678,17 @@ function loadBroadcastJS() var bgcolor = typeof data.colorId == "number" ? clientVars.colorPalette[data.colorId] : data.colorId; if (bgcolor && dynamicCSS) { - dynamicCSS.selectorStyle('.' + linestylefilter.getAuthorClassName(author)).backgroundColor = bgcolor; + 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/static/js/broadcast_revisions.js b/static/js/broadcast_revisions.js index 33a2f9dfa..19f3f5ff7 100644 --- a/static/js/broadcast_revisions.js +++ b/static/js/broadcast_revisions.js @@ -22,7 +22,6 @@ // 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. -var global = this; function loadBroadcastRevisionsJS() { @@ -125,3 +124,5 @@ function loadBroadcastRevisionsJS() }; } } + +exports.loadBroadcastRevisionsJS = loadBroadcastRevisionsJS; diff --git a/static/js/broadcast_slider.js b/static/js/broadcast_slider.js index af5a50411..0bdc2cabc 100644 --- a/static/js/broadcast_slider.js +++ b/static/js/broadcast_slider.js @@ -19,10 +19,12 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -var global = this; -function loadBroadcastSliderJS() + // These parameters were global, now they are injected. A reference to the + // Timeslider controller would probably be more appropriate. +function loadBroadcastSliderJS(fireWhenAllScriptsAreLoaded) { + var BroadcastSlider; (function() { // wrap this code in its own namespace @@ -203,7 +205,7 @@ function loadBroadcastSliderJS() } } - global.BroadcastSlider = { + BroadcastSlider = { onSlider: onSlider, getSliderPosition: getSliderPosition, setSliderPosition: setSliderPosition, @@ -495,4 +497,8 @@ function loadBroadcastSliderJS() { $("#viewlatest").html(loc == BroadcastSlider.getSliderLength() ? "Viewing latest content" : "View latest content"); }) + + return BroadcastSlider; } + +exports.loadBroadcastSliderJS = loadBroadcastSliderJS; diff --git a/static/js/changesettracker.js b/static/js/changesettracker.js index cc7f64623..e34dc107e 100644 --- a/static/js/changesettracker.js +++ b/static/js/changesettracker.js @@ -20,6 +20,8 @@ * limitations under the License. */ +var AttribPool = require('/AttributePoolFactory').createAttributePool; +var Changeset = require('/Changeset'); function makeChangesetTracker(scheduler, apool, aceCallbacksProvider) { @@ -207,3 +209,5 @@ function makeChangesetTracker(scheduler, apool, aceCallbacksProvider) }; } + +exports.makeChangesetTracker = makeChangesetTracker; diff --git a/static/js/chat.js b/static/js/chat.js index 4dade69f3..8f076af6a 100644 --- a/static/js/chat.js +++ b/static/js/chat.js @@ -20,62 +20,45 @@ * limitations under the License. */ +var padutils = require('/pad_utils').padutils; +var padcookie = require('/pad_cookie').padcookie; + var chat = (function() { - var ua = navigator.userAgent.toLowerCase(); - var isAndroid = ua.indexOf("android") > -1; - var isMobileSafari = ua.indexOf("mobile") > -1; - var bottomMargin = "0px"; - var sDuration = 500; - var hDuration = 750; + var isStuck = false; var chatMentions = 0; var title = document.title; - if (isAndroid || isMobileSafari){ - sDuration = 0; - hDuration = 0; - } var self = { show: function () { - $("#chaticon").hide("slide", { - direction: "down" - }, hDuration, function () - { - $("#chatbox").show("slide", { - direction: "down" - }, sDuration, self.scrollDown); - $("#chatbox").resizable( - { - handles: 'nw', - minHeight: 40, - minWidth: 80, - start: function (event, ui) - { - $("#focusprotector").show(); - }, - stop: function (event, ui) - { - $("#focusprotector").hide(); - - if(isAndroid || isMobileSafari) - bottommargin = "32px"; - - $("#chatbox").css({right: "20px", bottom: bottomMargin, left: "", top: ""}); - - self.scrollDown(); - } - }); - }); + $("#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"); - $("#chatbox").hide("slide", { direction: "down" }, sDuration, function() - { - $("#chaticon").show("slide", { direction: "down" }, hDuration); - }); + $("#chaticon").show(); + $("#chatbox").hide(); }, scrollDown: function() { @@ -85,13 +68,13 @@ var chat = (function() send: function() { var text = $("#chatinput").val(); - pad.collabClient.sendMessage({"type": "CHAT_MESSAGE", "text": text}); + this._pad.collabClient.sendMessage({"type": "CHAT_MESSAGE", "text": text}); $("#chatinput").val(""); }, addMessage: function(msg, increment) { //correct the time - msg.time += pad.clientTimeOffset; + msg.time += this._pad.clientTimeOffset; //create the time string var minutes = "" + new Date(msg.time).getMinutes(); @@ -109,7 +92,7 @@ var chat = (function() return 'z' + c.charCodeAt(0) + 'z'; }); - var text = padutils.escapeHtmlWithClickableLinks(padutils.escapeHtml(msg.text), "_blank"); + var text = padutils.escapeHtmlWithClickableLinks(msg.text, "_blank"); /* Performs an action if your name is mentioned */ var myName = $('#myusernameedit').val(); @@ -123,7 +106,7 @@ var chat = (function() var authorName = msg.userName == null ? "unnamed" : padutils.escapeHtml(msg.userName); - var html = "

" + authorName + ":" + timeStr + " " + text + "

"; + var html = "

" + authorName + ":" + timeStr + " " + text + "

"; $("#chattext").append(html); //should we increment the counter?? @@ -138,23 +121,22 @@ var chat = (function() if (chatMentions == 0){ title = document.title; } - $('#chatthrob').html(""+authorName+"" + ": " + text); - $('#chatthrob').effect("pulsate", {times:1,mode:"hide"},4000); + $('#chatthrob').html(""+authorName+"" + ": " + text).show().delay(4000).hide(400); chatMentions++; document.title = "("+chatMentions+") " + title; } else { - $('#chatthrob').html(""+authorName+"" + ": " + text); - $('#chatthrob').effect("pulsate", {times:1,mode:"hide"},2000); + $('#chatthrob').html(""+authorName+"" + ": " + text).show().delay(2000).hide(400); } } self.scrollDown(); }, - init: function() + init: function(pad) { + this._pad = pad; $("#chatinput").keypress(function(evt) { //if the user typed enter, fire the send @@ -175,3 +157,6 @@ var chat = (function() return self; }()); + +exports.chat = chat; + diff --git a/static/js/collab_client.js b/static/js/collab_client.js index fafe0fd2f..bb68f6040 100644 --- a/static/js/collab_client.js +++ b/static/js/collab_client.js @@ -20,17 +20,22 @@ * limitations under the License. */ -$(window).bind("load", function() -{ - getCollabClient.windowLoaded = true; -}); +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) +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; @@ -79,13 +84,6 @@ function getCollabClient(ace2editor, serverVars, initialUserInfo, options) {} }; - $(window).bind("unload", function() - { - if (socket) - { - setChannelState("DISCONNECTED", "unload"); - } - }); if ($.browser.mozilla) { // Prevent "escape" from taking effect and canceling a comet connection; @@ -111,7 +109,7 @@ function getCollabClient(ace2editor, serverVars, initialUserInfo, options) function handleUserChanges() { - if ((!socket) || channelState == "CONNECTING") + if ((!getSocket()) || channelState == "CONNECTING") { if (channelState == "CONNECTING" && (((+new Date()) - initialStartConnectTime) > 20000)) { @@ -258,19 +256,6 @@ function getCollabClient(ace2editor, serverVars, initialUserInfo, options) }*/ } - function setUpSocketWhenWindowLoaded() - { - if (getCollabClient.windowLoaded) - { - setUpSocket(); - } - else - { - setTimeout(setUpSocketWhenWindowLoaded, 200); - } - } - setTimeout(setUpSocketWhenWindowLoaded, 0); - var hiccupCount = 0; function handleCometHiccup(params) @@ -295,7 +280,7 @@ function getCollabClient(ace2editor, serverVars, initialUserInfo, options) function sendMessage(msg) { - socket.json.send( + getSocket().json.send( { type: "COLLABROOM", component: "pad", @@ -337,7 +322,7 @@ function getCollabClient(ace2editor, serverVars, initialUserInfo, options) { if (window.console) console.log(evt); - if (!socket) return; + if (!getSocket()) return; if (!evt.data) return; var wrapper = evt; if (wrapper.type != "COLLABROOM") return; @@ -442,7 +427,7 @@ function getCollabClient(ace2editor, serverVars, initialUserInfo, options) userInfo.userId = userId; userSet[userId] = userInfo; tellAceActiveAuthorInfo(userInfo); - if (!socket) return; + if (!getSocket()) return; sendMessage( { type: "USERINFO_UPDATE", @@ -644,8 +629,7 @@ function getCollabClient(ace2editor, serverVars, initialUserInfo, options) }, 0); } - var self; - return (self = { + var self = { setOnUserJoin: function(cb) { callbacks.onUserJoin = cb; @@ -688,7 +672,10 @@ function getCollabClient(ace2editor, serverVars, initialUserInfo, options) callWhenNotCommitting: callWhenNotCommitting, addHistoricalAuthors: tellAceAboutHistoricalAuthors, setChannelState: setChannelState - }); + }; + + $(document).ready(setUpSocket); + return self; } function selectElementContents(elem) @@ -714,3 +701,6 @@ function selectElementContents(elem) } } } + +exports.getCollabClient = getCollabClient; +exports.selectElementContents = selectElementContents; diff --git a/static/js/colorutils.js b/static/js/colorutils.js index 92d8da719..5fbefb4df 100644 --- a/static/js/colorutils.js +++ b/static/js/colorutils.js @@ -119,3 +119,20 @@ 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/static/js/contentcollector.js b/static/js/contentcollector.js index 69036ba25..96dc4b7dd 100644 --- a/static/js/contentcollector.js +++ b/static/js/contentcollector.js @@ -25,6 +25,9 @@ var _MAX_LIST_LEVEL = 8; +var Changeset = require('/Changeset'); +var plugins = require('/plugins').plugins; + function sanitizeUnicode(s) { return s.replace(/[\uffff\ufffe\ufeff\ufdd0-\ufdef\ud800-\udfff]/g, '?'); @@ -34,15 +37,7 @@ function makeContentCollector(collectStyles, browser, apool, domInterface, class { browser = browser || {}; - var plugins_; - if (typeof(plugins) != 'undefined') - { - plugins_ = plugins; - } - else - { - plugins_ = parent.parent.plugins; - } + var plugins_ = plugins; var dom = domInterface || { isNodeText: function(n) @@ -476,7 +471,7 @@ function makeContentCollector(collectStyles, browser, apool, domInterface, class { cc.doAttrib(state, "strikethrough"); } - if (tname == "ul") + if (tname == "ul" || tname == "ol") { var type; var rr = cls && /(?:^| )list-([a-z]+[12345678])\b/.exec(cls); @@ -692,3 +687,6 @@ function makeContentCollector(collectStyles, browser, apool, domInterface, class return cc; } + +exports.sanitizeUnicode = sanitizeUnicode; +exports.makeContentCollector = makeContentCollector; diff --git a/static/js/cssmanager.js b/static/js/cssmanager.js index b8208b83f..46075e578 100644 --- a/static/js/cssmanager.js +++ b/static/js/cssmanager.js @@ -118,3 +118,5 @@ function makeCSSManager(emptyStylesheetTitle, top) } }; } + +exports.makeCSSManager = makeCSSManager; diff --git a/static/js/cssmanager_client.js b/static/js/cssmanager_client.js deleted file mode 100644 index 60bf9f8c5..000000000 --- a/static/js/cssmanager_client.js +++ /dev/null @@ -1,116 +0,0 @@ -/** - * 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/cssmanager.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. - */ - -function makeCSSManager(emptyStylesheetTitle) -{ - - function getSheetByTitle(title) - { - 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= 0) - { - browserDeleteRule(i); - selectorList.splice(i, 1); - } - } - - return { - selectorStyle: selectorStyle, - removeSelectorStyle: removeSelectorStyle, - info: function() - { - return selectorList.length + ":" + browserRules().length; - } - }; -} diff --git a/static/js/domline.js b/static/js/domline.js index 56f74a1cd..3074c9e93 100644 --- a/static/js/domline.js +++ b/static/js/domline.js @@ -25,13 +25,15 @@ // requires: top // requires: plugins // requires: undefined + +var Security = require('/security'); +var Ace2Common = require('/ace2_common'); +var plugins = require('/plugins').plugins; +var map = Ace2Common.map; +var noop = Ace2Common.noop; +var identity = Ace2Common.identity; + var domline = {}; -domline.noop = function() -{}; -domline.identity = function(x) -{ - return x; -}; domline.addToLineClass = function(lineClass, cls) { @@ -55,11 +57,11 @@ domline.createDomLine = function(nonEmpty, doesWrap, optBrowser, optDocument) { var result = { node: null, - appendSpan: domline.noop, - prepareForAdd: domline.noop, - notifyAdded: domline.noop, - clearSpans: domline.noop, - finishUpdate: domline.noop, + appendSpan: noop, + prepareForAdd: noop, + notifyAdded: noop, + clearSpans: noop, + finishUpdate: noop, lineMarker: 0 }; @@ -86,7 +88,7 @@ domline.createDomLine = function(nonEmpty, doesWrap, optBrowser, optDocument) { return domline.processSpaces(s, doesWrap); } - var identity = domline.identity; + var perTextNodeProcess = (doesWrap ? identity : processSpaces); var perHtmlLineProcess = (doesWrap ? processSpaces : identity); var lineClass = 'ace-line'; @@ -95,13 +97,23 @@ domline.createDomLine = function(nonEmpty, doesWrap, optBrowser, optDocument) 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) { - preHtml = ''; + if(listType.indexOf("number") < 0) + { + preHtml = ''; + } + else + { + preHtml = '
  1. '; + postHtml = '
'; + } } result.lineMarker += txt.length; return; // don't append any text @@ -130,20 +142,12 @@ domline.createDomLine = function(nonEmpty, doesWrap, optBrowser, optDocument) var extraOpenTags = ""; var extraCloseTags = ""; - var plugins_; - if (typeof(plugins) != 'undefined') - { - plugins_ = plugins; - } - else - { - plugins_ = parent.parent.plugins; - } + var plugins_ = plugins; - plugins_.callHook("aceCreateDomLine", { + map(plugins_.callHook("aceCreateDomLine", { domline: domline, cls: cls - }).map(function(modifier) + }), function(modifier) { cls = modifier.cls; extraOpenTags = extraOpenTags + modifier.extraOpenTags; @@ -162,7 +166,7 @@ domline.createDomLine = function(nonEmpty, doesWrap, optBrowser, optDocument) { href = "http://"+href; } - extraOpenTags = extraOpenTags + ''; + extraOpenTags = extraOpenTags + ''; extraCloseTags = '' + extraCloseTags; } if (simpleTags) @@ -172,7 +176,7 @@ domline.createDomLine = function(nonEmpty, doesWrap, optBrowser, optDocument) simpleTags.reverse(); extraCloseTags = '' + extraCloseTags; } - html.push('', extraOpenTags, perTextNodeProcess(domline.escapeHTML(txt)), extraCloseTags, ''); + html.push('', extraOpenTags, perTextNodeProcess(Security.escapeHTML(txt)), extraCloseTags, ''); } }; result.clearSpans = function() @@ -218,27 +222,6 @@ domline.createDomLine = function(nonEmpty, doesWrap, optBrowser, optDocument) return result; }; -domline.escapeHTML = function(s) -{ - var re = /[&<>'"]/g; - /']/; // stupid indentation thing - if (!re.MAP) - { - // persisted across function calls! - re.MAP = { - '&': '&', - '<': '<', - '>': '>', - '"': '"', - "'": ''' - }; - } - return s.replace(re, function(c) - { - return re.MAP[c]; - }); -}; - domline.processSpaces = function(s, doesWrap) { if (s.indexOf("<") < 0 && !doesWrap) @@ -300,3 +283,5 @@ domline.processSpaces = function(s, doesWrap) } return parts.join(''); }; + +exports.domline = domline; diff --git a/static/js/domline_client.js b/static/js/domline_client.js deleted file mode 100644 index a152412c9..000000000 --- a/static/js/domline_client.js +++ /dev/null @@ -1,298 +0,0 @@ -/** - * 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/domline.js -// THIS FILE IS ALSO AN APPJET MODULE: etherpad.collab.ace.domline -/** - * 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 domline = {}; -domline.noop = function() -{}; -domline.identity = function(x) -{ - return x; -}; - -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: domline.noop, - prepareForAdd: domline.noop, - notifyAdded: domline.noop, - clearSpans: domline.noop, - finishUpdate: domline.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 identity = domline.identity; - 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); - if (listType) - { - listType = listType[1]; - if (listType) - { - preHtml = ''; - } - 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 = ""; - - var plugins_; - if (typeof(plugins) != 'undefined') - { - plugins_ = plugins; - } - else - { - plugins_ = parent.parent.plugins; - } - - plugins_.callHook("aceCreateDomLine", { - domline: domline, - cls: cls, - document: document - }).map(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) - { - extraOpenTags = extraOpenTags + ''; - extraCloseTags = '' + extraCloseTags; - } - if (simpleTags) - { - simpleTags.sort(); - extraOpenTags = extraOpenTags + '<' + simpleTags.join('><') + '>'; - simpleTags.reverse(); - extraCloseTags = '' + extraCloseTags; - } - html.push('', extraOpenTags, perTextNodeProcess(domline.escapeHTML(txt)), extraCloseTags, ''); - } - }; - 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 += '
'; - } - } - 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.escapeHTML = function(s) -{ - var re = /[&<>'"]/g; - /']/; // stupid indentation thing - if (!re.MAP) - { - // persisted across function calls! - re.MAP = { - '&': '&', - '<': '<', - '>': '>', - '"': '"', - "'": ''' - }; - } - return s.replace(re, function(c) - { - return re.MAP[c]; - }); -}; - -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(''); -}; diff --git a/static/js/draggable.js b/static/js/draggable.js index 7bf523eb5..8d1975459 100644 --- a/static/js/draggable.js +++ b/static/js/draggable.js @@ -193,3 +193,5 @@ function makeResizableHPane(left, sep, right, minLeft, minRight, sepWidth, sepOf } }); } + +exports.makeDraggable = makeDraggable; diff --git a/static/js/easysync2.js b/static/js/easysync2.js deleted file mode 100644 index 17ab585d9..000000000 --- a/static/js/easysync2.js +++ /dev/null @@ -1,2510 +0,0 @@ -/** - * 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.easysync2 -// %APPJET%: jimport("com.etherpad.Easysync2Support"); -/** - * 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 _opt = (this.Easysync2Support || null); -var _opt = null; // disable optimization for now - -function AttribPool() -{ - 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; -} - -var Changeset = {}; - -Changeset.error = function error(msg) -{ - var e = new Error(msg); - e.easysync = true; - throw e; -}; -Changeset.assert = function assert(b, msgParts) -{ - if (!b) - { - var msg = Array.prototype.slice.call(arguments, 1).join(''); - Changeset.error("Changeset: " + msg); - } -}; - -Changeset.parseNum = function(str) -{ - return parseInt(str, 36); -}; -Changeset.numToString = function(num) -{ - return num.toString(36).toLowerCase(); -}; -Changeset.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(Changeset.parseNum(s)); - }) + fromDollar; -}; - -Changeset.oldLen = function(cs) -{ - return Changeset.unpack(cs).oldLen; -}; -Changeset.newLen = function(cs) -{ - return Changeset.unpack(cs).newLen; -}; - -Changeset.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() == '?') - { - Changeset.error("Hit error opcode in op stream"); - } - curIndex = result.lastIndex(); - } - } - else - { - regex.lastIndex = curIndex; - result = regex.exec(opsStr); - curIndex = regex.lastIndex; - if (result[0] == '?') - { - Changeset.error("Hit error opcode in op stream"); - } - } - return result; - } - var regexResult = nextRegexMatch(); - var obj = Changeset.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 = Changeset.parseNum(regexResult[2] || 0); - op.opcode = regexResult[3]; - op.chars = Changeset.parseNum(regexResult[4]); - regexResult = nextRegexMatch(); - } - else - { - Changeset.clearOp(op); - } - return op; - } - - function hasNext() - { - return !!(_opt ? regexResult : regexResult[0]); - } - - function lastIndex() - { - return prevIndex; - } - return { - next: next, - hasNext: hasNext, - lastIndex: lastIndex - }; -}; - -Changeset.clearOp = function(op) -{ - op.opcode = ''; - op.chars = 0; - op.lines = 0; - op.attribs = ''; -}; -Changeset.newOp = function(optOpcode) -{ - return { - opcode: (optOpcode || ''), - chars: 0, - lines: 0, - attribs: '' - }; -}; -Changeset.cloneOp = function(op) -{ - return { - opcode: op.opcode, - chars: op.chars, - lines: op.lines, - attribs: op.attribs - }; -}; -Changeset.copyOp = function(op1, op2) -{ - op2.opcode = op1.opcode; - op2.chars = op1.chars; - op2.lines = op1.lines; - op2.attribs = op1.attribs; -}; -Changeset.opString = function(op) -{ - // just for debugging - if (!op.opcode) return 'null'; - var assem = Changeset.opAssembler(); - assem.append(op); - return assem.toString(); -}; -Changeset.stringOp = function(str) -{ - // just for debugging - return Changeset.opIterator(str).next(); -}; - -Changeset.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 = Changeset.unpack(cs); - var oldLen = unpacked.oldLen; - var newLen = unpacked.newLen; - var ops = unpacked.ops; - var charBank = unpacked.charBank; - - var assem = Changeset.smartOpAssembler(); - var oldPos = 0; - var calcNewLen = 0; - var numInserted = 0; - var iter = Changeset.opIterator(ops); - while (iter.hasNext()) - { - var o = iter.next(); - switch (o.opcode) - { - case '=': - oldPos += o.chars; - calcNewLen += o.chars; - break; - case '-': - oldPos += o.chars; - Changeset.assert(oldPos < oldLen, oldPos, " >= ", oldLen, " in ", cs); - break; - case '+': - { - calcNewLen += o.chars; - numInserted += o.chars; - Changeset.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 = Changeset.pack(oldLen, calcNewLen, assem.toString(), charBank); - Changeset.assert(normalized == cs, normalized, ' != ', cs); - - return cs; -} - -Changeset.smartOpAssembler = function() -{ - // Like opAssembler but able to produce conforming changesets - // 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 = Changeset.mergingOpAssembler(); - var plusAssem = Changeset.mergingOpAssembler(); - var keepAssem = Changeset.mergingOpAssembler(); - var assem = Changeset.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 = Changeset.newOp(opcode); - op.attribs = Changeset.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) -{ - Changeset.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 -{ - Changeset.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 = Changeset.opAssembler(); - var bufOp = Changeset.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(); - Changeset.copyOp(op, bufOp); - } - } - } - - function endDocument() - { - flush(true); - } - - function toString() - { - flush(); - return assem.toString(); - } - - function clear() - { - assem.clear(); - Changeset.clearOp(bufOp); - } - return { - append: append, - toString: toString, - clear: clear, - endDocument: endDocument - }; - }; -} - -if (_opt) -{ - Changeset.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 -{ - Changeset.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('|', Changeset.numToString(op.lines)); - } - pieces.push(op.opcode); - pieces.push(Changeset.numToString(op.chars)); - } - - function toString() - { - return pieces.join(''); - } - - function clear() - { - pieces.length = 0; - } - return { - append: append, - toString: toString, - clear: clear - }; - }; -} - -Changeset.stringIterator = function(str) -{ - var curIndex = 0; - - function assertRemaining(n) - { - Changeset.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 - }; -}; - -Changeset.stringAssembler = function() -{ - var pieces = []; - - function append(x) - { - pieces.push(String(x)); - } - - function toString() - { - return pieces.join(''); - } - return { - append: append, - toString: toString - }; -}; - -// "lines" need not be an array as long as it supports certain calls (lines_foo inside). -Changeset.textLinesMutator = function(lines) -{ - // Mutates lines, an array of strings, in place. - // Mutation operations have the same constraints as changeset 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 = Changeset.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; -}; - -Changeset.applyZip = function(in1, idx1, in2, idx2, func) -{ - var iter1 = Changeset.opIterator(in1, idx1); - var iter2 = Changeset.opIterator(in2, idx2); - var assem = Changeset.smartOpAssembler(); - var op1 = Changeset.newOp(); - var op2 = Changeset.newOp(); - var opOut = Changeset.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(); -}; - -Changeset.unpack = function(cs) -{ - var headerRegex = /Z:([0-9a-z]+)([><])([0-9a-z]+)|/; - var headerMatch = headerRegex.exec(cs); - if ((!headerMatch) || (!headerMatch[0])) - { - Changeset.error("Not a changeset: " + cs); - } - var oldLen = Changeset.parseNum(headerMatch[1]); - var changeSign = (headerMatch[2] == '>') ? 1 : -1; - var changeMag = Changeset.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) - }; -}; - -Changeset.pack = function(oldLen, newLen, opsStr, bank) -{ - var lenDiff = newLen - oldLen; - var lenDiffStr = (lenDiff >= 0 ? '>' + Changeset.numToString(lenDiff) : '<' + Changeset.numToString(-lenDiff)); - var a = []; - a.push('Z:', Changeset.numToString(oldLen), lenDiffStr, opsStr, '$', bank); - return a.join(''); -}; - -Changeset.applyToText = function(cs, str) -{ - var unpacked = Changeset.unpack(cs); - Changeset.assert(str.length == unpacked.oldLen, "mismatched apply: ", str.length, " / ", unpacked.oldLen); - var csIter = Changeset.opIterator(unpacked.ops); - var bankIter = Changeset.stringIterator(unpacked.charBank); - var strIter = Changeset.stringIterator(str); - var assem = Changeset.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(); -}; - -Changeset.mutateTextLines = function(cs, lines) -{ - var unpacked = Changeset.unpack(cs); - var csIter = Changeset.opIterator(unpacked.ops); - var bankIter = Changeset.stringIterator(unpacked.charBank); - var mut = Changeset.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(); -}; - -Changeset.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 changesets), - // 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(Changeset.parseNum(a))); - return ''; - }); - att2.replace(/\*([0-9a-z]+)/g, function(_, a) - { - var pair = pool.getAttrib(Changeset.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 = Changeset.stringAssembler(); - for (var i = 0; i < atts.length; i++) - { - buf.append('*'); - buf.append(Changeset.numToString(pool.putAttrib(atts[i]))); - } - //print(att1+" / "+att2+" / "+buf.toString()); - return buf.toString(); -}; - -Changeset._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 changesets being composed. - // pool can be null if definitely not needed. - //print(csOp.toSource()+" "+attOp.toSource()+" "+opOut.toSource()); - if (attOp.opcode == '-') - { - Changeset.copyOp(attOp, opOut); - attOp.opcode = ''; - } - else if (!attOp.opcode) - { - Changeset.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 - Changeset.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 = Changeset.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 = Changeset.composeAttributes(attOp.attribs, csOp.attribs, attOp.opcode == '=', pool); - attOp.opcode = ''; - csOp.chars -= attOp.chars; - csOp.lines -= attOp.lines; - } - break; - } - case '': - { - Changeset.copyOp(attOp, opOut); - attOp.opcode = ''; - break; - } - } - } -}; - -Changeset.applyToAttribution = function(cs, astr, pool) -{ - var unpacked = Changeset.unpack(cs); - - return Changeset.applyZip(astr, 0, unpacked.ops, 0, function(op1, op2, opOut) - { - return Changeset._slicerZipperFunc(op1, op2, opOut, pool); - }); -}; - -/*Changeset.oneInsertedLineAtATimeOpIterator = function(opsStr, optStartIndex, charBank) { - var iter = Changeset.opIterator(opsStr, optStartIndex); - var bankIndex = 0; - -};*/ - -Changeset.mutateAttributionLines = function(cs, lines, pool) -{ - //dmesg(cs); - //dmesg(lines.toSource()+" ->"); - var unpacked = Changeset.unpack(cs); - var csIter = Changeset.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 = Changeset.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 = Changeset.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 = Changeset.mergingOpAssembler(); - } - lineAssem.append(op); - if (op.lines > 0) - { - Changeset.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 = Changeset.newOp(); - var attOp = Changeset.newOp(); - var opOut = Changeset.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; - Changeset.copyOp(csOp, opOut); - csOp.chars -= firstLineLen; - csOp.lines--; - opOut.lines = 1; - opOut.chars = firstLineLen; - } - else - { - Changeset.copyOp(csOp, opOut); - csOp.opcode = ''; - } - outputMutOp(opOut); - csBankIndex += opOut.chars; - opOut.opcode = ''; - } - else - { - if ((!attOp.opcode) && isNextMutOp()) - { - nextMutOp(attOp); - } - //print("attOp: "+attOp.toSource()); - Changeset._slicerZipperFunc(attOp, csOp, opOut, pool); - if (opOut.opcode) - { - outputMutOp(opOut); - opOut.opcode = ''; - } - } - } - - Changeset.assert(!lineAssem, "line assembler not finished"); - mut.close(); - - //dmesg("-> "+lines.toSource()); -}; - -Changeset.joinAttributionLines = function(theAlines) -{ - var assem = Changeset.mergingOpAssembler(); - for (var i = 0; i < theAlines.length; i++) - { - var aline = theAlines[i]; - var iter = Changeset.opIterator(aline); - while (iter.hasNext()) - { - assem.append(iter.next()); - } - } - return assem.toString(); -}; - -Changeset.splitAttributionLines = function(attrOps, text) -{ - var iter = Changeset.opIterator(attrOps); - var assem = Changeset.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; - Changeset.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; -}; - -Changeset.splitTextLines = function(text) -{ - return text.match(/[^\n]*(?:\n|[^\n]$)/g); -}; - -Changeset.compose = function(cs1, cs2, pool) -{ - var unpacked1 = Changeset.unpack(cs1); - var unpacked2 = Changeset.unpack(cs2); - var len1 = unpacked1.oldLen; - var len2 = unpacked1.newLen; - Changeset.assert(len2 == unpacked2.oldLen, "mismatched composition"); - var len3 = unpacked2.newLen; - var bankIter1 = Changeset.stringIterator(unpacked1.charBank); - var bankIter2 = Changeset.stringIterator(unpacked2.charBank); - var bankAssem = Changeset.stringAssembler(); - - var newOps = Changeset.applyZip(unpacked1.ops, 0, unpacked2.ops, 0, function(op1, op2, opOut) - { - //var debugBuilder = Changeset.stringAssembler(); - //debugBuilder.append(Changeset.opString(op1)); - //debugBuilder.append(','); - //debugBuilder.append(Changeset.opString(op2)); - //debugBuilder.append(' / '); - var op1code = op1.opcode; - var op2code = op2.opcode; - if (op1code == '+' && op2code == '-') - { - bankIter1.skip(Math.min(op1.chars, op2.chars)); - } - Changeset._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(Changeset.opString(op1)); - //debugBuilder.append(','); - //debugBuilder.append(Changeset.opString(op2)); - //debugBuilder.append(' -> '); - //debugBuilder.append(Changeset.opString(opOut)); - //print(debugBuilder.toString()); - }); - - return Changeset.pack(len1, len3, newOps, bankAssem.toString()); -}; - -Changeset.attributeTester = function(attribPair, pool) -{ - // 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. - if (!pool) - { - return never; - } - var attribNum = pool.putAttrib(attribPair, true); - if (attribNum < 0) - { - return never; - } - else - { - var re = new RegExp('\\*' + Changeset.numToString(attribNum) + '(?!\\w)'); - return function(attribs) - { - return re.test(attribs); - }; - } - - function never(attribs) - { - return false; - } -}; - -Changeset.identity = function(N) -{ - return Changeset.pack(N, N, "", ""); -}; - -Changeset.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 = Changeset.smartOpAssembler(); - assem.appendOpWithText('=', oldFullText.substring(0, spliceStart)); - assem.appendOpWithText('-', oldText); - assem.appendOpWithText('+', newText, optNewTextAPairs, pool); - assem.endDocument(); - return Changeset.pack(oldLen, newLen, assem.toString(), newText); -}; - -Changeset.toSplices = function(cs) -{ - // get a list of splices, [startChar, endChar, newText] - var unpacked = Changeset.unpack(cs); - var splices = []; - - var oldPos = 0; - var iter = Changeset.opIterator(unpacked.ops); - var charIter = Changeset.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; -}; - -Changeset.characterRangeFollow = function(cs, startChar, endChar, insertionsAfter) -{ - var newStartChar = startChar; - var newEndChar = endChar; - var splices = Changeset.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]; -}; - -Changeset.moveOpsToNewPool = function(cs, oldPool, newPool) -{ - // works on changeset 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 = Changeset.parseNum(a); - var pair = oldPool.getAttrib(oldNum); - var newNum = newPool.putAttrib(pair); - return '*' + Changeset.numToString(newNum); - }) + fromDollar; -}; - -Changeset.makeAttribution = function(text) -{ - var assem = Changeset.smartOpAssembler(); - assem.appendOpWithText('+', text); - return assem.toString(); -}; - -// callable on a changeset, attribution string, or attribs property of an op -Changeset.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(Changeset.parseNum(a)); - return ''; - }); -}; - -// callable on a changeset, attribution string, or attribs property of an op, -// though it may easily create adjacent ops that can be merged. -Changeset.filterAttribNumbers = function(cs, filter) -{ - return Changeset.mapAttribNumbers(cs, filter); -}; - -Changeset.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(Changeset.parseNum(a)); - if (n === true) - { - return s; - } - else if ((typeof n) === "number") - { - return '*' + Changeset.numToString(n); - } - else - { - return ''; - } - }); - - return newUpToDollar + cs.substring(dollarPos); -}; - -Changeset.makeAText = function(text, attribs) -{ - return { - text: text, - attribs: (attribs || Changeset.makeAttribution(text)) - }; -}; - -Changeset.applyToAText = function(cs, atext, pool) -{ - return { - text: Changeset.applyToText(cs, atext.text), - attribs: Changeset.applyToAttribution(cs, atext.attribs, pool) - }; -}; - -Changeset.cloneAText = function(atext) -{ - return { - text: atext.text, - attribs: atext.attribs - }; -}; - -Changeset.copyAText = function(atext1, atext2) -{ - atext2.text = atext1.text; - atext2.attribs = atext1.attribs; -}; - -Changeset.appendATextToAssembler = function(atext, assem) -{ - // intentionally skips last newline char of atext - var iter = Changeset.opIterator(atext.attribs); - var op = Changeset.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); - } - } -}; - -Changeset.prepareForWire = function(cs, pool) -{ - var newPool = new AttribPool(); - var newCs = Changeset.moveOpsToNewPool(cs, pool, newPool); - return { - translated: newCs, - pool: newPool - }; -}; - -Changeset.isIdentity = function(cs) -{ - var unpacked = Changeset.unpack(cs); - return unpacked.ops == "" && unpacked.oldLen == unpacked.newLen; -}; - -Changeset.opAttributeValue = function(op, key, pool) -{ - return Changeset.attribsAttributeValue(op.attribs, key, pool); -}; - -Changeset.attribsAttributeValue = function(attribs, key, pool) -{ - var value = ''; - if (attribs) - { - Changeset.eachAttribNumber(attribs, function(n) - { - if (pool.getAttribKey(n) == key) - { - value = pool.getAttribValue(n); - } - }); - } - return value; -}; - -Changeset.builder = function(oldLen) -{ - var assem = Changeset.smartOpAssembler(); - var o = Changeset.newOp(); - var charBank = Changeset.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 && Changeset.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 Changeset.pack(oldLen, newLen, assem.toString(), charBank.toString()); - } - }; - - return self; -}; - -Changeset.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('*' + Changeset.numToString(pool.putAttrib(pair))); - } - } - return result.join(''); - } -}; - -// like "substring" but on a single-line attribution string -Changeset.subattribution = function(astr, start, optEnd) -{ - var iter = Changeset.opIterator(astr, 0); - var assem = Changeset.smartOpAssembler(); - var attOp = Changeset.newOp(); - var csOp = Changeset.newOp(); - var opOut = Changeset.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++; - } - - Changeset._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(); -}; - -Changeset.inverse = function(cs, lines, alines, pool) -{ - // lines and alines are what the changeset 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 = Changeset.newOp('+'); - - var unpacked = Changeset.unpack(cs); - var csIter = Changeset.opIterator(unpacked.ops); - var builder = Changeset.builder(unpacked.newLen); - - function consumeAttribRuns(numChars, func /*(len, attribs, endsLine)*/ ) - { - - if ((!curLineOpIter) || (curLineOpIterLine != curLine)) - { - // create curLineOpIter and advance it to curChar - curLineOpIter = Changeset.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 = Changeset.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 = Changeset.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; - Changeset.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 = Changeset.attribsAttributeValue(attribs, appliedKey, pool); - if (appliedValue != oldValue) - { - backAttribs.push([appliedKey, oldValue]); - } - } - return Changeset.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 Changeset.checkRep(builder.toString()); -}; - -// %CLIENT FILE ENDS HERE% -Changeset.follow = function(cs1, cs2, reverseInsertOrder, pool) -{ - var unpacked1 = Changeset.unpack(cs1); - var unpacked2 = Changeset.unpack(cs2); - var len1 = unpacked1.oldLen; - var len2 = unpacked2.oldLen; - Changeset.assert(len1 == len2, "mismatched follow"); - var chars1 = Changeset.stringIterator(unpacked1.charBank); - var chars2 = Changeset.stringIterator(unpacked2.charBank); - - var oldLen = unpacked1.newLen; - var oldPos = 0; - var newLen = 0; - - var hasInsertFirst = Changeset.attributeTester(['insertorder', 'first'], pool); - - var newOps = Changeset.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); - Changeset.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 == '-') - { - Changeset.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) - { - Changeset.copyOp(op2, opOut); - op2.opcode = ''; - } - else if (!op2.opcode) - { - Changeset.copyOp(op1, opOut); - op1.opcode = ''; - } - else - { - // both keeps - opOut.opcode = '='; - opOut.attribs = Changeset.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 Changeset.pack(oldLen, newLen, newOps, unpacked2.charBank); -}; - -Changeset.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(Changeset.parseNum(a))); - return ''; - }); - att1.replace(/\*([0-9a-z]+)/g, function(_, a) - { - var pair1 = pool.getAttrib(Changeset.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 = Changeset.stringAssembler(); - for (var i = 0; i < atts.length; i++) - { - buf.append('*'); - buf.append(Changeset.numToString(pool.putAttrib(atts[i]))); - } - return buf.toString(); -}; diff --git a/static/js/easysync2_client.js b/static/js/easysync2_client.js deleted file mode 100644 index 3611da233..000000000 --- a/static/js/easysync2_client.js +++ /dev/null @@ -1,2271 +0,0 @@ -/** - * 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/easysync2.js -// THIS FILE IS ALSO AN APPJET MODULE: etherpad.collab.ace.easysync2 -/** - * 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 _opt = (this.Easysync2Support || null); -var _opt = null; // disable optimization for now - -function AttribPool() -{ - 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; -} - -var Changeset = {}; - -Changeset.error = function error(msg) -{ - var e = new Error(msg); - e.easysync = true; - throw e; -}; -Changeset.assert = function assert(b, msgParts) -{ - if (!b) - { - var msg = Array.prototype.slice.call(arguments, 1).join(''); - Changeset.error("Changeset: " + msg); - } -}; - -Changeset.parseNum = function(str) -{ - return parseInt(str, 36); -}; -Changeset.numToString = function(num) -{ - return num.toString(36).toLowerCase(); -}; -Changeset.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(Changeset.parseNum(s)); - }) + fromDollar; -}; - -Changeset.oldLen = function(cs) -{ - return Changeset.unpack(cs).oldLen; -}; -Changeset.newLen = function(cs) -{ - return Changeset.unpack(cs).newLen; -}; - -Changeset.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() == '?') - { - Changeset.error("Hit error opcode in op stream"); - } - curIndex = result.lastIndex(); - } - } - else - { - regex.lastIndex = curIndex; - result = regex.exec(opsStr); - curIndex = regex.lastIndex; - if (result[0] == '?') - { - Changeset.error("Hit error opcode in op stream"); - } - } - return result; - } - var regexResult = nextRegexMatch(); - var obj = Changeset.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 = Changeset.parseNum(regexResult[2] || 0); - op.opcode = regexResult[3]; - op.chars = Changeset.parseNum(regexResult[4]); - regexResult = nextRegexMatch(); - } - else - { - Changeset.clearOp(op); - } - return op; - } - - function hasNext() - { - return !!(_opt ? regexResult : regexResult[0]); - } - - function lastIndex() - { - return prevIndex; - } - return { - next: next, - hasNext: hasNext, - lastIndex: lastIndex - }; -}; - -Changeset.clearOp = function(op) -{ - op.opcode = ''; - op.chars = 0; - op.lines = 0; - op.attribs = ''; -}; -Changeset.newOp = function(optOpcode) -{ - return { - opcode: (optOpcode || ''), - chars: 0, - lines: 0, - attribs: '' - }; -}; -Changeset.cloneOp = function(op) -{ - return { - opcode: op.opcode, - chars: op.chars, - lines: op.lines, - attribs: op.attribs - }; -}; -Changeset.copyOp = function(op1, op2) -{ - op2.opcode = op1.opcode; - op2.chars = op1.chars; - op2.lines = op1.lines; - op2.attribs = op1.attribs; -}; -Changeset.opString = function(op) -{ - // just for debugging - if (!op.opcode) return 'null'; - var assem = Changeset.opAssembler(); - assem.append(op); - return assem.toString(); -}; -Changeset.stringOp = function(str) -{ - // just for debugging - return Changeset.opIterator(str).next(); -}; - -Changeset.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 = Changeset.unpack(cs); - var oldLen = unpacked.oldLen; - var newLen = unpacked.newLen; - var ops = unpacked.ops; - var charBank = unpacked.charBank; - - var assem = Changeset.smartOpAssembler(); - var oldPos = 0; - var calcNewLen = 0; - var numInserted = 0; - var iter = Changeset.opIterator(ops); - while (iter.hasNext()) - { - var o = iter.next(); - switch (o.opcode) - { - case '=': - oldPos += o.chars; - calcNewLen += o.chars; - break; - case '-': - oldPos += o.chars; - Changeset.assert(oldPos < oldLen, oldPos, " >= ", oldLen, " in ", cs); - break; - case '+': - { - calcNewLen += o.chars; - numInserted += o.chars; - Changeset.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 = Changeset.pack(oldLen, calcNewLen, assem.toString(), charBank); - Changeset.assert(normalized == cs, normalized, ' != ', cs); - - return cs; -} - -Changeset.smartOpAssembler = function() -{ - // Like opAssembler but able to produce conforming changesets - // 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 = Changeset.mergingOpAssembler(); - var plusAssem = Changeset.mergingOpAssembler(); - var keepAssem = Changeset.mergingOpAssembler(); - var assem = Changeset.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 = Changeset.newOp(opcode); - op.attribs = Changeset.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) -{ - Changeset.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 -{ - Changeset.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 = Changeset.opAssembler(); - var bufOp = Changeset.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(); - Changeset.copyOp(op, bufOp); - } - } - } - - function endDocument() - { - flush(true); - } - - function toString() - { - flush(); - return assem.toString(); - } - - function clear() - { - assem.clear(); - Changeset.clearOp(bufOp); - } - return { - append: append, - toString: toString, - clear: clear, - endDocument: endDocument - }; - }; -} - -if (_opt) -{ - Changeset.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 -{ - Changeset.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('|', Changeset.numToString(op.lines)); - } - pieces.push(op.opcode); - pieces.push(Changeset.numToString(op.chars)); - } - - function toString() - { - return pieces.join(''); - } - - function clear() - { - pieces.length = 0; - } - return { - append: append, - toString: toString, - clear: clear - }; - }; -} - -Changeset.stringIterator = function(str) -{ - var curIndex = 0; - - function assertRemaining(n) - { - Changeset.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 - }; -}; - -Changeset.stringAssembler = function() -{ - var pieces = []; - - function append(x) - { - pieces.push(String(x)); - } - - function toString() - { - return pieces.join(''); - } - return { - append: append, - toString: toString - }; -}; - -// "lines" need not be an array as long as it supports certain calls (lines_foo inside). -Changeset.textLinesMutator = function(lines) -{ - // Mutates lines, an array of strings, in place. - // Mutation operations have the same constraints as changeset 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 = Changeset.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; -}; - -Changeset.applyZip = function(in1, idx1, in2, idx2, func) -{ - var iter1 = Changeset.opIterator(in1, idx1); - var iter2 = Changeset.opIterator(in2, idx2); - var assem = Changeset.smartOpAssembler(); - var op1 = Changeset.newOp(); - var op2 = Changeset.newOp(); - var opOut = Changeset.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(); -}; - -Changeset.unpack = function(cs) -{ - var headerRegex = /Z:([0-9a-z]+)([><])([0-9a-z]+)|/; - var headerMatch = headerRegex.exec(cs); - if ((!headerMatch) || (!headerMatch[0])) - { - Changeset.error("Not a changeset: " + cs); - } - var oldLen = Changeset.parseNum(headerMatch[1]); - var changeSign = (headerMatch[2] == '>') ? 1 : -1; - var changeMag = Changeset.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) - }; -}; - -Changeset.pack = function(oldLen, newLen, opsStr, bank) -{ - var lenDiff = newLen - oldLen; - var lenDiffStr = (lenDiff >= 0 ? '>' + Changeset.numToString(lenDiff) : '<' + Changeset.numToString(-lenDiff)); - var a = []; - a.push('Z:', Changeset.numToString(oldLen), lenDiffStr, opsStr, '$', bank); - return a.join(''); -}; - -Changeset.applyToText = function(cs, str) -{ - var unpacked = Changeset.unpack(cs); - Changeset.assert(str.length == unpacked.oldLen, "mismatched apply: ", str.length, " / ", unpacked.oldLen); - var csIter = Changeset.opIterator(unpacked.ops); - var bankIter = Changeset.stringIterator(unpacked.charBank); - var strIter = Changeset.stringIterator(str); - var assem = Changeset.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(); -}; - -Changeset.mutateTextLines = function(cs, lines) -{ - var unpacked = Changeset.unpack(cs); - var csIter = Changeset.opIterator(unpacked.ops); - var bankIter = Changeset.stringIterator(unpacked.charBank); - var mut = Changeset.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(); -}; - -Changeset.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 changesets), - // 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(Changeset.parseNum(a))); - return ''; - }); - att2.replace(/\*([0-9a-z]+)/g, function(_, a) - { - var pair = pool.getAttrib(Changeset.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 = Changeset.stringAssembler(); - for (var i = 0; i < atts.length; i++) - { - buf.append('*'); - buf.append(Changeset.numToString(pool.putAttrib(atts[i]))); - } - //print(att1+" / "+att2+" / "+buf.toString()); - return buf.toString(); -}; - -Changeset._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 changesets being composed. - // pool can be null if definitely not needed. - //print(csOp.toSource()+" "+attOp.toSource()+" "+opOut.toSource()); - if (attOp.opcode == '-') - { - Changeset.copyOp(attOp, opOut); - attOp.opcode = ''; - } - else if (!attOp.opcode) - { - Changeset.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 - Changeset.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 = Changeset.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 = Changeset.composeAttributes(attOp.attribs, csOp.attribs, attOp.opcode == '=', pool); - attOp.opcode = ''; - csOp.chars -= attOp.chars; - csOp.lines -= attOp.lines; - } - break; - } - case '': - { - Changeset.copyOp(attOp, opOut); - attOp.opcode = ''; - break; - } - } - } -}; - -Changeset.applyToAttribution = function(cs, astr, pool) -{ - var unpacked = Changeset.unpack(cs); - - return Changeset.applyZip(astr, 0, unpacked.ops, 0, function(op1, op2, opOut) - { - return Changeset._slicerZipperFunc(op1, op2, opOut, pool); - }); -}; - -/*Changeset.oneInsertedLineAtATimeOpIterator = function(opsStr, optStartIndex, charBank) { - var iter = Changeset.opIterator(opsStr, optStartIndex); - var bankIndex = 0; - -};*/ - -Changeset.mutateAttributionLines = function(cs, lines, pool) -{ - //dmesg(cs); - //dmesg(lines.toSource()+" ->"); - var unpacked = Changeset.unpack(cs); - var csIter = Changeset.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 = Changeset.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 = Changeset.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 = Changeset.mergingOpAssembler(); - } - lineAssem.append(op); - if (op.lines > 0) - { - Changeset.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 = Changeset.newOp(); - var attOp = Changeset.newOp(); - var opOut = Changeset.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; - Changeset.copyOp(csOp, opOut); - csOp.chars -= firstLineLen; - csOp.lines--; - opOut.lines = 1; - opOut.chars = firstLineLen; - } - else - { - Changeset.copyOp(csOp, opOut); - csOp.opcode = ''; - } - outputMutOp(opOut); - csBankIndex += opOut.chars; - opOut.opcode = ''; - } - else - { - if ((!attOp.opcode) && isNextMutOp()) - { - nextMutOp(attOp); - } - //print("attOp: "+attOp.toSource()); - Changeset._slicerZipperFunc(attOp, csOp, opOut, pool); - if (opOut.opcode) - { - outputMutOp(opOut); - opOut.opcode = ''; - } - } - } - - Changeset.assert(!lineAssem, "line assembler not finished"); - mut.close(); - - //dmesg("-> "+lines.toSource()); -}; - -Changeset.joinAttributionLines = function(theAlines) -{ - var assem = Changeset.mergingOpAssembler(); - for (var i = 0; i < theAlines.length; i++) - { - var aline = theAlines[i]; - var iter = Changeset.opIterator(aline); - while (iter.hasNext()) - { - assem.append(iter.next()); - } - } - return assem.toString(); -}; - -Changeset.splitAttributionLines = function(attrOps, text) -{ - var iter = Changeset.opIterator(attrOps); - var assem = Changeset.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; - Changeset.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; -}; - -Changeset.splitTextLines = function(text) -{ - return text.match(/[^\n]*(?:\n|[^\n]$)/g); -}; - -Changeset.compose = function(cs1, cs2, pool) -{ - var unpacked1 = Changeset.unpack(cs1); - var unpacked2 = Changeset.unpack(cs2); - var len1 = unpacked1.oldLen; - var len2 = unpacked1.newLen; - Changeset.assert(len2 == unpacked2.oldLen, "mismatched composition"); - var len3 = unpacked2.newLen; - var bankIter1 = Changeset.stringIterator(unpacked1.charBank); - var bankIter2 = Changeset.stringIterator(unpacked2.charBank); - var bankAssem = Changeset.stringAssembler(); - - var newOps = Changeset.applyZip(unpacked1.ops, 0, unpacked2.ops, 0, function(op1, op2, opOut) - { - //var debugBuilder = Changeset.stringAssembler(); - //debugBuilder.append(Changeset.opString(op1)); - //debugBuilder.append(','); - //debugBuilder.append(Changeset.opString(op2)); - //debugBuilder.append(' / '); - var op1code = op1.opcode; - var op2code = op2.opcode; - if (op1code == '+' && op2code == '-') - { - bankIter1.skip(Math.min(op1.chars, op2.chars)); - } - Changeset._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(Changeset.opString(op1)); - //debugBuilder.append(','); - //debugBuilder.append(Changeset.opString(op2)); - //debugBuilder.append(' -> '); - //debugBuilder.append(Changeset.opString(opOut)); - //print(debugBuilder.toString()); - }); - - return Changeset.pack(len1, len3, newOps, bankAssem.toString()); -}; - -Changeset.attributeTester = function(attribPair, pool) -{ - // 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. - if (!pool) - { - return never; - } - var attribNum = pool.putAttrib(attribPair, true); - if (attribNum < 0) - { - return never; - } - else - { - var re = new RegExp('\\*' + Changeset.numToString(attribNum) + '(?!\\w)'); - return function(attribs) - { - return re.test(attribs); - }; - } - - function never(attribs) - { - return false; - } -}; - -Changeset.identity = function(N) -{ - return Changeset.pack(N, N, "", ""); -}; - -Changeset.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 = Changeset.smartOpAssembler(); - assem.appendOpWithText('=', oldFullText.substring(0, spliceStart)); - assem.appendOpWithText('-', oldText); - assem.appendOpWithText('+', newText, optNewTextAPairs, pool); - assem.endDocument(); - return Changeset.pack(oldLen, newLen, assem.toString(), newText); -}; - -Changeset.toSplices = function(cs) -{ - // get a list of splices, [startChar, endChar, newText] - var unpacked = Changeset.unpack(cs); - var splices = []; - - var oldPos = 0; - var iter = Changeset.opIterator(unpacked.ops); - var charIter = Changeset.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; -}; - -Changeset.characterRangeFollow = function(cs, startChar, endChar, insertionsAfter) -{ - var newStartChar = startChar; - var newEndChar = endChar; - var splices = Changeset.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]; -}; - -Changeset.moveOpsToNewPool = function(cs, oldPool, newPool) -{ - // works on changeset 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 = Changeset.parseNum(a); - var pair = oldPool.getAttrib(oldNum); - var newNum = newPool.putAttrib(pair); - return '*' + Changeset.numToString(newNum); - }) + fromDollar; -}; - -Changeset.makeAttribution = function(text) -{ - var assem = Changeset.smartOpAssembler(); - assem.appendOpWithText('+', text); - return assem.toString(); -}; - -// callable on a changeset, attribution string, or attribs property of an op -Changeset.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(Changeset.parseNum(a)); - return ''; - }); -}; - -// callable on a changeset, attribution string, or attribs property of an op, -// though it may easily create adjacent ops that can be merged. -Changeset.filterAttribNumbers = function(cs, filter) -{ - return Changeset.mapAttribNumbers(cs, filter); -}; - -Changeset.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(Changeset.parseNum(a)); - if (n === true) - { - return s; - } - else if ((typeof n) === "number") - { - return '*' + Changeset.numToString(n); - } - else - { - return ''; - } - }); - - return newUpToDollar + cs.substring(dollarPos); -}; - -Changeset.makeAText = function(text, attribs) -{ - return { - text: text, - attribs: (attribs || Changeset.makeAttribution(text)) - }; -}; - -Changeset.applyToAText = function(cs, atext, pool) -{ - return { - text: Changeset.applyToText(cs, atext.text), - attribs: Changeset.applyToAttribution(cs, atext.attribs, pool) - }; -}; - -Changeset.cloneAText = function(atext) -{ - return { - text: atext.text, - attribs: atext.attribs - }; -}; - -Changeset.copyAText = function(atext1, atext2) -{ - atext2.text = atext1.text; - atext2.attribs = atext1.attribs; -}; - -Changeset.appendATextToAssembler = function(atext, assem) -{ - // intentionally skips last newline char of atext - var iter = Changeset.opIterator(atext.attribs); - var op = Changeset.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); - } - } -}; - -Changeset.prepareForWire = function(cs, pool) -{ - var newPool = new AttribPool(); - var newCs = Changeset.moveOpsToNewPool(cs, pool, newPool); - return { - translated: newCs, - pool: newPool - }; -}; - -Changeset.isIdentity = function(cs) -{ - var unpacked = Changeset.unpack(cs); - return unpacked.ops == "" && unpacked.oldLen == unpacked.newLen; -}; - -Changeset.opAttributeValue = function(op, key, pool) -{ - return Changeset.attribsAttributeValue(op.attribs, key, pool); -}; - -Changeset.attribsAttributeValue = function(attribs, key, pool) -{ - var value = ''; - if (attribs) - { - Changeset.eachAttribNumber(attribs, function(n) - { - if (pool.getAttribKey(n) == key) - { - value = pool.getAttribValue(n); - } - }); - } - return value; -}; - -Changeset.builder = function(oldLen) -{ - var assem = Changeset.smartOpAssembler(); - var o = Changeset.newOp(); - var charBank = Changeset.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 && Changeset.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 Changeset.pack(oldLen, newLen, assem.toString(), charBank.toString()); - } - }; - - return self; -}; - -Changeset.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('*' + Changeset.numToString(pool.putAttrib(pair))); - } - } - return result.join(''); - } -}; - -// like "substring" but on a single-line attribution string -Changeset.subattribution = function(astr, start, optEnd) -{ - var iter = Changeset.opIterator(astr, 0); - var assem = Changeset.smartOpAssembler(); - var attOp = Changeset.newOp(); - var csOp = Changeset.newOp(); - var opOut = Changeset.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++; - } - - Changeset._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(); -}; - -Changeset.inverse = function(cs, lines, alines, pool) -{ - // lines and alines are what the changeset 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 = Changeset.newOp('+'); - - var unpacked = Changeset.unpack(cs); - var csIter = Changeset.opIterator(unpacked.ops); - var builder = Changeset.builder(unpacked.newLen); - - function consumeAttribRuns(numChars, func /*(len, attribs, endsLine)*/ ) - { - - if ((!curLineOpIter) || (curLineOpIterLine != curLine)) - { - // create curLineOpIter and advance it to curChar - curLineOpIter = Changeset.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 = Changeset.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 = Changeset.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; - Changeset.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 = Changeset.attribsAttributeValue(attribs, appliedKey, pool); - if (appliedValue != oldValue) - { - backAttribs.push([appliedKey, oldValue]); - } - } - return Changeset.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 Changeset.checkRep(builder.toString()); -}; diff --git a/static/js/jquery-ui.js b/static/js/jquery-ui.js deleted file mode 100644 index 026ffdbea..000000000 --- a/static/js/jquery-ui.js +++ /dev/null @@ -1,157 +0,0 @@ -/*! - * jQuery UI 1.8.14 - * - * Copyright 2011, AUTHORS.txt (http://jqueryui.com/about) - * Dual licensed under the MIT or GPL Version 2 licenses. - * http://jquery.org/license - * - * http://docs.jquery.com/UI - */ -(function(c,j){function k(a,b){var d=a.nodeName.toLowerCase();if("area"===d){b=a.parentNode;d=b.name;if(!a.href||!d||b.nodeName.toLowerCase()!=="map")return false;a=c("img[usemap=#"+d+"]")[0];return!!a&&l(a)}return(/input|select|textarea|button|object/.test(d)?!a.disabled:"a"==d?a.href||b:b)&&l(a)}function l(a){return!c(a).parents().andSelf().filter(function(){return c.curCSS(this,"visibility")==="hidden"||c.expr.filters.hidden(this)}).length}c.ui=c.ui||{};if(!c.ui.version){c.extend(c.ui,{version:"1.8.14", -keyCode:{ALT:18,BACKSPACE:8,CAPS_LOCK:20,COMMA:188,COMMAND:91,COMMAND_LEFT:91,COMMAND_RIGHT:93,CONTROL:17,DELETE:46,DOWN:40,END:35,ENTER:13,ESCAPE:27,HOME:36,INSERT:45,LEFT:37,MENU:93,NUMPAD_ADD:107,NUMPAD_DECIMAL:110,NUMPAD_DIVIDE:111,NUMPAD_ENTER:108,NUMPAD_MULTIPLY:106,NUMPAD_SUBTRACT:109,PAGE_DOWN:34,PAGE_UP:33,PERIOD:190,RIGHT:39,SHIFT:16,SPACE:32,TAB:9,UP:38,WINDOWS:91}});c.fn.extend({_focus:c.fn.focus,focus:function(a,b){return typeof a==="number"?this.each(function(){var d=this;setTimeout(function(){c(d).focus(); -b&&b.call(d)},a)}):this._focus.apply(this,arguments)},scrollParent:function(){var a;a=c.browser.msie&&/(static|relative)/.test(this.css("position"))||/absolute/.test(this.css("position"))?this.parents().filter(function(){return/(relative|absolute|fixed)/.test(c.curCSS(this,"position",1))&&/(auto|scroll)/.test(c.curCSS(this,"overflow",1)+c.curCSS(this,"overflow-y",1)+c.curCSS(this,"overflow-x",1))}).eq(0):this.parents().filter(function(){return/(auto|scroll)/.test(c.curCSS(this,"overflow",1)+c.curCSS(this, -"overflow-y",1)+c.curCSS(this,"overflow-x",1))}).eq(0);return/fixed/.test(this.css("position"))||!a.length?c(document):a},zIndex:function(a){if(a!==j)return this.css("zIndex",a);if(this.length){a=c(this[0]);for(var b;a.length&&a[0]!==document;){b=a.css("position");if(b==="absolute"||b==="relative"||b==="fixed"){b=parseInt(a.css("zIndex"),10);if(!isNaN(b)&&b!==0)return b}a=a.parent()}}return 0},disableSelection:function(){return this.bind((c.support.selectstart?"selectstart":"mousedown")+".ui-disableSelection", -function(a){a.preventDefault()})},enableSelection:function(){return this.unbind(".ui-disableSelection")}});c.each(["Width","Height"],function(a,b){function d(f,g,m,n){c.each(e,function(){g-=parseFloat(c.curCSS(f,"padding"+this,true))||0;if(m)g-=parseFloat(c.curCSS(f,"border"+this+"Width",true))||0;if(n)g-=parseFloat(c.curCSS(f,"margin"+this,true))||0});return g}var e=b==="Width"?["Left","Right"]:["Top","Bottom"],h=b.toLowerCase(),i={innerWidth:c.fn.innerWidth,innerHeight:c.fn.innerHeight,outerWidth:c.fn.outerWidth, -outerHeight:c.fn.outerHeight};c.fn["inner"+b]=function(f){if(f===j)return i["inner"+b].call(this);return this.each(function(){c(this).css(h,d(this,f)+"px")})};c.fn["outer"+b]=function(f,g){if(typeof f!=="number")return i["outer"+b].call(this,f);return this.each(function(){c(this).css(h,d(this,f,true,g)+"px")})}});c.extend(c.expr[":"],{data:function(a,b,d){return!!c.data(a,d[3])},focusable:function(a){return k(a,!isNaN(c.attr(a,"tabindex")))},tabbable:function(a){var b=c.attr(a,"tabindex"),d=isNaN(b); -return(d||b>=0)&&k(a,!d)}});c(function(){var a=document.body,b=a.appendChild(b=document.createElement("div"));c.extend(b.style,{minHeight:"100px",height:"auto",padding:0,borderWidth:0});c.support.minHeight=b.offsetHeight===100;c.support.selectstart="onselectstart"in b;a.removeChild(b).style.display="none"});c.extend(c.ui,{plugin:{add:function(a,b,d){a=c.ui[a].prototype;for(var e in d){a.plugins[e]=a.plugins[e]||[];a.plugins[e].push([b,d[e]])}},call:function(a,b,d){if((b=a.plugins[b])&&a.element[0].parentNode)for(var e= -0;e0)return true;a[b]=1;d=a[b]>0;a[b]=0;return d},isOverAxis:function(a,b,d){return a>b&&a=9)&&!a.button)return this._mouseUp(a);if(this._mouseStarted){this._mouseDrag(a);return a.preventDefault()}if(this._mouseDistanceMet(a)&&this._mouseDelayMet(a))(this._mouseStarted=this._mouseStart(this._mouseDownEvent,a)!==false)?this._mouseDrag(a):this._mouseUp(a);return!this._mouseStarted},_mouseUp:function(a){b(document).unbind("mousemove."+this.widgetName,this._mouseMoveDelegate).unbind("mouseup."+this.widgetName,this._mouseUpDelegate);if(this._mouseStarted){this._mouseStarted= -false;a.target==this._mouseDownEvent.target&&b.data(a.target,this.widgetName+".preventClickEvent",true);this._mouseStop(a)}return false},_mouseDistanceMet:function(a){return Math.max(Math.abs(this._mouseDownEvent.pageX-a.pageX),Math.abs(this._mouseDownEvent.pageY-a.pageY))>=this.options.distance},_mouseDelayMet:function(){return this.mouseDelayMet},_mouseStart:function(){},_mouseDrag:function(){},_mouseStop:function(){},_mouseCapture:function(){return true}})})(jQuery); -;/* - * jQuery UI Resizable 1.8.14 - * - * Copyright 2011, AUTHORS.txt (http://jqueryui.com/about) - * Dual licensed under the MIT or GPL Version 2 licenses. - * http://jquery.org/license - * - * http://docs.jquery.com/UI/Resizables - * - * Depends: - * jquery.ui.core.js - * jquery.ui.mouse.js - * jquery.ui.widget.js - */ -(function(e){e.widget("ui.resizable",e.ui.mouse,{widgetEventPrefix:"resize",options:{alsoResize:false,animate:false,animateDuration:"slow",animateEasing:"swing",aspectRatio:false,autoHide:false,containment:false,ghost:false,grid:false,handles:"e,s,se",helper:false,maxHeight:null,maxWidth:null,minHeight:10,minWidth:10,zIndex:1E3},_create:function(){var b=this,a=this.options;this.element.addClass("ui-resizable");e.extend(this,{_aspectRatio:!!a.aspectRatio,aspectRatio:a.aspectRatio,originalElement:this.element, -_proportionallyResizeElements:[],_helper:a.helper||a.ghost||a.animate?a.helper||"ui-resizable-helper":null});if(this.element[0].nodeName.match(/canvas|textarea|input|select|button|img/i)){/relative/.test(this.element.css("position"))&&e.browser.opera&&this.element.css({position:"relative",top:"auto",left:"auto"});this.element.wrap(e('
').css({position:this.element.css("position"),width:this.element.outerWidth(),height:this.element.outerHeight(), -top:this.element.css("top"),left:this.element.css("left")}));this.element=this.element.parent().data("resizable",this.element.data("resizable"));this.elementIsWrapper=true;this.element.css({marginLeft:this.originalElement.css("marginLeft"),marginTop:this.originalElement.css("marginTop"),marginRight:this.originalElement.css("marginRight"),marginBottom:this.originalElement.css("marginBottom")});this.originalElement.css({marginLeft:0,marginTop:0,marginRight:0,marginBottom:0});this.originalResizeStyle= -this.originalElement.css("resize");this.originalElement.css("resize","none");this._proportionallyResizeElements.push(this.originalElement.css({position:"static",zoom:1,display:"block"}));this.originalElement.css({margin:this.originalElement.css("margin")});this._proportionallyResize()}this.handles=a.handles||(!e(".ui-resizable-handle",this.element).length?"e,s,se":{n:".ui-resizable-n",e:".ui-resizable-e",s:".ui-resizable-s",w:".ui-resizable-w",se:".ui-resizable-se",sw:".ui-resizable-sw",ne:".ui-resizable-ne", -nw:".ui-resizable-nw"});if(this.handles.constructor==String){if(this.handles=="all")this.handles="n,e,s,w,se,sw,ne,nw";var c=this.handles.split(",");this.handles={};for(var d=0;d');/sw|se|ne|nw/.test(f)&&g.css({zIndex:++a.zIndex});"se"==f&&g.addClass("ui-icon ui-icon-gripsmall-diagonal-se");this.handles[f]=".ui-resizable-"+f;this.element.append(g)}}this._renderAxis=function(h){h=h||this.element;for(var i in this.handles){if(this.handles[i].constructor== -String)this.handles[i]=e(this.handles[i],this.element).show();if(this.elementIsWrapper&&this.originalElement[0].nodeName.match(/textarea|input|select|button/i)){var j=e(this.handles[i],this.element),l=0;l=/sw|ne|nw|se|n|s/.test(i)?j.outerHeight():j.outerWidth();j=["padding",/ne|nw|n/.test(i)?"Top":/se|sw|s/.test(i)?"Bottom":/^e$/.test(i)?"Right":"Left"].join("");h.css(j,l);this._proportionallyResize()}e(this.handles[i])}};this._renderAxis(this.element);this._handles=e(".ui-resizable-handle",this.element).disableSelection(); -this._handles.mouseover(function(){if(!b.resizing){if(this.className)var h=this.className.match(/ui-resizable-(se|sw|ne|nw|n|e|s|w)/i);b.axis=h&&h[1]?h[1]:"se"}});if(a.autoHide){this._handles.hide();e(this.element).addClass("ui-resizable-autohide").hover(function(){if(!a.disabled){e(this).removeClass("ui-resizable-autohide");b._handles.show()}},function(){if(!a.disabled)if(!b.resizing){e(this).addClass("ui-resizable-autohide");b._handles.hide()}})}this._mouseInit()},destroy:function(){this._mouseDestroy(); -var b=function(c){e(c).removeClass("ui-resizable ui-resizable-disabled ui-resizable-resizing").removeData("resizable").unbind(".resizable").find(".ui-resizable-handle").remove()};if(this.elementIsWrapper){b(this.element);var a=this.element;a.after(this.originalElement.css({position:a.css("position"),width:a.outerWidth(),height:a.outerHeight(),top:a.css("top"),left:a.css("left")})).remove()}this.originalElement.css("resize",this.originalResizeStyle);b(this.originalElement);return this},_mouseCapture:function(b){var a= -false;for(var c in this.handles)if(e(this.handles[c])[0]==b.target)a=true;return!this.options.disabled&&a},_mouseStart:function(b){var a=this.options,c=this.element.position(),d=this.element;this.resizing=true;this.documentScroll={top:e(document).scrollTop(),left:e(document).scrollLeft()};if(d.is(".ui-draggable")||/absolute/.test(d.css("position")))d.css({position:"absolute",top:c.top,left:c.left});e.browser.opera&&/relative/.test(d.css("position"))&&d.css({position:"relative",top:"auto",left:"auto"}); -this._renderProxy();c=m(this.helper.css("left"));var f=m(this.helper.css("top"));if(a.containment){c+=e(a.containment).scrollLeft()||0;f+=e(a.containment).scrollTop()||0}this.offset=this.helper.offset();this.position={left:c,top:f};this.size=this._helper?{width:d.outerWidth(),height:d.outerHeight()}:{width:d.width(),height:d.height()};this.originalSize=this._helper?{width:d.outerWidth(),height:d.outerHeight()}:{width:d.width(),height:d.height()};this.originalPosition={left:c,top:f};this.sizeDiff= -{width:d.outerWidth()-d.width(),height:d.outerHeight()-d.height()};this.originalMousePosition={left:b.pageX,top:b.pageY};this.aspectRatio=typeof a.aspectRatio=="number"?a.aspectRatio:this.originalSize.width/this.originalSize.height||1;a=e(".ui-resizable-"+this.axis).css("cursor");e("body").css("cursor",a=="auto"?this.axis+"-resize":a);d.addClass("ui-resizable-resizing");this._propagate("start",b);return true},_mouseDrag:function(b){var a=this.helper,c=this.originalMousePosition,d=this._change[this.axis]; -if(!d)return false;c=d.apply(this,[b,b.pageX-c.left||0,b.pageY-c.top||0]);this._updateVirtualBoundaries(b.shiftKey);if(this._aspectRatio||b.shiftKey)c=this._updateRatio(c,b);c=this._respectSize(c,b);this._propagate("resize",b);a.css({top:this.position.top+"px",left:this.position.left+"px",width:this.size.width+"px",height:this.size.height+"px"});!this._helper&&this._proportionallyResizeElements.length&&this._proportionallyResize();this._updateCache(c);this._trigger("resize",b,this.ui());return false}, -_mouseStop:function(b){this.resizing=false;var a=this.options,c=this;if(this._helper){var d=this._proportionallyResizeElements,f=d.length&&/textarea/i.test(d[0].nodeName);d=f&&e.ui.hasScroll(d[0],"left")?0:c.sizeDiff.height;f=f?0:c.sizeDiff.width;f={width:c.helper.width()-f,height:c.helper.height()-d};d=parseInt(c.element.css("left"),10)+(c.position.left-c.originalPosition.left)||null;var g=parseInt(c.element.css("top"),10)+(c.position.top-c.originalPosition.top)||null;a.animate||this.element.css(e.extend(f, -{top:g,left:d}));c.helper.height(c.size.height);c.helper.width(c.size.width);this._helper&&!a.animate&&this._proportionallyResize()}e("body").css("cursor","auto");this.element.removeClass("ui-resizable-resizing");this._propagate("stop",b);this._helper&&this.helper.remove();return false},_updateVirtualBoundaries:function(b){var a=this.options,c,d,f;a={minWidth:k(a.minWidth)?a.minWidth:0,maxWidth:k(a.maxWidth)?a.maxWidth:Infinity,minHeight:k(a.minHeight)?a.minHeight:0,maxHeight:k(a.maxHeight)?a.maxHeight: -Infinity};if(this._aspectRatio||b){b=a.minHeight*this.aspectRatio;d=a.minWidth/this.aspectRatio;c=a.maxHeight*this.aspectRatio;f=a.maxWidth/this.aspectRatio;if(b>a.minWidth)a.minWidth=b;if(d>a.minHeight)a.minHeight=d;if(cb.width,h=k(b.height)&&a.minHeight&&a.minHeight>b.height;if(g)b.width=a.minWidth;if(h)b.height=a.minHeight;if(d)b.width=a.maxWidth;if(f)b.height=a.maxHeight;var i=this.originalPosition.left+this.originalSize.width,j=this.position.top+this.size.height,l=/sw|nw|w/.test(c);c=/nw|ne|n/.test(c);if(g&&l)b.left=i-a.minWidth;if(d&&l)b.left=i-a.maxWidth;if(h&&c)b.top=j-a.minHeight;if(f&&c)b.top=j-a.maxHeight;if((a=!b.width&&!b.height)&&!b.left&&b.top)b.top=null;else if(a&&!b.top&&b.left)b.left= -null;return b},_proportionallyResize:function(){if(this._proportionallyResizeElements.length)for(var b=this.helper||this.element,a=0;a');var a=e.browser.msie&&e.browser.version<7,c=a?1:0;a=a?2:-1;this.helper.addClass(this._helper).css({width:this.element.outerWidth()+ -a,height:this.element.outerHeight()+a,position:"absolute",left:this.elementOffset.left-c+"px",top:this.elementOffset.top-c+"px",zIndex:++b.zIndex});this.helper.appendTo("body").disableSelection()}else this.helper=this.element},_change:{e:function(b,a){return{width:this.originalSize.width+a}},w:function(b,a){return{left:this.originalPosition.left+a,width:this.originalSize.width-a}},n:function(b,a,c){return{top:this.originalPosition.top+c,height:this.originalSize.height-c}},s:function(b,a,c){return{height:this.originalSize.height+ -c}},se:function(b,a,c){return e.extend(this._change.s.apply(this,arguments),this._change.e.apply(this,[b,a,c]))},sw:function(b,a,c){return e.extend(this._change.s.apply(this,arguments),this._change.w.apply(this,[b,a,c]))},ne:function(b,a,c){return e.extend(this._change.n.apply(this,arguments),this._change.e.apply(this,[b,a,c]))},nw:function(b,a,c){return e.extend(this._change.n.apply(this,arguments),this._change.w.apply(this,[b,a,c]))}},_propagate:function(b,a){e.ui.plugin.call(this,b,[a,this.ui()]); -b!="resize"&&this._trigger(b,a,this.ui())},plugins:{},ui:function(){return{originalElement:this.originalElement,element:this.element,helper:this.helper,position:this.position,size:this.size,originalSize:this.originalSize,originalPosition:this.originalPosition}}});e.extend(e.ui.resizable,{version:"1.8.14"});e.ui.plugin.add("resizable","alsoResize",{start:function(){var b=e(this).data("resizable").options,a=function(c){e(c).each(function(){var d=e(this);d.data("resizable-alsoresize",{width:parseInt(d.width(), -10),height:parseInt(d.height(),10),left:parseInt(d.css("left"),10),top:parseInt(d.css("top"),10),position:d.css("position")})})};if(typeof b.alsoResize=="object"&&!b.alsoResize.parentNode)if(b.alsoResize.length){b.alsoResize=b.alsoResize[0];a(b.alsoResize)}else e.each(b.alsoResize,function(c){a(c)});else a(b.alsoResize)},resize:function(b,a){var c=e(this).data("resizable");b=c.options;var d=c.originalSize,f=c.originalPosition,g={height:c.size.height-d.height||0,width:c.size.width-d.width||0,top:c.position.top- -f.top||0,left:c.position.left-f.left||0},h=function(i,j){e(i).each(function(){var l=e(this),q=e(this).data("resizable-alsoresize"),p={},r=j&&j.length?j:l.parents(a.originalElement[0]).length?["width","height"]:["width","height","top","left"];e.each(r,function(n,o){if((n=(q[o]||0)+(g[o]||0))&&n>=0)p[o]=n||null});if(e.browser.opera&&/relative/.test(l.css("position"))){c._revertToRelativePosition=true;l.css({position:"absolute",top:"auto",left:"auto"})}l.css(p)})};typeof b.alsoResize=="object"&&!b.alsoResize.nodeType? -e.each(b.alsoResize,function(i,j){h(i,j)}):h(b.alsoResize)},stop:function(){var b=e(this).data("resizable"),a=b.options,c=function(d){e(d).each(function(){var f=e(this);f.css({position:f.data("resizable-alsoresize").position})})};if(b._revertToRelativePosition){b._revertToRelativePosition=false;typeof a.alsoResize=="object"&&!a.alsoResize.nodeType?e.each(a.alsoResize,function(d){c(d)}):c(a.alsoResize)}e(this).removeData("resizable-alsoresize")}});e.ui.plugin.add("resizable","animate",{stop:function(b){var a= -e(this).data("resizable"),c=a.options,d=a._proportionallyResizeElements,f=d.length&&/textarea/i.test(d[0].nodeName),g=f&&e.ui.hasScroll(d[0],"left")?0:a.sizeDiff.height;f={width:a.size.width-(f?0:a.sizeDiff.width),height:a.size.height-g};g=parseInt(a.element.css("left"),10)+(a.position.left-a.originalPosition.left)||null;var h=parseInt(a.element.css("top"),10)+(a.position.top-a.originalPosition.top)||null;a.element.animate(e.extend(f,h&&g?{top:h,left:g}:{}),{duration:c.animateDuration,easing:c.animateEasing, -step:function(){var i={width:parseInt(a.element.css("width"),10),height:parseInt(a.element.css("height"),10),top:parseInt(a.element.css("top"),10),left:parseInt(a.element.css("left"),10)};d&&d.length&&e(d[0]).css({width:i.width,height:i.height});a._updateCache(i);a._propagate("resize",b)}})}});e.ui.plugin.add("resizable","containment",{start:function(){var b=e(this).data("resizable"),a=b.element,c=b.options.containment;if(a=c instanceof e?c.get(0):/parent/.test(c)?a.parent().get(0):c){b.containerElement= -e(a);if(/document/.test(c)||c==document){b.containerOffset={left:0,top:0};b.containerPosition={left:0,top:0};b.parentData={element:e(document),left:0,top:0,width:e(document).width(),height:e(document).height()||document.body.parentNode.scrollHeight}}else{var d=e(a),f=[];e(["Top","Right","Left","Bottom"]).each(function(i,j){f[i]=m(d.css("padding"+j))});b.containerOffset=d.offset();b.containerPosition=d.position();b.containerSize={height:d.innerHeight()-f[3],width:d.innerWidth()-f[1]};c=b.containerOffset; -var g=b.containerSize.height,h=b.containerSize.width;h=e.ui.hasScroll(a,"left")?a.scrollWidth:h;g=e.ui.hasScroll(a)?a.scrollHeight:g;b.parentData={element:a,left:c.left,top:c.top,width:h,height:g}}}},resize:function(b){var a=e(this).data("resizable"),c=a.options,d=a.containerOffset,f=a.position;b=a._aspectRatio||b.shiftKey;var g={top:0,left:0},h=a.containerElement;if(h[0]!=document&&/static/.test(h.css("position")))g=d;if(f.left<(a._helper?d.left:0)){a.size.width+=a._helper?a.position.left-d.left: -a.position.left-g.left;if(b)a.size.height=a.size.width/c.aspectRatio;a.position.left=c.helper?d.left:0}if(f.top<(a._helper?d.top:0)){a.size.height+=a._helper?a.position.top-d.top:a.position.top;if(b)a.size.width=a.size.height*c.aspectRatio;a.position.top=a._helper?d.top:0}a.offset.left=a.parentData.left+a.position.left;a.offset.top=a.parentData.top+a.position.top;c=Math.abs((a._helper?a.offset.left-g.left:a.offset.left-g.left)+a.sizeDiff.width);d=Math.abs((a._helper?a.offset.top-g.top:a.offset.top- -d.top)+a.sizeDiff.height);f=a.containerElement.get(0)==a.element.parent().get(0);g=/relative|absolute/.test(a.containerElement.css("position"));if(f&&g)c-=a.parentData.left;if(c+a.size.width>=a.parentData.width){a.size.width=a.parentData.width-c;if(b)a.size.height=a.size.width/a.aspectRatio}if(d+a.size.height>=a.parentData.height){a.size.height=a.parentData.height-d;if(b)a.size.width=a.size.height*a.aspectRatio}},stop:function(){var b=e(this).data("resizable"),a=b.options,c=b.containerOffset,d=b.containerPosition, -f=b.containerElement,g=e(b.helper),h=g.offset(),i=g.outerWidth()-b.sizeDiff.width;g=g.outerHeight()-b.sizeDiff.height;b._helper&&!a.animate&&/relative/.test(f.css("position"))&&e(this).css({left:h.left-d.left-c.left,width:i,height:g});b._helper&&!a.animate&&/static/.test(f.css("position"))&&e(this).css({left:h.left-d.left-c.left,width:i,height:g})}});e.ui.plugin.add("resizable","ghost",{start:function(){var b=e(this).data("resizable"),a=b.options,c=b.size;b.ghost=b.originalElement.clone();b.ghost.css({opacity:0.25, -display:"block",position:"relative",height:c.height,width:c.width,margin:0,left:0,top:0}).addClass("ui-resizable-ghost").addClass(typeof a.ghost=="string"?a.ghost:"");b.ghost.appendTo(b.helper)},resize:function(){var b=e(this).data("resizable");b.ghost&&b.ghost.css({position:"relative",height:b.size.height,width:b.size.width})},stop:function(){var b=e(this).data("resizable");b.ghost&&b.helper&&b.helper.get(0).removeChild(b.ghost.get(0))}});e.ui.plugin.add("resizable","grid",{resize:function(){var b= -e(this).data("resizable"),a=b.options,c=b.size,d=b.originalSize,f=b.originalPosition,g=b.axis;a.grid=typeof a.grid=="number"?[a.grid,a.grid]:a.grid;var h=Math.round((c.width-d.width)/(a.grid[0]||1))*(a.grid[0]||1);a=Math.round((c.height-d.height)/(a.grid[1]||1))*(a.grid[1]||1);if(/^(se|s|e)$/.test(g)){b.size.width=d.width+h;b.size.height=d.height+a}else if(/^(ne)$/.test(g)){b.size.width=d.width+h;b.size.height=d.height+a;b.position.top=f.top-a}else{if(/^(sw)$/.test(g)){b.size.width=d.width+h;b.size.height= -d.height+a}else{b.size.width=d.width+h;b.size.height=d.height+a;b.position.top=f.top-a}b.position.left=f.left-h}}});var m=function(b){return parseInt(b,10)||0},k=function(b){return!isNaN(parseInt(b,10))}})(jQuery); -;/* - * jQuery UI Effects 1.8.14 - * - * Copyright 2011, AUTHORS.txt (http://jqueryui.com/about) - * Dual licensed under the MIT or GPL Version 2 licenses. - * http://jquery.org/license - * - * http://docs.jquery.com/UI/Effects/ - */ -jQuery.effects||function(f,j){function m(c){var a;if(c&&c.constructor==Array&&c.length==3)return c;if(a=/rgb\(\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*\)/.exec(c))return[parseInt(a[1],10),parseInt(a[2],10),parseInt(a[3],10)];if(a=/rgb\(\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*\)/.exec(c))return[parseFloat(a[1])*2.55,parseFloat(a[2])*2.55,parseFloat(a[3])*2.55];if(a=/#([a-fA-F0-9]{2})([a-fA-F0-9]{2})([a-fA-F0-9]{2})/.exec(c))return[parseInt(a[1], -16),parseInt(a[2],16),parseInt(a[3],16)];if(a=/#([a-fA-F0-9])([a-fA-F0-9])([a-fA-F0-9])/.exec(c))return[parseInt(a[1]+a[1],16),parseInt(a[2]+a[2],16),parseInt(a[3]+a[3],16)];if(/rgba\(0, 0, 0, 0\)/.exec(c))return n.transparent;return n[f.trim(c).toLowerCase()]}function s(c,a){var b;do{b=f.curCSS(c,a);if(b!=""&&b!="transparent"||f.nodeName(c,"body"))break;a="backgroundColor"}while(c=c.parentNode);return m(b)}function o(){var c=document.defaultView?document.defaultView.getComputedStyle(this,null):this.currentStyle, -a={},b,d;if(c&&c.length&&c[0]&&c[c[0]])for(var e=c.length;e--;){b=c[e];if(typeof c[b]=="string"){d=b.replace(/\-(\w)/g,function(g,h){return h.toUpperCase()});a[d]=c[b]}}else for(b in c)if(typeof c[b]==="string")a[b]=c[b];return a}function p(c){var a,b;for(a in c){b=c[a];if(b==null||f.isFunction(b)||a in t||/scrollbar/.test(a)||!/color/i.test(a)&&isNaN(parseFloat(b)))delete c[a]}return c}function u(c,a){var b={_:0},d;for(d in a)if(c[d]!=a[d])b[d]=a[d];return b}function k(c,a,b,d){if(typeof c=="object"){d= -a;b=null;a=c;c=a.effect}if(f.isFunction(a)){d=a;b=null;a={}}if(typeof a=="number"||f.fx.speeds[a]){d=b;b=a;a={}}if(f.isFunction(b)){d=b;b=null}a=a||{};b=b||a.duration;b=f.fx.off?0:typeof b=="number"?b:b in f.fx.speeds?f.fx.speeds[b]:f.fx.speeds._default;d=d||a.complete;return[c,a,b,d]}function l(c){if(!c||typeof c==="number"||f.fx.speeds[c])return true;if(typeof c==="string"&&!f.effects[c])return true;return false}f.effects={};f.each(["backgroundColor","borderBottomColor","borderLeftColor","borderRightColor", -"borderTopColor","borderColor","color","outlineColor"],function(c,a){f.fx.step[a]=function(b){if(!b.colorInit){b.start=s(b.elem,a);b.end=m(b.end);b.colorInit=true}b.elem.style[a]="rgb("+Math.max(Math.min(parseInt(b.pos*(b.end[0]-b.start[0])+b.start[0],10),255),0)+","+Math.max(Math.min(parseInt(b.pos*(b.end[1]-b.start[1])+b.start[1],10),255),0)+","+Math.max(Math.min(parseInt(b.pos*(b.end[2]-b.start[2])+b.start[2],10),255),0)+")"}});var n={aqua:[0,255,255],azure:[240,255,255],beige:[245,245,220],black:[0, -0,0],blue:[0,0,255],brown:[165,42,42],cyan:[0,255,255],darkblue:[0,0,139],darkcyan:[0,139,139],darkgrey:[169,169,169],darkgreen:[0,100,0],darkkhaki:[189,183,107],darkmagenta:[139,0,139],darkolivegreen:[85,107,47],darkorange:[255,140,0],darkorchid:[153,50,204],darkred:[139,0,0],darksalmon:[233,150,122],darkviolet:[148,0,211],fuchsia:[255,0,255],gold:[255,215,0],green:[0,128,0],indigo:[75,0,130],khaki:[240,230,140],lightblue:[173,216,230],lightcyan:[224,255,255],lightgreen:[144,238,144],lightgrey:[211, -211,211],lightpink:[255,182,193],lightyellow:[255,255,224],lime:[0,255,0],magenta:[255,0,255],maroon:[128,0,0],navy:[0,0,128],olive:[128,128,0],orange:[255,165,0],pink:[255,192,203],purple:[128,0,128],violet:[128,0,128],red:[255,0,0],silver:[192,192,192],white:[255,255,255],yellow:[255,255,0],transparent:[255,255,255]},q=["add","remove","toggle"],t={border:1,borderBottom:1,borderColor:1,borderLeft:1,borderRight:1,borderTop:1,borderWidth:1,margin:1,padding:1};f.effects.animateClass=function(c,a,b, -d){if(f.isFunction(b)){d=b;b=null}return this.queue(function(){var e=f(this),g=e.attr("style")||" ",h=p(o.call(this)),r,v=e.attr("class");f.each(q,function(w,i){c[i]&&e[i+"Class"](c[i])});r=p(o.call(this));e.attr("class",v);e.animate(u(h,r),{queue:false,duration:a,easing:b,complete:function(){f.each(q,function(w,i){c[i]&&e[i+"Class"](c[i])});if(typeof e.attr("style")=="object"){e.attr("style").cssText="";e.attr("style").cssText=g}else e.attr("style",g);d&&d.apply(this,arguments);f.dequeue(this)}})})}; -f.fn.extend({_addClass:f.fn.addClass,addClass:function(c,a,b,d){return a?f.effects.animateClass.apply(this,[{add:c},a,b,d]):this._addClass(c)},_removeClass:f.fn.removeClass,removeClass:function(c,a,b,d){return a?f.effects.animateClass.apply(this,[{remove:c},a,b,d]):this._removeClass(c)},_toggleClass:f.fn.toggleClass,toggleClass:function(c,a,b,d,e){return typeof a=="boolean"||a===j?b?f.effects.animateClass.apply(this,[a?{add:c}:{remove:c},b,d,e]):this._toggleClass(c,a):f.effects.animateClass.apply(this, -[{toggle:c},a,b,d])},switchClass:function(c,a,b,d,e){return f.effects.animateClass.apply(this,[{add:a,remove:c},b,d,e])}});f.extend(f.effects,{version:"1.8.14",save:function(c,a){for(var b=0;b").addClass("ui-effects-wrapper").css({fontSize:"100%",background:"transparent",border:"none",margin:0,padding:0}); -c.wrap(b);b=c.parent();if(c.css("position")=="static"){b.css({position:"relative"});c.css({position:"relative"})}else{f.extend(a,{position:c.css("position"),zIndex:c.css("z-index")});f.each(["top","left","bottom","right"],function(d,e){a[e]=c.css(e);if(isNaN(parseInt(a[e],10)))a[e]="auto"});c.css({position:"relative",top:0,left:0,right:"auto",bottom:"auto"})}return b.css(a).show()},removeWrapper:function(c){if(c.parent().is(".ui-effects-wrapper"))return c.parent().replaceWith(c);return c},setTransition:function(c, -a,b,d){d=d||{};f.each(a,function(e,g){unit=c.cssUnit(g);if(unit[0]>0)d[g]=unit[0]*b+unit[1]});return d}});f.fn.extend({effect:function(c){var a=k.apply(this,arguments),b={options:a[1],duration:a[2],callback:a[3]};a=b.options.mode;var d=f.effects[c];if(f.fx.off||!d)return a?this[a](b.duration,b.callback):this.each(function(){b.callback&&b.callback.call(this)});return d.call(this,b)},_show:f.fn.show,show:function(c){if(l(c))return this._show.apply(this,arguments);else{var a=k.apply(this,arguments); -a[1].mode="show";return this.effect.apply(this,a)}},_hide:f.fn.hide,hide:function(c){if(l(c))return this._hide.apply(this,arguments);else{var a=k.apply(this,arguments);a[1].mode="hide";return this.effect.apply(this,a)}},__toggle:f.fn.toggle,toggle:function(c){if(l(c)||typeof c==="boolean"||f.isFunction(c))return this.__toggle.apply(this,arguments);else{var a=k.apply(this,arguments);a[1].mode="toggle";return this.effect.apply(this,a)}},cssUnit:function(c){var a=this.css(c),b=[];f.each(["em","px","%", -"pt"],function(d,e){if(a.indexOf(e)>0)b=[parseFloat(a),e]});return b}});f.easing.jswing=f.easing.swing;f.extend(f.easing,{def:"easeOutQuad",swing:function(c,a,b,d,e){return f.easing[f.easing.def](c,a,b,d,e)},easeInQuad:function(c,a,b,d,e){return d*(a/=e)*a+b},easeOutQuad:function(c,a,b,d,e){return-d*(a/=e)*(a-2)+b},easeInOutQuad:function(c,a,b,d,e){if((a/=e/2)<1)return d/2*a*a+b;return-d/2*(--a*(a-2)-1)+b},easeInCubic:function(c,a,b,d,e){return d*(a/=e)*a*a+b},easeOutCubic:function(c,a,b,d,e){return d* -((a=a/e-1)*a*a+1)+b},easeInOutCubic:function(c,a,b,d,e){if((a/=e/2)<1)return d/2*a*a*a+b;return d/2*((a-=2)*a*a+2)+b},easeInQuart:function(c,a,b,d,e){return d*(a/=e)*a*a*a+b},easeOutQuart:function(c,a,b,d,e){return-d*((a=a/e-1)*a*a*a-1)+b},easeInOutQuart:function(c,a,b,d,e){if((a/=e/2)<1)return d/2*a*a*a*a+b;return-d/2*((a-=2)*a*a*a-2)+b},easeInQuint:function(c,a,b,d,e){return d*(a/=e)*a*a*a*a+b},easeOutQuint:function(c,a,b,d,e){return d*((a=a/e-1)*a*a*a*a+1)+b},easeInOutQuint:function(c,a,b,d,e){if((a/= -e/2)<1)return d/2*a*a*a*a*a+b;return d/2*((a-=2)*a*a*a*a+2)+b},easeInSine:function(c,a,b,d,e){return-d*Math.cos(a/e*(Math.PI/2))+d+b},easeOutSine:function(c,a,b,d,e){return d*Math.sin(a/e*(Math.PI/2))+b},easeInOutSine:function(c,a,b,d,e){return-d/2*(Math.cos(Math.PI*a/e)-1)+b},easeInExpo:function(c,a,b,d,e){return a==0?b:d*Math.pow(2,10*(a/e-1))+b},easeOutExpo:function(c,a,b,d,e){return a==e?b+d:d*(-Math.pow(2,-10*a/e)+1)+b},easeInOutExpo:function(c,a,b,d,e){if(a==0)return b;if(a==e)return b+d;if((a/= -e/2)<1)return d/2*Math.pow(2,10*(a-1))+b;return d/2*(-Math.pow(2,-10*--a)+2)+b},easeInCirc:function(c,a,b,d,e){return-d*(Math.sqrt(1-(a/=e)*a)-1)+b},easeOutCirc:function(c,a,b,d,e){return d*Math.sqrt(1-(a=a/e-1)*a)+b},easeInOutCirc:function(c,a,b,d,e){if((a/=e/2)<1)return-d/2*(Math.sqrt(1-a*a)-1)+b;return d/2*(Math.sqrt(1-(a-=2)*a)+1)+b},easeInElastic:function(c,a,b,d,e){c=1.70158;var g=0,h=d;if(a==0)return b;if((a/=e)==1)return b+d;g||(g=e*0.3);if(h 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:)/.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 plugins_; - if (typeof(plugins) != 'undefined') - { - plugins_ = plugins; - } - else - { - plugins_ = parent.parent.plugins; - } - - var hookFilters = plugins_.callHook("aceGetFilterStack", { - linestylefilter: linestylefilter, - browser: browser - }); - hookFilters.map(function(hookFilter) - { - func = hookFilter(lineText, func); - }); - - if (browser !== undefined && browser.msie) - { - // IE7+ will take an e-mail address like 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, ''); -}; diff --git a/static/js/pad2.js b/static/js/pad.js similarity index 78% rename from static/js/pad2.js rename to static/js/pad.js index b75c1f9cc..bda68956e 100644 --- a/static/js/pad2.js +++ b/static/js/pad.js @@ -23,67 +23,32 @@ /* global $, window */ var socket; -var LineNumbersDisabled = false; -var noColors = false; -var useMonospaceFontGlobal = false; -var globalUserName = false; -var hideQRCode = false; -var rtlIsTrue = false; -$(document).ready(function() -{ - //start the costum js - if(typeof costumStart == "function") costumStart(); - getParams(); - handshake(); -}); +// 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'); -$(window).unload(function() -{ - pad.dispose(); -}); +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; -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; -} - -function randomString() -{ - var chars = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"; - var string_length = 20; - 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 "t." + randomstring; -} +var createCookie = require('/pad_utils').createCookie; +var readCookie = require('/pad_utils').readCookie; +var randomString = require('/pad_utils').randomString; function getParams() { @@ -96,12 +61,13 @@ function getParams() var IsnoColors = params["noColors"]; var hideQRCode = params["hideQRCode"]; var rtl = params["rtl"]; + var alwaysShowChat = params["alwaysShowChat"]; if(IsnoColors) { if(IsnoColors == "true") { - noColors = true; + settings.noColors = true; $('#clearAuthorship').hide(); } } @@ -124,20 +90,20 @@ function getParams() { if(showLineNumbers == "false") { - LineNumbersDisabled = true; + settings.LineNumbersDisabled = true; } } if(useMonospaceFont) { if(useMonospaceFont == "true") { - useMonospaceFontGlobal = 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. - globalUserName = unescape(userName); + settings.globalUserName = decodeURIComponent(userName); } if(hideQRCode) { @@ -147,7 +113,14 @@ function getParams() { if(rtl == "true") { - rtlIsTrue = true + settings.rtlIsTrue = true + } + } + if(alwaysShowChat) + { + if(alwaysShowChat == "true") + { + chat.stickToScreen(); } } } @@ -173,6 +146,12 @@ function savePassword() 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. Fix this issue"); + } +} function handshake() { var loc = document.location; @@ -183,9 +162,10 @@ function handshake() //find out in which subfolder we are var resource = loc.pathname.substr(1, loc.pathname.indexOf("/p/")) + "socket.io"; //connect - socket = io.connect(url, { + socket = pad.socket = io.connect(url, { resource: resource, - 'max reconnection attempts': 3 + 'max reconnection attempts': 3, + 'sync disconnect on unload' : false }); function sendClientReady(isReconnect) @@ -194,12 +174,12 @@ function handshake() padId = decodeURIComponent(padId); // unescape neccesary due to Safari and Opera interpretation of spaces if(!isReconnect) - document.title = document.title + " | " + padId; + document.title = padId.replace(/_+/g, ' ') + " | " + document.title; var token = readCookie("token"); if (token == null) { - token = randomString(); + token = "t." + randomString(); createCookie("token", token, 60); } @@ -243,15 +223,19 @@ function handshake() sendClientReady(true); }); - socket.on('disconnect', function () { - function disconnectEvent() - { - pad.collabClient.setChannelState("DISCONNECTED", "reconnect_timeout"); + 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); } - - pad.collabClient.setChannelState("RECONNECTING"); - - disconnectTimeout = setTimeout(disconnectEvent, 10000); }); var receivedClientVars = false; @@ -270,13 +254,13 @@ function handshake() { $("#editorloadingbox").html("You need a password to access this pad
" + ""+ - ""); + ""); } else if(obj.accessStatus == "wrongPassword") { $("#editorloadingbox").html("You're password was wrong
" + ""+ - ""); + ""); } } @@ -294,37 +278,37 @@ function handshake() clientVars.collab_client_vars.clientAgent = "Anonymous"; //initalize the pad - pad.init(); + pad._afterHandshake(); initalized = true; // If the LineNumbersDisabled value is set to true then we need to hide the Line Numbers - if (LineNumbersDisabled == true) + if (settings.LineNumbersDisabled == true) { pad.changeViewOption('showLineNumbers', false); } - // If the noColors value is set to true then we need to hide the backround colors on the ace spans - if (noColors == true) + // 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 (rtlIsTrue == true) + if (settings.rtlIsTrue == true) { pad.changeViewOption('rtl', true); } // If the Monospacefont value is set to true then change it to monospace. - if (useMonospaceFontGlobal == true) + 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 (globalUserName !== false) + if (settings.globalUserName !== false) { - pad.notifyChangeName(globalUserName); // Notifies the server - pad.myUserInfo.name = globalUserName; - $('#myusernameedit').attr({"value":globalUserName}); // Updates the current users UI + 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 @@ -343,7 +327,6 @@ function handshake() } } }); - // Bind the colorpicker var fb = $('#colorpicker').farbtastic({ callback: '#mycolorpickerpreview', width: 220}); } @@ -392,7 +375,6 @@ var pad = { { return clientVars.userIsGuest; }, - // getUserId: function() { return pad.myUserInfo.userId; @@ -407,11 +389,25 @@ var pad = { }, 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(); + chat.init(this); pad.initTime = +(new Date()); pad.padOptions = clientVars.initialOptions; @@ -432,10 +428,10 @@ var pad = { } // order of inits is important here: - padcookie.init(clientVars.cookiePrefsToSet); - + padcookie.init(clientVars.cookiePrefsToSet, this); + $("#widthprefcheck").click(pad.toggleWidthPref); - $("#sidebarcheck").click(pad.toggleSidebar); + // $("#sidebarcheck").click(pad.togglewSidebar); pad.myUserInfo = { userId: clientVars.userId, @@ -459,20 +455,20 @@ var pad = { initialTitle: clientVars.initialTitle, initialPassword: clientVars.initialPassword, guestPolicy: pad.padOptions.guestPolicy - }); - padimpexp.init(); - padsavedrevs.init(clientVars.initialRevisionList); + }, this); + padimpexp.init(this); + padsavedrevs.init(clientVars.initialRevisionList, this); - padeditor.init(postAceInit, pad.padOptions.view || {}); + padeditor.init(postAceInit, pad.padOptions.view || {}, this); - paduserlist.init(pad.myUserInfo); + paduserlist.init(pad.myUserInfo, this); // padchat.init(clientVars.chatHistory, pad.myUserInfo); padconnectionstatus.init(); - padmodals.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); @@ -488,6 +484,13 @@ var pad = { { 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() @@ -543,16 +546,6 @@ var pad = { }; options.view[key] = value; pad.handleOptionsChange(options); - // if the request isn't to hide line numbers then broadcast this to other users - if (key != "showLineNumbers" && key != "useMonospaceFont") - { - pad.collabClient.sendClientMessage( - { - type: 'padoptions', - options: options, - changedBy: pad.myUserInfo.name || "unnamed" - }); - } }, handleOptionsChange: function(opts) { @@ -760,15 +753,18 @@ var pad = { padsavedrevs.handleIsFullyConnected(isConnected); - pad.determineSidebarVisibility(isConnected && !isInitialConnect); + // pad.determineSidebarVisibility(isConnected && !isInitialConnect); + pad.determineChatVisibility(isConnected && !isInitialConnect); + pad.determineAuthorshipColorsVisibility(); + }, - determineSidebarVisibility: function(asNowConnectedFeedback) +/* determineSidebarVisibility: function(asNowConnectedFeedback) { if (pad.isFullyConnected()) { var setSidebarVisibility = padutils.getCancellableAction("set-sidebar-visibility", function() { - $("body").toggleClass('hidesidebar', !! padcookie.getPref('hideSidebar')); + // $("body").toggleClass('hidesidebar', !! padcookie.getPref('hideSidebar')); }); window.setTimeout(setSidebarVisibility, asNowConnectedFeedback ? 3000 : 0); } @@ -778,6 +774,27 @@ var pad = { $("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") @@ -826,6 +843,7 @@ var pad = { $("#widthprefcheck").toggleClass('widthprefchecked', !! newValue).toggleClass('widthprefunchecked', !newValue); pad.handleWidthChange(); }, +/* toggleSidebar: function() { var newValue = !padcookie.getPref('hideSidebar'); @@ -833,6 +851,7 @@ var pad = { $("#sidebarcheck").toggleClass('sidebarchecked', !newValue).toggleClass('sidebarunchecked', !! newValue); pad.determineSidebarVisibility(); }, +*/ handleWidthChange: function() { var isFullWidth = padcookie.getPref('fullWidth'); @@ -951,3 +970,31 @@ var alertBar = (function() }; 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/static/js/pad_connectionstatus.js b/static/js/pad_connectionstatus.js index d35eb02d6..1de024e8d 100644 --- a/static/js/pad_connectionstatus.js +++ b/static/js/pad_connectionstatus.js @@ -20,6 +20,8 @@ * limitations under the License. */ +var padmodals = require('/pad_modals').padmodals; + var padconnectionstatus = (function() { @@ -85,3 +87,5 @@ var padconnectionstatus = (function() }; return self; }()); + +exports.padconnectionstatus = padconnectionstatus; diff --git a/static/js/pad_cookie.js b/static/js/pad_cookie.js index 64211750d..1bb5700ad 100644 --- a/static/js/pad_cookie.js +++ b/static/js/pad_cookie.js @@ -85,9 +85,12 @@ var padcookie = (function() var alreadyWarnedAboutNoCookies = false; var inited = false; + var pad = undefined; var self = { - init: function(prefsToSet) + init: function(prefsToSet, _pad) { + pad = _pad; + var rawCookie = getRawCookie(); if (rawCookie) { @@ -126,3 +129,5 @@ var padcookie = (function() }; return self; }()); + +exports.padcookie = padcookie; diff --git a/static/js/pad_docbar.js b/static/js/pad_docbar.js index c67ca55c4..b83bf3bfe 100644 --- a/static/js/pad_docbar.js +++ b/static/js/pad_docbar.js @@ -20,6 +20,7 @@ * limitations under the License. */ +var padutils = require('/pad_utils').padutils; var paddocbar = (function() { @@ -113,11 +114,14 @@ var paddocbar = (function() self.renderPassword(); } + var pad = undefined; var self = { title: null, password: null, - init: function(opts) + init: function(opts, _pad) { + pad = _pad; + panels = { impexp: { animator: getPanelOpenCloseAnimator("impexp", 160) @@ -444,6 +448,8 @@ var paddocbar = (function() }, handleResizePage: function() { + // Side-step circular reference. This should be injected. + var padsavedrevs = require('/pad_savedrevs').padsavedrevs; padsavedrevs.handleResizePage(); }, hideLaterIfNoOtherInteraction: function() @@ -456,3 +462,5 @@ var paddocbar = (function() }; return self; }()); + +exports.paddocbar = paddocbar; diff --git a/static/js/pad_editbar.js b/static/js/pad_editbar.js index 9de23f82e..236926310 100644 --- a/static/js/pad_editbar.js +++ b/static/js/pad_editbar.js @@ -20,6 +20,19 @@ * 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() { @@ -104,17 +117,20 @@ var padeditbar = (function() { self.toogleDropDown("users"); } + else if (cmd == 'settings') + { + self.toogleDropDown("settingsmenu"); + } else if (cmd == 'embed') { self.setEmbedLinks(); - $('#embedinput').focus().select(); + $('#linkinput').focus().select(); self.toogleDropDown("embed"); } else if (cmd == 'import_export') { self.toogleDropDown("importexport"); } - else if (cmd == 'save') { padsavedrevs.saveNow(); @@ -126,6 +142,7 @@ var padeditbar = (function() 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)) @@ -156,15 +173,16 @@ var padeditbar = (function() }, cmd, true); } } - padeditor.ace.focus(); + if(padeditor.ace) padeditor.ace.focus(); }, toogleDropDown: function(moduleName) { - var modules = ["embed", "users", "readonly", "importexport"]; + var modules = ["settingsmenu", "importexport", "embed", "users"]; //hide all modules if(moduleName == "none") { + $("#editbar ul#menu_right > li").removeClass("selected"); for(var i=0;i 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"); $('#linkinput').val(readonlyLink); - $('#embedreadonlyqr').attr("src","https://chart.googleapis.com/chart?chs=200x200&cht=qr&chld=H|0&chl=" + 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("