mirror of https://github.com/xwiki-labs/cryptpad
resolve merge conflict
This commit is contained in:
commit
50695f41ed
|
@ -5,3 +5,5 @@ customization
|
|||
.*.swp
|
||||
*.db
|
||||
/customize/
|
||||
messages.log
|
||||
.DS_Store
|
||||
|
|
27
.travis.yml
27
.travis.yml
|
@ -1,8 +1,25 @@
|
|||
language: node_js
|
||||
env:
|
||||
matrix:
|
||||
- "BROWSER='firefox:19:Windows 2012'"
|
||||
- "BROWSER='chrome::Windows 2008'"
|
||||
branches:
|
||||
only:
|
||||
- master
|
||||
- diffdom
|
||||
node_js:
|
||||
- "0.12"
|
||||
|
||||
- master
|
||||
- diffdom
|
||||
- beta
|
||||
- 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="
|
||||
|
|
196
ChainPadSrv.js
196
ChainPadSrv.js
|
@ -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')
|
||||
}
|
||||
}
|
||||
});
|
||||
})
|
||||
}
|
15
bower.json
15
bower.json
|
@ -18,8 +18,6 @@
|
|||
"tests"
|
||||
],
|
||||
"dependencies": {
|
||||
"markdown": "~0.5.0",
|
||||
"jquery.sheet": "master",
|
||||
"jquery": "~2.1.3",
|
||||
"tweetnacl": "~0.12.2",
|
||||
"ckeditor": "~4.5.6",
|
||||
|
@ -28,6 +26,17 @@
|
|||
"reconnectingWebsocket": "",
|
||||
"diff-dom": "https://github.com/fiduswriter/diffDOM.git#6fdb82c8a4f2096c07c129797a313fe13769e094",
|
||||
"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
|
||||
httpPort: 3000,
|
||||
// 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
|
||||
|
||||
|
@ -19,7 +33,7 @@ module.exports = {
|
|||
* it will not scale well if your server stays alive for a long time.
|
||||
* but it is completely dependency free
|
||||
*/
|
||||
storage: './storage/amnesia',
|
||||
//storage: './storage/amnesia',
|
||||
|
||||
/* the 'lvl' storage module uses leveldb
|
||||
* it persists, and will perform better than amnesiadb
|
||||
|
@ -31,8 +45,8 @@ module.exports = {
|
|||
*
|
||||
* to delete all pads, run `rm -rf $YOUR_DB`
|
||||
*/
|
||||
// storage: './storage/lvl',
|
||||
// levelPath: './test.level.db'
|
||||
storage: './storage/lvl',
|
||||
levelPath: './test.level.db'
|
||||
|
||||
/* mongo is the original storage engine for cryptpad
|
||||
* it has been more thoroughly tested, but requires a little more setup
|
||||
|
|
|
@ -122,9 +122,12 @@
|
|||
</p>
|
||||
</noscript>
|
||||
<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('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-code').setAttribute('href', '/code/#' + Crypto.genKey());
|
||||
});
|
||||
|
@ -215,7 +218,8 @@
|
|||
|
||||
<div id="buttons" class="buttons" style="display:none;">
|
||||
<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>
|
||||
</div>
|
||||
</center>
|
||||
|
|
|
@ -9,9 +9,11 @@
|
|||
"nthen": "~0.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"jshint": "~2.9.1"
|
||||
"jshint": "~2.9.1",
|
||||
"selenium-webdriver": "^2.53.1"
|
||||
},
|
||||
"scripts": {
|
||||
"lint": "jshint --config .jshintrc --exclude-path .jshintignore ."
|
||||
"lint": "jshint --config .jshintrc --exclude-path .jshintignore .",
|
||||
"test": "node TestSelenium.js"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -25,7 +25,7 @@ To install:
|
|||
npm install -g bower ## if necessary
|
||||
bower install
|
||||
|
||||
## copy config.js.dist to config.js
|
||||
## copy config.js.dist to config.js
|
||||
cp config.js.dist config.js
|
||||
|
||||
## modify configuration to use your own mongodb instance
|
||||
|
@ -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).
|
||||
|
||||
## 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
|
||||
|
||||
CryptPad is *private*, not *anonymous*. Privacy protects your data, anonymity protects you.
|
||||
|
|
17
server.js
17
server.js
|
@ -6,7 +6,8 @@ var Http = require('http');
|
|||
var Https = require('https');
|
||||
var Fs = require('fs');
|
||||
var WebSocketServer = require('ws').Server;
|
||||
var ChainPadSrv = require('./ChainPadSrv');
|
||||
var NetfluxSrv = require('./NetfluxWebsocketSrv');
|
||||
var WebRTCSrv = require('./WebRTCSrv');
|
||||
|
||||
var config = require('./config');
|
||||
config.websocketPort = config.websocketPort || config.httpPort;
|
||||
|
@ -17,12 +18,6 @@ var Storage = require(config.storage||'./storage/mongo');
|
|||
var app = Express();
|
||||
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";
|
||||
if (!Fs.existsSync(__dirname + "/customize")) {
|
||||
customize = "/customize.dist";
|
||||
|
@ -60,7 +55,9 @@ app.get('/api/config', function(req, res){
|
|||
res.setHeader('Content-Type', 'text/javascript');
|
||||
res.send('define(' + JSON.stringify({
|
||||
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");
|
||||
wsConfig = { port: config.websocketPort};
|
||||
}
|
||||
|
||||
var wsSrv = new WebSocketServer(wsConfig);
|
||||
Storage.create(config, function (store) {
|
||||
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
|
||||
|
||||
### 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.
|
||||
|
||||
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.
|
||||
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 likely to fail.
|
||||
In practice, out of order messages make your clientside application more likely to fail, however, they are generally tolerated.
|
||||
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
|
||||
|
||||
|
|
|
@ -18,6 +18,11 @@ module.exports.create = function(conf, cb){
|
|||
|
||||
var db=[],
|
||||
index=0;
|
||||
|
||||
if (conf.removeChannels) {
|
||||
console.log("Server is set to remove channels %sms after the last remaining client leaves.", conf.channelRemovalTimeout);
|
||||
}
|
||||
|
||||
cb({
|
||||
message: function(channelName, content, cb){
|
||||
var val = {
|
||||
|
@ -27,17 +32,25 @@ module.exports.create = function(conf, cb){
|
|||
time: new Date().getTime(),
|
||||
};
|
||||
db.push(val);
|
||||
cb();
|
||||
if (cb) { cb(); }
|
||||
},
|
||||
getMessages: function(channelName, cb){
|
||||
getMessages: function(channelName, handler, cb){
|
||||
db.sort(function(a,b){
|
||||
return a.id - b.id;
|
||||
});
|
||||
db.filter(function(val){
|
||||
return val.chan === channelName;
|
||||
}).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 index;
|
||||
nThen(function (waitFor) {
|
||||
getIndex(db, channelName, waitFor(function (i) { index = i+1; }));
|
||||
}).nThen(function (waitFor) {
|
||||
db.put(channelName+'=>'+index, content, waitFor(function (e) { if (e) { throw e; } }));
|
||||
}).nThen(function (waitFor) {
|
||||
db.put(channelName+'=>index', ''+index, waitFor(function (e) { if (e) { throw e; } }));
|
||||
}).nThen(cb);
|
||||
var doIt = function () {
|
||||
db.locked = true;
|
||||
nThen(function (waitFor) {
|
||||
getIndex(db, channelName, waitFor(function (i) { index = i+1; }));
|
||||
}).nThen(function (waitFor) {
|
||||
db.put(channelName+'=>'+index, content, waitFor(function (e) { if (e) { throw e; } }));
|
||||
}).nThen(function (waitFor) {
|
||||
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);
|
||||
};
|
||||
if (db.locked) {
|
||||
db.queue.push(doIt);
|
||||
} else {
|
||||
doIt();
|
||||
}
|
||||
};
|
||||
|
||||
var getMessages = function (db, channelName, msgHandler) {
|
||||
var getMessages = function (db, channelName, msgHandler, cb) {
|
||||
var index;
|
||||
nThen(function (waitFor) {
|
||||
getIndex(db, channelName, waitFor(function (i) { index = i; }));
|
||||
getIndex(db, channelName, waitFor(function (i) {
|
||||
index = i;
|
||||
}));
|
||||
}).nThen(function (waitFor) {
|
||||
var again = function (i) {
|
||||
db.get(channelName + '=>' + i, waitFor(function (e, out) {
|
||||
if (e) { throw e; }
|
||||
msgHandler(out);
|
||||
if (i < index) { again(i+1); }
|
||||
else if (cb) { cb(); }
|
||||
}));
|
||||
};
|
||||
if (index > -1) { again(0); }
|
||||
else if (cb) { cb(); }
|
||||
});
|
||||
};
|
||||
|
||||
module.exports.create = function (conf, cb) {
|
||||
var db = Level(conf.levelPath || './test.level.db');
|
||||
db.locked = false;
|
||||
db.queue = [];
|
||||
cb({
|
||||
message: function (channelName, content, cb) {
|
||||
insert(db, channelName, content, cb);
|
||||
},
|
||||
getMessages: function (channelName, msgHandler) {
|
||||
getMessages(db, channelName, msgHandler);
|
||||
getMessages: function (channelName, msgHandler, cb) {
|
||||
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 doesn’t understand escaping CSS special
|
||||
// characters like `#`. Don’t 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 name="viewport" content="width=device-width, initial-scale=1.0"/>
|
||||
<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>
|
||||
<body>
|
||||
|
||||
|
@ -12,7 +30,12 @@
|
|||
<h2>Test 1</h2>
|
||||
<h3>class strings</h3>
|
||||
<!-- 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>
|
||||
|
||||
|
@ -20,6 +43,6 @@
|
|||
<h3>XWiki Macros</h3>
|
||||
|
||||
<!-- 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>
|
||||
|
|
|
@ -1,27 +1,111 @@
|
|||
require.config({ paths: { 'json.sortify': '/bower_components/json.sortify/dist/JSON.sortify' } });
|
||||
define([
|
||||
'/bower_components/jquery/dist/jquery.min.js',
|
||||
'/assert/hyperjson.js', // serializing classes as an attribute
|
||||
'/assert/hyperscript.js', // using setAttribute
|
||||
'/common/TextPatcher.js'
|
||||
], function (jQuery, Hyperjson, Hyperscript, TextPatcher) {
|
||||
'/bower_components/hyperjson/hyperjson.amd.js', // serializing classes as an attribute
|
||||
'/common/hyperscript.js', // using setAttribute
|
||||
'/bower_components/textpatcher/TextPatcher.amd.js',
|
||||
'json.sortify',
|
||||
], function (jQuery, Hyperjson, Hyperscript, TextPatcher, Sortify) {
|
||||
var $ = window.jQuery;
|
||||
window.Hyperjson = Hyperjson;
|
||||
window.Hyperscript = Hyperscript;
|
||||
window.TextPatcher = TextPatcher;
|
||||
window.Sortify = Sortify;
|
||||
|
||||
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) {
|
||||
if (test()) {
|
||||
assertions++;
|
||||
} else {
|
||||
throw new Error(msg || '');
|
||||
}
|
||||
ASSERTS.push(function (i) {
|
||||
var returned = test();
|
||||
if (returned === true) {
|
||||
assertions++;
|
||||
} else {
|
||||
failed = true;
|
||||
failedOn = assertions;
|
||||
failMessages.push({
|
||||
test: i,
|
||||
message: msg,
|
||||
output: returned,
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
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 () {
|
||||
var hjson = Hyperjson.fromDOM(target);
|
||||
var cloned = Hyperjson.callOn(hjson, Hyperscript);
|
||||
|
@ -29,22 +113,117 @@ define([
|
|||
var success = cloned.outerHTML === target.outerHTML;
|
||||
|
||||
if (!success) {
|
||||
var op = TextPatcher.diff(target.outerHTML, cloned.outerHTML);
|
||||
window.DEBUG = {
|
||||
error: "Expected equality between A and B",
|
||||
A: target.outerHTML,
|
||||
B: cloned.outerHTML,
|
||||
target: target,
|
||||
diff: TextPatcher.diff(target.outerHTML, cloned.outerHTML)
|
||||
diff: op
|
||||
};
|
||||
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;
|
||||
}, "Round trip serialization introduced artifacts.");
|
||||
};
|
||||
|
||||
roundTrip($('#target')[0]);
|
||||
roundTrip($('#widget')[0]);
|
||||
var HTML_list = [
|
||||
'#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"> </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
|
||||
Allen Sarkisyan
|
||||
Amin Shali
|
||||
Amin Ullah Khan
|
||||
amshali@google.com
|
||||
Amsul
|
||||
amuntean
|
||||
Amy
|
||||
|
@ -59,10 +61,13 @@ Anthony Grimes
|
|||
Anton Kovalyov
|
||||
AQNOUCH Mohammed
|
||||
areos
|
||||
Arnab Bose
|
||||
as3boyan
|
||||
AtomicPages LLC
|
||||
Atul Bhouraskar
|
||||
Aurelian Oancea
|
||||
Barret Rennie
|
||||
Basarat Ali Syed
|
||||
Bastian Müller
|
||||
belhaj
|
||||
Bem Jones-Bey
|
||||
|
@ -70,6 +75,7 @@ benbro
|
|||
Beni Cherniavsky-Paskin
|
||||
Benjamin DeCoste
|
||||
Ben Keen
|
||||
Ben Mosher
|
||||
Bernhard Sirlinger
|
||||
Bert Chang
|
||||
Billy Moon
|
||||
|
@ -86,11 +92,14 @@ Brett Zamir
|
|||
Brian Grinstead
|
||||
Brian Sletten
|
||||
Bruce Mitchener
|
||||
Caitlin Potter
|
||||
Calin Barbat
|
||||
Chad Jolly
|
||||
Chandra Sekhar Pydi
|
||||
Charles Skelton
|
||||
Cheah Chu Yeow
|
||||
Chris Coyier
|
||||
Chris Ford
|
||||
Chris Granger
|
||||
Chris Houseknecht
|
||||
Chris Lohfink
|
||||
|
@ -100,9 +109,11 @@ Christian Petrov
|
|||
Christopher Brown
|
||||
Christopher Mitchell
|
||||
Christopher Pfohl
|
||||
Chunliang Lyu
|
||||
ciaranj
|
||||
CodeAnimal
|
||||
coderaiser
|
||||
Cole R Lawrence
|
||||
ComFreek
|
||||
Curtis Gagliardi
|
||||
dagsta
|
||||
|
@ -114,6 +125,7 @@ Daniel, Dao Quang Minh
|
|||
Daniele Di Sarli
|
||||
Daniel Faust
|
||||
Daniel Huigens
|
||||
Daniel Kesler
|
||||
Daniel KJ
|
||||
Daniel Neel
|
||||
Daniel Parnell
|
||||
|
@ -126,8 +138,10 @@ David Barnett
|
|||
David Mignot
|
||||
David Pathakjee
|
||||
David Vázquez
|
||||
David Whittington
|
||||
deebugger
|
||||
Deep Thought
|
||||
Devin Abbott
|
||||
Devon Carew
|
||||
dignifiedquire
|
||||
Dimage Sapelkin
|
||||
|
@ -139,13 +153,16 @@ Doug Wikle
|
|||
Drew Bratcher
|
||||
Drew Hintz
|
||||
Drew Khoury
|
||||
Drini Cami
|
||||
Dror BG
|
||||
duralog
|
||||
eborden
|
||||
edsharp
|
||||
ekhaled
|
||||
Elisée
|
||||
Enam Mijbah Noor
|
||||
Eric Allam
|
||||
Erik Welander
|
||||
eustas
|
||||
Fabien O'Carroll
|
||||
Fabio Zendhi Nagao
|
||||
|
@ -167,19 +184,24 @@ Gabriel Horner
|
|||
Gabriel Nahmias
|
||||
galambalazs
|
||||
Gautam Mehta
|
||||
Gavin Douglas
|
||||
gekkoe
|
||||
geowarin
|
||||
Gerard Braad
|
||||
Gergely Hegykozi
|
||||
Giovanni Calò
|
||||
Glebov Boris
|
||||
Glenn Jorde
|
||||
Glenn Ruehle
|
||||
Golevka
|
||||
Google Inc.
|
||||
Gordon Smith
|
||||
Grant Skinner
|
||||
greengiant
|
||||
Gregory Koberger
|
||||
Guillaume Massé
|
||||
Guillaume Massé
|
||||
guraga
|
||||
Gustavo Rodrigues
|
||||
Hakan Tunc
|
||||
Hans Engel
|
||||
|
@ -190,12 +212,14 @@ Herculano Campos
|
|||
Hiroyuki Makino
|
||||
hitsthings
|
||||
Hocdoc
|
||||
Hugues Malphettes
|
||||
Ian Beck
|
||||
Ian Dickinson
|
||||
Ian Wehrman
|
||||
Ian Wetherbee
|
||||
Ice White
|
||||
ICHIKAWA, Yuji
|
||||
idleberg
|
||||
ilvalle
|
||||
Ingo Richter
|
||||
Irakli Gozalishvili
|
||||
|
@ -212,6 +236,7 @@ Jan Jongboom
|
|||
jankeromnes
|
||||
Jan Keromnes
|
||||
Jan Odvarko
|
||||
Jan Schär
|
||||
Jan T. Sott
|
||||
Jared Forsyth
|
||||
Jason
|
||||
|
@ -227,12 +252,17 @@ jeffkenton
|
|||
Jeff Pickhardt
|
||||
jem (graphite)
|
||||
Jeremy Parmenter
|
||||
Jim
|
||||
JobJob
|
||||
jochenberger
|
||||
Jochen Berger
|
||||
Johan Ask
|
||||
John Connor
|
||||
John Engler
|
||||
John Lees-Miller
|
||||
John Snelson
|
||||
John Van Der Loo
|
||||
Jon Ander Peñalba
|
||||
Jonas Döbertin
|
||||
Jonathan Malmaud
|
||||
jongalloway
|
||||
|
@ -240,6 +270,7 @@ Jon Malmaud
|
|||
Jon Sangster
|
||||
Joost-Wim Boekesteijn
|
||||
Joseph Pecoraro
|
||||
Josh Cohen
|
||||
Joshua Newman
|
||||
Josh Watzman
|
||||
jots
|
||||
|
@ -248,11 +279,15 @@ ju1ius
|
|||
Juan Benavides Romero
|
||||
Jucovschi Constantin
|
||||
Juho Vuori
|
||||
Julien Rebetez
|
||||
Justin Andresen
|
||||
Justin Hileman
|
||||
jwallers@gmail.com
|
||||
kaniga
|
||||
karevn
|
||||
Kayur Patel
|
||||
Ken Newman
|
||||
ken restivo
|
||||
Ken Rockot
|
||||
Kevin Earls
|
||||
Kevin Sawicki
|
||||
|
@ -296,12 +331,15 @@ Marek Rudnicki
|
|||
Marijn Haverbeke
|
||||
Mário Gonçalves
|
||||
Mario Pietsch
|
||||
Mark Anderson
|
||||
Mark Lentczner
|
||||
Marko Bonaci
|
||||
Markus Bordihn
|
||||
Martin Balek
|
||||
Martín Gaitán
|
||||
Martin Hasoň
|
||||
Martin Hunt
|
||||
Martin Laine
|
||||
Martin Zagora
|
||||
Mason Malone
|
||||
Mateusz Paprocki
|
||||
|
@ -323,10 +361,12 @@ Max Kirsch
|
|||
Max Schaefer
|
||||
Max Xiantu
|
||||
mbarkhau
|
||||
McBrainy
|
||||
melpon
|
||||
Metatheos
|
||||
Micah Dubinko
|
||||
Michael
|
||||
Michael Goderbauer
|
||||
Michael Grey
|
||||
Michael Kaminsky
|
||||
Michael Lehenbauer
|
||||
|
@ -361,6 +401,7 @@ Nicholas Bollweg
|
|||
Nicholas Bollweg (Nick)
|
||||
Nick Kreeger
|
||||
Nick Small
|
||||
Nicolò Ribaudo
|
||||
Niels van Groningen
|
||||
nightwing
|
||||
Nikita Beloglazov
|
||||
|
@ -373,6 +414,7 @@ noragrossman
|
|||
Norman Rzepka
|
||||
Oreoluwa Onatemowo
|
||||
pablo
|
||||
pabloferz
|
||||
Page
|
||||
Panupong Pasupat
|
||||
paris
|
||||
|
@ -382,18 +424,25 @@ Patrick Stoica
|
|||
Patrick Strawderman
|
||||
Paul Garvin
|
||||
Paul Ivanov
|
||||
Pavel
|
||||
Pavel Feldman
|
||||
Pavel Strashkin
|
||||
Paweł Bartkiewicz
|
||||
peteguhl
|
||||
peter
|
||||
Peter Flynn
|
||||
peterkroon
|
||||
Peter Kroon
|
||||
Philipp A
|
||||
Philip Stadermann
|
||||
Pierre Gerold
|
||||
Piët Delport
|
||||
prasanthj
|
||||
Prasanth J
|
||||
Prayag Verma
|
||||
Radek Piórkowski
|
||||
Rahul
|
||||
Rahul Anand
|
||||
ramwin1
|
||||
Randall Mason
|
||||
Randy Burden
|
||||
|
@ -420,10 +469,12 @@ Sascha Peilicke
|
|||
satamas
|
||||
satchmorun
|
||||
sathyamoorthi
|
||||
S. Chris Colbert
|
||||
SCLINIC\jdecker
|
||||
Scott Aikin
|
||||
Scott Goodhew
|
||||
Sebastian Zaha
|
||||
Sergey Goder
|
||||
Se-Won Kim
|
||||
shaund
|
||||
shaun gilchrist
|
||||
|
@ -434,6 +485,7 @@ Shiv Deepak
|
|||
Shmuel Englard
|
||||
Shubham Jain
|
||||
silverwind
|
||||
sinkuu
|
||||
snasa
|
||||
soliton4
|
||||
sonson
|
||||
|
@ -443,14 +495,19 @@ Stanislav Oaserele
|
|||
Stas Kobzar
|
||||
Stefan Borsje
|
||||
Steffen Beyer
|
||||
Steffen Bruchmann
|
||||
Stephen Lavelle
|
||||
Steve Champagne
|
||||
Steve O'Hara
|
||||
stoskov
|
||||
Stu Kennedy
|
||||
Sungho Kim
|
||||
sverweij
|
||||
Taha Jahangir
|
||||
Tako Schotanus
|
||||
Takuji Shimokawa
|
||||
Tarmil
|
||||
TDaglis
|
||||
tel
|
||||
tfjgeorge
|
||||
Thaddee Tyl
|
||||
|
@ -471,6 +528,8 @@ Tom MacWright
|
|||
Tony Jian
|
||||
Travis Heppe
|
||||
Triangle717
|
||||
Tristan Tarrant
|
||||
TSUYUSATO Kitsune
|
||||
twifkak
|
||||
Vestimir Markov
|
||||
vf
|
||||
|
@ -481,15 +540,18 @@ wenli
|
|||
Wes Cossick
|
||||
Wesley Wiser
|
||||
Will Binns-Smith
|
||||
Will Dean
|
||||
William Jamieson
|
||||
William Stein
|
||||
Willy
|
||||
Wojtek Ptak
|
||||
Wu Cheng-Han
|
||||
Xavier Mendez
|
||||
Yassin N. Hassan
|
||||
YNH Webdev
|
||||
Yunchi Luo
|
||||
Yuvi Panda
|
||||
Zac Anger
|
||||
Zachary Dremann
|
||||
Zhang Hao
|
||||
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
|
||||
|
||||
- [Getting help](#getting-help-)
|
||||
- [Submitting bug reports](#submitting-bug-reports-)
|
||||
- [Contributing code](#contributing-code-)
|
||||
- [Getting help](#getting-help)
|
||||
- [Submitting bug reports](#submitting-bug-reports)
|
||||
- [Contributing code](#contributing-code)
|
||||
|
||||
## 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
|
||||
of this software and associated documentation files (the "Software"), to deal
|
|
@ -16,7 +16,7 @@ new functionality.
|
|||
You can find more information (and the
|
||||
[manual](http://codemirror.net/doc/manual.html)) on the [project
|
||||
page](http://codemirror.net). For questions and discussion, use the
|
||||
[discussion forum](http://discuss.codemirror.net/).
|
||||
[discussion forum](https://discuss.codemirror.net/).
|
||||
|
||||
See
|
||||
[CONTRIBUTING.md](https://github.com/codemirror/CodeMirror/blob/master/CONTRIBUTING.md)
|
|
@ -21,22 +21,28 @@
|
|||
}
|
||||
|
||||
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--) {
|
||||
var from = ranges[i].from(), to = ranges[i].to();
|
||||
if (from.line >= minLine) continue;
|
||||
if (to.line >= minLine) to = Pos(minLine, 0);
|
||||
minLine = from.line;
|
||||
if (mode == null) {
|
||||
if (cm.uncomment(from, to)) mode = "un";
|
||||
else { cm.lineComment(from, to); mode = "line"; }
|
||||
if (cm.uncomment(from, to, options)) mode = "un";
|
||||
else { cm.lineComment(from, to, options); mode = "line"; }
|
||||
} else if (mode == "un") {
|
||||
cm.uncomment(from, to);
|
||||
cm.uncomment(from, to, options);
|
||||
} else {
|
||||
cm.lineComment(from, to);
|
||||
cm.lineComment(from, to, options);
|
||||
}
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
CodeMirror.defineExtension("lineComment", function(from, to, options) {
|
||||
if (!options) options = noOptions;
|
||||
|
@ -57,7 +63,14 @@
|
|||
|
||||
self.operation(function() {
|
||||
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) {
|
||||
var line = self.getLine(i), cut = baseString.length;
|
||||
if (!blankLines && !nonWS.test(line)) continue;
|
|
@ -56,6 +56,8 @@
|
|||
|
||||
var inp = dialog.getElementsByTagName("input")[0], button;
|
||||
if (inp) {
|
||||
inp.focus();
|
||||
|
||||
if (options.value) {
|
||||
inp.value = options.value;
|
||||
if (options.selectValueOnOpen !== false) {
|
||||
|
@ -79,8 +81,6 @@
|
|||
});
|
||||
|
||||
if (options.closeOnBlur !== false) CodeMirror.on(inp, "blur", close);
|
||||
|
||||
inp.focus();
|
||||
} else if (button = dialog.getElementsByTagName("button")[0]) {
|
||||
CodeMirror.on(button, "click", function() {
|
||||
close();
|
|
@ -14,10 +14,12 @@
|
|||
if (val && !prev) {
|
||||
cm.on("blur", onBlur);
|
||||
cm.on("change", onChange);
|
||||
cm.on("swapDoc", onChange);
|
||||
onChange(cm);
|
||||
} else if (!val && prev) {
|
||||
cm.off("blur", onBlur);
|
||||
cm.off("change", onChange);
|
||||
cm.off("swapDoc", onChange);
|
||||
clearPlaceholder(cm);
|
||||
var wrapper = cm.getWrapperElement();
|
||||
wrapper.className = wrapper.className.replace(" CodeMirror-empty", "");
|
|
@ -63,7 +63,7 @@
|
|||
}
|
||||
for (var i = ranges.length - 1; i >= 0; i--) {
|
||||
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;
|
||||
}
|
||||
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;
|
||||
break;
|
||||
}
|
|
@ -20,7 +20,7 @@
|
|||
cm.off("viewportChange", onViewportChange);
|
||||
cm.off("fold", onFold);
|
||||
cm.off("unfold", onFold);
|
||||
cm.off("swapDoc", updateInViewport);
|
||||
cm.off("swapDoc", onChange);
|
||||
}
|
||||
if (val) {
|
||||
cm.state.foldGutter = new State(parseOptions(val));
|
||||
|
@ -30,7 +30,7 @@
|
|||
cm.on("viewportChange", onViewportChange);
|
||||
cm.on("fold", onFold);
|
||||
cm.on("unfold", onFold);
|
||||
cm.on("swapDoc", updateInViewport);
|
||||
cm.on("swapDoc", onChange);
|
||||
}
|
||||
});
|
||||
|
|
@ -25,8 +25,18 @@
|
|||
};
|
||||
|
||||
CodeMirror.defineExtension("showHint", function(options) {
|
||||
// We want a single cursor position.
|
||||
if (this.listSelections().length > 1 || this.somethingSelected()) return;
|
||||
options = parseOptions(this, this.getCursor("start"), options);
|
||||
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();
|
||||
var completion = this.state.completionActive = new Completion(this, options);
|
||||
|
@ -38,12 +48,12 @@
|
|||
|
||||
function Completion(cm, options) {
|
||||
this.cm = cm;
|
||||
this.options = this.buildOptions(options);
|
||||
this.options = options;
|
||||
this.widget = null;
|
||||
this.debounce = 0;
|
||||
this.tick = 0;
|
||||
this.startPos = this.cm.getCursor();
|
||||
this.startLen = this.cm.getLine(this.startPos.line).length;
|
||||
this.startPos = this.cm.getCursor("start");
|
||||
this.startLen = this.cm.getLine(this.startPos.line).length - this.cm.getSelection().length;
|
||||
|
||||
var self = this;
|
||||
cm.on("cursorActivity", this.activityFunc = function() { self.cursorActivity(); });
|
||||
|
@ -111,11 +121,13 @@
|
|||
|
||||
finishUpdate: function(data, first) {
|
||||
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);
|
||||
if (this.widget) this.widget.close();
|
||||
|
||||
if (data && this.data && isNewCompletion(this.data, data)) return;
|
||||
this.data = data;
|
||||
|
||||
if (data && data.list.length) {
|
||||
if (picked && data.list.length == 1) {
|
||||
this.pick(data, 0);
|
||||
|
@ -124,20 +136,26 @@
|
|||
CodeMirror.signal(data, "shown");
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
buildOptions: function(options) {
|
||||
var editor = this.cm.options.hintOptions;
|
||||
var out = {};
|
||||
for (var prop in defaultOptions) out[prop] = defaultOptions[prop];
|
||||
if (editor) for (var prop in editor)
|
||||
if (editor[prop] !== undefined) out[prop] = editor[prop];
|
||||
if (options) for (var prop in options)
|
||||
if (options[prop] !== undefined) out[prop] = options[prop];
|
||||
return out;
|
||||
}
|
||||
};
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
function parseOptions(cm, pos, options) {
|
||||
var editor = cm.options.hintOptions;
|
||||
var out = {};
|
||||
for (var prop in defaultOptions) out[prop] = defaultOptions[prop];
|
||||
if (editor) for (var prop in editor)
|
||||
if (editor[prop] !== undefined) out[prop] = editor[prop];
|
||||
if (options) for (var prop in options)
|
||||
if (options[prop] !== undefined) out[prop] = options[prop];
|
||||
if (out.hint.resolve) out.hint = out.hint.resolve(cm, pos)
|
||||
return out;
|
||||
}
|
||||
|
||||
function getText(completion) {
|
||||
if (typeof completion == "string") return completion;
|
||||
else return completion.text;
|
||||
|
@ -336,18 +354,61 @@
|
|||
}
|
||||
};
|
||||
|
||||
CodeMirror.registerHelper("hint", "auto", function(cm, options) {
|
||||
var helpers = cm.getHelpers(cm.getCursor(), "hint"), words;
|
||||
function applicableHelpers(cm, helpers) {
|
||||
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) {
|
||||
for (var i = 0; i < helpers.length; i++) {
|
||||
var cur = helpers[i](cm, options);
|
||||
if (cur && cur.list.length) return cur;
|
||||
var async = false, resolved
|
||||
for (var i = 0; i < helpers.length; i++) if (helpers[i].async) async = true
|
||||
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")) {
|
||||
if (words) return CodeMirror.hint.fromList(cm, {words: words});
|
||||
return function(cm) { return CodeMirror.hint.fromList(cm, {words: words}) }
|
||||
} 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) {
|
||||
|
@ -376,7 +437,7 @@
|
|||
alignWithWord: true,
|
||||
closeCharacters: /[\s()\[\]{};:>,]/,
|
||||
closeOnUnfocus: true,
|
||||
completeOnSingleClick: false,
|
||||
completeOnSingleClick: true,
|
||||
container: null,
|
||||
customKeys: null,
|
||||
extraKeys: null
|
|
@ -20,6 +20,8 @@
|
|||
};
|
||||
var Pos = CodeMirror.Pos;
|
||||
|
||||
function isArray(val) { return Object.prototype.toString.call(val) == "[object Array]" }
|
||||
|
||||
function getKeywords(editor) {
|
||||
var mode = editor.doc.modeOption;
|
||||
if (mode === "sql") mode = "text/x-sql";
|
||||
|
@ -30,10 +32,28 @@
|
|||
return typeof item == "string" ? item : item.text;
|
||||
}
|
||||
|
||||
function getItem(list, item) {
|
||||
if (!list.slice) return list[item];
|
||||
for (var i = list.length - 1; i >= 0; i--) if (getText(list[i]) == item)
|
||||
return list[i];
|
||||
function wrapTable(name, value) {
|
||||
if (isArray(value)) value = {columns: value}
|
||||
if (!value.text) value.text = name
|
||||
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) {
|
||||
|
@ -50,11 +70,18 @@
|
|||
}
|
||||
|
||||
function addMatches(result, search, wordlist, formatter) {
|
||||
for (var word in wordlist) {
|
||||
if (!wordlist.hasOwnProperty(word)) continue;
|
||||
if (wordlist.slice) word = wordlist[word];
|
||||
|
||||
if (match(search, word)) result.push(formatter(word));
|
||||
if (isArray(wordlist)) {
|
||||
for (var i = 0; i < wordlist.length; i++)
|
||||
if (match(search, wordlist[i])) result.push(formatter(wordlist[i]))
|
||||
} else {
|
||||
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 aliasTable = table;
|
||||
// Check if table is available. If not, find table by Alias
|
||||
if (!getItem(tables, table)) {
|
||||
if (!getTable(table)) {
|
||||
var oldTable = table;
|
||||
table = findTableByAlias(table, editor);
|
||||
if (table !== oldTable) alias = true;
|
||||
}
|
||||
|
||||
var columns = getItem(tables, table);
|
||||
var columns = getTable(table);
|
||||
if (columns && columns.columns)
|
||||
columns = columns.columns;
|
||||
|
||||
|
@ -184,7 +211,7 @@
|
|||
//find valid range
|
||||
var prevItem = 0;
|
||||
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]);
|
||||
if (current > prevItem && current <= _v) {
|
||||
validRange = { start: convertNumberToCur(prevItem), end: convertNumberToCur(_v) };
|
||||
|
@ -199,7 +226,7 @@
|
|||
var lineText = query[i];
|
||||
eachWord(lineText, function(word) {
|
||||
var wordUpperCase = word.toUpperCase();
|
||||
if (wordUpperCase === aliasUpperCase && getItem(tables, previousWord))
|
||||
if (wordUpperCase === aliasUpperCase && getTable(previousWord))
|
||||
table = previousWord;
|
||||
if (wordUpperCase !== CONS.ALIAS_KEYWORD)
|
||||
previousWord = word;
|
||||
|
@ -210,10 +237,10 @@
|
|||
}
|
||||
|
||||
CodeMirror.registerHelper("hint", "sql", function(editor, options) {
|
||||
tables = (options && options.tables) || {};
|
||||
tables = parseTables(options && options.tables)
|
||||
var defaultTableName = options && options.defaultTable;
|
||||
var disableKeywords = options && options.disableKeywords;
|
||||
defaultTable = defaultTableName && getItem(tables, defaultTableName);
|
||||
defaultTable = defaultTableName && getTable(defaultTableName);
|
||||
keywords = keywords || getKeywords(editor);
|
||||
|
||||
if (defaultTableName && !defaultTable)
|
|
@ -186,9 +186,14 @@
|
|||
state.timeout = setTimeout(function(){startLinting(cm);}, state.options.delay || 500);
|
||||
}
|
||||
|
||||
function popupSpanTooltip(ann, e) {
|
||||
function popupTooltips(annotations, e) {
|
||||
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) {
|
||||
|
@ -196,10 +201,12 @@
|
|||
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 spans = cm.findMarksAt(cm.coordsChar({left: x, top: y}, "client"));
|
||||
|
||||
var annotations = [];
|
||||
for (var i = 0; i < spans.length; ++i) {
|
||||
var ann = spans[i].__annotation;
|
||||
if (ann) return popupSpanTooltip(ann, e);
|
||||
annotations.push(spans[i].__annotation);
|
||||
}
|
||||
if (annotations.length) popupTooltips(annotations, e);
|
||||
}
|
||||
|
||||
CodeMirror.defineOption("lint", false, function(cm, val, old) {
|
|
@ -60,6 +60,7 @@
|
|||
position: absolute;
|
||||
cursor: pointer;
|
||||
color: #44c;
|
||||
z-index: 3;
|
||||
}
|
||||
|
||||
.CodeMirror-merge-copy-reverse {
|
|
@ -5,7 +5,7 @@
|
|||
|
||||
(function(mod) {
|
||||
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
|
||||
define(["../../lib/codemirror", "diff_match_patch"], mod);
|
||||
else // Plain browser env
|
||||
|
@ -427,8 +427,9 @@
|
|||
|
||||
function copyChunk(dv, to, from, chunk) {
|
||||
if (dv.diffOutOfDate) return;
|
||||
to.replaceRange(from.getRange(Pos(chunk.origFrom, 0), Pos(chunk.origTo, 0)),
|
||||
Pos(chunk.editFrom, 0), Pos(chunk.editTo, 0));
|
||||
var editStart = chunk.editTo > to.lastLine() ? Pos(chunk.editFrom - 1) : Pos(chunk.editFrom, 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.
|
||||
|
@ -471,13 +472,10 @@
|
|||
if (left) left.init(leftPane, origLeft, options);
|
||||
if (right) right.init(rightPane, origRight, options);
|
||||
|
||||
if (options.collapseIdentical) {
|
||||
updating = true;
|
||||
if (options.collapseIdentical)
|
||||
this.editor().operation(function() {
|
||||
collapseIdenticalStretches(self, options.collapseIdentical);
|
||||
});
|
||||
updating = false;
|
||||
}
|
||||
if (options.connect == "align") {
|
||||
this.aligners = [];
|
||||
alignChunks(this.left || this.right, true);
|
||||
|
@ -640,7 +638,7 @@
|
|||
mark.clear();
|
||||
cm.removeLineClass(from, "wrap", "CodeMirror-merge-collapsed-line");
|
||||
}
|
||||
widget.addEventListener("click", clear);
|
||||
CodeMirror.on(widget, "click", clear);
|
||||
return {mark: mark, clear: clear};
|
||||
}
|
||||
|
|
@ -51,7 +51,7 @@ CodeMirror.multiplexingMode = function(outer /*, others */) {
|
|||
if (!other.parseDelimiters) stream.match(other.open);
|
||||
state.innerActive = other;
|
||||
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) {
|
||||
cutOff = found;
|
||||
}
|
||||
|
@ -70,7 +70,7 @@ CodeMirror.multiplexingMode = function(outer /*, others */) {
|
|||
if (found == stream.pos && !curInner.parseDelimiters) {
|
||||
stream.match(curInner.close);
|
||||
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);
|
||||
var innerToken = curInner.mode.token(stream, state.inner);
|
||||
|
@ -80,7 +80,7 @@ CodeMirror.multiplexingMode = function(outer /*, others */) {
|
|||
state.innerActive = state.inner = null;
|
||||
|
||||
if (curInner.innerStyle) {
|
||||
if (innerToken) innerToken = innerToken + ' ' + curInner.innerStyle;
|
||||
if (innerToken) innerToken = innerToken + " " + curInner.innerStyle;
|
||||
else innerToken = curInner.innerStyle;
|
||||
}
|
||||
|
|
@ -29,5 +29,5 @@
|
|||
|
||||
MT(
|
||||
"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) {
|
||||
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) {
|
|
@ -16,7 +16,7 @@ CodeMirror.runMode = function(string, modespec, callback, options) {
|
|||
var ie = /MSIE \d/.test(navigator.userAgent);
|
||||
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 node = callback, col = 0;
|
||||
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("../../addon/runmode/runmode")] = require.cache[require.resolve("./runmode.node")];
|
|
@ -51,7 +51,7 @@
|
|||
Annotation.prototype.computeScale = function() {
|
||||
var cm = this.cm;
|
||||
var hScale = (cm.getWrapperElement().clientHeight - cm.display.barHeight - this.buttonHeight * 2) /
|
||||
cm.heightAtLine(cm.lastLine() + 1, "local");
|
||||
cm.getScrollerElement().scrollHeight
|
||||
if (hScale != this.hScale) {
|
||||
this.hScale = hScale;
|
||||
return true;
|
||||
|
@ -100,6 +100,9 @@
|
|||
elt.style.cssText = "position: absolute; right: 0px; width: " + Math.max(cm.display.barWidth - 1, 2) + "px; top: "
|
||||
+ (top + this.buttonHeight) + "px; height: " + height + "px";
|
||||
elt.className = this.options.className;
|
||||
if (ann.id) {
|
||||
elt.setAttribute("annotation-id", ann.id);
|
||||
}
|
||||
}
|
||||
this.div.textContent = "";
|
||||
this.div.appendChild(frag);
|
|
@ -59,16 +59,20 @@
|
|||
CodeMirror.on(this.node, "DOMMouseScroll", onWheel);
|
||||
}
|
||||
|
||||
Bar.prototype.moveTo = function(pos, update) {
|
||||
Bar.prototype.setPos = function(pos) {
|
||||
if (pos < 0) pos = 0;
|
||||
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.inner.style[this.orientation == "horizontal" ? "left" : "top"] =
|
||||
(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;
|
||||
|
||||
Bar.prototype.update = function(scrollSize, clientSize, barSize) {
|
||||
|
@ -83,8 +87,7 @@
|
|||
}
|
||||
this.inner.style[this.orientation == "horizontal" ? "width" : "height"] =
|
||||
buttonSize + "px";
|
||||
this.inner.style[this.orientation == "horizontal" ? "left" : "top"] =
|
||||
this.pos * (this.size / this.total) + "px";
|
||||
this.setPos(this.pos);
|
||||
};
|
||||
|
||||
function SimpleScrollbars(cls, place, scroll) {
|
||||
|
@ -111,7 +114,6 @@
|
|||
if (needsV) {
|
||||
this.vert.update(measure.scrollHeight, measure.clientHeight,
|
||||
measure.viewHeight - (needsH ? width : 0));
|
||||
this.vert.node.style.display = "block";
|
||||
this.vert.node.style.bottom = needsH ? width + "px" : "0";
|
||||
}
|
||||
if (needsH) {
|
||||
|
@ -125,11 +127,11 @@
|
|||
};
|
||||
|
||||
SimpleScrollbars.prototype.setScrollTop = function(pos) {
|
||||
this.vert.moveTo(pos, false);
|
||||
this.vert.setPos(pos);
|
||||
};
|
||||
|
||||
SimpleScrollbars.prototype.setScrollLeft = function(pos) {
|
||||
this.horiz.moveTo(pos, false);
|
||||
this.horiz.setPos(pos);
|
||||
};
|
||||
|
||||
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,
|
||||
// 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
|
||||
// highlighting the matches.
|
||||
// highlighting the matches. If annotateScrollbar is enabled, the occurances
|
||||
// will be highlighted on the scrollbar via the matchesonscrollbar addon.
|
||||
|
||||
(function(mod) {
|
||||
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
|
||||
define(["../../lib/codemirror"], mod);
|
||||
define(["../../lib/codemirror", "./matchesonscrollbar"], mod);
|
||||
else // Plain browser env
|
||||
mod(CodeMirror);
|
||||
})(function(CodeMirror) {
|
||||
|
@ -40,18 +41,19 @@
|
|||
this.showToken = options.showToken;
|
||||
this.delay = options.delay;
|
||||
this.wordsOnly = options.wordsOnly;
|
||||
this.annotateScrollbar = options.annotateScrollbar;
|
||||
}
|
||||
if (this.style == null) this.style = DEFAULT_TOKEN_STYLE;
|
||||
if (this.minChars == null) this.minChars = DEFAULT_MIN_CHARS;
|
||||
if (this.delay == null) this.delay = DEFAULT_DELAY;
|
||||
if (this.wordsOnly == null) this.wordsOnly = DEFAULT_WORDS_ONLY;
|
||||
this.overlay = this.timeout = null;
|
||||
this.matchesonscroll = null;
|
||||
}
|
||||
|
||||
CodeMirror.defineOption("highlightSelectionMatches", false, function(cm, val, old) {
|
||||
if (old && old != CodeMirror.Init) {
|
||||
var over = cm.state.matchHighlighter.overlay;
|
||||
if (over) cm.removeOverlay(over);
|
||||
removeOverlay(cm);
|
||||
clearTimeout(cm.state.matchHighlighter.timeout);
|
||||
cm.state.matchHighlighter = null;
|
||||
cm.off("cursorActivity", cursorActivity);
|
||||
|
@ -69,20 +71,39 @@
|
|||
state.timeout = setTimeout(function() {highlightMatches(cm);}, state.delay);
|
||||
}
|
||||
|
||||
function addOverlay(cm, query, hasBoundary, style) {
|
||||
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;
|
||||
if (state.overlay) {
|
||||
cm.removeOverlay(state.overlay);
|
||||
state.overlay = null;
|
||||
if (state.annotateScrollbar) {
|
||||
state.matchesonscroll.clear();
|
||||
state.matchesonscroll = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function highlightMatches(cm) {
|
||||
cm.operation(function() {
|
||||
var state = cm.state.matchHighlighter;
|
||||
if (state.overlay) {
|
||||
cm.removeOverlay(state.overlay);
|
||||
state.overlay = null;
|
||||
}
|
||||
removeOverlay(cm);
|
||||
if (!cm.somethingSelected() && state.showToken) {
|
||||
var re = state.showToken === true ? /[\w$]/ : state.showToken;
|
||||
var cur = cm.getCursor(), line = cm.getLine(cur.line), start = cur.ch, end = start;
|
||||
while (start && re.test(line.charAt(start - 1))) --start;
|
||||
while (end < line.length && re.test(line.charAt(end))) ++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;
|
||||
}
|
||||
var from = cm.getCursor("from"), to = cm.getCursor("to");
|
||||
|
@ -90,7 +111,7 @@
|
|||
if (state.wordsOnly && !isWord(cm, from, to)) return;
|
||||
var selection = cm.getRange(from, to).replace(/^\s+|\s+$/g, "");
|
||||
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;
|
||||
var match = query.exec(stream.string);
|
||||
if (match && match.index == stream.pos) {
|
||||
stream.pos += match[0].length;
|
||||
stream.pos += match[0].length || 1;
|
||||
return "searching";
|
||||
} else if (match) {
|
||||
stream.pos = match.index;
|
|
@ -18,6 +18,7 @@
|
|||
"use strict";
|
||||
var WRAP_CLASS = "CodeMirror-activeline";
|
||||
var BACK_CLASS = "CodeMirror-activeline-background";
|
||||
var GUTT_CLASS = "CodeMirror-activeline-gutter";
|
||||
|
||||
CodeMirror.defineOption("styleActiveLine", false, function(cm, val, old) {
|
||||
var prev = old && old != CodeMirror.Init;
|
||||
|
@ -36,6 +37,7 @@
|
|||
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], "background", BACK_CLASS);
|
||||
cm.removeLineClass(cm.state.activeLines[i], "gutter", GUTT_CLASS);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -60,6 +62,7 @@
|
|||
for (var i = 0; i < active.length; i++) {
|
||||
cm.addLineClass(active[i], "wrap", WRAP_CLASS);
|
||||
cm.addLineClass(active[i], "background", BACK_CLASS);
|
||||
cm.addLineClass(active[i], "gutter", GUTT_CLASS);
|
||||
}
|
||||
cm.state.activeLines = active;
|
||||
});
|
|
@ -135,6 +135,7 @@
|
|||
},
|
||||
|
||||
destroy: function () {
|
||||
closeArgHints(this)
|
||||
if (this.worker) {
|
||||
this.worker.terminate();
|
||||
this.worker = null;
|
|
@ -32,11 +32,13 @@
|
|||
function findBreakPoint(text, column, wrapOn, killTrailingSpace) {
|
||||
for (var at = column; at > 0; --at)
|
||||
if (wrapOn.test(text.slice(at - 1, at + 1))) break;
|
||||
if (at == 0) at = column;
|
||||
var endOfText = at;
|
||||
if (killTrailingSpace)
|
||||
while (text.charAt(endOfText - 1) == " ") --endOfText;
|
||||
return {from: endOfText, to: at};
|
||||
for (var first = true;; first = false) {
|
||||
var endOfText = at;
|
||||
if (killTrailingSpace)
|
||||
while (text.charAt(endOfText - 1) == " ") --endOfText;
|
||||
if (endOfText == 0 && first) at = column;
|
||||
else return {from: endOfText, to: at};
|
||||
}
|
||||
}
|
||||
|
||||
function wrapRange(cm, from, to, options) {
|
||||
|
@ -86,7 +88,8 @@
|
|||
if (changes.length) cm.operation(function() {
|
||||
for (var i = 0; i < changes.length; ++i) {
|
||||
var change = changes[i];
|
||||
cm.replaceRange(change.text, change.from, change.to);
|
||||
if (change.text || CodeMirror.cmpPos(change.from, change.to))
|
||||
cm.replaceRange(change.text, change.from, change.to);
|
||||
}
|
||||
});
|
||||
return changes.length ? {from: changes[0].from, to: CodeMirror.changeEnd(changes[changes.length - 1])} : null;
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue