clean up
This commit is contained in:
commit
87adcd7c14
|
@ -0,0 +1,55 @@
|
|||
# Developer Guidelines
|
||||
|
||||
Please talk to people on the mailing list before you change this page
|
||||
|
||||
Mailing list: https://groups.google.com/forum/?fromgroups#!forum/etherpad-lite-dev
|
||||
|
||||
IRC channels: [#etherpad](irc://freenode/#etherpad) ([webchat](webchat.freenode.net?channels=etherpad)), [#etherpad-lite-dev](irc://freenode/#etherpad-lite-dev) ([webchat](webchat.freenode.net?channels=etherpad-lite-dev))
|
||||
|
||||
**Our goal is to iterate in small steps. Release often, release early. Evolution instead of a revolution**
|
||||
|
||||
## General goals of Etherpad Lite
|
||||
* easy to install for admins
|
||||
* easy to use for people
|
||||
* using less resources on server side
|
||||
* easy to embed for admins
|
||||
* also runable as etherpad lite only
|
||||
* keep it maintainable, we don't wanna end ob as the monster Etherpad was
|
||||
* extensible, as much functionality should be extendable with plugins so changes don't have to be done in core
|
||||
|
||||
## How to code:
|
||||
* **Please write comments**. I don't mean you have to comment every line and every loop. I just mean, if you do anything thats a bit complex or a bit weird, please leave a comment. It's easy to do that if you do while you're writing the code. Keep in mind that you will probably leave the project at some point and that other people will read your code. Undocumented huge amounts of code are worthless
|
||||
* Never ever use tabs
|
||||
* Indentation: JS/CSS: 2 spaces; HTML: 4 spaces
|
||||
* Don't overengineer. Don't try to solve any possible problem in one step. Try to solve problems as easy as possible and improve the solution over time
|
||||
* Do generalize sooner or later - if an old solution hacked together according to the above point, poses more problems than it solves today, reengineer it, with the lessons learned taken into account.
|
||||
* Keep it compatible to API-Clients/older DBs/configurations. Don't make incompatible changes the protocol/database format without good reasons
|
||||
|
||||
## How to work with git
|
||||
* Make a new branch for every feature you're working on. Don't work in your master branch. This ensures that you can work you can do lot of small pull requests instead of one big one with complete different features
|
||||
* Don't use the online edit function of github. This only creates ugly and not working commits
|
||||
* Test before you push. Sounds easy, it isn't
|
||||
* Try to make clean commits that are easy readable
|
||||
* Don't check in stuff that gets generated during build or runtime (like jquery, minified files, dbs etc...)
|
||||
* Make pull requests from your feature branch to our develop branch once your feature is ready
|
||||
* Make small pull requests that are easy to review but make sure they do add value by themselves / individually
|
||||
|
||||
## Branching model in Etherpad Lite
|
||||
see git flow http://nvie.com/posts/a-successful-git-branching-model/
|
||||
|
||||
* master, the stable. This is the branch everyone should use for production stuff
|
||||
* develop, everything that is READY to go into master at some point in time. This stuff is tested and ready to go out
|
||||
* release branches, stuff that should go into master very soon, only bugfixes go into these (see http://nvie.com/posts/a-successful-git-branching-model/ for why)
|
||||
* you can set tags in the master branch, there is no real need for release branches imho
|
||||
* The latest tag is not what is shown in github by default. Doing a clone of master should give you latest stable, not what is gonna be latest stable in a week, also, we should not be blocking new features to develop, just because we feel that we should be releasing it to master soon. This is the situation that release branches solve/handle.
|
||||
* hotfix branches, fixes for bugs in master
|
||||
* feature branches (in your own repos), these are the branches where you develop your features in. If its ready to go out, it will be merged into develop
|
||||
|
||||
Over the time we pull features from feature branches into the develop branch. Every month we pull from develop into master. Bugs in master get fixed in hotfix branches. These branches will get merged into master AND develop. There should never be commits in master that aren't in develop
|
||||
|
||||
## Documentation
|
||||
The docs are in the `doc/` folder in the git repository, so people can easily find the suitable docs for the current git revision.
|
||||
|
||||
Documentation should be kept up-to-date. This means, whenever you add a new API method, add a new hook or change the database model, pack the relevant changes to the docs in the same pull request.
|
||||
|
||||
You can build the docs e.g. produce html, using `make docs`. At some point in the future we will provide an online documentation. The current documentation in the github wiki should always reflect the state of `master` (!), since there are no docs in master, yet.
|
|
@ -0,0 +1,13 @@
|
|||
doc_dirs = doc $(wildcard doc/*/)
|
||||
outdoc_dirs = out $(addprefix out/,$(doc_dirs))
|
||||
doc_sources = $(wildcard doc/*/*.md) $(wildcard doc/*.md)
|
||||
outdoc_files = $(addprefix out/,$(doc_sources:.md=.html))
|
||||
|
||||
docs: $(outdoc_files)
|
||||
|
||||
out/doc/%.html: doc/%.md
|
||||
mkdir -p $(@D)
|
||||
node tools/doc/generate.js --format=html --template=doc/template.html $< > $@
|
||||
|
||||
clean:
|
||||
rm -rf out/
|
131
README.md
131
README.md
|
@ -1,131 +0,0 @@
|
|||
# Our goal is to make collaborative editing the standard on the web
|
||||
|
||||
# About
|
||||
Etherpad lite is a really-real time collaborative editor spawned from the Hell fire of Etherpad.
|
||||
We're reusing the well tested Etherpad easysync library to make it really realtime. Etherpad Lite
|
||||
is based on node.js ergo is much lighter and more stable than the original Etherpad. Our hope
|
||||
is that this will encourage more users to use and install a realtime collaborative editor. A smaller, manageable and well
|
||||
documented codebase makes it easier for developers to improve the code and contribute towards the project.
|
||||
|
||||
Etherpad Lite is optimized to be easy embeddable. It provides a [HTTP API](https://github.com/Pita/etherpad-lite/wiki/HTTP-API)
|
||||
that allows your web application to manage pads, users and groups.
|
||||
There are several clients in for this API:
|
||||
|
||||
* [PHP](https://github.com/TomNomNom/etherpad-lite-client), thx to [TomNomNom](https://github.com/TomNomNom)
|
||||
* [.Net](https://github.com/ja-jo/EtherpadLiteDotNet), thx to [ja-jo](https://github.com/ja-jo)
|
||||
* [Node.js](https://github.com/tomassedovic/etherpad-lite-client-js), thx to [tomassedovic](https://github.com/tomassedovic)
|
||||
* [Ruby](https://github.com/jhollinger/ruby-etherpad-lite), thx to [jhollinger](https://github.com/jhollinger)
|
||||
* [Python](https://github.com/devjones/PyEtherpadLite), thx to [devjones](https://github.com/devjones)
|
||||
|
||||
There is also a [jQuery plugin](https://github.com/johnyma22/etherpad-lite-jquery-plugin) that helps you to embed Pads into your website
|
||||
|
||||
**Online demo**<br>
|
||||
Visit <http://beta.etherpad.org> to test it live
|
||||
|
||||
Here is the **[FAQ](https://github.com/Pita/etherpad-lite/wiki/FAQ)**
|
||||
|
||||
# Etherpad vs Etherpad Lite
|
||||
<table>
|
||||
<tr>
|
||||
<td> </td><td><b>Etherpad</b></td><td><b>Etherpad Lite</b></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="right">Size of the folder (without git history)</td><td>30 MB</td><td>1.5 MB</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="right">Languages used server side</td><td>Javascript (Rhino), Java, Scala</td><td>Javascript (node.js)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="right">Lines of server side Javascript code</td><td>~101k</td><td>~9k</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="right">RAM Usage immediately after start</td><td>257 MB (grows to ~1GB)</td><td>16 MB (grows to ~30MB)</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
# Installation
|
||||
|
||||
## Windows
|
||||
|
||||
1. Download <http://etherpad.org/etherpad-lite-win.zip>
|
||||
2. Extract the file
|
||||
3. Open the extracted folder and double click `start.bat`
|
||||
4. Open your web browser and browse to <http://localhost:9001>. You like it? Look at the 'Next Steps' section below
|
||||
|
||||
## Linux
|
||||
|
||||
**As root:**
|
||||
|
||||
<ol>
|
||||
<li>Install the dependencies. We need gzip, git, curl, libssl develop libraries, python and gcc. <br><i>For Debian/Ubuntu</i> <code>apt-get install gzip git-core curl python libssl-dev pkg-config build-essential</code><br>
|
||||
<i>For Fedora/CentOS</i> <code>yum install gzip git-core curl python openssl-devel && yum groupinstall "Development Tools"</code>
|
||||
</li><br>
|
||||
<li>Install node.js
|
||||
<ol type="a">
|
||||
<li>Download the latest node.js release (both 0.6 and 0.8 are supported, recommended is stable 0.8.8) from <a href="http://nodejs.org/download">http://nodejs.org</a></li>
|
||||
<li>Extract it with <code>tar xf node-v0.8.8</code></li>
|
||||
<li>Move into the node folder <code>cd node-v0.8.8</code> and build node with <code>./configure && make && make install</code></li>
|
||||
</ol>
|
||||
</li>
|
||||
</ol>
|
||||
|
||||
**As any user (we recommend creating a separate user called etherpad-lite):**
|
||||
|
||||
<ol start="3">
|
||||
<li>Move to a folder where you want to install Etherpad Lite. Clone the git repository <code>git clone 'git://github.com/Pita/etherpad-lite.git'</code><br></li>
|
||||
<li>Change into the directory containing the Etherpad Lite source code clone with <code>cd etherpad-lite</code><br></li>
|
||||
<li>Start it with <code>bin/run.sh</code><br> </li>
|
||||
<li>Open your web browser and visit <a href="http://localhost:9001">http://localhost:9001</a>. You like it? Look at the 'Next Steps' section below</li>
|
||||
</ol>
|
||||
|
||||
## 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. You probably need to do a `npm cache clean jshint` before, in case that throws an error message.
|
||||
|
||||
|
||||
Look at this wiki pages:
|
||||
|
||||
* [How to deploy Etherpad Lite as a service](https://github.com/Pita/etherpad-lite/wiki/How-to-deploy-Etherpad-Lite-as-a-service)
|
||||
* [How to put Etherpad Lite behind a reverse Proxy](https://github.com/Pita/etherpad-lite/wiki/How-to-put-Etherpad-Lite-behind-a-reverse-Proxy)
|
||||
* [How to customize your Etherpad Lite installation](https://github.com/Pita/etherpad-lite/wiki/How-to-customize-your-Etherpad-Lite-installation)
|
||||
* [How to use Etherpad-Lite with jQuery](https://github.com/Pita/etherpad-lite/wiki/How-to-use-Etherpad-Lite-with-jQuery)
|
||||
* [How to use Etherpad Lite with MySQL](https://github.com/Pita/etherpad-lite/wiki/How-to-use-Etherpad-Lite-with-MySQL)
|
||||
* [Sites that run Etherpad Lite](https://github.com/Pita/etherpad-lite/wiki/Sites-that-run-Etherpad-Lite)
|
||||
* [How to migrate the database from Etherpad to Etherpad Lite](https://github.com/Pita/etherpad-lite/wiki/How-to-migrate-the-database-from-Etherpad-to-Etherpad-Lite)
|
||||
|
||||
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 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 <http://youtu.be/jo_B4LTHi3I>.
|
||||
|
||||
You can debug with `bin/debugRun.sh`
|
||||
|
||||
If you want to find out how Etherpads Easysync works (the library that makes it really realtime), start with this [PDF](https://github.com/Pita/etherpad-lite/raw/master/doc/easysync/easysync-full-description.pdf) (complex, but worth reading).
|
||||
|
||||
You know all this and just want to know how you can help? Look at the [TODO list](https://github.com/Pita/etherpad-lite/wiki/TODO).
|
||||
You can join the [mailinglist](http://groups.google.com/group/etherpad-lite-dev) or go to the freenode irc channel [#etherpad-lite-dev](http://webchat.freenode.net?channels=#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)
|
|
@ -1,6 +1,6 @@
|
|||
#!/bin/sh
|
||||
|
||||
NODE_VERSION="0.6.5"
|
||||
NODE_VERSION="0.8.4"
|
||||
|
||||
#Move to the folder where ep-lite is installed
|
||||
cd `dirname $0`
|
||||
|
@ -50,7 +50,7 @@ mv node_modules_resolved node_modules
|
|||
|
||||
echo "download windows node..."
|
||||
cd bin
|
||||
wget "http://nodejs.org/dist/v$NODE_VERSION/node.exe" -O node.exe
|
||||
wget "http://nodejs.org/dist/v$NODE_VERSION/node.exe" -O ../node.exe
|
||||
|
||||
echo "create the zip..."
|
||||
cd /tmp
|
||||
|
|
|
@ -1,22 +1,19 @@
|
|||
@echo off
|
||||
set NODE_VERSION=0.8.1
|
||||
set JQUERY_VERSION=1.7
|
||||
|
||||
:: change directory to etherpad-lite root
|
||||
cd bin
|
||||
cd ..
|
||||
cd /D "%~dp0\.."
|
||||
|
||||
:: Is node installed?
|
||||
cmd /C node -e "" || ( echo "Please install node.js ( http://nodejs.org )" && exit /B 1 )
|
||||
|
||||
echo _
|
||||
echo Updating node...
|
||||
curl -lo bin\node.exe http://nodejs.org/dist/v%NODE_VERSION%/node.exe
|
||||
echo Checking node version...
|
||||
set check_version="if(['6','8'].indexOf(process.version.split('.')[1].toString()) === -1) { console.log('You are running a wrong version of Node. Etherpad Lite requires v0.6.x or v0.8.x'); process.exit(1) }"
|
||||
cmd /C node -e %check_version% || exit /B 1
|
||||
|
||||
echo _
|
||||
echo Installing etherpad-lite and dependencies...
|
||||
cmd /C npm install src/
|
||||
|
||||
echo _
|
||||
echo Updating jquery...
|
||||
curl -lo "node_modules\ep_etherpad-lite\static\js\jquery.min.js" "http://code.jquery.com/jquery-%JQUERY_VERSION%.min.js"
|
||||
cmd /C npm install src/ || exit /B 1
|
||||
|
||||
echo _
|
||||
echo Copying custom templates...
|
||||
|
@ -27,12 +24,16 @@ FOR %%f IN (index pad timeslider) DO (
|
|||
)
|
||||
|
||||
echo _
|
||||
echo Clearing cache.
|
||||
echo Clearing cache...
|
||||
del /S var\minified*
|
||||
|
||||
echo _
|
||||
echo Setting up settings.json...
|
||||
IF NOT EXIST settings.json copy settings.json.template settings.json
|
||||
IF NOT EXIST settings.json (
|
||||
echo Can't find settings.json.
|
||||
echo Copying settings.json.template...
|
||||
cmd /C copy settings.json.template settings.json || exit /B 1
|
||||
)
|
||||
|
||||
echo _
|
||||
echo Installed Etherpad-lite!
|
||||
echo Installed Etherpad-lite! To run Etherpad type start.bat
|
|
@ -0,0 +1,3 @@
|
|||
@include documentation
|
||||
@include api/api
|
||||
@include database
|
|
@ -0,0 +1,7 @@
|
|||
@include embed_parameters
|
||||
@include http_api
|
||||
@include hooks
|
||||
@include hooks_client-side
|
||||
@include hooks_server-side
|
||||
@include editorInfo
|
||||
@include changeset_library
|
|
@ -0,0 +1,151 @@
|
|||
# Changeset Library
|
||||
|
||||
```
|
||||
"Z:z>1|2=m=b*0|1+1$\n"
|
||||
```
|
||||
|
||||
This is a Changeset. Its just a string and its very difficult to read in this form. But the Changeset Library gives us some tools to read it.
|
||||
|
||||
A changeset describes the diff between two revisions of the document. The Browser sends changesets to the server and the server sends them to the clients to update them. This Changesets gets also saved into the history of a pad. Which allows us to go back to every revision from the past.
|
||||
|
||||
## Changeset.unpack(changeset)
|
||||
|
||||
* `changeset` {String}
|
||||
|
||||
This functions returns an object representaion of the changeset, similar to this:
|
||||
|
||||
```
|
||||
{ oldLen: 35, newLen: 36, ops: '|2=m=b*0|1+1', charBank: '\n' }
|
||||
```
|
||||
|
||||
* `oldLen` {Number} the original length of the document.
|
||||
* `newLen` {Number} the length of the document after the changeset is applied.
|
||||
* `ops` {String} the actual changes, introduced by this changeset.
|
||||
* `charBank` {String} All characters that are added by this changeset.
|
||||
|
||||
## Changeset.opIterator(ops)
|
||||
|
||||
* `ops` {String} The operators, returned by `Changeset.unpack()`
|
||||
|
||||
Returns an operator iterator. This iterator allows us to iterate over all operators that are in the changeset.
|
||||
|
||||
You can iterate with an opIterator using its `next()` and `hasNext()` methods. Next returns the `next()` operator object and `hasNext()` indicates, whether there are any operators left.
|
||||
|
||||
## The Operator object
|
||||
There are 3 types of operators: `+`,`-` and `=`. These operators describe different changes to the document, beginning with the first character of the document. A `=` operator doesn't change the text, but it may add or remove text attributes. A `-` operator removes text. And a `+` Operator adds text and optionally adds some attributes to it.
|
||||
|
||||
* `opcode` {String} the operator type
|
||||
* `chars` {Number} the length of the text changed by this operator.
|
||||
* `lines` {Number} the number of lines changed by this operator.
|
||||
* `attribs` {attribs} attributes set on this text.
|
||||
|
||||
### Example
|
||||
```
|
||||
{ opcode: '+',
|
||||
chars: 1,
|
||||
lines: 1,
|
||||
attribs: '*0' }
|
||||
```
|
||||
|
||||
## APool
|
||||
|
||||
```
|
||||
> var AttributePoolFactory = require("./utils/AttributePoolFactory");
|
||||
> var apool = AttributePoolFactory.createAttributePool();
|
||||
> console.log(apool)
|
||||
{ numToAttrib: {},
|
||||
attribToNum: {},
|
||||
nextNum: 0,
|
||||
putAttrib: [Function],
|
||||
getAttrib: [Function],
|
||||
getAttribKey: [Function],
|
||||
getAttribValue: [Function],
|
||||
eachAttrib: [Function],
|
||||
toJsonable: [Function],
|
||||
fromJsonable: [Function] }
|
||||
```
|
||||
|
||||
This creates an empty apool. A apool saves which attributes were used during the history of a pad. There is one apool for each pad. It only saves the attributes that were really used, it doesn't save unused attributes. Lets fill this apool with some values
|
||||
|
||||
```
|
||||
> apool.fromJsonable({"numToAttrib":{"0":["author","a.kVnWeomPADAT2pn9"],"1":["bold","true"],"2":["italic","true"]},"nextNum":3});
|
||||
> console.log(apool)
|
||||
{ numToAttrib:
|
||||
{ '0': [ 'author', 'a.kVnWeomPADAT2pn9' ],
|
||||
'1': [ 'bold', 'true' ],
|
||||
'2': [ 'italic', 'true' ] },
|
||||
attribToNum:
|
||||
{ 'author,a.kVnWeomPADAT2pn9': 0,
|
||||
'bold,true': 1,
|
||||
'italic,true': 2 },
|
||||
nextNum: 3,
|
||||
putAttrib: [Function],
|
||||
getAttrib: [Function],
|
||||
getAttribKey: [Function],
|
||||
getAttribValue: [Function],
|
||||
eachAttrib: [Function],
|
||||
toJsonable: [Function],
|
||||
fromJsonable: [Function] }
|
||||
```
|
||||
|
||||
We used the fromJsonable function to fill the empty apool with values. the fromJsonable and toJsonable functions are used to serialize and deserialize an apool. You can see that it stores the relation between numbers and attributes. So for example the attribute 1 is the attribute bold and vise versa. A attribute is always a key value pair. For stuff like bold and italic its just 'italic':'true'. For authors its author:$AUTHORID. So a character can be bold and italic. But it can't belong to multiple authors
|
||||
|
||||
```
|
||||
> apool.getAttrib(1)
|
||||
[ 'bold', 'true' ]
|
||||
```
|
||||
|
||||
Simple example of how to get the key value pair for the attribute 1
|
||||
|
||||
## AText
|
||||
|
||||
```
|
||||
> var atext = {"text":"bold text\nitalic text\nnormal text\n\n","attribs":"*0*1+9*0|1+1*0*1*2+b|1+1*0+b|2+2"};
|
||||
> console.log(atext)
|
||||
{ text: 'bold text\nitalic text\nnormal text\n\n',
|
||||
attribs: '*0*1+9*0|1+1*0*1*2+b|1+1*0+b|2+2' }
|
||||
```
|
||||
|
||||
This is an atext. An atext has two parts: text and attribs. The text is just the text of the pad as a string. We will look closer at the attribs at the next steps
|
||||
|
||||
```
|
||||
> var opiterator = Changeset.opIterator(atext.attribs)
|
||||
> console.log(opiterator)
|
||||
{ next: [Function: next],
|
||||
hasNext: [Function: hasNext],
|
||||
lastIndex: [Function: lastIndex] }
|
||||
> opiterator.next()
|
||||
{ opcode: '+',
|
||||
chars: 9,
|
||||
lines: 0,
|
||||
attribs: '*0*1' }
|
||||
> opiterator.next()
|
||||
{ opcode: '+',
|
||||
chars: 1,
|
||||
lines: 1,
|
||||
attribs: '*0' }
|
||||
> opiterator.next()
|
||||
{ opcode: '+',
|
||||
chars: 11,
|
||||
lines: 0,
|
||||
attribs: '*0*1*2' }
|
||||
> opiterator.next()
|
||||
{ opcode: '+',
|
||||
chars: 1,
|
||||
lines: 1,
|
||||
attribs: '' }
|
||||
> opiterator.next()
|
||||
{ opcode: '+',
|
||||
chars: 11,
|
||||
lines: 0,
|
||||
attribs: '*0' }
|
||||
> opiterator.next()
|
||||
{ opcode: '+',
|
||||
chars: 2,
|
||||
lines: 2,
|
||||
attribs: '' }
|
||||
```
|
||||
|
||||
The attribs are again a bunch of operators like .ops in the changeset was. But these operators are only + operators. They describe which part of the text has which attributes
|
||||
|
||||
For more information see /doc/easysync/easysync-notes.txt in the source.
|
|
@ -0,0 +1,70 @@
|
|||
# editorInfo
|
||||
|
||||
## editorInfo.ace_replaceRange(start, end, text)
|
||||
This function replaces a range (from `start` to `end`) with `text`.
|
||||
|
||||
## editorInfo.ace_getRep()
|
||||
Returns the `rep` object.
|
||||
|
||||
## editorInfo.ace_getAuthor()
|
||||
## editorInfo.ace_inCallStack()
|
||||
## editorInfo.ace_inCallStackIfNecessary(?)
|
||||
## editorInfo.ace_focus(?)
|
||||
## editorInfo.ace_importText(?)
|
||||
## editorInfo.ace_importAText(?)
|
||||
## editorInfo.ace_exportText(?)
|
||||
## editorInfo.ace_editorChangedSize(?)
|
||||
## editorInfo.ace_setOnKeyPress(?)
|
||||
## editorInfo.ace_setOnKeyDown(?)
|
||||
## editorInfo.ace_setNotifyDirty(?)
|
||||
## editorInfo.ace_dispose(?)
|
||||
## editorInfo.ace_getFormattedCode(?)
|
||||
## editorInfo.ace_setEditable(bool)
|
||||
## editorInfo.ace_execCommand(?)
|
||||
## editorInfo.ace_callWithAce(fn, callStack, normalize)
|
||||
## editorInfo.ace_setProperty(key, value)
|
||||
## editorInfo.ace_setBaseText(txt)
|
||||
## editorInfo.ace_setBaseAttributedText(atxt, apoolJsonObj)
|
||||
## editorInfo.ace_applyChangesToBase(c, optAuthor, apoolJsonObj)
|
||||
## editorInfo.ace_prepareUserChangeset()
|
||||
## editorInfo.ace_applyPreparedChangesetToBase()
|
||||
## editorInfo.ace_setUserChangeNotificationCallback(f)
|
||||
## editorInfo.ace_setAuthorInfo(author, info)
|
||||
## editorInfo.ace_setAuthorSelectionRange(author, start, end)
|
||||
## editorInfo.ace_getUnhandledErrors()
|
||||
## editorInfo.ace_getDebugProperty(prop)
|
||||
## editorInfo.ace_fastIncorp(?)
|
||||
## editorInfo.ace_isCaret(?)
|
||||
## editorInfo.ace_getLineAndCharForPoint(?)
|
||||
## editorInfo.ace_performDocumentApplyAttributesToCharRange(?)
|
||||
## editorInfo.ace_setAttributeOnSelection(?)
|
||||
## editorInfo.ace_toggleAttributeOnSelection(?)
|
||||
## editorInfo.ace_performSelectionChange(?)
|
||||
## editorInfo.ace_doIndentOutdent(?)
|
||||
## editorInfo.ace_doUndoRedo(?)
|
||||
## editorInfo.ace_doInsertUnorderedList(?)
|
||||
## editorInfo.ace_doInsertOrderedList(?)
|
||||
## editorInfo.ace_performDocumentApplyAttributesToRange()
|
||||
|
||||
## editorInfo.ace_getAuthorInfos()
|
||||
Returns an info object about the author. Object key = author_id and info includes author's bg color value.
|
||||
Use to define your own authorship.
|
||||
## editorInfo.ace_performDocumentReplaceRange(start, end, newText)
|
||||
This function replaces a range (from [x1,y1] to [x2,y2]) with `newText`.
|
||||
## editorInfo.ace_performDocumentReplaceCharRange(startChar, endChar, newText)
|
||||
This function replaces a range (from y1 to y2) with `newText`.
|
||||
## editorInfo.ace_renumberList(lineNum)
|
||||
If you delete a line, calling this method will fix the line numbering.
|
||||
## editorInfo.ace_doReturnKey()
|
||||
Forces a return key at the current carret position.
|
||||
## editorInfo.ace_isBlockElement(element)
|
||||
Returns true if your passed elment is registered as a block element
|
||||
## editorInfo.ace_getLineListType(lineNum)
|
||||
Returns the line's html list type.
|
||||
## editorInfo.ace_caretLine()
|
||||
Returns X position of the caret.
|
||||
## editorInfo.ace_caretColumn()
|
||||
Returns Y position of the caret.
|
||||
## editorInfo.ace_caretDocChar()
|
||||
Returns the Y offset starting from [x=0,y=0]
|
||||
## editorInfo.ace_isWordChar(?)
|
|
@ -0,0 +1,47 @@
|
|||
# Embed parameters
|
||||
You can easily embed your etherpad-lite into any webpage by using iframes. You can configure the embedded pad using embed paramters.
|
||||
|
||||
Example:
|
||||
|
||||
Cut and paste the following code into any webpage to embed a pad. The parameters below will hide the chat and the line numbers.
|
||||
|
||||
```
|
||||
<iframe src='http://pad.test.de/p/PAD_NAME?showChat=false&showLineNumbers=false' width=600 height=400></iframe>
|
||||
```
|
||||
|
||||
## showLineNumbers
|
||||
* Boolean
|
||||
|
||||
Default: true
|
||||
|
||||
## showControls
|
||||
* Boolean
|
||||
|
||||
Default: true
|
||||
|
||||
## showChat
|
||||
* Boolean
|
||||
|
||||
Default: true
|
||||
|
||||
## useMonospaceFont
|
||||
* Boolean
|
||||
|
||||
Default: false
|
||||
|
||||
## userName
|
||||
* String
|
||||
|
||||
Default: "unnamed"
|
||||
|
||||
Example: `userName=Etherpad%20User`
|
||||
|
||||
## noColors
|
||||
* Boolean
|
||||
|
||||
Default: false
|
||||
|
||||
## alwaysShowChat
|
||||
* Boolean
|
||||
|
||||
Default: false
|
|
@ -0,0 +1,11 @@
|
|||
# Hooks
|
||||
All hooks are called with two arguments:
|
||||
|
||||
1. name - the name of the hook being called
|
||||
2. context - an object with some relevant information about the context of the call
|
||||
|
||||
## Return values
|
||||
A hook should always return a list or undefined. Returning undefined is equivalent to returning an empty list.
|
||||
All the returned lists are appended to each other, so if the return values where `[1, 2]`, `undefined`, `[3, 4,]`, `undefined` and `[5]`, the value returned by callHook would be `[1, 2, 3, 4, 5]`.
|
||||
|
||||
This is, because it should never matter if you have one plugin or several plugins doing some work - a single plugin should be able to make callHook return the same value a set of plugins are able to return collectively. So, any plugin can return a list of values, of any length, not just one value.
|
|
@ -0,0 +1,244 @@
|
|||
# Client-side hooks
|
||||
Most of these hooks are called during or in order to set up the formatting process.
|
||||
|
||||
## documentReady
|
||||
Called from: src/templates/pad.html
|
||||
|
||||
Things in context:
|
||||
|
||||
nothing
|
||||
|
||||
This hook proxies the functionality of jQuery's `$(document).ready` event.
|
||||
|
||||
## aceDomLineProcessLineAttributes
|
||||
Called from: src/static/js/domline.js
|
||||
|
||||
Things in context:
|
||||
|
||||
1. domline - The current DOM line being processed
|
||||
2. cls - The class of the current block element (useful for styling)
|
||||
|
||||
This hook is called for elements in the DOM that have the "lineMarkerAttribute" set. You can add elements into this category with the aceRegisterBlockElements hook above.
|
||||
|
||||
The return value of this hook should have the following structure:
|
||||
|
||||
`{ preHtml: String, postHtml: String, processedMarker: Boolean }`
|
||||
|
||||
The preHtml and postHtml values will be added to the HTML display of the element, and if processedMarker is true, the engine won't try to process it any more.
|
||||
|
||||
## aceCreateDomLine
|
||||
Called from: src/static/js/domline.js
|
||||
|
||||
Things in context:
|
||||
|
||||
1. domline - the current DOM line being processed
|
||||
2. cls - The class of the current element (useful for styling)
|
||||
|
||||
This hook is called for any line being processed by the formatting engine, unless the aceDomLineProcessLineAttributes hook from above returned true, in which case this hook is skipped.
|
||||
|
||||
The return value of this hook should have the following structure:
|
||||
|
||||
`{ extraOpenTags: String, extraCloseTags: String, cls: String }`
|
||||
|
||||
extraOpenTags and extraCloseTags will be added before and after the element in question, and cls will be the new class of the element going forward.
|
||||
|
||||
## acePostWriteDomLineHTML
|
||||
Called from: src/static/js/domline.js
|
||||
|
||||
Things in context:
|
||||
|
||||
1. node - the DOM node that just got written to the page
|
||||
|
||||
This hook is for right after a node has been fully formatted and written to the page.
|
||||
|
||||
## aceAttribsToClasses
|
||||
Called from: src/static/js/linestylefilter.js
|
||||
|
||||
Things in context:
|
||||
|
||||
1. linestylefilter - the JavaScript object that's currently processing the ace attributes
|
||||
2. key - the current attribute being processed
|
||||
3. value - the value of the attribute being processed
|
||||
|
||||
This hook is called during the attribute processing procedure, and should be used to translate key, value pairs into valid HTML classes that can be inserted into the DOM.
|
||||
|
||||
The return value for this function should be a list of classes, which will then be parsed into a valid class string.
|
||||
|
||||
## aceGetFilterStack
|
||||
Called from: src/static/js/linestylefilter.js
|
||||
|
||||
Things in context:
|
||||
|
||||
1. linestylefilter - the JavaScript object that's currently processing the ace attributes
|
||||
2. browser - an object indicating which browser is accessing the page
|
||||
|
||||
This hook is called to apply custom regular expression filters to a set of styles. The one example available is the ep_linkify plugin, which adds internal links. They use it to find the telltale `[[ ]]` syntax that signifies internal links, and finding that syntax, they add in the internalHref attribute to be later used by the aceCreateDomLine hook (documented above).
|
||||
|
||||
## aceEditorCSS
|
||||
Called from: src/static/js/ace.js
|
||||
|
||||
Things in context: None
|
||||
|
||||
This hook is provided to allow custom CSS files to be loaded. The return value should be an array of paths relative to the plugins directory.
|
||||
|
||||
## aceInitInnerdocbodyHead
|
||||
Called from: src/static/js/ace.js
|
||||
|
||||
Things in context:
|
||||
|
||||
1. iframeHTML - the HTML of the editor iframe up to this point, in array format
|
||||
|
||||
This hook is called during the creation of the editor HTML. The array should have lines of HTML added to it, giving the plugin author a chance to add in meta, script, link, and other tags that go into the `<head>` element of the editor HTML document.
|
||||
|
||||
## aceEditEvent
|
||||
Called from: src/static/js/ace2_inner.js
|
||||
|
||||
Things in context:
|
||||
|
||||
1. callstack - a bunch of information about the current action
|
||||
2. editorInfo - information about the user who is making the change
|
||||
3. rep - information about where the change is being made
|
||||
4. documentAttributeManager - information about attributes in the document (this is a mystery to me)
|
||||
|
||||
This hook is made available to edit the edit events that might occur when changes are made. Currently you can change the editor information, some of the meanings of the edit, and so on. You can also make internal changes (internal to your plugin) that use the information provided by the edit event.
|
||||
|
||||
## aceRegisterBlockElements
|
||||
Called from: src/static/js/ace2_inner.js
|
||||
|
||||
Things in context: None
|
||||
|
||||
The return value of this hook will add elements into the "lineMarkerAttribute" category, making the aceDomLineProcessLineAttributes hook (documented below) call for those elements.
|
||||
|
||||
## aceInitialized
|
||||
Called from: src/static/js/ace2_inner.js
|
||||
|
||||
Things in context:
|
||||
|
||||
1. editorInfo - information about the user who will be making changes through the interface, and a way to insert functions into the main ace object (see ep_headings)
|
||||
2. rep - information about where the user's cursor is
|
||||
3. documentAttributeManager - some kind of magic
|
||||
|
||||
This hook is for inserting further information into the ace engine, for later use in formatting hooks.
|
||||
|
||||
## postAceInit
|
||||
Called from: src/static/js/pad.js
|
||||
|
||||
Things in context:
|
||||
|
||||
1. ace - the ace object that is applied to this editor.
|
||||
|
||||
There doesn't appear to be any example available of this particular hook being used, but it gets fired after the editor is all set up.
|
||||
|
||||
## userJoinOrUpdate
|
||||
Called from: src/static/js/pad_userlist.js
|
||||
|
||||
Things in context:
|
||||
|
||||
1. info - the user information
|
||||
|
||||
This hook is called on the client side whenever a user joins or changes. This can be used to create notifications or an alternate user list.
|
||||
|
||||
## collectContentPre
|
||||
Called from: src/static/js/contentcollector.js
|
||||
|
||||
Things in context:
|
||||
|
||||
1. cc - the contentcollector object
|
||||
2. state - the current state of the change being made
|
||||
3. tname - the tag name of this node currently being processed
|
||||
4. style - the style applied to the node (probably CSS)
|
||||
5. cls - the HTML class string of the node
|
||||
|
||||
This hook is called before the content of a node is collected by the usual methods. The cc object can be used to do a bunch of things that modify the content of the pad. See, for example, the heading1 plugin for etherpad original.
|
||||
|
||||
## collectContentPost
|
||||
Called from: src/static/js/contentcollector.js
|
||||
|
||||
Things in context:
|
||||
|
||||
1. cc - the contentcollector object
|
||||
2. state - the current state of the change being made
|
||||
3. tname - the tag name of this node currently being processed
|
||||
4. style - the style applied to the node (probably CSS)
|
||||
5. cls - the HTML class string of the node
|
||||
|
||||
This hook is called after the content of a node is collected by the usual methods. The cc object can be used to do a bunch of things that modify the content of the pad. See, for example, the heading1 plugin for etherpad original.
|
||||
|
||||
## handleClientMessage_`name`
|
||||
Called from: `src/static/js/collab_client.js`
|
||||
|
||||
Things in context:
|
||||
|
||||
1. payload - the data that got sent with the message (use it for custom message content)
|
||||
|
||||
This hook gets called every time the client receives a message of type `name`. This can most notably be used with the new HTTP API call, "sendClientsMessage", which sends a custom message type to all clients connected to a pad. You can also use this to handle existing types.
|
||||
|
||||
`collab_client.js` has a pretty extensive list of message types, if you want to take a look.
|
||||
|
||||
##aceStartLineAndCharForPoint-aceEndLineAndCharForPoint
|
||||
Called from: src/static/js/ace2_inner.js
|
||||
|
||||
Things in context:
|
||||
|
||||
1. callstack - a bunch of information about the current action
|
||||
2. editorInfo - information about the user who is making the change
|
||||
3. rep - information about where the change is being made
|
||||
4. root - the span element of the current line
|
||||
5. point - the starting/ending element where the cursor highlights
|
||||
6. documentAttributeManager - information about attributes in the document
|
||||
|
||||
This hook is provided to allow a plugin to turn DOM node selection into [line,char] selection.
|
||||
The return value should be an array of [line,char]
|
||||
|
||||
##aceKeyEvent
|
||||
Called from: src/static/js/ace2_inner.js
|
||||
|
||||
Things in context:
|
||||
|
||||
1. callstack - a bunch of information about the current action
|
||||
2. editorInfo - information about the user who is making the change
|
||||
3. rep - information about where the change is being made
|
||||
4. documentAttributeManager - information about attributes in the document
|
||||
5. evt - the fired event
|
||||
|
||||
This hook is provided to allow a plugin to handle key events.
|
||||
The return value should be true if you have handled the event.
|
||||
|
||||
##collectContentLineText
|
||||
Called from: src/static/js/contentcollector.js
|
||||
|
||||
Things in context:
|
||||
|
||||
1. cc - the contentcollector object
|
||||
2. state - the current state of the change being made
|
||||
3. tname - the tag name of this node currently being processed
|
||||
4. text - the text for that line
|
||||
|
||||
This hook allows you to validate/manipulate the text before it's sent to the server side.
|
||||
The return value should be the validated/manipulated text.
|
||||
|
||||
##collectContentLineBreak
|
||||
Called from: src/static/js/contentcollector.js
|
||||
|
||||
Things in context:
|
||||
|
||||
1. cc - the contentcollector object
|
||||
2. state - the current state of the change being made
|
||||
3. tname - the tag name of this node currently being processed
|
||||
|
||||
This hook is provided to allow whether the br tag should induce a new magic domline or not.
|
||||
The return value should be either true(break the line) or false.
|
||||
|
||||
##disableAuthorColorsForThisLine
|
||||
Called from: src/static/js/linestylefilter.js
|
||||
|
||||
Things in context:
|
||||
|
||||
1. linestylefilter - the JavaScript object that's currently processing the ace attributes
|
||||
2. text - the line text
|
||||
3. class - line class
|
||||
|
||||
This hook is provided to allow whether a given line should be deliniated with multiple authors.
|
||||
Multiple authors in one line cause the creation of magic span lines. This might not suit you and
|
||||
now you can disable it and handle your own deliniation.
|
||||
The return value should be either true(disable) or false.
|
|
@ -0,0 +1,153 @@
|
|||
# Server-side hooks
|
||||
These hooks are called on server-side.
|
||||
|
||||
## loadSettings
|
||||
Called from: src/node/server.js
|
||||
|
||||
Things in context:
|
||||
|
||||
1. settings - the settings object
|
||||
|
||||
Use this hook to receive the global settings in your plugin.
|
||||
|
||||
## pluginUninstall
|
||||
Called from: src/static/js/pluginfw/installer.js
|
||||
|
||||
Things in context:
|
||||
|
||||
1. plugin_name - self-explanatory
|
||||
|
||||
If this hook returns an error, the callback to the uninstall function gets an error as well. This mostly seems useful for handling additional features added in based on the installation of other plugins, which is pretty cool!
|
||||
|
||||
## pluginInstall
|
||||
Called from: src/static/js/pluginfw/installer.js
|
||||
|
||||
Things in context:
|
||||
|
||||
1. plugin_name - self-explanatory
|
||||
|
||||
If this hook returns an error, the callback to the install function gets an error, too. This seems useful for adding in features when a particular plugin is installed.
|
||||
|
||||
## init_`<plugin name>`
|
||||
Called from: src/static/js/pluginfw/plugins.js
|
||||
|
||||
Things in context: None
|
||||
|
||||
This function is called after a specific plugin is initialized. This would probably be more useful than the previous two functions if you only wanted to add in features to one specific plugin.
|
||||
|
||||
## expressConfigure
|
||||
Called from: src/node/server.js
|
||||
|
||||
Things in context:
|
||||
|
||||
1. app - the main application object
|
||||
|
||||
This is a helpful hook for changing the behavior and configuration of the application. It's called right after the application gets configured.
|
||||
|
||||
## expressCreateServer
|
||||
Called from: src/node/server.js
|
||||
|
||||
Things in context:
|
||||
|
||||
1. app - the main express application object (helpful for adding new paths and such)
|
||||
2. server - the http server object
|
||||
|
||||
This hook gets called after the application object has been created, but before it starts listening. This is similar to the expressConfigure hook, but it's not guaranteed that the application object will have all relevant configuration variables.
|
||||
|
||||
## eejsBlock_`<name>`
|
||||
Called from: src/node/eejs/index.js
|
||||
|
||||
Things in context:
|
||||
|
||||
1. content - the content of the block
|
||||
|
||||
This hook gets called upon the rendering of an ejs template block. For any specific kind of block, you can change how that block gets rendered by modifying the content object passed in.
|
||||
|
||||
Have a look at `src/templates/pad.html` and `src/templates/timeslider.html` to see which blocks are available.
|
||||
|
||||
## socketio
|
||||
Called from: src/node/hooks/express/socketio.js
|
||||
|
||||
Things in context:
|
||||
|
||||
1. app - the application object
|
||||
2. io - the socketio object
|
||||
3. server - the http server object
|
||||
|
||||
I have no idea what this is useful for, someone else will have to add this description.
|
||||
|
||||
## authorize
|
||||
Called from: src/node/hooks/express/webaccess.js
|
||||
|
||||
Things in context:
|
||||
|
||||
1. req - the request object
|
||||
2. res - the response object
|
||||
3. next - ?
|
||||
4. resource - the path being accessed
|
||||
|
||||
This is useful for modifying the way authentication is done, especially for specific paths.
|
||||
|
||||
## authenticate
|
||||
Called from: src/node/hooks/express/webaccess.js
|
||||
|
||||
Things in context:
|
||||
|
||||
1. req - the request object
|
||||
2. res - the response object
|
||||
3. next - ?
|
||||
4. username - the username used (optional)
|
||||
5. password - the password used (optional)
|
||||
|
||||
This is useful for modifying the way authentication is done.
|
||||
|
||||
## authFailure
|
||||
Called from: src/node/hooks/express/webaccess.js
|
||||
|
||||
Things in context:
|
||||
|
||||
1. req - the request object
|
||||
2. res - the response object
|
||||
3. next - ?
|
||||
|
||||
This is useful for modifying the way authentication is done.
|
||||
|
||||
## handleMessage
|
||||
Called from: src/node/handler/PadMessageHandler.js
|
||||
|
||||
Things in context:
|
||||
|
||||
1. message - the message being handled
|
||||
2. client - the client object from socket.io
|
||||
|
||||
This hook will be called once a message arrive. If a plugin calls `callback(null)` the message will be dropped. However it is not possible to modify the message.
|
||||
|
||||
Plugins may also decide to implement custom behavior once a message arrives.
|
||||
|
||||
**WARNING**: handleMessage will be called, even if the client is not authorized to send this message. It's up to the plugin to check permissions.
|
||||
|
||||
Example:
|
||||
|
||||
```
|
||||
function handleMessage ( hook, context, callback ) {
|
||||
if ( context.message.type == 'USERINFO_UPDATE' ) {
|
||||
// If the message type is USERINFO_UPDATE, drop the message
|
||||
callback(null);
|
||||
}else{
|
||||
callback();
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
|
||||
## getLineHTMLForExport
|
||||
Called from: src/node/utils/ExportHtml.js
|
||||
|
||||
Things in context:
|
||||
|
||||
1. apool - pool object
|
||||
2. attribLine - line attributes
|
||||
3. text - line text
|
||||
|
||||
This hook will allow a plug-in developer to re-write each line when exporting to HTML.
|
||||
|
|
@ -0,0 +1,254 @@
|
|||
# HTTP API
|
||||
|
||||
## What can I do with this API?
|
||||
The API gives another web application control of the pads. The basic functions are
|
||||
|
||||
* create/delete pads
|
||||
* grant/forbid access to pads
|
||||
* get/set pad content
|
||||
|
||||
The API is designed in a way, so you can reuse your existing user system with their permissions, and map it to etherpad lite. Means: Your web application still has to do authentication, but you can tell etherpad lite via the api, which visitors should get which permissions. This allows etherpad lite to fit into any web application and extend it with real-time functionality. You can embed the pads via an iframe into your website.
|
||||
|
||||
Take a look at [HTTP API client libraries](https://github.com/Pita/etherpad-lite/wiki/HTTP-API-client-libraries) to see if a library in your favorite language.
|
||||
|
||||
## Examples
|
||||
|
||||
### Example 1
|
||||
|
||||
A portal (such as WordPress) wants to give a user access to a new pad. Let's assume the user have the internal id 7 and his name is michael.
|
||||
|
||||
Portal maps the internal userid to an etherpad author.
|
||||
|
||||
> Request: `http://pad.domain/api/1/createAuthorIfNotExistsFor?apikey=secret&name=Michael&authorMapper=7`
|
||||
>
|
||||
> Response: `{code: 0, message:"ok", data: {authorID: "a.s8oes9dhwrvt0zif"}}`
|
||||
|
||||
Portal maps the internal userid to an etherpad group:
|
||||
|
||||
> Request: `http://pad.domain/api/1/createGroupIfNotExistsFor?apikey=secret&groupMapper=7`
|
||||
>
|
||||
> Response: `{code: 0, message:"ok", data: {groupID: "g.s8oes9dhwrvt0zif"}}`
|
||||
|
||||
Portal creates a pad in the userGroup
|
||||
|
||||
> Request: `http://pad.domain/api/1/createGroupPad?apikey=secret&groupID=g.s8oes9dhwrvt0zif&padName=samplePad&text=This is the first sentence in the pad`
|
||||
>
|
||||
> Response: `{code: 0, message:"ok", data: null}`
|
||||
|
||||
Portal starts the session for the user on the group:
|
||||
|
||||
> Request: `http://pad.domain/api/1/createSession?apikey=secret&groupID=g.s8oes9dhwrvt0zif&authorID=a.s8oes9dhwrvt0zif&validUntil=1312201246`
|
||||
>
|
||||
> Response: `{"data":{"sessionID": "s.s8oes9dhwrvt0zif"}}`
|
||||
|
||||
Portal places the cookie "sessionID" with the given value on the client and creates an iframe including the pad.
|
||||
|
||||
### Example 2
|
||||
|
||||
A portal (such as WordPress) wants to transform the contents of a pad that multiple admins edited into a blog post.
|
||||
|
||||
Portal retrieves the contents of the pad for entry into the db as a blog post:
|
||||
|
||||
> Request: `http://pad.domain/api/1/getText?apikey=secret&padID=g.s8oes9dhwrvt0zif$123`
|
||||
>
|
||||
> Response: `{code: 0, message:"ok", data: {text:"Welcome Text"}}`
|
||||
|
||||
Portal submits content into new blog post
|
||||
|
||||
> Portal.AddNewBlog(content)
|
||||
>
|
||||
|
||||
## Usage
|
||||
|
||||
### Request Format
|
||||
|
||||
The API is accessible via HTTP. HTTP Requests are in the format /api/$APIVERSION/$FUNCTIONNAME. Parameters are transmitted via HTTP GET. $APIVERSION is 1
|
||||
|
||||
### Response Format
|
||||
Responses are valid JSON in the following format:
|
||||
|
||||
```js
|
||||
{
|
||||
"code": number,
|
||||
"message": string,
|
||||
"data": obj
|
||||
}
|
||||
```
|
||||
|
||||
* **code** a return code
|
||||
* **0** everything ok
|
||||
* **1** wrong parameters
|
||||
* **2** internal error
|
||||
* **3** no such function
|
||||
* **4** no or wrong API Key
|
||||
* **message** a status message. Its ok if everything is fine, else it contains an error message
|
||||
* **data** the payload
|
||||
|
||||
### Overview
|
||||
|
||||
![API Overview](http://i.imgur.com/d0nWp.png)
|
||||
|
||||
## Data Types
|
||||
|
||||
* **groupID** a string, the unique id of a group. Format is g.16RANDOMCHARS, for example g.s8oes9dhwrvt0zif
|
||||
* **sessionID** a string, the unique id of a session. Format is s.16RANDOMCHARS, for example s.s8oes9dhwrvt0zif
|
||||
* **authorID** a string, the unique id of an author. Format is a.16RANDOMCHARS, for example a.s8oes9dhwrvt0zif
|
||||
* **readOnlyID** a string, the unique id of an readonly relation to a pad. Format is r.16RANDOMCHARS, for example r.s8oes9dhwrvt0zif
|
||||
* **padID** a string, format is GROUPID$PADNAME, for example the pad test of group g.s8oes9dhwrvt0zif has padID g.s8oes9dhwrvt0zif$test
|
||||
|
||||
### Authentication
|
||||
|
||||
Authentication works via a token that is sent with each request as a post parameter. There is a single token per Etherpad-Lite deployment. This token will be random string, generated by Etherpad-Lite at the first start. It will be saved in APIKEY.txt in the root folder of Etherpad Lite. Only Etherpad Lite and the requesting application knows this key. Token management will not be exposed through this API.
|
||||
|
||||
### Node Interoperability
|
||||
|
||||
All functions will also be available through a node module accessable from other node.js applications.
|
||||
|
||||
### JSONP
|
||||
|
||||
The API provides _JSONP_ support to allow requests from a server in a different domain.
|
||||
Simply add `&jsonp=?` to the API call.
|
||||
|
||||
Example usage: http://api.jquery.com/jQuery.getJSON/
|
||||
|
||||
## API Methods
|
||||
|
||||
### Groups
|
||||
Pads can belong to a group. The padID of grouppads is starting with a groupID like g.asdfasdfasdfasdf$test
|
||||
|
||||
* **createGroup()** creates a new group <br><br>*Example returns:*
|
||||
* `{code: 0, message:"ok", data: {groupID: g.s8oes9dhwrvt0zif}}`
|
||||
|
||||
* **createGroupIfNotExistsFor(groupMapper)** this functions helps you to map your application group ids to etherpad lite group ids <br><br>*Example returns:*
|
||||
* `{code: 0, message:"ok", data: {groupID: g.s8oes9dhwrvt0zif}}`
|
||||
|
||||
* **deleteGroup(groupID)** deletes a group <br><br>*Example returns:*
|
||||
* `{code: 0, message:"ok", data: null}`
|
||||
* `{code: 1, message:"groupID does not exist", data: null}`
|
||||
|
||||
* **listPads(groupID)** returns all pads of this group<br><br>*Example returns:*
|
||||
* `{code: 0, message:"ok", data: {padIDs : ["g.s8oes9dhwrvt0zif$test", "g.s8oes9dhwrvt0zif$test2"]}`
|
||||
* `{code: 1, message:"groupID does not exist", data: null}`
|
||||
|
||||
* **createGroupPad(groupID, padName [, text])** creates a new pad in this group <br><br>*Example returns:*
|
||||
* `{code: 0, message:"ok", data: null}`
|
||||
* `{code: 1, message:"pad does already exist", data: null}`
|
||||
* `{code: 1, message:"groupID does not exist", data: null}`
|
||||
|
||||
* **listAllGroups()** lists all existing groups<br><br>*Example returns:*
|
||||
* `{code: 0, message:"ok", data: {groupIDs: ["g.mKjkmnAbSMtCt8eL", "g.3ADWx6sbGuAiUmCy"]}}`
|
||||
* `{code: 0, message:"ok", data: {groupIDs: []}}`
|
||||
|
||||
### Author
|
||||
These authors are bound to the attributes the users choose (color and name).
|
||||
|
||||
* **createAuthor([name])** creates a new author <br><br>*Example returns:*
|
||||
* `{code: 0, message:"ok", data: {authorID: "a.s8oes9dhwrvt0zif"}}`
|
||||
|
||||
* **createAuthorIfNotExistsFor(authorMapper [, name])** this functions helps you to map your application author ids to etherpad lite author ids <br><br>*Example returns:*
|
||||
* `{code: 0, message:"ok", data: {authorID: "a.s8oes9dhwrvt0zif"}}`
|
||||
|
||||
* **listPadsOfAuthor(authorID)** returns an array of all pads this author contributed to<br><br>*Example returns:*
|
||||
* `{code: 0, message:"ok", data: {padIDs: ["g.s8oes9dhwrvt0zif$test", "g.s8oejklhwrvt0zif$foo"]}}`
|
||||
* `{code: 1, message:"authorID does not exist", data: null}`
|
||||
|
||||
* **getAuthorName(authorID)** Returns the Author Name of the author <br><br>*Example returns:*
|
||||
* `{code: 0, message:"ok", data: {authorName: "John McLear"}}`
|
||||
|
||||
-> can't be deleted cause this would involve scanning all the pads where this author was
|
||||
|
||||
### Session
|
||||
Sessions can be created between a group and an author. This allows an author to access more than one group. The sessionID will be set as a cookie to the client and is valid until a certain date. The session cookie can also contain multiple comma-seperated sessionIDs, allowing a user to edit pads in different groups at the same time. Only users with a valid session for this group, can access group pads. You can create a session after you authenticated the user at your web application, to give them access to the pads. You should save the sessionID of this session and delete it after the user logged out.
|
||||
|
||||
* **createSession(groupID, authorID, validUntil)** creates a new session. validUntil is an unix timestamp in seconds <br><br>*Example returns:*
|
||||
* `{code: 0, message:"ok", data: {sessionID: "s.s8oes9dhwrvt0zif"}}`
|
||||
* `{code: 1, message:"groupID doesn't exist", data: null}`
|
||||
* `{code: 1, message:"authorID doesn't exist", data: null}`
|
||||
* `{code: 1, message:"validUntil is in the past", data: null}`
|
||||
|
||||
* **deleteSession(sessionID)** deletes a session <br><br>*Example returns:*
|
||||
* `{code: 1, message:"ok", data: null}`
|
||||
* `{code: 1, message:"sessionID does not exist", data: null}`
|
||||
|
||||
* **getSessionInfo(sessionID)** returns informations about a session <br><br>*Example returns:*
|
||||
* `{code: 0, message:"ok", data: {authorID: "a.s8oes9dhwrvt0zif", groupID: g.s8oes9dhwrvt0zif, validUntil: 1312201246}}`
|
||||
* `{code: 1, message:"sessionID does not exist", data: null}`
|
||||
|
||||
* **listSessionsOfGroup(groupID)** returns all sessions of a group <br><br>*Example returns:*
|
||||
* `{"code":0,"message":"ok","data":{"s.oxf2ras6lvhv2132":{"groupID":"g.s8oes9dhwrvt0zif","authorID":"a.akf8finncvomlqva","validUntil":2312905480}}}`
|
||||
* `{code: 1, message:"groupID does not exist", data: null}`
|
||||
|
||||
* **listSessionsOfAuthor(authorID)** returns all sessions of an author <br><br>*Example returns:*
|
||||
* `{"code":0,"message":"ok","data":{"s.oxf2ras6lvhv2132":{"groupID":"g.s8oes9dhwrvt0zif","authorID":"a.akf8finncvomlqva","validUntil":2312905480}}}`
|
||||
* `{code: 1, message:"authorID does not exist", data: null}`
|
||||
|
||||
### Pad Content
|
||||
|
||||
Pad content can be updated and retrieved through the API
|
||||
|
||||
* **getText(padID, [rev])** returns the text of a pad <br><br>*Example returns:*
|
||||
* `{code: 0, message:"ok", data: {text:"Welcome Text"}}`
|
||||
* `{code: 1, message:"padID does not exist", data: null}`
|
||||
|
||||
* **setText(padID, text)** sets the text of a pad <br><br>*Example returns:*
|
||||
* `{code: 0, message:"ok", data: null}`
|
||||
* `{code: 1, message:"padID does not exist", data: null}`
|
||||
* `{code: 1, message:"text too long", data: null}`
|
||||
|
||||
* **getHTML(padID, [rev])** returns the text of a pad formatted as HTML<br><br>*Example returns:*
|
||||
* `{code: 0, message:"ok", data: {html:"Welcome Text<br>More Text"}}`
|
||||
* `{code: 1, message:"padID does not exist", data: null}`
|
||||
|
||||
### Pad
|
||||
Group pads are normal pads, but with the name schema GROUPID$PADNAME. A security manager controls access of them and its forbidden for normal pads to include a $ in the name.
|
||||
|
||||
* **createPad(padID [, text])** creates a new (non-group) pad. Note that if you need to create a group Pad, you should call **createGroupPad**.<br><br>*Example returns:*
|
||||
* `{code: 0, message:"ok", data: null}`
|
||||
* `{code: 1, message:"pad does already exist", data: null}`
|
||||
|
||||
* **getRevisionsCount(padID)** returns the number of revisions of this pad <br><br>*Example returns:*
|
||||
* `{code: 0, message:"ok", data: {revisions: 56}}`
|
||||
* `{code: 1, message:"padID does not exist", data: null}`
|
||||
|
||||
* **padUsersCount(padID)** returns the number of user that are currently editing this pad <br><br>*Example returns:*
|
||||
* `{code: 0, message:"ok", data: {padUsersCount: 5}}`
|
||||
|
||||
* **padUsers(padID)** returns the list of users that are currently editing this pad <br><br>*Example returns:*
|
||||
* `{code: 0, message:"ok", data: {padUsers: [{colorId:"#c1a9d9","name":"username1","timestamp":1345228793126},{"colorId":"#d9a9cd","name":"Hmmm","timestamp":1345228796042}]}}`
|
||||
* `{code: 0, message:"ok", data: {padUsers: []}}`
|
||||
|
||||
* **deletePad(padID)** deletes a pad <br><br>*Example returns:*
|
||||
* `{code: 0, message:"ok", data: null}`
|
||||
* `{code: 1, message:"padID does not exist", data: null}`
|
||||
|
||||
* **getReadOnlyID(padID)** returns the read only link of a pad <br><br>*Example returns:*
|
||||
* `{code: 0, message:"ok", data: {readOnlyID: "r.s8oes9dhwrvt0zif"}}`
|
||||
* `{code: 1, message:"padID does not exist", data: null}`
|
||||
|
||||
* **setPublicStatus(padID, publicStatus)** sets a boolean for the public status of a pad <br><br>*Example returns:*
|
||||
* `{code: 0, message:"ok", data: null}`
|
||||
* `{code: 1, message:"padID does not exist", data: null}`
|
||||
|
||||
* **getPublicStatus(padID)** return true of false <br><br>*Example returns:*
|
||||
* `{code: 0, message:"ok", data: {publicStatus: true}}`
|
||||
* `{code: 1, message:"padID does not exist", data: null}`
|
||||
|
||||
* **setPassword(padID, password)** returns ok or a error message <br><br>*Example returns:*
|
||||
* `{code: 0, message:"ok", data: null}`
|
||||
* `{code: 1, message:"padID does not exist", data: null}`
|
||||
|
||||
* **isPasswordProtected(padID)** returns true or false <br><br>*Example returns:*
|
||||
* `{code: 0, message:"ok", data: {passwordProtection: true}}`
|
||||
* `{code: 1, message:"padID does not exist", data: null}`
|
||||
|
||||
* **listAuthorsOfPad(padID)** returns an array of authors who contributed to this pad <br><br>*Example returns:*
|
||||
* `{code: 0, message:"ok", data: {authorIDs : ["a.s8oes9dhwrvt0zif", "a.akf8finncvomlqva"]}`
|
||||
* `{code: 1, message:"padID does not exist", data: null}`
|
||||
|
||||
* **getLastEdited(padID)** returns the timestamp of the last revision of the pad <br><br>*Example returns:*
|
||||
* `{code: 0, message:"ok", data: {lastEdited: 1340815946602}}`
|
||||
* `{code: 1, message:"padID does not exist", data: null}`
|
||||
|
||||
* **sendClientsMessage(padID, msg)** sends a custom message of type `msg` to the pad <br><br>*Example returns:*
|
||||
* `{code: 0, message:"ok", data: {}}`
|
||||
* `{code: 1, message:"padID does not exist", data: null}`
|
|
@ -1,5 +1,10 @@
|
|||
# Database structure
|
||||
|
||||
## Keys and their values
|
||||
|
||||
### groups
|
||||
A list of all existing groups (a JSON object with groupIDs as keys and `1` as values).
|
||||
|
||||
### pad:$PADID
|
||||
Saves all informations about pads
|
||||
|
||||
|
|
|
@ -0,0 +1,15 @@
|
|||
# About this Documentation
|
||||
|
||||
<!-- type=misc -->
|
||||
|
||||
The goal of this documentation is to comprehensively explain Etherpad-Lite,
|
||||
both from a reference as well as a conceptual point of view.
|
||||
|
||||
Where appropriate, property types, method arguments, and the arguments
|
||||
provided to event handlers are detailed in a list underneath the topic
|
||||
heading.
|
||||
|
||||
Every `.html` file is generated based on the corresponding
|
||||
`.markdown` file in the `doc/api/` folder in the source tree. The
|
||||
documentation is generated using the `tools/doc/generate.js` program.
|
||||
The HTML template is located at `doc/template.html`.
|
|
@ -0,0 +1,23 @@
|
|||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>__SECTION__ Etherpad-Lite Manual & Documentation</title>
|
||||
<link rel="stylesheet" href="style.css">
|
||||
</head>
|
||||
<body class="apidoc" id="api-section-__FILENAME__">
|
||||
<header id="header">
|
||||
<h1>Etherpad-Lite Manual & Documentation</h1>
|
||||
</header>
|
||||
|
||||
<div id="toc">
|
||||
<h2>Table of Contents</h2>
|
||||
__TOC__
|
||||
</div>
|
||||
|
||||
<div id="apicontent">
|
||||
__CONTENT__
|
||||
</div>
|
||||
|
||||
</body>
|
||||
</html>
|
|
@ -13,6 +13,7 @@
|
|||
{ "name": "importexport", "hooks": { "expressCreateServer": "ep_etherpad-lite/node/hooks/express/importexport:expressCreateServer" } },
|
||||
{ "name": "errorhandling", "hooks": { "expressCreateServer": "ep_etherpad-lite/node/hooks/express/errorhandling:expressCreateServer" } },
|
||||
{ "name": "socketio", "hooks": { "expressCreateServer": "ep_etherpad-lite/node/hooks/express/socketio:expressCreateServer" } },
|
||||
{ "name": "tests", "hooks": { "expressCreateServer": "ep_etherpad-lite/node/hooks/express/tests:expressCreateServer" } },
|
||||
{ "name": "adminplugins", "hooks": {
|
||||
"expressCreateServer": "ep_etherpad-lite/node/hooks/express/adminplugins:expressCreateServer",
|
||||
"socketio": "ep_etherpad-lite/node/hooks/express/adminplugins:socketio" } }
|
||||
|
|
|
@ -35,6 +35,7 @@ var cleanText = require("./Pad").cleanText;
|
|||
/**GROUP FUNCTIONS*****/
|
||||
/**********************/
|
||||
|
||||
exports.listAllGroups = groupManager.listAllGroups;
|
||||
exports.createGroup = groupManager.createGroup;
|
||||
exports.createGroupIfNotExistsFor = groupManager.createGroupIfNotExistsFor;
|
||||
exports.deleteGroup = groupManager.deleteGroup;
|
||||
|
@ -47,7 +48,9 @@ exports.createGroupPad = groupManager.createGroupPad;
|
|||
|
||||
exports.createAuthor = authorManager.createAuthor;
|
||||
exports.createAuthorIfNotExistsFor = authorManager.createAuthorIfNotExistsFor;
|
||||
exports.getAuthorName = authorManager.getAuthorName;
|
||||
exports.listPadsOfAuthor = authorManager.listPadsOfAuthor;
|
||||
exports.padUsers = padMessageHandler.padUsers;
|
||||
exports.padUsersCount = padMessageHandler.padUsersCount;
|
||||
|
||||
/**********************/
|
||||
|
@ -512,6 +515,39 @@ exports.listAuthorsOfPad = function(padID, callback)
|
|||
});
|
||||
}
|
||||
|
||||
/**
|
||||
sendClientsMessage(padID, msg) sends a message to all clients connected to the
|
||||
pad, possibly for the purpose of signalling a plugin.
|
||||
|
||||
Note, this will only accept strings from the HTTP API, so sending bogus changes
|
||||
or chat messages will probably not be possible.
|
||||
|
||||
The resulting message will be structured like so:
|
||||
|
||||
{
|
||||
type: 'COLLABROOM',
|
||||
data: {
|
||||
type: <msg>,
|
||||
time: <time the message was sent>
|
||||
}
|
||||
}
|
||||
|
||||
Example returns:
|
||||
|
||||
{code: 0, message:"ok"}
|
||||
{code: 1, message:"padID does not exist"}
|
||||
*/
|
||||
|
||||
exports.sendClientsMessage = function (padID, msg, callback) {
|
||||
getPadSafe(padID, true, function (err, pad) {
|
||||
if (ERR(err, callback)) {
|
||||
return;
|
||||
} else {
|
||||
padMessageHandler.handleCustomMessage(padID, msg, callback);
|
||||
}
|
||||
} );
|
||||
}
|
||||
|
||||
|
||||
/******************************/
|
||||
/** INTERNAL HELPER FUNCTIONS */
|
||||
|
|
|
@ -141,6 +141,24 @@ exports.getAuthor = function (author, callback)
|
|||
db.get("globalAuthor:" + author, callback);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the Author Name of the author
|
||||
* @param {String} author The id of the author
|
||||
* @param {Function} callback callback(err, name)
|
||||
*/
|
||||
|
||||
exports.getAuthorName = function (authorID, callback)
|
||||
{
|
||||
db.getSub("globalAuthor:" + author, ["name"], callback);
|
||||
console.log(authorID);
|
||||
db.getSub("globalAuthor:" + authorID, ["name"], function(err, authorName){
|
||||
if(ERR(err, callback)) return;
|
||||
callback(null, {authorName: authorName});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Returns the color Id of the author
|
||||
* @param {String} author The id of the author
|
||||
|
|
|
@ -26,6 +26,24 @@ var db = require("./DB").db;
|
|||
var async = require("async");
|
||||
var padManager = require("./PadManager");
|
||||
var sessionManager = require("./SessionManager");
|
||||
|
||||
exports.listAllGroups = function(callback) {
|
||||
db.get("groups", function (err, groups) {
|
||||
if(ERR(err, callback)) return;
|
||||
|
||||
// there are no groups
|
||||
if(groups == null) {
|
||||
callback(null, {groupIDs: []});
|
||||
return;
|
||||
}
|
||||
|
||||
var groupIDs = [];
|
||||
for ( var groupID in groups ) {
|
||||
groupIDs.push(groupID);
|
||||
}
|
||||
callback(null, {groupIDs: groupIDs});
|
||||
});
|
||||
}
|
||||
|
||||
exports.deleteGroup = function(groupID, callback)
|
||||
{
|
||||
|
@ -105,6 +123,39 @@ exports.deleteGroup = function(groupID, callback)
|
|||
db.remove("group2sessions:" + groupID);
|
||||
db.remove("group:" + groupID);
|
||||
callback();
|
||||
},
|
||||
//unlist the group
|
||||
function(callback)
|
||||
{
|
||||
exports.listAllGroups(function(err, groups) {
|
||||
if(ERR(err, callback)) return;
|
||||
groups = groups? groups.groupIDs : [];
|
||||
|
||||
// it's not listed
|
||||
if(groups.indexOf(groupID) == -1) {
|
||||
callback();
|
||||
return;
|
||||
}
|
||||
|
||||
groups.splice(groups.indexOf(groupID), 1);
|
||||
|
||||
// store empty groupe list
|
||||
if(groups.length == 0) {
|
||||
db.set("groups", {});
|
||||
callback();
|
||||
return;
|
||||
}
|
||||
|
||||
// regenerate group list
|
||||
var newGroups = {};
|
||||
async.forEach(groups, function(group, cb) {
|
||||
newGroups[group] = 1;
|
||||
cb();
|
||||
},function() {
|
||||
db.set("groups", newGroups);
|
||||
callback();
|
||||
});
|
||||
});
|
||||
}
|
||||
], function(err)
|
||||
{
|
||||
|
@ -130,7 +181,24 @@ exports.createGroup = function(callback)
|
|||
|
||||
//create the group
|
||||
db.set("group:" + groupID, {pads: {}});
|
||||
callback(null, {groupID: groupID});
|
||||
|
||||
//list the group
|
||||
exports.listAllGroups(function(err, groups) {
|
||||
if(ERR(err, callback)) return;
|
||||
groups = groups? groups.groupIDs : [];
|
||||
|
||||
groups.push(groupID);
|
||||
|
||||
// regenerate group list
|
||||
var newGroups = {};
|
||||
async.forEach(groups, function(group, cb) {
|
||||
newGroups[group] = 1;
|
||||
cb();
|
||||
},function() {
|
||||
db.set("groups", newGroups);
|
||||
callback(null, {groupID: groupID});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
exports.createGroupIfNotExistsFor = function(groupMapper, callback)
|
||||
|
|
|
@ -36,15 +36,15 @@ var randomString = require('ep_etherpad-lite/static/js/pad_utils').randomString;
|
|||
* @param password the password the user has given to access this pad, can be null
|
||||
* @param callback will be called with (err, {accessStatus: grant|deny|wrongPassword|needPassword, authorID: a.xxxxxx})
|
||||
*/
|
||||
exports.checkAccess = function (padID, sessionID, token, password, callback)
|
||||
exports.checkAccess = function (padID, sessionCookie, token, password, callback)
|
||||
{
|
||||
var statusObject;
|
||||
|
||||
// a valid session is required (api-only mode)
|
||||
if(settings.requireSession)
|
||||
{
|
||||
// no sessionID, access is denied
|
||||
if(!sessionID)
|
||||
// without sessionCookie, access is denied
|
||||
if(!sessionCookie)
|
||||
{
|
||||
callback(null, {accessStatus: "deny"});
|
||||
return;
|
||||
|
@ -114,32 +114,37 @@ exports.checkAccess = function (padID, sessionID, token, password, callback)
|
|||
callback();
|
||||
});
|
||||
},
|
||||
//get informations about this session
|
||||
//get information about all sessions contained in this cookie
|
||||
function(callback)
|
||||
{
|
||||
sessionManager.getSessionInfo(sessionID, function(err, sessionInfo)
|
||||
{
|
||||
//skip session validation if the session doesn't exists
|
||||
if(err && err.message == "sessionID does not exist")
|
||||
{
|
||||
callback();
|
||||
return;
|
||||
}
|
||||
|
||||
if(ERR(err, callback)) return;
|
||||
|
||||
var now = Math.floor(new Date().getTime()/1000);
|
||||
|
||||
//is it for this group? and is validUntil still ok? --> validSession
|
||||
if(sessionInfo.groupID == groupID && sessionInfo.validUntil > now)
|
||||
{
|
||||
validSession = true;
|
||||
}
|
||||
|
||||
sessionAuthor = sessionInfo.authorID;
|
||||
|
||||
if (!sessionCookie) {
|
||||
callback();
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
var sessionIDs = sessionCookie.split(',');
|
||||
async.forEach(sessionIDs, function(sessionID, callback) {
|
||||
sessionManager.getSessionInfo(sessionID, function(err, sessionInfo) {
|
||||
//skip session if it doesn't exist
|
||||
if(err && err.message == "sessionID does not exist") return;
|
||||
|
||||
if(ERR(err, callback)) return;
|
||||
|
||||
var now = Math.floor(new Date().getTime()/1000);
|
||||
|
||||
//is it for this group?
|
||||
if(sessionInfo.groupID != groupID) return;
|
||||
|
||||
//is validUntil still ok?
|
||||
if(sessionInfo.validUntil <= now) return;
|
||||
|
||||
// There is a valid session
|
||||
validSession = true;
|
||||
sessionAuthor = sessionInfo.authorID;
|
||||
|
||||
callback();
|
||||
});
|
||||
}, callback);
|
||||
},
|
||||
//get author for token
|
||||
function(callback)
|
||||
|
|
|
@ -139,7 +139,7 @@ exports.createSession = function(groupID, authorID, validUntil, callback)
|
|||
if(ERR(err, callback)) return;
|
||||
|
||||
//the entry doesn't exist so far, let's create it
|
||||
if(group2sessions == null)
|
||||
if(group2sessions == null || group2sessions.sessionIDs == null)
|
||||
{
|
||||
group2sessions = {sessionIDs : {}};
|
||||
}
|
||||
|
@ -162,7 +162,7 @@ exports.createSession = function(groupID, authorID, validUntil, callback)
|
|||
if(ERR(err, callback)) return;
|
||||
|
||||
//the entry doesn't exist so far, let's create it
|
||||
if(author2sessions == null)
|
||||
if(author2sessions == null || author2sessions.sessionIDs == null)
|
||||
{
|
||||
author2sessions = {sessionIDs : {}};
|
||||
}
|
||||
|
|
|
@ -38,35 +38,71 @@ catch(e)
|
|||
}
|
||||
|
||||
//a list of all functions
|
||||
var functions = {
|
||||
"createGroup" : [],
|
||||
"createGroupIfNotExistsFor" : ["groupMapper"],
|
||||
"deleteGroup" : ["groupID"],
|
||||
"listPads" : ["groupID"],
|
||||
"createPad" : ["padID", "text"],
|
||||
"createGroupPad" : ["groupID", "padName", "text"],
|
||||
"createAuthor" : ["name"],
|
||||
"createAuthorIfNotExistsFor": ["authorMapper" , "name"],
|
||||
"listPadsOfAuthor" : ["authorID"],
|
||||
"createSession" : ["groupID", "authorID", "validUntil"],
|
||||
"deleteSession" : ["sessionID"],
|
||||
"getSessionInfo" : ["sessionID"],
|
||||
"listSessionsOfGroup" : ["groupID"],
|
||||
"listSessionsOfAuthor" : ["authorID"],
|
||||
"getText" : ["padID", "rev"],
|
||||
"setText" : ["padID", "text"],
|
||||
"getHTML" : ["padID", "rev"],
|
||||
"setHTML" : ["padID", "html"],
|
||||
"getRevisionsCount" : ["padID"],
|
||||
"getLastEdited" : ["padID"],
|
||||
"deletePad" : ["padID"],
|
||||
"getReadOnlyID" : ["padID"],
|
||||
"setPublicStatus" : ["padID", "publicStatus"],
|
||||
"getPublicStatus" : ["padID"],
|
||||
"setPassword" : ["padID", "password"],
|
||||
"isPasswordProtected" : ["padID"],
|
||||
"listAuthorsOfPad" : ["padID"],
|
||||
"padUsersCount" : ["padID"]
|
||||
var version =
|
||||
{ "1":
|
||||
{ "createGroup" : []
|
||||
, "createGroupIfNotExistsFor" : ["groupMapper"]
|
||||
, "deleteGroup" : ["groupID"]
|
||||
, "listPads" : ["groupID"]
|
||||
, "createPad" : ["padID", "text"]
|
||||
, "createGroupPad" : ["groupID", "padName", "text"]
|
||||
, "createAuthor" : ["name"]
|
||||
, "createAuthorIfNotExistsFor": ["authorMapper" , "name"]
|
||||
, "listPadsOfAuthor" : ["authorID"]
|
||||
, "createSession" : ["groupID", "authorID", "validUntil"]
|
||||
, "deleteSession" : ["sessionID"]
|
||||
, "getSessionInfo" : ["sessionID"]
|
||||
, "listSessionsOfGroup" : ["groupID"]
|
||||
, "listSessionsOfAuthor" : ["authorID"]
|
||||
, "getText" : ["padID", "rev"]
|
||||
, "setText" : ["padID", "text"]
|
||||
, "getHTML" : ["padID", "rev"]
|
||||
, "setHTML" : ["padID", "html"]
|
||||
, "getRevisionsCount" : ["padID"]
|
||||
, "getLastEdited" : ["padID"]
|
||||
, "deletePad" : ["padID"]
|
||||
, "getReadOnlyID" : ["padID"]
|
||||
, "setPublicStatus" : ["padID", "publicStatus"]
|
||||
, "getPublicStatus" : ["padID"]
|
||||
, "setPassword" : ["padID", "password"]
|
||||
, "isPasswordProtected" : ["padID"]
|
||||
, "listAuthorsOfPad" : ["padID"]
|
||||
, "padUsersCount" : ["padID"]
|
||||
}
|
||||
, "1.1":
|
||||
{ "createGroup" : []
|
||||
, "createGroupIfNotExistsFor" : ["groupMapper"]
|
||||
, "deleteGroup" : ["groupID"]
|
||||
, "listPads" : ["groupID"]
|
||||
, "createPad" : ["padID", "text"]
|
||||
, "createGroupPad" : ["groupID", "padName", "text"]
|
||||
, "createAuthor" : ["name"]
|
||||
, "createAuthorIfNotExistsFor": ["authorMapper" , "name"]
|
||||
, "listPadsOfAuthor" : ["authorID"]
|
||||
, "createSession" : ["groupID", "authorID", "validUntil"]
|
||||
, "deleteSession" : ["sessionID"]
|
||||
, "getSessionInfo" : ["sessionID"]
|
||||
, "listSessionsOfGroup" : ["groupID"]
|
||||
, "listSessionsOfAuthor" : ["authorID"]
|
||||
, "getText" : ["padID", "rev"]
|
||||
, "setText" : ["padID", "text"]
|
||||
, "getHTML" : ["padID", "rev"]
|
||||
, "setHTML" : ["padID", "html"]
|
||||
, "getRevisionsCount" : ["padID"]
|
||||
, "getLastEdited" : ["padID"]
|
||||
, "deletePad" : ["padID"]
|
||||
, "getReadOnlyID" : ["padID"]
|
||||
, "setPublicStatus" : ["padID", "publicStatus"]
|
||||
, "getPublicStatus" : ["padID"]
|
||||
, "setPassword" : ["padID", "password"]
|
||||
, "isPasswordProtected" : ["padID"]
|
||||
, "listAuthorsOfPad" : ["padID"]
|
||||
, "padUsersCount" : ["padID"]
|
||||
, "getAuthorName" : ["authorID"]
|
||||
, "padUsers" : ["padID"]
|
||||
, "sendClientsMessage" : ["padID", "msg"]
|
||||
, "listAllGroups" : []
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -76,18 +112,30 @@ var functions = {
|
|||
* @req express request object
|
||||
* @res express response object
|
||||
*/
|
||||
exports.handle = function(functionName, fields, req, res)
|
||||
exports.handle = function(apiVersion, functionName, fields, req, res)
|
||||
{
|
||||
//check the api key!
|
||||
if(fields["apikey"] != apikey.trim())
|
||||
//check if this is a valid apiversion
|
||||
var isKnownApiVersion = false;
|
||||
for(var knownApiVersion in version)
|
||||
{
|
||||
res.send({code: 4, message: "no or wrong API Key", data: null});
|
||||
if(knownApiVersion == apiVersion)
|
||||
{
|
||||
isKnownApiVersion = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
//say goodbye if this is an unkown API version
|
||||
if(!isKnownApiVersion)
|
||||
{
|
||||
res.statusCode = 404;
|
||||
res.send({code: 3, message: "no such api version", data: null});
|
||||
return;
|
||||
}
|
||||
|
||||
//check if this is a valid function name
|
||||
var isKnownFunctionname = false;
|
||||
for(var knownFunctionname in functions)
|
||||
for(var knownFunctionname in version[apiVersion])
|
||||
{
|
||||
if(knownFunctionname == functionName)
|
||||
{
|
||||
|
@ -102,6 +150,13 @@ exports.handle = function(functionName, fields, req, res)
|
|||
res.send({code: 3, message: "no such function", data: null});
|
||||
return;
|
||||
}
|
||||
|
||||
//check the api key!
|
||||
if(fields["apikey"] != apikey.trim())
|
||||
{
|
||||
res.send({code: 4, message: "no or wrong API Key", data: null});
|
||||
return;
|
||||
}
|
||||
|
||||
//sanitize any pad id's before continuing
|
||||
if(fields["padID"])
|
||||
|
@ -109,7 +164,7 @@ exports.handle = function(functionName, fields, req, res)
|
|||
padManager.sanitizePadId(fields["padID"], function(padId)
|
||||
{
|
||||
fields["padID"] = padId;
|
||||
callAPI(functionName, fields, req, res);
|
||||
callAPI(apiVersion, functionName, fields, req, res);
|
||||
});
|
||||
}
|
||||
else if(fields["padName"])
|
||||
|
@ -117,23 +172,23 @@ exports.handle = function(functionName, fields, req, res)
|
|||
padManager.sanitizePadId(fields["padName"], function(padId)
|
||||
{
|
||||
fields["padName"] = padId;
|
||||
callAPI(functionName, fields, req, res);
|
||||
callAPI(apiVersion, functionName, fields, req, res);
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
callAPI(functionName, fields, req, res);
|
||||
callAPI(apiVersion, functionName, fields, req, res);
|
||||
}
|
||||
}
|
||||
|
||||
//calls the api function
|
||||
function callAPI(functionName, fields, req, res)
|
||||
function callAPI(apiVersion, functionName, fields, req, res)
|
||||
{
|
||||
//put the function parameters in an array
|
||||
var functionParams = [];
|
||||
for(var i=0;i<functions[functionName].length;i++)
|
||||
for(var i=0;i<version[apiVersion][functionName].length;i++)
|
||||
{
|
||||
functionParams.push(fields[functions[functionName][i]]);
|
||||
functionParams.push(fields[ version[apiVersion][functionName][i] ]);
|
||||
}
|
||||
|
||||
//add a callback function to handle the response
|
||||
|
|
|
@ -159,11 +159,7 @@ exports.handleDisconnect = function(client)
|
|||
*/
|
||||
exports.handleMessage = function(client, message)
|
||||
{
|
||||
_.map(hooks.callAll( "handleMessage", { client: client, message: message }), function ( newmessage ) {
|
||||
if ( newmessage || newmessage === null ) {
|
||||
message = newmessage;
|
||||
}
|
||||
});
|
||||
|
||||
if(message == null)
|
||||
{
|
||||
messageLogger.warn("Message is null!");
|
||||
|
@ -175,6 +171,23 @@ exports.handleMessage = function(client, message)
|
|||
return;
|
||||
}
|
||||
|
||||
var handleMessageHook = function(callback){
|
||||
var dropMessage = false;
|
||||
|
||||
// Call handleMessage hook. If a plugin returns null, the message will be dropped. Note that for all messages
|
||||
// handleMessage will be called, even if the client is not authorized
|
||||
hooks.aCallAll("handleMessage", { client: client, message: message }, function ( messages ) {
|
||||
_.each(messages, function(newMessage){
|
||||
if ( newmessage === null ) {
|
||||
dropMessage = true;
|
||||
}
|
||||
});
|
||||
|
||||
// If no plugins explicitly told us to drop the message, its ok to proceed
|
||||
if(!dropMessage){ callback() };
|
||||
});
|
||||
}
|
||||
|
||||
var finalHandler = function () {
|
||||
//Check what type of message we get and delegate to the other methodes
|
||||
if(message.type == "CLIENT_READY") {
|
||||
|
@ -203,11 +216,18 @@ exports.handleMessage = function(client, message)
|
|||
}
|
||||
};
|
||||
|
||||
if (message && message.padId) {
|
||||
if (message) {
|
||||
async.series([
|
||||
handleMessageHook,
|
||||
//check permissions
|
||||
function(callback)
|
||||
{
|
||||
|
||||
if(!message.padId){
|
||||
// If the message has a padId we assume the client is already known to the server and needs no re-authorization
|
||||
callback();
|
||||
return;
|
||||
}
|
||||
// Note: message.sessionID is an entirely different kind of
|
||||
// session from the sessions we use here! Beware! FIXME: Call
|
||||
// our "sessions" "connections".
|
||||
|
@ -231,8 +251,6 @@ exports.handleMessage = function(client, message)
|
|||
},
|
||||
finalHandler
|
||||
]);
|
||||
} else {
|
||||
finalHandler();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -254,6 +272,28 @@ function handleSaveRevisionMessage(client, message){
|
|||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles a custom message (sent via HTTP API request)
|
||||
*
|
||||
* @param padID {Pad} the pad to which we're sending this message
|
||||
* @param msg {String} the message we're sending
|
||||
*/
|
||||
exports.handleCustomMessage = function (padID, msg, cb) {
|
||||
var time = new Date().getTime();
|
||||
var msg = {
|
||||
type: 'COLLABROOM',
|
||||
data: {
|
||||
type: msg,
|
||||
time: time
|
||||
}
|
||||
};
|
||||
for (var i in pad2sessions[padID]) {
|
||||
socketio.sockets.sockets[pad2sessions[padID][i]].json.send(msg);
|
||||
}
|
||||
|
||||
cb(null, {});
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles a Chat Message
|
||||
* @param client the client that send this message
|
||||
|
@ -396,15 +436,22 @@ function handleUserInfoUpdate(client, message)
|
|||
}
|
||||
|
||||
/**
|
||||
* Handles a USERINFO_UPDATE, that means that a user have changed his color or name. Anyway, we get both informations
|
||||
* This Method is nearly 90% copied out of the Etherpad Source Code. So I can't tell you what happens here exactly
|
||||
* Look at https://github.com/ether/pad/blob/master/etherpad/src/etherpad/collab/collab_server.js in the function applyUserChanges()
|
||||
* Handles a USER_CHANGES message, where the client submits its local
|
||||
* edits as a changeset.
|
||||
*
|
||||
* This handler's job is to update the incoming changeset so that it applies
|
||||
* to the latest revision, then add it to the pad, broadcast the changes
|
||||
* to all other clients, and send a confirmation to the submitting client.
|
||||
*
|
||||
* This function is based on a similar one in the original Etherpad.
|
||||
* See https://github.com/ether/pad/blob/master/etherpad/src/etherpad/collab/collab_server.js in the function applyUserChanges()
|
||||
*
|
||||
* @param client the client that send this message
|
||||
* @param message the message from the client
|
||||
*/
|
||||
function handleUserChanges(client, message)
|
||||
{
|
||||
//check if all ok
|
||||
// Make sure all required fields are present
|
||||
if(message.data.baseRev == null)
|
||||
{
|
||||
messageLogger.warn("Dropped message, USER_CHANGES Message has no baseRev!");
|
||||
|
@ -425,6 +472,9 @@ function handleUserChanges(client, message)
|
|||
var baseRev = message.data.baseRev;
|
||||
var wireApool = (new AttributePool()).fromJsonable(message.data.apool);
|
||||
var changeset = message.data.changeset;
|
||||
// The client might disconnect between our callbacks. We should still
|
||||
// finish processing the changeset, so keep a reference to the session.
|
||||
var thisSession = sessioninfos[client.id];
|
||||
|
||||
var r, apool, pad;
|
||||
|
||||
|
@ -432,7 +482,7 @@ function handleUserChanges(client, message)
|
|||
//get the pad
|
||||
function(callback)
|
||||
{
|
||||
padManager.getPad(sessioninfos[client.id].padId, function(err, value)
|
||||
padManager.getPad(thisSession.padId, function(err, value)
|
||||
{
|
||||
if(ERR(err, callback)) return;
|
||||
pad = value;
|
||||
|
@ -444,22 +494,23 @@ function handleUserChanges(client, message)
|
|||
{
|
||||
//ex. _checkChangesetAndPool
|
||||
|
||||
//Copied from Etherpad, don't know what it does exactly
|
||||
try
|
||||
{
|
||||
//this looks like a changeset check, it throws errors sometimes
|
||||
// Verify that the changeset has valid syntax and is in canonical form
|
||||
Changeset.checkRep(changeset);
|
||||
|
||||
|
||||
// Verify that the attribute indexes used in the changeset are all
|
||||
// defined in the accompanying attribute pool.
|
||||
Changeset.eachAttribNumber(changeset, function(n) {
|
||||
if (! wireApool.getAttrib(n)) {
|
||||
throw "Attribute pool is missing attribute "+n+" for changeset "+changeset;
|
||||
}
|
||||
});
|
||||
}
|
||||
//there is an error in this changeset, so just refuse it
|
||||
catch(e)
|
||||
{
|
||||
console.warn("Can't apply USER_CHANGES "+changeset+", cause it faild checkRep");
|
||||
// There is an error in this changeset, so just refuse it
|
||||
console.warn("Can't apply USER_CHANGES "+changeset+", because it failed checkRep");
|
||||
client.json.send({disconnect:"badChangeset"});
|
||||
return;
|
||||
}
|
||||
|
@ -472,7 +523,10 @@ function handleUserChanges(client, message)
|
|||
//ex. applyUserChanges
|
||||
apool = pad.pool;
|
||||
r = baseRev;
|
||||
|
||||
|
||||
// The client's changeset might not be based on the latest revision,
|
||||
// since other clients are sending changes at the same time.
|
||||
// Update the changeset so that it can be applied to the latest revision.
|
||||
//https://github.com/caolan/async#whilst
|
||||
async.whilst(
|
||||
function() { return r < pad.getHeadRevisionNumber(); },
|
||||
|
@ -483,9 +537,18 @@ function handleUserChanges(client, message)
|
|||
pad.getRevisionChangeset(r, function(err, c)
|
||||
{
|
||||
if(ERR(err, callback)) return;
|
||||
|
||||
|
||||
// At this point, both "c" (from the pad) and "changeset" (from the
|
||||
// client) are relative to revision r - 1. The follow function
|
||||
// rebases "changeset" so that it is relative to revision r
|
||||
// and can be applied after "c".
|
||||
changeset = Changeset.follow(c, changeset, false, apool);
|
||||
callback(null);
|
||||
|
||||
if ((r - baseRev) % 200 == 0) { // don't let the stack get too deep
|
||||
async.nextTick(callback);
|
||||
} else {
|
||||
callback(null);
|
||||
}
|
||||
});
|
||||
},
|
||||
//use the callback of the series function
|
||||
|
@ -505,15 +568,14 @@ function handleUserChanges(client, message)
|
|||
return;
|
||||
}
|
||||
|
||||
var thisAuthor = sessioninfos[client.id].author;
|
||||
|
||||
pad.appendRevision(changeset, thisAuthor);
|
||||
pad.appendRevision(changeset, thisSession.author);
|
||||
|
||||
var correctionChangeset = _correctMarkersInPad(pad.atext, pad.pool);
|
||||
if (correctionChangeset) {
|
||||
pad.appendRevision(correctionChangeset);
|
||||
}
|
||||
|
||||
|
||||
// Make sure the pad always ends with an empty line.
|
||||
if (pad.text().lastIndexOf("\n\n") != pad.text().length-2) {
|
||||
var nlChangeset = Changeset.makeSplice(pad.text(), pad.text().length-1, 0, "\n");
|
||||
pad.appendRevision(nlChangeset);
|
||||
|
@ -820,6 +882,13 @@ function handleClientReady(client, message)
|
|||
},
|
||||
function(callback)
|
||||
{
|
||||
//Check that the client is still here. It might have disconnected between callbacks.
|
||||
if(sessioninfos[client.id] === undefined)
|
||||
{
|
||||
callback();
|
||||
return;
|
||||
}
|
||||
|
||||
//Check if this author is already on the pad, if yes, kick the other sessions!
|
||||
if(pad2sessions[padIds.padId])
|
||||
{
|
||||
|
@ -834,10 +903,9 @@ function handleClientReady(client, message)
|
|||
}
|
||||
|
||||
//Save in sessioninfos that this session belonges to this pad
|
||||
var sessionId=String(client.id);
|
||||
sessioninfos[sessionId].padId = padIds.padId;
|
||||
sessioninfos[sessionId].readOnlyPadId = padIds.readOnlyPadId;
|
||||
sessioninfos[sessionId].readonly = padIds.readonly;
|
||||
sessioninfos[client.id].padId = padIds.padId;
|
||||
sessioninfos[client.id].readOnlyPadId = padIds.readOnlyPadId;
|
||||
sessioninfos[client.id].readonly = padIds.readonly;
|
||||
|
||||
//check if there is already a pad2sessions entry, if not, create one
|
||||
if(!pad2sessions[padIds.padId])
|
||||
|
@ -846,7 +914,7 @@ function handleClientReady(client, message)
|
|||
}
|
||||
|
||||
//Saves in pad2sessions that this session belongs to this pad
|
||||
pad2sessions[padIds.padId].push(sessionId);
|
||||
pad2sessions[padIds.padId].push(client.id);
|
||||
|
||||
//prepare all values for the wire
|
||||
var atext = Changeset.cloneAText(pad.atext);
|
||||
|
@ -911,26 +979,22 @@ function handleClientReady(client, message)
|
|||
clientVars.userName = authorName;
|
||||
}
|
||||
|
||||
if(sessioninfos[client.id] !== undefined)
|
||||
//If this is a reconnect, we don't have to send the client the ClientVars again
|
||||
if(message.reconnect == true)
|
||||
{
|
||||
//This is a reconnect, so we don't have to send the client the ClientVars again
|
||||
if(message.reconnect == true)
|
||||
{
|
||||
//Save the revision in sessioninfos, we take the revision from the info the client send to us
|
||||
sessioninfos[client.id].rev = message.client_rev;
|
||||
}
|
||||
//This is a normal first connect
|
||||
else
|
||||
{
|
||||
//Send the clientVars to the Client
|
||||
client.json.send({type: "CLIENT_VARS", data: clientVars});
|
||||
//Save the revision in sessioninfos
|
||||
sessioninfos[client.id].rev = pad.getHeadRevisionNumber();
|
||||
}
|
||||
|
||||
//Save the revision and the author id in sessioninfos
|
||||
sessioninfos[client.id].author = author;
|
||||
//Save the revision in sessioninfos, we take the revision from the info the client send to us
|
||||
sessioninfos[client.id].rev = message.client_rev;
|
||||
}
|
||||
//This is a normal first connect
|
||||
else
|
||||
{
|
||||
//Send the clientVars to the Client
|
||||
client.json.send({type: "CLIENT_VARS", data: clientVars});
|
||||
//Save the current revision in sessioninfos, should be the same as in clientVars
|
||||
sessioninfos[client.id].rev = pad.getHeadRevisionNumber();
|
||||
}
|
||||
|
||||
sessioninfos[client.id].author = author;
|
||||
|
||||
//prepare the notification for the other users on the pad, that this user joined
|
||||
var messageToTheOtherUsers = {
|
||||
|
@ -1370,3 +1434,26 @@ exports.padUsersCount = function (padID, callback) {
|
|||
callback(null, {padUsersCount: pad2sessions[padID].length});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the list of users in a pad
|
||||
*/
|
||||
exports.padUsers = function (padID, callback) {
|
||||
if (!pad2sessions[padID] || typeof pad2sessions[padID] != typeof []) {
|
||||
callback(null, {padUsers: []});
|
||||
} else {
|
||||
var authors = [];
|
||||
for ( var ix in sessioninfos ) {
|
||||
if ( sessioninfos[ix].padId !== padID ) {
|
||||
continue;
|
||||
}
|
||||
var aid = sessioninfos[ix].author;
|
||||
authorManager.getAuthor( aid, function ( err, author ) {
|
||||
authors.push( author );
|
||||
if ( authors.length === pad2sessions[padID].length ) {
|
||||
callback(null, {padUsers: authors});
|
||||
}
|
||||
} );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
var hooks = require("ep_etherpad-lite/static/js/pluginfw/hooks");
|
||||
var http = require('http');
|
||||
var express = require('express');
|
||||
var settings = require('../utils/Settings');
|
||||
var fs = require('fs');
|
||||
|
@ -42,22 +43,24 @@ exports.createServer = function () {
|
|||
}
|
||||
|
||||
exports.restartServer = function () {
|
||||
|
||||
if (server) {
|
||||
console.log("Restarting express server");
|
||||
server.close();
|
||||
}
|
||||
|
||||
server = express.createServer();
|
||||
var app = express(); // New syntax for express v3
|
||||
server = http.createServer(app);
|
||||
|
||||
server.use(function (req, res, next) {
|
||||
app.use(function (req, res, next) {
|
||||
res.header("Server", serverName);
|
||||
next();
|
||||
});
|
||||
|
||||
server.configure(function() {
|
||||
hooks.callAll("expressConfigure", {"app": server});
|
||||
app.configure(function() {
|
||||
hooks.callAll("expressConfigure", {"app": app});
|
||||
});
|
||||
hooks.callAll("expressCreateServer", {"app": server});
|
||||
hooks.callAll("expressCreateServer", {"app": app, "server": server});
|
||||
|
||||
server.listen(settings.port, settings.ip);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -12,14 +12,10 @@ exports.expressCreateServer = function (hook_name, args, cb) {
|
|||
errors: [],
|
||||
};
|
||||
|
||||
res.send(eejs.require(
|
||||
"ep_etherpad-lite/templates/admin/plugins.html",
|
||||
render_args), {});
|
||||
res.send( eejs.require("ep_etherpad-lite/templates/admin/plugins.html", render_args) );
|
||||
});
|
||||
args.app.get('/admin/plugins/info', function(req, res) {
|
||||
res.send(eejs.require(
|
||||
"ep_etherpad-lite/templates/admin/plugins-info.html",
|
||||
{}), {});
|
||||
res.send( eejs.require("ep_etherpad-lite/templates/admin/plugins-info.html", {}) );
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -7,7 +7,7 @@ var apiHandler = require('../../handler/APIHandler');
|
|||
var apiCaller = function(req, res, fields) {
|
||||
res.header("Content-Type", "application/json; charset=utf-8");
|
||||
|
||||
apiLogger.info("REQUEST, " + req.params.func + ", " + JSON.stringify(fields));
|
||||
apiLogger.info("REQUEST, v"+ req.params.version + ":" + req.params.func + ", " + JSON.stringify(fields));
|
||||
|
||||
//wrap the send function so we can log the response
|
||||
//note: res._send seems to be already in use, so better use a "unique" name
|
||||
|
@ -24,19 +24,19 @@ var apiCaller = function(req, res, fields) {
|
|||
}
|
||||
|
||||
//call the api handler
|
||||
apiHandler.handle(req.params.func, fields, req, res);
|
||||
apiHandler.handle(req.params.version, req.params.func, fields, req, res);
|
||||
}
|
||||
|
||||
exports.apiCaller = apiCaller;
|
||||
|
||||
exports.expressCreateServer = function (hook_name, args, cb) {
|
||||
//This is a api GET call, collect all post informations and pass it to the apiHandler
|
||||
args.app.get('/api/1/:func', function (req, res) {
|
||||
args.app.get('/api/:version/:func', function (req, res) {
|
||||
apiCaller(req, res, req.query)
|
||||
});
|
||||
|
||||
//This is a api POST call, collect all post informations and pass it to the apiHandler
|
||||
args.app.post('/api/1/:func', function(req, res) {
|
||||
args.app.post('/api/:version/:func', function(req, res) {
|
||||
new formidable.IncomingForm().parse(req, function (err, fields, files) {
|
||||
apiCaller(req, res, fields)
|
||||
});
|
||||
|
|
|
@ -16,9 +16,6 @@ exports.gracefulShutdown = function(err) {
|
|||
|
||||
console.log("graceful shutdown...");
|
||||
|
||||
//stop the http server
|
||||
exports.app.close();
|
||||
|
||||
//do the db shutdown
|
||||
db.db.doShutdown(function() {
|
||||
console.log("db sucessfully closed.");
|
||||
|
@ -35,11 +32,14 @@ exports.gracefulShutdown = function(err) {
|
|||
exports.expressCreateServer = function (hook_name, args, cb) {
|
||||
exports.app = args.app;
|
||||
|
||||
args.app.error(function(err, req, res, next){
|
||||
res.send(500);
|
||||
console.error(err.stack ? err.stack : err.toString());
|
||||
exports.gracefulShutdown();
|
||||
});
|
||||
// Handle errors
|
||||
args.app.use(function(err, req, res, next){
|
||||
// if an error occurs Connect will pass it down
|
||||
// through these "error-handling" middleware
|
||||
// allowing you to respond however you like
|
||||
res.send(500, { error: 'Sorry, something bad happened!' });
|
||||
console.error(err.stack? err.stack : err.toString());
|
||||
})
|
||||
|
||||
//connect graceful shutdown with sigint and uncaughtexception
|
||||
if(os.type().indexOf("Windows") == -1) {
|
||||
|
|
|
@ -56,7 +56,7 @@ exports.expressCreateServer = function (hook_name, args, cb) {
|
|||
ERR(err);
|
||||
|
||||
if(err == "notfound")
|
||||
res.send('404 - Not Found', 404);
|
||||
res.send(404, '404 - Not Found');
|
||||
else
|
||||
res.send(html);
|
||||
});
|
||||
|
|
|
@ -7,7 +7,7 @@ exports.expressCreateServer = function (hook_name, args, cb) {
|
|||
//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);
|
||||
res.send(404, 'Such a padname is forbidden');
|
||||
}
|
||||
else
|
||||
{
|
||||
|
@ -19,7 +19,7 @@ exports.expressCreateServer = function (hook_name, args, cb) {
|
|||
var query = url.parse(req.url).query;
|
||||
if ( query ) real_url += '?' + query;
|
||||
res.header('Location', real_url);
|
||||
res.send('You should be redirected to <a href="' + real_url + '">' + real_url + '</a>', 302);
|
||||
res.send(302, 'You should be redirected to <a href="' + real_url + '">' + real_url + '</a>');
|
||||
}
|
||||
//the pad id was fine, so just render it
|
||||
else
|
||||
|
|
|
@ -3,6 +3,7 @@ var socketio = require('socket.io');
|
|||
var settings = require('../../utils/Settings');
|
||||
var socketIORouter = require("../../handler/SocketIORouter");
|
||||
var hooks = require("ep_etherpad-lite/static/js/pluginfw/hooks");
|
||||
var webaccess = require("ep_etherpad-lite/node/hooks/express/webaccess");
|
||||
|
||||
var padMessageHandler = require("../../handler/PadMessageHandler");
|
||||
|
||||
|
@ -10,19 +11,28 @@ var connect = require('connect');
|
|||
|
||||
exports.expressCreateServer = function (hook_name, args, cb) {
|
||||
//init socket.io and redirect all requests to the MessageHandler
|
||||
var io = socketio.listen(args.app);
|
||||
var io = socketio.listen(args.server);
|
||||
|
||||
/* Require an express session cookie to be present, and load the
|
||||
* session. See http://www.danielbaulig.de/socket-ioexpress for more
|
||||
* info */
|
||||
io.set('authorization', function (data, accept) {
|
||||
if (!data.headers.cookie) return accept('No session cookie transmitted.', false);
|
||||
data.cookie = connect.utils.parseCookie(data.headers.cookie);
|
||||
data.sessionID = data.cookie.express_sid;
|
||||
args.app.sessionStore.get(data.sessionID, function (err, session) {
|
||||
if (err || !session) return accept('Bad session / session has expired', false);
|
||||
data.session = new connect.middleware.session.Session(data, session);
|
||||
accept(null, true);
|
||||
|
||||
// Use connect's cookie parser, because it knows how to parse signed cookies
|
||||
connect.cookieParser(webaccess.secret)(data, {}, function(err){
|
||||
if(err) {
|
||||
console.error(err);
|
||||
accept("Couldn't parse request cookies. ", false);
|
||||
return;
|
||||
}
|
||||
|
||||
data.sessionID = data.signedCookies.express_sid;
|
||||
args.app.sessionStore.get(data.sessionID, function (err, session) {
|
||||
if (err || !session) return accept('Bad session / session has expired', false);
|
||||
data.session = new connect.middleware.session.Session(data, session);
|
||||
accept(null, true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -62,5 +72,5 @@ exports.expressCreateServer = function (hook_name, args, cb) {
|
|||
socketIORouter.setSocketIO(io);
|
||||
socketIORouter.addComponent("pad", padMessageHandler);
|
||||
|
||||
hooks.callAll("socketio", {"app": args.app, "io": io});
|
||||
hooks.callAll("socketio", {"app": args.app, "io": io, "server": args.server});
|
||||
}
|
||||
|
|
|
@ -9,6 +9,7 @@ var ERR = require("async-stacktrace");
|
|||
var _ = require("underscore");
|
||||
|
||||
exports.expressCreateServer = function (hook_name, args, cb) {
|
||||
|
||||
// Cache both minified and static.
|
||||
var assetCache = new CachingMiddleware;
|
||||
args.app.all('/(javascripts|static)/*', assetCache.handle);
|
||||
|
@ -24,6 +25,7 @@ exports.expressCreateServer = function (hook_name, args, cb) {
|
|||
, rootURI: 'http://localhost:' + settings.port + '/static/js/'
|
||||
, libraryPath: 'javascripts/lib/'
|
||||
, libraryURI: 'http://localhost:' + settings.port + '/static/plugins/'
|
||||
, requestURIs: minify.requestURIs // Loop-back is causing problems, this is a workaround.
|
||||
});
|
||||
|
||||
var StaticAssociator = Yajsml.associators.StaticAssociator;
|
||||
|
|
|
@ -0,0 +1,16 @@
|
|||
var path = require("path");
|
||||
|
||||
exports.expressCreateServer = function (hook_name, args, cb) {
|
||||
args.app.get('/tests/frontend/*', function (req, res) {
|
||||
var subPath = req.url.substr("/tests/frontend".length);
|
||||
if (subPath == ""){
|
||||
subPath = "index.html"
|
||||
}
|
||||
subPath = subPath.split("?")[0];
|
||||
|
||||
var filePath = path.normalize(__dirname + "/../../../../tests/frontend/")
|
||||
filePath += subPath.replace("..", "");
|
||||
|
||||
res.sendfile(filePath);
|
||||
});
|
||||
}
|
|
@ -56,10 +56,10 @@ exports.basicAuth = function (req, res, next) {
|
|||
res.header('WWW-Authenticate', 'Basic realm="Protected Area"');
|
||||
if (req.headers.authorization) {
|
||||
setTimeout(function () {
|
||||
res.send('Authentication required', 401);
|
||||
res.send(401, 'Authentication required');
|
||||
}, 1000);
|
||||
} else {
|
||||
res.send('Authentication required', 401);
|
||||
res.send(401, 'Authentication required');
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
@ -88,14 +88,13 @@ exports.basicAuth = function (req, res, next) {
|
|||
});
|
||||
}
|
||||
|
||||
var secret = null;
|
||||
exports.secret = null;
|
||||
|
||||
exports.expressConfigure = function (hook_name, args, cb) {
|
||||
// If the log level specified in the config file is WARN or ERROR the application server never starts listening to requests as reported in issue #158.
|
||||
// Not installing the log4js connect logger when the log level has a higher severity than INFO since it would not log at that level anyway.
|
||||
if (!(settings.loglevel === "WARN" || settings.loglevel == "ERROR"))
|
||||
args.app.use(log4js.connectLogger(httpLogger, { level: log4js.levels.INFO, format: ':status, :method :url'}));
|
||||
args.app.use(express.cookieParser());
|
||||
|
||||
/* Do not let express create the session, so that we can retain a
|
||||
* reference to it for socket.io to use. Also, set the key (cookie
|
||||
|
@ -104,13 +103,14 @@ exports.expressConfigure = function (hook_name, args, cb) {
|
|||
|
||||
if (!exports.sessionStore) {
|
||||
exports.sessionStore = new express.session.MemoryStore();
|
||||
secret = randomString(32);
|
||||
exports.secret = randomString(32);
|
||||
}
|
||||
|
||||
args.app.use(express.cookieParser(exports.secret));
|
||||
|
||||
args.app.sessionStore = exports.sessionStore;
|
||||
args.app.use(express.session({store: args.app.sessionStore,
|
||||
key: 'express_sid',
|
||||
secret: secret}));
|
||||
key: 'express_sid' }));
|
||||
|
||||
args.app.use(exports.basicAuth);
|
||||
}
|
||||
|
|
|
@ -15,7 +15,7 @@ module.exports = function (req, res, callback) {
|
|||
callback();
|
||||
//no access
|
||||
} else {
|
||||
res.send("403 - Can't touch this", 403);
|
||||
res.send(403, "403 - Can't touch this");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
@ -21,17 +21,22 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
// set up logger
|
||||
var log4js = require('log4js');
|
||||
log4js.replaceConsole();
|
||||
|
||||
var settings = require('./utils/Settings');
|
||||
|
||||
//set loglevel
|
||||
log4js.setGlobalLogLevel(settings.loglevel);
|
||||
|
||||
var db = require('./db/DB');
|
||||
var async = require('async');
|
||||
var plugins = require("ep_etherpad-lite/static/js/pluginfw/plugins");
|
||||
var hooks = require("ep_etherpad-lite/static/js/pluginfw/hooks");
|
||||
var npm = require("npm/lib/npm.js");
|
||||
|
||||
|
||||
//set loglevel
|
||||
log4js.setGlobalLogLevel(settings.loglevel);
|
||||
hooks.plugins = plugins;
|
||||
|
||||
async.waterfall([
|
||||
//initalize the database
|
||||
|
@ -46,6 +51,10 @@ async.waterfall([
|
|||
console.info("Installed plugins: " + plugins.formatPlugins());
|
||||
console.debug("Installed parts:\n" + plugins.formatParts());
|
||||
console.debug("Installed hooks:\n" + plugins.formatHooks());
|
||||
|
||||
// Call loadSettings hook
|
||||
hooks.aCallAll("loadSettings", { settings: settings });
|
||||
|
||||
callback();
|
||||
},
|
||||
|
||||
|
|
|
@ -252,7 +252,12 @@ function getDokuWikiFromAtext(pad, atext)
|
|||
|
||||
if (line.listLevel && lineContent)
|
||||
{
|
||||
pieces.push(new Array(line.listLevel + 1).join(' ') + '* ');
|
||||
if (line.listTypeName == "number")
|
||||
{
|
||||
pieces.push(new Array(line.listLevel + 1).join(' ') + ' - ');
|
||||
} else {
|
||||
pieces.push(new Array(line.listLevel + 1).join(' ') + '* ');
|
||||
}
|
||||
}
|
||||
pieces.push(lineContent);
|
||||
}
|
||||
|
|
|
@ -20,7 +20,7 @@ var Changeset = require("ep_etherpad-lite/static/js/Changeset");
|
|||
var padManager = require("../db/PadManager");
|
||||
var ERR = require("async-stacktrace");
|
||||
var Security = require('ep_etherpad-lite/static/js/security');
|
||||
|
||||
var hooks = require('ep_etherpad-lite/static/js/pluginfw/hooks');
|
||||
function getPadPlainText(pad, revNum)
|
||||
{
|
||||
var atext = ((revNum !== undefined) ? pad.getInternalRevisionAText(revNum) : pad.atext());
|
||||
|
@ -401,8 +401,22 @@ function getHTMLFromAtext(pad, atext)
|
|||
pieces.push('</li></ul>');
|
||||
}
|
||||
lists.length--;
|
||||
}
|
||||
pieces.push(lineContent, '<br>');
|
||||
}
|
||||
var lineContentFromHook = hooks.callAllStr("getLineHTMLForExport",
|
||||
{
|
||||
line: line,
|
||||
apool: apool,
|
||||
attribLine: attribLines[i],
|
||||
text: textLines[i]
|
||||
}, " ", " ", "");
|
||||
if (lineContentFromHook)
|
||||
{
|
||||
pieces.push(lineContentFromHook, '');
|
||||
}
|
||||
else
|
||||
{
|
||||
pieces.push(lineContent, '<br>');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -469,6 +483,7 @@ exports.getPadHTMLDocument = function (padId, revNum, noDocType, callback)
|
|||
var head =
|
||||
(noDocType ? '' : '<!doctype html>\n') +
|
||||
'<html lang="en">\n' + (noDocType ? '' : '<head>\n' +
|
||||
'<title>' + Security.escapeHTML(padId) + '</title>\n' +
|
||||
'<meta charset="utf-8">\n' +
|
||||
'<style> * { font-family: arial, sans-serif;\n' +
|
||||
'font-size: 13px;\n' +
|
||||
|
|
|
@ -29,29 +29,115 @@ var pro = require("uglify-js").uglify;
|
|||
var path = require('path');
|
||||
var plugins = require("ep_etherpad-lite/static/js/pluginfw/plugins");
|
||||
var RequireKernel = require('require-kernel');
|
||||
var urlutil = require('url');
|
||||
|
||||
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 LIBRARY_WHITELIST = [
|
||||
'async'
|
||||
, 'security'
|
||||
, 'tinycon'
|
||||
, 'underscore'
|
||||
, 'unorm'
|
||||
];
|
||||
|
||||
// Rewrite tar to include modules with no extensions and proper rooted paths.
|
||||
var LIBRARY_PREFIX = 'ep_etherpad-lite/static/js';
|
||||
exports.tar = {};
|
||||
function prefixLocalLibraryPath(path) {
|
||||
if (path.charAt(0) == '$') {
|
||||
return path.slice(1);
|
||||
} else {
|
||||
return LIBRARY_PREFIX + '/' + path;
|
||||
}
|
||||
}
|
||||
|
||||
for (var key in tar) {
|
||||
exports.tar[LIBRARY_PREFIX + '/' + key] =
|
||||
tar[key].map(function (p) {return LIBRARY_PREFIX + '/' + p}).concat(
|
||||
tar[key].map(function (p) {
|
||||
return LIBRARY_PREFIX + '/' + p.replace(/\.js$/, '')
|
||||
exports.tar[prefixLocalLibraryPath(key)] =
|
||||
tar[key].map(prefixLocalLibraryPath).concat(
|
||||
tar[key].map(prefixLocalLibraryPath).map(function (p) {
|
||||
return p.replace(/\.js$/, '');
|
||||
})
|
||||
).concat(
|
||||
tar[key].map(prefixLocalLibraryPath).map(function (p) {
|
||||
return p.replace(/\.js$/, '') + '/index.js';
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
// What follows is a terrible hack to avoid loop-back within the server.
|
||||
// TODO: Serve files from another service, or directly from the file system.
|
||||
function requestURI(url, method, headers, callback, redirectCount) {
|
||||
var parsedURL = urlutil.parse(url);
|
||||
|
||||
var status = 500, headers = {}, content = [];
|
||||
|
||||
var mockRequest = {
|
||||
url: url
|
||||
, method: method
|
||||
, params: {filename: parsedURL.path.replace(/^\/static\//, '')}
|
||||
, headers: headers
|
||||
};
|
||||
var mockResponse = {
|
||||
writeHead: function (_status, _headers) {
|
||||
status = _status;
|
||||
for (var header in _headers) {
|
||||
if (Object.prototype.hasOwnProperty.call(_headers, header)) {
|
||||
headers[header] = _headers[header];
|
||||
}
|
||||
}
|
||||
}
|
||||
, setHeader: function (header, value) {
|
||||
headers[header.toLowerCase()] = value.toString();
|
||||
}
|
||||
, header: function (header, value) {
|
||||
headers[header.toLowerCase()] = value.toString();
|
||||
}
|
||||
, write: function (_content) {
|
||||
_content && content.push(_content);
|
||||
}
|
||||
, end: function (_content) {
|
||||
_content && content.push(_content);
|
||||
callback(status, headers, content.join(''));
|
||||
}
|
||||
};
|
||||
|
||||
minify(mockRequest, mockResponse);
|
||||
}
|
||||
function requestURIs(locations, method, headers, callback) {
|
||||
var pendingRequests = locations.length;
|
||||
var responses = [];
|
||||
|
||||
function respondFor(i) {
|
||||
return function (status, headers, content) {
|
||||
responses[i] = [status, headers, content];
|
||||
if (--pendingRequests == 0) {
|
||||
completed();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
for (var i = 0, ii = locations.length; i < ii; i++) {
|
||||
requestURI(locations[i], method, headers, respondFor(i));
|
||||
}
|
||||
|
||||
function completed() {
|
||||
var statuss = responses.map(function (x) {return x[0]});
|
||||
var headerss = responses.map(function (x) {return x[1]});
|
||||
var contentss = responses.map(function (x) {return x[2]});
|
||||
callback(statuss, headerss, contentss);
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* creates the minifed javascript for the given minified name
|
||||
* @param req the Express request
|
||||
* @param res the Express response
|
||||
*/
|
||||
exports.minify = function(req, res, next)
|
||||
function minify(req, res, next)
|
||||
{
|
||||
var filename = req.params['filename'];
|
||||
|
||||
|
@ -66,19 +152,26 @@ exports.minify = function(req, res, next)
|
|||
return;
|
||||
}
|
||||
|
||||
/* Handle static files for plugins:
|
||||
/* Handle static files for plugins/libraries:
|
||||
paths like "plugins/ep_myplugin/static/js/test.js"
|
||||
are rewritten into ROOT_PATH_OF_MYPLUGIN/static/js/test.js,
|
||||
commonly ETHERPAD_ROOT/node_modules/ep_myplugin/static/js/test.js
|
||||
*/
|
||||
var match = filename.match(/^plugins\/([^\/]+)\/static\/(.*)/);
|
||||
var match = filename.match(/^plugins\/([^\/]+)(\/(?:(static\/.*)|.*))?$/);
|
||||
if (match) {
|
||||
var pluginName = match[1];
|
||||
var resourcePath = match[2];
|
||||
var plugin = plugins.plugins[pluginName];
|
||||
if (plugin) {
|
||||
var library = match[1];
|
||||
var libraryPath = match[2] || '';
|
||||
|
||||
if (plugins.plugins[library] && match[3]) {
|
||||
var plugin = plugins.plugins[library];
|
||||
var pluginPath = plugin.package.realPath;
|
||||
filename = path.relative(ROOT_DIR, pluginPath + '/static/' + resourcePath);
|
||||
filename = path.relative(ROOT_DIR, pluginPath + libraryPath);
|
||||
filename = filename.replace(/\\/g, '/'); // Windows (safe generally?)
|
||||
} else if (LIBRARY_WHITELIST.indexOf(library) != -1) {
|
||||
// Go straight into node_modules
|
||||
// Avoid `require.resolve()`, since 'mustache' and 'mustache/index.js'
|
||||
// would end up resolving to logically distinct resources.
|
||||
filename = '../node_modules/' + library + libraryPath;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -174,10 +267,11 @@ function getAceFile(callback) {
|
|||
var resourceURI = baseURI + path.normalize(path.join('/static/', filename));
|
||||
resourceURI = resourceURI.replace(/\\/g, '/'); // Windows (safe generally?)
|
||||
|
||||
request(resourceURI, function (error, response, body) {
|
||||
if (!error && response.statusCode == 200) {
|
||||
requestURI(resourceURI, 'GET', {}, function (status, headers, body) {
|
||||
var error = !(status == 200 || status == 404);
|
||||
if (!error) {
|
||||
data += 'Ace2Editor.EMBEDED[' + JSON.stringify(filename) + '] = '
|
||||
+ JSON.stringify(body || '') + ';\n';
|
||||
+ JSON.stringify(status == 200 ? body || '' : null) + ';\n';
|
||||
} else {
|
||||
// Silence?
|
||||
}
|
||||
|
@ -190,8 +284,14 @@ function getAceFile(callback) {
|
|||
}
|
||||
|
||||
// Check for the existance of the file and get the last modification date.
|
||||
function statFile(filename, callback) {
|
||||
if (filename == 'js/ace.js') {
|
||||
function statFile(filename, callback, dirStatLimit) {
|
||||
if (typeof dirStatLimit === 'undefined') {
|
||||
dirStatLimit = 3;
|
||||
}
|
||||
|
||||
if (dirStatLimit < 1 || filename == '' || filename == '/') {
|
||||
callback(null, null, false);
|
||||
} else if (filename == 'js/ace.js') {
|
||||
// Sometimes static assets are inlined into this file, so we have to stat
|
||||
// everything.
|
||||
lastModifiedDateOfEverything(function (error, date) {
|
||||
|
@ -204,22 +304,16 @@ function statFile(filename, callback) {
|
|||
if (error) {
|
||||
if (error.code == "ENOENT") {
|
||||
// Stat the directory instead.
|
||||
fs.stat(path.dirname(ROOT_DIR + filename), function (error, stats) {
|
||||
if (error) {
|
||||
if (error.code == "ENOENT") {
|
||||
callback(null, null, false);
|
||||
} else {
|
||||
callback(error);
|
||||
}
|
||||
} else {
|
||||
callback(null, stats.mtime.getTime(), false);
|
||||
}
|
||||
});
|
||||
statFile(path.dirname(filename), function (error, date, exists) {
|
||||
callback(error, date, false);
|
||||
}, dirStatLimit-1);
|
||||
} else {
|
||||
callback(error);
|
||||
}
|
||||
} else {
|
||||
} else if (stats.isFile()) {
|
||||
callback(null, stats.mtime.getTime(), true);
|
||||
} else {
|
||||
callback(null, stats.mtime.getTime(), false);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
@ -319,3 +413,8 @@ function compressCSS(values)
|
|||
var complete = values.join("\n");
|
||||
return cleanCSS.process(complete);
|
||||
}
|
||||
|
||||
exports.minify = minify;
|
||||
|
||||
exports.requestURI = requestURI;
|
||||
exports.requestURIs = requestURIs;
|
||||
|
|
|
@ -24,6 +24,7 @@ var os = require("os");
|
|||
var path = require('path');
|
||||
var argv = require('./Cli').argv;
|
||||
var npm = require("npm/lib/npm.js");
|
||||
var vm = require('vm');
|
||||
|
||||
/* Root path of the installation */
|
||||
exports.root = path.normalize(path.join(npm.dir, ".."));
|
||||
|
@ -45,6 +46,7 @@ exports.dbType = "dirty";
|
|||
* This setting is passed with dbType to ueberDB to set up the database
|
||||
*/
|
||||
exports.dbSettings = { "filename" : path.join(exports.root, "dirty.db") };
|
||||
|
||||
/**
|
||||
* The default Text of a new pad
|
||||
*/
|
||||
|
@ -102,32 +104,24 @@ exports.abiwordAvailable = function()
|
|||
|
||||
// Discover where the settings file lives
|
||||
var settingsFilename = argv.settings || "settings.json";
|
||||
if (settingsFilename.charAt(0) != '/') {
|
||||
settingsFilename = path.normalize(path.join(root, settingsFilename));
|
||||
}
|
||||
settingsFilename = path.resolve(path.join(root, settingsFilename));
|
||||
|
||||
var settingsStr
|
||||
var settingsStr;
|
||||
try{
|
||||
//read the settings sync
|
||||
settingsStr = fs.readFileSync(settingsFilename).toString();
|
||||
} catch(e){
|
||||
console.warn('No settings file found. Using defaults.');
|
||||
settingsStr = '{}';
|
||||
console.warn('No settings file found. Continuing using defaults!');
|
||||
}
|
||||
|
||||
//remove all comments
|
||||
settingsStr = settingsStr.replace(/\*([^*]|[\r\n]|(\*+([^*/]|[\r\n])))*\*+/gm,"").replace(/#.*/g,"").replace(/\/\/.*/g,"");
|
||||
|
||||
//try to parse the settings
|
||||
// try to parse the settings
|
||||
var settings;
|
||||
try
|
||||
{
|
||||
settings = JSON.parse(settingsStr);
|
||||
}
|
||||
catch(e)
|
||||
{
|
||||
console.error("There is a syntax error in your settings.json file");
|
||||
console.error(e.message);
|
||||
try {
|
||||
if(settingsStr) {
|
||||
settings = vm.runInContext('exports = '+settingsStr, vm.createContext(), "settings.json");
|
||||
}
|
||||
}catch(e){
|
||||
console.error('There was an error processing your settings.json file: '+e.message);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
|
@ -148,8 +142,7 @@ for(var i in settings)
|
|||
//this setting is unkown, output a warning and throw it away
|
||||
else
|
||||
{
|
||||
console.warn("Unkown Setting: '" + i + "'");
|
||||
console.warn("This setting doesn't exist or it was removed");
|
||||
console.warn("Unknown Setting: '" + i + "'. This setting doesn't exist or it was removed");
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -14,6 +14,7 @@
|
|||
, "pad_savedrevs.js"
|
||||
, "pad_connectionstatus.js"
|
||||
, "chat.js"
|
||||
, "$tinycon/tinycon.js"
|
||||
, "excanvas.js"
|
||||
, "farbtastic.js"
|
||||
]
|
||||
|
@ -48,6 +49,7 @@
|
|||
, "cssmanager.js"
|
||||
, "colorutils.js"
|
||||
, "undomodule.js"
|
||||
, "$unorm.js"
|
||||
, "contentcollector.js"
|
||||
, "changesettracker.js"
|
||||
, "linestylefilter.js"
|
||||
|
@ -58,12 +60,16 @@
|
|||
"ace2_common.js"
|
||||
, "jquery.js"
|
||||
, "rjquery.js"
|
||||
, "$async.js"
|
||||
, "$async/lib/async.js"
|
||||
, "underscore.js"
|
||||
, "$underscore.js"
|
||||
, "$underscore/underscore.js"
|
||||
, "security.js"
|
||||
, "$security.js"
|
||||
, "json2.js"
|
||||
, "pluginfw/plugins.js"
|
||||
, "pluginfw/client_plugins.js"
|
||||
, "pluginfw/shared.js"
|
||||
, "pluginfw/hooks.js"
|
||||
, "pluginfw/async.js"
|
||||
, "pluginfw/parent_require.js"
|
||||
]
|
||||
}
|
||||
|
|
|
@ -10,19 +10,19 @@
|
|||
"name": "Robin Buse" }
|
||||
],
|
||||
"dependencies" : {
|
||||
"yajsml" : "1.1.3",
|
||||
"yajsml" : "1.1.6",
|
||||
"request" : "2.9.100",
|
||||
"require-kernel" : "1.0.5",
|
||||
"resolve" : "0.2.x",
|
||||
"socket.io" : "0.9.x",
|
||||
"ueberDB" : "0.1.7",
|
||||
"async" : "0.1.x",
|
||||
"express" : "2.5.x",
|
||||
"connect" : "1.x",
|
||||
"async" : "0.1.22",
|
||||
"express" : "3.x",
|
||||
"connect" : "2.4.x",
|
||||
"clean-css" : "0.3.2",
|
||||
"uglify-js" : "1.2.5",
|
||||
"formidable" : "1.0.9",
|
||||
"log4js" : "0.4.1",
|
||||
"log4js" : "0.5.x",
|
||||
"jsdom-nocontextifiy" : "0.2.10",
|
||||
"async-stacktrace" : "0.0.2",
|
||||
"npm" : "1.1.24",
|
||||
|
@ -30,7 +30,10 @@
|
|||
"graceful-fs" : "1.1.5",
|
||||
"slide" : "1.1.3",
|
||||
"semver" : "1.0.13",
|
||||
"underscore" : "1.3.1"
|
||||
"security" : "1.0.0",
|
||||
"tinycon" : "0.0.1",
|
||||
"underscore" : "1.3.1",
|
||||
"unorm" : "1.0.0"
|
||||
},
|
||||
"bin": { "etherpad-lite": "./node/server.js" },
|
||||
"devDependencies": {
|
||||
|
@ -39,5 +42,5 @@
|
|||
"engines" : { "node" : ">=0.6.0",
|
||||
"npm" : ">=1.0"
|
||||
},
|
||||
"version" : "1.0.0"
|
||||
"version" : "1.1.2"
|
||||
}
|
||||
|
|
|
@ -401,180 +401,36 @@ table#otheruserstable {
|
|||
color: #888;
|
||||
font-style: italic;
|
||||
}
|
||||
.modaldialog.cboxreconnecting .modaldialog-inner,
|
||||
.modaldialog.cboxconnecting .modaldialog-inner {
|
||||
background: url(../../static/img/connectingbar.gif) no-repeat center 60px;
|
||||
height: 100px;
|
||||
|
||||
#connectivity {
|
||||
z-index: 600 !important;
|
||||
}
|
||||
.modaldialog.cboxreconnecting,
|
||||
.modaldialog.cboxconnecting,
|
||||
.modaldialog.cboxdisconnected {
|
||||
background: #8FCDE0
|
||||
}
|
||||
.cboxdisconnected #connectionboxinner div {
|
||||
display: none
|
||||
}
|
||||
.cboxdisconnected_userdup #connectionboxinner #disconnected_userdup {
|
||||
display: block
|
||||
}
|
||||
.cboxdisconnected_deleted #connectionboxinner #disconnected_deleted {
|
||||
display: block
|
||||
}
|
||||
.cboxdisconnected_initsocketfail #connectionboxinner #disconnected_initsocketfail {
|
||||
display: block
|
||||
}
|
||||
.cboxdisconnected_looping #connectionboxinner #disconnected_looping {
|
||||
display: block
|
||||
}
|
||||
.cboxdisconnected_slowcommit #connectionboxinner #disconnected_slowcommit {
|
||||
display: block
|
||||
}
|
||||
.cboxdisconnected_unauth #connectionboxinner #disconnected_unauth {
|
||||
display: block
|
||||
}
|
||||
.cboxdisconnected_unknown #connectionboxinner #disconnected_unknown {
|
||||
display: block
|
||||
}
|
||||
.cboxdisconnected_initsocketfail #connectionboxinner #reconnect_advise,
|
||||
.cboxdisconnected_looping #connectionboxinner #reconnect_advise,
|
||||
.cboxdisconnected_slowcommit #connectionboxinner #reconnect_advise,
|
||||
.cboxdisconnected_unknown #connectionboxinner #reconnect_advise {
|
||||
display: block
|
||||
}
|
||||
.cboxdisconnected div#reconnect_form {
|
||||
display: block
|
||||
}
|
||||
.cboxdisconnected .disconnected h2 {
|
||||
display: none
|
||||
}
|
||||
.cboxdisconnected .disconnected .h2_disconnect {
|
||||
display: block
|
||||
}
|
||||
.cboxdisconnected_userdup .disconnected h2.h2_disconnect {
|
||||
display: none
|
||||
}
|
||||
.cboxdisconnected_userdup .disconnected h2.h2_userdup {
|
||||
display: block
|
||||
}
|
||||
.cboxdisconnected_unauth .disconnected h2.h2_disconnect {
|
||||
display: none
|
||||
}
|
||||
.cboxdisconnected_unauth .disconnected h2.h2_unauth {
|
||||
display: block
|
||||
}
|
||||
#connectionstatus {
|
||||
position: absolute;
|
||||
width: 37px;
|
||||
height: 41px;
|
||||
overflow: hidden;
|
||||
right: 0;
|
||||
z-index: 11;
|
||||
}
|
||||
#connectionboxinner .connecting {
|
||||
margin-top: 20px;
|
||||
font-size: 2.0em;
|
||||
color: #555;
|
||||
text-align: center;
|
||||
|
||||
#connectivity * {
|
||||
display: none;
|
||||
}
|
||||
.cboxconnecting #connectionboxinner .connecting {
|
||||
display: block
|
||||
}
|
||||
#connectionboxinner .disconnected h2 {
|
||||
font-size: 1.8em;
|
||||
color: #333;
|
||||
text-align: left;
|
||||
margin-top: 10px;
|
||||
margin-left: 10px;
|
||||
margin-right: 10px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
#connectionboxinner .disconnected p {
|
||||
margin: 10px 10px;
|
||||
font-size: 1.2em;
|
||||
line-height: 1.1;
|
||||
color: #333;
|
||||
}
|
||||
#connectionboxinner .disconnected {
|
||||
display: none
|
||||
}
|
||||
.cboxdisconnected #connectionboxinner .disconnected {
|
||||
display: block
|
||||
}
|
||||
#connectionboxinner .reconnecting {
|
||||
margin-top: 20px;
|
||||
font-size: 1.6em;
|
||||
color: #555;
|
||||
text-align: center;
|
||||
display: none;
|
||||
}
|
||||
.cboxreconnecting #connectionboxinner .reconnecting {
|
||||
display: block
|
||||
|
||||
#connectivity .visible,
|
||||
#connectivity .visible * {
|
||||
display: block;
|
||||
}
|
||||
|
||||
#reconnect_form button {
|
||||
font-size: 12pt;
|
||||
padding: 5px;
|
||||
}
|
||||
#mainmodals {
|
||||
z-index: 600; /* higher than the modals themselves: */
|
||||
}
|
||||
.modalfield {
|
||||
font-size: 1.2em;
|
||||
padding: 1px;
|
||||
border: 1px solid #bbb;
|
||||
}
|
||||
#mainmodals .editempty {
|
||||
color: #aaa
|
||||
}
|
||||
.modaldialog {
|
||||
position: absolute;
|
||||
top: 100px;
|
||||
left: 50%;
|
||||
margin-left: -243px;
|
||||
width: 485px;
|
||||
display: none;
|
||||
z-index: 501;
|
||||
zoom: 1;
|
||||
overflow: hidden;
|
||||
background: white;
|
||||
border: 1px solid #999;
|
||||
}
|
||||
.modaldialog .modaldialog-inner {
|
||||
padding: 10pt
|
||||
}
|
||||
.modaldialog .modaldialog-hide {
|
||||
float: right;
|
||||
background-repeat: no-repeat;
|
||||
background-image: url(static/img/sharebox4.gif);
|
||||
display: block;
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
background-position: -454px -6px;
|
||||
margin-right: -5px;
|
||||
margin-top: -5px;
|
||||
}
|
||||
.modaldialog label,
|
||||
.modaldialog h1 {
|
||||
color: #222222;
|
||||
font-size: 125%;
|
||||
font-weight: bold;
|
||||
}
|
||||
.modaldialog th {
|
||||
vertical-align: top;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
#modaloverlay {
|
||||
.toolbar #overlay {
|
||||
z-index: 500;
|
||||
display: none;
|
||||
background-repeat: repeat-both;
|
||||
width: 100%;
|
||||
position: absolute;
|
||||
height: 100%;
|
||||
height: inherit;
|
||||
left: 0;
|
||||
top: 0;
|
||||
}
|
||||
* html #modaloverlay {
|
||||
* html #overlay {
|
||||
/* for IE 6+ */
|
||||
-ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=100)";
|
||||
filter: alpha(opacity=100);
|
||||
|
@ -874,6 +730,10 @@ input[type=checkbox] {
|
|||
.popup input[type=text], #users input[type=text] {
|
||||
outline: none;
|
||||
}
|
||||
.popup button {
|
||||
padding: 5px;
|
||||
font-size: 14px;
|
||||
}
|
||||
.popup a {
|
||||
text-decoration: none
|
||||
}
|
||||
|
@ -895,6 +755,7 @@ input[type=checkbox] {
|
|||
#settings,
|
||||
#importexport,
|
||||
#embed,
|
||||
#connectivity,
|
||||
#users {
|
||||
position: absolute;
|
||||
top: 36px;
|
||||
|
@ -914,15 +775,6 @@ input[type=checkbox] {
|
|||
border-left: 1px solid #ccc !important;
|
||||
width: 185px !important;
|
||||
}
|
||||
@media screen and (max-width: 960px) {
|
||||
.modaldialog {
|
||||
position: relative;
|
||||
margin: 0 auto;
|
||||
width: 80%;
|
||||
top: 40px;
|
||||
left: 0;
|
||||
}
|
||||
}
|
||||
@media screen and (max-width: 600px) {
|
||||
.toolbar ul li.separator {
|
||||
display: none;
|
||||
|
@ -986,6 +838,7 @@ input[type=checkbox] {
|
|||
}
|
||||
#settings,
|
||||
#importexport,
|
||||
#connectivity,
|
||||
#embed {
|
||||
left: 0;
|
||||
top: 0;
|
||||
|
|
|
@ -150,6 +150,13 @@
|
|||
margin-top: 0;
|
||||
padding-right: 6px;
|
||||
}
|
||||
#settings,
|
||||
#importexport,
|
||||
#embed,
|
||||
#connectivity,
|
||||
#users {
|
||||
top: 62px;
|
||||
}
|
||||
#importexport .popup {
|
||||
width: 185px
|
||||
}
|
||||
|
|
Binary file not shown.
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 11 KiB |
|
@ -27,8 +27,6 @@
|
|||
|
||||
var AttributePool = require("./AttributePool");
|
||||
|
||||
var _opt = null;
|
||||
|
||||
/**
|
||||
* ==================== General Util Functions =======================
|
||||
*/
|
||||
|
@ -127,22 +125,13 @@ exports.opIterator = function (opsStr, optStartIndex) {
|
|||
function nextRegexMatch() {
|
||||
prevIndex = curIndex;
|
||||
var result;
|
||||
if (_opt) {
|
||||
result = _opt.nextOpInString(opsStr, curIndex);
|
||||
if (result) {
|
||||
if (result.opcode() == '?') {
|
||||
exports.error("Hit error opcode in op stream");
|
||||
}
|
||||
curIndex = result.lastIndex();
|
||||
}
|
||||
} else {
|
||||
regex.lastIndex = curIndex;
|
||||
result = regex.exec(opsStr);
|
||||
curIndex = regex.lastIndex;
|
||||
if (result[0] == '?') {
|
||||
exports.error("Hit error opcode in op stream");
|
||||
}
|
||||
regex.lastIndex = curIndex;
|
||||
result = regex.exec(opsStr);
|
||||
curIndex = regex.lastIndex;
|
||||
if (result[0] == '?') {
|
||||
exports.error("Hit error opcode in op stream");
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
var regexResult = nextRegexMatch();
|
||||
|
@ -150,13 +139,7 @@ exports.opIterator = function (opsStr, optStartIndex) {
|
|||
|
||||
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]) {
|
||||
if (regexResult[0]) {
|
||||
op.attribs = regexResult[1];
|
||||
op.lines = exports.parseNum(regexResult[2] || 0);
|
||||
op.opcode = regexResult[3];
|
||||
|
@ -169,7 +152,7 @@ exports.opIterator = function (opsStr, optStartIndex) {
|
|||
}
|
||||
|
||||
function hasNext() {
|
||||
return !!(_opt ? regexResult : regexResult[0]);
|
||||
return !!(regexResult[0]);
|
||||
}
|
||||
|
||||
function lastIndex() {
|
||||
|
@ -414,159 +397,109 @@ exports.smartOpAssembler = function () {
|
|||
};
|
||||
};
|
||||
|
||||
if (_opt) {
|
||||
exports.mergingOpAssembler = function () {
|
||||
var assem = _opt.mergingOpAssembler();
|
||||
|
||||
function append(op) {
|
||||
assem.append(op.opcode, op.chars, op.lines, op.attribs);
|
||||
}
|
||||
exports.mergingOpAssembler = function () {
|
||||
// This assembler can be used in production; it efficiently
|
||||
// merges consecutive operations that are mergeable, ignores
|
||||
// no-ops, and drops final pure "keeps". It does not re-order
|
||||
// operations.
|
||||
var assem = exports.opAssembler();
|
||||
var bufOp = exports.newOp();
|
||||
|
||||
function toString() {
|
||||
return assem.toString();
|
||||
}
|
||||
// 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 clear() {
|
||||
assem.clear();
|
||||
}
|
||||
|
||||
function endDocument() {
|
||||
assem.endDocument();
|
||||
}
|
||||
|
||||
return {
|
||||
append: append,
|
||||
toString: toString,
|
||||
clear: clear,
|
||||
endDocument: endDocument
|
||||
};
|
||||
};
|
||||
} else {
|
||||
exports.mergingOpAssembler = function () {
|
||||
// This assembler can be used in production; it efficiently
|
||||
// merges consecutive operations that are mergeable, ignores
|
||||
// no-ops, and drops final pure "keeps". It does not re-order
|
||||
// operations.
|
||||
var assem = exports.opAssembler();
|
||||
var bufOp = exports.newOp();
|
||||
|
||||
// If we get, for example, insertions [xxx\n,yyy], those don't merge,
|
||||
// but if we get [xxx\n,yyy,zzz\n], that merges to [xxx\nyyyzzz\n].
|
||||
// This variable stores the length of yyy and any other newline-less
|
||||
// ops immediately after it.
|
||||
var bufOpAdditionalCharsAfterNewline = 0;
|
||||
|
||||
function flush(isEndDocument) {
|
||||
if (bufOp.opcode) {
|
||||
if (isEndDocument && bufOp.opcode == '=' && !bufOp.attribs) {
|
||||
// final merged keep, leave it implicit
|
||||
} else {
|
||||
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);
|
||||
if (bufOpAdditionalCharsAfterNewline) {
|
||||
bufOp.chars = bufOpAdditionalCharsAfterNewline;
|
||||
bufOp.lines = 0;
|
||||
assem.append(bufOp);
|
||||
bufOpAdditionalCharsAfterNewline = 0;
|
||||
}
|
||||
bufOpAdditionalCharsAfterNewline = 0;
|
||||
}
|
||||
bufOp.opcode = '';
|
||||
}
|
||||
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;
|
||||
}
|
||||
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 {
|
||||
flush();
|
||||
exports.copyOp(op, bufOp);
|
||||
// append in-line text to multi-line bufOp
|
||||
bufOpAdditionalCharsAfterNewline += op.chars;
|
||||
}
|
||||
} else {
|
||||
flush();
|
||||
exports.copyOp(op, bufOp);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function endDocument() {
|
||||
flush(true);
|
||||
}
|
||||
function endDocument() {
|
||||
flush(true);
|
||||
}
|
||||
|
||||
function toString() {
|
||||
flush();
|
||||
return assem.toString();
|
||||
}
|
||||
function toString() {
|
||||
flush();
|
||||
return assem.toString();
|
||||
}
|
||||
|
||||
function clear() {
|
||||
assem.clear();
|
||||
exports.clearOp(bufOp);
|
||||
}
|
||||
return {
|
||||
append: append,
|
||||
toString: toString,
|
||||
clear: clear,
|
||||
endDocument: endDocument
|
||||
};
|
||||
function clear() {
|
||||
assem.clear();
|
||||
exports.clearOp(bufOp);
|
||||
}
|
||||
return {
|
||||
append: append,
|
||||
toString: toString,
|
||||
clear: clear,
|
||||
endDocument: endDocument
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
if (_opt) {
|
||||
exports.opAssembler = function () {
|
||||
var assem = _opt.opAssembler();
|
||||
// this function allows op to be mutated later (doesn't keep a ref)
|
||||
|
||||
function append(op) {
|
||||
assem.append(op.opcode, op.chars, op.lines, op.attribs);
|
||||
|
||||
exports.opAssembler = function () {
|
||||
var pieces = [];
|
||||
// this function allows op to be mutated later (doesn't keep a ref)
|
||||
|
||||
function append(op) {
|
||||
pieces.push(op.attribs);
|
||||
if (op.lines) {
|
||||
pieces.push('|', exports.numToString(op.lines));
|
||||
}
|
||||
pieces.push(op.opcode);
|
||||
pieces.push(exports.numToString(op.chars));
|
||||
}
|
||||
|
||||
function toString() {
|
||||
return assem.toString();
|
||||
}
|
||||
function toString() {
|
||||
return pieces.join('');
|
||||
}
|
||||
|
||||
function clear() {
|
||||
assem.clear();
|
||||
}
|
||||
return {
|
||||
append: append,
|
||||
toString: toString,
|
||||
clear: clear
|
||||
};
|
||||
function clear() {
|
||||
pieces.length = 0;
|
||||
}
|
||||
return {
|
||||
append: append,
|
||||
toString: toString,
|
||||
clear: clear
|
||||
};
|
||||
} else {
|
||||
exports.opAssembler = function () {
|
||||
var pieces = [];
|
||||
// this function allows op to be mutated later (doesn't keep a ref)
|
||||
|
||||
function append(op) {
|
||||
pieces.push(op.attribs);
|
||||
if (op.lines) {
|
||||
pieces.push('|', exports.numToString(op.lines));
|
||||
}
|
||||
pieces.push(op.opcode);
|
||||
pieces.push(exports.numToString(op.chars));
|
||||
}
|
||||
|
||||
function toString() {
|
||||
return pieces.join('');
|
||||
}
|
||||
|
||||
function clear() {
|
||||
pieces.length = 0;
|
||||
}
|
||||
return {
|
||||
append: append,
|
||||
toString: toString,
|
||||
clear: clear
|
||||
};
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* A custom made String Iterator
|
||||
|
|
|
@ -24,6 +24,8 @@
|
|||
// requires: plugins
|
||||
// requires: undefined
|
||||
|
||||
var KERNEL_SOURCE = '../static/js/require-kernel.js';
|
||||
|
||||
Ace2Editor.registry = {
|
||||
nextId: 1
|
||||
};
|
||||
|
@ -31,6 +33,14 @@ Ace2Editor.registry = {
|
|||
var hooks = require('./pluginfw/hooks');
|
||||
var _ = require('./underscore');
|
||||
|
||||
function scriptTag(source) {
|
||||
return (
|
||||
'<script type="text/javascript">\n'
|
||||
+ source.replace(/<\//g, '<\\/') +
|
||||
'</script>'
|
||||
)
|
||||
}
|
||||
|
||||
function Ace2Editor()
|
||||
{
|
||||
var ace2 = Ace2Editor;
|
||||
|
@ -155,42 +165,6 @@ function Ace2Editor()
|
|||
|
||||
return {embeded: embededFiles, remote: remoteFiles};
|
||||
}
|
||||
function pushRequireScriptTo(buffer) {
|
||||
var KERNEL_SOURCE = '../static/js/require-kernel.js';
|
||||
var KERNEL_BOOT = '\
|
||||
require.setRootURI("../javascripts/src");\n\
|
||||
require.setLibraryURI("../javascripts/lib");\n\
|
||||
require.setGlobalKeyPath("require");\n\
|
||||
';
|
||||
if (Ace2Editor.EMBEDED && Ace2Editor.EMBEDED[KERNEL_SOURCE]) {
|
||||
buffer.push('<script type="text/javascript">');
|
||||
buffer.push(Ace2Editor.EMBEDED[KERNEL_SOURCE]);
|
||||
buffer.push(KERNEL_BOOT);
|
||||
buffer.push('<\/script>');
|
||||
} else {
|
||||
file = KERNEL_SOURCE;
|
||||
buffer.push('<script type="application/javascript" src="' + KERNEL_SOURCE + '"><\/script>');
|
||||
buffer.push('<script type="text/javascript">');
|
||||
buffer.push(KERNEL_BOOT);
|
||||
buffer.push('<\/script>');
|
||||
}
|
||||
}
|
||||
function pushScriptsTo(buffer) {
|
||||
/* Folling is for packaging regular expression. */
|
||||
/* $$INCLUDE_JS("../javascripts/lib/ep_etherpad-lite/static/js/ace2_inner.js?callback=require.define"); */
|
||||
/* $$INCLUDE_JS("../javascripts/lib/ep_etherpad-lite/static/js/ace2_common.js?callback=require.define"); */
|
||||
var ACE_SOURCE = '../javascripts/lib/ep_etherpad-lite/static/js/ace2_inner.js?callback=require.define';
|
||||
var ACE_COMMON = '../javascripts/lib/ep_etherpad-lite/static/js/ace2_common.js?callback=require.define';
|
||||
if (Ace2Editor.EMBEDED && Ace2Editor.EMBEDED[ACE_SOURCE]) {
|
||||
buffer.push('<script type="text/javascript">');
|
||||
buffer.push(Ace2Editor.EMBEDED[ACE_SOURCE]);
|
||||
buffer.push(Ace2Editor.EMBEDED[ACE_COMMON]);
|
||||
buffer.push('<\/script>');
|
||||
} else {
|
||||
buffer.push('<script type="application/javascript" src="' + ACE_SOURCE + '"><\/script>');
|
||||
buffer.push('<script type="application/javascript" src="' + ACE_COMMON + '"><\/script>');
|
||||
}
|
||||
}
|
||||
function pushStyleTagsFor(buffer, files) {
|
||||
var sorted = sortFilesByEmbeded(files);
|
||||
var embededFiles = sorted.embeded;
|
||||
|
@ -200,7 +174,7 @@ require.setGlobalKeyPath("require");\n\
|
|||
buffer.push('<style type="text/css">');
|
||||
for (var i = 0, ii = embededFiles.length; i < ii; i++) {
|
||||
var file = embededFiles[i];
|
||||
buffer.push(Ace2Editor.EMBEDED[file].replace(/<\//g, '<\\/'));
|
||||
buffer.push((Ace2Editor.EMBEDED[file] || '').replace(/<\//g, '<\\/'));
|
||||
}
|
||||
buffer.push('<\/style>');
|
||||
}
|
||||
|
@ -254,23 +228,30 @@ require.setGlobalKeyPath("require");\n\
|
|||
|
||||
pushStyleTagsFor(iframeHTML, includedCSS);
|
||||
|
||||
var includedJS = [];
|
||||
pushRequireScriptTo(iframeHTML);
|
||||
pushScriptsTo(iframeHTML);
|
||||
if (!Ace2Editor.EMBEDED && Ace2Editor.EMBEDED[KERNEL_SOURCE]) {
|
||||
// Remotely src'd script tag will not work in IE; it must be embedded, so
|
||||
// throw an error if it is not.
|
||||
throw new Error("Require kernel could not be found.");
|
||||
}
|
||||
|
||||
// Inject my plugins into my child.
|
||||
iframeHTML.push('\
|
||||
<script type="text/javascript">\
|
||||
parent_req = require("ep_etherpad-lite/static/js/pluginfw/parent_require");\
|
||||
parent_req.getRequirementFromParent(require, "ep_etherpad-lite/static/js/pluginfw/hooks");\
|
||||
parent_req.getRequirementFromParent(require, "ep_etherpad-lite/static/js/pluginfw/plugins");\
|
||||
</script>\
|
||||
');
|
||||
|
||||
iframeHTML.push('<script type="text/javascript">');
|
||||
iframeHTML.push('$ = jQuery = require("ep_etherpad-lite/static/js/rjquery").jQuery; // Expose jQuery #HACK');
|
||||
iframeHTML.push('require("ep_etherpad-lite/static/js/ace2_inner");');
|
||||
iframeHTML.push('<\/script>');
|
||||
iframeHTML.push(scriptTag(
|
||||
Ace2Editor.EMBEDED[KERNEL_SOURCE] + '\n\
|
||||
require.setRootURI("../javascripts/src");\n\
|
||||
require.setLibraryURI("../javascripts/lib");\n\
|
||||
require.setGlobalKeyPath("require");\n\
|
||||
\n\
|
||||
var hooks = require("ep_etherpad-lite/static/js/pluginfw/hooks");\n\
|
||||
var plugins = require("ep_etherpad-lite/static/js/pluginfw/client_plugins");\n\
|
||||
hooks.plugins = plugins;\n\
|
||||
plugins.adoptPluginsFromAncestorsOf(window);\n\
|
||||
\n\
|
||||
$ = jQuery = require("ep_etherpad-lite/static/js/rjquery").jQuery; // Expose jQuery #HACK\n\
|
||||
var Ace2Inner = require("ep_etherpad-lite/static/js/ace2_inner");\n\
|
||||
\n\
|
||||
plugins.ensure(function () {\n\
|
||||
Ace2Inner.init();\n\
|
||||
});\n\
|
||||
'));
|
||||
|
||||
iframeHTML.push('<style type="text/css" title="dynamicsyntax"></style>');
|
||||
|
||||
|
@ -284,8 +265,32 @@ require.setGlobalKeyPath("require");\n\
|
|||
var thisFunctionsName = "ChildAccessibleAce2Editor";
|
||||
(function () {return this}())[thisFunctionsName] = Ace2Editor;
|
||||
|
||||
var outerScript = 'editorId = "' + info.id + '"; editorInfo = parent.' + thisFunctionsName + '.registry[editorId]; ' + 'window.onload = function() ' + '{ window.onload = null; setTimeout' + '(function() ' + '{ var iframe = document.createElement("IFRAME"); iframe.name = "ace_inner";' + 'iframe.scrolling = "no"; var outerdocbody = document.getElementById("outerdocbody"); ' + 'iframe.frameBorder = 0; iframe.allowTransparency = true; ' + // for IE
|
||||
'outerdocbody.insertBefore(iframe, outerdocbody.firstChild); ' + 'iframe.ace_outerWin = window; ' + 'readyFunc = function() { editorInfo.onEditorReady(); readyFunc = null; editorInfo = null; }; ' + 'var doc = iframe.contentWindow.document; doc.open(); var text = (' + JSON.stringify(iframeHTML.join('\n')) + ');doc.write(text); doc.close(); ' + '}, 0); }';
|
||||
var outerScript = '\
|
||||
editorId = ' + JSON.stringify(info.id) + ';\n\
|
||||
editorInfo = parent[' + JSON.stringify(thisFunctionsName) + '].registry[editorId];\n\
|
||||
window.onload = function () {\n\
|
||||
window.onload = null;\n\
|
||||
setTimeout(function () {\n\
|
||||
var iframe = document.createElement("IFRAME");\n\
|
||||
iframe.name = "ace_inner";\n\
|
||||
iframe.scrolling = "no";\n\
|
||||
var outerdocbody = document.getElementById("outerdocbody");\n\
|
||||
iframe.frameBorder = 0;\n\
|
||||
iframe.allowTransparency = true; // for IE\n\
|
||||
outerdocbody.insertBefore(iframe, outerdocbody.firstChild);\n\
|
||||
iframe.ace_outerWin = window;\n\
|
||||
readyFunc = function () {\n\
|
||||
editorInfo.onEditorReady();\n\
|
||||
readyFunc = null;\n\
|
||||
editorInfo = null;\n\
|
||||
};\n\
|
||||
var doc = iframe.contentWindow.document;\n\
|
||||
doc.open();\n\
|
||||
var text = (' + JSON.stringify(iframeHTML.join('\n')) + ');\n\
|
||||
doc.write(text);\n\
|
||||
doc.close();\n\
|
||||
}, 0);\n\
|
||||
}';
|
||||
|
||||
var outerHTML = [doctype, '<html><head>']
|
||||
|
||||
|
@ -303,7 +308,7 @@ require.setGlobalKeyPath("require");\n\
|
|||
|
||||
// bizarrely, in FF2, a file with no "external" dependencies won't finish loading properly
|
||||
// (throbs busy while typing)
|
||||
outerHTML.push('<link rel="stylesheet" type="text/css" href="data:text/css,"/>', '\x3cscript>\n', outerScript.replace(/<\//g, '<\\/'), '\n\x3c/script>', '</head><body id="outerdocbody"><div id="sidediv"><!-- --></div><div id="linemetricsdiv">x</div><div id="overlaysdiv"><!-- --></div></body></html>');
|
||||
outerHTML.push('<link rel="stylesheet" type="text/css" href="data:text/css,"/>', scriptTag(outerScript), '</head><body id="outerdocbody"><div id="sidediv"><!-- --></div><div id="linemetricsdiv">x</div><div id="overlaysdiv"><!-- --></div></body></html>');
|
||||
|
||||
var outerFrame = document.createElement("IFRAME");
|
||||
outerFrame.name = "ace_outer";
|
||||
|
|
|
@ -19,11 +19,11 @@
|
|||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
var editor, _, $, jQuery, plugins, Ace2Common;
|
||||
var _, $, jQuery, plugins, Ace2Common;
|
||||
|
||||
Ace2Common = require('./ace2_common');
|
||||
|
||||
plugins = require('ep_etherpad-lite/static/js/pluginfw/plugins');
|
||||
plugins = require('ep_etherpad-lite/static/js/pluginfw/client_plugins');
|
||||
$ = jQuery = require('./rjquery').$;
|
||||
_ = require("./underscore");
|
||||
|
||||
|
@ -200,6 +200,11 @@ function Ace2Inner(){
|
|||
|
||||
var authorInfos = {}; // presence of key determines if author is present in doc
|
||||
|
||||
function getAuthorInfos(){
|
||||
return authorInfos;
|
||||
};
|
||||
editorInfo.ace_getAuthorInfos= getAuthorInfos;
|
||||
|
||||
function setAuthorInfo(author, info)
|
||||
{
|
||||
if ((typeof author) != "string")
|
||||
|
@ -884,7 +889,14 @@ function Ace2Inner(){
|
|||
editorInfo.ace_setEditable = setEditable;
|
||||
editorInfo.ace_execCommand = execCommand;
|
||||
editorInfo.ace_replaceRange = replaceRange;
|
||||
|
||||
editorInfo.ace_getAuthorInfos= getAuthorInfos;
|
||||
editorInfo.ace_performDocumentReplaceRange = performDocumentReplaceRange;
|
||||
editorInfo.ace_performDocumentReplaceCharRange = performDocumentReplaceCharRange;
|
||||
editorInfo.ace_renumberList = renumberList;
|
||||
editorInfo.ace_doReturnKey = doReturnKey;
|
||||
editorInfo.ace_isBlockElement = isBlockElement;
|
||||
editorInfo.ace_getLineListType = getLineListType;
|
||||
|
||||
editorInfo.ace_callWithAce = function(fn, callStack, normalize)
|
||||
{
|
||||
var wrapper = function()
|
||||
|
@ -1161,7 +1173,7 @@ function Ace2Inner(){
|
|||
//if (! top.BEFORE) top.BEFORE = [];
|
||||
//top.BEFORE.push(magicdom.root.dom.innerHTML);
|
||||
//if (! isEditable) return; // and don't reschedule
|
||||
if (inInternationalComposition)
|
||||
if (window.parent.parent.inInternationalComposition)
|
||||
{
|
||||
// don't do idle input incorporation during international input composition
|
||||
idleWorkTimer.atLeast(500);
|
||||
|
@ -1486,7 +1498,6 @@ function Ace2Inner(){
|
|||
|
||||
if (currentCallStack.domClean) return false;
|
||||
|
||||
inInternationalComposition = false; // if we need the document normalized, so be it
|
||||
currentCallStack.isUserChange = true;
|
||||
|
||||
isTimeUp = (isTimeUp ||
|
||||
|
@ -1686,11 +1697,27 @@ function Ace2Inner(){
|
|||
if (selection && !selStart)
|
||||
{
|
||||
//if (domChanges) dmesg("selection not collected");
|
||||
selStart = getLineAndCharForPoint(selection.startPoint);
|
||||
var selStartFromHook = hooks.callAll('aceStartLineAndCharForPoint', {
|
||||
callstack: currentCallStack,
|
||||
editorInfo: editorInfo,
|
||||
rep: rep,
|
||||
root:root,
|
||||
point:selection.startPoint,
|
||||
documentAttributeManager: documentAttributeManager
|
||||
});
|
||||
selStart = (selStartFromHook==null||selStartFromHook.length==0)?getLineAndCharForPoint(selection.startPoint):selStartFromHook;
|
||||
}
|
||||
if (selection && !selEnd)
|
||||
{
|
||||
selEnd = getLineAndCharForPoint(selection.endPoint);
|
||||
var selEndFromHook = hooks.callAll('aceEndLineAndCharForPoint', {
|
||||
callstack: currentCallStack,
|
||||
editorInfo: editorInfo,
|
||||
rep: rep,
|
||||
root:root,
|
||||
point:selection.endPoint,
|
||||
documentAttributeManager: documentAttributeManager
|
||||
});
|
||||
selEnd = (selEndFromHook==null||selEndFromHook.length==0)?getLineAndCharForPoint(selection.endPoint):selEndFromHook;
|
||||
}
|
||||
|
||||
// selection from content collection can, in various ways, extend past final
|
||||
|
@ -1845,17 +1872,20 @@ function Ace2Inner(){
|
|||
{
|
||||
return rep.selStart[0];
|
||||
}
|
||||
|
||||
editorInfo.ace_caretLine = caretLine;
|
||||
|
||||
function caretColumn()
|
||||
{
|
||||
return rep.selStart[1];
|
||||
}
|
||||
|
||||
editorInfo.ace_caretColumn = caretColumn;
|
||||
|
||||
function caretDocChar()
|
||||
{
|
||||
return rep.lines.offsetOfIndex(caretLine()) + caretColumn();
|
||||
}
|
||||
|
||||
editorInfo.ace_caretDocChar = caretDocChar;
|
||||
|
||||
function handleReturnIndentation()
|
||||
{
|
||||
// on return, indent to level of previous line
|
||||
|
@ -3237,7 +3267,7 @@ function Ace2Inner(){
|
|||
}
|
||||
//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");
|
||||
window.parent.parent.padeditbar.toggleDropDown("none");
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -3447,7 +3477,8 @@ function Ace2Inner(){
|
|||
{
|
||||
return !!REGEX_WORDCHAR.exec(c);
|
||||
}
|
||||
|
||||
editorInfo.ace_isWordChar = isWordChar;
|
||||
|
||||
function isSpaceChar(c)
|
||||
{
|
||||
return !!REGEX_SPACE.exec(c);
|
||||
|
@ -3514,6 +3545,13 @@ function Ace2Inner(){
|
|||
var keyCode = evt.keyCode;
|
||||
var which = evt.which;
|
||||
|
||||
// prevent ESC key
|
||||
if (keyCode == 27)
|
||||
{
|
||||
evt.preventDefault();
|
||||
return;
|
||||
}
|
||||
|
||||
//dmesg("keyevent type: "+type+", which: "+which);
|
||||
// Don't take action based on modifier keys going up and down.
|
||||
// Modifier keys do not generate "keypress" events.
|
||||
|
@ -3548,7 +3586,15 @@ function Ace2Inner(){
|
|||
|
||||
if (!stopped)
|
||||
{
|
||||
if (isTypeForSpecialKey && keyCode == 8)
|
||||
var specialHandledInHook = hooks.callAll('aceKeyEvent', {
|
||||
callstack: currentCallStack,
|
||||
editorInfo: editorInfo,
|
||||
rep: rep,
|
||||
documentAttributeManager: documentAttributeManager,
|
||||
evt:evt
|
||||
});
|
||||
specialHandled = (specialHandledInHook&&specialHandledInHook.length>0)?specialHandledInHook[0]:specialHandled;
|
||||
if ((!specialHandled) && isTypeForSpecialKey && keyCode == 8)
|
||||
{
|
||||
// "delete" key; in mozilla, if we're at the beginning of a line, normalize now,
|
||||
// or else deleting a blank line can take two delete presses.
|
||||
|
@ -3683,7 +3729,7 @@ function Ace2Inner(){
|
|||
thisKeyDoesntTriggerNormalize = true;
|
||||
}
|
||||
|
||||
if ((!specialHandled) && (!thisKeyDoesntTriggerNormalize) && (!inInternationalComposition))
|
||||
if ((!specialHandled) && (!thisKeyDoesntTriggerNormalize) && (!window.parent.parent.inInternationalComposition))
|
||||
{
|
||||
if (type != "keyup" || !incorpIfQuick())
|
||||
{
|
||||
|
@ -4543,19 +4589,9 @@ function Ace2Inner(){
|
|||
}
|
||||
}
|
||||
|
||||
var inInternationalComposition = false;
|
||||
|
||||
function handleCompositionEvent(evt)
|
||||
{
|
||||
// international input events, fired in FF3, at least; allow e.g. Japanese input
|
||||
if (evt.type == "compositionstart")
|
||||
{
|
||||
inInternationalComposition = true;
|
||||
}
|
||||
else if (evt.type == "compositionend")
|
||||
{
|
||||
inInternationalComposition = false;
|
||||
}
|
||||
window.parent.parent.handleCompositionEvent(evt);
|
||||
}
|
||||
|
||||
function bindTheEventHandlers()
|
||||
|
@ -4570,7 +4606,8 @@ function Ace2Inner(){
|
|||
$(document).on("click", handleIEOuterClick);
|
||||
}
|
||||
if (browser.msie) $(root).on("paste", handleIEPaste);
|
||||
if ((!browser.msie) && document.documentElement)
|
||||
// CompositionEvent is not implemented below IE version 8
|
||||
if ( !(browser.msie && browser.version < 9) && document.documentElement)
|
||||
{
|
||||
$(document.documentElement).on("compositionstart", handleCompositionEvent);
|
||||
$(document.documentElement).on("compositionend", handleCompositionEvent);
|
||||
|
@ -5393,62 +5430,64 @@ function Ace2Inner(){
|
|||
return documentAttributeManager.setAttributesOnRange.apply(documentAttributeManager, arguments);
|
||||
};
|
||||
|
||||
$(document).ready(function(){
|
||||
doc = document; // defined as a var in scope outside
|
||||
inCallStack("setup", function()
|
||||
{
|
||||
var body = doc.getElementById("innerdocbody");
|
||||
root = body; // defined as a var in scope outside
|
||||
if (browser.mozilla) $(root).addClass("mozilla");
|
||||
if (browser.safari) $(root).addClass("safari");
|
||||
if (browser.msie) $(root).addClass("msie");
|
||||
if (browser.msie)
|
||||
this.init = function () {
|
||||
$(document).ready(function(){
|
||||
doc = document; // defined as a var in scope outside
|
||||
inCallStack("setup", function()
|
||||
{
|
||||
// cache CSS background images
|
||||
try
|
||||
var body = doc.getElementById("innerdocbody");
|
||||
root = body; // defined as a var in scope outside
|
||||
if (browser.mozilla) $(root).addClass("mozilla");
|
||||
if (browser.safari) $(root).addClass("safari");
|
||||
if (browser.msie) $(root).addClass("msie");
|
||||
if (browser.msie)
|
||||
{
|
||||
doc.execCommand("BackgroundImageCache", false, true);
|
||||
// cache CSS background images
|
||||
try
|
||||
{
|
||||
doc.execCommand("BackgroundImageCache", false, true);
|
||||
}
|
||||
catch (e)
|
||||
{ /* throws an error in some IE 6 but not others! */
|
||||
}
|
||||
}
|
||||
catch (e)
|
||||
{ /* throws an error in some IE 6 but not others! */
|
||||
}
|
||||
}
|
||||
setClassPresence(root, "authorColors", true);
|
||||
setClassPresence(root, "doesWrap", doesWrap);
|
||||
setClassPresence(root, "authorColors", true);
|
||||
setClassPresence(root, "doesWrap", doesWrap);
|
||||
|
||||
initDynamicCSS();
|
||||
initDynamicCSS();
|
||||
|
||||
enforceEditability();
|
||||
enforceEditability();
|
||||
|
||||
// set up dom and rep
|
||||
while (root.firstChild) root.removeChild(root.firstChild);
|
||||
var oneEntry = createDomLineEntry("");
|
||||
doRepLineSplice(0, rep.lines.length(), [oneEntry]);
|
||||
insertDomLines(null, [oneEntry.domInfo], null);
|
||||
rep.alines = Changeset.splitAttributionLines(
|
||||
Changeset.makeAttribution("\n"), "\n");
|
||||
// set up dom and rep
|
||||
while (root.firstChild) root.removeChild(root.firstChild);
|
||||
var oneEntry = createDomLineEntry("");
|
||||
doRepLineSplice(0, rep.lines.length(), [oneEntry]);
|
||||
insertDomLines(null, [oneEntry.domInfo], null);
|
||||
rep.alines = Changeset.splitAttributionLines(
|
||||
Changeset.makeAttribution("\n"), "\n");
|
||||
|
||||
bindTheEventHandlers();
|
||||
bindTheEventHandlers();
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
hooks.callAll('aceInitialized', {
|
||||
editorInfo: editorInfo,
|
||||
rep: rep,
|
||||
documentAttributeManager: documentAttributeManager
|
||||
});
|
||||
hooks.callAll('aceInitialized', {
|
||||
editorInfo: editorInfo,
|
||||
rep: rep,
|
||||
documentAttributeManager: documentAttributeManager
|
||||
});
|
||||
|
||||
scheduler.setTimeout(function()
|
||||
{
|
||||
parent.readyFunc(); // defined in code that sets up the inner iframe
|
||||
}, 0);
|
||||
scheduler.setTimeout(function()
|
||||
{
|
||||
parent.readyFunc(); // defined in code that sets up the inner iframe
|
||||
}, 0);
|
||||
|
||||
isSetUp = true;
|
||||
});
|
||||
isSetUp = true;
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Ensure that plugins are loaded before initializing the editor
|
||||
plugins.ensure(function () {
|
||||
var editor = new Ace2Inner();
|
||||
});
|
||||
exports.init = function () {
|
||||
var editor = new Ace2Inner()
|
||||
editor.init();
|
||||
};
|
||||
|
|
|
@ -15,7 +15,7 @@ $(document).ready(function () {
|
|||
$('.search-results').data('query', {
|
||||
pattern: '',
|
||||
offset: 0,
|
||||
limit: 4,
|
||||
limit: 12,
|
||||
});
|
||||
|
||||
var doUpdate = false;
|
||||
|
|
|
@ -155,9 +155,7 @@ function loadBroadcastSliderJS(fireWhenAllScriptsAreLoaded)
|
|||
|
||||
function showReconnectUI()
|
||||
{
|
||||
var cls = 'modaldialog cboxdisconnected cboxdisconnected_unknown';
|
||||
$("#connectionbox").get(0).className = cls;
|
||||
padmodals.showModal("#connectionbox", 500);
|
||||
padmodals.showModal("disconnected");
|
||||
}
|
||||
|
||||
var fixPadHeight = _.throttle(function(){
|
||||
|
|
|
@ -23,11 +23,12 @@
|
|||
var padutils = require('./pad_utils').padutils;
|
||||
var padcookie = require('./pad_cookie').padcookie;
|
||||
|
||||
var Tinycon = require('tinycon/tinycon');
|
||||
|
||||
var chat = (function()
|
||||
{
|
||||
var isStuck = false;
|
||||
var chatMentions = 0;
|
||||
var title = document.title;
|
||||
var self = {
|
||||
show: function ()
|
||||
{
|
||||
|
@ -35,7 +36,7 @@ var chat = (function()
|
|||
$("#chatbox").show();
|
||||
self.scrollDown();
|
||||
chatMentions = 0;
|
||||
document.title = title;
|
||||
Tinycon.setBubble(0);
|
||||
},
|
||||
stickToScreen: function(fromInitialCall) // Make chat stick to right hand side of screen
|
||||
{
|
||||
|
@ -62,8 +63,12 @@ var chat = (function()
|
|||
},
|
||||
scrollDown: function()
|
||||
{
|
||||
if($('#chatbox').css("display") != "none")
|
||||
$('#chattext').animate({scrollTop: $('#chattext')[0].scrollHeight}, "slow");
|
||||
if($('#chatbox').css("display") != "none"){
|
||||
if(!self.lastMessage || !self.lastMessage.position() || self.lastMessage.position().top < $('#chattext').height()) {
|
||||
$('#chattext').animate({scrollTop: $('#chattext')[0].scrollHeight}, "slow");
|
||||
self.lastMessage = $('#chattext > p').eq(-1);
|
||||
}
|
||||
}
|
||||
},
|
||||
send: function()
|
||||
{
|
||||
|
@ -122,12 +127,9 @@ var chat = (function()
|
|||
// chat throb stuff -- Just make it throw for twice as long
|
||||
if(wasMentioned && !alreadyFocused)
|
||||
{ // If the user was mentioned show for twice as long and flash the browser window
|
||||
if (chatMentions == 0){
|
||||
title = document.title;
|
||||
}
|
||||
$('#chatthrob').html("<b>"+authorName+"</b>" + ": " + text).show().delay(4000).hide(400);
|
||||
chatMentions++;
|
||||
document.title = "("+chatMentions+") " + title;
|
||||
Tinycon.setBubble(chatMentions);
|
||||
}
|
||||
else
|
||||
{
|
||||
|
@ -137,7 +139,7 @@ var chat = (function()
|
|||
// Clear the chat mentions when the user clicks on the chat input box
|
||||
$('#chatinput').click(function(){
|
||||
chatMentions = 0;
|
||||
document.title = title;
|
||||
Tinycon.setBubble(0);
|
||||
});
|
||||
self.scrollDown();
|
||||
|
||||
|
|
|
@ -21,6 +21,7 @@
|
|||
*/
|
||||
|
||||
var chat = require('./chat').chat;
|
||||
var hooks = require('./pluginfw/hooks');
|
||||
|
||||
// Dependency fill on init. This exists for `pad.socket` only.
|
||||
// TODO: bind directly to the socket.
|
||||
|
@ -61,6 +62,7 @@ function getCollabClient(ace2editor, serverVars, initialUserInfo, options, _pad)
|
|||
var caughtErrorCatchers = [];
|
||||
var caughtErrorTimes = [];
|
||||
var debugMessages = [];
|
||||
var msgQueue = [];
|
||||
|
||||
tellAceAboutHistoricalAuthors(serverVars.historicalAuthorData);
|
||||
tellAceActiveAuthorInfo(initialUserInfo);
|
||||
|
@ -109,6 +111,7 @@ function getCollabClient(ace2editor, serverVars, initialUserInfo, options, _pad)
|
|||
|
||||
function handleUserChanges()
|
||||
{
|
||||
if (window.parent.parent.inInternationalComposition) return;
|
||||
if ((!getSocket()) || channelState == "CONNECTING")
|
||||
{
|
||||
if (channelState == "CONNECTING" && (((+new Date()) - initialStartConnectTime) > 20000))
|
||||
|
@ -127,12 +130,12 @@ function getCollabClient(ace2editor, serverVars, initialUserInfo, options, _pad)
|
|||
|
||||
if (state != "IDLE")
|
||||
{
|
||||
if (state == "COMMITTING" && (t - lastCommitTime) > 20000)
|
||||
if (state == "COMMITTING" && msgQueue.length == 0 && (t - lastCommitTime) > 20000)
|
||||
{
|
||||
// a commit is taking too long
|
||||
setChannelState("DISCONNECTED", "slowcommit");
|
||||
}
|
||||
else if (state == "COMMITTING" && (t - lastCommitTime) > 5000)
|
||||
else if (state == "COMMITTING" && msgQueue.length == 0 && (t - lastCommitTime) > 5000)
|
||||
{
|
||||
callbacks.onConnectionTrouble("SLOW");
|
||||
}
|
||||
|
@ -151,6 +154,36 @@ function getCollabClient(ace2editor, serverVars, initialUserInfo, options, _pad)
|
|||
return;
|
||||
}
|
||||
|
||||
// apply msgQueue changeset.
|
||||
if (msgQueue.length != 0) {
|
||||
while (msg = msgQueue.shift()) {
|
||||
var newRev = msg.newRev;
|
||||
rev=newRev;
|
||||
if (msg.type == "ACCEPT_COMMIT")
|
||||
{
|
||||
editor.applyPreparedChangesetToBase();
|
||||
setStateIdle();
|
||||
callCatchingErrors("onInternalAction", function()
|
||||
{
|
||||
callbacks.onInternalAction("commitAcceptedByServer");
|
||||
});
|
||||
callCatchingErrors("onConnectionTrouble", function()
|
||||
{
|
||||
callbacks.onConnectionTrouble("OK");
|
||||
});
|
||||
handleUserChanges();
|
||||
}
|
||||
else if (msg.type == "NEW_CHANGES")
|
||||
{
|
||||
var changeset = msg.changeset;
|
||||
var author = (msg.author || '');
|
||||
var apool = msg.apool;
|
||||
|
||||
editor.applyChangesToBase(changeset, author, apool);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var sentMessage = false;
|
||||
var userChangesData = editor.prepareUserChangeset();
|
||||
if (userChangesData.changeset)
|
||||
|
@ -253,6 +286,22 @@ function getCollabClient(ace2editor, serverVars, initialUserInfo, options, _pad)
|
|||
var changeset = msg.changeset;
|
||||
var author = (msg.author || '');
|
||||
var apool = msg.apool;
|
||||
|
||||
// When inInternationalComposition, msg pushed msgQueue.
|
||||
if (msgQueue.length > 0 || window.parent.parent.inInternationalComposition) {
|
||||
if (msgQueue.length > 0) oldRev = msgQueue[msgQueue.length - 1].newRev;
|
||||
else oldRev = rev;
|
||||
|
||||
if (newRev != (oldRev + 1))
|
||||
{
|
||||
dmesg("bad message revision on NEW_CHANGES: " + newRev + " not " + (oldRev + 1));
|
||||
setChannelState("DISCONNECTED", "badmessage_newchanges");
|
||||
return;
|
||||
}
|
||||
msgQueue.push(msg);
|
||||
return;
|
||||
}
|
||||
|
||||
if (newRev != (rev + 1))
|
||||
{
|
||||
dmesg("bad message revision on NEW_CHANGES: " + newRev + " not " + (rev + 1));
|
||||
|
@ -265,6 +314,18 @@ function getCollabClient(ace2editor, serverVars, initialUserInfo, options, _pad)
|
|||
else if (msg.type == "ACCEPT_COMMIT")
|
||||
{
|
||||
var newRev = msg.newRev;
|
||||
if (msgQueue.length > 0)
|
||||
{
|
||||
if (newRev != (msgQueue[msgQueue.length - 1].newRev + 1))
|
||||
{
|
||||
dmesg("bad message revision on ACCEPT_COMMIT: " + newRev + " not " + (msgQueue[msgQueue.length - 1][0] + 1));
|
||||
setChannelState("DISCONNECTED", "badmessage_acceptcommit");
|
||||
return;
|
||||
}
|
||||
msgQueue.push(msg);
|
||||
return;
|
||||
}
|
||||
|
||||
if (newRev != (rev + 1))
|
||||
{
|
||||
dmesg("bad message revision on ACCEPT_COMMIT: " + newRev + " not " + (rev + 1));
|
||||
|
@ -337,6 +398,7 @@ function getCollabClient(ace2editor, serverVars, initialUserInfo, options, _pad)
|
|||
{
|
||||
callbacks.onServerMessage(msg.payload);
|
||||
}
|
||||
hooks.callAll('handleClientMessage_' + msg.type, {payload: msg.payload});
|
||||
}
|
||||
|
||||
function updateUserInfo(userInfo)
|
||||
|
|
|
@ -25,13 +25,14 @@
|
|||
|
||||
var _MAX_LIST_LEVEL = 8;
|
||||
|
||||
var UNorm = require('unorm');
|
||||
var Changeset = require('./Changeset');
|
||||
var hooks = require('./pluginfw/hooks');
|
||||
var _ = require('./underscore');
|
||||
|
||||
function sanitizeUnicode(s)
|
||||
{
|
||||
return s.replace(/[\uffff\ufffe\ufeff\ufdd0-\ufdef\ud800-\udfff]/g, '?');
|
||||
return UNorm.nfc(s).replace(/[\uffff\ufffe\ufeff\ufdd0-\ufdef\ud800-\udfff]/g, '?');
|
||||
}
|
||||
|
||||
function makeContentCollector(collectStyles, browser, apool, domInterface, className2Author)
|
||||
|
@ -258,7 +259,8 @@ function makeContentCollector(collectStyles, browser, apool, domInterface, class
|
|||
{
|
||||
state.listNesting--;
|
||||
}
|
||||
if(oldListType) state.lineAttributes['list'] = oldListType;
|
||||
if (oldListType && oldListType != 'none') { state.lineAttributes['list'] = oldListType; }
|
||||
else { delete state.lineAttributes['list']; }
|
||||
_recalcAttribString(state);
|
||||
}
|
||||
|
||||
|
@ -309,7 +311,7 @@ function makeContentCollector(collectStyles, browser, apool, domInterface, class
|
|||
['insertorder', 'first']
|
||||
].concat(
|
||||
_.map(state.lineAttributes,function(value,key){
|
||||
console.log([key, value])
|
||||
if (window.console) console.log([key, value])
|
||||
return [key, value];
|
||||
})
|
||||
);
|
||||
|
@ -373,6 +375,19 @@ function makeContentCollector(collectStyles, browser, apool, domInterface, class
|
|||
if (dom.isNodeText(node))
|
||||
{
|
||||
var txt = dom.nodeValue(node);
|
||||
var tname = dom.nodeAttr(node.parentNode,"name");
|
||||
|
||||
var txtFromHook = hooks.callAll('collectContentLineText', {
|
||||
cc: this,
|
||||
state: state,
|
||||
tname: tname,
|
||||
node:node,
|
||||
text:txt,
|
||||
styl: null,
|
||||
cls: null
|
||||
});
|
||||
var txt = (typeof(txtFromHook)=='object'&&txtFromHook.length==0)?dom.nodeValue(node):txtFromHook[0];
|
||||
|
||||
var rest = '';
|
||||
var x = 0; // offset into original text
|
||||
if (txt.length == 0)
|
||||
|
@ -384,7 +399,7 @@ function makeContentCollector(collectStyles, browser, apool, domInterface, class
|
|||
if (endPoint && node == endPoint.node)
|
||||
{
|
||||
selEnd = _pointHere(0, state);
|
||||
}
|
||||
}
|
||||
}
|
||||
while (txt.length > 0)
|
||||
{
|
||||
|
@ -439,8 +454,21 @@ function makeContentCollector(collectStyles, browser, apool, domInterface, class
|
|||
{
|
||||
var tname = (dom.nodeTagName(node) || "").toLowerCase();
|
||||
if (tname == "br")
|
||||
{
|
||||
cc.startNewLine(state);
|
||||
{
|
||||
this.breakLine = true;
|
||||
var tvalue = dom.nodeAttr(node, 'value');
|
||||
var induceLineBreak = hooks.callAll('collectContentLineBreak', {
|
||||
cc: this,
|
||||
state: state,
|
||||
tname: tname,
|
||||
tvalue:tvalue,
|
||||
styl: null,
|
||||
cls: null
|
||||
});
|
||||
var startNewLine= (typeof(induceLineBreak)=='object'&&induceLineBreak.length==0)?true:induceLineBreak[0];
|
||||
if(startNewLine){
|
||||
cc.startNewLine(state);
|
||||
}
|
||||
}
|
||||
else if (tname == "script" || tname == "style")
|
||||
{
|
||||
|
|
|
@ -146,9 +146,16 @@ linestylefilter.getLineStyleFilter = function(lineLength, aline, textAndClassFun
|
|||
|
||||
return function(txt, cls)
|
||||
{
|
||||
|
||||
var disableAuthColorForThisLine = hooks.callAll("disableAuthorColorsForThisLine", {
|
||||
linestylefilter: linestylefilter,
|
||||
text: txt,
|
||||
"class": cls
|
||||
}, " ", " ", "");
|
||||
var disableAuthors = (disableAuthColorForThisLine==null||disableAuthColorForThisLine.length==0)?false:disableAuthColorForThisLine[0];
|
||||
while (txt.length > 0)
|
||||
{
|
||||
if (leftInAuthor <= 0)
|
||||
if (leftInAuthor <= 0 || disableAuthors)
|
||||
{
|
||||
// prevent infinite loop if something funny's going on
|
||||
return nextAfterAuthorColors(txt, cls);
|
||||
|
|
|
@ -50,6 +50,22 @@ var randomString = require('./pad_utils').randomString;
|
|||
|
||||
var hooks = require('./pluginfw/hooks');
|
||||
|
||||
window.inInternationalComposition = false;
|
||||
var inInternationalComposition = window.inInternationalComposition;
|
||||
|
||||
window.handleCompositionEvent = function handleCompositionEvent(evt)
|
||||
{
|
||||
// international input events, fired in FF3, at least; allow e.g. Japanese input
|
||||
if (evt.type == "compositionstart")
|
||||
{
|
||||
this.inInternationalComposition = true;
|
||||
}
|
||||
else if (evt.type == "compositionend")
|
||||
{
|
||||
this.inInternationalComposition = false;
|
||||
}
|
||||
}
|
||||
|
||||
function createCookie(name, value, days, path)
|
||||
{
|
||||
if (days)
|
||||
|
@ -368,6 +384,10 @@ function handshake()
|
|||
});
|
||||
// Bind the colorpicker
|
||||
var fb = $('#colorpicker').farbtastic({ callback: '#mycolorpickerpreview', width: 220});
|
||||
// Bind the read only button
|
||||
$('#readonlyinput').on('click',function(){
|
||||
padeditbar.setEmbedLinks();
|
||||
});
|
||||
}
|
||||
|
||||
var pad = {
|
||||
|
@ -782,8 +802,6 @@ var pad = {
|
|||
}, 1000);
|
||||
}
|
||||
|
||||
padsavedrevs.handleIsFullyConnected(isConnected);
|
||||
|
||||
// pad.determineSidebarVisibility(isConnected && !isInitialConnect);
|
||||
pad.determineChatVisibility(isConnected && !isInitialConnect);
|
||||
pad.determineAuthorshipColorsVisibility();
|
||||
|
|
|
@ -21,6 +21,7 @@
|
|||
*/
|
||||
|
||||
var padmodals = require('./pad_modals').padmodals;
|
||||
var padeditbar = require('./pad_editbar').padeditbar;
|
||||
|
||||
var padconnectionstatus = (function()
|
||||
{
|
||||
|
@ -42,15 +43,18 @@ var padconnectionstatus = (function()
|
|||
status = {
|
||||
what: 'connected'
|
||||
};
|
||||
padmodals.hideModal(500);
|
||||
|
||||
padmodals.showModal('connected');
|
||||
padmodals.hideOverlay(500);
|
||||
},
|
||||
reconnecting: function()
|
||||
{
|
||||
status = {
|
||||
what: 'reconnecting'
|
||||
};
|
||||
$("#connectionbox").get(0).className = 'modaldialog cboxreconnecting';
|
||||
padmodals.showModal("#connectionbox", 500);
|
||||
|
||||
padmodals.showModal('reconnecting');
|
||||
padmodals.showOverlay(500);
|
||||
},
|
||||
disconnected: function(msg)
|
||||
{
|
||||
|
@ -61,20 +65,15 @@ var padconnectionstatus = (function()
|
|||
what: 'disconnected',
|
||||
why: msg
|
||||
};
|
||||
|
||||
var k = String(msg).toLowerCase(); // known reason why
|
||||
if (!(k == 'userdup' || k == 'deleted' || k == 'looping' || k == 'slowcommit' || k == 'initsocketfail' || k == 'unauth'))
|
||||
{
|
||||
k = 'unknown';
|
||||
k = 'disconnected';
|
||||
}
|
||||
|
||||
var cls = 'modaldialog cboxdisconnected cboxdisconnected_' + k;
|
||||
$("#connectionbox").get(0).className = cls;
|
||||
padmodals.showModal("#connectionbox", 500);
|
||||
|
||||
$('button#forcereconnect').click(function()
|
||||
{
|
||||
window.location.reload();
|
||||
});
|
||||
padmodals.showModal(k);
|
||||
padmodals.showOverlay(500);
|
||||
},
|
||||
isFullyConnected: function()
|
||||
{
|
||||
|
|
|
@ -122,21 +122,25 @@ var padeditbar = (function()
|
|||
{
|
||||
if(cmd == "showusers")
|
||||
{
|
||||
self.toogleDropDown("users");
|
||||
self.toggleDropDown("users");
|
||||
}
|
||||
else if (cmd == 'settings')
|
||||
{
|
||||
self.toogleDropDown("settings");
|
||||
self.toggleDropDown("settings");
|
||||
}
|
||||
else if (cmd == 'connectivity')
|
||||
{
|
||||
self.toggleDropDown("connectivity");
|
||||
}
|
||||
else if (cmd == 'embed')
|
||||
{
|
||||
self.setEmbedLinks();
|
||||
$('#linkinput').focus().select();
|
||||
self.toogleDropDown("embed");
|
||||
self.toggleDropDown("embed");
|
||||
}
|
||||
else if (cmd == 'import_export')
|
||||
{
|
||||
self.toogleDropDown("importexport");
|
||||
self.toggleDropDown("importexport");
|
||||
}
|
||||
else if (cmd == 'savedRevision')
|
||||
{
|
||||
|
@ -182,13 +186,14 @@ var padeditbar = (function()
|
|||
}
|
||||
if(padeditor.ace) padeditor.ace.focus();
|
||||
},
|
||||
toogleDropDown: function(moduleName)
|
||||
toggleDropDown: function(moduleName, cb)
|
||||
{
|
||||
var modules = ["settings", "importexport", "embed", "users"];
|
||||
var modules = ["settings", "connectivity", "importexport", "embed", "users"];
|
||||
|
||||
// hide all modules and remove highlighting of all buttons
|
||||
if(moduleName == "none")
|
||||
{
|
||||
var returned = false
|
||||
for(var i=0;i<modules.length;i++)
|
||||
{
|
||||
//skip the userlist
|
||||
|
@ -200,9 +205,11 @@ var padeditbar = (function()
|
|||
if(module.css('display') != "none")
|
||||
{
|
||||
$("#" + modules[i] + "link").removeClass("selected");
|
||||
module.slideUp("fast");
|
||||
module.slideUp("fast", cb);
|
||||
returned = true;
|
||||
}
|
||||
}
|
||||
if(!returned && cb) return cb();
|
||||
}
|
||||
else
|
||||
{
|
||||
|
@ -220,7 +227,7 @@ var padeditbar = (function()
|
|||
else if(modules[i]==moduleName)
|
||||
{
|
||||
$("#" + modules[i] + "link").addClass("selected");
|
||||
module.slideDown("fast");
|
||||
module.slideDown("fast", cb);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -242,13 +249,13 @@ var padeditbar = (function()
|
|||
{
|
||||
var basePath = document.location.href.substring(0, document.location.href.indexOf("/p/"));
|
||||
var readonlyLink = basePath + "/p/" + clientVars.readOnlyId;
|
||||
$('#embedinput').val("<iframe name='embed_readonly' src='" + readonlyLink + "?showControls=true&showChat=true&showLineNumbers=true&useMonospaceFont=false' width=600 height=400>");
|
||||
$('#embedinput').val("<iframe name='embed_readonly' src='" + readonlyLink + "?showControls=true&showChat=true&showLineNumbers=true&useMonospaceFont=false' width=600 height=400></iframe>");
|
||||
$('#linkinput').val(readonlyLink);
|
||||
}
|
||||
else
|
||||
{
|
||||
var padurl = window.location.href.split("?")[0];
|
||||
$('#embedinput').val("<iframe name='embed_readwrite' src='" + padurl + "?showControls=true&showChat=true&showLineNumbers=true&useMonospaceFont=false' width=600 height=400>");
|
||||
$('#embedinput').val("<iframe name='embed_readwrite' src='" + padurl + "?showControls=true&showChat=true&showLineNumbers=true&useMonospaceFont=false' width=600 height=400></iframe>");
|
||||
$('#linkinput').val(padurl);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -21,6 +21,7 @@
|
|||
*/
|
||||
|
||||
var padutils = require('./pad_utils').padutils;
|
||||
var padeditbar = require('./pad_editbar').padeditbar;
|
||||
|
||||
var padmodals = (function()
|
||||
{
|
||||
|
@ -30,17 +31,16 @@ var padmodals = (function()
|
|||
{
|
||||
pad = _pad;
|
||||
},
|
||||
showModal: function(modalId, duration)
|
||||
showModal: function(messageId)
|
||||
{
|
||||
$(".modaldialog").hide();
|
||||
$(modalId).show().css(
|
||||
{
|
||||
'opacity': 0
|
||||
}).animate(
|
||||
{
|
||||
'opacity': 1
|
||||
}, duration);
|
||||
$("#modaloverlay").show().css(
|
||||
padeditbar.toggleDropDown("none", function() {
|
||||
$("#connectivity .visible").removeClass('visible');
|
||||
$("#connectivity ."+messageId).addClass('visible');
|
||||
padeditbar.toggleDropDown("connectivity");
|
||||
});
|
||||
},
|
||||
showOverlay: function(duration) {
|
||||
$("#overlay").show().css(
|
||||
{
|
||||
'opacity': 0
|
||||
}).animate(
|
||||
|
@ -48,19 +48,8 @@ var padmodals = (function()
|
|||
'opacity': 1
|
||||
}, duration);
|
||||
},
|
||||
hideModal: function(duration)
|
||||
{
|
||||
padutils.cancelActions('hide-feedbackbox');
|
||||
padutils.cancelActions('hide-sharebox');
|
||||
$("#sharebox-response").hide();
|
||||
$(".modaldialog").animate(
|
||||
{
|
||||
'opacity': 0
|
||||
}, duration, function()
|
||||
{
|
||||
$("#modaloverlay").hide();
|
||||
});
|
||||
$("#modaloverlay").animate(
|
||||
hideOverlay: function(duration) {
|
||||
$("#overlay").animate(
|
||||
{
|
||||
'opacity': 0
|
||||
}, duration, function()
|
||||
|
|
|
@ -21,6 +21,7 @@
|
|||
*/
|
||||
|
||||
var padutils = require('./pad_utils').padutils;
|
||||
var hooks = require('./pluginfw/hooks');
|
||||
|
||||
var myUserInfo = {};
|
||||
|
||||
|
@ -529,6 +530,10 @@ var paduserlist = (function()
|
|||
return;
|
||||
}
|
||||
|
||||
hooks.callAll('userJoinOrUpdate', {
|
||||
userInfo: info
|
||||
});
|
||||
|
||||
var userData = {};
|
||||
userData.color = typeof info.colorId == "number" ? clientVars.colorPalette[info.colorId] : info.colorId;
|
||||
userData.name = info.name;
|
||||
|
|
|
@ -1,690 +0,0 @@
|
|||
/*global setTimeout: false, console: false */
|
||||
(function () {
|
||||
|
||||
var async = {};
|
||||
|
||||
// global on the server, window in the browser
|
||||
var root = this,
|
||||
previous_async = root.async;
|
||||
|
||||
if (typeof module !== 'undefined' && module.exports) {
|
||||
module.exports = async;
|
||||
}
|
||||
else {
|
||||
root.async = async;
|
||||
}
|
||||
|
||||
async.noConflict = function () {
|
||||
root.async = previous_async;
|
||||
return async;
|
||||
};
|
||||
|
||||
//// cross-browser compatiblity functions ////
|
||||
|
||||
var _forEach = function (arr, iterator) {
|
||||
if (arr.forEach) {
|
||||
return arr.forEach(iterator);
|
||||
}
|
||||
for (var i = 0; i < arr.length; i += 1) {
|
||||
iterator(arr[i], i, arr);
|
||||
}
|
||||
};
|
||||
|
||||
var _map = function (arr, iterator) {
|
||||
if (arr.map) {
|
||||
return arr.map(iterator);
|
||||
}
|
||||
var results = [];
|
||||
_forEach(arr, function (x, i, a) {
|
||||
results.push(iterator(x, i, a));
|
||||
});
|
||||
return results;
|
||||
};
|
||||
|
||||
var _reduce = function (arr, iterator, memo) {
|
||||
if (arr.reduce) {
|
||||
return arr.reduce(iterator, memo);
|
||||
}
|
||||
_forEach(arr, function (x, i, a) {
|
||||
memo = iterator(memo, x, i, a);
|
||||
});
|
||||
return memo;
|
||||
};
|
||||
|
||||
var _keys = function (obj) {
|
||||
if (Object.keys) {
|
||||
return Object.keys(obj);
|
||||
}
|
||||
var keys = [];
|
||||
for (var k in obj) {
|
||||
if (obj.hasOwnProperty(k)) {
|
||||
keys.push(k);
|
||||
}
|
||||
}
|
||||
return keys;
|
||||
};
|
||||
|
||||
var _indexOf = function (arr, item) {
|
||||
if (arr.indexOf) {
|
||||
return arr.indexOf(item);
|
||||
}
|
||||
for (var i = 0; i < arr.length; i += 1) {
|
||||
if (arr[i] === item) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
};
|
||||
|
||||
//// exported async module functions ////
|
||||
|
||||
//// nextTick implementation with browser-compatible fallback ////
|
||||
if (typeof process === 'undefined' || !(process.nextTick)) {
|
||||
async.nextTick = function (fn) {
|
||||
setTimeout(fn, 0);
|
||||
};
|
||||
}
|
||||
else {
|
||||
async.nextTick = process.nextTick;
|
||||
}
|
||||
|
||||
async.forEach = function (arr, iterator, callback) {
|
||||
if (!arr.length) {
|
||||
return callback();
|
||||
}
|
||||
var completed = 0;
|
||||
_forEach(arr, function (x) {
|
||||
iterator(x, function (err) {
|
||||
if (err) {
|
||||
callback(err);
|
||||
callback = function () {};
|
||||
}
|
||||
else {
|
||||
completed += 1;
|
||||
if (completed === arr.length) {
|
||||
callback();
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
async.forEachSeries = function (arr, iterator, callback) {
|
||||
if (!arr.length) {
|
||||
return callback();
|
||||
}
|
||||
var completed = 0;
|
||||
var iterate = function () {
|
||||
iterator(arr[completed], function (err) {
|
||||
if (err) {
|
||||
callback(err);
|
||||
callback = function () {};
|
||||
}
|
||||
else {
|
||||
completed += 1;
|
||||
if (completed === arr.length) {
|
||||
callback();
|
||||
}
|
||||
else {
|
||||
iterate();
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
iterate();
|
||||
};
|
||||
|
||||
async.forEachLimit = function (arr, limit, iterator, callback) {
|
||||
if (!arr.length || limit <= 0) {
|
||||
return callback();
|
||||
}
|
||||
var completed = 0;
|
||||
var started = 0;
|
||||
var running = 0;
|
||||
|
||||
(function replenish () {
|
||||
if (completed === arr.length) {
|
||||
return callback();
|
||||
}
|
||||
|
||||
while (running < limit && started < arr.length) {
|
||||
iterator(arr[started], function (err) {
|
||||
if (err) {
|
||||
callback(err);
|
||||
callback = function () {};
|
||||
}
|
||||
else {
|
||||
completed += 1;
|
||||
running -= 1;
|
||||
if (completed === arr.length) {
|
||||
callback();
|
||||
}
|
||||
else {
|
||||
replenish();
|
||||
}
|
||||
}
|
||||
});
|
||||
started += 1;
|
||||
running += 1;
|
||||
}
|
||||
})();
|
||||
};
|
||||
|
||||
|
||||
var doParallel = function (fn) {
|
||||
return function () {
|
||||
var args = Array.prototype.slice.call(arguments);
|
||||
return fn.apply(null, [async.forEach].concat(args));
|
||||
};
|
||||
};
|
||||
var doSeries = function (fn) {
|
||||
return function () {
|
||||
var args = Array.prototype.slice.call(arguments);
|
||||
return fn.apply(null, [async.forEachSeries].concat(args));
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
var _asyncMap = function (eachfn, arr, iterator, callback) {
|
||||
var results = [];
|
||||
arr = _map(arr, function (x, i) {
|
||||
return {index: i, value: x};
|
||||
});
|
||||
eachfn(arr, function (x, callback) {
|
||||
iterator(x.value, function (err, v) {
|
||||
results[x.index] = v;
|
||||
callback(err);
|
||||
});
|
||||
}, function (err) {
|
||||
callback(err, results);
|
||||
});
|
||||
};
|
||||
async.map = doParallel(_asyncMap);
|
||||
async.mapSeries = doSeries(_asyncMap);
|
||||
|
||||
|
||||
// reduce only has a series version, as doing reduce in parallel won't
|
||||
// work in many situations.
|
||||
async.reduce = function (arr, memo, iterator, callback) {
|
||||
async.forEachSeries(arr, function (x, callback) {
|
||||
iterator(memo, x, function (err, v) {
|
||||
memo = v;
|
||||
callback(err);
|
||||
});
|
||||
}, function (err) {
|
||||
callback(err, memo);
|
||||
});
|
||||
};
|
||||
// inject alias
|
||||
async.inject = async.reduce;
|
||||
// foldl alias
|
||||
async.foldl = async.reduce;
|
||||
|
||||
async.reduceRight = function (arr, memo, iterator, callback) {
|
||||
var reversed = _map(arr, function (x) {
|
||||
return x;
|
||||
}).reverse();
|
||||
async.reduce(reversed, memo, iterator, callback);
|
||||
};
|
||||
// foldr alias
|
||||
async.foldr = async.reduceRight;
|
||||
|
||||
var _filter = function (eachfn, arr, iterator, callback) {
|
||||
var results = [];
|
||||
arr = _map(arr, function (x, i) {
|
||||
return {index: i, value: x};
|
||||
});
|
||||
eachfn(arr, function (x, callback) {
|
||||
iterator(x.value, function (v) {
|
||||
if (v) {
|
||||
results.push(x);
|
||||
}
|
||||
callback();
|
||||
});
|
||||
}, function (err) {
|
||||
callback(_map(results.sort(function (a, b) {
|
||||
return a.index - b.index;
|
||||
}), function (x) {
|
||||
return x.value;
|
||||
}));
|
||||
});
|
||||
};
|
||||
async.filter = doParallel(_filter);
|
||||
async.filterSeries = doSeries(_filter);
|
||||
// select alias
|
||||
async.select = async.filter;
|
||||
async.selectSeries = async.filterSeries;
|
||||
|
||||
var _reject = function (eachfn, arr, iterator, callback) {
|
||||
var results = [];
|
||||
arr = _map(arr, function (x, i) {
|
||||
return {index: i, value: x};
|
||||
});
|
||||
eachfn(arr, function (x, callback) {
|
||||
iterator(x.value, function (v) {
|
||||
if (!v) {
|
||||
results.push(x);
|
||||
}
|
||||
callback();
|
||||
});
|
||||
}, function (err) {
|
||||
callback(_map(results.sort(function (a, b) {
|
||||
return a.index - b.index;
|
||||
}), function (x) {
|
||||
return x.value;
|
||||
}));
|
||||
});
|
||||
};
|
||||
async.reject = doParallel(_reject);
|
||||
async.rejectSeries = doSeries(_reject);
|
||||
|
||||
var _detect = function (eachfn, arr, iterator, main_callback) {
|
||||
eachfn(arr, function (x, callback) {
|
||||
iterator(x, function (result) {
|
||||
if (result) {
|
||||
main_callback(x);
|
||||
main_callback = function () {};
|
||||
}
|
||||
else {
|
||||
callback();
|
||||
}
|
||||
});
|
||||
}, function (err) {
|
||||
main_callback();
|
||||
});
|
||||
};
|
||||
async.detect = doParallel(_detect);
|
||||
async.detectSeries = doSeries(_detect);
|
||||
|
||||
async.some = function (arr, iterator, main_callback) {
|
||||
async.forEach(arr, function (x, callback) {
|
||||
iterator(x, function (v) {
|
||||
if (v) {
|
||||
main_callback(true);
|
||||
main_callback = function () {};
|
||||
}
|
||||
callback();
|
||||
});
|
||||
}, function (err) {
|
||||
main_callback(false);
|
||||
});
|
||||
};
|
||||
// any alias
|
||||
async.any = async.some;
|
||||
|
||||
async.every = function (arr, iterator, main_callback) {
|
||||
async.forEach(arr, function (x, callback) {
|
||||
iterator(x, function (v) {
|
||||
if (!v) {
|
||||
main_callback(false);
|
||||
main_callback = function () {};
|
||||
}
|
||||
callback();
|
||||
});
|
||||
}, function (err) {
|
||||
main_callback(true);
|
||||
});
|
||||
};
|
||||
// all alias
|
||||
async.all = async.every;
|
||||
|
||||
async.sortBy = function (arr, iterator, callback) {
|
||||
async.map(arr, function (x, callback) {
|
||||
iterator(x, function (err, criteria) {
|
||||
if (err) {
|
||||
callback(err);
|
||||
}
|
||||
else {
|
||||
callback(null, {value: x, criteria: criteria});
|
||||
}
|
||||
});
|
||||
}, function (err, results) {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
else {
|
||||
var fn = function (left, right) {
|
||||
var a = left.criteria, b = right.criteria;
|
||||
return a < b ? -1 : a > b ? 1 : 0;
|
||||
};
|
||||
callback(null, _map(results.sort(fn), function (x) {
|
||||
return x.value;
|
||||
}));
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
async.auto = function (tasks, callback) {
|
||||
callback = callback || function () {};
|
||||
var keys = _keys(tasks);
|
||||
if (!keys.length) {
|
||||
return callback(null);
|
||||
}
|
||||
|
||||
var results = {};
|
||||
|
||||
var listeners = [];
|
||||
var addListener = function (fn) {
|
||||
listeners.unshift(fn);
|
||||
};
|
||||
var removeListener = function (fn) {
|
||||
for (var i = 0; i < listeners.length; i += 1) {
|
||||
if (listeners[i] === fn) {
|
||||
listeners.splice(i, 1);
|
||||
return;
|
||||
}
|
||||
}
|
||||
};
|
||||
var taskComplete = function () {
|
||||
_forEach(listeners, function (fn) {
|
||||
fn();
|
||||
});
|
||||
};
|
||||
|
||||
addListener(function () {
|
||||
if (_keys(results).length === keys.length) {
|
||||
callback(null, results);
|
||||
}
|
||||
});
|
||||
|
||||
_forEach(keys, function (k) {
|
||||
var task = (tasks[k] instanceof Function) ? [tasks[k]]: tasks[k];
|
||||
var taskCallback = function (err) {
|
||||
if (err) {
|
||||
callback(err);
|
||||
// stop subsequent errors hitting callback multiple times
|
||||
callback = function () {};
|
||||
}
|
||||
else {
|
||||
var args = Array.prototype.slice.call(arguments, 1);
|
||||
if (args.length <= 1) {
|
||||
args = args[0];
|
||||
}
|
||||
results[k] = args;
|
||||
taskComplete();
|
||||
}
|
||||
};
|
||||
var requires = task.slice(0, Math.abs(task.length - 1)) || [];
|
||||
var ready = function () {
|
||||
return _reduce(requires, function (a, x) {
|
||||
return (a && results.hasOwnProperty(x));
|
||||
}, true);
|
||||
};
|
||||
if (ready()) {
|
||||
task[task.length - 1](taskCallback, results);
|
||||
}
|
||||
else {
|
||||
var listener = function () {
|
||||
if (ready()) {
|
||||
removeListener(listener);
|
||||
task[task.length - 1](taskCallback, results);
|
||||
}
|
||||
};
|
||||
addListener(listener);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
async.waterfall = function (tasks, callback) {
|
||||
if (!tasks.length) {
|
||||
return callback();
|
||||
}
|
||||
callback = callback || function () {};
|
||||
var wrapIterator = function (iterator) {
|
||||
return function (err) {
|
||||
if (err) {
|
||||
callback(err);
|
||||
callback = function () {};
|
||||
}
|
||||
else {
|
||||
var args = Array.prototype.slice.call(arguments, 1);
|
||||
var next = iterator.next();
|
||||
if (next) {
|
||||
args.push(wrapIterator(next));
|
||||
}
|
||||
else {
|
||||
args.push(callback);
|
||||
}
|
||||
async.nextTick(function () {
|
||||
iterator.apply(null, args);
|
||||
});
|
||||
}
|
||||
};
|
||||
};
|
||||
wrapIterator(async.iterator(tasks))();
|
||||
};
|
||||
|
||||
async.parallel = function (tasks, callback) {
|
||||
callback = callback || function () {};
|
||||
if (tasks.constructor === Array) {
|
||||
async.map(tasks, function (fn, callback) {
|
||||
if (fn) {
|
||||
fn(function (err) {
|
||||
var args = Array.prototype.slice.call(arguments, 1);
|
||||
if (args.length <= 1) {
|
||||
args = args[0];
|
||||
}
|
||||
callback.call(null, err, args);
|
||||
});
|
||||
}
|
||||
}, callback);
|
||||
}
|
||||
else {
|
||||
var results = {};
|
||||
async.forEach(_keys(tasks), function (k, callback) {
|
||||
tasks[k](function (err) {
|
||||
var args = Array.prototype.slice.call(arguments, 1);
|
||||
if (args.length <= 1) {
|
||||
args = args[0];
|
||||
}
|
||||
results[k] = args;
|
||||
callback(err);
|
||||
});
|
||||
}, function (err) {
|
||||
callback(err, results);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
async.series = function (tasks, callback) {
|
||||
callback = callback || function () {};
|
||||
if (tasks.constructor === Array) {
|
||||
async.mapSeries(tasks, function (fn, callback) {
|
||||
if (fn) {
|
||||
fn(function (err) {
|
||||
var args = Array.prototype.slice.call(arguments, 1);
|
||||
if (args.length <= 1) {
|
||||
args = args[0];
|
||||
}
|
||||
callback.call(null, err, args);
|
||||
});
|
||||
}
|
||||
}, callback);
|
||||
}
|
||||
else {
|
||||
var results = {};
|
||||
async.forEachSeries(_keys(tasks), function (k, callback) {
|
||||
tasks[k](function (err) {
|
||||
var args = Array.prototype.slice.call(arguments, 1);
|
||||
if (args.length <= 1) {
|
||||
args = args[0];
|
||||
}
|
||||
results[k] = args;
|
||||
callback(err);
|
||||
});
|
||||
}, function (err) {
|
||||
callback(err, results);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
async.iterator = function (tasks) {
|
||||
var makeCallback = function (index) {
|
||||
var fn = function () {
|
||||
if (tasks.length) {
|
||||
tasks[index].apply(null, arguments);
|
||||
}
|
||||
return fn.next();
|
||||
};
|
||||
fn.next = function () {
|
||||
return (index < tasks.length - 1) ? makeCallback(index + 1): null;
|
||||
};
|
||||
return fn;
|
||||
};
|
||||
return makeCallback(0);
|
||||
};
|
||||
|
||||
async.apply = function (fn) {
|
||||
var args = Array.prototype.slice.call(arguments, 1);
|
||||
return function () {
|
||||
return fn.apply(
|
||||
null, args.concat(Array.prototype.slice.call(arguments))
|
||||
);
|
||||
};
|
||||
};
|
||||
|
||||
var _concat = function (eachfn, arr, fn, callback) {
|
||||
var r = [];
|
||||
eachfn(arr, function (x, cb) {
|
||||
fn(x, function (err, y) {
|
||||
r = r.concat(y || []);
|
||||
cb(err);
|
||||
});
|
||||
}, function (err) {
|
||||
callback(err, r);
|
||||
});
|
||||
};
|
||||
async.concat = doParallel(_concat);
|
||||
async.concatSeries = doSeries(_concat);
|
||||
|
||||
async.whilst = function (test, iterator, callback) {
|
||||
if (test()) {
|
||||
iterator(function (err) {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
async.whilst(test, iterator, callback);
|
||||
});
|
||||
}
|
||||
else {
|
||||
callback();
|
||||
}
|
||||
};
|
||||
|
||||
async.until = function (test, iterator, callback) {
|
||||
if (!test()) {
|
||||
iterator(function (err) {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
async.until(test, iterator, callback);
|
||||
});
|
||||
}
|
||||
else {
|
||||
callback();
|
||||
}
|
||||
};
|
||||
|
||||
async.queue = function (worker, concurrency) {
|
||||
var workers = 0;
|
||||
var q = {
|
||||
tasks: [],
|
||||
concurrency: concurrency,
|
||||
saturated: null,
|
||||
empty: null,
|
||||
drain: null,
|
||||
push: function (data, callback) {
|
||||
q.tasks.push({data: data, callback: callback});
|
||||
if(q.saturated && q.tasks.length == concurrency) q.saturated();
|
||||
async.nextTick(q.process);
|
||||
},
|
||||
process: function () {
|
||||
if (workers < q.concurrency && q.tasks.length) {
|
||||
var task = q.tasks.shift();
|
||||
if(q.empty && q.tasks.length == 0) q.empty();
|
||||
workers += 1;
|
||||
worker(task.data, function () {
|
||||
workers -= 1;
|
||||
if (task.callback) {
|
||||
task.callback.apply(task, arguments);
|
||||
}
|
||||
if(q.drain && q.tasks.length + workers == 0) q.drain();
|
||||
q.process();
|
||||
});
|
||||
}
|
||||
},
|
||||
length: function () {
|
||||
return q.tasks.length;
|
||||
},
|
||||
running: function () {
|
||||
return workers;
|
||||
}
|
||||
};
|
||||
return q;
|
||||
};
|
||||
|
||||
var _console_fn = function (name) {
|
||||
return function (fn) {
|
||||
var args = Array.prototype.slice.call(arguments, 1);
|
||||
fn.apply(null, args.concat([function (err) {
|
||||
var args = Array.prototype.slice.call(arguments, 1);
|
||||
if (typeof console !== 'undefined') {
|
||||
if (err) {
|
||||
if (console.error) {
|
||||
console.error(err);
|
||||
}
|
||||
}
|
||||
else if (console[name]) {
|
||||
_forEach(args, function (x) {
|
||||
console[name](x);
|
||||
});
|
||||
}
|
||||
}
|
||||
}]));
|
||||
};
|
||||
};
|
||||
async.log = _console_fn('log');
|
||||
async.dir = _console_fn('dir');
|
||||
/*async.info = _console_fn('info');
|
||||
async.warn = _console_fn('warn');
|
||||
async.error = _console_fn('error');*/
|
||||
|
||||
async.memoize = function (fn, hasher) {
|
||||
var memo = {};
|
||||
var queues = {};
|
||||
hasher = hasher || function (x) {
|
||||
return x;
|
||||
};
|
||||
var memoized = function () {
|
||||
var args = Array.prototype.slice.call(arguments);
|
||||
var callback = args.pop();
|
||||
var key = hasher.apply(null, args);
|
||||
if (key in memo) {
|
||||
callback.apply(null, memo[key]);
|
||||
}
|
||||
else if (key in queues) {
|
||||
queues[key].push(callback);
|
||||
}
|
||||
else {
|
||||
queues[key] = [callback];
|
||||
fn.apply(null, args.concat([function () {
|
||||
memo[key] = arguments;
|
||||
var q = queues[key];
|
||||
delete queues[key];
|
||||
for (var i = 0, l = q.length; i < l; i++) {
|
||||
q[i].apply(null, arguments);
|
||||
}
|
||||
}]));
|
||||
}
|
||||
};
|
||||
memoized.unmemoized = fn;
|
||||
return memoized;
|
||||
};
|
||||
|
||||
async.unmemoize = function (fn) {
|
||||
return function () {
|
||||
return (fn.unmemoized || fn).apply(null, arguments);
|
||||
}
|
||||
};
|
||||
|
||||
}());
|
|
@ -0,0 +1,70 @@
|
|||
var $, jQuery;
|
||||
$ = jQuery = require("ep_etherpad-lite/static/js/rjquery").$;
|
||||
var _ = require("underscore");
|
||||
|
||||
var pluginUtils = require('./shared');
|
||||
|
||||
exports.loaded = false;
|
||||
exports.plugins = {};
|
||||
exports.parts = [];
|
||||
exports.hooks = {};
|
||||
exports.baseURL = '';
|
||||
|
||||
exports.ensure = function (cb) {
|
||||
if (!exports.loaded)
|
||||
exports.update(cb);
|
||||
else
|
||||
cb();
|
||||
};
|
||||
|
||||
exports.update = function (cb) {
|
||||
// It appears that this response (see #620) may interrupt the current thread
|
||||
// of execution on Firefox. This schedules the response in the run-loop,
|
||||
// which appears to fix the issue.
|
||||
var callback = function () {setTimeout(cb, 0);};
|
||||
|
||||
jQuery.getJSON(exports.baseURL + 'pluginfw/plugin-definitions.json', function(data) {
|
||||
exports.plugins = data.plugins;
|
||||
exports.parts = data.parts;
|
||||
exports.hooks = pluginUtils.extractHooks(exports.parts, "client_hooks");
|
||||
exports.loaded = true;
|
||||
callback();
|
||||
}).error(function(xhr, s, err){
|
||||
console.error("Failed to load plugin-definitions: " + err);
|
||||
callback();
|
||||
});
|
||||
};
|
||||
|
||||
function adoptPlugins(plugins) {
|
||||
var keys = [
|
||||
'loaded', 'plugins', 'parts', 'hooks', 'baseURL', 'ensure', 'update'];
|
||||
|
||||
for (var i = 0, ii = keys.length; i < ii; i++) {
|
||||
var key = keys[i];
|
||||
exports[key] = plugins[key];
|
||||
}
|
||||
}
|
||||
|
||||
function adoptPluginsFromAncestorsOf(frame) {
|
||||
// Bind plugins with parent;
|
||||
var parentRequire = null;
|
||||
try {
|
||||
while (frame = frame.parent) {
|
||||
if (typeof (frame.require) !== "undefined") {
|
||||
parentRequire = frame.require;
|
||||
break;
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
// Silence (this can only be a XDomain issue).
|
||||
}
|
||||
if (parentRequire) {
|
||||
var ancestorPlugins = parentRequire("ep_etherpad-lite/static/js/pluginfw/client_plugins");
|
||||
exports.adoptPlugins(ancestorPlugins);
|
||||
} else {
|
||||
throw new Error("Parent plugins could not be found.")
|
||||
}
|
||||
}
|
||||
|
||||
exports.adoptPlugins = adoptPlugins;
|
||||
exports.adoptPluginsFromAncestorsOf = adoptPluginsFromAncestorsOf;
|
|
@ -1,14 +1,5 @@
|
|||
var plugins = require("ep_etherpad-lite/static/js/pluginfw/plugins");
|
||||
var _;
|
||||
|
||||
/* FIXME: Ugly hack, in the future, use same code for server & client */
|
||||
if (plugins.isClient) {
|
||||
var async = require("ep_etherpad-lite/static/js/pluginfw/async");
|
||||
var _ = require("ep_etherpad-lite/static/js/underscore");
|
||||
} else {
|
||||
var async = require("async");
|
||||
var _ = require("underscore");
|
||||
}
|
||||
var async = require("async");
|
||||
var _ = require("underscore");
|
||||
|
||||
exports.bubbleExceptions = true
|
||||
|
||||
|
@ -79,8 +70,8 @@ exports.flatten = function (lst) {
|
|||
|
||||
exports.callAll = function (hook_name, args) {
|
||||
if (!args) args = {};
|
||||
if (plugins.hooks[hook_name] === undefined) return [];
|
||||
return _.flatten(_.map(plugins.hooks[hook_name], function (hook) {
|
||||
if (exports.plugins.hooks[hook_name] === undefined) return [];
|
||||
return _.flatten(_.map(exports.plugins.hooks[hook_name], function (hook) {
|
||||
return hookCallWrapper(hook, hook_name, args);
|
||||
}), true);
|
||||
}
|
||||
|
@ -88,9 +79,9 @@ exports.callAll = function (hook_name, args) {
|
|||
exports.aCallAll = function (hook_name, args, cb) {
|
||||
if (!args) args = {};
|
||||
if (!cb) cb = function () {};
|
||||
if (plugins.hooks[hook_name] === undefined) return cb(null, []);
|
||||
if (exports.plugins.hooks[hook_name] === undefined) return cb(null, []);
|
||||
async.map(
|
||||
plugins.hooks[hook_name],
|
||||
exports.plugins.hooks[hook_name],
|
||||
function (hook, cb) {
|
||||
hookCallWrapper(hook, hook_name, args, function (res) { cb(null, res); });
|
||||
},
|
||||
|
@ -102,8 +93,8 @@ exports.aCallAll = function (hook_name, args, cb) {
|
|||
|
||||
exports.callFirst = function (hook_name, args) {
|
||||
if (!args) args = {};
|
||||
if (plugins.hooks[hook_name] === undefined) return [];
|
||||
return exports.syncMapFirst(plugins.hooks[hook_name], function (hook) {
|
||||
if (exports.plugins.hooks[hook_name] === undefined) return [];
|
||||
return exports.syncMapFirst(exports.plugins.hooks[hook_name], function (hook) {
|
||||
return hookCallWrapper(hook, hook_name, args);
|
||||
});
|
||||
}
|
||||
|
@ -111,9 +102,9 @@ exports.callFirst = function (hook_name, args) {
|
|||
exports.aCallFirst = function (hook_name, args, cb) {
|
||||
if (!args) args = {};
|
||||
if (!cb) cb = function () {};
|
||||
if (plugins.hooks[hook_name] === undefined) return cb(null, []);
|
||||
if (exports.plugins.hooks[hook_name] === undefined) return cb(null, []);
|
||||
exports.mapFirst(
|
||||
plugins.hooks[hook_name],
|
||||
exports.plugins.hooks[hook_name],
|
||||
function (hook, cb) {
|
||||
hookCallWrapper(hook, hook_name, args, function (res) { cb(null, res); });
|
||||
},
|
||||
|
|
|
@ -1,39 +0,0 @@
|
|||
/**
|
||||
* This module allows passing require modules instances to
|
||||
* embedded iframes in a page.
|
||||
* For example, if a page has the "plugins" module initialized,
|
||||
* it is important to use exactly the same "plugins" instance
|
||||
* inside iframes as well. Otherwise, plugins cannot save any
|
||||
* state.
|
||||
*/
|
||||
|
||||
|
||||
/**
|
||||
* Instructs the require object that when a reqModuleName module
|
||||
* needs to be loaded, that it iterates through the parents of the
|
||||
* current window until it finds one who can execute "require"
|
||||
* statements and asks it to perform require on reqModuleName.
|
||||
*
|
||||
* @params requireDefObj Require object which supports define
|
||||
* statements. This object is accessible after loading require-kernel.
|
||||
* @params reqModuleName Module name e.g. (ep_etherpad-lite/static/js/plugins)
|
||||
*/
|
||||
exports.getRequirementFromParent = function(requireDefObj, reqModuleName) {
|
||||
// Force the 'undefinition' of the modules (if they already have been loaded).
|
||||
delete (requireDefObj._definitions)[reqModuleName];
|
||||
delete (requireDefObj._modules)[reqModuleName];
|
||||
requireDefObj.define(reqModuleName, function(require, exports, module) {
|
||||
var t = parent;
|
||||
var max = 0; // make sure I don't go up more than 10 times
|
||||
while (typeof(t) != "undefined") {
|
||||
max++;
|
||||
if (max==10)
|
||||
break;
|
||||
if (typeof(t.require) != "undefined") {
|
||||
module.exports = t.require(reqModuleName);
|
||||
return;
|
||||
}
|
||||
t = t.parent;
|
||||
}
|
||||
});
|
||||
}
|
|
@ -1,30 +1,21 @@
|
|||
exports.isClient = typeof global != "object";
|
||||
var npm = require("npm/lib/npm.js");
|
||||
var readInstalled = require("./read-installed.js");
|
||||
var relativize = require("npm/lib/utils/relativize.js");
|
||||
var readJson = require("npm/lib/utils/read-json.js");
|
||||
var path = require("path");
|
||||
var async = require("async");
|
||||
var fs = require("fs");
|
||||
var tsort = require("./tsort");
|
||||
var util = require("util");
|
||||
var _ = require("underscore");
|
||||
|
||||
var _;
|
||||
|
||||
if (!exports.isClient) {
|
||||
var npm = require("npm/lib/npm.js");
|
||||
var readInstalled = require("./read-installed.js");
|
||||
var relativize = require("npm/lib/utils/relativize.js");
|
||||
var readJson = require("npm/lib/utils/read-json.js");
|
||||
var path = require("path");
|
||||
var async = require("async");
|
||||
var fs = require("fs");
|
||||
var tsort = require("./tsort");
|
||||
var util = require("util");
|
||||
_ = require("underscore");
|
||||
}else{
|
||||
var $, jQuery;
|
||||
$ = jQuery = require("ep_etherpad-lite/static/js/rjquery").$;
|
||||
_ = require("ep_etherpad-lite/static/js/underscore");
|
||||
}
|
||||
var pluginUtils = require('./shared');
|
||||
|
||||
exports.prefix = 'ep_';
|
||||
exports.loaded = false;
|
||||
exports.plugins = {};
|
||||
exports.parts = [];
|
||||
exports.hooks = {};
|
||||
exports.baseURL = '';
|
||||
|
||||
exports.ensure = function (cb) {
|
||||
if (!exports.loaded)
|
||||
|
@ -43,7 +34,7 @@ exports.formatParts = function () {
|
|||
|
||||
exports.formatHooks = function (hook_set_name) {
|
||||
var res = [];
|
||||
var hooks = exports.extractHooks(exports.parts, hook_set_name || "hooks");
|
||||
var hooks = pluginUtils.extractHooks(exports.parts, hook_set_name || "hooks");
|
||||
|
||||
_.chain(hooks).keys().forEach(function (hook_name) {
|
||||
_.forEach(hooks[hook_name], function (hook) {
|
||||
|
@ -53,84 +44,6 @@ exports.formatHooks = function (hook_set_name) {
|
|||
return "<dl>" + res.join("\n") + "</dl>";
|
||||
};
|
||||
|
||||
exports.loadFn = function (path, hookName) {
|
||||
var functionName
|
||||
, parts = path.split(":");
|
||||
|
||||
// on windows: C:\foo\bar:xyz
|
||||
if(parts[0].length == 1) {
|
||||
if(parts.length == 3)
|
||||
functionName = parts.pop();
|
||||
path = parts.join(":");
|
||||
}else{
|
||||
path = parts[0];
|
||||
functionName = parts[1];
|
||||
}
|
||||
|
||||
var fn = require(path);
|
||||
functionName = functionName ? functionName : hookName;
|
||||
|
||||
_.each(functionName.split("."), function (name) {
|
||||
fn = fn[name];
|
||||
});
|
||||
return fn;
|
||||
};
|
||||
|
||||
exports.extractHooks = function (parts, hook_set_name) {
|
||||
var hooks = {};
|
||||
_.each(parts,function (part) {
|
||||
_.chain(part[hook_set_name] || {})
|
||||
.keys()
|
||||
.each(function (hook_name) {
|
||||
if (hooks[hook_name] === undefined) hooks[hook_name] = [];
|
||||
|
||||
var hook_fn_name = part[hook_set_name][hook_name];
|
||||
|
||||
/* On the server side, you can't just
|
||||
* require("pluginname/whatever") if the plugin is installed as
|
||||
* a dependency of another plugin! Bah, pesky little details of
|
||||
* npm... */
|
||||
if (!exports.isClient) {
|
||||
hook_fn_name = path.normalize(path.join(path.dirname(exports.plugins[part.plugin].package.path), hook_fn_name));
|
||||
}
|
||||
|
||||
try {
|
||||
var hook_fn = exports.loadFn(hook_fn_name, hook_name);
|
||||
if (!hook_fn) {
|
||||
throw "Not a function";
|
||||
}
|
||||
} catch (exc) {
|
||||
console.error("Failed to load '" + hook_fn_name + "' for '" + part.full_name + "/" + hook_set_name + "/" + hook_name + "': " + exc.toString())
|
||||
}
|
||||
if (hook_fn) {
|
||||
hooks[hook_name].push({"hook_name": hook_name, "hook_fn": hook_fn, "hook_fn_name": hook_fn_name, "part": part});
|
||||
}
|
||||
});
|
||||
});
|
||||
return hooks;
|
||||
};
|
||||
|
||||
|
||||
if (exports.isClient) {
|
||||
exports.update = function (cb) {
|
||||
// It appears that this response (see #620) may interrupt the current thread
|
||||
// of execution on Firefox. This schedules the response in the run-loop,
|
||||
// which appears to fix the issue.
|
||||
var callback = function () {setTimeout(cb, 0);};
|
||||
|
||||
jQuery.getJSON(exports.baseURL + 'pluginfw/plugin-definitions.json', function(data) {
|
||||
exports.plugins = data.plugins;
|
||||
exports.parts = data.parts;
|
||||
exports.hooks = exports.extractHooks(exports.parts, "client_hooks");
|
||||
exports.loaded = true;
|
||||
callback();
|
||||
}).error(function(xhr, s, err){
|
||||
console.error("Failed to load plugin-definitions: " + err);
|
||||
callback();
|
||||
});
|
||||
};
|
||||
} else {
|
||||
|
||||
exports.callInit = function (cb) {
|
||||
var hooks = require("./hooks");
|
||||
async.map(
|
||||
|
@ -153,6 +66,10 @@ exports.callInit = function (cb) {
|
|||
);
|
||||
}
|
||||
|
||||
exports.pathNormalization = function (part, hook_fn_name) {
|
||||
return path.normalize(path.join(path.dirname(exports.plugins[part.plugin].package.path), hook_fn_name));
|
||||
}
|
||||
|
||||
exports.update = function (cb) {
|
||||
exports.getPackages(function (er, packages) {
|
||||
var parts = [];
|
||||
|
@ -161,14 +78,14 @@ exports.update = function (cb) {
|
|||
async.forEach(
|
||||
Object.keys(packages),
|
||||
function (plugin_name, cb) {
|
||||
exports.loadPlugin(packages, plugin_name, plugins, parts, cb);
|
||||
loadPlugin(packages, plugin_name, plugins, parts, cb);
|
||||
},
|
||||
function (err) {
|
||||
if (err) cb(err);
|
||||
exports.plugins = plugins;
|
||||
exports.parts = exports.sortParts(parts);
|
||||
exports.hooks = exports.extractHooks(exports.parts, "hooks");
|
||||
exports.loaded = true;
|
||||
exports.plugins = plugins;
|
||||
exports.parts = sortParts(parts);
|
||||
exports.hooks = pluginUtils.extractHooks(exports.parts, "hooks", exports.pathNormalization);
|
||||
exports.loaded = true;
|
||||
exports.callInit(cb);
|
||||
}
|
||||
);
|
||||
|
@ -200,9 +117,9 @@ exports.getPackages = function (cb) {
|
|||
flatten(tmp);
|
||||
cb(null, packages);
|
||||
});
|
||||
};
|
||||
};
|
||||
|
||||
exports.loadPlugin = function (packages, plugin_name, plugins, parts, cb) {
|
||||
function loadPlugin(packages, plugin_name, plugins, parts, cb) {
|
||||
var plugin_path = path.resolve(packages[plugin_name].path, "ep.json");
|
||||
fs.readFile(
|
||||
plugin_path,
|
||||
|
@ -226,9 +143,9 @@ exports.getPackages = function (cb) {
|
|||
cb();
|
||||
}
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
exports.partsToParentChildList = function (parts) {
|
||||
function partsToParentChildList(parts) {
|
||||
var res = [];
|
||||
_.chain(parts).keys().forEach(function (name) {
|
||||
_.each(parts[name].post || [], function (child_name) {
|
||||
|
@ -242,18 +159,15 @@ exports.partsToParentChildList = function (parts) {
|
|||
}
|
||||
});
|
||||
return res;
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
// Used only in Node, so no need for _
|
||||
exports.sortParts = function(parts) {
|
||||
function sortParts(parts) {
|
||||
return tsort(
|
||||
exports.partsToParentChildList(parts)
|
||||
partsToParentChildList(parts)
|
||||
).filter(
|
||||
function (name) { return parts[name] !== undefined; }
|
||||
).map(
|
||||
function (name) { return parts[name]; }
|
||||
);
|
||||
};
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,61 @@
|
|||
var _ = require("underscore");
|
||||
|
||||
function loadFn(path, hookName) {
|
||||
var functionName
|
||||
, parts = path.split(":");
|
||||
|
||||
// on windows: C:\foo\bar:xyz
|
||||
if (parts[0].length == 1) {
|
||||
if (parts.length == 3) {
|
||||
functionName = parts.pop();
|
||||
}
|
||||
path = parts.join(":");
|
||||
} else {
|
||||
path = parts[0];
|
||||
functionName = parts[1];
|
||||
}
|
||||
|
||||
var fn = require(path);
|
||||
functionName = functionName ? functionName : hookName;
|
||||
|
||||
_.each(functionName.split("."), function (name) {
|
||||
fn = fn[name];
|
||||
});
|
||||
return fn;
|
||||
};
|
||||
|
||||
function extractHooks(parts, hook_set_name, normalizer) {
|
||||
var hooks = {};
|
||||
_.each(parts,function (part) {
|
||||
_.chain(part[hook_set_name] || {})
|
||||
.keys()
|
||||
.each(function (hook_name) {
|
||||
if (hooks[hook_name] === undefined) hooks[hook_name] = [];
|
||||
|
||||
var hook_fn_name = part[hook_set_name][hook_name];
|
||||
|
||||
/* On the server side, you can't just
|
||||
* require("pluginname/whatever") if the plugin is installed as
|
||||
* a dependency of another plugin! Bah, pesky little details of
|
||||
* npm... */
|
||||
if (normalizer) {
|
||||
hook_fn_name = normalizer(part, hook_fn_name);
|
||||
}
|
||||
|
||||
try {
|
||||
var hook_fn = loadFn(hook_fn_name, hook_name);
|
||||
if (!hook_fn) {
|
||||
throw "Not a function";
|
||||
}
|
||||
} catch (exc) {
|
||||
console.error("Failed to load '" + hook_fn_name + "' for '" + part.full_name + "/" + hook_set_name + "/" + hook_name + "': " + exc.toString())
|
||||
}
|
||||
if (hook_fn) {
|
||||
hooks[hook_name].push({"hook_name": hook_name, "hook_fn": hook_fn, "hook_fn_name": hook_fn_name, "part": part});
|
||||
}
|
||||
});
|
||||
});
|
||||
return hooks;
|
||||
};
|
||||
|
||||
exports.extractHooks = extractHooks;
|
|
@ -14,41 +14,4 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
var HTML_ENTITY_MAP = {
|
||||
'&': '&'
|
||||
, '<': '<'
|
||||
, '>': '>'
|
||||
, '"': '"'
|
||||
, "'": '''
|
||||
, '/': '/'
|
||||
};
|
||||
|
||||
// OSWASP Guidlines: &, <, >, ", ' plus forward slash.
|
||||
var HTML_CHARACTERS_EXPRESSION = /[&"'<>\/]/g;
|
||||
function escapeHTML(text) {
|
||||
return text && text.replace(HTML_CHARACTERS_EXPRESSION, function (c) {
|
||||
return HTML_ENTITY_MAP[c] || c;
|
||||
});
|
||||
}
|
||||
|
||||
// OSWASP Guidlines: escape all non alphanumeric characters in ASCII space.
|
||||
var HTML_ATTRIBUTE_CHARACTERS_EXPRESSION =
|
||||
/[\x00-\x2F\x3A-\x40\5B-\x60\x7B-\xFF]/g;
|
||||
function escapeHTMLAttribute(text) {
|
||||
return text && text.replace(HTML_ATTRIBUTE_CHARACTERS_EXPRESSION, function (c) {
|
||||
return "&#x" + ('00' + c.charCodeAt(0).toString(16)).slice(-2) + ";";
|
||||
});
|
||||
};
|
||||
|
||||
// OSWASP Guidlines: escape all non alphanumeric characters in ASCII space.
|
||||
var JAVASCRIPT_CHARACTERS_EXPRESSION =
|
||||
/[\x00-\x2F\x3A-\x40\5B-\x60\x7B-\xFF]/g;
|
||||
function escapeJavaScriptData(text) {
|
||||
return text && text.replace(JAVASCRIPT_CHARACTERS_EXPRESSION, function (c) {
|
||||
return "\\x" + ('00' + c.charCodeAt(0).toString(16)).slice(-2);
|
||||
});
|
||||
}
|
||||
|
||||
exports.escapeHTML = escapeHTML;
|
||||
exports.escapeHTMLAttribute = escapeHTMLAttribute;
|
||||
exports.escapeJavaScriptData = escapeJavaScriptData;
|
||||
module.exports = require('security');
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -21,7 +21,7 @@
|
|||
|
||||
<h1>Etherpad Lite</h1>
|
||||
|
||||
<a href="/admin/plugins/info">Technical information on installed plugins</a>
|
||||
<a href="plugins/info">Technical information on installed plugins</a>
|
||||
|
||||
<div class="separator"></div>
|
||||
<h2>Installed plugins</h2>
|
||||
|
|
|
@ -22,6 +22,9 @@
|
|||
|
||||
<% e.begin_block("body"); %>
|
||||
<div id="editbar" class="toolbar">
|
||||
<div id="overlay">
|
||||
<div id="overlay-inner"></div>
|
||||
</div>
|
||||
<ul class="menu_left">
|
||||
<% e.begin_block("editbarMenuLeft"); %>
|
||||
<li class="acl-write" id="bold" data-key="bold">
|
||||
|
@ -86,9 +89,14 @@
|
|||
</ul>
|
||||
<ul class="menu_right">
|
||||
<% e.begin_block("editbarMenuRight"); %>
|
||||
<li class="acl-write" data-key="settings">
|
||||
<a class="grouped-left" id="settingslink" title="Settings of this pad">
|
||||
<span class="buttonicon buttonicon-settings"></span>
|
||||
<li data-key="import_export">
|
||||
<a class="grouped-left" id="importexportlink" title="Import/Export from/to different document formats">
|
||||
<span class="buttonicon buttonicon-import_export"></span>
|
||||
</a>
|
||||
</li>
|
||||
<li onClick="document.location = document.location.pathname+ '/timeslider'">
|
||||
<a id="timesliderlink" class="grouped-middle" title="Show the history of this pad">
|
||||
<span class="buttonicon buttonicon-history"></span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="acl-write" data-key="savedRevision">
|
||||
|
@ -97,9 +105,9 @@
|
|||
</a>
|
||||
</li>
|
||||
<li class="acl-write separator"></li>
|
||||
<li data-key="import_export">
|
||||
<a class="grouped-left" id="importexportlink" title="Import/Export from/to different document formats">
|
||||
<span class="buttonicon buttonicon-import_export"></span>
|
||||
<li class="acl-write" data-key="settings">
|
||||
<a class="grouped-left" id="settingslink" title="Settings of this pad">
|
||||
<span class="buttonicon buttonicon-settings"></span>
|
||||
</a>
|
||||
</li>
|
||||
<li data-key="embed">
|
||||
|
@ -108,11 +116,6 @@
|
|||
</a>
|
||||
</li>
|
||||
<li class="separator"></li>
|
||||
<li onClick="document.location = document.location.pathname+ '/timeslider'">
|
||||
<a id="timesliderlink" title="Show the history of this pad">
|
||||
<span class="buttonicon buttonicon-history"></span>
|
||||
</a>
|
||||
</li>
|
||||
<li id="usericon" data-key="showusers">
|
||||
<a title="Show connected users">
|
||||
<span class="buttonicon buttonicon-showusers"></span>
|
||||
|
@ -148,7 +151,10 @@
|
|||
|
||||
<div id="editorcontainerbox">
|
||||
<div id="editorcontainer"></div>
|
||||
<div id="editorloadingbox">Loading...</div>
|
||||
<div id="editorloadingbox">
|
||||
<p>Loading...</p>
|
||||
<noscript><strong>Sorry, you have to enable Javascript in order to use this.</strong></noscript>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="settings" class="popup">
|
||||
|
@ -219,11 +225,65 @@
|
|||
<% e.end_block(); %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="connectivity" class="popup">
|
||||
<% e.begin_block("modals"); %>
|
||||
<div class="connected visible">
|
||||
<h2>Connected.</h2>
|
||||
</div>
|
||||
<div class="reconnecting">
|
||||
<h1>Reestablishing connection...</h1>
|
||||
<p><img alt="" border="0" src="/static/img/connectingbar.gif" /></p>
|
||||
</div>
|
||||
<div class="userdup">
|
||||
<h1>Opened in another window.</h1>
|
||||
<h2>You seem to have opened this pad in another browser window.</h2>
|
||||
<p>If you'd like to use this window instead, you can reconnect.</p>
|
||||
<button id="forcereconnect">Reconnect Now</button>
|
||||
</div>
|
||||
<div class="unauth">
|
||||
<h1>No Authorization.</h1>
|
||||
<p>Your browser's credentials or permissions have changed while viewing this pad. Try reconnecting.</p>
|
||||
<button id="forcereconnect">Reconnect Now</button>
|
||||
</div>
|
||||
<div class="looping">
|
||||
<h1>Disconnected.</h1>
|
||||
<h2>We're having trouble talking to the EtherPad lite synchronization server.</h2>
|
||||
<p>You may be connecting through an incompatible firewall or proxy server.</p>
|
||||
</div>
|
||||
<div class="initsocketfail">
|
||||
<h1>Disconnected.</h1>
|
||||
<h2>We were unable to connect to the EtherPad lite synchronization server.</h2>
|
||||
<p>This may be due to an incompatibility with your web browser or internet connection.</p>
|
||||
</div>
|
||||
<div class="slowcommit">
|
||||
<h1>Disconnected.</h1>
|
||||
<h2>Server not responding.</h2>
|
||||
<p>This may be due to network connectivity issues or high load on the server.</p>
|
||||
<button id="forcereconnect">Reconnect Now</button>
|
||||
</div>
|
||||
<div class="deleted">
|
||||
<h1>Disconnected.</h1>
|
||||
<p>This pad was deleted.</p>
|
||||
</div>
|
||||
<div class="disconnected">
|
||||
<h1>Disconnected.</h1>
|
||||
<h2>Lost connection with the EtherPad lite synchronization server.</h2>
|
||||
<p>This may be due to a loss of network connectivity. If this continues to happen, please let us know</p>
|
||||
<button id="forcereconnect">Reconnect Now</button>
|
||||
</div>
|
||||
<form id="reconnectform" method="post" action="/ep/pad/reconnect" accept-charset="UTF-8" style="display: none;">
|
||||
<input type="hidden" class="padId" name="padId">
|
||||
<input type="hidden" class="diagnosticInfo" name="diagnosticInfo">
|
||||
<input type="hidden" class="missedChanges" name="missedChanges">
|
||||
</form>
|
||||
<% e.end_block(); %>
|
||||
</div>
|
||||
|
||||
<div id="embed" class="popup">
|
||||
<% e.begin_block("embedPopup"); %>
|
||||
<div id="embedreadonly" class="right acl-write">
|
||||
<input type="checkbox" id="readonlyinput" onClick="padeditbar.setEmbedLinks();">
|
||||
<input type="checkbox" id="readonlyinput">
|
||||
<label for="readonlyinput">Read only</label>
|
||||
</div>
|
||||
<h1>Share this pad</h1>
|
||||
|
@ -259,60 +319,26 @@
|
|||
|
||||
<div id="focusprotector"> </div>
|
||||
|
||||
<div id="modaloverlay">
|
||||
<div id="modaloverlay-inner"></div>
|
||||
</div>
|
||||
|
||||
<div id="mainmodals">
|
||||
<% e.begin_block("modals"); %>
|
||||
<div id="connectionbox" class="modaldialog">
|
||||
<div id="connectionboxinner" class="modaldialog-inner">
|
||||
<div class="connecting">Connecting...</div>
|
||||
<div class="reconnecting">Reestablishing connection...</div>
|
||||
<div class="disconnected">
|
||||
<h2 class="h2_disconnect">Disconnected.</h2>
|
||||
<h2 class="h2_userdup">Opened in another window.</h2>
|
||||
<h2 class="h2_unauth">No Authorization.</h2>
|
||||
<div id="disconnected_looping">
|
||||
<p><b>We're having trouble talking to the EtherPad lite synchronization server.</b> You may be connecting through an incompatible firewall or proxy server.</p>
|
||||
</div>
|
||||
<div id="disconnected_initsocketfail">
|
||||
<p><b>We were unable to connect to the EtherPad lite synchronization server.</b> This may be due to an incompatibility with your web browser or internet connection.</p>
|
||||
</div>
|
||||
<div id="disconnected_userdup">
|
||||
<p><b>You seem to have opened this pad in another browser window.</b> If you'd like to use this window instead, you can reconnect.</p>
|
||||
</div>
|
||||
<div id="disconnected_unknown">
|
||||
<p><b>Lost connection with the EtherPad lite synchronization server.</b> This may be due to a loss of network connectivity.</p>
|
||||
</div>
|
||||
<div id="disconnected_slowcommit">
|
||||
<p><b>Server not responding.</b> This may be due to network connectivity issues or high load on the server.</p>
|
||||
</div>
|
||||
<div id="disconnected_unauth">
|
||||
<p>Your browser's credentials or permissions have changed while viewing this pad. Try reconnecting.</p>
|
||||
</div>
|
||||
<div id="disconnected_deleted">
|
||||
<p>This pad was deleted.</p>
|
||||
</div>
|
||||
<div id="reconnect_advise">
|
||||
<p>If this continues to happen, please let us know</p>
|
||||
</div>
|
||||
<div id="reconnect_form">
|
||||
<button id="forcereconnect">Reconnect Now</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<form id="reconnectform" method="post" action="/ep/pad/reconnect" accept-charset="UTF-8" style="display: none;">
|
||||
<input type="hidden" class="padId" name="padId">
|
||||
<input type="hidden" class="diagnosticInfo" name="diagnosticInfo">
|
||||
<input type="hidden" class="missedChanges" name="missedChanges">
|
||||
</form>
|
||||
</div>
|
||||
<% e.end_block(); %>
|
||||
</div>
|
||||
<% e.end_block(); %>
|
||||
|
||||
<% e.begin_block("scripts"); %>
|
||||
<script type="text/javascript">
|
||||
/* Display errors on page load to the user
|
||||
(Gets overridden by padutils.setupGlobalExceptionHandler)
|
||||
*/
|
||||
(function() {
|
||||
var originalHandler = window.onerror;
|
||||
window.onerror = function(msg, url, line) {
|
||||
var box = document.getElementById('editorloadingbox');
|
||||
box.innerHTML = '<p><b>An error occured while loading the pad</b></p>'
|
||||
+ '<p><b>'+msg+'</b> '
|
||||
+ '<small>in '+ url +' (line '+ line +')</small></p>';
|
||||
// call original error handler
|
||||
if(typeof(originalHandler) == 'function') originalHandler.call(null, arguments);
|
||||
};
|
||||
})();
|
||||
</script>
|
||||
|
||||
<script type="text/javascript" src="../static/js/require-kernel.js"></script>
|
||||
<script type="text/javascript" src="../socket.io/socket.io.js"></script>
|
||||
|
||||
|
@ -343,9 +369,18 @@
|
|||
document.domain = document.domain; // for comet
|
||||
}
|
||||
|
||||
var plugins = require('ep_etherpad-lite/static/js/pluginfw/plugins');
|
||||
var plugins = require('ep_etherpad-lite/static/js/pluginfw/client_plugins');
|
||||
var hooks = require('ep_etherpad-lite/static/js/pluginfw/hooks');
|
||||
|
||||
plugins.baseURL = baseURL;
|
||||
plugins.update(function () {
|
||||
hooks.plugins = plugins;
|
||||
|
||||
// Call documentReady hook
|
||||
$(function() {
|
||||
hooks.aCallAll('documentReady');
|
||||
});
|
||||
|
||||
var pad = require('ep_etherpad-lite/static/js/pad');
|
||||
pad.baseURL = baseURL;
|
||||
pad.init();
|
||||
|
|
|
@ -33,6 +33,12 @@
|
|||
<div class="stepper" id="rightstep"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="overlay">
|
||||
<div id="overlay-inner">
|
||||
<!-- -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="timeslider-bar">
|
||||
|
@ -70,54 +76,59 @@
|
|||
</div><!-- /padmain -->
|
||||
</div><!-- /padpage -->
|
||||
|
||||
<div id="modaloverlay">
|
||||
<div id="modaloverlay-inner">
|
||||
<!-- -->
|
||||
<div id="connectivity" class="popup">
|
||||
<% e.begin_block("modals"); %>
|
||||
<div class="connected visible">
|
||||
<h2>Connected.</h2>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="mainmodals">
|
||||
<% e.begin_block("modals"); %>
|
||||
<div id="connectionbox" class="modaldialog">
|
||||
<div id="connectionboxinner" class="modaldialog-inner">
|
||||
<div class="connecting">Connecting...</div>
|
||||
<div class="reconnecting">Reestablishing connection...</div>
|
||||
<div class="disconnected">
|
||||
<h2 class="h2_disconnect">Disconnected.</h2>
|
||||
<h2 class="h2_userdup">Opened in another window.</h2>
|
||||
<h2 class="h2_unauth">No Authorization.</h2>
|
||||
<div id="disconnected_looping">
|
||||
<p><b>We're having trouble talking to the EtherPad lite synchronization server.</b> You may be connecting through an incompatible firewall or proxy server.</p>
|
||||
</div>
|
||||
<div id="disconnected_initsocketfail">
|
||||
<p><b>We were unable to connect to the EtherPad lite synchronization server.</b> This may be due to an incompatibility with your web browser or internet connection.</p>
|
||||
</div>
|
||||
<div id="disconnected_userdup">
|
||||
<p><b>You seem to have opened this pad in another browser window.</b> If you'd like to use this window instead, you can reconnect.</p>
|
||||
</div>
|
||||
<div id="disconnected_unknown">
|
||||
<p><b>Lost connection with the EtherPad lite synchronization server.</b> This may be due to a loss of network connectivity.</p>
|
||||
</div>
|
||||
<div id="disconnected_slowcommit">
|
||||
<p><b>Server not responding.</b> This may be due to network connectivity issues or high load on the server.</p>
|
||||
</div>
|
||||
<div id="disconnected_unauth">
|
||||
<p>Your browser's credentials or permissions have changed while viewing this pad. Try reconnecting.</p>
|
||||
</div>
|
||||
<div id="disconnected_deleted">
|
||||
<p>This pad was deleted.</p>
|
||||
</div>
|
||||
<div id="reconnect_advise">
|
||||
<p>If this continues to happen, please let us know</p>
|
||||
</div>
|
||||
<div id="reconnect_form">
|
||||
<button id="forcereconnect">Reconnect Now</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<% e.end_block(); %>
|
||||
</div>
|
||||
<div class="reconnecting">
|
||||
<h1>Reestablishing connection...</h1>
|
||||
<p><img alt="" border="0" src="/static/img/connectingbar.gif" /></p>
|
||||
</div>
|
||||
<div class="userdup">
|
||||
<h1>Opened in another window.</h1>
|
||||
<h2>You seem to have opened this pad in another browser window.</h2>
|
||||
<p>If you'd like to use this window instead, you can reconnect.</p>
|
||||
<button id="forcereconnect">Reconnect Now</button>
|
||||
</div>
|
||||
<div class="unauth">
|
||||
<h1>No Authorization.</h1>
|
||||
<p>Your browser's credentials or permissions have changed while viewing this pad. Try reconnecting.</p>
|
||||
<button id="forcereconnect">Reconnect Now</button>
|
||||
</div>
|
||||
<div class="looping">
|
||||
<h1>Disconnected.</h1>
|
||||
<h2>We're having trouble talking to the EtherPad lite synchronization server.</h2>
|
||||
<p>You may be connecting through an incompatible firewall or proxy server.</p>
|
||||
</div>
|
||||
<div class="initsocketfail">
|
||||
<h1>Disconnected.</h1>
|
||||
<h2>We were unable to connect to the EtherPad lite synchronization server.</h2>
|
||||
<p>This may be due to an incompatibility with your web browser or internet connection.</p>
|
||||
</div>
|
||||
<div class="slowcommit">
|
||||
<h1>Disconnected.</h1>
|
||||
<h2>Server not responding.</h2>
|
||||
<p>This may be due to network connectivity issues or high load on the server.</p>
|
||||
<button id="forcereconnect">Reconnect Now</button>
|
||||
</div>
|
||||
<div class="deleted">
|
||||
<h1>Disconnected.</h1>
|
||||
<p>This pad was deleted.</p>
|
||||
</div>
|
||||
<div class="disconnected">
|
||||
<h1>Disconnected.</h1>
|
||||
<h2>Lost connection with the EtherPad lite synchronization server.</h2>
|
||||
<p>This may be due to a loss of network connectivity. If this continues to happen, please let us know</p>
|
||||
<button id="forcereconnect">Reconnect Now</button>
|
||||
</div>
|
||||
<form id="reconnectform" method="post" action="/ep/pad/reconnect" accept-charset="UTF-8" style="display: none;">
|
||||
<input type="hidden" class="padId" name="padId">
|
||||
<input type="hidden" class="diagnosticInfo" name="diagnosticInfo">
|
||||
<input type="hidden" class="missedChanges" name="missedChanges">
|
||||
</form>
|
||||
<% e.end_block(); %>
|
||||
</div>
|
||||
|
||||
<!-- export code -->
|
||||
<div id="importexport">
|
||||
|
@ -164,10 +175,13 @@
|
|||
document.domain = document.domain; // for comet
|
||||
}
|
||||
|
||||
var plugins = require('ep_etherpad-lite/static/js/pluginfw/plugins');
|
||||
var plugins = require('ep_etherpad-lite/static/js/pluginfw/client_plugins');
|
||||
plugins.baseURL = baseURL;
|
||||
|
||||
plugins.update(function () {
|
||||
var hooks = require('ep_etherpad-lite/static/js/pluginfw/hooks');
|
||||
hooks.plugins = plugins;
|
||||
|
||||
var timeslider = require('ep_etherpad-lite/static/js/timeslider')
|
||||
timeslider.baseURL = baseURL;
|
||||
timeslider.init();
|
||||
|
|
|
@ -1 +1 @@
|
|||
bin\node.exe node_modules\ep_etherpad-lite\node\server.js
|
||||
node node_modules\ep_etherpad-lite\node\server.js
|
|
@ -0,0 +1,137 @@
|
|||
var helper = {};
|
||||
|
||||
(function(){
|
||||
var $iframeContainer, $iframe, jsLibraries = {};
|
||||
|
||||
helper.init = function(cb){
|
||||
$iframeContainer = $("#iframe-container");
|
||||
|
||||
$.get('/static/js/jquery.js').done(function(code){
|
||||
// make sure we don't override existing jquery
|
||||
jsLibraries["jquery"] = "if(typeof $ === 'undefined') {\n" + code + "\n}";
|
||||
|
||||
$.get('/tests/frontend/lib/sendkeys.js').done(function(code){
|
||||
jsLibraries["sendkeys"] = code;
|
||||
|
||||
cb();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
helper.randomString = function randomString(len)
|
||||
{
|
||||
var chars = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
|
||||
var randomstring = '';
|
||||
for (var i = 0; i < len; i++)
|
||||
{
|
||||
var rnum = Math.floor(Math.random() * chars.length);
|
||||
randomstring += chars.substring(rnum, rnum + 1);
|
||||
}
|
||||
return randomstring;
|
||||
}
|
||||
|
||||
var getFrameJQuery = function($iframe){
|
||||
/*
|
||||
I tried over 9000 ways to inject javascript into iframes.
|
||||
This is the only way I found that worked in IE 7+8+9, FF and Chrome
|
||||
*/
|
||||
|
||||
var win = $iframe[0].contentWindow;
|
||||
var doc = win.document;
|
||||
|
||||
//IE 8+9 Hack to make eval appear
|
||||
//http://stackoverflow.com/questions/2720444/why-does-this-window-object-not-have-the-eval-function
|
||||
win.execScript && win.execScript("null");
|
||||
|
||||
win.eval(jsLibraries["jquery"]);
|
||||
win.eval(jsLibraries["sendkeys"]);
|
||||
|
||||
win.$.window = win;
|
||||
win.$.document = doc;
|
||||
|
||||
return win.$;
|
||||
}
|
||||
|
||||
helper.newPad = function(cb){
|
||||
var padName = "FRONTEND_TEST_" + helper.randomString(20);
|
||||
$iframe = $("<iframe src='/p/" + padName + "'></iframe>");
|
||||
|
||||
//clean up inner iframe references
|
||||
helper.padChrome$ = helper.padOuter$ = helper.padInner$ = null;
|
||||
|
||||
//clean up iframes properly to prevent IE from memoryleaking
|
||||
$iframeContainer.find("iframe").purgeFrame().done(function(){
|
||||
$iframeContainer.append($iframe);
|
||||
$iframe.one('load', function(){
|
||||
helper.waitFor(function(){
|
||||
return !$iframe.contents().find("#editorloadingbox").is(":visible");
|
||||
}, 4000).done(function(){
|
||||
helper.padChrome$ = getFrameJQuery( $('#iframe-container iframe'));
|
||||
helper.padOuter$ = getFrameJQuery(helper.padChrome$('iframe.[name="ace_outer"]'));
|
||||
helper.padInner$ = getFrameJQuery( helper.padOuter$('iframe.[name="ace_inner"]'));
|
||||
|
||||
cb();
|
||||
}).fail(function(){
|
||||
throw new Error("Pad never loaded");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
return padName;
|
||||
}
|
||||
|
||||
helper.waitFor = function(conditionFunc, _timeoutTime, _intervalTime){
|
||||
var timeoutTime = _timeoutTime || 1000;
|
||||
var intervalTime = _intervalTime || 10;
|
||||
|
||||
var deferred = $.Deferred();
|
||||
|
||||
var _fail = deferred.fail;
|
||||
var listenForFail = false;
|
||||
deferred.fail = function(){
|
||||
listenForFail = true;
|
||||
_fail.apply(this, arguments);
|
||||
}
|
||||
|
||||
var intervalCheck = setInterval(function(){
|
||||
var passed = false;
|
||||
|
||||
passed = conditionFunc();
|
||||
|
||||
if(passed){
|
||||
clearInterval(intervalCheck);
|
||||
clearTimeout(timeout);
|
||||
|
||||
deferred.resolve();
|
||||
}
|
||||
}, intervalTime);
|
||||
|
||||
var timeout = setTimeout(function(){
|
||||
clearInterval(intervalCheck);
|
||||
var error = new Error("wait for condition never became true " + conditionFunc.toString());
|
||||
deferred.reject(error);
|
||||
|
||||
if(!listenForFail){
|
||||
throw error;
|
||||
}
|
||||
}, timeoutTime);
|
||||
|
||||
return deferred;
|
||||
}
|
||||
|
||||
/* Ensure console.log doesn't blow up in IE, ugly but ok for a test framework imho*/
|
||||
window.console = window.console || {};
|
||||
window.console.log = window.console.log || function(){}
|
||||
|
||||
//force usage of callbacks in it
|
||||
var _it = it;
|
||||
it = function(name, func){
|
||||
if(func && func.length !== 1){
|
||||
func = function(){
|
||||
throw new Error("Please use always a callback with it() - " + func.toString());
|
||||
}
|
||||
}
|
||||
|
||||
_it(name, func);
|
||||
}
|
||||
})()
|
|
@ -0,0 +1,32 @@
|
|||
<!doctype html>
|
||||
<html>
|
||||
<title>Frontend tests</title>
|
||||
<meta charset="utf-8">
|
||||
|
||||
<link rel="stylesheet" href="runner.css" />
|
||||
|
||||
<div id="mocha"></div>
|
||||
<div id="iframe-container"></div>
|
||||
|
||||
<script src="/static/js/jquery.js"></script>
|
||||
|
||||
<script src="lib/mocha.js"></script>
|
||||
<script> mocha.setup('bdd') </script>
|
||||
<script src="lib/expect.js"></script>
|
||||
|
||||
<script src="lib/sendkeys.js"></script>
|
||||
<script src="lib/jquery.iframe.js"></script>
|
||||
<script src="helper.js"></script>
|
||||
|
||||
<script src="specs/helper.js"></script>
|
||||
<script src="specs/button_bold.js"></script>
|
||||
<script src="specs/button_italic.js"></script>
|
||||
<script src="specs/keystroke_urls_become_clickable.js"></script>
|
||||
<script src="specs/keystroke_delete.js"></script>
|
||||
<script src="specs/font_type.js"></script>
|
||||
<script src="specs/embed_value.js"></script>
|
||||
<script src="specs/keystroke_urls_become_clickable.js"></script>
|
||||
<script src="specs/button_indentation.js"></script>
|
||||
|
||||
<script src="runner.js"></script>
|
||||
</html>
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,39 @@
|
|||
//copied from http://stackoverflow.com/questions/8407946/is-it-possible-to-use-iframes-in-ie-without-memory-leaks
|
||||
(function($) {
|
||||
$.fn.purgeFrame = function() {
|
||||
var deferred;
|
||||
|
||||
if ($.browser.msie && parseFloat($.browser.version, 10) < 9) {
|
||||
deferred = purge(this);
|
||||
} else {
|
||||
this.remove();
|
||||
deferred = $.Deferred();
|
||||
deferred.resolve();
|
||||
}
|
||||
|
||||
return deferred;
|
||||
};
|
||||
|
||||
function purge($frame) {
|
||||
var sem = $frame.length
|
||||
, deferred = $.Deferred();
|
||||
|
||||
$frame.load(function() {
|
||||
var frame = this;
|
||||
frame.contentWindow.document.innerHTML = '';
|
||||
|
||||
sem -= 1;
|
||||
if (sem <= 0) {
|
||||
$frame.remove();
|
||||
deferred.resolve();
|
||||
}
|
||||
});
|
||||
$frame.attr('src', 'about:blank');
|
||||
|
||||
if ($frame.length === 0) {
|
||||
deferred.resolve();
|
||||
}
|
||||
|
||||
return deferred.promise();
|
||||
}
|
||||
})(jQuery);
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,467 @@
|
|||
// Cross-broswer implementation of text ranges and selections
|
||||
// documentation: http://bililite.com/blog/2011/01/11/cross-browser-.and-selections/
|
||||
// Version: 1.1
|
||||
// Copyright (c) 2010 Daniel Wachsstock
|
||||
// MIT license:
|
||||
// Permission is hereby granted, free of charge, to any person
|
||||
// obtaining a copy of this software and associated documentation
|
||||
// files (the "Software"), to deal in the Software without
|
||||
// restriction, including without limitation the rights to use,
|
||||
// copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the
|
||||
// Software is furnished to do so, subject to the following
|
||||
// conditions:
|
||||
|
||||
// The above copyright notice and this permission notice shall be
|
||||
// included in all copies or substantial portions of the Software.
|
||||
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
|
||||
// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
|
||||
// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
||||
// OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
(function($){
|
||||
|
||||
bililiteRange = function(el, debug){
|
||||
var ret;
|
||||
if (debug){
|
||||
ret = new NothingRange(); // Easier to force it to use the no-selection type than to try to find an old browser
|
||||
}else if (document.selection){
|
||||
// Internet Explorer
|
||||
ret = new IERange();
|
||||
}else if (window.getSelection && el.setSelectionRange){
|
||||
// Standards. Element is an input or textarea
|
||||
ret = new InputRange();
|
||||
}else if (window.getSelection){
|
||||
// Standards, with any other kind of element
|
||||
ret = new W3CRange()
|
||||
}else{
|
||||
// doesn't support selection
|
||||
ret = new NothingRange();
|
||||
}
|
||||
ret._el = el;
|
||||
ret._doc = el.ownerDocument;
|
||||
ret._win = 'defaultView' in ret._doc ? ret._doc.defaultView : ret._doc.parentWindow;
|
||||
ret._textProp = textProp(el);
|
||||
ret._bounds = [0, ret.length()];
|
||||
return ret;
|
||||
}
|
||||
|
||||
function textProp(el){
|
||||
// returns the property that contains the text of the element
|
||||
if (typeof el.value != 'undefined') return 'value';
|
||||
if (typeof el.text != 'undefined') return 'text';
|
||||
if (typeof el.textContent != 'undefined') return 'textContent';
|
||||
return 'innerText';
|
||||
}
|
||||
|
||||
// base class
|
||||
function Range(){}
|
||||
Range.prototype = {
|
||||
length: function() {
|
||||
return this._el[this._textProp].replace(/\r/g, '').length; // need to correct for IE's CrLf weirdness
|
||||
},
|
||||
bounds: function(s){
|
||||
if (s === 'all'){
|
||||
this._bounds = [0, this.length()];
|
||||
}else if (s === 'start'){
|
||||
this._bounds = [0, 0];
|
||||
}else if (s === 'end'){
|
||||
this._bounds = [this.length(), this.length()];
|
||||
}else if (s === 'selection'){
|
||||
this.bounds ('all'); // first select the whole thing for constraining
|
||||
this._bounds = this._nativeSelection();
|
||||
}else if (s){
|
||||
this._bounds = s; // don't error check now; the element may change at any moment, so constrain it when we need it.
|
||||
}else{
|
||||
var b = [
|
||||
Math.max(0, Math.min (this.length(), this._bounds[0])),
|
||||
Math.max(0, Math.min (this.length(), this._bounds[1]))
|
||||
];
|
||||
return b; // need to constrain it to fit
|
||||
}
|
||||
return this; // allow for chaining
|
||||
},
|
||||
select: function(){
|
||||
this._nativeSelect(this._nativeRange(this.bounds()));
|
||||
return this; // allow for chaining
|
||||
},
|
||||
text: function(text, select){
|
||||
if (arguments.length){
|
||||
this._nativeSetText(text, this._nativeRange(this.bounds()));
|
||||
if (select == 'start'){
|
||||
this.bounds ([this._bounds[0], this._bounds[0]]);
|
||||
this.select();
|
||||
}else if (select == 'end'){
|
||||
this.bounds ([this._bounds[0]+text.length, this._bounds[0]+text.length]);
|
||||
this.select();
|
||||
}else if (select == 'all'){
|
||||
this.bounds ([this._bounds[0], this._bounds[0]+text.length]);
|
||||
this.select();
|
||||
}
|
||||
return this; // allow for chaining
|
||||
}else{
|
||||
return this._nativeGetText(this._nativeRange(this.bounds()));
|
||||
}
|
||||
},
|
||||
insertEOL: function (){
|
||||
this._nativeEOL();
|
||||
this._bounds = [this._bounds[0]+1, this._bounds[0]+1]; // move past the EOL marker
|
||||
return this;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
function IERange(){}
|
||||
IERange.prototype = new Range();
|
||||
IERange.prototype._nativeRange = function (bounds){
|
||||
var rng;
|
||||
if (this._el.tagName == 'INPUT'){
|
||||
// IE 8 is very inconsistent; textareas have createTextRange but it doesn't work
|
||||
rng = this._el.createTextRange();
|
||||
}else{
|
||||
rng = this._doc.body.createTextRange ();
|
||||
rng.moveToElementText(this._el);
|
||||
}
|
||||
if (bounds){
|
||||
if (bounds[1] < 0) bounds[1] = 0; // IE tends to run elements out of bounds
|
||||
if (bounds[0] > this.length()) bounds[0] = this.length();
|
||||
if (bounds[1] < rng.text.replace(/\r/g, '').length){ // correct for IE's CrLf wierdness
|
||||
// block-display elements have an invisible, uncounted end of element marker, so we move an extra one and use the current length of the range
|
||||
rng.moveEnd ('character', -1);
|
||||
rng.moveEnd ('character', bounds[1]-rng.text.replace(/\r/g, '').length);
|
||||
}
|
||||
if (bounds[0] > 0) rng.moveStart('character', bounds[0]);
|
||||
}
|
||||
return rng;
|
||||
};
|
||||
IERange.prototype._nativeSelect = function (rng){
|
||||
rng.select();
|
||||
};
|
||||
IERange.prototype._nativeSelection = function (){
|
||||
// returns [start, end] for the selection constrained to be in element
|
||||
var rng = this._nativeRange(); // range of the element to constrain to
|
||||
var len = this.length();
|
||||
if (this._doc.selection.type != 'Text') return [len, len]; // append to the end
|
||||
var sel = this._doc.selection.createRange();
|
||||
try{
|
||||
return [
|
||||
iestart(sel, rng),
|
||||
ieend (sel, rng)
|
||||
];
|
||||
}catch (e){
|
||||
// IE gets upset sometimes about comparing text to input elements, but the selections cannot overlap, so make a best guess
|
||||
return (sel.parentElement().sourceIndex < this._el.sourceIndex) ? [0,0] : [len, len];
|
||||
}
|
||||
};
|
||||
IERange.prototype._nativeGetText = function (rng){
|
||||
return rng.text.replace(/\r/g, ''); // correct for IE's CrLf weirdness
|
||||
};
|
||||
IERange.prototype._nativeSetText = function (text, rng){
|
||||
rng.text = text;
|
||||
};
|
||||
IERange.prototype._nativeEOL = function(){
|
||||
if (typeof this._el.value != 'undefined'){
|
||||
this.text('\n'); // for input and textarea, insert it straight
|
||||
}else{
|
||||
this._nativeRange(this.bounds()).pasteHTML('<br/>');
|
||||
}
|
||||
};
|
||||
// IE internals
|
||||
function iestart(rng, constraint){
|
||||
// returns the position (in character) of the start of rng within constraint. If it's not in constraint, returns 0 if it's before, length if it's after
|
||||
var len = constraint.text.replace(/\r/g, '').length; // correct for IE's CrLf wierdness
|
||||
if (rng.compareEndPoints ('StartToStart', constraint) <= 0) return 0; // at or before the beginning
|
||||
if (rng.compareEndPoints ('StartToEnd', constraint) >= 0) return len;
|
||||
for (var i = 0; rng.compareEndPoints ('StartToStart', constraint) > 0; ++i, rng.moveStart('character', -1));
|
||||
return i;
|
||||
}
|
||||
function ieend (rng, constraint){
|
||||
// returns the position (in character) of the end of rng within constraint. If it's not in constraint, returns 0 if it's before, length if it's after
|
||||
var len = constraint.text.replace(/\r/g, '').length; // correct for IE's CrLf wierdness
|
||||
if (rng.compareEndPoints ('EndToEnd', constraint) >= 0) return len; // at or after the end
|
||||
if (rng.compareEndPoints ('EndToStart', constraint) <= 0) return 0;
|
||||
for (var i = 0; rng.compareEndPoints ('EndToStart', constraint) > 0; ++i, rng.moveEnd('character', -1));
|
||||
return i;
|
||||
}
|
||||
|
||||
// an input element in a standards document. "Native Range" is just the bounds array
|
||||
function InputRange(){}
|
||||
InputRange.prototype = new Range();
|
||||
InputRange.prototype._nativeRange = function(bounds) {
|
||||
return bounds || [0, this.length()];
|
||||
};
|
||||
InputRange.prototype._nativeSelect = function (rng){
|
||||
this._el.setSelectionRange(rng[0], rng[1]);
|
||||
};
|
||||
InputRange.prototype._nativeSelection = function(){
|
||||
return [this._el.selectionStart, this._el.selectionEnd];
|
||||
};
|
||||
InputRange.prototype._nativeGetText = function(rng){
|
||||
return this._el.value.substring(rng[0], rng[1]);
|
||||
};
|
||||
InputRange.prototype._nativeSetText = function(text, rng){
|
||||
var val = this._el.value;
|
||||
this._el.value = val.substring(0, rng[0]) + text + val.substring(rng[1]);
|
||||
};
|
||||
InputRange.prototype._nativeEOL = function(){
|
||||
this.text('\n');
|
||||
};
|
||||
|
||||
function W3CRange(){}
|
||||
W3CRange.prototype = new Range();
|
||||
W3CRange.prototype._nativeRange = function (bounds){
|
||||
var rng = this._doc.createRange();
|
||||
rng.selectNodeContents(this._el);
|
||||
if (bounds){
|
||||
w3cmoveBoundary (rng, bounds[0], true, this._el);
|
||||
rng.collapse (true);
|
||||
w3cmoveBoundary (rng, bounds[1]-bounds[0], false, this._el);
|
||||
}
|
||||
return rng;
|
||||
};
|
||||
W3CRange.prototype._nativeSelect = function (rng){
|
||||
this._win.getSelection().removeAllRanges();
|
||||
this._win.getSelection().addRange (rng);
|
||||
};
|
||||
W3CRange.prototype._nativeSelection = function (){
|
||||
// returns [start, end] for the selection constrained to be in element
|
||||
var rng = this._nativeRange(); // range of the element to constrain to
|
||||
if (this._win.getSelection().rangeCount == 0) return [this.length(), this.length()]; // append to the end
|
||||
var sel = this._win.getSelection().getRangeAt(0);
|
||||
return [
|
||||
w3cstart(sel, rng),
|
||||
w3cend (sel, rng)
|
||||
];
|
||||
}
|
||||
W3CRange.prototype._nativeGetText = function (rng){
|
||||
return rng.toString();
|
||||
};
|
||||
W3CRange.prototype._nativeSetText = function (text, rng){
|
||||
rng.deleteContents();
|
||||
rng.insertNode (this._doc.createTextNode(text));
|
||||
this._el.normalize(); // merge the text with the surrounding text
|
||||
};
|
||||
W3CRange.prototype._nativeEOL = function(){
|
||||
var rng = this._nativeRange(this.bounds());
|
||||
rng.deleteContents();
|
||||
var br = this._doc.createElement('br');
|
||||
br.setAttribute ('_moz_dirty', ''); // for Firefox
|
||||
rng.insertNode (br);
|
||||
rng.insertNode (this._doc.createTextNode('\n'));
|
||||
rng.collapse (false);
|
||||
};
|
||||
// W3C internals
|
||||
function nextnode (node, root){
|
||||
// in-order traversal
|
||||
// we've already visited node, so get kids then siblings
|
||||
if (node.firstChild) return node.firstChild;
|
||||
if (node.nextSibling) return node.nextSibling;
|
||||
if (node===root) return null;
|
||||
while (node.parentNode){
|
||||
// get uncles
|
||||
node = node.parentNode;
|
||||
if (node == root) return null;
|
||||
if (node.nextSibling) return node.nextSibling;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
function w3cmoveBoundary (rng, n, bStart, el){
|
||||
// move the boundary (bStart == true ? start : end) n characters forward, up to the end of element el. Forward only!
|
||||
// if the start is moved after the end, then an exception is raised
|
||||
if (n <= 0) return;
|
||||
var node = rng[bStart ? 'startContainer' : 'endContainer'];
|
||||
if (node.nodeType == 3){
|
||||
// we may be starting somewhere into the text
|
||||
n += rng[bStart ? 'startOffset' : 'endOffset'];
|
||||
}
|
||||
while (node){
|
||||
if (node.nodeType == 3){
|
||||
if (n <= node.nodeValue.length){
|
||||
rng[bStart ? 'setStart' : 'setEnd'](node, n);
|
||||
// special case: if we end next to a <br>, include that node.
|
||||
if (n == node.nodeValue.length){
|
||||
// skip past zero-length text nodes
|
||||
for (var next = nextnode (node, el); next && next.nodeType==3 && next.nodeValue.length == 0; next = nextnode(next, el)){
|
||||
rng[bStart ? 'setStartAfter' : 'setEndAfter'](next);
|
||||
}
|
||||
if (next && next.nodeType == 1 && next.nodeName == "BR") rng[bStart ? 'setStartAfter' : 'setEndAfter'](next);
|
||||
}
|
||||
return;
|
||||
}else{
|
||||
rng[bStart ? 'setStartAfter' : 'setEndAfter'](node); // skip past this one
|
||||
n -= node.nodeValue.length; // and eat these characters
|
||||
}
|
||||
}
|
||||
node = nextnode (node, el);
|
||||
}
|
||||
}
|
||||
var START_TO_START = 0; // from the w3c definitions
|
||||
var START_TO_END = 1;
|
||||
var END_TO_END = 2;
|
||||
var END_TO_START = 3;
|
||||
// from the Mozilla documentation, for range.compareBoundaryPoints(how, sourceRange)
|
||||
// -1, 0, or 1, indicating whether the corresponding boundary-point of range is respectively before, equal to, or after the corresponding boundary-point of sourceRange.
|
||||
// * Range.END_TO_END compares the end boundary-point of sourceRange to the end boundary-point of range.
|
||||
// * Range.END_TO_START compares the end boundary-point of sourceRange to the start boundary-point of range.
|
||||
// * Range.START_TO_END compares the start boundary-point of sourceRange to the end boundary-point of range.
|
||||
// * Range.START_TO_START compares the start boundary-point of sourceRange to the start boundary-point of range.
|
||||
function w3cstart(rng, constraint){
|
||||
if (rng.compareBoundaryPoints (START_TO_START, constraint) <= 0) return 0; // at or before the beginning
|
||||
if (rng.compareBoundaryPoints (END_TO_START, constraint) >= 0) return constraint.toString().length;
|
||||
rng = rng.cloneRange(); // don't change the original
|
||||
rng.setEnd (constraint.endContainer, constraint.endOffset); // they now end at the same place
|
||||
return constraint.toString().length - rng.toString().length;
|
||||
}
|
||||
function w3cend (rng, constraint){
|
||||
if (rng.compareBoundaryPoints (END_TO_END, constraint) >= 0) return constraint.toString().length; // at or after the end
|
||||
if (rng.compareBoundaryPoints (START_TO_END, constraint) <= 0) return 0;
|
||||
rng = rng.cloneRange(); // don't change the original
|
||||
rng.setStart (constraint.startContainer, constraint.startOffset); // they now start at the same place
|
||||
return rng.toString().length;
|
||||
}
|
||||
|
||||
function NothingRange(){}
|
||||
NothingRange.prototype = new Range();
|
||||
NothingRange.prototype._nativeRange = function(bounds) {
|
||||
return bounds || [0,this.length()];
|
||||
};
|
||||
NothingRange.prototype._nativeSelect = function (rng){ // do nothing
|
||||
};
|
||||
NothingRange.prototype._nativeSelection = function(){
|
||||
return [0,0];
|
||||
};
|
||||
NothingRange.prototype._nativeGetText = function (rng){
|
||||
return this._el[this._textProp].substring(rng[0], rng[1]);
|
||||
};
|
||||
NothingRange.prototype._nativeSetText = function (text, rng){
|
||||
var val = this._el[this._textProp];
|
||||
this._el[this._textProp] = val.substring(0, rng[0]) + text + val.substring(rng[1]);
|
||||
};
|
||||
NothingRange.prototype._nativeEOL = function(){
|
||||
this.text('\n');
|
||||
};
|
||||
|
||||
})(jQuery);
|
||||
|
||||
// insert characters in a textarea or text input field
|
||||
// special characters are enclosed in {}; use {{} for the { character itself
|
||||
// documentation: http://bililite.com/blog/2008/08/20/the-fnsendkeys-plugin/
|
||||
// Version: 2.0
|
||||
// Copyright (c) 2010 Daniel Wachsstock
|
||||
// MIT license:
|
||||
// Permission is hereby granted, free of charge, to any person
|
||||
// obtaining a copy of this software and associated documentation
|
||||
// files (the "Software"), to deal in the Software without
|
||||
// restriction, including without limitation the rights to use,
|
||||
// copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the
|
||||
// Software is furnished to do so, subject to the following
|
||||
// conditions:
|
||||
|
||||
// The above copyright notice and this permission notice shall be
|
||||
// included in all copies or substantial portions of the Software.
|
||||
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
|
||||
// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
|
||||
// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
||||
// OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
(function($){
|
||||
|
||||
$.fn.sendkeys = function (x, opts){
|
||||
return this.each( function(){
|
||||
var localkeys = $.extend({}, opts, $(this).data('sendkeys')); // allow for element-specific key functions
|
||||
// most elements to not keep track of their selection when they lose focus, so we have to do it for them
|
||||
var rng = $.data (this, 'sendkeys.selection');
|
||||
if (!rng){
|
||||
rng = bililiteRange(this).bounds('selection');
|
||||
$.data(this, 'sendkeys.selection', rng);
|
||||
$(this).bind('mouseup.sendkeys', function(){
|
||||
// we have to update the saved range. The routines here update the bounds with each press, but actual keypresses and mouseclicks do not
|
||||
$.data(this, 'sendkeys.selection').bounds('selection');
|
||||
}).bind('keyup.sendkeys', function(evt){
|
||||
// restore the selection if we got here with a tab (a click should select what was clicked on)
|
||||
if (evt.which == 9){
|
||||
// there's a flash of selection when we restore the focus, but I don't know how to avoid that.
|
||||
$.data(this, 'sendkeys.selection').select();
|
||||
}else{
|
||||
$.data(this, 'sendkeys.selection').bounds('selection');
|
||||
}
|
||||
});
|
||||
}
|
||||
this.focus();
|
||||
if (typeof x === 'undefined') return; // no string, so we just set up the event handlers
|
||||
$.data(this, 'sendkeys.originalText', rng.text());
|
||||
x.replace(/\n/g, '{enter}'). // turn line feeds into explicit break insertions
|
||||
replace(/{[^}]*}|[^{]+/g, function(s){
|
||||
(localkeys[s] || $.fn.sendkeys.defaults[s] || $.fn.sendkeys.defaults.simplechar)(rng, s);
|
||||
});
|
||||
$(this).trigger({type: 'sendkeys', which: x});
|
||||
});
|
||||
}; // sendkeys
|
||||
|
||||
|
||||
// add the functions publicly so they can be overridden
|
||||
$.fn.sendkeys.defaults = {
|
||||
simplechar: function (rng, s){
|
||||
rng.text(s, 'end');
|
||||
for (var i =0; i < s.length; ++i){
|
||||
var x = s.charCodeAt(i);
|
||||
// a bit of cheating: rng._el is the element associated with rng.
|
||||
$(rng._el).trigger({type: 'keypress', keyCode: x, which: x, charCode: x});
|
||||
}
|
||||
},
|
||||
'{{}': function (rng){
|
||||
$.fn.sendkeys.defaults.simplechar (rng, '{')
|
||||
},
|
||||
'{enter}': function (rng){
|
||||
rng.insertEOL();
|
||||
rng.select();
|
||||
var x = '\n'.charCodeAt(0);
|
||||
$(rng._el).trigger({type: 'keypress', keyCode: x, which: x, charCode: x});
|
||||
},
|
||||
'{backspace}': function (rng){
|
||||
var b = rng.bounds();
|
||||
if (b[0] == b[1]) rng.bounds([b[0]-1, b[0]]); // no characters selected; it's just an insertion point. Remove the previous character
|
||||
rng.text('', 'end'); // delete the characters and update the selection
|
||||
},
|
||||
'{del}': function (rng){
|
||||
var b = rng.bounds();
|
||||
if (b[0] == b[1]) rng.bounds([b[0], b[0]+1]); // no characters selected; it's just an insertion point. Remove the next character
|
||||
rng.text('', 'end'); // delete the characters and update the selection
|
||||
},
|
||||
'{rightarrow}': function (rng){
|
||||
var b = rng.bounds();
|
||||
if (b[0] == b[1]) ++b[1]; // no characters selected; it's just an insertion point. Move to the right
|
||||
rng.bounds([b[1], b[1]]).select();
|
||||
},
|
||||
'{leftarrow}': function (rng){
|
||||
var b = rng.bounds();
|
||||
if (b[0] == b[1]) --b[0]; // no characters selected; it's just an insertion point. Move to the left
|
||||
rng.bounds([b[0], b[0]]).select();
|
||||
},
|
||||
'{selectall}' : function (rng){
|
||||
rng.bounds('all').select();
|
||||
},
|
||||
'{selection}': function (rng){
|
||||
$.fn.sendkeys.defaults.simplechar(rng, $.data(rng._el, 'sendkeys.originalText'));
|
||||
},
|
||||
'{mark}' : function (rng){
|
||||
var bounds = rng.bounds();
|
||||
$(rng._el).one('sendkeys', function(){
|
||||
// set up the event listener to change the selection after the sendkeys is done
|
||||
rng.bounds(bounds).select();
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
})(jQuery)
|
|
@ -0,0 +1,228 @@
|
|||
html {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
body {
|
||||
padding: 0px;
|
||||
margin: 0px;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
#iframe-container {
|
||||
width: 50%;
|
||||
height: 100%;
|
||||
float:right;
|
||||
}
|
||||
|
||||
#iframe-container iframe {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
#mocha {
|
||||
font: 20px/1.5 "Helvetica Neue", Helvetica, Arial, sans-serif;
|
||||
border-right: 2px solid #999;
|
||||
width: 50%;
|
||||
height: 100%;
|
||||
position: absolute;
|
||||
overflow: auto;
|
||||
float:left;
|
||||
}
|
||||
|
||||
#mocha #report {
|
||||
margin-top: 50px;
|
||||
}
|
||||
|
||||
#mocha ul, #mocha li {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
#mocha ul {
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
#mocha h1, #mocha h2 {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
#mocha h1 {
|
||||
margin-top: 15px;
|
||||
font-size: 1em;
|
||||
font-weight: 200;
|
||||
}
|
||||
|
||||
#mocha h1 a {
|
||||
text-decoration: none;
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
#mocha h1 a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
#mocha .suite .suite h1 {
|
||||
margin-top: 0;
|
||||
font-size: .8em;
|
||||
}
|
||||
|
||||
#mocha h2 {
|
||||
font-size: 12px;
|
||||
font-weight: normal;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
#mocha .suite {
|
||||
margin-left: 15px;
|
||||
}
|
||||
|
||||
#mocha .test {
|
||||
margin-left: 15px;
|
||||
}
|
||||
|
||||
#mocha .test:hover h2::after {
|
||||
position: relative;
|
||||
top: 0;
|
||||
right: -10px;
|
||||
content: '(view source)';
|
||||
font-size: 12px;
|
||||
font-family: arial;
|
||||
color: #888;
|
||||
}
|
||||
|
||||
#mocha .test.pending:hover h2::after {
|
||||
content: '(pending)';
|
||||
font-family: arial;
|
||||
}
|
||||
|
||||
#mocha .test.pass.medium .duration {
|
||||
background: #C09853;
|
||||
}
|
||||
|
||||
#mocha .test.pass.slow .duration {
|
||||
background: #B94A48;
|
||||
}
|
||||
|
||||
#mocha .test.pass::before {
|
||||
content: '✓';
|
||||
font-size: 12px;
|
||||
display: block;
|
||||
float: left;
|
||||
margin-right: 5px;
|
||||
color: #00d6b2;
|
||||
}
|
||||
|
||||
#mocha .test.pass .duration {
|
||||
font-size: 9px;
|
||||
margin-left: 5px;
|
||||
padding: 2px 5px;
|
||||
color: white;
|
||||
-webkit-box-shadow: inset 0 1px 1px rgba(0,0,0,.2);
|
||||
-moz-box-shadow: inset 0 1px 1px rgba(0,0,0,.2);
|
||||
box-shadow: inset 0 1px 1px rgba(0,0,0,.2);
|
||||
-webkit-border-radius: 5px;
|
||||
-moz-border-radius: 5px;
|
||||
-ms-border-radius: 5px;
|
||||
-o-border-radius: 5px;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
#mocha .test.pass.fast .duration {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#mocha .test.pending {
|
||||
color: #0b97c4;
|
||||
}
|
||||
|
||||
#mocha .test.pending::before {
|
||||
content: '◦';
|
||||
color: #0b97c4;
|
||||
}
|
||||
|
||||
#mocha .test.fail {
|
||||
color: #c00;
|
||||
}
|
||||
|
||||
#mocha .test.fail pre {
|
||||
color: black;
|
||||
}
|
||||
|
||||
#mocha .test.fail::before {
|
||||
content: '✖';
|
||||
font-size: 12px;
|
||||
display: block;
|
||||
float: left;
|
||||
margin-right: 5px;
|
||||
color: #c00;
|
||||
}
|
||||
|
||||
#mocha .test pre.error {
|
||||
color: #c00;
|
||||
}
|
||||
|
||||
#mocha .test pre {
|
||||
display: inline-block;
|
||||
font: 12px/1.5 monaco, monospace;
|
||||
margin: 5px;
|
||||
padding: 15px;
|
||||
border: 1px solid #eee;
|
||||
border-bottom-color: #ddd;
|
||||
-webkit-border-radius: 3px;
|
||||
-webkit-box-shadow: 0 1px 3px #eee;
|
||||
}
|
||||
|
||||
#report.pass .test.fail {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#report.fail .test.pass {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#error {
|
||||
color: #c00;
|
||||
font-size: 1.5 em;
|
||||
font-weight: 100;
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
|
||||
#stats {
|
||||
position: absolute;
|
||||
top: 15px;
|
||||
right: 10px;
|
||||
font-size: 12px;
|
||||
margin: 0;
|
||||
color: #888;
|
||||
}
|
||||
|
||||
#stats .progress {
|
||||
float: right;
|
||||
padding-top: 0;
|
||||
}
|
||||
|
||||
#stats em {
|
||||
color: black;
|
||||
}
|
||||
|
||||
#stats a {
|
||||
text-decoration: none;
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
#stats a:hover {
|
||||
border-bottom: 1px solid #eee;
|
||||
}
|
||||
|
||||
#stats li {
|
||||
display: inline-block;
|
||||
margin: 0 5px;
|
||||
list-style: none;
|
||||
padding-top: 11px;
|
||||
}
|
||||
|
||||
code .comment { color: #ddd }
|
||||
code .init { color: #2F6FAD }
|
||||
code .string { color: #5890AD }
|
||||
code .keyword { color: #8A6343 }
|
||||
code .number { color: #2F6FAD }
|
|
@ -0,0 +1,14 @@
|
|||
$(function(){
|
||||
//allow cross iframe access
|
||||
if ((!$.browser.msie) && (!($.browser.mozilla && $.browser.version.indexOf("1.8.") == 0))) {
|
||||
document.domain = document.domain; // for comet
|
||||
}
|
||||
|
||||
//initalize the test helper
|
||||
helper.init(function(){
|
||||
//configure and start the test framework
|
||||
//mocha.suite.timeout(5000);
|
||||
mocha.ignoreLeaks();
|
||||
mocha.run();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,36 @@
|
|||
describe("bold button", function(){
|
||||
//create a new pad before each test run
|
||||
beforeEach(function(cb){
|
||||
helper.newPad(cb);
|
||||
this.timeout(5000);
|
||||
});
|
||||
|
||||
it("makes text bold", function(done) {
|
||||
var inner$ = helper.padInner$;
|
||||
var chrome$ = helper.padChrome$;
|
||||
|
||||
//get the first text element out of the inner iframe
|
||||
var $firstTextElement = inner$("div").first();
|
||||
|
||||
//select this text element
|
||||
$firstTextElement.sendkeys('{selectall}');
|
||||
|
||||
//get the bold button and click it
|
||||
var $boldButton = chrome$(".buttonicon-bold");
|
||||
$boldButton.click();
|
||||
|
||||
//ace creates a new dom element when you press a button, so just get the first text element again
|
||||
var $newFirstTextElement = inner$("div").first();
|
||||
|
||||
// is there a <b> element now?
|
||||
var isBold = $newFirstTextElement.find("b").length === 1;
|
||||
|
||||
//expect it to be bold
|
||||
expect(isBold).to.be(true);
|
||||
|
||||
//make sure the text hasn't changed
|
||||
expect($newFirstTextElement.text()).to.eql($firstTextElement.text());
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,179 @@
|
|||
describe("indentation button", function(){
|
||||
//create a new pad before each test run
|
||||
beforeEach(function(cb){
|
||||
helper.newPad(cb);
|
||||
this.timeout(5000);
|
||||
});
|
||||
|
||||
it("indent text", function(done){
|
||||
var inner$ = helper.padInner$;
|
||||
var chrome$ = helper.padChrome$;
|
||||
|
||||
var $indentButton = chrome$(".buttonicon-indent");
|
||||
$indentButton.click();
|
||||
|
||||
helper.waitFor(function(){
|
||||
return inner$("div").first().find("ul li").length === 1;
|
||||
}).done(done);
|
||||
});
|
||||
|
||||
it("keeps the indent on enter for the new line", function(done){
|
||||
var inner$ = helper.padInner$;
|
||||
var chrome$ = helper.padChrome$;
|
||||
|
||||
var $indentButton = chrome$(".buttonicon-indent");
|
||||
$indentButton.click();
|
||||
|
||||
//type a bit, make a line break and type again
|
||||
var $firstTextElement = inner$("div span").first();
|
||||
$firstTextElement.sendkeys('line 1');
|
||||
$firstTextElement.sendkeys('{enter}');
|
||||
$firstTextElement.sendkeys('line 2');
|
||||
$firstTextElement.sendkeys('{enter}');
|
||||
|
||||
helper.waitFor(function(){
|
||||
return inner$("div span").first().text().indexOf("line 2") === -1;
|
||||
}).done(function(){
|
||||
var $newSecondLine = inner$("div").first().next();
|
||||
var hasULElement = $newSecondLine.find("ul li").length === 1;
|
||||
|
||||
expect(hasULElement).to.be(true);
|
||||
expect($newSecondLine.text()).to.be("line 2");
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
/*
|
||||
|
||||
it("makes text indented and outdented", function() {
|
||||
|
||||
//get the inner iframe
|
||||
var $inner = testHelper.$getPadInner();
|
||||
|
||||
//get the first text element out of the inner iframe
|
||||
var firstTextElement = $inner.find("div").first();
|
||||
|
||||
//select this text element
|
||||
testHelper.selectText(firstTextElement[0], $inner);
|
||||
|
||||
//get the indentation button and click it
|
||||
var $indentButton = testHelper.$getPadChrome().find(".buttonicon-indent");
|
||||
$indentButton.click();
|
||||
|
||||
//ace creates a new dom element when you press a button, so just get the first text element again
|
||||
var newFirstTextElement = $inner.find("div").first();
|
||||
|
||||
// is there a list-indent class element now?
|
||||
var firstChild = newFirstTextElement.children(":first");
|
||||
var isUL = firstChild.is('ul');
|
||||
|
||||
//expect it to be the beginning of a list
|
||||
expect(isUL).to.be(true);
|
||||
|
||||
var secondChild = firstChild.children(":first");
|
||||
var isLI = secondChild.is('li');
|
||||
//expect it to be part of a list
|
||||
expect(isLI).to.be(true);
|
||||
|
||||
//indent again
|
||||
$indentButton.click();
|
||||
|
||||
var newFirstTextElement = $inner.find("div").first();
|
||||
|
||||
// is there a list-indent class element now?
|
||||
var firstChild = newFirstTextElement.children(":first");
|
||||
var hasListIndent2 = firstChild.hasClass('list-indent2');
|
||||
|
||||
//expect it to be part of a list
|
||||
expect(hasListIndent2).to.be(true);
|
||||
|
||||
//make sure the text hasn't changed
|
||||
expect(newFirstTextElement.text()).to.eql(firstTextElement.text());
|
||||
|
||||
|
||||
// test outdent
|
||||
|
||||
//get the unindentation button and click it twice
|
||||
var $outdentButton = testHelper.$getPadChrome().find(".buttonicon-outdent");
|
||||
$outdentButton.click();
|
||||
$outdentButton.click();
|
||||
|
||||
//ace creates a new dom element when you press a button, so just get the first text element again
|
||||
var newFirstTextElement = $inner.find("div").first();
|
||||
|
||||
// is there a list-indent class element now?
|
||||
var firstChild = newFirstTextElement.children(":first");
|
||||
var isUL = firstChild.is('ul');
|
||||
|
||||
//expect it not to be the beginning of a list
|
||||
expect(isUL).to.be(false);
|
||||
|
||||
var secondChild = firstChild.children(":first");
|
||||
var isLI = secondChild.is('li');
|
||||
//expect it to not be part of a list
|
||||
expect(isLI).to.be(false);
|
||||
|
||||
//make sure the text hasn't changed
|
||||
expect(newFirstTextElement.text()).to.eql(firstTextElement.text());
|
||||
|
||||
|
||||
// Next test tests multiple line indentation
|
||||
|
||||
//select this text element
|
||||
testHelper.selectText(firstTextElement[0], $inner);
|
||||
|
||||
//indent twice
|
||||
$indentButton.click();
|
||||
$indentButton.click();
|
||||
|
||||
//get the first text element out of the inner iframe
|
||||
var firstTextElement = $inner.find("div").first();
|
||||
|
||||
//select this text element
|
||||
testHelper.selectText(firstTextElement[0], $inner);
|
||||
|
||||
/* this test creates the below content, both should have double indentation
|
||||
line1
|
||||
line2
|
||||
|
||||
|
||||
firstTextElement.sendkeys('{rightarrow}'); // simulate a keypress of enter
|
||||
firstTextElement.sendkeys('{enter}'); // simulate a keypress of enter
|
||||
firstTextElement.sendkeys('line 1'); // simulate writing the first line
|
||||
firstTextElement.sendkeys('{enter}'); // simulate a keypress of enter
|
||||
firstTextElement.sendkeys('line 2'); // simulate writing the second line
|
||||
|
||||
//get the second text element out of the inner iframe
|
||||
setTimeout(function(){ // THIS IS REALLY BAD
|
||||
var secondTextElement = $('iframe').contents().find('iframe').contents().find('iframe').contents().find('body > div').get(1); // THIS IS UGLY
|
||||
|
||||
// is there a list-indent class element now?
|
||||
var firstChild = secondTextElement.children(":first");
|
||||
var isUL = firstChild.is('ul');
|
||||
|
||||
//expect it to be the beginning of a list
|
||||
expect(isUL).to.be(true);
|
||||
|
||||
var secondChild = secondChild.children(":first");
|
||||
var isLI = secondChild.is('li');
|
||||
//expect it to be part of a list
|
||||
expect(isLI).to.be(true);
|
||||
|
||||
//get the first text element out of the inner iframe
|
||||
var thirdTextElement = $('iframe').contents().find('iframe').contents().find('iframe').contents().find('body > div').get(2); // THIS IS UGLY TOO
|
||||
|
||||
// is there a list-indent class element now?
|
||||
var firstChild = thirdTextElement.children(":first");
|
||||
var isUL = firstChild.is('ul');
|
||||
|
||||
//expect it to be the beginning of a list
|
||||
expect(isUL).to.be(true);
|
||||
|
||||
var secondChild = firstChild.children(":first");
|
||||
var isLI = secondChild.is('li');
|
||||
|
||||
//expect it to be part of a list
|
||||
expect(isLI).to.be(true);
|
||||
},1000);
|
||||
});*/
|
||||
});
|
|
@ -0,0 +1,36 @@
|
|||
describe("italic button", function(){
|
||||
//create a new pad before each test run
|
||||
beforeEach(function(cb){
|
||||
helper.newPad(cb);
|
||||
this.timeout(5000);
|
||||
});
|
||||
|
||||
it("makes text italic", function(done) {
|
||||
var inner$ = helper.padInner$;
|
||||
var chrome$ = helper.padChrome$;
|
||||
|
||||
//get the first text element out of the inner iframe
|
||||
var $firstTextElement = inner$("div").first();
|
||||
|
||||
//select this text element
|
||||
$firstTextElement.sendkeys('{selectall}');
|
||||
|
||||
//get the bold button and click it
|
||||
var $boldButton = chrome$(".buttonicon-italic");
|
||||
$boldButton.click();
|
||||
|
||||
//ace creates a new dom element when you press a button, so just get the first text element again
|
||||
var $newFirstTextElement = inner$("div").first();
|
||||
|
||||
// is there a <i> element now?
|
||||
var isItalic = $newFirstTextElement.find("i").length === 1;
|
||||
|
||||
//expect it to be bold
|
||||
expect(isItalic).to.be(true);
|
||||
|
||||
//make sure the text hasn't changed
|
||||
expect($newFirstTextElement.text()).to.eql($firstTextElement.text());
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,133 @@
|
|||
describe("embed links", function(){
|
||||
var objectify = function (str)
|
||||
{
|
||||
var hash = {};
|
||||
var parts = str.split('&');
|
||||
for(var i = 0; i < parts.length; i++)
|
||||
{
|
||||
var keyValue = parts[i].split('=');
|
||||
hash[keyValue[0]] = keyValue[1];
|
||||
}
|
||||
return hash;
|
||||
}
|
||||
|
||||
var checkiFrameCode = function(embedCode, readonly){
|
||||
//turn the code into an html element
|
||||
var $embediFrame = $(embedCode);
|
||||
|
||||
//read and check the frame attributes
|
||||
var width = $embediFrame.attr("width");
|
||||
var height = $embediFrame.attr("height");
|
||||
var name = $embediFrame.attr("name");
|
||||
expect(width).to.be('600');
|
||||
expect(height).to.be('400');
|
||||
expect(name).to.be(readonly ? "embed_readonly" : "embed_readwrite");
|
||||
|
||||
//parse the url
|
||||
var src = $embediFrame.attr("src");
|
||||
var questionMark = src.indexOf("?");
|
||||
var url = src.substr(0,questionMark);
|
||||
var paramsStr = src.substr(questionMark+1);
|
||||
var params = objectify(paramsStr);
|
||||
|
||||
var expectedParams = {
|
||||
showControls: 'true'
|
||||
, showChat: 'true'
|
||||
, showLineNumbers: 'true'
|
||||
, useMonospaceFont: 'false'
|
||||
}
|
||||
|
||||
//check the url
|
||||
if(readonly){
|
||||
expect(url.indexOf("r.") > 0).to.be(true);
|
||||
} else {
|
||||
expect(url).to.be(helper.padChrome$.window.location.href);
|
||||
}
|
||||
|
||||
//check if all parts of the url are like expected
|
||||
expect(params).to.eql(expectedParams);
|
||||
}
|
||||
|
||||
describe("read and write", function(){
|
||||
//create a new pad before each test run
|
||||
beforeEach(function(cb){
|
||||
helper.newPad(cb);
|
||||
this.timeout(5000);
|
||||
});
|
||||
|
||||
describe("the share link", function(){
|
||||
it("is the actual pad url", function(done){
|
||||
var chrome$ = helper.padChrome$;
|
||||
|
||||
//open share dropdown
|
||||
chrome$(".buttonicon-embed").click();
|
||||
|
||||
//get the link of the share field + the actual pad url and compare them
|
||||
var shareLink = chrome$("#linkinput").val();
|
||||
var padURL = chrome$.window.location.href;
|
||||
expect(shareLink).to.be(padURL);
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
describe("the embed as iframe code", function(){
|
||||
it("is an iframe with the the correct url parameters and correct size", function(done){
|
||||
var chrome$ = helper.padChrome$;
|
||||
|
||||
//open share dropdown
|
||||
chrome$(".buttonicon-embed").click();
|
||||
|
||||
//get the link of the share field + the actual pad url and compare them
|
||||
var embedCode = chrome$("#embedinput").val();
|
||||
|
||||
checkiFrameCode(embedCode, false)
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("when read only option is set", function(){
|
||||
beforeEach(function(cb){
|
||||
helper.newPad(cb);
|
||||
this.timeout(5000);
|
||||
});
|
||||
|
||||
describe("the share link", function(){
|
||||
it("shows a read only url", function(done){
|
||||
var chrome$ = helper.padChrome$;
|
||||
|
||||
//open share dropdown
|
||||
chrome$(".buttonicon-embed").click();
|
||||
//check read only checkbox, a bit hacky
|
||||
chrome$('#readonlyinput').attr('checked','checked').click().attr('checked','checked');
|
||||
|
||||
//get the link of the share field + the actual pad url and compare them
|
||||
var shareLink = chrome$("#linkinput").val();
|
||||
var containsReadOnlyLink = shareLink.indexOf("r.") > 0
|
||||
expect(containsReadOnlyLink).to.be(true);
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
describe("the embed as iframe code", function(){
|
||||
it("is an iframe with the the correct url parameters and correct size", function(done){
|
||||
var chrome$ = helper.padChrome$;
|
||||
|
||||
//open share dropdown
|
||||
chrome$(".buttonicon-embed").click();
|
||||
//check read only checkbox, a bit hacky
|
||||
chrome$('#readonlyinput').attr('checked','checked').click().attr('checked','checked');
|
||||
|
||||
//get the link of the share field + the actual pad url and compare them
|
||||
var embedCode = chrome$("#embedinput").val();
|
||||
|
||||
checkiFrameCode(embedCode, true);
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,30 @@
|
|||
describe("font select", function(){
|
||||
//create a new pad before each test run
|
||||
beforeEach(function(cb){
|
||||
helper.newPad(cb);
|
||||
this.timeout(5000);
|
||||
});
|
||||
|
||||
it("makes text monospace", function(done) {
|
||||
var inner$ = helper.padInner$;
|
||||
var chrome$ = helper.padChrome$;
|
||||
|
||||
//click on the settings button to make settings visible
|
||||
var $settingsButton = chrome$(".buttonicon-settings");
|
||||
$settingsButton.click();
|
||||
|
||||
//get the font menu and monospace option
|
||||
var $viewfontmenu = chrome$("#viewfontmenu");
|
||||
var $monospaceoption = $viewfontmenu.find("[value=monospace]");
|
||||
|
||||
//select monospace and fire change event
|
||||
$monospaceoption.attr('selected','selected');
|
||||
$viewfontmenu.change();
|
||||
|
||||
//check if font changed to monospace
|
||||
var fontFamily = inner$("body").css("font-family").toLowerCase();
|
||||
expect(fontFamily).to.be("monospace");
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,99 @@
|
|||
describe("the test helper", function(){
|
||||
describe("the newPad method", function(){
|
||||
xit("doesn't leak memory if you creates iframes over and over again", function(done){
|
||||
this.timeout(100000);
|
||||
|
||||
var times = 10;
|
||||
|
||||
var loadPad = function(){
|
||||
helper.newPad(function(){
|
||||
times--;
|
||||
if(times > 0){
|
||||
loadPad();
|
||||
} else {
|
||||
done();
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
loadPad();
|
||||
});
|
||||
|
||||
it("gives me 3 jquery instances of chrome, outer and inner", function(done){
|
||||
this.timeout(5000);
|
||||
|
||||
helper.newPad(function(){
|
||||
//check if the jquery selectors have the desired elements
|
||||
expect(helper.padChrome$("#editbar").length).to.be(1);
|
||||
expect(helper.padOuter$("#outerdocbody").length).to.be(1);
|
||||
expect(helper.padInner$("#innerdocbody").length).to.be(1);
|
||||
|
||||
//check if the document object was set correctly
|
||||
expect(helper.padChrome$.window.document).to.be(helper.padChrome$.document);
|
||||
expect(helper.padOuter$.window.document).to.be(helper.padOuter$.document);
|
||||
expect(helper.padInner$.window.document).to.be(helper.padInner$.document);
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("the waitFor method", function(){
|
||||
it("takes a timeout and waits long enough", function(done){
|
||||
this.timeout(2000);
|
||||
var startTime = new Date().getTime();
|
||||
|
||||
helper.waitFor(function(){
|
||||
return false;
|
||||
}, 1500).fail(function(){
|
||||
var duration = new Date().getTime() - startTime;
|
||||
expect(duration).to.be.greaterThan(1400);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it("takes an interval and checks on every interval", function(done){
|
||||
this.timeout(4000);
|
||||
var checks = 0;
|
||||
|
||||
helper.waitFor(function(){
|
||||
checks++;
|
||||
return false;
|
||||
}, 2000, 100).fail(function(){
|
||||
expect(checks).to.be.greaterThan(18);
|
||||
expect(checks).to.be.lessThan(22);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
describe("returns a deferred object", function(){
|
||||
it("it calls done after success", function(done){
|
||||
helper.waitFor(function(){
|
||||
return true;
|
||||
}).done(function(){
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it("calls fail after failure", function(done){
|
||||
helper.waitFor(function(){
|
||||
return false;
|
||||
},0).fail(function(){
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
xit("throws if you don't listen for fails", function(done){
|
||||
var onerror = window.onerror;
|
||||
window.onerror = function(){
|
||||
window.onerror = onerror;
|
||||
done();
|
||||
}
|
||||
|
||||
helper.waitFor(function(){
|
||||
return false;
|
||||
},100);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,37 @@
|
|||
describe("delete keystroke", function(){
|
||||
//create a new pad before each test run
|
||||
beforeEach(function(cb){
|
||||
helper.newPad(cb);
|
||||
this.timeout(5000);
|
||||
});
|
||||
|
||||
it("makes text delete", function(done) {
|
||||
var inner$ = helper.padInner$;
|
||||
var chrome$ = helper.padChrome$;
|
||||
|
||||
//get the first text element out of the inner iframe
|
||||
var $firstTextElement = inner$("div").first();
|
||||
|
||||
// get the original length of this element
|
||||
var elementLength = $firstTextElement.text().length;
|
||||
|
||||
// get the original string value minus the last char
|
||||
var originalTextValue = $firstTextElement.text();
|
||||
originalTextValueMinusFirstChar = originalTextValue.substring(1, originalTextValue.length );
|
||||
|
||||
// simulate key presses to delete content
|
||||
$firstTextElement.sendkeys('{leftarrow}'); // simulate a keypress of the left arrow key
|
||||
$firstTextElement.sendkeys('{del}'); // simulate a keypress of delete
|
||||
|
||||
//ace creates a new dom element when you press a keystroke, so just get the first text element again
|
||||
var $newFirstTextElement = inner$("div").first();
|
||||
|
||||
// get the new length of this element
|
||||
var newElementLength = $newFirstTextElement.text().length;
|
||||
|
||||
//expect it to be one char less in length
|
||||
expect(newElementLength).to.be((elementLength-1));
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,24 @@
|
|||
describe("urls", function(){
|
||||
//create a new pad before each test run
|
||||
beforeEach(function(cb){
|
||||
helper.newPad(cb);
|
||||
this.timeout(5000);
|
||||
});
|
||||
|
||||
it("when you enter an url, it becomes clickable", function(done) {
|
||||
var inner$ = helper.padInner$;
|
||||
var chrome$ = helper.padChrome$;
|
||||
|
||||
//get the first text element out of the inner iframe
|
||||
var firstTextElement = inner$("div").first();
|
||||
|
||||
// simulate key presses to delete content
|
||||
firstTextElement.sendkeys('{selectall}'); // select all
|
||||
firstTextElement.sendkeys('{del}'); // clear the first line
|
||||
firstTextElement.sendkeys('http://etherpad.org'); // insert a URL
|
||||
|
||||
helper.waitFor(function(){
|
||||
return inner$("div").first().find("a").length === 1;
|
||||
}, 2000).done(done);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,18 @@
|
|||
Copyright Joyent, Inc. and other Node contributors. All rights reserved.
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to
|
||||
deal in the Software without restriction, including without limitation the
|
||||
rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
|
||||
sell copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
|
||||
IN THE SOFTWARE.
|
|
@ -0,0 +1,76 @@
|
|||
Here's how the node docs work.
|
||||
|
||||
Each type of heading has a description block.
|
||||
|
||||
|
||||
## module
|
||||
|
||||
Stability: 3 - Stable
|
||||
|
||||
description and examples.
|
||||
|
||||
### module.property
|
||||
|
||||
* Type
|
||||
|
||||
description of the property.
|
||||
|
||||
### module.someFunction(x, y, [z=100])
|
||||
|
||||
* `x` {String} the description of the string
|
||||
* `y` {Boolean} Should I stay or should I go?
|
||||
* `z` {Number} How many zebras to bring.
|
||||
|
||||
A description of the function.
|
||||
|
||||
### Event: 'blerg'
|
||||
|
||||
* Argument: SomeClass object.
|
||||
|
||||
Modules don't usually raise events on themselves. `cluster` is the
|
||||
only exception.
|
||||
|
||||
## Class: SomeClass
|
||||
|
||||
description of the class.
|
||||
|
||||
### Class Method: SomeClass.classMethod(anArg)
|
||||
|
||||
* `anArg` {Object} Just an argument
|
||||
* `field` {String} anArg can have this field.
|
||||
* `field2` {Boolean} Another field. Default: `false`.
|
||||
* Return: {Boolean} `true` if it worked.
|
||||
|
||||
Description of the method for humans.
|
||||
|
||||
### someClass.nextSibling()
|
||||
|
||||
* Return: {SomeClass object | null} The next someClass in line.
|
||||
|
||||
### someClass.someProperty
|
||||
|
||||
* String
|
||||
|
||||
The indication of what someProperty is.
|
||||
|
||||
### Event: 'grelb'
|
||||
|
||||
* `isBlerg` {Boolean}
|
||||
|
||||
This event is emitted on instances of SomeClass, not on the module itself.
|
||||
|
||||
|
||||
* Modules have (description, Properties, Functions, Classes, Examples)
|
||||
* Properties have (type, description)
|
||||
* Functions have (list of arguments, description)
|
||||
* Classes have (description, Properties, Methods, Events)
|
||||
* Events have (list of arguments, description)
|
||||
* Methods have (list of arguments, description)
|
||||
* Properties have (type, description)
|
||||
|
||||
# CLI usage
|
||||
|
||||
Run the following from the etherpad-lite root directory:
|
||||
```sh
|
||||
$ node tools/doc/generate doc/all.md --format=html --template=doc/template.html > out.htm
|
||||
```
|
|
@ -0,0 +1,120 @@
|
|||
#!/usr/bin/env node
|
||||
// Copyright Joyent, Inc. and other Node contributors.
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a
|
||||
// copy of this software and associated documentation files (the
|
||||
// "Software"), to deal in the Software without restriction, including
|
||||
// without limitation the rights to use, copy, modify, merge, publish,
|
||||
// distribute, sublicense, and/or sell copies of the Software, and to permit
|
||||
// persons to whom the Software is furnished to do so, subject to the
|
||||
// following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included
|
||||
// in all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
||||
// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
|
||||
// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
|
||||
// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
|
||||
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
|
||||
// USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
var marked = require('marked');
|
||||
var fs = require('fs');
|
||||
var path = require('path');
|
||||
|
||||
// parse the args.
|
||||
// Don't use nopt or whatever for this. It's simple enough.
|
||||
|
||||
var args = process.argv.slice(2);
|
||||
var format = 'json';
|
||||
var template = null;
|
||||
var inputFile = null;
|
||||
|
||||
args.forEach(function (arg) {
|
||||
if (!arg.match(/^\-\-/)) {
|
||||
inputFile = arg;
|
||||
} else if (arg.match(/^\-\-format=/)) {
|
||||
format = arg.replace(/^\-\-format=/, '');
|
||||
} else if (arg.match(/^\-\-template=/)) {
|
||||
template = arg.replace(/^\-\-template=/, '');
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
if (!inputFile) {
|
||||
throw new Error('No input file specified');
|
||||
}
|
||||
|
||||
|
||||
console.error('Input file = %s', inputFile);
|
||||
fs.readFile(inputFile, 'utf8', function(er, input) {
|
||||
if (er) throw er;
|
||||
// process the input for @include lines
|
||||
processIncludes(inputFile, input, next);
|
||||
});
|
||||
|
||||
|
||||
var includeExpr = /^@include\s+([A-Za-z0-9-_\/]+)(?:\.)?([a-zA-Z]*)$/gmi;
|
||||
var includeData = {};
|
||||
function processIncludes(inputFile, input, cb) {
|
||||
var includes = input.match(includeExpr);
|
||||
if (includes === null) return cb(null, input);
|
||||
var errState = null;
|
||||
console.error(includes);
|
||||
var incCount = includes.length;
|
||||
if (incCount === 0) cb(null, input);
|
||||
|
||||
includes.forEach(function(include) {
|
||||
var fname = include.replace(/^@include\s+/, '');
|
||||
if (!fname.match(/\.md$/)) fname += '.md';
|
||||
|
||||
if (includeData.hasOwnProperty(fname)) {
|
||||
input = input.split(include).join(includeData[fname]);
|
||||
incCount--;
|
||||
if (incCount === 0) {
|
||||
return cb(null, input);
|
||||
}
|
||||
}
|
||||
|
||||
var fullFname = path.resolve(path.dirname(inputFile), fname);
|
||||
fs.readFile(fullFname, 'utf8', function(er, inc) {
|
||||
if (errState) return;
|
||||
if (er) return cb(errState = er);
|
||||
processIncludes(fullFname, inc, function(er, inc) {
|
||||
if (errState) return;
|
||||
if (er) return cb(errState = er);
|
||||
incCount--;
|
||||
includeData[fname] = inc;
|
||||
input = input.split(include).join(includeData[fname]);
|
||||
if (incCount === 0) {
|
||||
return cb(null, input);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
function next(er, input) {
|
||||
if (er) throw er;
|
||||
switch (format) {
|
||||
case 'json':
|
||||
require('./json.js')(input, inputFile, function(er, obj) {
|
||||
console.log(JSON.stringify(obj, null, 2));
|
||||
if (er) throw er;
|
||||
});
|
||||
break;
|
||||
|
||||
case 'html':
|
||||
require('./html.js')(input, inputFile, template, function(er, html) {
|
||||
if (er) throw er;
|
||||
console.log(html);
|
||||
});
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new Error('Invalid format: ' + format);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,174 @@
|
|||
// Copyright Joyent, Inc. and other Node contributors.
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a
|
||||
// copy of this software and associated documentation files (the
|
||||
// "Software"), to deal in the Software without restriction, including
|
||||
// without limitation the rights to use, copy, modify, merge, publish,
|
||||
// distribute, sublicense, and/or sell copies of the Software, and to permit
|
||||
// persons to whom the Software is furnished to do so, subject to the
|
||||
// following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included
|
||||
// in all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
||||
// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
|
||||
// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
|
||||
// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
|
||||
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
|
||||
// USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
var fs = require('fs');
|
||||
var marked = require('marked');
|
||||
var path = require('path');
|
||||
|
||||
module.exports = toHTML;
|
||||
|
||||
function toHTML(input, filename, template, cb) {
|
||||
var lexed = marked.lexer(input);
|
||||
fs.readFile(template, 'utf8', function(er, template) {
|
||||
if (er) return cb(er);
|
||||
render(lexed, filename, template, cb);
|
||||
});
|
||||
}
|
||||
|
||||
function render(lexed, filename, template, cb) {
|
||||
// get the section
|
||||
var section = getSection(lexed);
|
||||
|
||||
filename = path.basename(filename, '.md');
|
||||
|
||||
lexed = parseLists(lexed);
|
||||
|
||||
// generate the table of contents.
|
||||
// this mutates the lexed contents in-place.
|
||||
buildToc(lexed, filename, function(er, toc) {
|
||||
if (er) return cb(er);
|
||||
|
||||
template = template.replace(/__FILENAME__/g, filename);
|
||||
template = template.replace(/__SECTION__/g, section);
|
||||
template = template.replace(/__VERSION__/g, process.version);
|
||||
template = template.replace(/__TOC__/g, toc);
|
||||
|
||||
// content has to be the last thing we do with
|
||||
// the lexed tokens, because it's destructive.
|
||||
content = marked.parser(lexed);
|
||||
template = template.replace(/__CONTENT__/g, content);
|
||||
|
||||
cb(null, template);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
// just update the list item text in-place.
|
||||
// lists that come right after a heading are what we're after.
|
||||
function parseLists(input) {
|
||||
var state = null;
|
||||
var depth = 0;
|
||||
var output = [];
|
||||
output.links = input.links;
|
||||
input.forEach(function(tok) {
|
||||
if (state === null) {
|
||||
if (tok.type === 'heading') {
|
||||
state = 'AFTERHEADING';
|
||||
}
|
||||
output.push(tok);
|
||||
return;
|
||||
}
|
||||
if (state === 'AFTERHEADING') {
|
||||
if (tok.type === 'list_start') {
|
||||
state = 'LIST';
|
||||
if (depth === 0) {
|
||||
output.push({ type:'html', text: '<div class="signature">' });
|
||||
}
|
||||
depth++;
|
||||
output.push(tok);
|
||||
return;
|
||||
}
|
||||
state = null;
|
||||
output.push(tok);
|
||||
return;
|
||||
}
|
||||
if (state === 'LIST') {
|
||||
if (tok.type === 'list_start') {
|
||||
depth++;
|
||||
output.push(tok);
|
||||
return;
|
||||
}
|
||||
if (tok.type === 'list_end') {
|
||||
depth--;
|
||||
if (depth === 0) {
|
||||
state = null;
|
||||
output.push({ type:'html', text: '</div>' });
|
||||
}
|
||||
output.push(tok);
|
||||
return;
|
||||
}
|
||||
if (tok.text) {
|
||||
tok.text = parseListItem(tok.text);
|
||||
}
|
||||
}
|
||||
output.push(tok);
|
||||
});
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
|
||||
function parseListItem(text) {
|
||||
text = text.replace(/\{([^\}]+)\}/, '<span class="type">$1</span>');
|
||||
//XXX maybe put more stuff here?
|
||||
return text;
|
||||
}
|
||||
|
||||
|
||||
// section is just the first heading
|
||||
function getSection(lexed) {
|
||||
var section = '';
|
||||
for (var i = 0, l = lexed.length; i < l; i++) {
|
||||
var tok = lexed[i];
|
||||
if (tok.type === 'heading') return tok.text;
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
|
||||
function buildToc(lexed, filename, cb) {
|
||||
var indent = 0;
|
||||
var toc = [];
|
||||
var depth = 0;
|
||||
lexed.forEach(function(tok) {
|
||||
if (tok.type !== 'heading') return;
|
||||
if (tok.depth - depth > 1) {
|
||||
return cb(new Error('Inappropriate heading level\n' +
|
||||
JSON.stringify(tok)));
|
||||
}
|
||||
|
||||
depth = tok.depth;
|
||||
var id = getId(filename + '_' + tok.text.trim());
|
||||
toc.push(new Array((depth - 1) * 2 + 1).join(' ') +
|
||||
'* <a href="#' + id + '">' +
|
||||
tok.text + '</a>');
|
||||
tok.text += '<span><a class="mark" href="#' + id + '" ' +
|
||||
'id="' + id + '">#</a></span>';
|
||||
});
|
||||
|
||||
toc = marked.parse(toc.join('\n'));
|
||||
cb(null, toc);
|
||||
}
|
||||
|
||||
var idCounters = {};
|
||||
function getId(text) {
|
||||
text = text.toLowerCase();
|
||||
text = text.replace(/[^a-z0-9]+/g, '_');
|
||||
text = text.replace(/^_+|_+$/, '');
|
||||
text = text.replace(/^([^a-z])/, '_$1');
|
||||
if (idCounters.hasOwnProperty(text)) {
|
||||
text += '_' + (++idCounters[text]);
|
||||
} else {
|
||||
idCounters[text] = 0;
|
||||
}
|
||||
return text;
|
||||
}
|
||||
|
|
@ -0,0 +1,557 @@
|
|||
// Copyright Joyent, Inc. and other Node contributors.
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a
|
||||
// copy of this software and associated documentation files (the
|
||||
// "Software"), to deal in the Software without restriction, including
|
||||
// without limitation the rights to use, copy, modify, merge, publish,
|
||||
// distribute, sublicense, and/or sell copies of the Software, and to permit
|
||||
// persons to whom the Software is furnished to do so, subject to the
|
||||
// following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included
|
||||
// in all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
||||
// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
|
||||
// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
|
||||
// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
|
||||
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
|
||||
// USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
module.exports = doJSON;
|
||||
|
||||
// Take the lexed input, and return a JSON-encoded object
|
||||
// A module looks like this: https://gist.github.com/1777387
|
||||
|
||||
var marked = require('marked');
|
||||
|
||||
function doJSON(input, filename, cb) {
|
||||
var root = {source: filename};
|
||||
var stack = [root];
|
||||
var depth = 0;
|
||||
var current = root;
|
||||
var state = null;
|
||||
var lexed = marked.lexer(input);
|
||||
lexed.forEach(function (tok) {
|
||||
var type = tok.type;
|
||||
var text = tok.text;
|
||||
|
||||
// <!-- type = module -->
|
||||
// This is for cases where the markdown semantic structure is lacking.
|
||||
if (type === 'paragraph' || type === 'html') {
|
||||
var metaExpr = /<!--([^=]+)=([^\-]+)-->\n*/g;
|
||||
text = text.replace(metaExpr, function(_0, k, v) {
|
||||
current[k.trim()] = v.trim();
|
||||
return '';
|
||||
});
|
||||
text = text.trim();
|
||||
if (!text) return;
|
||||
}
|
||||
|
||||
if (type === 'heading' &&
|
||||
!text.trim().match(/^example/i)) {
|
||||
if (tok.depth - depth > 1) {
|
||||
return cb(new Error('Inappropriate heading level\n'+
|
||||
JSON.stringify(tok)));
|
||||
}
|
||||
|
||||
// Sometimes we have two headings with a single
|
||||
// blob of description. Treat as a clone.
|
||||
if (current &&
|
||||
state === 'AFTERHEADING' &&
|
||||
depth === tok.depth) {
|
||||
var clone = current;
|
||||
current = newSection(tok);
|
||||
current.clone = clone;
|
||||
// don't keep it around on the stack.
|
||||
stack.pop();
|
||||
} else {
|
||||
// if the level is greater than the current depth,
|
||||
// then it's a child, so we should just leave the stack
|
||||
// as it is.
|
||||
// However, if it's a sibling or higher, then it implies
|
||||
// the closure of the other sections that came before.
|
||||
// root is always considered the level=0 section,
|
||||
// and the lowest heading is 1, so this should always
|
||||
// result in having a valid parent node.
|
||||
var d = tok.depth;
|
||||
while (d <= depth) {
|
||||
finishSection(stack.pop(), stack[stack.length - 1]);
|
||||
d++;
|
||||
}
|
||||
current = newSection(tok);
|
||||
}
|
||||
|
||||
depth = tok.depth;
|
||||
stack.push(current);
|
||||
state = 'AFTERHEADING';
|
||||
return;
|
||||
} // heading
|
||||
|
||||
// Immediately after a heading, we can expect the following
|
||||
//
|
||||
// { type: 'code', text: 'Stability: ...' },
|
||||
//
|
||||
// a list: starting with list_start, ending with list_end,
|
||||
// maybe containing other nested lists in each item.
|
||||
//
|
||||
// If one of these isnt' found, then anything that comes between
|
||||
// here and the next heading should be parsed as the desc.
|
||||
var stability
|
||||
if (state === 'AFTERHEADING') {
|
||||
if (type === 'code' &&
|
||||
(stability = text.match(/^Stability: ([0-5])(?:\s*-\s*)?(.*)$/))) {
|
||||
current.stability = parseInt(stability[1], 10);
|
||||
current.stabilityText = stability[2].trim();
|
||||
return;
|
||||
} else if (type === 'list_start' && !tok.ordered) {
|
||||
state = 'AFTERHEADING_LIST';
|
||||
current.list = current.list || [];
|
||||
current.list.push(tok);
|
||||
current.list.level = 1;
|
||||
} else {
|
||||
current.desc = current.desc || [];
|
||||
if (!Array.isArray(current.desc)) {
|
||||
current.shortDesc = current.desc;
|
||||
current.desc = [];
|
||||
}
|
||||
current.desc.push(tok);
|
||||
state = 'DESC';
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (state === 'AFTERHEADING_LIST') {
|
||||
current.list.push(tok);
|
||||
if (type === 'list_start') {
|
||||
current.list.level++;
|
||||
} else if (type === 'list_end') {
|
||||
current.list.level--;
|
||||
}
|
||||
if (current.list.level === 0) {
|
||||
state = 'AFTERHEADING';
|
||||
processList(current);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
current.desc = current.desc || [];
|
||||
current.desc.push(tok);
|
||||
|
||||
});
|
||||
|
||||
// finish any sections left open
|
||||
while (root !== (current = stack.pop())) {
|
||||
finishSection(current, stack[stack.length - 1]);
|
||||
}
|
||||
|
||||
return cb(null, root)
|
||||
}
|
||||
|
||||
|
||||
// go from something like this:
|
||||
// [ { type: 'list_item_start' },
|
||||
// { type: 'text',
|
||||
// text: '`settings` Object, Optional' },
|
||||
// { type: 'list_start', ordered: false },
|
||||
// { type: 'list_item_start' },
|
||||
// { type: 'text',
|
||||
// text: 'exec: String, file path to worker file. Default: `__filename`' },
|
||||
// { type: 'list_item_end' },
|
||||
// { type: 'list_item_start' },
|
||||
// { type: 'text',
|
||||
// text: 'args: Array, string arguments passed to worker.' },
|
||||
// { type: 'text',
|
||||
// text: 'Default: `process.argv.slice(2)`' },
|
||||
// { type: 'list_item_end' },
|
||||
// { type: 'list_item_start' },
|
||||
// { type: 'text',
|
||||
// text: 'silent: Boolean, whether or not to send output to parent\'s stdio.' },
|
||||
// { type: 'text', text: 'Default: `false`' },
|
||||
// { type: 'space' },
|
||||
// { type: 'list_item_end' },
|
||||
// { type: 'list_end' },
|
||||
// { type: 'list_item_end' },
|
||||
// { type: 'list_end' } ]
|
||||
// to something like:
|
||||
// [ { name: 'settings',
|
||||
// type: 'object',
|
||||
// optional: true,
|
||||
// settings:
|
||||
// [ { name: 'exec',
|
||||
// type: 'string',
|
||||
// desc: 'file path to worker file',
|
||||
// default: '__filename' },
|
||||
// { name: 'args',
|
||||
// type: 'array',
|
||||
// default: 'process.argv.slice(2)',
|
||||
// desc: 'string arguments passed to worker.' },
|
||||
// { name: 'silent',
|
||||
// type: 'boolean',
|
||||
// desc: 'whether or not to send output to parent\'s stdio.',
|
||||
// default: 'false' } ] } ]
|
||||
|
||||
function processList(section) {
|
||||
var list = section.list;
|
||||
var values = [];
|
||||
var current;
|
||||
var stack = [];
|
||||
|
||||
// for now, *just* build the heirarchical list
|
||||
list.forEach(function(tok) {
|
||||
var type = tok.type;
|
||||
if (type === 'space') return;
|
||||
if (type === 'list_item_start') {
|
||||
if (!current) {
|
||||
var n = {};
|
||||
values.push(n);
|
||||
current = n;
|
||||
} else {
|
||||
current.options = current.options || [];
|
||||
stack.push(current);
|
||||
var n = {};
|
||||
current.options.push(n);
|
||||
current = n;
|
||||
}
|
||||
return;
|
||||
} else if (type === 'list_item_end') {
|
||||
if (!current) {
|
||||
throw new Error('invalid list - end without current item\n' +
|
||||
JSON.stringify(tok) + '\n' +
|
||||
JSON.stringify(list));
|
||||
}
|
||||
current = stack.pop();
|
||||
} else if (type === 'text') {
|
||||
if (!current) {
|
||||
throw new Error('invalid list - text without current item\n' +
|
||||
JSON.stringify(tok) + '\n' +
|
||||
JSON.stringify(list));
|
||||
}
|
||||
current.textRaw = current.textRaw || '';
|
||||
current.textRaw += tok.text + ' ';
|
||||
}
|
||||
});
|
||||
|
||||
// shove the name in there for properties, since they are always
|
||||
// just going to be the value etc.
|
||||
if (section.type === 'property' && values[0]) {
|
||||
values[0].textRaw = '`' + section.name + '` ' + values[0].textRaw;
|
||||
}
|
||||
|
||||
// now pull the actual values out of the text bits.
|
||||
values.forEach(parseListItem);
|
||||
|
||||
// Now figure out what this list actually means.
|
||||
// depending on the section type, the list could be different things.
|
||||
|
||||
switch (section.type) {
|
||||
case 'ctor':
|
||||
case 'classMethod':
|
||||
case 'method':
|
||||
// each item is an argument, unless the name is 'return',
|
||||
// in which case it's the return value.
|
||||
section.signatures = section.signatures || [];
|
||||
var sig = {}
|
||||
section.signatures.push(sig);
|
||||
sig.params = values.filter(function(v) {
|
||||
if (v.name === 'return') {
|
||||
sig.return = v;
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
parseSignature(section.textRaw, sig);
|
||||
break;
|
||||
|
||||
case 'property':
|
||||
// there should be only one item, which is the value.
|
||||
// copy the data up to the section.
|
||||
var value = values[0] || {};
|
||||
delete value.name;
|
||||
section.typeof = value.type;
|
||||
delete value.type;
|
||||
Object.keys(value).forEach(function(k) {
|
||||
section[k] = value[k];
|
||||
});
|
||||
break;
|
||||
|
||||
case 'event':
|
||||
// event: each item is an argument.
|
||||
section.params = values;
|
||||
break;
|
||||
}
|
||||
|
||||
// section.listParsed = values;
|
||||
delete section.list;
|
||||
}
|
||||
|
||||
|
||||
// textRaw = "someobject.someMethod(a, [b=100], [c])"
|
||||
function parseSignature(text, sig) {
|
||||
var params = text.match(paramExpr);
|
||||
if (!params) return;
|
||||
params = params[1];
|
||||
// the ] is irrelevant. [ indicates optionalness.
|
||||
params = params.replace(/\]/g, '');
|
||||
params = params.split(/,/)
|
||||
params.forEach(function(p, i, _) {
|
||||
p = p.trim();
|
||||
if (!p) return;
|
||||
var param = sig.params[i];
|
||||
var optional = false;
|
||||
var def;
|
||||
// [foo] -> optional
|
||||
if (p.charAt(0) === '[') {
|
||||
optional = true;
|
||||
p = p.substr(1);
|
||||
}
|
||||
var eq = p.indexOf('=');
|
||||
if (eq !== -1) {
|
||||
def = p.substr(eq + 1);
|
||||
p = p.substr(0, eq);
|
||||
}
|
||||
if (!param) {
|
||||
param = sig.params[i] = { name: p };
|
||||
}
|
||||
// at this point, the name should match.
|
||||
if (p !== param.name) {
|
||||
console.error('Warning: invalid param "%s"', p);
|
||||
console.error(' > ' + JSON.stringify(param));
|
||||
console.error(' > ' + text);
|
||||
}
|
||||
if (optional) param.optional = true;
|
||||
if (def !== undefined) param.default = def;
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
function parseListItem(item) {
|
||||
if (item.options) item.options.forEach(parseListItem);
|
||||
if (!item.textRaw) return;
|
||||
|
||||
// the goal here is to find the name, type, default, and optional.
|
||||
// anything left over is 'desc'
|
||||
var text = item.textRaw.trim();
|
||||
// text = text.replace(/^(Argument|Param)s?\s*:?\s*/i, '');
|
||||
|
||||
text = text.replace(/^, /, '').trim();
|
||||
var retExpr = /^returns?\s*:?\s*/i;
|
||||
var ret = text.match(retExpr);
|
||||
if (ret) {
|
||||
item.name = 'return';
|
||||
text = text.replace(retExpr, '');
|
||||
} else {
|
||||
var nameExpr = /^['`"]?([^'`": \{]+)['`"]?\s*:?\s*/;
|
||||
var name = text.match(nameExpr);
|
||||
if (name) {
|
||||
item.name = name[1];
|
||||
text = text.replace(nameExpr, '');
|
||||
}
|
||||
}
|
||||
|
||||
text = text.trim();
|
||||
var defaultExpr = /\(default\s*[:=]?\s*['"`]?([^, '"`]*)['"`]?\)/i;
|
||||
var def = text.match(defaultExpr);
|
||||
if (def) {
|
||||
item.default = def[1];
|
||||
text = text.replace(defaultExpr, '');
|
||||
}
|
||||
|
||||
text = text.trim();
|
||||
var typeExpr = /^\{([^\}]+)\}/;
|
||||
var type = text.match(typeExpr);
|
||||
if (type) {
|
||||
item.type = type[1];
|
||||
text = text.replace(typeExpr, '');
|
||||
}
|
||||
|
||||
text = text.trim();
|
||||
var optExpr = /^Optional\.|(?:, )?Optional$/;
|
||||
var optional = text.match(optExpr);
|
||||
if (optional) {
|
||||
item.optional = true;
|
||||
text = text.replace(optExpr, '');
|
||||
}
|
||||
|
||||
text = text.replace(/^\s*-\s*/, '');
|
||||
text = text.trim();
|
||||
if (text) item.desc = text;
|
||||
}
|
||||
|
||||
|
||||
function finishSection(section, parent) {
|
||||
if (!section || !parent) {
|
||||
throw new Error('Invalid finishSection call\n'+
|
||||
JSON.stringify(section) + '\n' +
|
||||
JSON.stringify(parent));
|
||||
}
|
||||
|
||||
if (!section.type) {
|
||||
section.type = 'module';
|
||||
if (parent && (parent.type === 'misc')) {
|
||||
section.type = 'misc';
|
||||
}
|
||||
section.displayName = section.name;
|
||||
section.name = section.name.toLowerCase()
|
||||
.trim().replace(/\s+/g, '_');
|
||||
}
|
||||
|
||||
if (section.desc && Array.isArray(section.desc)) {
|
||||
section.desc.links = section.desc.links || [];
|
||||
section.desc = marked.parser(section.desc);
|
||||
}
|
||||
|
||||
if (!section.list) section.list = [];
|
||||
processList(section);
|
||||
|
||||
// classes sometimes have various 'ctor' children
|
||||
// which are actually just descriptions of a constructor
|
||||
// class signature.
|
||||
// Merge them into the parent.
|
||||
if (section.type === 'class' && section.ctors) {
|
||||
section.signatures = section.signatures || [];
|
||||
var sigs = section.signatures;
|
||||
section.ctors.forEach(function(ctor) {
|
||||
ctor.signatures = ctor.signatures || [{}];
|
||||
ctor.signatures.forEach(function(sig) {
|
||||
sig.desc = ctor.desc;
|
||||
});
|
||||
sigs.push.apply(sigs, ctor.signatures);
|
||||
});
|
||||
delete section.ctors;
|
||||
}
|
||||
|
||||
// properties are a bit special.
|
||||
// their "type" is the type of object, not "property"
|
||||
if (section.properties) {
|
||||
section.properties.forEach(function (p) {
|
||||
if (p.typeof) p.type = p.typeof;
|
||||
else delete p.type;
|
||||
delete p.typeof;
|
||||
});
|
||||
}
|
||||
|
||||
// handle clones
|
||||
if (section.clone) {
|
||||
var clone = section.clone;
|
||||
delete section.clone;
|
||||
delete clone.clone;
|
||||
deepCopy(section, clone);
|
||||
finishSection(clone, parent);
|
||||
}
|
||||
|
||||
var plur;
|
||||
if (section.type.slice(-1) === 's') {
|
||||
plur = section.type + 'es';
|
||||
} else if (section.type.slice(-1) === 'y') {
|
||||
plur = section.type.replace(/y$/, 'ies');
|
||||
} else {
|
||||
plur = section.type + 's';
|
||||
}
|
||||
|
||||
// if the parent's type is 'misc', then it's just a random
|
||||
// collection of stuff, like the "globals" section.
|
||||
// Make the children top-level items.
|
||||
if (section.type === 'misc') {
|
||||
Object.keys(section).forEach(function(k) {
|
||||
switch (k) {
|
||||
case 'textRaw':
|
||||
case 'name':
|
||||
case 'type':
|
||||
case 'desc':
|
||||
case 'miscs':
|
||||
return;
|
||||
default:
|
||||
if (parent.type === 'misc') {
|
||||
return;
|
||||
}
|
||||
if (Array.isArray(k) && parent[k]) {
|
||||
parent[k] = parent[k].concat(section[k]);
|
||||
} else if (!parent[k]) {
|
||||
parent[k] = section[k];
|
||||
} else {
|
||||
// parent already has, and it's not an array.
|
||||
return;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
parent[plur] = parent[plur] || [];
|
||||
parent[plur].push(section);
|
||||
}
|
||||
|
||||
|
||||
// Not a general purpose deep copy.
|
||||
// But sufficient for these basic things.
|
||||
function deepCopy(src, dest) {
|
||||
Object.keys(src).filter(function(k) {
|
||||
return !dest.hasOwnProperty(k);
|
||||
}).forEach(function(k) {
|
||||
dest[k] = deepCopy_(src[k]);
|
||||
});
|
||||
}
|
||||
|
||||
function deepCopy_(src) {
|
||||
if (!src) return src;
|
||||
if (Array.isArray(src)) {
|
||||
var c = new Array(src.length);
|
||||
src.forEach(function(v, i) {
|
||||
c[i] = deepCopy_(v);
|
||||
});
|
||||
return c;
|
||||
}
|
||||
if (typeof src === 'object') {
|
||||
var c = {};
|
||||
Object.keys(src).forEach(function(k) {
|
||||
c[k] = deepCopy_(src[k]);
|
||||
});
|
||||
return c;
|
||||
}
|
||||
return src;
|
||||
}
|
||||
|
||||
|
||||
// these parse out the contents of an H# tag
|
||||
var eventExpr = /^Event(?::|\s)+['"]?([^"']+).*$/i;
|
||||
var classExpr = /^Class:\s*([^ ]+).*?$/i;
|
||||
var propExpr = /^(?:property:?\s*)?[^\.]+\.([^ \.\(\)]+)\s*?$/i;
|
||||
var braceExpr = /^(?:property:?\s*)?[^\.\[]+(\[[^\]]+\])\s*?$/i;
|
||||
var classMethExpr =
|
||||
/^class\s*method\s*:?[^\.]+\.([^ \.\(\)]+)\([^\)]*\)\s*?$/i;
|
||||
var methExpr =
|
||||
/^(?:method:?\s*)?(?:[^\.]+\.)?([^ \.\(\)]+)\([^\)]*\)\s*?$/i;
|
||||
var newExpr = /^new ([A-Z][a-z]+)\([^\)]*\)\s*?$/;
|
||||
var paramExpr = /\((.*)\);?$/;
|
||||
|
||||
function newSection(tok) {
|
||||
var section = {};
|
||||
// infer the type from the text.
|
||||
var text = section.textRaw = tok.text;
|
||||
if (text.match(eventExpr)) {
|
||||
section.type = 'event';
|
||||
section.name = text.replace(eventExpr, '$1');
|
||||
} else if (text.match(classExpr)) {
|
||||
section.type = 'class';
|
||||
section.name = text.replace(classExpr, '$1');
|
||||
} else if (text.match(braceExpr)) {
|
||||
section.type = 'property';
|
||||
section.name = text.replace(braceExpr, '$1');
|
||||
} else if (text.match(propExpr)) {
|
||||
section.type = 'property';
|
||||
section.name = text.replace(propExpr, '$1');
|
||||
} else if (text.match(classMethExpr)) {
|
||||
section.type = 'classMethod';
|
||||
section.name = text.replace(classMethExpr, '$1');
|
||||
} else if (text.match(methExpr)) {
|
||||
section.type = 'method';
|
||||
section.name = text.replace(methExpr, '$1');
|
||||
} else if (text.match(newExpr)) {
|
||||
section.type = 'ctor';
|
||||
section.name = text.replace(newExpr, '$1');
|
||||
} else {
|
||||
section.name = text;
|
||||
}
|
||||
return section;
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
../marked/bin/marked
|
|
@ -0,0 +1,2 @@
|
|||
.git*
|
||||
test/
|
|
@ -0,0 +1,19 @@
|
|||
Copyright (c) 2011-2012, Christopher Jeffrey (https://github.com/chjj/)
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
|
@ -0,0 +1,9 @@
|
|||
all:
|
||||
@cp lib/marked.js marked.js
|
||||
@uglifyjs -o marked.min.js marked.js
|
||||
|
||||
clean:
|
||||
@rm marked.js
|
||||
@rm marked.min.js
|
||||
|
||||
.PHONY: clean all
|
|
@ -0,0 +1,135 @@
|
|||
# marked
|
||||
|
||||
A full-featured markdown parser and compiler.
|
||||
Built for speed.
|
||||
|
||||
## Benchmarks
|
||||
|
||||
node v0.4.x
|
||||
|
||||
``` bash
|
||||
$ node test --bench
|
||||
marked completed in 12071ms.
|
||||
showdown (reuse converter) completed in 27387ms.
|
||||
showdown (new converter) completed in 75617ms.
|
||||
markdown-js completed in 70069ms.
|
||||
```
|
||||
|
||||
node v0.6.x
|
||||
|
||||
``` bash
|
||||
$ node test --bench
|
||||
marked completed in 6485ms.
|
||||
marked (with gfm) completed in 7466ms.
|
||||
discount completed in 7169ms.
|
||||
showdown (reuse converter) completed in 15937ms.
|
||||
showdown (new converter) completed in 18279ms.
|
||||
markdown-js completed in 23572ms.
|
||||
```
|
||||
|
||||
__Marked is now faster than Discount, which is written in C.__
|
||||
|
||||
For those feeling skeptical: These benchmarks run the entire markdown test suite
|
||||
1000 times. The test suite tests every feature. It doesn't cater to specific
|
||||
aspects.
|
||||
|
||||
Benchmarks for other engines to come (?).
|
||||
|
||||
## Install
|
||||
|
||||
``` bash
|
||||
$ npm install marked
|
||||
```
|
||||
|
||||
## Another javascript markdown parser
|
||||
|
||||
The point of marked was to create a markdown compiler where it was possible to
|
||||
frequently parse huge chunks of markdown without having to worry about
|
||||
caching the compiled output somehow...or blocking for an unnecesarily long time.
|
||||
|
||||
marked is very concise and still implements all markdown features. It is also
|
||||
now fully compatible with the client-side.
|
||||
|
||||
marked more or less passes the official markdown test suite in its
|
||||
entirety. This is important because a surprising number of markdown compilers
|
||||
cannot pass more than a few tests. It was very difficult to get marked as
|
||||
compliant as it is. It could have cut corners in several areas for the sake
|
||||
of performance, but did not in order to be exactly what you expect in terms
|
||||
of a markdown rendering. In fact, this is why marked could be considered at a
|
||||
disadvantage in the benchmarks above.
|
||||
|
||||
Along with implementing every markdown feature, marked also implements
|
||||
[GFM features](http://github.github.com/github-flavored-markdown/).
|
||||
|
||||
## Usage
|
||||
|
||||
``` js
|
||||
var marked = require('marked');
|
||||
console.log(marked('i am using __markdown__.'));
|
||||
```
|
||||
|
||||
You also have direct access to the lexer and parser if you so desire.
|
||||
|
||||
``` js
|
||||
var tokens = marked.lexer(str);
|
||||
console.log(marked.parser(tokens));
|
||||
```
|
||||
|
||||
``` bash
|
||||
$ node
|
||||
> require('marked').lexer('> i am using marked.')
|
||||
[ { type: 'blockquote_start' },
|
||||
{ type: 'text', text: ' i am using marked.' },
|
||||
{ type: 'blockquote_end' },
|
||||
links: {} ]
|
||||
```
|
||||
|
||||
## CLI
|
||||
|
||||
``` bash
|
||||
$ marked -o hello.html
|
||||
hello world
|
||||
^D
|
||||
$ cat hello.html
|
||||
<p>hello world</p>
|
||||
```
|
||||
|
||||
## Syntax Highlighting
|
||||
|
||||
Marked has an interface that allows for a syntax highlighter to highlight code
|
||||
blocks before they're output.
|
||||
|
||||
Example implementation:
|
||||
|
||||
``` js
|
||||
var highlight = require('my-syntax-highlighter')
|
||||
, marked_ = require('marked');
|
||||
|
||||
var marked = function(text) {
|
||||
var tokens = marked_.lexer(text)
|
||||
, l = tokens.length
|
||||
, i = 0
|
||||
, token;
|
||||
|
||||
for (; i < l; i++) {
|
||||
token = tokens[i];
|
||||
if (token.type === 'code') {
|
||||
token.text = highlight(token.text, token.lang);
|
||||
// marked should not escape this
|
||||
token.escaped = true;
|
||||
}
|
||||
}
|
||||
|
||||
text = marked_.parser(tokens);
|
||||
|
||||
return text;
|
||||
};
|
||||
|
||||
module.exports = marked;
|
||||
```
|
||||
|
||||
## License
|
||||
|
||||
Copyright (c) 2011-2012, Christopher Jeffrey. (MIT License)
|
||||
|
||||
See LICENSE for more info.
|
|
@ -0,0 +1,115 @@
|
|||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* Marked CLI
|
||||
* Copyright (c) 2011-2012, Christopher Jeffrey (MIT License)
|
||||
*/
|
||||
|
||||
var fs = require('fs')
|
||||
, util = require('util')
|
||||
, marked = require('../');
|
||||
|
||||
/**
|
||||
* Man Page
|
||||
*/
|
||||
|
||||
var help = function() {
|
||||
var spawn = require('child_process').spawn;
|
||||
|
||||
var options = {
|
||||
cwd: process.cwd(),
|
||||
env: process.env,
|
||||
setsid: false,
|
||||
customFds: [0, 1, 2]
|
||||
};
|
||||
|
||||
spawn('man',
|
||||
[__dirname + '/../man/marked.1'],
|
||||
options);
|
||||
};
|
||||
|
||||
/**
|
||||
* Main
|
||||
*/
|
||||
|
||||
var main = function(argv) {
|
||||
var files = []
|
||||
, data = ''
|
||||
, input
|
||||
, output
|
||||
, arg
|
||||
, tokens;
|
||||
|
||||
var getarg = function() {
|
||||
var arg = argv.shift();
|
||||
arg = arg.split('=');
|
||||
if (arg.length > 1) {
|
||||
argv.unshift(arg.slice(1).join('='));
|
||||
}
|
||||
return arg[0];
|
||||
};
|
||||
|
||||
while (argv.length) {
|
||||
arg = getarg();
|
||||
switch (arg) {
|
||||
case '-o':
|
||||
case '--output':
|
||||
output = argv.shift();
|
||||
break;
|
||||
case '-i':
|
||||
case '--input':
|
||||
input = argv.shift();
|
||||
break;
|
||||
case '-t':
|
||||
case '--tokens':
|
||||
tokens = true;
|
||||
break;
|
||||
case '-h':
|
||||
case '--help':
|
||||
return help();
|
||||
default:
|
||||
files.push(arg);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!input) {
|
||||
if (files.length <= 2) {
|
||||
var stdin = process.stdin;
|
||||
|
||||
stdin.setEncoding('utf8');
|
||||
stdin.resume();
|
||||
|
||||
stdin.on('data', function(text) {
|
||||
data += text;
|
||||
});
|
||||
|
||||
stdin.on('end', write);
|
||||
|
||||
return;
|
||||
}
|
||||
input = files.pop();
|
||||
}
|
||||
|
||||
data = fs.readFileSync(input, 'utf8');
|
||||
write();
|
||||
|
||||
function write() {
|
||||
data = tokens
|
||||
? JSON.stringify(marked.lexer(data), null, 2)
|
||||
: marked(data);
|
||||
|
||||
if (!output) {
|
||||
process.stdout.write(data + '\n');
|
||||
} else {
|
||||
fs.writeFileSync(output, data);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if (!module.parent) {
|
||||
process.title = 'marked';
|
||||
main(process.argv.slice());
|
||||
} else {
|
||||
module.exports = main;
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
module.exports = require('./lib/marked');
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue