resolve merge conflict

pull/1/head
ansuz 9 years ago
commit 50695f41ed

2
.gitignore vendored

@ -5,3 +5,5 @@ customization
.*.swp .*.swp
*.db *.db
/customize/ /customize/
messages.log
.DS_Store

@ -1,8 +1,25 @@
language: node_js language: node_js
env:
matrix:
- "BROWSER='firefox:19:Windows 2012'"
- "BROWSER='chrome::Windows 2008'"
branches: branches:
only: only:
- master - master
- diffdom - diffdom
node_js: - beta
- "0.12" - netflux
node_js:
- "4.2.1"
before_script:
- npm run-script lint
- cp config.js.dist config.js
- npm install bower
- ./node_modules/bower/bin/bower install
- node ./server.js &
- sleep 2
addons:
sauce_connect:
username: "cjdelisle"
access_key:
secure: "pgGh8YGXLPq6fpdwwK2jnjRtwXPbVWQ/HIFvwX7E6HBpzxxcF2edE8sCdonWW9TP2LQisZFmVLqoSnZWMnjBr2CBAMKMFvaHQDJDQCo4v3BXkID7KgqyKmNcwW+FPfSJ5MxNBro8/GE/awkhZzJLYGUTS5zi/gVuIUwdi6cHI8s="

@ -1,196 +0,0 @@
/*
* Copyright 2014 XWiki SAS
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
// triggers jsh because we're not in a browser and WebSocket is a thing
var WebSocket = require('ws'); // jshint ignore:line
var REGISTER = 0;
var REGISTER_ACK = 1;
var PATCH = 2;
var DISCONNECT = 3;
var PING = 4;
var PONG = 5;
var parseMessage = function (msg) {
var res ={};
// two or more? use a for
['pass','user','channelId','content'].forEach(function(attr){
var len=msg.slice(0,msg.indexOf(':')),
// taking an offset lets us slice out the prop
// and saves us one string copy
o=len.length+1,
prop=res[attr]=msg.slice(o,Number(len)+o);
// slice off the property and its descriptor
msg = msg.slice(prop.length+o);
});
// content is the only attribute that's not a string
res.content=JSON.parse(res.content);
return res;
};
// get the password off the message before sending it to other clients.
var popPassword = function (msg) {
var passLen = msg.substring(0,msg.indexOf(':'));
return msg.substring(passLen.length+1 + Number(passLen));
};
var sendMsg = function (msg, socket) {
socket.send(msg);
};
var sendChannelMessage = function (ctx, channel, msg, cb) {
ctx.store.message(channel.name, msg, function () {
channel.forEach(function (user) {
try {
sendMsg(msg, user.socket);
} catch (e) {
console.log(e.stack);
try { user.socket.close(); } catch (e) { }
}
});
if (cb) { cb(); }
});
};
var mkMessage = function (user, channel, content) {
content = JSON.stringify(content);
return user.length + ':' + user +
channel.length + ':' + channel +
content.length + ':' + content;
};
var dropClient = function (ctx, userpass) {
var client = ctx.registeredClients[userpass];
if (client.socket.readyState !== WebSocket.CLOSING
&& client.socket.readyState !== WebSocket.CLOSED)
{
try {
client.socket.close();
} catch (e) {
console.log("Failed to disconnect ["+client.userName+"], attempting to terminate");
try {
client.socket.terminate();
} catch (ee) {
console.log("Failed to terminate ["+client.userName+"] *shrug*");
}
}
}
for (var i = 0; i < client.channels.length; i++) {
var chanName = client.channels[i];
var chan = ctx.channels[chanName];
var idx = chan.indexOf(client);
if (idx < 0) { continue; }
console.log("Removing ["+client.userName+"] from channel ["+chanName+"]");
chan.splice(idx, 1);
if (chan.length === 0) {
console.log("Removing empty channel ["+chanName+"]");
delete ctx.channels[chanName];
} else {
sendChannelMessage(ctx, chan, mkMessage(client.userName, chanName, [DISCONNECT,0]));
}
}
delete ctx.registeredClients[userpass];
};
var handleMessage = function (ctx, socket, msg) {
var parsed = parseMessage(msg);
var userPass = parsed.user + ':' + parsed.pass;
msg = popPassword(msg);
if (parsed.content[0] === REGISTER) {
if (parsed.user.length === 0) { throw new Error(); }
console.log("[" + userPass + "] registered");
var user = ctx.registeredClients[userPass] = ctx.registeredClients[userPass] || {
channels: [parsed.channelId],
userName: parsed.user
};
if (user.socket && user.socket !== socket) { user.socket.close(); }
user.socket = socket;
var chan = ctx.channels[parsed.channelId] = ctx.channels[parsed.channelId] || [];
var newChan = (chan.length === 0);
chan.name = parsed.channelId;
// we send a register ack right away but then we fallthrough
// to let other users know that we were registered.
sendMsg(mkMessage('', parsed.channelId, [1,0]), socket);
var sendMsgs = function () {
sendChannelMessage(ctx, chan, msg, function () {
chan.push(user);
ctx.store.getMessages(chan.name, function (msg) {
try {
sendMsg(msg, socket);
} catch (e) {
console.log(e.stack);
try { socket.close(); } catch (e) { }
}
});
});
};
if (newChan) {
sendChannelMessage(ctx, chan, mkMessage('', chan.name, [DISCONNECT,0]), sendMsgs);
} else {
sendMsgs();
}
return;
}
if (parsed.content[0] === PING) {
// 31:xwiki:XWiki.Admin-141475016907510:RWJ5xF2+SL17:[5,1414752676547]
// 1:y31:xwiki:XWiki.Admin-141475016907510:RWJ5xF2+SL17:[4,1414752676547]
sendMsg(mkMessage(parsed.user, parsed.channelId, [ PONG, parsed.content[1] ]), socket);
return;
}
var client = ctx.registeredClients[userPass];
if (typeof(client) === 'undefined') { throw new Error('unregistered'); }
var channel = ctx.channels[parsed.channelId];
if (typeof(channel) === 'undefined') { throw new Error('no such channel'); }
if (channel.indexOf(client) === -1) { throw new Error('client not in channel'); }
sendChannelMessage(ctx, channel, msg);
};
var create = module.exports.create = function (socketServer, store) {
var ctx = {
registeredClients: {},
channels: {},
store: store
};
socketServer.on('connection', function(socket) {
socket.on('message', function(message) {
try {
handleMessage(ctx, socket, message);
} catch (e) {
console.log(e.stack);
try { socket.close(); } catch (e) { }
}
});
socket.on('close', function (evt) {
for (var client in ctx.registeredClients) {
if (ctx.registeredClients[client].socket === socket) {
dropClient(ctx, client);
}
}
});
});
};

@ -0,0 +1,259 @@
;(function () { 'use strict';
const Crypto = require('crypto');
const LogStore = require('./storage/LogStore');
const LAG_MAX_BEFORE_DISCONNECT = 30000;
const LAG_MAX_BEFORE_PING = 15000;
const HISTORY_KEEPER_ID = Crypto.randomBytes(8).toString('hex');
const USE_HISTORY_KEEPER = true;
const USE_FILE_BACKUP_STORAGE = true;
let dropUser;
const now = function () { return (new Date()).getTime(); };
const sendMsg = function (ctx, user, msg) {
try {
if (ctx.config.logToStdout) { console.log('<' + JSON.stringify(msg)); }
user.socket.send(JSON.stringify(msg));
} catch (e) {
console.log(e.stack);
dropUser(ctx, user);
}
};
const sendChannelMessage = function (ctx, channel, msgStruct) {
msgStruct.unshift(0);
channel.forEach(function (user) {
if(msgStruct[2] !== 'MSG' || user.id !== msgStruct[1]) { // We don't want to send back a message to its sender, in order to save bandwidth
sendMsg(ctx, user, msgStruct);
}
});
if (USE_HISTORY_KEEPER && msgStruct[2] === 'MSG') {
ctx.store.message(channel.id, JSON.stringify(msgStruct), function () { });
}
};
dropUser = function (ctx, user) {
if (user.socket.readyState !== 2 /* WebSocket.CLOSING */
&& user.socket.readyState !== 3 /* WebSocket.CLOSED */)
{
try {
user.socket.close();
} catch (e) {
console.log("Failed to disconnect ["+user.id+"], attempting to terminate");
try {
user.socket.terminate();
} catch (ee) {
console.log("Failed to terminate ["+user.id+"] *shrug*");
}
}
}
delete ctx.users[user.id];
Object.keys(ctx.channels).forEach(function (chanName) {
let chan = ctx.channels[chanName];
let idx = chan.indexOf(user);
if (idx < 0) { return; }
console.log("Removing ["+user.id+"] from channel ["+chanName+"]");
chan.splice(idx, 1);
if (chan.length === 0) {
console.log("Removing empty channel ["+chanName+"]");
delete ctx.channels[chanName];
/* Call removeChannel if it is a function and channel removal is
set to true in the config file */
if (ctx.config.removeChannels) {
if (typeof(ctx.store.removeChannel) === 'function') {
ctx.timeouts[chanName] = setTimeout(function () {
ctx.store.removeChannel(chanName, function (err) {
if (err) { console.error("[removeChannelErr]: %s", err); }
else {
console.log("Deleted channel [%s] history from database...", chanName);
}
});
}, ctx.config.channelRemovalTimeout);
} else {
console.error("You have configured your server to remove empty channels, " +
"however, the database adaptor you are using has not implemented this behaviour.");
}
}
} else {
sendChannelMessage(ctx, chan, [user.id, 'LEAVE', chanName, 'Quit: [ dropUser() ]']);
}
});
};
const getHistory = function (ctx, channelName, handler, cb) {
var messageBuf = [];
ctx.store.getMessages(channelName, function (msgStr) {
messageBuf.push(JSON.parse(msgStr));
}, function () {
var startPoint;
var cpCount = 0;
var msgBuff2 = [];
for (startPoint = messageBuf.length - 1; startPoint >= 0; startPoint--) {
var msg = messageBuf[startPoint];
msgBuff2.push(msg);
if (msg[2] === 'MSG' && msg[4].indexOf('cp|') === 0) {
cpCount++;
if (cpCount >= 2) {
for (var x = msgBuff2.pop(); x; x = msgBuff2.pop()) { handler(x); }
break;
}
}
//console.log(messageBuf[startPoint]);
}
if (cpCount < 2) {
// no checkpoints.
for (var x = msgBuff2.pop(); x; x = msgBuff2.pop()) { handler(x); }
}
cb();
});
};
const randName = function () { return Crypto.randomBytes(16).toString('hex'); };
const handleMessage = function (ctx, user, msg) {
let json = JSON.parse(msg);
let seq = json.shift();
let cmd = json[0];
let obj = json[1];
user.timeOfLastMessage = now();
user.pingOutstanding = false;
if (cmd === 'JOIN') {
if (obj && obj.length !== 32) {
sendMsg(ctx, user, [seq, 'ERROR', 'ENOENT', obj]);
return;
}
let chanName = obj || randName();
sendMsg(ctx, user, [seq, 'JACK', chanName]);
let chan = ctx.channels[chanName] = ctx.channels[chanName] || [];
// prevent removal of the channel if there is a pending timeout
if (ctx.config.removeChannels && ctx.timeouts[chanName]) {
clearTimeout(ctx.timeouts[chanName]);
}
chan.id = chanName;
if (USE_HISTORY_KEEPER) {
sendMsg(ctx, user, [0, HISTORY_KEEPER_ID, 'JOIN', chanName]);
}
chan.forEach(function (u) { sendMsg(ctx, user, [0, u.id, 'JOIN', chanName]); });
chan.push(user);
sendChannelMessage(ctx, chan, [user.id, 'JOIN', chanName]);
return;
}
if (cmd === 'MSG') {
if (obj === HISTORY_KEEPER_ID) {
let parsed;
try { parsed = JSON.parse(json[2]); } catch (err) { console.error(err); return; }
if (parsed[0] === 'GET_HISTORY') {
sendMsg(ctx, user, [seq, 'ACK']);
getHistory(ctx, parsed[1], function (msg) {
sendMsg(ctx, user, [0, HISTORY_KEEPER_ID, 'MSG', user.id, JSON.stringify(msg)]);
}, function () {
sendMsg(ctx, user, [0, HISTORY_KEEPER_ID, 'MSG', user.id, 0]);
});
}
return;
}
if (obj && !ctx.channels[obj] && !ctx.users[obj]) {
sendMsg(ctx, user, [seq, 'ERROR', 'ENOENT', obj]);
return;
}
sendMsg(ctx, user, [seq, 'ACK']);
let target;
json.unshift(user.id);
if ((target = ctx.channels[obj])) {
sendChannelMessage(ctx, target, json);
return;
}
if ((target = ctx.users[obj])) {
json.unshift(0);
sendMsg(ctx, target, json);
return;
}
}
if (cmd === 'LEAVE') {
let err;
let chan;
let idx;
if (!obj) { err = 'EINVAL'; obj = 'undefined';}
if (!err && !(chan = ctx.channels[obj])) { err = 'ENOENT'; }
if (!err && (idx = chan.indexOf(user)) === -1) { err = 'NOT_IN_CHAN'; }
if (err) {
sendMsg(ctx, user, [seq, 'ERROR', err, obj]);
return;
}
sendMsg(ctx, user, [seq, 'ACK']);
json.unshift(user.id);
sendChannelMessage(ctx, chan, [user.id, 'LEAVE', chan.id]);
chan.splice(idx, 1);
}
if (cmd === 'PING') {
sendMsg(ctx, user, [seq, 'ACK']);
return;
}
};
let run = module.exports.run = function (storage, socketServer, config) {
/* Channel removal timeout defaults to 60000ms (one minute) */
config.channelRemovalTimeout =
typeof(config.channelRemovalTimeout) === 'number'?
config.channelRemovalTimeout:
60000;
let ctx = {
users: {},
channels: {},
timeouts: {},
store: (USE_FILE_BACKUP_STORAGE) ? LogStore.create('messages.log', storage) : storage,
config: config
};
setInterval(function () {
Object.keys(ctx.users).forEach(function (userId) {
let u = ctx.users[userId];
if (now() - u.timeOfLastMessage > LAG_MAX_BEFORE_DISCONNECT) {
dropUser(ctx, u);
} else if (!u.pingOutstanding && now() - u.timeOfLastMessage > LAG_MAX_BEFORE_PING) {
sendMsg(ctx, u, [0, '', 'PING', now()]);
u.pingOutstanding = true;
}
});
}, 5000);
socketServer.on('connection', function(socket) {
if(socket.upgradeReq.url !== '/cryptpad_websocket') { return; }
let conn = socket.upgradeReq.connection;
let user = {
addr: conn.remoteAddress + '|' + conn.remotePort,
socket: socket,
id: randName(),
timeOfLastMessage: now(),
pingOutstanding: false
};
ctx.users[user.id] = user;
sendMsg(ctx, user, [0, '', 'IDENT', user.id]);
socket.on('message', function(message) {
if (ctx.config.logToStdout) { console.log('>'+message); }
try {
handleMessage(ctx, user, message);
} catch (e) {
console.log(e.stack);
dropUser(ctx, user);
}
});
socket.on('close', function (evt) {
for (let userId in ctx.users) {
if (ctx.users[userId].socket === socket) {
dropUser(ctx, ctx.users[userId]);
}
}
});
});
};
}());

@ -0,0 +1,30 @@
/* global process */
var WebDriver = require("selenium-webdriver");
var driver;
if (process.env.SAUCE_USERNAME !== undefined) {
var browserArray = process.env.BROWSER.split(':');
driver = new WebDriver.Builder().usingServer(
'http://'+ process.env.SAUCE_USERNAME+':'+process.env.SAUCE_ACCESS_KEY+'@ondemand.saucelabs.com:80/wd/hub'
).withCapabilities({
"tunnel-identifier": process.env.TRAVIS_JOB_NUMBER,
"build": process.env.TRAVIS_JOB_NUMBER,
"username": process.env.SAUCE_USERNAME,
"accessKey": process.env.SAUCE_ACCESS_KEY,
}).forBrowser(browserArray[0], browserArray[1], browserArray[2]).build();
} else {
driver = new WebDriver.Builder().withCapabilities({ browserName: "chrome" }).build();
}
driver.get('http://localhost:3000/assert/');
var report = driver.wait(WebDriver.until.elementLocated(WebDriver.By.className("report")), 5000);
report.getAttribute("class").then(function (cls) {
driver.quit();
if (!cls) {
throw new Error("cls is null");
} else if (cls.indexOf("failure") !== -1) {
throw new Error("cls contains the word failure");
} else if (cls.indexOf("success") === -1) {
throw new Error("cls does not contain the word success");
}
});

@ -0,0 +1,61 @@
'use strict'
let WebSocketServer = require('ws').Server
const UNSUPPORTED_DATA = 1007
const POLICY_VIOLATION = 1008
const CLOSE_UNSUPPORTED = 1003
var run = module.exports.run = function(server) {
server.on('connection', (socket) => {
if(socket.upgradeReq.url !== '/cryptpad_webrtc') { return; }
socket.on('message', (data) => {
try {
let msg = JSON.parse(data)
console.log(msg)
if (msg.hasOwnProperty('key')) {
for (let master of server.clients) {
if (master.key === msg.key) {
socket.close(POLICY_VIOLATION, 'The key already exists')
return
}
}
socket.key = msg.key
socket.joiningClients = []
} else if (msg.hasOwnProperty('id')) {
for (let index in socket.joiningClients) {
if (index == msg.id) {
socket.joiningClients[index].send(JSON.stringify({data: msg.data}))
return
}
}
socket.close(POLICY_VIOLATION, 'Unknown id')
} else if (msg.hasOwnProperty('join')) {
for (let master of server.clients) {
if (master.key === msg.join) {
socket.master = master
master.joiningClients.push(socket)
let id = master.joiningClients.length - 1
master.send(JSON.stringify({id, data: msg.data}))
return
}
}
socket.close(POLICY_VIOLATION, 'Unknown key')
} else if (msg.hasOwnProperty('data') && socket.hasOwnProperty('master')) {
let id = socket.master.joiningClients.indexOf(socket)
socket.master.send(JSON.stringify({id, data: msg.data}))
} else {
socket.close(UNSUPPORTED_DATA, 'Unsupported message format')
}
} catch (event) {
socket.close(CLOSE_UNSUPPORTED, 'Server accepts only JSON')
}
})
socket.on('close', (event) => {
if (socket.hasOwnProperty('joiningClients')) {
for (let client of socket.joiningClients) {
client.close(POLICY_VIOLATION, 'The peer is no longer available')
}
}
});
})
}

@ -18,8 +18,6 @@
"tests" "tests"
], ],
"dependencies": { "dependencies": {
"markdown": "~0.5.0",
"jquery.sheet": "master",
"jquery": "~2.1.3", "jquery": "~2.1.3",
"tweetnacl": "~0.12.2", "tweetnacl": "~0.12.2",
"ckeditor": "~4.5.6", "ckeditor": "~4.5.6",
@ -28,6 +26,17 @@
"reconnectingWebsocket": "", "reconnectingWebsocket": "",
"diff-dom": "https://github.com/fiduswriter/diffDOM.git#6fdb82c8a4f2096c07c129797a313fe13769e094", "diff-dom": "https://github.com/fiduswriter/diffDOM.git#6fdb82c8a4f2096c07c129797a313fe13769e094",
"marked": "~0.3.5", "marked": "~0.3.5",
"rangy": "rangy-release#~1.3.0" "rangy": "rangy-release#~1.3.0",
"json.sortify": "~2.1.0",
"fabric.js": "fabric#~1.6.0",
"hyperjson": "~1.2.2",
"textpatcher": "^1.2.0",
"proxy-polyfill": "^0.1.5",
"chainpad": "^0.2.2",
"chainpad-json-validator": "^0.1.1",
"chainpad-crypto": "^0.1.1",
"netflux-websocket": "^0.1.0",
"chainpad-netflux": "^0.1.0",
"chainpad-listmap": "^0.1.0"
} }
} }

@ -10,7 +10,21 @@ module.exports = {
// the port on which your httpd will listen // the port on which your httpd will listen
httpPort: 3000, httpPort: 3000,
// the port used for websockets // the port used for websockets
websocketPort: 3001, websocketPort: 3000,
/* Cryptpad can log activity to stdout
* This may be useful for debugging
*/
logToStdout: false,
/* Cryptpad can be configured to remove channels some number of ms
after the last remaining client has disconnected.
Default behaviour is to keep channels forever.
If you enable channel removal, the default removal time is one minute
*/
removeChannels: false,
channelRemovalTimeout: 60000,
// You now have a choice of storage engines // You now have a choice of storage engines
@ -19,7 +33,7 @@ module.exports = {
* it will not scale well if your server stays alive for a long time. * it will not scale well if your server stays alive for a long time.
* but it is completely dependency free * but it is completely dependency free
*/ */
storage: './storage/amnesia', //storage: './storage/amnesia',
/* the 'lvl' storage module uses leveldb /* the 'lvl' storage module uses leveldb
* it persists, and will perform better than amnesiadb * it persists, and will perform better than amnesiadb
@ -31,8 +45,8 @@ module.exports = {
* *
* to delete all pads, run `rm -rf $YOUR_DB` * to delete all pads, run `rm -rf $YOUR_DB`
*/ */
// storage: './storage/lvl', storage: './storage/lvl',
// levelPath: './test.level.db' levelPath: './test.level.db'
/* mongo is the original storage engine for cryptpad /* mongo is the original storage engine for cryptpad
* it has been more thoroughly tested, but requires a little more setup * it has been more thoroughly tested, but requires a little more setup

@ -122,9 +122,12 @@
</p> </p>
</noscript> </noscript>
<script> <script>
require(['/common/crypto.js'], function (Crypto) { require(['/common/crypto.js', '/api/config?cb=' + Math.random().toString(16).substring(2)], function (Crypto, Config) {
document.getElementById('buttons').setAttribute('style', ''); document.getElementById('buttons').setAttribute('style', '');
document.getElementById('create-pad').setAttribute('href', '/pad/#' + Crypto.genKey()); document.getElementById('create-pad').setAttribute('href', '/pad/');
if(Config.webrtcURL !== '') {
document.getElementById('create-rtcpad').setAttribute('href', '/padrtc/');
}
document.getElementById('create-sheet').setAttribute('href', '/sheet/#' + Crypto.genKey()); document.getElementById('create-sheet').setAttribute('href', '/sheet/#' + Crypto.genKey());
document.getElementById('create-code').setAttribute('href', '/code/#' + Crypto.genKey()); document.getElementById('create-code').setAttribute('href', '/code/#' + Crypto.genKey());
}); });
@ -215,7 +218,8 @@
<div id="buttons" class="buttons" style="display:none;"> <div id="buttons" class="buttons" style="display:none;">
<a id="create-pad" class="button create" href="pad">CREATE NEW PAD</a> <a id="create-pad" class="button create" href="pad">CREATE NEW PAD</a>
<a id="create-sheet" class="button create" href="sheet">CREATE NEW SPREADSHEET</a> <a id="create-rtcpad" class="button create" href="pad">CREATE NEW WEBRTC PAD</a>
<!--<a id="create-sheet" class="button create" href="sheet">CREATE NEW SPREADSHEET</a>-->
<a id="create-code" class="button create" href="code">CREATE NEW CODE COLLABORATION PAD</a> <a id="create-code" class="button create" href="code">CREATE NEW CODE COLLABORATION PAD</a>
</div> </div>
</center> </center>

@ -9,9 +9,11 @@
"nthen": "~0.1.0" "nthen": "~0.1.0"
}, },
"devDependencies": { "devDependencies": {
"jshint": "~2.9.1" "jshint": "~2.9.1",
"selenium-webdriver": "^2.53.1"
}, },
"scripts": { "scripts": {
"lint": "jshint --config .jshintrc --exclude-path .jshintignore ." "lint": "jshint --config .jshintrc --exclude-path .jshintignore .",
"test": "node TestSelenium.js"
} }
} }

@ -67,6 +67,13 @@ rm -rf ./cryptpad.db
If you are using the mongodb adaptor, [drop the relevant collection](https://docs.mongodb.org/manual/reference/method/db.collection.drop/#db.collection.drop). If you are using the mongodb adaptor, [drop the relevant collection](https://docs.mongodb.org/manual/reference/method/db.collection.drop/#db.collection.drop).
## Testing
To test CryptPad, go to http://your.server:3000/assert/
You can use WebDriver to run this test automatically by running TestSelenium.js but you will need chromedriver installed.
If you use Mac, you can `brew install chromedriver`.
## Security ## Security
CryptPad is *private*, not *anonymous*. Privacy protects your data, anonymity protects you. CryptPad is *private*, not *anonymous*. Privacy protects your data, anonymity protects you.

@ -6,7 +6,8 @@ var Http = require('http');
var Https = require('https'); var Https = require('https');
var Fs = require('fs'); var Fs = require('fs');
var WebSocketServer = require('ws').Server; var WebSocketServer = require('ws').Server;
var ChainPadSrv = require('./ChainPadSrv'); var NetfluxSrv = require('./NetfluxWebsocketSrv');
var WebRTCSrv = require('./WebRTCSrv');
var config = require('./config'); var config = require('./config');
config.websocketPort = config.websocketPort || config.httpPort; config.websocketPort = config.websocketPort || config.httpPort;
@ -17,12 +18,6 @@ var Storage = require(config.storage||'./storage/mongo');
var app = Express(); var app = Express();
app.use(Express.static(__dirname + '/www')); app.use(Express.static(__dirname + '/www'));
// Bower is broken and does not allow components nested within components...
// And jquery.sheet expects it!
// *Workaround*
app.use("/bower_components/jquery.sheet/bower_components",
Express.static(__dirname + '/www/bower_components'));
var customize = "/customize"; var customize = "/customize";
if (!Fs.existsSync(__dirname + "/customize")) { if (!Fs.existsSync(__dirname + "/customize")) {
customize = "/customize.dist"; customize = "/customize.dist";
@ -60,7 +55,9 @@ app.get('/api/config', function(req, res){
res.setHeader('Content-Type', 'text/javascript'); res.setHeader('Content-Type', 'text/javascript');
res.send('define(' + JSON.stringify({ res.send('define(' + JSON.stringify({
websocketURL:'ws' + ((httpsOpts) ? 's' : '') + '://' + host + ':' + websocketURL:'ws' + ((httpsOpts) ? 's' : '') + '://' + host + ':' +
config.websocketPort + '/cryptpad_websocket' config.websocketPort + '/cryptpad_websocket',
webrtcURL:'ws' + ((httpsOpts) ? 's' : '') + '://' + host + ':' +
config.websocketPort + '/cryptpad_webrtc',
}) + ');'); }) + ');');
}); });
@ -75,9 +72,9 @@ if (config.websocketPort !== config.httpPort) {
console.log("setting up a new websocket server"); console.log("setting up a new websocket server");
wsConfig = { port: config.websocketPort}; wsConfig = { port: config.websocketPort};
} }
var wsSrv = new WebSocketServer(wsConfig); var wsSrv = new WebSocketServer(wsConfig);
Storage.create(config, function (store) { Storage.create(config, function (store) {
console.log('DB connected'); console.log('DB connected');
ChainPadSrv.create(wsSrv, store); NetfluxSrv.run(store, wsSrv, config);
WebRTCSrv.run(wsSrv);
}); });

@ -0,0 +1,19 @@
var Fs = require("fs");
var message = function(file, msg) {
file.write(msg+"\n");
};
var create = module.exports.create = function(filePath, backingStore) {
var file = Fs.createWriteStream(filePath, {flags: 'a+'});
var originalMessageFunction = backingStore.message;
backingStore.message = function(channel, msg, callback) {
message(file, msg);
originalMessageFunction(channel, msg, callback);
};
return backingStore;
};

@ -21,21 +21,32 @@ That function must accept two arguments:
## Methods ## Methods
### message(channelName, content, callback) ### message(channelName, content, handler)
When Cryptpad receives a message, it saves it into its datastore using its equivalent of a table for its channel name, and then relays the message to every other client which is participating in the same channel. When Cryptpad receives a message, it saves it into its datastore using its equivalent of a table for its channel name, and then relays the message to every other client which is participating in the same channel.
Relaying logic exists outside of the storage module, you simply need to store the message then execute the callback on success. Relaying logic exists outside of the storage module, you simply need to store the message then execute the handler on success.
### getMessages(channelName, callback) ### getMessages(channelName, handler, callback)
When a new client joins, they request the entire history of messages for a particular channel. When a new client joins, they request the entire history of messages for a particular channel.
This method retreives those messages, and delivers them in order. This method retreives those messages, and delivers them in order.
In theory, it should be possible for Chainpad to make sense of out of order messages, however, this has not yet been implemented. In practice, out of order messages make your clientside application more likely to fail, however, they are generally tolerated.
In practice, out of order messages make your clientside application likely to fail.
As a channel accumulates a greater number of messages, the likelihood of the application receiving them in the wrong order becomes greater. As a channel accumulates a greater number of messages, the likelihood of the application receiving them in the wrong order becomes greater.
This results in older sessions becoming less reliable This results in older sessions becoming less reliable.
This function accepts the name of the channel in which the user is interested, the handler for each message, and the callback to be executed when the last message has been fetched and handled.
**Note**, the callback is a new addition to this API.
It is only implemented within the leveldb adaptor, making our latest code incompatible with the other back ends.
While we migrate to our new Netflux API, only the leveldb adaptor will be supported.
## removeChannel(channelName, callback)
This method is called (optionally, see config.js.dist for more info) some amount of time after the last client in a channel disconnects.
It should remove any history of that channel, and execute a callback which takes an error message as an argument.
## Documenting your adaptor ## Documenting your adaptor

@ -18,6 +18,11 @@ module.exports.create = function(conf, cb){
var db=[], var db=[],
index=0; index=0;
if (conf.removeChannels) {
console.log("Server is set to remove channels %sms after the last remaining client leaves.", conf.channelRemovalTimeout);
}
cb({ cb({
message: function(channelName, content, cb){ message: function(channelName, content, cb){
var val = { var val = {
@ -27,17 +32,25 @@ module.exports.create = function(conf, cb){
time: new Date().getTime(), time: new Date().getTime(),
}; };
db.push(val); db.push(val);
cb(); if (cb) { cb(); }
}, },
getMessages: function(channelName, cb){ getMessages: function(channelName, handler, cb){
db.sort(function(a,b){ db.sort(function(a,b){
return a.id - b.id; return a.id - b.id;
}); });
db.filter(function(val){ db.filter(function(val){
return val.chan === channelName; return val.chan === channelName;
}).forEach(function(doc){ }).forEach(function(doc){
cb(doc.msg); handler(doc.msg);
});
if (cb) { cb(); }
},
removeChannel: function (channelName, cb) {
var err = false;
db = db.filter(function (msg) {
return msg.chan !== channelName;
}); });
cb(err);
}, },
}); });
}; };

@ -17,39 +17,57 @@ var getIndex = function(db, cName, cb) {
var insert = function (db, channelName, content, cb) { var insert = function (db, channelName, content, cb) {
var index; var index;
var doIt = function () {
db.locked = true;
nThen(function (waitFor) { nThen(function (waitFor) {
getIndex(db, channelName, waitFor(function (i) { index = i+1; })); getIndex(db, channelName, waitFor(function (i) { index = i+1; }));
}).nThen(function (waitFor) { }).nThen(function (waitFor) {
db.put(channelName+'=>'+index, content, waitFor(function (e) { if (e) { throw e; } })); db.put(channelName+'=>'+index, content, waitFor(function (e) { if (e) { throw e; } }));
}).nThen(function (waitFor) { }).nThen(function (waitFor) {
db.put(channelName+'=>index', ''+index, waitFor(function (e) { if (e) { throw e; } })); db.put(channelName+'=>index', ''+index, waitFor(function (e) { if (e) { throw e; } }));
}).nThen(function (waitFor) {
db.locked = false;
if (!db.queue.length) { return; }
db.queue.shift()();
}).nThen(cb); }).nThen(cb);
};
if (db.locked) {
db.queue.push(doIt);
} else {
doIt();
}
}; };
var getMessages = function (db, channelName, msgHandler) { var getMessages = function (db, channelName, msgHandler, cb) {
var index; var index;
nThen(function (waitFor) { nThen(function (waitFor) {
getIndex(db, channelName, waitFor(function (i) { index = i; })); getIndex(db, channelName, waitFor(function (i) {
index = i;
}));
}).nThen(function (waitFor) { }).nThen(function (waitFor) {
var again = function (i) { var again = function (i) {
db.get(channelName + '=>' + i, waitFor(function (e, out) { db.get(channelName + '=>' + i, waitFor(function (e, out) {
if (e) { throw e; } if (e) { throw e; }
msgHandler(out); msgHandler(out);
if (i < index) { again(i+1); } if (i < index) { again(i+1); }
else if (cb) { cb(); }
})); }));
}; };
if (index > -1) { again(0); } if (index > -1) { again(0); }
else if (cb) { cb(); }
}); });
}; };
module.exports.create = function (conf, cb) { module.exports.create = function (conf, cb) {
var db = Level(conf.levelPath || './test.level.db'); var db = Level(conf.levelPath || './test.level.db');
db.locked = false;
db.queue = [];
cb({ cb({
message: function (channelName, content, cb) { message: function (channelName, content, cb) {
insert(db, channelName, content, cb); insert(db, channelName, content, cb);
}, },
getMessages: function (channelName, msgHandler) { getMessages: function (channelName, msgHandler, cb) {
getMessages(db, channelName, msgHandler); getMessages(db, channelName, msgHandler, cb);
} }
}); });
}; };

@ -1,124 +0,0 @@
define([], function () {
// this makes recursing a lot simpler
var isArray = function (A) {
return Object.prototype.toString.call(A)==='[object Array]';
};
var parseStyle = function(el){
var style = el.style;
var output = {};
for (var i = 0; i < style.length; ++i) {
var item = style.item(i);
output[item] = style[item];
}
return output;
};
var callOnHyperJSON = function (hj, cb) {
var children;
if (hj && hj[2]) {
children = hj[2].map(function (child) {
if (isArray(child)) {
// if the child is an array, recurse
return callOnHyperJSON(child, cb);
} else if (typeof (child) === 'string') {
// string nodes have leading and trailing quotes
return child.replace(/(^"|"$)/g,"");
} else {
// the above branches should cover all methods
// if we hit this, there is a problem
throw new Error();
}
});
} else {
children = [];
}
// this should return the top level element of your new DOM
return cb(hj[0], hj[1], children);
};
var classify = function (token) {
return '.' + token.trim();
};
var isValidClass = function (x) {
if (x && /\S/.test(x)) {
return true;
}
};
var isTruthy = function (x) {
return x;
};
var DOM2HyperJSON = function(el, predicate, filter){
if(!el.tagName && el.nodeType === Node.TEXT_NODE){
return el.textContent;
}
if(!el.attributes){
return;
}
if (predicate) {
if (!predicate(el)) {
// shortcircuit
return;
}
}
var attributes = {};
var i = 0;
for(;i < el.attributes.length; i++){
var attr = el.attributes[i];
if(attr.name && attr.value){
if(attr.name === "style"){
attributes.style = parseStyle(el);
}
else{
attributes[attr.name] = attr.value;
}
}
}
// this should never be longer than three elements
var result = [];
// get the element type, id, and classes of the element
// and push them to the result array
var sel = el.tagName;
if(attributes.id){
// we don't have to do much to validate IDs because the browser
// will only permit one id to exist
// unless we come across a strange browser in the wild
sel = sel +'#'+ attributes.id;
delete attributes.id;
}
result.push(sel);
// second element of the array is the element attributes
result.push(attributes);
// third element of the array is an array of child nodes
var children = [];
// js hint complains if we use 'var' here
i = 0;
for(; i < el.childNodes.length; i++){
children.push(DOM2HyperJSON(el.childNodes[i], predicate, filter));
}
result.push(children.filter(isTruthy));
if (filter) {
return filter(result);
} else {
return result;
}
};
return {
fromDOM: DOM2HyperJSON,
callOn: callOnHyperJSON
};
});

@ -1,400 +0,0 @@
define([], function () {
var Hyperscript;
(function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o<r.length;o++)s(r[o]);return s})({1:[function(require,module,exports){
var split = require('browser-split')
var ClassList = require('class-list')
require('html-element')
function context () {
var cleanupFuncs = []
function h() {
var args = [].slice.call(arguments), e = null
function item (l) {
var r
function parseClass (string) {
// Our minimal parser doesnt understand escaping CSS special
// characters like `#`. Dont use them. More reading:
// https://mathiasbynens.be/notes/css-escapes .
var m = split(string, /([\.#]?[^\s#.]+)/)
if(/^\.|#/.test(m[1]))
e = document.createElement('div')
forEach(m, function (v) {
var s = v.substring(1,v.length)
if(!v) return
if(!e)
e = document.createElement(v)
else if (v[0] === '.')
ClassList(e).add(s)
else if (v[0] === '#')
e.setAttribute('id', s)
})
}
if(l == null)
;
else if('string' === typeof l) {
if(!e)
parseClass(l)
else
e.appendChild(r = document.createTextNode(l))
}
else if('number' === typeof l
|| 'boolean' === typeof l
|| l instanceof Date
|| l instanceof RegExp ) {
e.appendChild(r = document.createTextNode(l.toString()))
}
//there might be a better way to handle this...
else if (isArray(l))
forEach(l, item)
else if(isNode(l))
e.appendChild(r = l)
else if(l instanceof Text)
e.appendChild(r = l)
else if ('object' === typeof l) {
for (var k in l) {
if('function' === typeof l[k]) {
if(/^on\w+/.test(k)) {
(function (k, l) { // capture k, l in the closure
if (e.addEventListener){
e.addEventListener(k.substring(2), l[k], false)
cleanupFuncs.push(function(){
e.removeEventListener(k.substring(2), l[k], false)
})
}else{
e.attachEvent(k, l[k])
cleanupFuncs.push(function(){
e.detachEvent(k, l[k])
})
}
})(k, l)
} else {
// observable
e[k] = l[k]()
cleanupFuncs.push(l[k](function (v) {
e[k] = v
}))
}
}
else if(k === 'style') {
if('string' === typeof l[k]) {
e.style.cssText = l[k]
}else{
for (var s in l[k]) (function(s, v) {
if('function' === typeof v) {
// observable
e.style.setProperty(s, v())
cleanupFuncs.push(v(function (val) {
e.style.setProperty(s, val)
}))
} else
e.style.setProperty(s, l[k][s])
})(s, l[k][s])
}
} else if (k.substr(0, 5) === "data-") {
e.setAttribute(k, l[k])
} else {
e.setAttribute(k, l[k])
if (e.getAttribute(k) !== l[k]) {
e[k] = l[k]
}
}
}
} else if ('function' === typeof l) {
//assume it's an observable!
var v = l()
e.appendChild(r = isNode(v) ? v : document.createTextNode(v))
cleanupFuncs.push(l(function (v) {
if(isNode(v) && r.parentElement)
r.parentElement.replaceChild(v, r), r = v
else
r.textContent = v
}))
}
return r
}
while(args.length)
item(args.shift())
return e
}
h.cleanup = function () {
for (var i = 0; i < cleanupFuncs.length; i++){
cleanupFuncs[i]()
}
cleanupFuncs.length = 0
}
return h
}
var h = module.exports = context()
h.context = context
Hyperscript = h;
function isNode (el) {
return el && el.nodeName && el.nodeType
}
function forEach (arr, fn) {
if (arr.forEach) return arr.forEach(fn)
for (var i = 0; i < arr.length; i++) fn(arr[i], i)
}
function isArray (arr) {
return Object.prototype.toString.call(arr) == '[object Array]'
}
},{"browser-split":2,"class-list":3,"html-element":6}],2:[function(require,module,exports){
/*!
* Cross-Browser Split 1.1.1
* Copyright 2007-2012 Steven Levithan <stevenlevithan.com>
* Available under the MIT License
* ECMAScript compliant, uniform cross-browser split method
*/
/**
* Splits a string into an array of strings using a regex or string separator. Matches of the
* separator are not included in the result array. However, if `separator` is a regex that contains
* capturing groups, backreferences are spliced into the result each time `separator` is matched.
* Fixes browser bugs compared to the native `String.prototype.split` and can be used reliably
* cross-browser.
* @param {String} str String to split.
* @param {RegExp|String} separator Regex or string to use for separating the string.
* @param {Number} [limit] Maximum number of items to include in the result array.
* @returns {Array} Array of substrings.
* @example
*
* // Basic use
* split('a b c d', ' ');
* // -> ['a', 'b', 'c', 'd']
*
* // With limit
* split('a b c d', ' ', 2);
* // -> ['a', 'b']
*
* // Backreferences in result array
* split('..word1 word2..', /([a-z]+)(\d+)/i);
* // -> ['..', 'word', '1', ' ', 'word', '2', '..']
*/
module.exports = (function split(undef) {
var nativeSplit = String.prototype.split,
compliantExecNpcg = /()??/.exec("")[1] === undef,
// NPCG: nonparticipating capturing group
self;
self = function(str, separator, limit) {
// If `separator` is not a regex, use `nativeSplit`
if (Object.prototype.toString.call(separator) !== "[object RegExp]") {
return nativeSplit.call(str, separator, limit);
}
var output = [],
flags = (separator.ignoreCase ? "i" : "") + (separator.multiline ? "m" : "") + (separator.extended ? "x" : "") + // Proposed for ES6
(separator.sticky ? "y" : ""),
// Firefox 3+
lastLastIndex = 0,
// Make `global` and avoid `lastIndex` issues by working with a copy
separator = new RegExp(separator.source, flags + "g"),
separator2, match, lastIndex, lastLength;
str += ""; // Type-convert
if (!compliantExecNpcg) {
// Doesn't need flags gy, but they don't hurt
separator2 = new RegExp("^" + separator.source + "$(?!\\s)", flags);
}
/* Values for `limit`, per the spec:
* If undefined: 4294967295 // Math.pow(2, 32) - 1
* If 0, Infinity, or NaN: 0
* If positive number: limit = Math.floor(limit); if (limit > 4294967295) limit -= 4294967296;
* If negative number: 4294967296 - Math.floor(Math.abs(limit))
* If other: Type-convert, then use the above rules
*/
limit = limit === undef ? -1 >>> 0 : // Math.pow(2, 32) - 1
limit >>> 0; // ToUint32(limit)
while (match = separator.exec(str)) {
// `separator.lastIndex` is not reliable cross-browser
lastIndex = match.index + match[0].length;
if (lastIndex > lastLastIndex) {
output.push(str.slice(lastLastIndex, match.index));
// Fix browsers whose `exec` methods don't consistently return `undefined` for
// nonparticipating capturing groups
if (!compliantExecNpcg && match.length > 1) {
match[0].replace(separator2, function() {
for (var i = 1; i < arguments.length - 2; i++) {
if (arguments[i] === undef) {
match[i] = undef;
}
}
});
}
if (match.length > 1 && match.index < str.length) {
Array.prototype.push.apply(output, match.slice(1));
}
lastLength = match[0].length;
lastLastIndex = lastIndex;
if (output.length >= limit) {
break;
}
}
if (separator.lastIndex === match.index) {
separator.lastIndex++; // Avoid an infinite loop
}
}
if (lastLastIndex === str.length) {
if (lastLength || !separator.test("")) {
output.push("");
}
} else {
output.push(str.slice(lastLastIndex));
}
return output.length > limit ? output.slice(0, limit) : output;
};
return self;
})();
},{}],3:[function(require,module,exports){
// contains, add, remove, toggle
var indexof = require('indexof')
module.exports = ClassList
function ClassList(elem) {
var cl = elem.classList
if (cl) {
return cl
}
var classList = {
add: add
, remove: remove
, contains: contains
, toggle: toggle
, toString: $toString
, length: 0
, item: item
}
return classList
function add(token) {
var list = getTokens()
if (indexof(list, token) > -1) {
return
}
list.push(token)
setTokens(list)
}
function remove(token) {
var list = getTokens()
, index = indexof(list, token)
if (index === -1) {
return
}
list.splice(index, 1)
setTokens(list)
}
function contains(token) {
return indexof(getTokens(), token) > -1
}
function toggle(token) {
if (contains(token)) {
remove(token)
return false
} else {
add(token)
return true
}
}
function $toString() {
return elem.className
}
function item(index) {
var tokens = getTokens()
return tokens[index] || null
}
function getTokens() {
var className = elem.className
return filter(className.split(" "), isTruthy)
}
function setTokens(list) {
var length = list.length
elem.className = list.join(" ")
classList.length = length
for (var i = 0; i < list.length; i++) {
classList[i] = list[i]
}
delete list[length]
}
}
function filter (arr, fn) {
var ret = []
for (var i = 0; i < arr.length; i++) {
if (fn(arr[i])) ret.push(arr[i])
}
return ret
}
function isTruthy(value) {
return !!value
}
},{"indexof":4}],4:[function(require,module,exports){
var indexOf = [].indexOf;
module.exports = function(arr, obj){
if (indexOf) return arr.indexOf(obj);
for (var i = 0; i < arr.length; ++i) {
if (arr[i] === obj) return i;
}
return -1;
};
},{}],5:[function(require,module,exports){
var h = require("./index.js");
module.exports = h;
/*
$(function () {
var newDoc = h('p',
h('ul', 'bang bang bang'.split(/\s/).map(function (word) {
return h('li', word);
}))
);
$('body').html(newDoc.outerHTML);
});
*/
},{"./index.js":1}],6:[function(require,module,exports){
},{}]},{},[5]);
return Hyperscript;
});

@ -4,6 +4,24 @@
<meta content="text/html; charset=utf-8" http-equiv="content-type"/> <meta content="text/html; charset=utf-8" http-equiv="content-type"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/> <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<script data-main="main" src="/bower_components/requirejs/require.js"></script> <script data-main="main" src="/bower_components/requirejs/require.js"></script>
<style>
.report {
font-size: 40px;
}
.success {
border: 3px solid green;
}
.failure {
border: 3px solid red;
}
.error {
border: 1px solid red;
}
</style>
</head> </head>
<body> <body>
@ -12,7 +30,12 @@
<h2>Test 1</h2> <h2>Test 1</h2>
<h3>class strings</h3> <h3>class strings</h3>
<!-- put in weird HTML that might cause problems --> <!-- put in weird HTML that might cause problems -->
<div id="target"><p class=" alice bob charlie has.dot" id="bang">pewpewpew</p></div> <div id="target"><p class=" alice bob charlie has.dot">pewpewpew</p></div>
<h2>Test 1</h2>
<h3>paragraph text</h3>
<!-- -->
<div id="quot"><p>"pewpewpew"</p></div>
<hr> <hr>
@ -20,6 +43,6 @@
<h3>XWiki Macros</h3> <h3>XWiki Macros</h3>
<!-- Can we serialize XWiki Macros? --> <!-- Can we serialize XWiki Macros? -->
<div id="widget"><div data-cke-widget-id="0" tabindex="-1" data-cke-widget-wrapper="1" data-cke-filter="off" class="cke_widget_wrapper cke_widget_block" data-cke-display-name="macro:velocity" contenteditable="false"><div class="macro cke_widget_element" data-macro="startmacro:velocity|-||-|Here is a macro" data-cke-widget-data="%7B%22classes%22%3A%7B%22macro%22%3A1%7D%7D" data-cke-widget-upcasted="1" data-cke-widget-keep-attr="0" data-widget="xwiki-macro"><p>Here is a macro</p></div><span style='background: rgba(220, 220, 220, 0.5) url("/common/cryptofist.png") repeat scroll 0% 0%; top: -15px; left: 0px; display: block;' class="cke_reset cke_widget_drag_handler_container"><img title="Click and drag to move" src="data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==" data-cke-widget-drag-handler="1" class="cke_reset cke_widget_drag_handler" height="15" width="15"></span></div></div> <div id="widget"><div data-cke-widget-id="0" tabindex="-1" data-cke-widget-wrapper="1" data-cke-filter="off" class="cke_widget_wrapper cke_widget_block" data-cke-display-name="macro:velocity" contenteditable="false"><div class="macro cke_widget_element" data-macro="startmacro:velocity|-||-|Here is a macro" data-cke-widget-data="%7B%22classes%22%3A%7B%22macro%22%3A1%7D%7D" data-cke-widget-upcasted="1" data-cke-widget-keep-attr="0" data-widget="xwiki-macro"><p>Here is a macro</p></div><span style='background: rgba(220, 220, 220, 0.5) url("/customize/cryptofist_small.png") repeat scroll 0% 0%; top: -15px; left: 0px; display: block;' class="cke_reset cke_widget_drag_handler_container"><img title="Click and drag to move" src="data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==" data-cke-widget-drag-handler="1" class="cke_reset cke_widget_drag_handler" height="15" width="15"></span></div></div>
<hr> <hr>

@ -1,27 +1,111 @@
require.config({ paths: { 'json.sortify': '/bower_components/json.sortify/dist/JSON.sortify' } });
define([ define([
'/bower_components/jquery/dist/jquery.min.js', '/bower_components/jquery/dist/jquery.min.js',
'/assert/hyperjson.js', // serializing classes as an attribute '/bower_components/hyperjson/hyperjson.amd.js', // serializing classes as an attribute
'/assert/hyperscript.js', // using setAttribute '/common/hyperscript.js', // using setAttribute
'/common/TextPatcher.js' '/bower_components/textpatcher/TextPatcher.amd.js',
], function (jQuery, Hyperjson, Hyperscript, TextPatcher) { 'json.sortify',
], function (jQuery, Hyperjson, Hyperscript, TextPatcher, Sortify) {
var $ = window.jQuery; var $ = window.jQuery;
window.Hyperjson = Hyperjson; window.Hyperjson = Hyperjson;
window.Hyperscript = Hyperscript; window.Hyperscript = Hyperscript;
window.TextPatcher = TextPatcher; window.TextPatcher = TextPatcher;
window.Sortify = Sortify;
var assertions = 0; var assertions = 0;
var failed = false;
var failedOn;
var failMessages = [];
var ASSERTS = [];
var runASSERTS = function () {
ASSERTS.forEach(function (f, index) {
f(index);
});
};
var assert = function (test, msg) { var assert = function (test, msg) {
if (test()) { ASSERTS.push(function (i) {
var returned = test();
if (returned === true) {
assertions++; assertions++;
} else { } else {
throw new Error(msg || ''); failed = true;
failedOn = assertions;
failMessages.push({
test: i,
message: msg,
output: returned,
});
} }
});
}; };
var $body = $('body'); var $body = $('body');
var roundTrip = function (target) { var HJSON_list = [
'["DIV#target",{},[["P#bang",{"class":" alice bob charlie has.dot"},["pewpewpew"]]]]',
'["DIV#quot",{},[["P",{},["\\"pewpewpew\\""]]]]',
'["DIV#widget",{},[["DIV",{"class":"cke_widget_wrapper cke_widget_block","contenteditable":"false","data-cke-display-name":"macro:velocity","data-cke-filter":"off","data-cke-widget-id":"0","data-cke-widget-wrapper":"1","tabindex":"-1"},[["DIV",{"class":"macro cke_widget_element","data-cke-widget-data":"%7B%22classes%22%3A%7B%22macro%22%3A1%7D%7D","data-cke-widget-keep-attr":"0","data-cke-widget-upcasted":"1","data-macro":"startmacro:velocity|-||-|Here is a macro","data-widget":"xwiki-macro"},[["P",{},["Here is a macro"]]]],["SPAN",{"class":"cke_reset cke_widget_drag_handler_container","style":"background: rgba(220, 220, 220, 0.5) url(\\"/customize/cryptofist_small.png\\") repeat scroll 0% 0%; top: -15px; left: 0px; display: block;"},[["IMG",{"class":"cke_reset cke_widget_drag_handler","data-cke-widget-drag-handler":"1","height":"15","src":"data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==","title":"Click and drag to move","width":"15"},[]]]]]]]]',
];
var elementFilter = function () {
// pass everything
return true;
};
var attributeFilter = function (h) {
// don't filter anything
return h;
};
var HJSON_equal = function (shjson) {
assert(function () {
// parse your stringified Hyperjson
var hjson;
try {
hjson = JSON.parse(shjson);
} catch (e) {
console.log(e);
return false;
}
// turn it into a DOM
var DOM = Hyperjson.callOn(hjson, Hyperscript);
// turn it back into stringified Hyperjson, but apply filters
var shjson2 = Sortify(Hyperjson.fromDOM(DOM, elementFilter, attributeFilter));
var success = shjson === shjson2;
var op = TextPatcher.diff(shjson, shjson2);
//console.log(TextPatcher.format(shjson, op));
var diff = TextPatcher.format(shjson, op);
if (success) {
return true;
} else {
return '<br><br>insert: ' + diff.insert + '<br><br>' +
'remove: ' + diff.remove + '<br><br>';
}
}, "expected hyperjson equality");
};
HJSON_list.map(HJSON_equal);
/* FIXME
This test is not correct. It passes in Firefox, but fails in Chrome,
even though for our purposes the produced code is valid. Passing
`<p class="bob" id="alice"></p>` through the function yields
`<p id="alice" class="bob"></p>`. This is the same element, but string
equality is not a correct metric. */
var roundTrip = function (sel) {
var target = $(sel)[0];
assert(function () { assert(function () {
var hjson = Hyperjson.fromDOM(target); var hjson = Hyperjson.fromDOM(target);
var cloned = Hyperjson.callOn(hjson, Hyperscript); var cloned = Hyperjson.callOn(hjson, Hyperscript);
@ -29,22 +113,117 @@ define([
var success = cloned.outerHTML === target.outerHTML; var success = cloned.outerHTML === target.outerHTML;
if (!success) { if (!success) {
var op = TextPatcher.diff(target.outerHTML, cloned.outerHTML);
window.DEBUG = { window.DEBUG = {
error: "Expected equality between A and B", error: "Expected equality between A and B",
A: target.outerHTML, A: target.outerHTML,
B: cloned.outerHTML, B: cloned.outerHTML,
target: target, diff: op
diff: TextPatcher.diff(target.outerHTML, cloned.outerHTML)
}; };
console.log(JSON.stringify(window.DEBUG, null, 2)); //console.log(JSON.stringify(window.DEBUG, null, 2));
console.log("DIFF:");
TextPatcher.log(target.outerHTML, op);
} }
return success; return success;
}, "Round trip serialization introduced artifacts."); }, "Round trip serialization introduced artifacts.");
}; };
roundTrip($('#target')[0]); var HTML_list = [
roundTrip($('#widget')[0]); '#target',
'#widget', // fails in Firefox 19?
'#quot',
];
HTML_list.forEach(roundTrip);
var strungJSON = function (orig) {
var result;
assert(function () {
result = JSON.stringify(JSON.parse(orig));
return result === orig;
}, "expected result (" + result + ") to equal original (" + orig + ")");
};
[ '{"border":"1","style":{"width":"500px"}}',
'{"style":"width: 500px;","border":"1"}',
].forEach(function (orig) {
strungJSON(orig);
});
//assert(function () { }, "this is expected to fail");
/* TODO Test how browsers handle weird elements
like "_moz-resizing":"true"
and anything else you can think of.
Start with Hyperjson, turn it into a DOM and come back
*/
var swap = function (str, dict) {
return str.replace(/\{\{(.*?)\}\}/g, function (all, key) {
return typeof dict[key] !== 'undefined'? dict[key] : all;
});
};
var multiline = function (f) {
var str;
f.toString().replace(/\/\*([\s\S]*)\*\//g, function (all, out) {
str = out;
});
return str || '';
};
var formatFailures = function () {
var template = multiline(function () { /*
<p class="error">
Failed on test number {{test}} with error message:
"{{message}}"
</p>
<p>
The test returned:
{{output}}
</p>
<br>
*/});
return failMessages.map(function (obj) {
console.log(obj);
return swap(template, obj);
}).join("\n");
};
runASSERTS();
$("body").html(function (i, val) {
var dict = {
previous: val,
totalAssertions: ASSERTS.length,
passedAssertions: assertions,
plural: (assertions === 1? '' : 's'),
failMessages: formatFailures()
};
var SUCCESS = swap(multiline(function(){/*
<div class="report">{{passedAssertions}} / {{totalAssertions}} test{{plural}} passed.
{{failMessages}}
</div>
{{previous}}
*/}), dict);
var report = SUCCESS;
return report;
});
var $report = $('.report');
$report.addClass(failed?'failure':'success');
console.log("%s test%s passed", assertions, assertions === 1? '':'s');
}); });

@ -0,0 +1,78 @@
<!DOCTYPE html>
<html>
<head>
<meta content="text/html; charset=utf-8" http-equiv="content-type"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<script data-main="main" src="/bower_components/requirejs/require.js"></script>
<style>
html, body{
padding: 0px;
margin: 0px;
overflow: hidden;
box-sizing: border-box;
}
textarea{
width: 100%;
height: 100vh;
max-width: 100%;
max-height: 100vh;
font-size: 18px;
background-color: #073642;
color: #839496;
overflow-x: hidden;
/* disallow textarea resizes */
resize: none;
}
canvas {
border: 5px solid black;
}
#clear {
display: inline;
}
#colors {
z-index: 100;
border: 3px solid black;
padding: 5px;
}
#copy {
padding-left: 75px;
}
span.palette {
height: 4vw;
width: 4vw;
display: inline-block;
margin: 5px;
border: 2px solid black;
}
#controls {
display: block;
position: relative;
border: 3px solid black;
}
#width, #colors {
position: relative;
}
</style>
</head>
<body>
<canvas id="canvas" width="600" height="600" ></canvas>
<div id="copy">
<h2>Welcome to CryptCanvas!</h2>
<h3>Zero Knowledge Realtime Collaborative Canvas Editing</h3>
</div>
<div id="controls">
<button id="clear">Clear</button>
<input id="width" type="number" value="5"></input>
<div id="colors">&nbsp;</div>
</div>
</body>
</html>

@ -0,0 +1,120 @@
require.config({ paths: {
'json.sortify': '/bower_components/json.sortify/dist/JSON.sortify'
}});
define([
'/api/config?cb=' + Math.random().toString(16).substring(2),
'/bower_components/chainpad-netflux/chainpad-netflux.js',
'/common/messages.js',
'/bower_components/chainpad-crypto/crypto.js',
'/bower_components/textpatcher/TextPatcher.amd.js',
'json.sortify',
'/bower_components/chainpad-json-validator/json-ot.js',
'/bower_components/fabric.js/dist/fabric.min.js',
'/bower_components/jquery/dist/jquery.min.js',
'/customize/pad.js'
], function (Config, Realtime, Messages, Crypto, TextPatcher, JSONSortify, JsonOT) {
var module = window.APP = { };
var $ = module.$ = window.jQuery;
var Fabric = module.Fabric = window.fabric;
var key;
var channel = '';
if (!/#/.test(window.location.href)) {
key = Crypto.genKey();
} else {
var hash = window.location.hash.slice(1);
channel = hash.slice(0, 32);
key = hash.slice(32);
}
/* Initialize Fabric */
var canvas = module.canvas = new Fabric.Canvas('canvas');
var $canvas = $('canvas');
var $width = $('#width');
var updateBrushWidth = function () {
canvas.freeDrawingBrush.width = Number($width.val());
};
updateBrushWidth();
$width.on('change', updateBrushWidth);
var palette = ['red', 'blue', 'green', 'white', 'black', 'purple',
'gray', 'beige', 'brown', 'cyan', 'darkcyan', 'gold', 'yellow', 'pink'];
var $colors = $('#colors');
$colors.html(function (i, val) {
return palette.map(function (c) {
return "<span class='palette' style='background-color:"+c+"'></span>";
}).join("");
});
$('.palette').on('click', function () {
var color = $(this).css('background-color');
canvas.freeDrawingBrush.color = color;
});
var setEditable = function (bool) {
canvas.isDrawingMode = bool;
$canvas.css('border-color', bool? 'black': 'red');
};
var initializing = true;
var config = module.config = {
// TODO initialState ?
websocketURL: Config.websocketURL,
userName: Crypto.rand64(8),
channel: channel,
cryptKey: key,
crypto: Crypto,
transformFunction: JsonOT.validate,
};
var onInit = config.onInit = function (info) {
window.location.hash = info.channel + key;
$(window).on('hashchange', function() {
window.location.reload();
});
};
var onRemote = config.onRemote = function () {
if (initializing) { return; }
var userDoc = module.realtime.getUserDoc();
canvas.loadFromJSON(userDoc);
canvas.renderAll();
};
var onLocal = config.onLocal = function () {
if (initializing) { return; }
var content = JSONSortify(canvas.toDatalessJSON());
module.patchText(content);
};
var onReady = config.onReady = function (info) {
var realtime = module.realtime = info.realtime;
module.patchText = TextPatcher.create({
realtime: realtime
});
setEditable(true);
initializing = false;
onRemote();
};
var onAbort = config.onAbort = function (info) {
setEditable(false);
window.alert("Server Connection Lost");
};
var rt = Realtime.start(config);
canvas.on('mouse:up', onLocal);
$('#clear').on('click', function () {
canvas.clear();
});
});

@ -27,6 +27,8 @@ Alex Piggott
Aliaksei Chapyzhenka Aliaksei Chapyzhenka
Allen Sarkisyan Allen Sarkisyan
Amin Shali Amin Shali
Amin Ullah Khan
amshali@google.com
Amsul Amsul
amuntean amuntean
Amy Amy
@ -59,10 +61,13 @@ Anthony Grimes
Anton Kovalyov Anton Kovalyov
AQNOUCH Mohammed AQNOUCH Mohammed
areos areos
Arnab Bose
as3boyan as3boyan
AtomicPages LLC AtomicPages LLC
Atul Bhouraskar Atul Bhouraskar
Aurelian Oancea Aurelian Oancea
Barret Rennie
Basarat Ali Syed
Bastian Müller Bastian Müller
belhaj belhaj
Bem Jones-Bey Bem Jones-Bey
@ -70,6 +75,7 @@ benbro
Beni Cherniavsky-Paskin Beni Cherniavsky-Paskin
Benjamin DeCoste Benjamin DeCoste
Ben Keen Ben Keen
Ben Mosher
Bernhard Sirlinger Bernhard Sirlinger
Bert Chang Bert Chang
Billy Moon Billy Moon
@ -86,11 +92,14 @@ Brett Zamir
Brian Grinstead Brian Grinstead
Brian Sletten Brian Sletten
Bruce Mitchener Bruce Mitchener
Caitlin Potter
Calin Barbat Calin Barbat
Chad Jolly
Chandra Sekhar Pydi Chandra Sekhar Pydi
Charles Skelton Charles Skelton
Cheah Chu Yeow Cheah Chu Yeow
Chris Coyier Chris Coyier
Chris Ford
Chris Granger Chris Granger
Chris Houseknecht Chris Houseknecht
Chris Lohfink Chris Lohfink
@ -100,9 +109,11 @@ Christian Petrov
Christopher Brown Christopher Brown
Christopher Mitchell Christopher Mitchell
Christopher Pfohl Christopher Pfohl
Chunliang Lyu
ciaranj ciaranj
CodeAnimal CodeAnimal
coderaiser coderaiser
Cole R Lawrence
ComFreek ComFreek
Curtis Gagliardi Curtis Gagliardi
dagsta dagsta
@ -114,6 +125,7 @@ Daniel, Dao Quang Minh
Daniele Di Sarli Daniele Di Sarli
Daniel Faust Daniel Faust
Daniel Huigens Daniel Huigens
Daniel Kesler
Daniel KJ Daniel KJ
Daniel Neel Daniel Neel
Daniel Parnell Daniel Parnell
@ -126,8 +138,10 @@ David Barnett
David Mignot David Mignot
David Pathakjee David Pathakjee
David Vázquez David Vázquez
David Whittington
deebugger deebugger
Deep Thought Deep Thought
Devin Abbott
Devon Carew Devon Carew
dignifiedquire dignifiedquire
Dimage Sapelkin Dimage Sapelkin
@ -139,13 +153,16 @@ Doug Wikle
Drew Bratcher Drew Bratcher
Drew Hintz Drew Hintz
Drew Khoury Drew Khoury
Drini Cami
Dror BG Dror BG
duralog duralog
eborden eborden
edsharp edsharp
ekhaled ekhaled
Elisée
Enam Mijbah Noor Enam Mijbah Noor
Eric Allam Eric Allam
Erik Welander
eustas eustas
Fabien O'Carroll Fabien O'Carroll
Fabio Zendhi Nagao Fabio Zendhi Nagao
@ -167,19 +184,24 @@ Gabriel Horner
Gabriel Nahmias Gabriel Nahmias
galambalazs galambalazs
Gautam Mehta Gautam Mehta
Gavin Douglas
gekkoe gekkoe
geowarin
Gerard Braad Gerard Braad
Gergely Hegykozi Gergely Hegykozi
Giovanni Calò Giovanni Calò
Glebov Boris
Glenn Jorde Glenn Jorde
Glenn Ruehle Glenn Ruehle
Golevka Golevka
Google Inc.
Gordon Smith Gordon Smith
Grant Skinner Grant Skinner
greengiant greengiant
Gregory Koberger Gregory Koberger
Guillaume Massé Guillaume Massé
Guillaume Massé Guillaume Massé
guraga
Gustavo Rodrigues Gustavo Rodrigues
Hakan Tunc Hakan Tunc
Hans Engel Hans Engel
@ -190,12 +212,14 @@ Herculano Campos
Hiroyuki Makino Hiroyuki Makino
hitsthings hitsthings
Hocdoc Hocdoc
Hugues Malphettes
Ian Beck Ian Beck
Ian Dickinson Ian Dickinson
Ian Wehrman Ian Wehrman
Ian Wetherbee Ian Wetherbee
Ice White Ice White
ICHIKAWA, Yuji ICHIKAWA, Yuji
idleberg
ilvalle ilvalle
Ingo Richter Ingo Richter
Irakli Gozalishvili Irakli Gozalishvili
@ -212,6 +236,7 @@ Jan Jongboom
jankeromnes jankeromnes
Jan Keromnes Jan Keromnes
Jan Odvarko Jan Odvarko
Jan Schär
Jan T. Sott Jan T. Sott
Jared Forsyth Jared Forsyth
Jason Jason
@ -227,12 +252,17 @@ jeffkenton
Jeff Pickhardt Jeff Pickhardt
jem (graphite) jem (graphite)
Jeremy Parmenter Jeremy Parmenter
Jim
JobJob
jochenberger
Jochen Berger Jochen Berger
Johan Ask Johan Ask
John Connor John Connor
John Engler
John Lees-Miller John Lees-Miller
John Snelson John Snelson
John Van Der Loo John Van Der Loo
Jon Ander Peñalba
Jonas Döbertin Jonas Döbertin
Jonathan Malmaud Jonathan Malmaud
jongalloway jongalloway
@ -240,6 +270,7 @@ Jon Malmaud
Jon Sangster Jon Sangster
Joost-Wim Boekesteijn Joost-Wim Boekesteijn
Joseph Pecoraro Joseph Pecoraro
Josh Cohen
Joshua Newman Joshua Newman
Josh Watzman Josh Watzman
jots jots
@ -248,11 +279,15 @@ ju1ius
Juan Benavides Romero Juan Benavides Romero
Jucovschi Constantin Jucovschi Constantin
Juho Vuori Juho Vuori
Julien Rebetez
Justin Andresen
Justin Hileman Justin Hileman
jwallers@gmail.com jwallers@gmail.com
kaniga kaniga
karevn karevn
Kayur Patel
Ken Newman Ken Newman
ken restivo
Ken Rockot Ken Rockot
Kevin Earls Kevin Earls
Kevin Sawicki Kevin Sawicki
@ -296,12 +331,15 @@ Marek Rudnicki
Marijn Haverbeke Marijn Haverbeke
Mário Gonçalves Mário Gonçalves
Mario Pietsch Mario Pietsch
Mark Anderson
Mark Lentczner Mark Lentczner
Marko Bonaci Marko Bonaci
Markus Bordihn
Martin Balek Martin Balek
Martín Gaitán Martín Gaitán
Martin Hasoň Martin Hasoň
Martin Hunt Martin Hunt
Martin Laine
Martin Zagora Martin Zagora
Mason Malone Mason Malone
Mateusz Paprocki Mateusz Paprocki
@ -323,10 +361,12 @@ Max Kirsch
Max Schaefer Max Schaefer
Max Xiantu Max Xiantu
mbarkhau mbarkhau
McBrainy
melpon melpon
Metatheos Metatheos
Micah Dubinko Micah Dubinko
Michael Michael
Michael Goderbauer
Michael Grey Michael Grey
Michael Kaminsky Michael Kaminsky
Michael Lehenbauer Michael Lehenbauer
@ -361,6 +401,7 @@ Nicholas Bollweg
Nicholas Bollweg (Nick) Nicholas Bollweg (Nick)
Nick Kreeger Nick Kreeger
Nick Small Nick Small
Nicolò Ribaudo
Niels van Groningen Niels van Groningen
nightwing nightwing
Nikita Beloglazov Nikita Beloglazov
@ -373,6 +414,7 @@ noragrossman
Norman Rzepka Norman Rzepka
Oreoluwa Onatemowo Oreoluwa Onatemowo
pablo pablo
pabloferz
Page Page
Panupong Pasupat Panupong Pasupat
paris paris
@ -382,18 +424,25 @@ Patrick Stoica
Patrick Strawderman Patrick Strawderman
Paul Garvin Paul Garvin
Paul Ivanov Paul Ivanov
Pavel
Pavel Feldman Pavel Feldman
Pavel Strashkin Pavel Strashkin
Paweł Bartkiewicz Paweł Bartkiewicz
peteguhl peteguhl
peter
Peter Flynn Peter Flynn
peterkroon peterkroon
Peter Kroon Peter Kroon
Philipp A
Philip Stadermann Philip Stadermann
Pierre Gerold
Piët Delport
prasanthj prasanthj
Prasanth J Prasanth J
Prayag Verma
Radek Piórkowski Radek Piórkowski
Rahul Rahul
Rahul Anand
ramwin1 ramwin1
Randall Mason Randall Mason
Randy Burden Randy Burden
@ -420,10 +469,12 @@ Sascha Peilicke
satamas satamas
satchmorun satchmorun
sathyamoorthi sathyamoorthi
S. Chris Colbert
SCLINIC\jdecker SCLINIC\jdecker
Scott Aikin Scott Aikin
Scott Goodhew Scott Goodhew
Sebastian Zaha Sebastian Zaha
Sergey Goder
Se-Won Kim Se-Won Kim
shaund shaund
shaun gilchrist shaun gilchrist
@ -434,6 +485,7 @@ Shiv Deepak
Shmuel Englard Shmuel Englard
Shubham Jain Shubham Jain
silverwind silverwind
sinkuu
snasa snasa
soliton4 soliton4
sonson sonson
@ -443,14 +495,19 @@ Stanislav Oaserele
Stas Kobzar Stas Kobzar
Stefan Borsje Stefan Borsje
Steffen Beyer Steffen Beyer
Steffen Bruchmann
Stephen Lavelle Stephen Lavelle
Steve Champagne
Steve O'Hara Steve O'Hara
stoskov stoskov
Stu Kennedy
Sungho Kim Sungho Kim
sverweij sverweij
Taha Jahangir Taha Jahangir
Tako Schotanus
Takuji Shimokawa Takuji Shimokawa
Tarmil Tarmil
TDaglis
tel tel
tfjgeorge tfjgeorge
Thaddee Tyl Thaddee Tyl
@ -471,6 +528,8 @@ Tom MacWright
Tony Jian Tony Jian
Travis Heppe Travis Heppe
Triangle717 Triangle717
Tristan Tarrant
TSUYUSATO Kitsune
twifkak twifkak
Vestimir Markov Vestimir Markov
vf vf
@ -481,15 +540,18 @@ wenli
Wes Cossick Wes Cossick
Wesley Wiser Wesley Wiser
Will Binns-Smith Will Binns-Smith
Will Dean
William Jamieson William Jamieson
William Stein William Stein
Willy Willy
Wojtek Ptak Wojtek Ptak
Wu Cheng-Han
Xavier Mendez Xavier Mendez
Yassin N. Hassan Yassin N. Hassan
YNH Webdev YNH Webdev
Yunchi Luo Yunchi Luo
Yuvi Panda Yuvi Panda
Zac Anger
Zachary Dremann Zachary Dremann
Zhang Hao Zhang Hao
zziuni zziuni

@ -0,0 +1,718 @@
## 5.13.2 (2016-03-23)
### Bugfixes
Solves a problem where the gutter would sometimes not extend all the way to the end of the document.
## 5.13.0 (2016-03-21)
### New features
New DOM event forwarded: [`"dragleave"`](http://codemirror.net/doc/manual.html#event_dom).
[protobuf mode](http://codemirror.net/mode/protobuf/index.html): Newly added.
### Bugfixes
Fix problem where [`findMarks`](http://codemirror.net/doc/manual.html#findMarks) sometimes failed to find multi-line marks.
Fix crash that showed up when atomic ranges and bidi text were combined.
[show-hint addon](http://codemirror.net/demo/complete.html): Completion widgets no longer close when the line indented or dedented.
[merge addon](http://codemirror.net/demo/merge.html): Fix bug when merging chunks at the end of the file.
[placeholder addon](http://codemirror.net/doc/manual.html#addon_placeholder): No longer gets confused by [`swapDoc`](http://codemirror.net/doc/manual.html#swapDoc).
[simplescrollbars addon](http://codemirror.net/doc/manual.html#addon_simplescrollbars): Fix invalid state when deleting at end of document.
[clike mode](http://codemirror.net/mode/clike/index.html): No longer gets confused when a comment starts after an operator.
[markdown mode](http://codemirror.net/mode/markdown/index.html): Now supports CommonMark-style flexible list indentation.
[dylan mode](http://codemirror.net/mode/dylan/index.html): Several improvements and fixes.
## 5.12.0 (2016-02-19)
### New features
[Vim bindings](http://codemirror.net/demo/vim.html): Ctrl-Q is now an alias for Ctrl-V.
[Vim bindings](http://codemirror.net/demo/vim.html): The Vim API now exposes an `unmap` method to unmap bindings.
[active-line addon](http://codemirror.net/demo/activeline.html): This addon can now style the active line's gutter.
[FCL mode](http://codemirror.net/mode/fcl/): Newly added.
[SQL mode](http://codemirror.net/mode/sql/): Now has a Postgresql dialect.
### Bugfixes
Fix [issue](https://github.com/codemirror/CodeMirror/issues/3781) where trying to scroll to a horizontal position outside of the document's width could cause the gutter to be positioned incorrectly.
Use absolute, rather than fixed positioning in the context-menu intercept hack, to work around a [problem](https://github.com/codemirror/CodeMirror/issues/3238) when the editor is inside a transformed parent container.
Solve a [problem](https://github.com/codemirror/CodeMirror/issues/3821) where the horizontal scrollbar could hide text in Firefox.
Fix a [bug](https://github.com/codemirror/CodeMirror/issues/3834) that caused phantom scroll space under the text in some situations.
[Sublime Text bindings](http://codemirror.net/demo/sublime.html): Bind delete-line to Shift-Ctrl-K on OS X.
[Markdown mode](http://codemirror.net/mode/markdown/): Fix [issue](https://github.com/codemirror/CodeMirror/issues/3787) where the mode would keep state related to fenced code blocks in an unsafe way, leading to occasional corrupted parses.
[Markdown mode](http://codemirror.net/mode/markdown/): Ignore backslashes in code fragments.
[Markdown mode](http://codemirror.net/mode/markdown/): Use whichever mode is registered as `text/html` to parse HTML.
[Clike mode](http://codemirror.net/mode/clike/): Improve indentation of Scala `=>` functions.
[Python mode](http://codemirror.net/mode/python/): Improve indentation of bracketed code.
[HTMLMixed mode](http://codemirror.net/mode/htmlmixed/): Support multi-line opening tags for sub-languages (`<script>`, `<style>`, etc).
[Spreadsheet mode](http://codemirror.net/mode/spreadsheet/): Fix bug where the mode did not advance the stream when finding a backslash.
[XML mode](http://codemirror.net/mode/xml/): The mode now takes a `matchClosing` option to configure whether mismatched closing tags should be highlighted as errors.
## 5.11.0 (2016-01-20)
* New modes: [JSX](http://codemirror.net/mode/jsx/index.html), [literate Haskell](http://codemirror.net/mode/haskell-literate/index.html)
* The editor now forwards more [DOM events](http://codemirror.net/doc/manual.html#event_dom): `cut`, `copy`, `paste`, and `touchstart`. It will also forward `mousedown` for drag events
* Fixes a bug where bookmarks next to collapsed spans were not rendered
* The [Swift](http://codemirror.net/mode/swift/index.html) mode now supports auto-indentation
* Frontmatters in the [YAML frontmatter](http://codemirror.net/mode/yaml-frontmatter/index.html) mode are now optional as intended
## 5.10.0 (2015-12-21)
* Modify the way [atomic ranges](http://codemirror.net/doc/manual.html#mark_atomic) are skipped by selection to try and make it less surprising.
* The [Swift mode](http://codemirror.net/mode/swift/index.html) was rewritten.
* New addon: [jump-to-line](http://codemirror.net/doc/manual.html#addon_jump-to-line).
* New method: [`isReadOnly`](http://codemirror.net/doc/manual.html#isReadOnly).
* The [show-hint addon](http://codemirror.net/doc/manual.html#addon_show-hint) now defaults to picking completions on single click.
* The object passed to [`"beforeSelectionChange"`](http://codemirror.net/doc/manual.html#event_beforeSelectionChange) events now has an `origin` property.
* New mode: [Crystal](http://codemirror.net/mode/crystal/index.html).
## 5.9.0 (2015-11-23)
* Improve the way overlay (OS X-style) scrollbars are handled
* Make [annotatescrollbar](http://codemirror.net/doc/manual.html#addon_annotatescrollbar) and scrollpastend addons work properly together
* Make [show-hint](http://codemirror.net/doc/manual.html#addon_show-hint) addon select options on single click by default, move selection to hovered item
* Properly fold comments that include block-comment-start markers
* Many small language mode fixes
## 5.8.0 (2015-10-20)
* Fixes an infinite loop in the [hardwrap addon](http://codemirror.net/doc/manual.html#addon_hardwrap)
* New modes: [NSIS](http://codemirror.net/mode/nsis/index.html), [Ceylon](http://codemirror.net/mode/clike/index.html)
* The Kotlin mode is now a [clike](http://codemirror.net/mode/clike/index.html) dialect, rather than a stand-alone mode
* New option: [`allowDropFileTypes`](http://codemirror.net/doc/manual.html#option_allowDropFileTypes). Binary files can no longer be dropped into CodeMirror
* New themes: [bespin](http://codemirror.net/demo/theme.html#bespin), [hopscotch](http://codemirror.net/demo/theme.html#hopscotch), [isotope](http://codemirror.net/demo/theme.html#isotope), [railscasts](http://codemirror.net/demo/theme.html#railscasts)
## 5.7.0 (2015-09-21)
* New modes: [Vue](http://codemirror.net/mode/vue/index.html), [Oz](http://codemirror.net/mode/oz/index.html), [MscGen](http://codemirror.net/mode/mscgen/index.html) (and dialects), [Closure Stylesheets](http://codemirror.net/mode/css/gss.html)
* Implement [CommonMark](http://commonmark.org)-style flexible list indent and cross-line code spans in [Markdown](http://codemirror.net/mode/markdown/index.html) mode
* Add a replace-all button to the [search addon](http://codemirror.net/doc/manual.html#addon_search), and make the persistent search dialog transparent when it obscures the match
* Handle `acync`/`await` and ocal and binary numbers in [JavaScript mode](http://codemirror.net/mode/javascript/index.html)
* Fix various issues with the [Haxe mode](http://codemirror.net/mode/haxe/index.html)
* Make the [closebrackets addon](http://codemirror.net/doc/manual.html#addon_closebrackets) select only the wrapped text when wrapping selection in brackets
* Tokenize properties as properties in the [CoffeeScript mode](http://codemirror.net/mode/coffeescript/index.html)
* The [placeholder addon](http://codemirror.net/doc/manual.html#addon_placeholder) now accepts a DOM node as well as a string placeholder
## 5.6.0 (2015-08-20)
* Fix bug where you could paste into a `readOnly` editor
* Show a cursor at the drop location when dragging over the editor
* The [Rust mode](http://codemirror.net/mode/rust/index.html) was rewritten to handle modern Rust
* The editor and theme CSS was cleaned up. Some selectors are now less specific than before
* New theme: [abcdef](http://codemirror.net/demo/theme.html#abcdef)
* Lines longer than [`maxHighlightLength`](http://codemirror.net/doc/manual.html#option_maxHighlightLength) are now less likely to mess up indentation
* New addons: [`autorefresh`](http://codemirror.net/doc/manual.html#addon_autorefresh) for refreshing an editor the first time it becomes visible, and `html-lint` for using [HTMLHint](http://htmlhint.com/)
* The [`search`](http://codemirror.net/doc/manual.html#addon_search) addon now recognizes `\r` and `\n` in pattern and replacement input
## 5.5.0 (2015-07-20)
* New option: [`lineSeparator`](http://codemirror.net/doc/manual.html#option_lineSeparator) (with corresponding [method](http://codemirror.net/doc/manual.html#lineSeparator))
* New themes: [dracula](http://codemirror.net/demo/theme.html#dracula), [seti](http://codemirror.net/demo/theme.html#seti), [yeti](http://codemirror.net/demo/theme.html#yeti), [material](http://codemirror.net/demo/theme.html#material), and [icecoder](http://codemirror.net/demo/theme.html#icecoder)
* New modes: [Brainfuck](http://codemirror.net/mode/brainfuck/index.html), [VHDL](http://codemirror.net/mode/vhdl/index.html), Squirrel ([clike](http://codemirror.net/mode/clike/index.html) dialect)
* Define a `findPersistent` command in the [search](http://codemirror.net/demo/search.html) addon, for a dialog that stays open as you cycle through matches
* From this release on, the NPM module doesn't include documentation and demos
* Full [list of patches](https://github.com/codemirror/CodeMirror/compare/5.4.0...5.5.0)
## 5.4.0 (2015-06-25)
* New modes: [Twig](http://codemirror.net/mode/twig/index.html), [Elm](http://codemirror.net/mode/elm/index.html), [Factor](http://codemirror.net/mode/factor/index.html), [Swift](http://codemirror.net/mode/swift/index.html)
* Prefer clipboard API (if available) when pasting
* Refined definition highlighting in [clike](http://codemirror.net/mode/clike/index.html) mode
* Full [list of patches](https://github.com/codemirror/CodeMirror/compare/5.3.0...5.4.0)
## 5.3.0 (2015-05-20)
* Fix several regressions in the [`show-hint`](http://codemirror.net/doc/manual.html#addon_show-hint) addon (`completeSingle` option, `"shown"` and `"close"` events)
* The [vim mode](http://codemirror.net/demo/vim.html) API was [documented](http://codemirror.net/doc/manual.html#vimapi)
* New modes: [ASN.1](http://codemirror.net/mode/asn.1/index.html), [TTCN](http://codemirror.net/mode/ttcn/index.html), and [TTCN-CFG](http://codemirror.net/mode/ttcn-cfg/index.html)
* The [clike](http://codemirror.net/mode/clike/index.html) mode can now deep-indent `switch` statements, and roughly recognizes types and defined identifiers
* Full [list of patches](https://github.com/codemirror/CodeMirror/compare/5.2.0...5.3.0)
## 5.2.0 (2015-04-20)
* Fix several race conditions in [`show-hint`](http://codemirror.net/doc/manual.html#addon_show-hint)'s asynchronous mode
* Fix backspace binding in [Sublime bindings](http://codemirror.net/demo/sublime.html)
* Change the way IME is handled in the `"textarea"` [input style](http://codemirror.net/doc/manual.html#option_inputStyle)
* New modes: [MUMPS](http://codemirror.net/mode/mumps/index.html), [Handlebars](http://codemirror.net/mode/handlebars/index.html)
* Rewritten modes: [Django](http://codemirror.net/mode/django/index.html), [Z80](http://codemirror.net/mode/z80/index.html)
* New theme: [Liquibyte](http://codemirror.net/demo/theme.html#liquibyte)
* New option: [`lineWiseCopyCut`](http://codemirror.net/doc/manual.html#option_lineWiseCopyCut)
* The [Vim mode](http://codemirror.net/demo/vim.html) now supports buffer-local options and the `filetype` setting
* Full [list of patches](https://github.com/codemirror/CodeMirror/compare/5.1.0...5.2.0)
## 5.1.0 (2015-03-23)
* New modes: [ASCII armor](http://codemirror.net/mode/asciiarmor/index.html) (PGP data), [Troff](http://codemirror.net/mode/troff/index.html), and [CMake](http://codemirror.net/mode/cmake/index.html).
* Remove SmartyMixed mode, rewrite [Smarty](http://codemirror.net/mode/smarty/index.html) mode to supersede it.
* New commands in the [merge addon](http://codemirror.net/doc/manual.html#addon_merge): `goNextDiff` and `goPrevDiff`.
* The [closebrackets addon](http://codemirror.net/doc/manual.html#addon_closebrackets) can now be configured per mode.
* Full [list of patches](https://github.com/codemirror/CodeMirror/compare/5.0.0...5.1.0).
## 5.0.0 (2015-02-20)
* Experimental mobile support (tested on iOS, Android Chrome, stock Android browser)
* New option [`inputStyle`](http://codemirror.net/doc/manual.html#option_inputStyle) to switch between hidden textarea and contenteditable input.
* The [`getInputField`](http://codemirror.net/doc/manual.html#getInputField) method is no longer guaranteed to return a textarea.
* Full [list of patches](https://github.com/codemirror/CodeMirror/compare/4.13.0...5.0.0).
## 4.13.0 (2015-02-20)
* Fix the way the [`closetag`](http://codemirror.net/demo/closetag.html) demo handles the slash character.
* New modes: [Forth](http://codemirror.net/mode/forth/index.html), [Stylus](http://codemirror.net/mode/stylus/index.html).
* Make the [CSS mode](http://codemirror.net/mode/css/index.html) understand some modern CSS extensions.
* Have the [Scala mode](http://codemirror.net/mode/clike/index.html) handle symbols and triple-quoted strings.
* Full [list of patches](https://github.com/codemirror/CodeMirror/compare/4.12.0...4.13.0).
## 4.12.0 (2015-01-22)
* The [`closetag`](http://codemirror.net/doc/manual.html#addon_closetag) addon now defines a `"closeTag"` command.
* Adds a `findModeByFileName` to the [mode metadata](http://codemirror.net/doc/manual.html#addon_meta) addon.
* [Simple mode](http://codemirror.net/demo/simplemode.html) rules can now contain a `sol` property to only match at the start of a line.
* New addon: [`selection-pointer`](http://codemirror.net/doc/manual.html#addon_selection-pointer) to style the mouse cursor over the selection.
* Improvements to the [Sass mode](http://codemirror.net/mode/sass/index.html)'s indentation.
* The [Vim keymap](http://codemirror.net/demo/vim.html)'s search functionality now supports [scrollbar annotation](http://codemirror.net/doc/manual.html#addon_matchesonscrollbar).
* Full [list of patches](https://github.com/codemirror/CodeMirror/compare/4.11.0...4.12.0).
## 4.11.0 (2015-01-09)
Unfortunately, 4.10 did not take care of the Firefox scrolling issue entirely. This release adds two more patches to address that.
## 4.10.0 (2014-12-29)
Emergency single-patch update to 4.9\. Fixes Firefox-specific problem where the cursor could end up behind the horizontal scrollbar.
## 4.9.0 (2014-12-23)
* Overhauled scroll bar handling. Add pluggable [scrollbar implementations](http://codemirror.net/demo/simplescrollbars.html).
* Tweaked behavior for the [completion addons](http://codemirror.net/doc/manual.html#addon_show-hint) to not take text after cursor into account.
* Two new optional features in the [merge addon](http://codemirror.net/doc/manual.html#addon_merge): aligning editors, and folding unchanged text.
* New modes: [Dart](http://codemirror.net/mode/dart/index.html), [EBNF](http://codemirror.net/mode/ebnf/index.html), [spreadsheet](http://codemirror.net/mode/spreadsheet/index.html), and [Soy](http://codemirror.net/mode/soy/index.html).
* New [addon](http://codemirror.net/demo/panel.html) to show persistent panels below/above an editor.
* New themes: [zenburn](http://codemirror.net/demo/theme.html#zenburn) and [tomorrow night bright](http://codemirror.net/demo/theme.html#tomorrow-night-bright).
* Allow ctrl-click to clear existing cursors.
* Full [list of patches](https://github.com/codemirror/CodeMirror/compare/4.8.0...4.9.0).
## 4.8.0 (2014-11-22)
* Built-in support for [multi-stroke key bindings](http://codemirror.net/doc/manual.html#normalizeKeyMap).
* New method: [`getLineTokens`](http://codemirror.net/doc/manual.html#getLineTokens).
* New modes: [dockerfile](http://codemirror.net/mode/dockerfile/index.html), [IDL](http://codemirror.net/mode/idl/index.html), [Objective C](http://codemirror.net/mode/clike/index.html) (crude).
* Support styling of gutter backgrounds, allow `"gutter"` styles in [`addLineClass`](http://codemirror.net/doc/manual.html#addLineClass).
* Many improvements to the [Vim mode](http://codemirror.net/demo/vim.html), rewritten visual mode.
* Improvements to modes: [gfm](http://codemirror.net/mode/gfm/index.html) (strikethrough), [SPARQL](http://codemirror.net/mode/sparql/index.html) (version 1.1 support), and [sTeX](http://codemirror.net/mode/stex/index.html) (no more runaway math mode).
* Full [list of patches](https://github.com/codemirror/CodeMirror/compare/4.7.0...4.8.0).
## 4.7.0 (2014-10-20)
* **Incompatible**: The [lint addon](http://codemirror.net/demo/lint.html) now passes the editor's value as first argument to asynchronous lint functions, for consistency. The editor is still passed, as fourth argument.
* Improved handling of unicode identifiers in modes for languages that support them.
* More mode improvements: [CoffeeScript](http://codemirror.net/mode/coffeescript/index.html) (indentation), [Verilog](http://codemirror.net/mode/verilog/index.html) (indentation), [Scala](http://codemirror.net/mode/clike/index.html) (indentation, triple-quoted strings), and [PHP](http://codemirror.net/mode/php/index.html) (interpolated variables in heredoc strings).
* New modes: [Textile](http://codemirror.net/mode/textile/index.html) and [Tornado templates](http://codemirror.net/mode/tornado/index.html).
* Experimental new [way to define modes](http://codemirror.net/demo/simplemode.html).
* Improvements to the [Vim bindings](http://codemirror.net/demo/vim.html): Arbitrary insert mode key mappings are now possible, and text objects are supported in visual mode.
* The mode [meta-information file](http://codemirror.net/mode/meta.js) now includes information about file extensions, and [helper functions](http://codemirror.net/doc/manual.html#addon_meta) `findModeByMIME` and `findModeByExtension`.
* New logo!
* Full [list of patches](https://github.com/codemirror/CodeMirror/compare/4.6.0...4.7.0).
## 4.6.0 (2014-09-19)
* New mode: [Modelica](http://codemirror.net/mode/modelica/index.html)
* New method: [`findWordAt`](http://codemirror.net/doc/manual.html#findWordAt)
* Make it easier to [use text background styling](http://codemirror.net/demo/markselection.html)
* Full [list of patches](https://github.com/codemirror/CodeMirror/compare/4.5.0...4.6.0).
## 4.5.0 (2014-08-21)
* Fix several serious bugs with horizontal scrolling
* New mode: [Slim](http://codemirror.net/mode/slim/index.html)
* New command: [`goLineLeftSmart`](http://codemirror.net/doc/manual.html#command_goLineLeftSmart)
* More fixes and extensions for the [Vim](http://codemirror.net/demo/vim.html) visual block mode
* Full [list of patches](https://github.com/codemirror/CodeMirror/compare/4.4.0...4.5.0).
## 4.4.0 (2014-07-21)
* **Note:** Some events might now fire in slightly different order (`"change"` is still guaranteed to fire before `"cursorActivity"`)
* Nested operations in multiple editors are now synced (complete at same time, reducing DOM reflows)
* Visual block mode for [vim](http://codemirror.net/demo/vim.html) (<C-v>) is nearly complete
* New mode: [Kotlin](http://codemirror.net/mode/kotlin/index.html)
* Better multi-selection paste for text copied from multiple CodeMirror selections
* Full [list of patches](https://github.com/codemirror/CodeMirror/compare/4.3.0...4.4.0).
## 4.3.0 (2014-06-23)
* Several [vim bindings](http://codemirror.net/demo/vim.html) improvements: search and exCommand history, global flag for `:substitute`, `:global` command.
* Allow hiding the cursor by setting [`cursorBlinkRate`](http://codemirror.net/doc/manual.html#option_cursorBlinkRate) to a negative value.
* Make gutter markers themeable, use this in foldgutter.
* Full [list of patches](https://github.com/codemirror/CodeMirror/compare/4.2.0...4.3.0).
## 4.2.0 (2014-05-19)
* Fix problem where some modes were broken by the fact that empty tokens were forbidden.
* Several fixes to context menu handling.
* On undo, scroll _change_, not cursor, into view.
* Rewritten [Jade](http://codemirror.net/mode/jade/index.html) mode.
* Various improvements to [Shell](http://codemirror.net/mode/shell/index.html) (support for more syntax) and [Python](http://codemirror.net/mode/python/index.html) (better indentation) modes.
* New mode: [Cypher](http://codemirror.net/mode/cypher/index.html).
* New theme: [Neo](http://codemirror.net/demo/theme.html#neo).
* Support direct styling options (color, line style, width) in the [rulers](http://codemirror.net/doc/manual.html#addon_rulers) addon.
* Recognize per-editor configuration for the [show-hint](http://codemirror.net/doc/manual.html#addon_show-hint) and [foldcode](http://codemirror.net/doc/manual.html#addon_foldcode) addons.
* More intelligent scanning for existing close tags in [closetag](http://codemirror.net/doc/manual.html#addon_closetag) addon.
* In the [Vim bindings](http://codemirror.net/demo/vim.html): Fix bracket matching, support case conversion in visual mode, visual paste, append action.
* Full [list of patches](https://github.com/codemirror/CodeMirror/compare/4.1.0...4.2.0).
## 4.1.0 (2014-04-22)
* _Slightly incompatible_: The [`"cursorActivity"`](http://codemirror.net/doc/manual.html#event_cursorActivity) event now fires after all other events for the operation (and only for handlers that were actually registered at the time the activity happened).
* New command: [`insertSoftTab`](http://codemirror.net/doc/manual.html#command_insertSoftTab).
* New mode: [Django](http://codemirror.net/mode/django/index.html).
* Improved modes: [Verilog](http://codemirror.net/mode/verilog/index.html) (rewritten), [Jinja2](http://codemirror.net/mode/jinja2/index.html), [Haxe](http://codemirror.net/mode/haxe/index.html), [PHP](http://codemirror.net/mode/php/index.html) (string interpolation highlighted), [JavaScript](http://codemirror.net/mode/javascript/index.html) (indentation of trailing else, template strings), [LiveScript](http://codemirror.net/mode/livescript/index.html) (multi-line strings).
* Many small issues from the 3.x→4.x transition were found and fixed.
* Full [list of patches](https://github.com/codemirror/CodeMirror/compare/4.0.3...4.1.0).
## 3.24.0 (2014-04-22)
Merges the improvements from 4.1 that could easily be applied to the 3.x code. Also improves the way the editor size is updated when line widgets change.
## 3.23.0 (2014-03-20)
* In the [XML mode](http://codemirror.net/mode/xml/index.html), add `brackets` style to angle brackets, fix case-sensitivity of tags for HTML.
* New mode: [Dylan](http://codemirror.net/mode/dylan/index.html).
* Many improvements to the [Vim bindings](http://codemirror.net/demo/vim.html).
## 3.22.0 (2014-02-21)
* Adds the [`findMarks`](http://codemirror.net/doc/manual.html#findMarks) method.
* New addons: [rulers](http://codemirror.net/doc/manual.html#addon_rulers), markdown-fold, yaml-lint.
* New theme: [mdn-like](http://codemirror.net/demo/theme.html#mdn-like).
* New mode: [Solr](http://codemirror.net/mode/solr/index.html).
* Full [list of patches](https://github.com/codemirror/CodeMirror/compare/3.21.0...3.22.0).
## 3.21.0 (2014-01-16)
* Auto-indenting a block will no longer add trailing whitespace to blank lines.
* Marking text has a new option [`clearWhenEmpty`](http://codemirror.net/doc/manual.html#markText) to control auto-removal.
* Several bugfixes in the handling of bidirectional text.
* The [XML](http://codemirror.net/mode/xml/index.html) and [CSS](http://codemirror.net/mode/css/index.html) modes were largely rewritten. [LESS](http://codemirror.net/mode/css/less.html) support was added to the CSS mode.
* The OCaml mode was moved to an [mllike](http://codemirror.net/mode/mllike/index.html) mode, F# support added.
* Make it possible to fetch multiple applicable helper values with [`getHelpers`](http://codemirror.net/doc/manual.html#getHelpers), and to register helpers matched on predicates with [`registerGlobalHelper`](http://codemirror.net/doc/manual.html#registerGlobalHelper).
* New theme [pastel-on-dark](http://codemirror.net/demo/theme.html#pastel-on-dark).
* Better ECMAScript 6 support in [JavaScript](http://codemirror.net/mode/javascript/index.html) mode.
* Full [list of patches](https://github.com/codemirror/CodeMirror/compare/3.20.0...3.21.0).
## 3.20.0 (2013-11-21)
* New modes: [Julia](http://codemirror.net/mode/julia/index.html) and [PEG.js](http://codemirror.net/mode/pegjs/index.html).
* Support ECMAScript 6 in the [JavaScript mode](http://codemirror.net/mode/javascript/index.html).
* Improved indentation for the [CoffeeScript mode](http://codemirror.net/mode/coffeescript/index.html).
* Make non-printable-character representation [configurable](http://codemirror.net/doc/manual.html#option_specialChars).
* Add notification functionality to [dialog](http://codemirror.net/doc/manual.html#addon_dialog) addon.
* Full [list of patches](https://github.com/codemirror/CodeMirror/compare/3.19.0...3.20.0).
## 3.19.0 (2013-10-21)
* New modes: [Eiffel](http://codemirror.net/mode/eiffel/index.html), [Gherkin](http://codemirror.net/mode/gherkin/index.html), [MSSQL dialect](http://codemirror.net/mode/sql/?mime=text/x-mssql).
* New addons: [hardwrap](http://codemirror.net/doc/manual.html#addon_hardwrap), [sql-hint](http://codemirror.net/doc/manual.html#addon_sql-hint).
* New theme: [MBO](http://codemirror.net/demo/theme.html#mbo).
* Add [support](http://codemirror.net/doc/manual.html#token_style_line) for line-level styling from mode tokenizers.
* Full [list of patches](https://github.com/codemirror/CodeMirror/compare/3.18.0...3.19.0).
## 3.18.0 (2013-09-23)
Emergency release to fix a problem in 3.17 where `.setOption("lineNumbers", false)` would raise an error.
## 3.17.0 (2013-09-23)
* New modes: [Fortran](http://codemirror.net/mode/fortran/index.html), [Octave](http://codemirror.net/mode/octave/index.html) (Matlab), [TOML](http://codemirror.net/mode/toml/index.html), and [DTD](http://codemirror.net/mode/dtd/index.html).
* New addons: [`css-lint`](http://codemirror.net/addon/lint/css-lint.js), [`css-hint`](http://codemirror.net/doc/manual.html#addon_css-hint).
* Improve resilience to CSS 'frameworks' that globally mess up `box-sizing`.
* Full [list of patches](https://github.com/codemirror/CodeMirror/compare/3.16.0...3.17.0).
## 3.16.0 (2013-08-21)
* The whole codebase is now under a single [license](http://codemirror.net/LICENSE) file.
* The project page was overhauled and redesigned.
* New themes: [Paraiso](http://codemirror.net/demo/theme.html#paraiso-dark) ([light](http://codemirror.net/demo/theme.html#paraiso-light)), [The Matrix](http://codemirror.net/demo/theme.html#the-matrix).
* Improved interaction between themes and [active-line](http://codemirror.net/doc/manual.html#addon_active-line)/[matchbrackets](http://codemirror.net/doc/manual.html#addon_matchbrackets) addons.
* New [folding](http://codemirror.net/doc/manual.html#addon_foldcode) function `CodeMirror.fold.comment`.
* Added [fullscreen](http://codemirror.net/doc/manual.html#addon_fullscreen) addon.
* Full [list of patches](https://github.com/codemirror/CodeMirror/compare/3.15.0...3.16.0).
## 3.15.0 (2013-07-29)
* New modes: [Jade](http://codemirror.net/mode/jade/index.html), [Nginx](http://codemirror.net/mode/nginx/index.html).
* New addons: [Tern](http://codemirror.net/demo/tern.html), [matchtags](http://codemirror.net/doc/manual.html#addon_matchtags), and [foldgutter](http://codemirror.net/doc/manual.html#addon_foldgutter).
* Introduced [_helper_](http://codemirror.net/doc/manual.html#getHelper) concept ([context](https://groups.google.com/forum/#!msg/codemirror/cOc0xvUUEUU/nLrX1-qnidgJ)).
* New method: [`getModeAt`](http://codemirror.net/doc/manual.html#getModeAt).
* New themes: base16 [dark](http://codemirror.net/demo/theme.html#base16-dark)/[light](http://codemirror.net/demo/theme.html#base16-light), 3024 [dark](http://codemirror.net/demo/theme.html#3024-night)/[light](http://codemirror.net/demo/theme.html#3024-day), [tomorrow-night](http://codemirror.net/demo/theme.html#tomorrow-night-eighties).
* Full [list of patches](https://github.com/codemirror/CodeMirror/compare/3.14.0...3.15.0).
## 3.14.0 (2013-06-20)
* New addons: [trailing space highlight](http://codemirror.net/doc/manual.html#addon_trailingspace), [XML completion](http://codemirror.net/doc/manual.html#addon_xml-hint) (rewritten), and [diff merging](http://codemirror.net/doc/manual.html#addon_merge).
* [`markText`](http://codemirror.net/doc/manual.html#markText) and [`addLineWidget`](http://codemirror.net/doc/manual.html#addLineWidget) now take a `handleMouseEvents` option.
* New methods: [`lineAtHeight`](http://codemirror.net/doc/manual.html#lineAtHeight), [`getTokenTypeAt`](http://codemirror.net/doc/manual.html#getTokenTypeAt).
* More precise cleanness-tracking using [`changeGeneration`](http://codemirror.net/doc/manual.html#changeGeneration) and [`isClean`](http://codemirror.net/doc/manual.html#isClean).
* Many extensions to [Emacs](http://codemirror.net/demo/emacs.html) mode (prefixes, more navigation units, and more).
* New events [`"keyHandled"`](http://codemirror.net/doc/manual.html#event_keyHandled) and [`"inputRead"`](http://codemirror.net/doc/manual.html#event_inputRead).
* Various improvements to [Ruby](http://codemirror.net/mode/ruby/index.html), [Smarty](http://codemirror.net/mode/smarty/index.html), [SQL](http://codemirror.net/mode/sql/index.html), and [Vim](http://codemirror.net/demo/vim.html) modes.
* Full [list of patches](https://github.com/codemirror/CodeMirror/compare/3.13.0...3.14.0).
## 3.13.0 (2013-05-20)
* New modes: [COBOL](http://codemirror.net/mode/cobol/index.html) and [HAML](http://codemirror.net/mode/haml/index.html).
* New options: [`cursorScrollMargin`](http://codemirror.net/doc/manual.html#option_cursorScrollMargin) and [`coverGutterNextToScrollbar`](http://codemirror.net/doc/manual.html#option_coverGutterNextToScrollbar).
* New addon: [commenting](http://codemirror.net/doc/manual.html#addon_comment).
* More features added to the [Vim keymap](http://codemirror.net/demo/vim.html).
* Full [list of patches](https://github.com/codemirror/CodeMirror/compare/v3.12...3.13.0).
## 3.12.0 (2013-04-19)
* New mode: [GNU assembler](http://codemirror.net/mode/gas/index.html).
* New options: [`maxHighlightLength`](http://codemirror.net/doc/manual.html#option_maxHighlightLength) and [`historyEventDelay`](http://codemirror.net/doc/manual.html#option_historyEventDelay).
* Added [`addToHistory`](http://codemirror.net/doc/manual.html#mark_addToHistory) option for `markText`.
* Various fixes to JavaScript tokenization and indentation corner cases.
* Further improvements to the vim mode.
* Full [list of patches](https://github.com/codemirror/CodeMirror/compare/v3.11...v3.12).
## 3.11.0 (2013-03-20)
* **Removed code:** `collapserange`, `formatting`, and `simple-hint` addons. `plsql` and `mysql` modes (use [`sql`](http://codemirror.net/mode/sql/index.html) mode).
* **Moved code:** the range-finding functions for folding now have [their own files](http://codemirror.net/addon/fold/).
* **Changed interface:** the [`continuecomment`](http://codemirror.net/doc/manual.html#addon_continuecomment) addon now exposes an option, rather than a command.
* New modes: [SCSS](http://codemirror.net/mode/css/scss.html), [Tcl](http://codemirror.net/mode/tcl/index.html), [LiveScript](http://codemirror.net/mode/livescript/index.html), and [mIRC](http://codemirror.net/mode/mirc/index.html).
* New addons: [`placeholder`](http://codemirror.net/demo/placeholder.html), [HTML completion](http://codemirror.net/demo/html5complete.html).
* New methods: [`hasFocus`](http://codemirror.net/doc/manual.html#hasFocus), [`defaultCharWidth`](http://codemirror.net/doc/manual.html#defaultCharWidth).
* New events: [`beforeCursorEnter`](http://codemirror.net/doc/manual.html#event_beforeCursorEnter), [`renderLine`](http://codemirror.net/doc/manual.html#event_renderLine).
* Many improvements to the [`show-hint`](http://codemirror.net/doc/manual.html#addon_show-hint) completion dialog addon.
* Tweak behavior of by-word cursor motion.
* Further improvements to the [vim mode](http://codemirror.net/demo/vim.html).
* Full [list of patches](https://github.com/codemirror/CodeMirror/compare/v3.1...v3.11).
## 3.02.0 (2013-01-25)
Single-bugfix release. Fixes a problem that prevents CodeMirror instances from being garbage-collected after they become unused.
## 3.01.0 (2013-01-21)
* Move all add-ons into an organized directory structure under [`/addon`](http://codemirror.net/addon/). **You might have to adjust your paths.**
* New modes: [D](http://codemirror.net/mode/d/index.html), [Sass](http://codemirror.net/mode/sass/index.html), [APL](http://codemirror.net/mode/apl/index.html), [SQL](http://codemirror.net/mode/sql/index.html) (configurable), and [Asterisk](http://codemirror.net/mode/asterisk/index.html).
* Several bugfixes in right-to-left text support.
* Add [`rtlMoveVisually`](http://codemirror.net/doc/manual.html#option_rtlMoveVisually) option.
* Improvements to vim keymap.
* Add built-in (lightweight) [overlay mode](http://codemirror.net/doc/manual.html#addOverlay) support.
* Support `showIfHidden` option for [line widgets](http://codemirror.net/doc/manual.html#addLineWidget).
* Add simple [Python hinter](http://codemirror.net/doc/manual.html#addon_python-hint).
* Bring back the [`fixedGutter`](http://codemirror.net/doc/manual.html#option_fixedGutter) option.
* Full [list of patches](https://github.com/codemirror/CodeMirror/compare/v3.0...v3.01).
## 3.1.0 (2013-02-21)
* **Incompatible:** key handlers may now _return_, rather than _throw_ `CodeMirror.Pass` to signal they didn't handle the key.
* Make documents a [first-class construct](http://codemirror.net/doc/manual.html#api_doc), support split views and subviews.
* Add a [new module](http://codemirror.net/doc/manual.html#addon_show-hint) for showing completion hints. Deprecate `simple-hint.js`.
* Extend [htmlmixed mode](http://codemirror.net/mode/htmlmixed/index.html) to allow custom handling of script types.
* Support an `insertLeft` option to [`setBookmark`](http://codemirror.net/doc/manual.html#setBookmark).
* Add an [`eachLine`](http://codemirror.net/doc/manual.html#eachLine) method to iterate over a document.
* New addon modules: [selection marking](http://codemirror.net/demo/markselection.html), [linting](http://codemirror.net/demo/lint.html), and [automatic bracket closing](http://codemirror.net/demo/closebrackets.html).
* Add [`"beforeChange"`](http://codemirror.net/doc/manual.html#event_beforeChange) and [`"beforeSelectionChange"`](http://codemirror.net/doc/manual.html#event_beforeSelectionChange) events.
* Add [`"hide"`](http://codemirror.net/doc/manual.html#event_hide) and [`"unhide"`](http://codemirror.net/doc/manual.html#event_unhide) events to marked ranges.
* Fix [`coordsChar`](http://codemirror.net/doc/manual.html#coordsChar)'s interpretation of its argument to match the documentation.
* New modes: [Turtle](http://codemirror.net/mode/turtle/index.html) and [Q](http://codemirror.net/mode/q/index.html).
* Further improvements to the [vim mode](http://codemirror.net/demo/vim.html).
* Full [list of patches](https://github.com/codemirror/CodeMirror/compare/v3.01...v3.1).
## 3.0.0 (2012-12-10)
**New major version**. Only partially backwards-compatible. See the [upgrading guide](http://codemirror.net/doc/upgrade_v3.html) for more information. Changes since release candidate 2:
* Rewritten VIM mode.
* Fix a few minor scrolling and sizing issues.
* Work around Safari segfault when dragging.
* Full [list of patches](https://github.com/codemirror/CodeMirror/compare/v3.0rc2...v3.0).
## 2.38.0 (2013-01-21)
Integrate some bugfixes, enhancements to the vim keymap, and new modes ([D](http://codemirror.net/mode/d/index.html), [Sass](http://codemirror.net/mode/sass/index.html), [APL](http://codemirror.net/mode/apl/index.html)) from the v3 branch.
## 2.37.0 (2012-12-20)
* New mode: [SQL](http://codemirror.net/mode/sql/index.html) (will replace [plsql](http://codemirror.net/mode/plsql/index.html) and [mysql](http://codemirror.net/mode/mysql/index.html) modes).
* Further work on the new VIM mode.
* Fix Cmd/Ctrl keys on recent Operas on OS X.
* Full [list of patches](https://github.com/codemirror/CodeMirror/compare/v2.36...v2.37).
## 2.36.0 (2012-11-20)
* New mode: [Z80 assembly](http://codemirror.net/mode/z80/index.html).
* New theme: [Twilight](http://codemirror.net/demo/theme.html#twilight).
* Add command-line compression helper.
* Make [`scrollIntoView`](http://codemirror.net/doc/manual.html#scrollIntoView) public.
* Add [`defaultTextHeight`](http://codemirror.net/doc/manual.html#defaultTextHeight) method.
* Various extensions to the vim keymap.
* Make [PHP mode](http://codemirror.net/mode/php/index.html) build on [mixed HTML mode](http://codemirror.net/mode/htmlmixed/index.html).
* Add [comment-continuing](http://codemirror.net/doc/manual.html#addon_continuecomment) add-on.
* Full [list of patches](http://codemirror.net/https://github.com/codemirror/CodeMirror/compare/v2.35...v2.36).
## 2.35.0 (2012-10-22)
* New (sub) mode: [TypeScript](http://codemirror.net/mode/javascript/typescript.html).
* Don't overwrite (insert key) when pasting.
* Fix several bugs in [`markText`](http://codemirror.net/doc/manual.html#markText)/undo interaction.
* Better indentation of JavaScript code without semicolons.
* Add [`defineInitHook`](http://codemirror.net/doc/manual.html#defineInitHook) function.
* Full [list of patches](https://github.com/codemirror/CodeMirror/compare/v2.34...v2.35).
## 2.34.0 (2012-09-19)
* New mode: [Common Lisp](http://codemirror.net/mode/commonlisp/index.html).
* Fix right-click select-all on most browsers.
* Change the way highlighting happens:
Saves memory and CPU cycles.
`compareStates` is no longer needed.
`onHighlightComplete` no longer works.
* Integrate mode (Markdown, XQuery, CSS, sTex) tests in central testsuite.
* Add a [`CodeMirror.version`](http://codemirror.net/doc/manual.html#version) property.
* More robust handling of nested modes in [formatting](http://codemirror.net/demo/formatting.html) and [closetag](http://codemirror.net/demo/closetag.html) plug-ins.
* Un/redo now preserves [marked text](http://codemirror.net/doc/manual.html#markText) and bookmarks.
* [Full list](https://github.com/codemirror/CodeMirror/compare/v2.33...v2.34) of patches.
## 2.33.0 (2012-08-23)
* New mode: [Sieve](http://codemirror.net/mode/sieve/index.html).
* New [`getViewPort`](http://codemirror.net/doc/manual.html#getViewport) and [`onViewportChange`](http://codemirror.net/doc/manual.html#option_onViewportChange) API.
* [Configurable](http://codemirror.net/doc/manual.html#option_cursorBlinkRate) cursor blink rate.
* Make binding a key to `false` disabling handling (again).
* Show non-printing characters as red dots.
* More tweaks to the scrolling model.
* Expanded testsuite. Basic linter added.
* Remove most uses of `innerHTML`. Remove `CodeMirror.htmlEscape`.
* [Full list](https://github.com/codemirror/CodeMirror/compare/v2.32...v2.33) of patches.
## 2.32.0 (2012-07-23)
Emergency fix for a bug where an editor with line wrapping on IE will break when there is _no_ scrollbar.
## 2.31.0 (2012-07-20)
* New modes: [OCaml](http://codemirror.net/mode/ocaml/index.html), [Haxe](http://codemirror.net/mode/haxe/index.html), and [VB.NET](http://codemirror.net/mode/vb/index.html).
* Several fixes to the new scrolling model.
* Add a [`setSize`](http://codemirror.net/doc/manual.html#setSize) method for programmatic resizing.
* Add [`getHistory`](http://codemirror.net/doc/manual.html#getHistory) and [`setHistory`](http://codemirror.net/doc/manual.html#setHistory) methods.
* Allow custom line separator string in [`getValue`](http://codemirror.net/doc/manual.html#getValue) and [`getRange`](http://codemirror.net/doc/manual.html#getRange).
* Support double- and triple-click drag, double-clicking whitespace.
* And more... [(all patches)](https://github.com/codemirror/CodeMirror/compare/v2.3...v2.31)
## 2.30.0 (2012-06-22)
* **New scrollbar implementation**. Should flicker less. Changes DOM structure of the editor.
* New theme: [vibrant-ink](http://codemirror.net/demo/theme.html#vibrant-ink).
* Many extensions to the VIM keymap (including text objects).
* Add [mode-multiplexing](http://codemirror.net/demo/multiplex.html) utility script.
* Fix bug where right-click paste works in read-only mode.
* Add a [`getScrollInfo`](http://codemirror.net/doc/manual.html#getScrollInfo) method.
* Lots of other [fixes](https://github.com/codemirror/CodeMirror/compare/v2.25...v2.3).
## 2.25.0 (2012-05-23)
* New mode: [Erlang](http://codemirror.net/mode/erlang/index.html).
* **Remove xmlpure mode** (use [xml.js](http://codemirror.net/mode/xml/index.html)).
* Fix line-wrapping in Opera.
* Fix X Windows middle-click paste in Chrome.
* Fix bug that broke pasting of huge documents.
* Fix backspace and tab key repeat in Opera.
## 2.24.0 (2012-04-23)
* **Drop support for Internet Explorer 6**.
* New modes: [Shell](http://codemirror.net/mode/shell/index.html), [Tiki wiki](http://codemirror.net/mode/tiki/index.html), [Pig Latin](http://codemirror.net/mode/pig/index.html).
* New themes: [Ambiance](http://codemirror.net/demo/theme.html#ambiance), [Blackboard](http://codemirror.net/demo/theme.html#blackboard).
* More control over drag/drop with [`dragDrop`](http://codemirror.net/doc/manual.html#option_dragDrop) and [`onDragEvent`](http://codemirror.net/doc/manual.html#option_onDragEvent) options.
* Make HTML mode a bit less pedantic.
* Add [`compoundChange`](http://codemirror.net/doc/manual.html#compoundChange) API method.
* Several fixes in undo history and line hiding.
* Remove (broken) support for `catchall` in key maps, add `nofallthrough` boolean field instead.
## 2.23.0 (2012-03-26)
* Change **default binding for tab**. Starting in 2.23, these bindings are default:
* Tab: Insert tab character
* Shift-tab: Reset line indentation to default
* Ctrl/Cmd-[: Reduce line indentation (old tab behaviour)
* Ctrl/Cmd-]: Increase line indentation (old shift-tab behaviour)
* New modes: [XQuery](http://codemirror.net/mode/xquery/index.html) and [VBScript](http://codemirror.net/mode/vbscript/index.html).
* Two new themes: [lesser-dark](http://codemirror.net/mode/less/index.html) and [xq-dark](http://codemirror.net/mode/xquery/index.html).
* Differentiate between background and text styles in [`setLineClass`](http://codemirror.net/doc/manual.html#setLineClass).
* Fix drag-and-drop in IE9+.
* Extend [`charCoords`](http://codemirror.net/doc/manual.html#charCoords) and [`cursorCoords`](http://codemirror.net/doc/manual.html#cursorCoords) with a `mode` argument.
* Add [`autofocus`](http://codemirror.net/doc/manual.html#option_autofocus) option.
* Add [`findMarksAt`](http://codemirror.net/doc/manual.html#findMarksAt) method.
## 2.22.0 (2012-02-27)
* Allow [key handlers](http://codemirror.net/doc/manual.html#keymaps) to pass up events, allow binding characters.
* Add [`autoClearEmptyLines`](http://codemirror.net/doc/manual.html#option_autoClearEmptyLines) option.
* Properly use tab stops when rendering tabs.
* Make PHP mode more robust.
* Support indentation blocks in [code folder](http://codemirror.net/doc/manual.html#addon_foldcode).
* Add a script for [highlighting instances of the selection](http://codemirror.net/doc/manual.html#addon_match-highlighter).
* New [.properties](http://codemirror.net/mode/properties/index.html) mode.
* Fix many bugs.
## 2.21.0 (2012-01-27)
* Added [LESS](http://codemirror.net/mode/less/index.html), [MySQL](http://codemirror.net/mode/mysql/index.html), [Go](http://codemirror.net/mode/go/index.html), and [Verilog](http://codemirror.net/mode/verilog/index.html) modes.
* Add [`smartIndent`](http://codemirror.net/doc/manual.html#option_smartIndent) option.
* Support a cursor in [`readOnly`](http://codemirror.net/doc/manual.html#option_readOnly)-mode.
* Support assigning multiple styles to a token.
* Use a new approach to drawing the selection.
* Add [`scrollTo`](http://codemirror.net/doc/manual.html#scrollTo) method.
* Allow undo/redo events to span non-adjacent lines.
* Lots and lots of bugfixes.
## 2.20.0 (2011-12-20)
* Slightly incompatible API changes. Read [this](http://codemirror.net/doc/upgrade_v2.2.html).
* New approach to [binding](http://codemirror.net/doc/manual.html#option_extraKeys) keys, support for [custom bindings](http://codemirror.net/doc/manual.html#option_keyMap).
* Support for overwrite (insert).
* [Custom-width](http://codemirror.net/doc/manual.html#option_tabSize) and [stylable](http://codemirror.net/demo/visibletabs.html) tabs.
* Moved more code into [add-on scripts](http://codemirror.net/doc/manual.html#addons).
* Support for sane vertical cursor movement in wrapped lines.
* More reliable handling of editing [marked text](http://codemirror.net/doc/manual.html#markText).
* Add minimal [emacs](http://codemirror.net/demo/emacs.html) and [vim](http://codemirror.net/demo/vim.html) bindings.
* Rename `coordsFromIndex` to [`posFromIndex`](http://codemirror.net/doc/manual.html#posFromIndex), add [`indexFromPos`](http://codemirror.net/doc/manual.html#indexFromPos) method.
## 2.18.0 (2011-11-21)
Fixes `TextMarker.clear`, which is broken in 2.17.
## 2.17.0 (2011-11-21)
* Add support for [line wrapping](http://codemirror.net/doc/manual.html#option_lineWrapping) and [code folding](http://codemirror.net/doc/manual.html#hideLine).
* Add [Github-style Markdown](http://codemirror.net/mode/gfm/index.html) mode.
* Add [Monokai](http://codemirror.net/theme/monokai.css) and [Rubyblue](http://codemirror.net/theme/rubyblue.css) themes.
* Add [`setBookmark`](http://codemirror.net/doc/manual.html#setBookmark) method.
* Move some of the demo code into reusable components under [`lib/util`](http://codemirror.net/addon/).
* Make screen-coord-finding code faster and more reliable.
* Fix drag-and-drop in Firefox.
* Improve support for IME.
* Speed up content rendering.
* Fix browser's built-in search in Webkit.
* Make double- and triple-click work in IE.
* Various fixes to modes.
## 2.16.0 (2011-10-27)
* Add [Perl](http://codemirror.net/mode/perl/index.html), [Rust](http://codemirror.net/mode/rust/index.html), [TiddlyWiki](http://codemirror.net/mode/tiddlywiki/index.html), and [Groovy](http://codemirror.net/mode/groovy/index.html) modes.
* Dragging text inside the editor now moves, rather than copies.
* Add a [`coordsFromIndex`](http://codemirror.net/doc/manual.html#coordsFromIndex) method.
* **API change**: `setValue` now no longer clears history. Use [`clearHistory`](http://codemirror.net/doc/manual.html#clearHistory) for that.
* **API change**: [`markText`](http://codemirror.net/doc/manual.html#markText) now returns an object with `clear` and `find` methods. Marked text is now more robust when edited.
* Fix editing code with tabs in Internet Explorer.
## 2.15.0 (2011-09-26)
Fix bug that snuck into 2.14: Clicking the character that currently has the cursor didn't re-focus the editor.
## 2.14.0 (2011-09-26)
* Add [Clojure](http://codemirror.net/mode/clojure/index.html), [Pascal](http://codemirror.net/mode/pascal/index.html), [NTriples](http://codemirror.net/mode/ntriples/index.html), [Jinja2](http://codemirror.net/mode/jinja2/index.html), and [Markdown](http://codemirror.net/mode/markdown/index.html) modes.
* Add [Cobalt](http://codemirror.net/theme/cobalt.css) and [Eclipse](http://codemirror.net/theme/eclipse.css) themes.
* Add a [`fixedGutter`](http://codemirror.net/doc/manual.html#option_fixedGutter) option.
* Fix bug with `setValue` breaking cursor movement.
* Make gutter updates much more efficient.
* Allow dragging of text out of the editor (on modern browsers).
## 2.13.0 (2011-08-23)
* Add [Ruby](http://codemirror.net/mode/ruby/index.html), [R](http://codemirror.net/mode/r/index.html), [CoffeeScript](http://codemirror.net/mode/coffeescript/index.html), and [Velocity](http://codemirror.net/mode/velocity/index.html) modes.
* Add [`getGutterElement`](http://codemirror.net/doc/manual.html#getGutterElement) to API.
* Several fixes to scrolling and positioning.
* Add [`smartHome`](http://codemirror.net/doc/manual.html#option_smartHome) option.
* Add an experimental [pure XML](http://codemirror.net/mode/xmlpure/index.html) mode.
## 2.12.0 (2011-07-25)
* Add a [SPARQL](http://codemirror.net/mode/sparql/index.html) mode.
* Fix bug with cursor jumping around in an unfocused editor in IE.
* Allow key and mouse events to bubble out of the editor. Ignore widget clicks.
* Solve cursor flakiness after undo/redo.
* Fix block-reindent ignoring the last few lines.
* Fix parsing of multi-line attrs in XML mode.
* Use `innerHTML` for HTML-escaping.
* Some fixes to indentation in C-like mode.
* Shrink horiz scrollbars when long lines removed.
* Fix width feedback loop bug that caused the width of an inner DIV to shrink.
## 2.11.0 (2011-07-04)
* Add a [Scheme mode](http://codemirror.net/mode/scheme/index.html).
* Add a `replace` method to search cursors, for cursor-preserving replacements.
* Make the [C-like mode](http://codemirror.net/mode/clike/index.html) mode more customizable.
* Update XML mode to spot mismatched tags.
* Add `getStateAfter` API and `compareState` mode API methods for finer-grained mode magic.
* Add a `getScrollerElement` API method to manipulate the scrolling DIV.
* Fix drag-and-drop for Firefox.
* Add a C# configuration for the [C-like mode](http://codemirror.net/mode/clike/index.html).
* Add [full-screen editing](http://codemirror.net/demo/fullscreen.html) and [mode-changing](http://codemirror.net/demo/changemode.html) demos.
## 2.10.0 (2011-06-07)
Add a [theme](http://codemirror.net/doc/manual.html#option_theme) system ([demo](http://codemirror.net/demo/theme.html)). Note that this is not backwards-compatible—you'll have to update your styles and modes!
## 2.2.0 (2011-06-07)
* Add a [Lua mode](http://codemirror.net/mode/lua/index.html).
* Fix reverse-searching for a regexp.
* Empty lines can no longer break highlighting.
* Rework scrolling model (the outer wrapper no longer does the scrolling).
* Solve horizontal jittering on long lines.
* Add [runmode.js](http://codemirror.net/demo/runmode.html).
* Immediately re-highlight text when typing.
* Fix problem with 'sticking' horizontal scrollbar.
## 2.1.0 (2011-05-26)
* Add a [Smalltalk mode](http://codemirror.net/mode/smalltalk/index.html).
* Add a [reStructuredText mode](http://codemirror.net/mode/rst/index.html).
* Add a [Python mode](http://codemirror.net/mode/python/index.html).
* Add a [PL/SQL mode](http://codemirror.net/mode/plsql/index.html).
* `coordsChar` now works
* Fix a problem where `onCursorActivity` interfered with `onChange`.
* Fix a number of scrolling and mouse-click-position glitches.
* Pass information about the changed lines to `onChange`.
* Support cmd-up/down on OS X.
* Add triple-click line selection.
* Don't handle shift when changing the selection through the API.
* Support `"nocursor"` mode for `readOnly` option.
* Add an `onHighlightComplete` option.
* Fix the context menu for Firefox.
## 2.0.0 (2011-03-28)
CodeMirror 2 is a complete rewrite that's faster, smaller, simpler to use, and less dependent on browser quirks. See [this](http://codemirror.net/doc/internals.html) and [this](http://groups.google.com/group/codemirror/browse_thread/thread/5a8e894024a9f580) for more information.

@ -1,8 +1,8 @@
# How to contribute # How to contribute
- [Getting help](#getting-help-) - [Getting help](#getting-help)
- [Submitting bug reports](#submitting-bug-reports-) - [Submitting bug reports](#submitting-bug-reports)
- [Contributing code](#contributing-code-) - [Contributing code](#contributing-code)
## Getting help ## Getting help

@ -1,4 +1,4 @@
Copyright (C) 2015 by Marijn Haverbeke <marijnh@gmail.com> and others Copyright (C) 2016 by Marijn Haverbeke <marijnh@gmail.com> and others
Permission is hereby granted, free of charge, to any person obtaining a copy Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal of this software and associated documentation files (the "Software"), to deal

@ -16,7 +16,7 @@ new functionality.
You can find more information (and the You can find more information (and the
[manual](http://codemirror.net/doc/manual.html)) on the [project [manual](http://codemirror.net/doc/manual.html)) on the [project
page](http://codemirror.net). For questions and discussion, use the page](http://codemirror.net). For questions and discussion, use the
[discussion forum](http://discuss.codemirror.net/). [discussion forum](https://discuss.codemirror.net/).
See See
[CONTRIBUTING.md](https://github.com/codemirror/CodeMirror/blob/master/CONTRIBUTING.md) [CONTRIBUTING.md](https://github.com/codemirror/CodeMirror/blob/master/CONTRIBUTING.md)

@ -21,22 +21,28 @@
} }
CodeMirror.commands.toggleComment = function(cm) { CodeMirror.commands.toggleComment = function(cm) {
var minLine = Infinity, ranges = cm.listSelections(), mode = null; cm.toggleComment();
};
CodeMirror.defineExtension("toggleComment", function(options) {
if (!options) options = noOptions;
var cm = this;
var minLine = Infinity, ranges = this.listSelections(), mode = null;
for (var i = ranges.length - 1; i >= 0; i--) { for (var i = ranges.length - 1; i >= 0; i--) {
var from = ranges[i].from(), to = ranges[i].to(); var from = ranges[i].from(), to = ranges[i].to();
if (from.line >= minLine) continue; if (from.line >= minLine) continue;
if (to.line >= minLine) to = Pos(minLine, 0); if (to.line >= minLine) to = Pos(minLine, 0);
minLine = from.line; minLine = from.line;
if (mode == null) { if (mode == null) {
if (cm.uncomment(from, to)) mode = "un"; if (cm.uncomment(from, to, options)) mode = "un";
else { cm.lineComment(from, to); mode = "line"; } else { cm.lineComment(from, to, options); mode = "line"; }
} else if (mode == "un") { } else if (mode == "un") {
cm.uncomment(from, to); cm.uncomment(from, to, options);
} else { } else {
cm.lineComment(from, to); cm.lineComment(from, to, options);
} }
} }
}; });
CodeMirror.defineExtension("lineComment", function(from, to, options) { CodeMirror.defineExtension("lineComment", function(from, to, options) {
if (!options) options = noOptions; if (!options) options = noOptions;
@ -57,7 +63,14 @@
self.operation(function() { self.operation(function() {
if (options.indent) { if (options.indent) {
var baseString = firstLine.slice(0, firstNonWS(firstLine)); var baseString = null;
for (var i = from.line; i < end; ++i) {
var line = self.getLine(i);
var whitespace = line.slice(0, firstNonWS(line));
if (baseString == null || baseString.length > whitespace.length) {
baseString = whitespace;
}
}
for (var i = from.line; i < end; ++i) { for (var i = from.line; i < end; ++i) {
var line = self.getLine(i), cut = baseString.length; var line = self.getLine(i), cut = baseString.length;
if (!blankLines && !nonWS.test(line)) continue; if (!blankLines && !nonWS.test(line)) continue;

@ -56,6 +56,8 @@
var inp = dialog.getElementsByTagName("input")[0], button; var inp = dialog.getElementsByTagName("input")[0], button;
if (inp) { if (inp) {
inp.focus();
if (options.value) { if (options.value) {
inp.value = options.value; inp.value = options.value;
if (options.selectValueOnOpen !== false) { if (options.selectValueOnOpen !== false) {
@ -79,8 +81,6 @@
}); });
if (options.closeOnBlur !== false) CodeMirror.on(inp, "blur", close); if (options.closeOnBlur !== false) CodeMirror.on(inp, "blur", close);
inp.focus();
} else if (button = dialog.getElementsByTagName("button")[0]) { } else if (button = dialog.getElementsByTagName("button")[0]) {
CodeMirror.on(button, "click", function() { CodeMirror.on(button, "click", function() {
close(); close();

@ -14,10 +14,12 @@
if (val && !prev) { if (val && !prev) {
cm.on("blur", onBlur); cm.on("blur", onBlur);
cm.on("change", onChange); cm.on("change", onChange);
cm.on("swapDoc", onChange);
onChange(cm); onChange(cm);
} else if (!val && prev) { } else if (!val && prev) {
cm.off("blur", onBlur); cm.off("blur", onBlur);
cm.off("change", onChange); cm.off("change", onChange);
cm.off("swapDoc", onChange);
clearPlaceholder(cm); clearPlaceholder(cm);
var wrapper = cm.getWrapperElement(); var wrapper = cm.getWrapperElement();
wrapper.className = wrapper.className.replace(" CodeMirror-empty", ""); wrapper.className = wrapper.className.replace(" CodeMirror-empty", "");

@ -63,7 +63,7 @@
} }
for (var i = ranges.length - 1; i >= 0; i--) { for (var i = ranges.length - 1; i >= 0; i--) {
var cur = ranges[i].head; var cur = ranges[i].head;
cm.replaceRange("", Pos(cur.line, cur.ch - 1), Pos(cur.line, cur.ch + 1)); cm.replaceRange("", Pos(cur.line, cur.ch - 1), Pos(cur.line, cur.ch + 1), "+delete");
} }
} }

@ -28,7 +28,9 @@ CodeMirror.registerGlobalHelper("fold", "comment", function(mode) {
continue; continue;
} }
if (pass == 1 && found < start.ch) return; if (pass == 1 && found < start.ch) return;
if (/comment/.test(cm.getTokenTypeAt(CodeMirror.Pos(line, found + 1)))) { if (/comment/.test(cm.getTokenTypeAt(CodeMirror.Pos(line, found + 1))) &&
(lineText.slice(found - endToken.length, found) == endToken ||
!/comment/.test(cm.getTokenTypeAt(CodeMirror.Pos(line, found))))) {
startCh = found + startToken.length; startCh = found + startToken.length;
break; break;
} }

@ -20,7 +20,7 @@
cm.off("viewportChange", onViewportChange); cm.off("viewportChange", onViewportChange);
cm.off("fold", onFold); cm.off("fold", onFold);
cm.off("unfold", onFold); cm.off("unfold", onFold);
cm.off("swapDoc", updateInViewport); cm.off("swapDoc", onChange);
} }
if (val) { if (val) {
cm.state.foldGutter = new State(parseOptions(val)); cm.state.foldGutter = new State(parseOptions(val));
@ -30,7 +30,7 @@
cm.on("viewportChange", onViewportChange); cm.on("viewportChange", onViewportChange);
cm.on("fold", onFold); cm.on("fold", onFold);
cm.on("unfold", onFold); cm.on("unfold", onFold);
cm.on("swapDoc", updateInViewport); cm.on("swapDoc", onChange);
} }
}); });

@ -25,8 +25,18 @@
}; };
CodeMirror.defineExtension("showHint", function(options) { CodeMirror.defineExtension("showHint", function(options) {
// We want a single cursor position. options = parseOptions(this, this.getCursor("start"), options);
if (this.listSelections().length > 1 || this.somethingSelected()) return; var selections = this.listSelections()
if (selections.length > 1) return;
// By default, don't allow completion when something is selected.
// A hint function can have a `supportsSelection` property to
// indicate that it can handle selections.
if (this.somethingSelected()) {
if (!options.hint.supportsSelection) return;
// Don't try with cross-line selections
for (var i = 0; i < selections.length; i++)
if (selections[i].head.line != selections[i].anchor.line) return;
}
if (this.state.completionActive) this.state.completionActive.close(); if (this.state.completionActive) this.state.completionActive.close();
var completion = this.state.completionActive = new Completion(this, options); var completion = this.state.completionActive = new Completion(this, options);
@ -38,12 +48,12 @@
function Completion(cm, options) { function Completion(cm, options) {
this.cm = cm; this.cm = cm;
this.options = this.buildOptions(options); this.options = options;
this.widget = null; this.widget = null;
this.debounce = 0; this.debounce = 0;
this.tick = 0; this.tick = 0;
this.startPos = this.cm.getCursor(); this.startPos = this.cm.getCursor("start");
this.startLen = this.cm.getLine(this.startPos.line).length; this.startLen = this.cm.getLine(this.startPos.line).length - this.cm.getSelection().length;
var self = this; var self = this;
cm.on("cursorActivity", this.activityFunc = function() { self.cursorActivity(); }); cm.on("cursorActivity", this.activityFunc = function() { self.cursorActivity(); });
@ -111,11 +121,13 @@
finishUpdate: function(data, first) { finishUpdate: function(data, first) {
if (this.data) CodeMirror.signal(this.data, "update"); if (this.data) CodeMirror.signal(this.data, "update");
if (data && this.data && CodeMirror.cmpPos(data.from, this.data.from)) data = null;
this.data = data;
var picked = (this.widget && this.widget.picked) || (first && this.options.completeSingle); var picked = (this.widget && this.widget.picked) || (first && this.options.completeSingle);
if (this.widget) this.widget.close(); if (this.widget) this.widget.close();
if (data && this.data && isNewCompletion(this.data, data)) return;
this.data = data;
if (data && data.list.length) { if (data && data.list.length) {
if (picked && data.list.length == 1) { if (picked && data.list.length == 1) {
this.pick(data, 0); this.pick(data, 0);
@ -124,19 +136,25 @@
CodeMirror.signal(data, "shown"); CodeMirror.signal(data, "shown");
} }
} }
}, }
};
function isNewCompletion(old, nw) {
var moved = CodeMirror.cmpPos(nw.from, old.from)
return moved > 0 && old.to.ch - old.from.ch != nw.to.ch - nw.from.ch
}
buildOptions: function(options) { function parseOptions(cm, pos, options) {
var editor = this.cm.options.hintOptions; var editor = cm.options.hintOptions;
var out = {}; var out = {};
for (var prop in defaultOptions) out[prop] = defaultOptions[prop]; for (var prop in defaultOptions) out[prop] = defaultOptions[prop];
if (editor) for (var prop in editor) if (editor) for (var prop in editor)
if (editor[prop] !== undefined) out[prop] = editor[prop]; if (editor[prop] !== undefined) out[prop] = editor[prop];
if (options) for (var prop in options) if (options) for (var prop in options)
if (options[prop] !== undefined) out[prop] = options[prop]; if (options[prop] !== undefined) out[prop] = options[prop];
if (out.hint.resolve) out.hint = out.hint.resolve(cm, pos)
return out; return out;
} }
};
function getText(completion) { function getText(completion) {
if (typeof completion == "string") return completion; if (typeof completion == "string") return completion;
@ -336,18 +354,61 @@
} }
}; };
CodeMirror.registerHelper("hint", "auto", function(cm, options) { function applicableHelpers(cm, helpers) {
var helpers = cm.getHelpers(cm.getCursor(), "hint"), words; if (!cm.somethingSelected()) return helpers
var result = []
for (var i = 0; i < helpers.length; i++)
if (helpers[i].supportsSelection) result.push(helpers[i])
return result
}
function resolveAutoHints(cm, pos) {
var helpers = cm.getHelpers(pos, "hint"), words
if (helpers.length) { if (helpers.length) {
for (var i = 0; i < helpers.length; i++) { var async = false, resolved
var cur = helpers[i](cm, options); for (var i = 0; i < helpers.length; i++) if (helpers[i].async) async = true
if (cur && cur.list.length) return cur; if (async) {
resolved = function(cm, callback, options) {
var app = applicableHelpers(cm, helpers)
function run(i, result) {
if (i == app.length) return callback(null)
var helper = app[i]
if (helper.async) {
helper(cm, function(result) {
if (result) callback(result)
else run(i + 1)
}, options)
} else {
var result = helper(cm, options)
if (result) callback(result)
else run(i + 1)
}
}
run(0)
}
resolved.async = true
} else {
resolved = function(cm, options) {
var app = applicableHelpers(cm, helpers)
for (var i = 0; i < app.length; i++) {
var cur = app[i](cm, options)
if (cur && cur.list.length) return cur
} }
}
}
resolved.supportsSelection = true
return resolved
} else if (words = cm.getHelper(cm.getCursor(), "hintWords")) { } else if (words = cm.getHelper(cm.getCursor(), "hintWords")) {
if (words) return CodeMirror.hint.fromList(cm, {words: words}); return function(cm) { return CodeMirror.hint.fromList(cm, {words: words}) }
} else if (CodeMirror.hint.anyword) { } else if (CodeMirror.hint.anyword) {
return CodeMirror.hint.anyword(cm, options); return function(cm, options) { return CodeMirror.hint.anyword(cm, options) }
} else {
return function() {}
} }
}
CodeMirror.registerHelper("hint", "auto", {
resolve: resolveAutoHints
}); });
CodeMirror.registerHelper("hint", "fromList", function(cm, options) { CodeMirror.registerHelper("hint", "fromList", function(cm, options) {
@ -376,7 +437,7 @@
alignWithWord: true, alignWithWord: true,
closeCharacters: /[\s()\[\]{};:>,]/, closeCharacters: /[\s()\[\]{};:>,]/,
closeOnUnfocus: true, closeOnUnfocus: true,
completeOnSingleClick: false, completeOnSingleClick: true,
container: null, container: null,
customKeys: null, customKeys: null,
extraKeys: null extraKeys: null

@ -20,6 +20,8 @@
}; };
var Pos = CodeMirror.Pos; var Pos = CodeMirror.Pos;
function isArray(val) { return Object.prototype.toString.call(val) == "[object Array]" }
function getKeywords(editor) { function getKeywords(editor) {
var mode = editor.doc.modeOption; var mode = editor.doc.modeOption;
if (mode === "sql") mode = "text/x-sql"; if (mode === "sql") mode = "text/x-sql";
@ -30,10 +32,28 @@
return typeof item == "string" ? item : item.text; return typeof item == "string" ? item : item.text;
} }
function getItem(list, item) { function wrapTable(name, value) {
if (!list.slice) return list[item]; if (isArray(value)) value = {columns: value}
for (var i = list.length - 1; i >= 0; i--) if (getText(list[i]) == item) if (!value.text) value.text = name
return list[i]; return value
}
function parseTables(input) {
var result = {}
if (isArray(input)) {
for (var i = input.length - 1; i >= 0; i--) {
var item = input[i]
result[getText(item).toUpperCase()] = wrapTable(getText(item), item)
}
} else if (input) {
for (var name in input)
result[name.toUpperCase()] = wrapTable(name, input[name])
}
return result
}
function getTable(name) {
return tables[name.toUpperCase()]
} }
function shallowClone(object) { function shallowClone(object) {
@ -50,11 +70,18 @@
} }
function addMatches(result, search, wordlist, formatter) { function addMatches(result, search, wordlist, formatter) {
for (var word in wordlist) { if (isArray(wordlist)) {
if (!wordlist.hasOwnProperty(word)) continue; for (var i = 0; i < wordlist.length; i++)
if (wordlist.slice) word = wordlist[word]; if (match(search, wordlist[i])) result.push(formatter(wordlist[i]))
} else {
if (match(search, word)) result.push(formatter(word)); for (var word in wordlist) if (wordlist.hasOwnProperty(word)) {
var val = wordlist[word]
if (!val || val === true)
val = word
else
val = val.displayText ? {text: val.text, displayText: val.displayText} : val.text
if (match(search, val)) result.push(formatter(val))
}
} }
} }
@ -115,13 +142,13 @@
var alias = false; var alias = false;
var aliasTable = table; var aliasTable = table;
// Check if table is available. If not, find table by Alias // Check if table is available. If not, find table by Alias
if (!getItem(tables, table)) { if (!getTable(table)) {
var oldTable = table; var oldTable = table;
table = findTableByAlias(table, editor); table = findTableByAlias(table, editor);
if (table !== oldTable) alias = true; if (table !== oldTable) alias = true;
} }
var columns = getItem(tables, table); var columns = getTable(table);
if (columns && columns.columns) if (columns && columns.columns)
columns = columns.columns; columns = columns.columns;
@ -184,7 +211,7 @@
//find valid range //find valid range
var prevItem = 0; var prevItem = 0;
var current = convertCurToNumber(editor.getCursor()); var current = convertCurToNumber(editor.getCursor());
for (var i=0; i< separator.length; i++) { for (var i = 0; i < separator.length; i++) {
var _v = convertCurToNumber(separator[i]); var _v = convertCurToNumber(separator[i]);
if (current > prevItem && current <= _v) { if (current > prevItem && current <= _v) {
validRange = { start: convertNumberToCur(prevItem), end: convertNumberToCur(_v) }; validRange = { start: convertNumberToCur(prevItem), end: convertNumberToCur(_v) };
@ -199,7 +226,7 @@
var lineText = query[i]; var lineText = query[i];
eachWord(lineText, function(word) { eachWord(lineText, function(word) {
var wordUpperCase = word.toUpperCase(); var wordUpperCase = word.toUpperCase();
if (wordUpperCase === aliasUpperCase && getItem(tables, previousWord)) if (wordUpperCase === aliasUpperCase && getTable(previousWord))
table = previousWord; table = previousWord;
if (wordUpperCase !== CONS.ALIAS_KEYWORD) if (wordUpperCase !== CONS.ALIAS_KEYWORD)
previousWord = word; previousWord = word;
@ -210,10 +237,10 @@
} }
CodeMirror.registerHelper("hint", "sql", function(editor, options) { CodeMirror.registerHelper("hint", "sql", function(editor, options) {
tables = (options && options.tables) || {}; tables = parseTables(options && options.tables)
var defaultTableName = options && options.defaultTable; var defaultTableName = options && options.defaultTable;
var disableKeywords = options && options.disableKeywords; var disableKeywords = options && options.disableKeywords;
defaultTable = defaultTableName && getItem(tables, defaultTableName); defaultTable = defaultTableName && getTable(defaultTableName);
keywords = keywords || getKeywords(editor); keywords = keywords || getKeywords(editor);
if (defaultTableName && !defaultTable) if (defaultTableName && !defaultTable)

@ -186,9 +186,14 @@
state.timeout = setTimeout(function(){startLinting(cm);}, state.options.delay || 500); state.timeout = setTimeout(function(){startLinting(cm);}, state.options.delay || 500);
} }
function popupSpanTooltip(ann, e) { function popupTooltips(annotations, e) {
var target = e.target || e.srcElement; var target = e.target || e.srcElement;
showTooltipFor(e, annotationTooltip(ann), target); var tooltip = document.createDocumentFragment();
for (var i = 0; i < annotations.length; i++) {
var ann = annotations[i];
tooltip.appendChild(annotationTooltip(ann));
}
showTooltipFor(e, tooltip, target);
} }
function onMouseOver(cm, e) { function onMouseOver(cm, e) {
@ -196,10 +201,12 @@
if (!/\bCodeMirror-lint-mark-/.test(target.className)) return; if (!/\bCodeMirror-lint-mark-/.test(target.className)) return;
var box = target.getBoundingClientRect(), x = (box.left + box.right) / 2, y = (box.top + box.bottom) / 2; var box = target.getBoundingClientRect(), x = (box.left + box.right) / 2, y = (box.top + box.bottom) / 2;
var spans = cm.findMarksAt(cm.coordsChar({left: x, top: y}, "client")); var spans = cm.findMarksAt(cm.coordsChar({left: x, top: y}, "client"));
var annotations = [];
for (var i = 0; i < spans.length; ++i) { for (var i = 0; i < spans.length; ++i) {
var ann = spans[i].__annotation; annotations.push(spans[i].__annotation);
if (ann) return popupSpanTooltip(ann, e);
} }
if (annotations.length) popupTooltips(annotations, e);
} }
CodeMirror.defineOption("lint", false, function(cm, val, old) { CodeMirror.defineOption("lint", false, function(cm, val, old) {

@ -60,6 +60,7 @@
position: absolute; position: absolute;
cursor: pointer; cursor: pointer;
color: #44c; color: #44c;
z-index: 3;
} }
.CodeMirror-merge-copy-reverse { .CodeMirror-merge-copy-reverse {

@ -5,7 +5,7 @@
(function(mod) { (function(mod) {
if (typeof exports == "object" && typeof module == "object") // CommonJS if (typeof exports == "object" && typeof module == "object") // CommonJS
mod(require("../../lib/codemirror"), require("diff_match_patch")); mod(require("../../lib/codemirror")); // Note non-packaged dependency diff_match_patch
else if (typeof define == "function" && define.amd) // AMD else if (typeof define == "function" && define.amd) // AMD
define(["../../lib/codemirror", "diff_match_patch"], mod); define(["../../lib/codemirror", "diff_match_patch"], mod);
else // Plain browser env else // Plain browser env
@ -427,8 +427,9 @@
function copyChunk(dv, to, from, chunk) { function copyChunk(dv, to, from, chunk) {
if (dv.diffOutOfDate) return; if (dv.diffOutOfDate) return;
to.replaceRange(from.getRange(Pos(chunk.origFrom, 0), Pos(chunk.origTo, 0)), var editStart = chunk.editTo > to.lastLine() ? Pos(chunk.editFrom - 1) : Pos(chunk.editFrom, 0)
Pos(chunk.editFrom, 0), Pos(chunk.editTo, 0)); var origStart = chunk.origTo > from.lastLine() ? Pos(chunk.origFrom - 1) : Pos(chunk.origFrom, 0)
to.replaceRange(from.getRange(origStart, Pos(chunk.origTo, 0)), editStart, Pos(chunk.editTo, 0))
} }
// Merge view, containing 0, 1, or 2 diff views. // Merge view, containing 0, 1, or 2 diff views.
@ -471,13 +472,10 @@
if (left) left.init(leftPane, origLeft, options); if (left) left.init(leftPane, origLeft, options);
if (right) right.init(rightPane, origRight, options); if (right) right.init(rightPane, origRight, options);
if (options.collapseIdentical) { if (options.collapseIdentical)
updating = true;
this.editor().operation(function() { this.editor().operation(function() {
collapseIdenticalStretches(self, options.collapseIdentical); collapseIdenticalStretches(self, options.collapseIdentical);
}); });
updating = false;
}
if (options.connect == "align") { if (options.connect == "align") {
this.aligners = []; this.aligners = [];
alignChunks(this.left || this.right, true); alignChunks(this.left || this.right, true);
@ -640,7 +638,7 @@
mark.clear(); mark.clear();
cm.removeLineClass(from, "wrap", "CodeMirror-merge-collapsed-line"); cm.removeLineClass(from, "wrap", "CodeMirror-merge-collapsed-line");
} }
widget.addEventListener("click", clear); CodeMirror.on(widget, "click", clear);
return {mark: mark, clear: clear}; return {mark: mark, clear: clear};
} }

@ -51,7 +51,7 @@ CodeMirror.multiplexingMode = function(outer /*, others */) {
if (!other.parseDelimiters) stream.match(other.open); if (!other.parseDelimiters) stream.match(other.open);
state.innerActive = other; state.innerActive = other;
state.inner = CodeMirror.startState(other.mode, outer.indent ? outer.indent(state.outer, "") : 0); state.inner = CodeMirror.startState(other.mode, outer.indent ? outer.indent(state.outer, "") : 0);
return other.delimStyle; return other.delimStyle && (other.delimStyle + " " + other.delimStyle + "-open");
} else if (found != -1 && found < cutOff) { } else if (found != -1 && found < cutOff) {
cutOff = found; cutOff = found;
} }
@ -70,7 +70,7 @@ CodeMirror.multiplexingMode = function(outer /*, others */) {
if (found == stream.pos && !curInner.parseDelimiters) { if (found == stream.pos && !curInner.parseDelimiters) {
stream.match(curInner.close); stream.match(curInner.close);
state.innerActive = state.inner = null; state.innerActive = state.inner = null;
return curInner.delimStyle; return curInner.delimStyle && (curInner.delimStyle + " " + curInner.delimStyle + "-close");
} }
if (found > -1) stream.string = oldContent.slice(0, found); if (found > -1) stream.string = oldContent.slice(0, found);
var innerToken = curInner.mode.token(stream, state.inner); var innerToken = curInner.mode.token(stream, state.inner);
@ -80,7 +80,7 @@ CodeMirror.multiplexingMode = function(outer /*, others */) {
state.innerActive = state.inner = null; state.innerActive = state.inner = null;
if (curInner.innerStyle) { if (curInner.innerStyle) {
if (innerToken) innerToken = innerToken + ' ' + curInner.innerStyle; if (innerToken) innerToken = innerToken + " " + curInner.innerStyle;
else innerToken = curInner.innerStyle; else innerToken = curInner.innerStyle;
} }

@ -29,5 +29,5 @@
MT( MT(
"stexInsideMarkdown", "stexInsideMarkdown",
"[strong **Equation:**] [delim $][inner&tag \\pi][delim $]"); "[strong **Equation:**] [delim&delim-open $][inner&tag \\pi][delim&delim-close $]");
})(); })();

@ -60,7 +60,7 @@
function ensureState(states, name) { function ensureState(states, name) {
if (!states.hasOwnProperty(name)) if (!states.hasOwnProperty(name))
throw new Error("Undefined state " + name + "in simple mode"); throw new Error("Undefined state " + name + " in simple mode");
} }
function toRegex(val, caret) { function toRegex(val, caret) {

@ -16,7 +16,7 @@ CodeMirror.runMode = function(string, modespec, callback, options) {
var ie = /MSIE \d/.test(navigator.userAgent); var ie = /MSIE \d/.test(navigator.userAgent);
var ie_lt9 = ie && (document.documentMode == null || document.documentMode < 9); var ie_lt9 = ie && (document.documentMode == null || document.documentMode < 9);
if (callback.nodeType == 1) { if (callback.appendChild) {
var tabSize = (options && options.tabSize) || CodeMirror.defaults.tabSize; var tabSize = (options && options.tabSize) || CodeMirror.defaults.tabSize;
var node = callback, col = 0; var node = callback, col = 0;
node.innerHTML = ""; node.innerHTML = "";

@ -176,3 +176,4 @@ exports.runMode = function(string, modespec, callback, options) {
}; };
require.cache[require.resolve("../../lib/codemirror")] = require.cache[require.resolve("./runmode.node")]; require.cache[require.resolve("../../lib/codemirror")] = require.cache[require.resolve("./runmode.node")];
require.cache[require.resolve("../../addon/runmode/runmode")] = require.cache[require.resolve("./runmode.node")];

@ -51,7 +51,7 @@
Annotation.prototype.computeScale = function() { Annotation.prototype.computeScale = function() {
var cm = this.cm; var cm = this.cm;
var hScale = (cm.getWrapperElement().clientHeight - cm.display.barHeight - this.buttonHeight * 2) / var hScale = (cm.getWrapperElement().clientHeight - cm.display.barHeight - this.buttonHeight * 2) /
cm.heightAtLine(cm.lastLine() + 1, "local"); cm.getScrollerElement().scrollHeight
if (hScale != this.hScale) { if (hScale != this.hScale) {
this.hScale = hScale; this.hScale = hScale;
return true; return true;
@ -100,6 +100,9 @@
elt.style.cssText = "position: absolute; right: 0px; width: " + Math.max(cm.display.barWidth - 1, 2) + "px; top: " elt.style.cssText = "position: absolute; right: 0px; width: " + Math.max(cm.display.barWidth - 1, 2) + "px; top: "
+ (top + this.buttonHeight) + "px; height: " + height + "px"; + (top + this.buttonHeight) + "px; height: " + height + "px";
elt.className = this.options.className; elt.className = this.options.className;
if (ann.id) {
elt.setAttribute("annotation-id", ann.id);
}
} }
this.div.textContent = ""; this.div.textContent = "";
this.div.appendChild(frag); this.div.appendChild(frag);

@ -59,16 +59,20 @@
CodeMirror.on(this.node, "DOMMouseScroll", onWheel); CodeMirror.on(this.node, "DOMMouseScroll", onWheel);
} }
Bar.prototype.moveTo = function(pos, update) { Bar.prototype.setPos = function(pos) {
if (pos < 0) pos = 0; if (pos < 0) pos = 0;
if (pos > this.total - this.screen) pos = this.total - this.screen; if (pos > this.total - this.screen) pos = this.total - this.screen;
if (pos == this.pos) return; if (pos == this.pos) return false;
this.pos = pos; this.pos = pos;
this.inner.style[this.orientation == "horizontal" ? "left" : "top"] = this.inner.style[this.orientation == "horizontal" ? "left" : "top"] =
(pos * (this.size / this.total)) + "px"; (pos * (this.size / this.total)) + "px";
if (update !== false) this.scroll(pos, this.orientation); return true
}; };
Bar.prototype.moveTo = function(pos) {
if (this.setPos(pos)) this.scroll(pos, this.orientation);
}
var minButtonSize = 10; var minButtonSize = 10;
Bar.prototype.update = function(scrollSize, clientSize, barSize) { Bar.prototype.update = function(scrollSize, clientSize, barSize) {
@ -83,8 +87,7 @@
} }
this.inner.style[this.orientation == "horizontal" ? "width" : "height"] = this.inner.style[this.orientation == "horizontal" ? "width" : "height"] =
buttonSize + "px"; buttonSize + "px";
this.inner.style[this.orientation == "horizontal" ? "left" : "top"] = this.setPos(this.pos);
this.pos * (this.size / this.total) + "px";
}; };
function SimpleScrollbars(cls, place, scroll) { function SimpleScrollbars(cls, place, scroll) {
@ -111,7 +114,6 @@
if (needsV) { if (needsV) {
this.vert.update(measure.scrollHeight, measure.clientHeight, this.vert.update(measure.scrollHeight, measure.clientHeight,
measure.viewHeight - (needsH ? width : 0)); measure.viewHeight - (needsH ? width : 0));
this.vert.node.style.display = "block";
this.vert.node.style.bottom = needsH ? width + "px" : "0"; this.vert.node.style.bottom = needsH ? width + "px" : "0";
} }
if (needsH) { if (needsH) {
@ -125,11 +127,11 @@
}; };
SimpleScrollbars.prototype.setScrollTop = function(pos) { SimpleScrollbars.prototype.setScrollTop = function(pos) {
this.vert.moveTo(pos, false); this.vert.setPos(pos);
}; };
SimpleScrollbars.prototype.setScrollLeft = function(pos) { SimpleScrollbars.prototype.setScrollLeft = function(pos) {
this.horiz.moveTo(pos, false); this.horiz.setPos(pos);
}; };
SimpleScrollbars.prototype.clear = function() { SimpleScrollbars.prototype.clear = function() {

@ -0,0 +1,49 @@
// CodeMirror, copyright (c) by Marijn Haverbeke and others
// Distributed under an MIT license: http://codemirror.net/LICENSE
// Defines jumpToLine command. Uses dialog.js if present.
(function(mod) {
if (typeof exports == "object" && typeof module == "object") // CommonJS
mod(require("../../lib/codemirror"), require("../dialog/dialog"));
else if (typeof define == "function" && define.amd) // AMD
define(["../../lib/codemirror", "../dialog/dialog"], mod);
else // Plain browser env
mod(CodeMirror);
})(function(CodeMirror) {
"use strict";
function dialog(cm, text, shortText, deflt, f) {
if (cm.openDialog) cm.openDialog(text, f, {value: deflt, selectValueOnOpen: true});
else f(prompt(shortText, deflt));
}
var jumpDialog =
'Jump to line: <input type="text" style="width: 10em" class="CodeMirror-search-field"/> <span style="color: #888" class="CodeMirror-search-hint">(Use line:column or scroll% syntax)</span>';
function interpretLine(cm, string) {
var num = Number(string)
if (/^[-+]/.test(string)) return cm.getCursor().line + num
else return num - 1
}
CodeMirror.commands.jumpToLine = function(cm) {
var cur = cm.getCursor();
dialog(cm, jumpDialog, "Jump to line:", (cur.line + 1) + ":" + cur.ch, function(posStr) {
if (!posStr) return;
var match;
if (match = /^\s*([\+\-]?\d+)\s*\:\s*(\d+)\s*$/.exec(posStr)) {
cm.setCursor(interpretLine(cm, match[1]), Number(match[2]))
} else if (match = /^\s*([\+\-]?\d+(\.\d+)?)\%\s*/.exec(posStr)) {
var line = Math.round(cm.lineCount() * Number(match[1]) / 100);
if (/^[-+]/.test(match[1])) line = cur.line + line + 1;
cm.setCursor(line - 1, cur.ch);
} else if (match = /^\s*\:?\s*([\+\-]?\d+)\s*/.exec(posStr)) {
cm.setCursor(interpretLine(cm, match[1]), cur.ch);
}
});
};
CodeMirror.keyMap["default"]["Alt-G"] = "jumpToLine";
});

@ -16,13 +16,14 @@
// highlighted only if the selected text is a word. showToken, when enabled, // highlighted only if the selected text is a word. showToken, when enabled,
// will cause the current token to be highlighted when nothing is selected. // will cause the current token to be highlighted when nothing is selected.
// delay is used to specify how much time to wait, in milliseconds, before // delay is used to specify how much time to wait, in milliseconds, before
// highlighting the matches. // highlighting the matches. If annotateScrollbar is enabled, the occurances
// will be highlighted on the scrollbar via the matchesonscrollbar addon.
(function(mod) { (function(mod) {
if (typeof exports == "object" && typeof module == "object") // CommonJS if (typeof exports == "object" && typeof module == "object") // CommonJS
mod(require("../../lib/codemirror")); mod(require("../../lib/codemirror"), require("./matchesonscrollbar"));
else if (typeof define == "function" && define.amd) // AMD else if (typeof define == "function" && define.amd) // AMD
define(["../../lib/codemirror"], mod); define(["../../lib/codemirror", "./matchesonscrollbar"], mod);
else // Plain browser env else // Plain browser env
mod(CodeMirror); mod(CodeMirror);
})(function(CodeMirror) { })(function(CodeMirror) {
@ -40,18 +41,19 @@
this.showToken = options.showToken; this.showToken = options.showToken;
this.delay = options.delay; this.delay = options.delay;
this.wordsOnly = options.wordsOnly; this.wordsOnly = options.wordsOnly;
this.annotateScrollbar = options.annotateScrollbar;
} }
if (this.style == null) this.style = DEFAULT_TOKEN_STYLE; if (this.style == null) this.style = DEFAULT_TOKEN_STYLE;
if (this.minChars == null) this.minChars = DEFAULT_MIN_CHARS; if (this.minChars == null) this.minChars = DEFAULT_MIN_CHARS;
if (this.delay == null) this.delay = DEFAULT_DELAY; if (this.delay == null) this.delay = DEFAULT_DELAY;
if (this.wordsOnly == null) this.wordsOnly = DEFAULT_WORDS_ONLY; if (this.wordsOnly == null) this.wordsOnly = DEFAULT_WORDS_ONLY;
this.overlay = this.timeout = null; this.overlay = this.timeout = null;
this.matchesonscroll = null;
} }
CodeMirror.defineOption("highlightSelectionMatches", false, function(cm, val, old) { CodeMirror.defineOption("highlightSelectionMatches", false, function(cm, val, old) {
if (old && old != CodeMirror.Init) { if (old && old != CodeMirror.Init) {
var over = cm.state.matchHighlighter.overlay; removeOverlay(cm);
if (over) cm.removeOverlay(over);
clearTimeout(cm.state.matchHighlighter.timeout); clearTimeout(cm.state.matchHighlighter.timeout);
cm.state.matchHighlighter = null; cm.state.matchHighlighter = null;
cm.off("cursorActivity", cursorActivity); cm.off("cursorActivity", cursorActivity);
@ -69,20 +71,39 @@
state.timeout = setTimeout(function() {highlightMatches(cm);}, state.delay); state.timeout = setTimeout(function() {highlightMatches(cm);}, state.delay);
} }
function highlightMatches(cm) { function addOverlay(cm, query, hasBoundary, style) {
cm.operation(function() { var state = cm.state.matchHighlighter;
cm.addOverlay(state.overlay = makeOverlay(query, hasBoundary, style));
if (state.annotateScrollbar) {
var searchFor = hasBoundary ? new RegExp("\\b" + query + "\\b") : query;
state.matchesonscroll = cm.showMatchesOnScrollbar(searchFor, true,
{className: "CodeMirror-selection-highlight-scrollbar"});
}
}
function removeOverlay(cm) {
var state = cm.state.matchHighlighter; var state = cm.state.matchHighlighter;
if (state.overlay) { if (state.overlay) {
cm.removeOverlay(state.overlay); cm.removeOverlay(state.overlay);
state.overlay = null; state.overlay = null;
if (state.annotateScrollbar) {
state.matchesonscroll.clear();
state.matchesonscroll = null;
} }
}
}
function highlightMatches(cm) {
cm.operation(function() {
var state = cm.state.matchHighlighter;
removeOverlay(cm);
if (!cm.somethingSelected() && state.showToken) { if (!cm.somethingSelected() && state.showToken) {
var re = state.showToken === true ? /[\w$]/ : state.showToken; var re = state.showToken === true ? /[\w$]/ : state.showToken;
var cur = cm.getCursor(), line = cm.getLine(cur.line), start = cur.ch, end = start; var cur = cm.getCursor(), line = cm.getLine(cur.line), start = cur.ch, end = start;
while (start && re.test(line.charAt(start - 1))) --start; while (start && re.test(line.charAt(start - 1))) --start;
while (end < line.length && re.test(line.charAt(end))) ++end; while (end < line.length && re.test(line.charAt(end))) ++end;
if (start < end) if (start < end)
cm.addOverlay(state.overlay = makeOverlay(line.slice(start, end), re, state.style)); addOverlay(cm, line.slice(start, end), re, state.style);
return; return;
} }
var from = cm.getCursor("from"), to = cm.getCursor("to"); var from = cm.getCursor("from"), to = cm.getCursor("to");
@ -90,7 +111,7 @@
if (state.wordsOnly && !isWord(cm, from, to)) return; if (state.wordsOnly && !isWord(cm, from, to)) return;
var selection = cm.getRange(from, to).replace(/^\s+|\s+$/g, ""); var selection = cm.getRange(from, to).replace(/^\s+|\s+$/g, "");
if (selection.length >= state.minChars) if (selection.length >= state.minChars)
cm.addOverlay(state.overlay = makeOverlay(selection, false, state.style)); addOverlay(cm, selection, false, state.style);
}); });
} }

@ -29,7 +29,7 @@
query.lastIndex = stream.pos; query.lastIndex = stream.pos;
var match = query.exec(stream.string); var match = query.exec(stream.string);
if (match && match.index == stream.pos) { if (match && match.index == stream.pos) {
stream.pos += match[0].length; stream.pos += match[0].length || 1;
return "searching"; return "searching";
} else if (match) { } else if (match) {
stream.pos = match.index; stream.pos = match.index;

@ -18,6 +18,7 @@
"use strict"; "use strict";
var WRAP_CLASS = "CodeMirror-activeline"; var WRAP_CLASS = "CodeMirror-activeline";
var BACK_CLASS = "CodeMirror-activeline-background"; var BACK_CLASS = "CodeMirror-activeline-background";
var GUTT_CLASS = "CodeMirror-activeline-gutter";
CodeMirror.defineOption("styleActiveLine", false, function(cm, val, old) { CodeMirror.defineOption("styleActiveLine", false, function(cm, val, old) {
var prev = old && old != CodeMirror.Init; var prev = old && old != CodeMirror.Init;
@ -36,6 +37,7 @@
for (var i = 0; i < cm.state.activeLines.length; i++) { for (var i = 0; i < cm.state.activeLines.length; i++) {
cm.removeLineClass(cm.state.activeLines[i], "wrap", WRAP_CLASS); cm.removeLineClass(cm.state.activeLines[i], "wrap", WRAP_CLASS);
cm.removeLineClass(cm.state.activeLines[i], "background", BACK_CLASS); cm.removeLineClass(cm.state.activeLines[i], "background", BACK_CLASS);
cm.removeLineClass(cm.state.activeLines[i], "gutter", GUTT_CLASS);
} }
} }
@ -60,6 +62,7 @@
for (var i = 0; i < active.length; i++) { for (var i = 0; i < active.length; i++) {
cm.addLineClass(active[i], "wrap", WRAP_CLASS); cm.addLineClass(active[i], "wrap", WRAP_CLASS);
cm.addLineClass(active[i], "background", BACK_CLASS); cm.addLineClass(active[i], "background", BACK_CLASS);
cm.addLineClass(active[i], "gutter", GUTT_CLASS);
} }
cm.state.activeLines = active; cm.state.activeLines = active;
}); });

@ -135,6 +135,7 @@
}, },
destroy: function () { destroy: function () {
closeArgHints(this)
if (this.worker) { if (this.worker) {
this.worker.terminate(); this.worker.terminate();
this.worker = null; this.worker = null;

@ -32,11 +32,13 @@
function findBreakPoint(text, column, wrapOn, killTrailingSpace) { function findBreakPoint(text, column, wrapOn, killTrailingSpace) {
for (var at = column; at > 0; --at) for (var at = column; at > 0; --at)
if (wrapOn.test(text.slice(at - 1, at + 1))) break; if (wrapOn.test(text.slice(at - 1, at + 1))) break;
if (at == 0) at = column; for (var first = true;; first = false) {
var endOfText = at; var endOfText = at;
if (killTrailingSpace) if (killTrailingSpace)
while (text.charAt(endOfText - 1) == " ") --endOfText; while (text.charAt(endOfText - 1) == " ") --endOfText;
return {from: endOfText, to: at}; if (endOfText == 0 && first) at = column;
else return {from: endOfText, to: at};
}
} }
function wrapRange(cm, from, to, options) { function wrapRange(cm, from, to, options) {
@ -86,6 +88,7 @@
if (changes.length) cm.operation(function() { if (changes.length) cm.operation(function() {
for (var i = 0; i < changes.length; ++i) { for (var i = 0; i < changes.length; ++i) {
var change = changes[i]; var change = changes[i];
if (change.text || CodeMirror.cmpPos(change.from, change.to))
cm.replaceRange(change.text, change.from, change.to); cm.replaceRange(change.text, change.from, change.to);
} }
}); });

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

Loading…
Cancel
Save