diff --git a/.gitignore b/.gitignore index b1cc390c..5a1e39c8 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ node_modules/* npm-debug.log *data.json +*.db +.idea/ \ No newline at end of file diff --git a/.travis.yml b/.travis.yml index 742f4ca0..c3dcb94f 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,5 +1,7 @@ language: node_js node_js: + - 0.6 - 0.8 - 0.10 - - 0.11 \ No newline at end of file + - 0.11 + - 0.12 \ No newline at end of file diff --git a/README.md b/README.md index 18689c14..f544a3a2 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,12 @@ -gun [![Build Status](https://travis-ci.org/amark/gun.svg?branch=master)](https://travis-ci.org/amark/gun) +gun [![Build Status](https://travis-ci.org/amark/gun.svg?branch=master)](https://travis-ci.org/amark/gun) [![Gitter](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/amark/gun?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) === +GUN is a realtime, decentralized, embedded, graph database engine. + ## Getting Started +For the browser, try out this [tutorial](https://dl.dropboxusercontent.com/u/4374976/gun/web/think.html). This README is for GUN servers. + If you do not have [node](http://nodejs.org/) or [npm](https://www.npmjs.com/), read [this](https://github.com/amark/gun/blob/master/examples/install.sh) first. Then in your terminal, run: @@ -33,42 +37,26 @@ These are the default persistence layers, they are modular and can be replaced o Using S3 is recommended for deployment, and using a file is recommended for local development. -Now you can save your first object, and create a reference to it. +## Demos -```javascript -gun.set({ hello: 'world' }).key('my/first/data'); -``` - -Altogether, try it with the node hello world web server which will reply with your data. - -```javascript -var Gun = require('gun'); -var gun = Gun({ file: 'data.json' }); -gun.set({ hello: 'world' }).key('my/first/data'); - -var http = require('http'); -http.createServer(function(req, res){ - gun.load('my/first/data', function(err, data){ - res.writeHead(200, {'Content-Type': 'application/json'}); - res.end(JSON.stringify(data)); - }); -}).listen(1337, '127.0.0.1'); -console.log('Server running at http://127.0.0.1:1337/'); -``` - -Fire up your browser and hit that URL - you'll see your data, plus some gun specific metadata. - -## Examples - -Try out some online [examples](http://gunjs.herokuapp.com/) or run them yourself with the following command: +The examples included in this repo are online [here](http://gunjs.herokuapp.com/), you can run them locally by: ```bash -git clone http://github.com/amark/gun -cd gun/examples && npm install -node express.js 8080 +sudo npm install gun +cd node_modules/gun +node examples/http.js 8080 ``` -Then visit [http://localhost:8080](http://localhost:8080) in your browser. +Then visit [http://localhost:8080](http://localhost:8080) in your browser. If that did not work it is probably because npm installed to a global directory, to fix this try `mkdir node_modules` in your desired directory and re-run the above commands. + +*** +## WARNINGS +### v0.2.0 [![Queued](https://badge.waffle.io/amark/gun.svg?label=Queue&title=Queue)](http://waffle.io/amark/gun) [![In Progress](https://badge.waffle.io/amark/gun.svg?label=InProgress&title=In%20Progress)](http://waffle.io/amark/gun) [![Pending Deploy](https://badge.waffle.io/amark/gun.svg?label=Pending&title=Done)](http://waffle.io/amark/gun) Status + +Version 0.2.0 is currently in alpha. Important changes include `.get` to `.val`, `.load` to `.get`, and `.set` to `.put`. Documentation is our current focus, and `.all` functionality will be coming soon. The latest documentation can be found at https://github.com/amark/gun/wiki/0.2.0-API-and-How-to. Please report any issues via https://github.com/amark/gun/issues. + +GUN is not stable, and therefore should not be trusted in a production environment. +*** ## API @@ -85,20 +73,20 @@ In gun, it can be helpful to think of everything as field/value pairs. For examp "email": "mark@gunDB.io" } ``` -Now, we want to save this object to a key called `usernames/marknadal`. We can do that like this: +Now, we want to save this object to a key called `'usernames/marknadal'`. We can do that like this: ```javascript -gun.set({ +gun.put({ username: "marknadal", name: "Mark Nadal", email: "mark@gunDB.io" }).key('usernames/marknadal'); ``` -We can also pass `set` a callback that can be used to handle errors: +We can also pass `put` a callback that can be used to handle errors: ```javascript -gun.set({ +gun.put({ username: "marknadal", name: "Mark Nadal", email: "mark@gunDB.io" @@ -112,33 +100,40 @@ gun.set({ Once we have some data stored in gun, we need a way to get them out again. Retrieving the data that we just stored would look like this: ```javascript -gun.load('usernames/marknadal').get(function(user){ +gun.get('usernames/marknadal').val(function(user){ console.log(user.name); // Prints `Mark Nadal` to the console }); ``` -Basically, this tells gun to check `usernames/marknadal`, and then return the object it finds associated with it. For more information, including how to save relational or document based data, [check out the wiki](https://github.com/amark/gun/wiki). +Basically, this tells gun to check `'usernames/marknadal'`, and then return the object it finds associated with it. For more information, including how to save relational or document based data, [check out the wiki](https://github.com/amark/gun/wiki). --- -## YOU -We're just getting started, so join us! Being lonely is never any fun, especially when programming. -I want to help you, because my goal is for GUN to be the easiest database ever. -That means if you ever get stuck on something for longer than 5 minutes, -you should talk to me so I can help you solve it. -Your input will then help me improve gun. -We are also really open to contributions! GUN is easy to extend and customize: +## YOU +Being lonely is never any fun, especially when programming. +Our goal is for GUN to be the easiest database ever, +which means if you ever get stuck on something for longer than 5 minutes, +let us know so we can help you. Your input is invaluable, +as it enables us where to refine GUN. So drop us a line in the [![Gitter](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/amark/gun?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)! Or join the [mail list](https://groups.google.com/forum/#!forum/g-u-n). + +Thanks to the following people who have contributed to GUN, via code, issues, or conversation: + +[agborkowski](https://github.com/agborkowski), [alexlafroscia](https://github.com/alexlafroscia), [anubiann00b](https://github.com/anubiann00b), [bromagosa](https://github.com/bromagosa), [coolaj86](https://github.com/coolaj86), [d-oliveros](https://github.com/d-oliveros), [danscan](https://github.com/danscan), [forrestjt](https://github.com/forrestjt), [gedw99](https://github.com/gedw99), [HelloCodeMing](https://github.com/HelloCodeMing), [JosePedroDias](https://github.com/josepedrodias), [onetom](https://github.com/onetom), [ndarilek](https://github.com/ndarilek), [phpnode](https://github.com/phpnode), [riston](https://github.com/riston), [rootsical](https://github.com/rootsical), [rrrene](https://github.com/rrrene), [ssr1ram](https://github.com/ssr1ram), [Xe](https://github.com/Xe), [zot](https://github.com/zot) + +This list of contributors was manually compiled, alphabetically sorted. If we missed you, please submit an issue so we can get you added! + +## Contribute + +Extending GUN or writing modules for it is as simple as: `Gun.on('opt').event(function(gun, opt){ /* Your module here! */ })` -It is also important to us that your database is not a magical black box. -So often our questions get dismissed with "its complicated hard low level stuff, let the experts handle it." -And we do not think that attitude will generate any progress for people. -Instead, we want to make everyone an expert by actually getting really good at explaining the concepts. -So join our community, in the quest of learning cool things and helping yourself and others build awesome technology. +We also want our database to be comprehensible, not some magical black box. +So often database questions get dismissed with "its complicated hard low level stuff, let the experts handle it". +That attitude prevents progress, instead we welcome teaching people and listening to new perspectives. +Join along side us in a quest to learn cool things and help others build awesome technology! - - [![Gitter](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/amark/gun?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) (all chats relating to GUN and development should be here! IRC style) - - Google Group: https://groups.google.com/forum/#!forum/g-u-n (for slower threaded discussions) +We need help on the following roadmap. ## Ahead - ~~Realtime push to the browser~~ @@ -155,9 +150,7 @@ So join our community, in the quest of learning cool things and helping yourself - LRU or some Expiry (so RAM doesn't asplode) - Bug fixes - Data Structures: - - ~~Groups~~ - - Linked Lists - - Collections (hybrid: linked-groups/paginated-lists) + - ~~Sets~~ (Table/Collections, Unordered Lists) - CRDTs - OT - Locking / Strong Consistency (sacrifices Availability) diff --git a/examples/admin/Procfile b/examples/admin/Procfile deleted file mode 100644 index 207d22f8..00000000 --- a/examples/admin/Procfile +++ /dev/null @@ -1 +0,0 @@ -web: node app.js \ No newline at end of file diff --git a/examples/admin/app.js b/examples/admin/app.js deleted file mode 100644 index 912a1bf1..00000000 --- a/examples/admin/app.js +++ /dev/null @@ -1,18 +0,0 @@ -console.log("If modules not found, run `npm install` in example/admin folder!"); // git subtree push -P examples/admin heroku master -var port = process.env.OPENSHIFT_NODEJS_PORT || process.env.VCAP_APP_PORT || process.env.PORT || 8888; -var express = require('express'); -var bodyParser = require('body-parser'); -var app = express(); -var Gun = require('gun'); -var gun = Gun({ - s3: (process.env.NODE_ENV === 'production')? null : require('../../test/shotgun') // replace this with your own keys! -}); -app.use(gun.server) - .use(express.static(__dirname)) -app.listen(port); - -console.log('Express started on port ' + port + ' with /gun'); -gun.load('blob/data').blank(function(){ // in case there is no data on this key - console.log("blankety blank"); - gun.set({ hello: "world", from: "Mark Nadal",_:{'#':'0DFXd0ckJ9cXGczusNf1ovrE'}}).key('blob/data'); // save some sample data -}); \ No newline at end of file diff --git a/examples/admin/index.html b/examples/admin/index.html deleted file mode 100644 index 74c1316a..00000000 --- a/examples/admin/index.html +++ /dev/null @@ -1,85 +0,0 @@ - - - - - - - - - - - -

Admin Data Editor

- This is a live view of your JSON data, you can edit it in realtime or add new key/values. - - - - - \ No newline at end of file diff --git a/examples/admin/old_gun_for_slinger.js b/examples/admin/old_gun_for_slinger.js deleted file mode 100644 index 70a662e4..00000000 --- a/examples/admin/old_gun_for_slinger.js +++ /dev/null @@ -1,1062 +0,0 @@ -;(function(){ - function Gun(opt){ - var gun = this; - if(!Gun.is(gun)){ - return new Gun(opt); - } - gun.opt(opt); - } - Gun._ = { - soul: '#' - ,meta: '_' - ,HAM: '>' - } - ;(function(Gun){ - Gun.is = function(gun){ return (gun instanceof Gun)? true : false } - Gun.version = 0.8; - Gun.union = function(graph, prime){ - var context = Gun.shot(); - context.nodes = {}; - context('done');context('change'); - Gun.obj.map(prime, function(node, soul){ - var vertex = graph[soul], env; - if(!vertex){ // disjoint - context.nodes[node._[Gun._.soul]] = graph[node._[Gun._.soul]] = node; - context('change').fire(node); - return; - } - env = Gun.HAM(vertex, node, function(current, field, deltaValue){ - if(!current){ return } - var change = {}; - current[field] = change[field] = deltaValue; // current and vertex are the same - current._[Gun._.HAM][field] = node._[Gun._.HAM][field]; - change._ = current._; - context.nodes[change._[Gun._.soul]] = change; - context('change').fire(change); - }).upper(function(c){ - context.err = c.err; - context.up -= 1; - if(!context.up){ - context('done').fire(context.err, context); - } - }); - context.up += env.up; - }); - if(!context.up){ - context('done').fire(context.err, context); - } - return context; - } - Gun.HAM = function(current, delta, each){ // HAM only handles primitives values, all other data structures need to be built ontop and reduce to HAM. - function HAM(machineState, incomingState, currentState, incomingValue, currentValue){ // TODO: Lester's comments on roll backs could be vulnerable to divergence, investigate! - if(machineState < incomingState){ - // the incoming value is outside the boundary of the machine's state, it must be reprocessed in another state. - return {amnesiaQuarantine: true}; - } - if(incomingState < currentState){ - // the incoming value is within the boundary of the machine's state, but not within the range. - return {quarantineState: true}; - } - if(currentState < incomingState){ - // the incoming value is within both the boundary and the range of the machine's state. - return {converge: true, incoming: true}; - } - if(incomingState === currentState){ - if(incomingValue === currentValue){ // Note: while these are practically the same, the deltas could be technically different - return {state: true}; - } - /* - The following is a naive implementation, but will always work. - Never change it unless you have specific needs that absolutely require it. - If changed, your data will diverge unless you guarantee every peer's algorithm has also been changed to be the same. - As a result, it is highly discouraged to modify despite the fact that it is naive, - because convergence (data integrity) is generally more important. - Any difference in this algorithm must be given a new and different name. - */ - if(String(incomingValue) < String(currentValue)){ // String only works on primitive values! - return {converge: true, current: true}; - } - if(String(currentValue) < String(incomingValue)){ // String only works on primitive values! - return {converge: true, incoming: true}; - } - } - return {err: "you have not properly handled recursion through your data or filtered it as JSON"}; - } - var context = Gun.shot(); - context.HAM = {}; - context.states = {}; - context.states.delta = delta._[Gun._.HAM]; - context.states.current = current._[Gun._.HAM] = current._[Gun._.HAM] || {}; - context('lower');context('upper');context.up = context.up || 0; - Gun.obj.map(delta, function update(deltaValue, field){ - if(field === Gun._.meta){ return } - if(!Gun.obj.has(current, field)){ // does not need to be applied through HAM - each.call({incoming: true, converge: true}, current, field, deltaValue); - return; - } - var serverState = Gun.time.is(); - // add more checks? - var state = HAM(serverState, context.states.delta[field], context.states.current[field], deltaValue, current[field]); - //console.log("HAM:", field, deltaValue, context.states.delta[field], context.states.current[field], 'the', state, (context.states.delta[field] - serverState)); - if(state.err){ - Gun.log(".!HYPOTHETICAL AMNESIA MACHINE ERR!.", state.err); - return; - } - if(state.state || state.quarantineState || state.current){ - context('lower').fire(context, state, current, field, deltaValue); - return; - } - if(state.incoming){ - each.call(state, current, field, deltaValue); - return; - } - if(state.amnesiaQuarantine){ - context.up += 1; - Gun.schedule(context.states.delta[field], function(){ - update(deltaValue, field); - context.up -= 1; - context('upper').fire(context, state, current, field, deltaValue); - }); - } - }); - if(!context.up){ - context('upper').fire(context, {}); - } - return context; - } - Gun.roulette = function(l, c){ - var gun = Gun.is(this)? this : {}; - if(gun._ && gun.__.opt && gun.__.opt.uuid){ - if(Gun.fns.is(gun.__.opt.uuid)){ - return gun.__.opt.uuid(l, c); - } - l = l || gun.__.opt.uuid.length; - } - return Gun.text.random(l, c); - } - Gun.log = function(a, b, c, d, e, f){ - //console.log(a, b, c, d, e, f); - //console.log.apply(console, arguments); - } - }(Gun)); - ;(function(Chain){ - Chain.opt = function(opt, stun){ // idempotently update or set options - var gun = this; - gun._ = gun._ || {}; - gun.__ = gun.__ || {}; - gun.shot = Gun.shot(); - gun.shot('then'); - gun.shot('err'); - if(!opt){ return gun } - gun.__.opt = gun.__.opt || {}; - gun.__.keys = gun.__.keys || {}; - gun.__.graph = gun.__.graph || {}; - gun.__.on = gun.__.on || Gun.on.create(); - if(Gun.text.is(opt)){ opt = {peers: opt} } - if(Gun.list.is(opt)){ opt = {peers: opt} } - if(Gun.text.is(opt.peers)){ opt.peers = [opt.peers] } - if(Gun.list.is(opt.peers)){ opt.peers = Gun.obj.map(opt.peers, function(n,f,m){ m(n,{}) }) } - gun.__.opt.peers = opt.peers || gun.__.opt.peers || {}; - gun.__.opt.uuid = opt.uuid || gun.__.opt.uuid || {}; - gun.__.opt.hooks = gun.__.opt.hooks || {}; - Gun.obj.map(opt.hooks, function(h, f){ - if(!Gun.fns.is(h)){ return } - gun.__.opt.hooks[f] = h; - }); - if(!stun){ Gun.on('opt').emit(gun, opt) } - return gun; - } - Chain.chain = function(from){ - var gun = Gun(); - from = from || this; - gun.back = from; - gun.__ = from.__; - gun._ = {}; - Gun.obj.map(from._, function(val, field){ - gun._[field] = val; - }); - return gun; - } - Chain.load = function(key, cb, opt){ - var gun = this.chain(); - cb = cb || function(){}; - gun.shot.then(function(){ cb.apply(gun, arguments) }); - cb.soul = (key||{})[Gun._.soul]; - if(cb.soul){ - cb.node = gun.__.graph[cb.soul]; - } else { - gun._.key = key; - cb.node = gun.__.keys[key]; - } - if(cb.node){ // set this to the current node, too! - Gun.log("from gun"); // remember to do all the same stack stuff here also! - var freeze = Gun.obj.copy(gun._.node = cb.node); - gun.shot('then').fire(freeze); // freeze now even though internals use this? OK for now. - return gun; // TODO: BUG: This needs to react the same as below! - } - cb.fn = function(){} - // missing: hear shots! - if(Gun.fns.is(gun.__.opt.hooks.load)){ - gun.__.opt.hooks.load(key, function(err, data){ - gun._.loaded = (gun._.loaded || 0) + 1; // TODO: loading should be idempotent even if we got an err or no data - if(err){ return (gun._.dud||cb.fn)(err) } - if(!data){ return (gun._.blank||cb.fn)() } - var context = gun.union(data); // safely transform the data - if(context.err){ return (gun._.dud||cb.fn)(context.err) } - gun._.node = gun.__.graph[data._[Gun._.soul]]; // don't wait for the union to be done because we want the immediate state not the intended state. - if(!cb.soul){ gun.__.keys[key] = gun._.node } - var freeze = Gun.obj.copy(gun._.node); - gun.shot('then').fire(freeze); // freeze now even though internals use this? OK for now. - }, opt); - } else { - Gun.log("Warning! You have no persistence layer to load from!"); - } - return gun; - } - Chain.key = function(key, cb){ - var gun = this; - gun.shot.then(function(){ - Gun.log("make key", key); - cb = cb || function(){}; - cb.node = gun.__.keys[key] = gun._.node; - if(!cb.node){ return gun } - if(Gun.fns.is(gun.__.opt.hooks.key)){ - gun.__.opt.hooks.key(key, cb.node._[Gun._.soul], function(err, data){ - Gun.log("key made", key); - if(err){ return cb(err) } - return cb(null); - }); - } else { - Gun.log("Warning! You have no key hook!"); - } - }); - if(!gun.back){ gun.shot('then').fire() } - return gun; - } - /* - how many different ways can we get something? - Find via a singular path - .path('blah').get(blah); - Find via multiple paths with the callback getting called many times - .path('foo', 'bar').get(foorOrBar); - Find via multiple paths with the callback getting called once with matching arguments - .path('foo', 'bar').get(foo, bar) - Find via multiple paths with the result aggregated into an object of pre-given fields - .path('foo', 'bar').get({foo: foo, bar: bar}) || .path({a: 'foo', b: 'bar'}).get({a: foo, b: bar}) - Find via multiple paths where the fields and values must match - .path({foo: val, bar: val}).get({}) - */ - Chain.path = function(path){ // The focal point follows the path - var gun = this.chain(); - path = (path || '').split('.'); - gun.back.shot.then(function trace(node){ // should handle blank and err! Err already handled? - //console.log("shot path", path, node); - gun.field = null; - gun._.node = node; - if(!path.length){ // if the path resolves to another node, we finish here - return gun.shot('then').fire(node); // already frozen from loaded. - } - var field = path.shift() - , val = node[field]; - gun.field = field; - if(Gun.ify.is.soul(val)){ // we might end on a link, so we must resolve - return gun.load(val).shot.then(trace); - } else - if(path.length){ // we cannot go any further, despite the fact there is more path, which means the thing we wanted does not exist - gun.shot('then').fire(); - } else { // we are done, and this should be the value we wanted. - gun.shot('then').fire(val); // primitive values are passed as copies in JS. - } - }); - // if(!gun.back){ gun.shot('then').fire() } // replace below with this? maybe??? - if(gun.back && gun.back._ && gun.back._.loaded){ - gun._.node = gun.back._.node; - gun.back.shot('then').fire(gun.back._.node); - } - return gun; - } - Chain.get = function(cb){ - var gun = this; - gun.shot.then(function(val){ - cb.call(gun, val); // frozen from done. - gun.__.on(gun._.node._[Gun._.soul]).event(function(delta){ - if(!delta){ return } - if(!gun.field){ - cb.call(gun, Gun.obj.copy(gun._.node)); - return; - } - if(Gun.obj.has(delta, gun.field)){ - cb.call(gun, delta[gun.field]); - } - }) - }); - return gun; - } - /* - ACID compliant, unfortunately the vocabulary is vague, as such the following is an explicit definition: - A - Atomic, if you set a full node, or nodes of nodes, if any value is in error then nothing will be set. - If you want sets to be independent of each other, you need to set each piece of the data individually. - C - Consistency, if you use any reserved symbols or similar, the operation will be rejected as it could lead to an invalid read and thus an invalid state. - I - Isolation, the conflict resolution algorithm guarantees idempotent transactions, across every peer, regardless of any partition, - including a peer acting by itself or one having been disconnected from the network. - D - Durability, if the acknowledgement receipt is received, then the state at which the final persistence hook was called on is guaranteed to have been written. - The live state at point of confirmation may or may not be different than when it was called. - If this causes any application-level concern, it can compare against the live data by immediately reading it, or accessing the logs if enabled. - */ - Chain.set = function(val, cb, opt){ // TODO: need to turn deserializer into a trampolining function so stackoverflow doesn't happen. - opt = opt || {}; - var gun = this, set; - gun.shot.then(function(){ - if(gun.field){ // a field cannot be 0! - set = {}; // in case we are doing a set on a field, not on a node - set[gun.field] = val; // we create a blank node with the field/value to be set - val = set; - } // TODO: should be able to handle val being a relation or a gun context or a gun promise. - val._ = Gun.ify.soul.call(gun, {}, gun._.node); // and then set their souls to be the same that way they will merge correctly for us during the union! - cb = Gun.fns.is(cb)? cb : function(){}; - set = Gun.ify.call(gun, val); - cb.root = set.root; - if(set.err){ return cb(set.err), gun } - set = Gun.ify.state(set.nodes, Gun.time.is()); // set time state on nodes? - if(set.err){ return cb(set.err), gun } - Gun.union(gun.__.graph, set.nodes); // while this maybe should return a list of the nodes that were changed, we want to send the actual delta - gun._.node = gun.__.graph[cb.root._[Gun._.soul]] || cb.root; - // TODO? ^ Maybe BUG! if val is a new node on a field, _.node should now be that! Or will that happen automatically? - if(Gun.fns.is(gun.__.opt.hooks.set)){ - gun.__.opt.hooks.set(set.nodes, function(err, data){ // now iterate through those nodes to S3 and get a callback once all are saved - //Gun.log("gun set hook callback called"); - if(err){ return cb(err) } - return cb(null); - }); - } else { - Gun.log("Warning! You have no persistence layer to save to!"); - } - }); - if(!gun.back){ gun.shot('then').fire() } - return gun; - } - Chain.union = function(prime, cb){ - var tmp, gun = this, context = Gun.shot(); - context.nodes = {}; - cb = cb || function(){} - if(!prime){ - context.err = {err: "No data to merge!"}; - } else - if(prime._ && prime._[Gun._.soul]){ - tmp = {}; - tmp[prime._[Gun._.soul]] = prime; - prime = tmp; - } - if(!gun || context.err){ - cb(context.err = context.err || {err: "No gun instance!", corrupt: true}, context); - return context; - } - Gun.obj.map(prime, function(node){ // map over the prime graph, to get each node that has been modified - var set = Gun.ify.call(gun, node); - if(set.err){ return context.err = set.err } // check to see if the node is valid - Gun.obj.map(set.nodes, function(node, soul){ // if so, map over it, and any other nodes that were deserialized from it - context.nodes[soul] = node; // into a valid context we'll actually do a union on. - }); - }); - if(context.err){ return cb(context.err, context), context } // if any errors happened in the previous steps, then fail. - Gun.union(gun.__.graph, context.nodes).done(function(err, env){ // now merge prime into the graph - context.err = err || env.err; - cb(context.err, context || {}); - }).change(function(delta){ - if(!delta || !delta._ || !delta._[Gun._.soul]){ return } - gun.__.on(delta._[Gun._.soul]).emit(Gun.obj.copy(delta)); // this is in reaction to HAM - }); - return context; - } - Chain.match = function(){ // same as path, except using objects - return this; - } - Chain.blank = function(blank){ - this._.blank = Gun.fns.is(blank)? blank : function(){}; - return this; - } - Chain.dud = function(dud){ - this._.dud = Gun.fns.is(dud)? dud : function(){}; - return this; - } - }(Gun.chain = Gun.prototype)); - ;(function(Util){ - Util.fns = {}; - Util.fns.is = function(fn){ return (fn instanceof Function)? true : false } - Util.bi = {}; - Util.bi.is = function(b){ return (b instanceof Boolean || typeof b == 'boolean')? true : false } - Util.num = {}; - Util.num.is = function(n){ - return ((n===0)? true : (!isNaN(n) && !Util.bi.is(n) && !Util.list.is(n) && !Util.text.is(n))? true : false ); - } - Util.text = {}; - Util.text.is = function(t){ return typeof t == 'string'? true : false } - Util.text.ify = function(t){ - if(Util.text.is(t)){ return t } - if(JSON){ return JSON.stringify(t) } - return (t && t.toString)? t.toString() : t; - } - Util.text.random = function(l, c){ - var s = ''; - l = l || 24; // you are not going to make a 0 length random number, so no need to check type - c = c || '0123456789ABCDEFGHIJKLMNOPQRSTUVWXZabcdefghiklmnopqrstuvwxyz'; - while(l > 0){ s += c.charAt(Math.floor(Math.random() * c.length)); l-- } - return s; - } - Util.list = {}; - Util.list.is = function(l){ return (l instanceof Array)? true : false } - Util.list.slit = Array.prototype.slice; - Util.list.sort = function(k){ // creates a new sort function based off some field - return function(A,B){ - if(!A || !B){ return 0 } A = A[k]; B = B[k]; - if(A < B){ return -1 }else if(A > B){ return 1 } - else { return 0 } - } - } - Util.list.map = function(l, c, _){ return Util.obj.map(l, c, _) } - Util.list.index = 1; // change this to 0 if you want non-logical, non-mathematical, non-matrix, non-convenient array notation - Util.obj = {}; - Util.obj.is = function(o){ return (o instanceof Object && !Util.list.is(o) && !Util.fns.is(o))? true : false } - Util.obj.del = function(o, k){ - if(!o){ return } - o[k] = null; - delete o[k]; - return true; - } - Util.obj.ify = function(o){ - if(Util.obj.is(o)){ return o } - try{o = JSON.parse(o); - }catch(e){o={}}; - return o; - } - Util.obj.copy = function(o){ // because http://web.archive.org/web/20140328224025/http://jsperf.com/cloning-an-object/2 - return !o? o : JSON.parse(JSON.stringify(o)); // is shockingly faster than anything else, and our data has to be a subset of JSON anyways! - } - Util.obj.has = function(o, t){ return Object.prototype.hasOwnProperty.call(o, t) } - Util.obj.map = function(l, c, _){ - var u, i = 0, ii = 0, x, r, rr, f = Util.fns.is(c), - t = function(k,v){ - if(v !== u){ - rr = rr || {}; - rr[k] = v; - return; - } rr = rr || []; - rr.push(k); - }; - if(Util.list.is(l)){ - x = l.length; - for(;i < x; i++){ - ii = (i + Util.list.index); - if(f){ - r = _? c.call(_, l[i], ii, t) : c(l[i], ii, t); - if(r !== u){ return r } - } else { - //if(Util.test.is(c,l[i])){ return ii } // should implement deep equality testing! - if(c === l[i]){ return ii } // use this for now - } - } - } else { - for(i in l){ - if(f){ - if(Util.obj.has(l,i)){ - r = _? c.call(_, l[i], i, t) : c(l[i], i, t); - if(r !== u){ return r } - } - } else { - //if(a.test.is(c,l[i])){ return i } // should implement deep equality testing! - if(c === l[i]){ return i } - } - } - } - return f? rr : Util.list.index? 0 : -1; - } - Util.time = {}; - Util.time.is = function(t){ return t? t instanceof Date : (+new Date().getTime()) } - }(Gun)); - ;Gun.shot=(function(){ - // I hate the idea of using setTimeouts in my code to do callbacks (promises and sorts) - // as there is no way to guarantee any type of state integrity or the completion of callback. - // However, I have fallen. HAM is suppose to assure side effect free safety of unknown states. - var setImmediate = setImmediate || function(cb){setTimeout(cb,0)} - function Flow(){ - var chain = new Flow.chain(); - return chain.$ = function(where){ - (chain._ = chain._ || {})[where] = chain._[where] || []; - chain.$[where] = chain.$[where] || function(fn){ - (chain._[where]||[]).push(fn); - return chain.$; - } - chain.where = where; - return chain; - } - } - Flow.is = function(flow){ return (Flow instanceof flow)? true : false } - ;Flow.chain=(function(){ - function Chain(){ - if(!(this instanceof Chain)){ - return new Chain(); - } - } - Chain.chain = Chain.prototype; - Chain.chain.pipe = function(a,s,d,f){ - var me = this - , where = me.where - , args = Array.prototype.slice.call(arguments); - setImmediate(function(){ - if(!me || !me._ || !me._[where]){ return } - while(0 < me._[where].length){ - (me._[where].shift()||function(){}).apply(me, args); - } - // do a done? That would be nice. :) - }); - return me; - } - return Chain; - }()); - return Flow; - }());Gun.shot.chain.chain.fire=Gun.shot.chain.chain.pipe; - ;Gun.on=(function(){ - function On(where){ - if(where){ - return (On.event = On.event || On.create())(where); - } - return On.create(); - } - On.is = function(on){ return (On instanceof on)? true : false } - On.create = function(){ - var chain = new On.chain(); - return chain.$ = function(where){ - chain.where = where; - return chain; - } - } - On.sort = Gun.list.sort('i'); - ;On.chain=(function(){ - function Chain(){ - if(!(this instanceof Chain)){ - return new Chain(); - } - } - Chain.chain = Chain.prototype; - Chain.chain.emit = function(what){ - var me = this - , where = me.where - , args = arguments - , on = (me._ = me._ || {})[where] = me._[where] || []; - if(!(me._[where] = Gun.list.map(on, function(hear, i, map){ - if(!hear || !hear.as){ return } - map(hear); - hear.as.apply(hear, args); - }))){ Gun.obj.del(on, where) } - } - Chain.chain.event = function(as, i){ - if(!as){ return } - var me = this - , where = me.where - , args = arguments - , on = (me._ = me._ || {})[where] = me._[where] || [] - , e = {as: as, i: i || 0, off: function(){ return !(e.as = false) }}; - return on.push(e), on.sort(On.sort), e; - } - Chain.chain.once = function(as, i){ - var me = this - , once = function(){ - this.off(); - as.apply(this, arguments) - } - return me.event(once, i) - } - return Chain; - }()); - return On; - }()); - ;(function(schedule){ // maybe use lru-cache - schedule.waiting = []; - schedule.soonest = Infinity; - schedule.sort = Gun.list.sort('when'); - schedule.set = function(future){ - var now = Gun.time.is(); - future = (future <= now)? 0 : (future - now); - clearTimeout(schedule.id); - schedule.id = setTimeout(schedule.check, future); - } - schedule.check = function(){ - var now = Gun.time.is(), soonest = Infinity; - schedule.waiting.sort(schedule.sort); - schedule.waiting = Gun.list.map(schedule.waiting, function(wait, i, map){ - if(!wait){ return } - if(wait.when <= now){ - if(Gun.fns.is(wait.event)){ - wait.event(); - } - } else { - soonest = (soonest < wait.when)? soonest : wait.when; - map(wait); - } - }) || []; - schedule.set(soonest); - } - Gun.schedule = function(state, cb){ - schedule.waiting.push({when: state, event: cb}); - if(schedule.soonest < state){ return } - schedule.set(state); - } - }({})); - ;(function(Serializer){ - Gun.ify = function(data){ // TODO: BUG: Modify lists to include HAM state - var gun = Gun.is(this)? this : {} - , context = { - nodes: {} - ,seen: [] - ,_seen: [] - }, nothing; - function ify(data, context, sub){ - sub = sub || {}; - sub.path = sub.path || ''; - context = context || {}; - context.nodes = context.nodes || {}; - if((sub.simple = Gun.ify.is(data)) && !(sub._ && Gun.text.is(sub.simple))){ - return data; - } else - if(Gun.obj.is(data)){ - var value = {}, symbol = {}, seen - , err = {err: "Metadata does not support external or circular references at " + sub.path, meta: true}; - context.root = context.root || value; - if(seen = ify.seen(context._seen, data)){ - //Gun.log("seen in _", sub._, sub.path, data); - context.err = err; - return; - } else - if(seen = ify.seen(context.seen, data)){ - //Gun.log("seen in data", sub._, sub.path, data); - if(sub._){ - context.err = err; - return; - } - symbol = Gun.ify.soul.call(gun, symbol, seen); - return symbol; - } else { - //Gun.log("seen nowhere", sub._, sub.path, data); - if(sub._){ - context.seen.push({data: data, node: value}); - } else { - value._ = Gun.ify.soul.call(gun, {}, data); - context.seen.push({data: data, node: value}); - context.nodes[value._[Gun._.soul]] = value; - } - } - Gun.obj.map(data, function(val, field){ - var subs = {path: sub.path + field + '.', _: sub._ || (field == Gun._.meta)? true : false }; - val = ify(val, context, subs); - //Gun.log('>>>>', sub.path + field, 'is', val); - if(context.err){ return true } - if(nothing === val){ return } - // TODO: check field validity - value[field] = val; - }); - if(sub._){ return value } - if(!value._ || !value._[Gun._.soul]){ return } - symbol[Gun._.soul] = value._[Gun._.soul]; - return symbol; - } else - if(Gun.list.is(data)){ - var unique = {}, edges - , err = {err: "Arrays cause data corruption at " + sub.path, array: true} - edges = Gun.list.map(data, function(val, i, map){ - val = ify(val, context, sub); - if(context.err){ return true } - if(!Gun.obj.is(val)){ - context.err = err; - return true; - } - return Gun.obj.map(val, function(soul, field){ - if(field !== Gun._.soul){ - context.err = err; - return true; - } - if(unique[soul]){ return } - unique[soul] = 1; - map(val); - }); - }); - if(context.err){ return } - return edges; - } else { - context.err = {err: "Data type not supported at " + sub.path, invalid: true}; - } - } - ify.seen = function(seen, data){ - // unfortunately, using seen[data] = true will cause false-positives for data's children - return Gun.list.map(seen, function(check){ - if(check.data === data){ return check.node } - }); - } - ify(data, context); - return context; - } - Gun.ify.state = function(nodes, now){ - var context = {}; - context.nodes = nodes; - context.now = now = (now === 0)? now : now || Gun.time.is(); - Gun.obj.map(context.nodes, function(node, soul){ - if(!node || !soul || !node._ || !node._[Gun._.soul] || node._[Gun._.soul] !== soul){ - return context.err = {err: "There is a corruption of nodes and or their souls", corrupt: true}; - } - var states = node._[Gun._.HAM] = node._[Gun._.HAM] || {}; - Gun.obj.map(node, function(val, field){ - if(field == Gun._.meta){ return } - val = states[field]; - states[field] = (val === 0)? val : val || now; - }); - }); - return context; - } - Gun.ify.soul = function(to, from){ - var gun = this; - to = to || {}; - if(Gun.ify.soul.is(from)){ - to[Gun._.soul] = from._[Gun._.soul]; - return to; - } - to[Gun._.soul] = Gun.roulette.call(gun); - return to; - } - Gun.ify.soul.is = function(o){ - if(o && o._ && o._[Gun._.soul]){ - return true; - } - } - Gun.ify.is = function(v){ // null, binary, number (!Infinity), text, or a rel. - if(v === null){ return true } // deletes - if(v === Infinity){ return false } // we want this to be, but JSON does not support it, sad face. - if(Gun.bi.is(v) - || Gun.num.is(v) - || Gun.text.is(v)){ - return true; // simple values - } - var yes; - if(yes = Gun.ify.is.soul(v)){ - return yes; - } - return false; - } - Gun.ify.is.soul = function(v){ - if(Gun.obj.is(v)){ - var yes; - Gun.obj.map(v, function(soul, field){ - if(yes){ return yes = false } - if(field === Gun._.soul && Gun.text.is(soul)){ - yes = soul; - } - }); - if(yes){ - return yes; - } - } - return false; - } - }()); - if(typeof window !== "undefined"){ - window.Gun = Gun; - } else { - module.exports = Gun; - } -}({})); - -;(function(tab){ - if(!this.Gun){ return } - if(!window.JSON){ Gun.log("Include JSON first: ajax.cdnjs.com/ajax/libs/json2/20110223/json2.js") } // for old IE use - Gun.on('opt').event(function(gun, opt){ - tab.server = tab.server || function(req, res, next){ - - } - window.tab = tab; // window.XMLHttpRequest = null; // for debugging purposes - (function(){ - tab.store = {}; - var store = window.localStorage || {setItem: function(){}, removeItem: function(){}, getItem: function(){}}; - tab.store.set = function(key, val){console.log('setting', key); return store.setItem(key, Gun.text.ify(val)) } - tab.store.get = function(key){ return Gun.obj.ify(store.getItem(key)) } - tab.store.del = function(key){ return store.removeItem(key) } - }()); - tab.load = tab.load || function(key, cb, opt){ - if(!key){ return } - cb = cb || function(){}; - opt = opt || {}; - if(key[Gun._.soul]){ - key = '_' + tab.query(key); - } - Gun.obj.map(gun.__.opt.peers, function(peer, url){ - tab.ajax(url + '/' + key, null, function(err, reply){ - console.log('via', url, key, reply); - if(!reply){ return } // handle reconnect? - if(reply.body && reply.body.err){ - cb(reply.body.err); - } else { - cb(null, reply.body); - } - - (function(){ - tab.subscribe.sub = (reply.headers || {})['gun-sub'] || tab.subscribe.sub; - //console.log("We are sub", tab.subscribe.sub); - var data = reply.body; - if(!data || !data._){ return } - tab.subscribe(data._[Gun._.soul]); - }()); - }, {headers: {'Gun-Sub': tab.subscribe.sub || ''}, header: {'Gun-Sub': 1}}); - }); - } - tab.url = function(nodes){ - return; - console.log("urlify delta", nodes); - var s = '' - , uri = encodeURIComponent; - Gun.obj.map(nodes, function(delta, soul){ - var ham; - if(!delta || !delta._ || !(ham = delta._[Gun._.HAM])){ return } - s += uri('#') + '=' + uri(soul) + '&'; - Gun.obj.map(delta, function(val, field){ - if(field === Gun._.meta){ return } - s += uri(field) + '=' + uri(Gun.text.ify(val)) + uri('>') + uri(ham[field]) + '&'; - }) - }); - console.log(s); - return s; - } - tab.set = tab.set || function(nodes, cb){ - cb = cb || function(){}; - // TODO: batch and throttle later. - //tab.store.set(cb.id = 'send/' + Gun.text.random(), nodes); - //tab.url(nodes); - Gun.obj.map(gun.__.opt.peers, function(peer, url){ - tab.ajax(url, nodes, function respond(err, reply, id){ - var body = reply && reply.body; - respond.id = respond.id || cb.id; - Gun.obj.del(tab.set.defer, id); // handle err with a retry? Or make a system auto-do it? - if(!body){ return } - if(body.defer){ - //console.log("deferring post", body.defer); - tab.set.defer[body.defer] = respond; - } - if(body.reply){ - respond(null, {headers: reply.headers, body: body.reply }); - } - if(body.refed){ - console.log("-------post-reply-all--------->", 1 || reply, err); - Gun.obj.map(body.refed, function(r, id){ - var cb; - if(cb = tab.set.defer[id]){ - cb(null, {headers: reply.headers, body: r}, id); - } - }); - // TODO: should be able to do some type of "checksum" that every request cleared, and if not, figure out what is wrong/wait for finish. - return; - } - if(body.reply || body.defer || body.refed){ return } - //tab.store.del(respond.id); - }, {headers: {'Gun-Sub': tab.subscribe.sub || ''}}); - }); - Gun.obj.map(nodes, function(node, soul){ - gun.__.on(soul).emit(node, true); // should we emit difference between local and not? - }); - } - tab.set.defer = {}; - tab.subscribe = function(soul){ // TODO: BUG!!! ERROR! Handle disconnection (onerror)!!!! - tab.subscribe.to = tab.subscribe.to || {}; - if(soul){ - tab.subscribe.to[soul] = 1; - } - var opt = { - header: {'Gun-Sub': 1}, - headers: { - 'Gun-Sub': tab.subscribe.sub || '' - } - }, query = tab.subscribe.sub? '' : tab.query(tab.subscribe.to); - console.log("subscribing poll", tab.subscribe.sub); - Gun.obj.map(gun.__.opt.peers, function(peer, url){ - tab.ajax(url + query, null, function(err, reply){ - if(err || !reply || !reply.body || reply.body.err){ // not interested in any null/0/''/undefined values - //console.log(err, reply); - return; - } - console.log("poll", 1 || reply); - tab.subscribe.poll(); - if(reply.headers){ - tab.subscribe.sub = reply.headers['gun-sub'] || tab.subscribe.sub; - } - if(!reply.body){ return } // do anything? - gun.union(reply.body); // safely transform data - }, opt); - }); - } - tab.subscribe.poll = function(){ - clearTimeout(tab.subscribe.poll.id); - tab.subscribe.poll.id = setTimeout(tab.subscribe, 1); //1000 * 10); // should enable some server-side control of this. - } - tab.query = function(params){ - var s = '?' - , uri = encodeURIComponent; - Gun.obj.map(params, function(val, field){ - s += uri(field) + '=' + uri(val || '') + '&'; - }); - return s; - } - tab.ajax = (function(){ - function ajax(url, data, cb, opt){ - var u; - opt = opt || {}; - opt.header = opt.header || {}; - opt.header["Content-Type"] = 1; - opt.headers = opt.headers || {}; - if(data === u || data === null){ - data = u; - } else { - try{data = JSON.stringify(data); - opt.headers["Content-Type"] = "application/json"; - }catch(e){} - } - opt.method = opt.method || (data? 'POST' : 'GET'); - var xhr = ajax.xhr() || ajax.jsonp() // TODO: BUG: JSONP push is working, but not post - , clean = function(){ - if(!xhr){ return } - xhr.onreadystatechange = xhr.onerror = null; - try{xhr.abort(); - }catch(e){} - xhr = null; - } - xhr.onerror = function(){ - if(cb){ - cb({err: err || 'Unknown error.', status: xhr.status }); - } - clean(xhr.status === 200 ? 'network' : 'permanent'); - }; - xhr.onreadystatechange = function(){ - if(!xhr){ return } - var reply, status; - try{reply = xhr.responseText; - status = xhr.status; - }catch(e){} - if(status === 1223){ status = 204 } - if(xhr.readyState === 3){ - if(reply && 0 < reply.length){ - opt.ondata(status, reply); - } - } else - if(xhr.readyState === 4){ - opt.ondata(status, reply, true); - clean(status === 200? 'network' : 'permanent'); - } - }; - opt.ondata = opt.ondata || function(status, chunk, end){ - if(status !== 200){ return } - try{ajax.each(opt.header, function(val, i){ - (xhr.responseHeader = xhr.responseHeader||{})[i.toLowerCase()] = xhr.getResponseHeader(i); - }); - }catch(e){} - var data, buf, pos = 1; - while(pos || end){ // in order to end - if(u !== data){ // we need at least one loop - opt.onload({ - headers: xhr.responseHeader || {} - ,body: data - }); - end = false; // now both pos and end will be false - } - if(ajax.string(chunk)){ - buf = chunk.slice(xhr.index = xhr.index || 0); - pos = buf.indexOf('\n') + 1; - data = pos? buf.slice(0, pos - 1) : buf; - xhr.index += pos; - } else { - data = chunk; - pos = 0; - } - } - } - opt.onload = opt.onload || function(reply){ - if(!reply){ return } - if( reply.headers && ("application/json" === reply.headers["content-type"])){ - var body; - try{body = JSON.parse(reply.body); - }catch(e){body = reply.body} - reply.body = body; - } - if(cb){ - cb(null, reply); - } - } - if(opt.cookies || opt.credentials || opt.withCredentials){ - xhr.withCredentials = true; - } - opt.headers["X-Requested-With"] = xhr.transport || "XMLHttpRequest"; - try{xhr.open(opt.method, url, true); - }catch(e){ return xhr.onerror("Open failed.") } - if(opt.headers){ - try{ajax.each(opt.headers, function(val, i){ - xhr.setRequestHeader(i, val); - }); - }catch(e){ return xhr.onerror("Invalid headers.") } - } - try{xhr.send(data); - }catch(e){ return xhr.onerror("Failed to send request.") } - } - ajax.xhr = function(xhr){ - return (window.XMLHttpRequest && "withCredentials" in (xhr = new XMLHttpRequest()))? xhr : null; - } - ajax.jsonp = function(xhr){ - xhr = {}; - xhr.transport = "jsonp"; - xhr.open = function(method, url){ - xhr.url = url; - } - xhr.send = function(){ - xhr.url += ((xhr.url.indexOf('?') + 1)? '&' : '?') + 'jsonp=' + xhr.js.id; - ajax.each(xhr.headers, function(val, i){ - xhr.url += '&' + encodeURIComponent(i) + "=" + encodeURIComponent(val); - }); - xhr.js.src = xhr.url = xhr.url.replace(/%20/g, "+"); - document.getElementsByTagName('head')[0].appendChild(xhr.js); - } - xhr.setRequestHeader = function(i, val){ - (xhr.headers = xhr.headers||{})[i] = val; - } - xhr.getResponseHeader = function(i){ return (xhr.responseHeaders||{})[i] } - xhr.js = document.createElement('script'); - window[xhr.js.id = 'P'+Math.floor((Math.random()*65535)+1)] = function(reply){ - xhr.status = 200; - if(reply.chunks && reply.chunks.length){ - xhr.readyState = 3 - while(0 < reply.chunks.length){ - xhr.responseText = reply.chunks.shift(); - xhr.onreadystatechange(); - } - } - xhr.responseHeaders = reply.headers || {}; - xhr.readyState = 4; - xhr.responseText = reply.body; - xhr.onreadystatechange(); - xhr.id = xhr.js.id; - xhr.js.parentNode.removeChild(xhr.js); - window[xhr.id] = null; - try{delete window[xhr.id]; - }catch(e){} - - } - xhr.abort = function(){} // clean up? - xhr.js.async = true; - return xhr; - } - ajax.string = function(s){ return (typeof s == 'string') } - ajax.each = function(obj, cb){ - if(!obj || !cb){ return } - for(var i in obj){ - if(obj.hasOwnProperty(i)){ - cb(obj[i], i); - } - } - } - return ajax; - }()); - gun.__.opt.hooks.load = gun.__.opt.hooks.load || tab.load; - gun.__.opt.hooks.set = gun.__.opt.hooks.set || tab.set; - }); -}({})); \ No newline at end of file diff --git a/examples/admin/package.json b/examples/admin/package.json deleted file mode 100644 index a9f9782e..00000000 --- a/examples/admin/package.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "name": "admin", - "main": "app.js", - "description": "Example gun app, using Express & Angular." -, "version": "0.0.1" -, "engines": { - "node": "~>0.6.6" - } -, "dependencies": { - "express": "~>4.9.0", - "body-parser": "~>1.8.1", - "gun": "0.0.7" - } -, "scripts": { - "start": "node app.js", - "test": "mocha" - } -} \ No newline at end of file diff --git a/examples/admin/slinger-t.html b/examples/admin/slinger-t.html deleted file mode 100644 index 9400166d..00000000 --- a/examples/admin/slinger-t.html +++ /dev/null @@ -1,229 +0,0 @@ - - - - - - - - -
- -
- -

GUN SLINGER

-

Select!

- - -
Next game available in 15 seconds or less...
-

Fastest draw in the west, no seconds, by nobody.

-
Previous duel won in no seconds, by no one.
-
-
-

GET READY!

-
-
-

FIRE!

-
by tapping this screen
-
-
-

STOP!

-
...waiting for the other player...
-
-
-

DISQUALIFIED!

-
-
-

YOU DIED!

-
-
-

YOU BOTH DIED!

- -
-
-

YOU WON!

- -
-
-

YOU WON!

-
- - -
-
- -
- \ No newline at end of file diff --git a/examples/admin/slinger.html b/examples/admin/slinger.html deleted file mode 100644 index 9fc14674..00000000 --- a/examples/admin/slinger.html +++ /dev/null @@ -1,229 +0,0 @@ - - - - - - - - -
- -
- -

GUN SLINGER

-

Select!

- - -
Next game available in 15 seconds or less...
-

Fastest draw in the west, no seconds, by nobody.

-
Previous duel won in no seconds, by no one.
-
-
-

GET READY!

-
-
-

FIRE!

-
by tapping this screen
-
-
-

STOP!

-
...waiting for the other player...
-
-
-

DISQUALIFIED!

-
-
-

YOU DIED!

-
-
-

YOU BOTH DIED!

- -
-
-

YOU WON!

- -
-
-

YOU WON!

-
- - -
-
- -
- \ No newline at end of file diff --git a/examples/admin/slinger_.html b/examples/admin/slinger_.html deleted file mode 100644 index f9fb6c83..00000000 --- a/examples/admin/slinger_.html +++ /dev/null @@ -1,195 +0,0 @@ - - - - - - - - -
- -
- -

GUN SLINGER

-

Select!

- - -
Next game available in 15 seconds or less...
-
-
-

GET READY!

-
-
-

FIRE!

-
by tapping this screen
-
-
-

STOP!

-
...waiting for the other player...
-
-
-

DISQUALIFIED!

-
-
-

YOU DIED!

-
-
-

YOU BOTH DIED!

- -
-
-

YOU WON!

- -
- -
- \ No newline at end of file diff --git a/examples/angular/index.html b/examples/angular/index.html deleted file mode 100644 index 4a32c5ea..00000000 --- a/examples/angular/index.html +++ /dev/null @@ -1,79 +0,0 @@ - - - - - - - - - - -

Admin JSON Editor

- This is a live view of your data, you can edit it in realtime or add new key/values. - - - - diff --git a/examples/cats/get.js b/examples/cats/get.js deleted file mode 100644 index 926aa14b..00000000 --- a/examples/cats/get.js +++ /dev/null @@ -1,5 +0,0 @@ -var gun = require('gun')({ - s3: (process.env.NODE_ENV === 'production')? null : require('../../test/shotgun') // replace this with your own keys! -}); - -gun.load('kitten/hobbes').path('servant.cat.servant.name').get(function(name){ console.log(name) }) \ No newline at end of file diff --git a/examples/cats/load.js b/examples/cats/load.js deleted file mode 100644 index b5a9a5af..00000000 --- a/examples/cats/load.js +++ /dev/null @@ -1,13 +0,0 @@ -var gun = require('gun')({ - s3: (process.env.NODE_ENV === 'production')? null : require('../../test/shotgun') // replace this with your own keys! -}); - -gun.load('email/mark@gundb.io').get(function(Mark){ - console.log("Hello ", Mark); - this.path('username').set('amark'); // because we hadn't saved it yet! - this.path('cat').get(function(Hobbes){ // `this` is context of the nodes you explore via path - console.log(Hobbes); - this.set({ servant: Mark, coat: "tabby" }); // oh no! Hobbes has become Mark's master. - this.key('kitten/hobbes'); // cats are taking over the internet! Better make an index for them. - }); -}); \ No newline at end of file diff --git a/examples/cats/set.js b/examples/cats/set.js deleted file mode 100644 index 4ba48876..00000000 --- a/examples/cats/set.js +++ /dev/null @@ -1,7 +0,0 @@ -var gun = require('gun')({ - s3: (process.env.NODE_ENV === 'production')? null : require('../../test/shotgun') // replace this with your own keys! -}); - -gun.set({ name: "Mark Nadal", email: "mark@gunDB.io", cat: { name: "Hobbes", species: "kitty" } }) - .key('email/mark@gundb.io') -; \ No newline at end of file diff --git a/examples/chat/index.html b/examples/chat/index.html new file mode 100644 index 00000000..4ba9ff79 --- /dev/null +++ b/examples/chat/index.html @@ -0,0 +1,60 @@ + + + + + + + + + +
+ + + +
+ + + \ No newline at end of file diff --git a/examples/chat/spam.js b/examples/chat/spam.js new file mode 100644 index 00000000..1adfcd18 --- /dev/null +++ b/examples/chat/spam.js @@ -0,0 +1,10 @@ +function spam(){ + spam.start = true; spam.lock = false; + if(spam.count >= 100){ return } + var $f = $('form'); + $('.what', $f).value = ++spam.count; + $f.onsubmit(); + setTimeout(spam, 0); +}; spam.count = 0; spam.lock = true; + +alert("ADD THIS LINE TO THE TOP OF THE MAP.VAL CALLBACK: `if(!spam.lock && !spam.start){ spam() }`"); \ No newline at end of file diff --git a/examples/express.js b/examples/express.js index d108ebf1..e311d5e1 100644 --- a/examples/express.js +++ b/examples/express.js @@ -17,4 +17,4 @@ var gun = Gun({ gun.attach(app); app.use(express.static(__dirname)).listen(port); -console.log('Server started on port ' + port + ' with /gun'); +console.log('Server started on port ' + port + ' with /gun'); \ No newline at end of file diff --git a/examples/hello-world.js b/examples/hello-world.js index 1337fff6..380c8170 100644 --- a/examples/hello-world.js +++ b/examples/hello-world.js @@ -6,11 +6,11 @@ var gun = Gun({ bucket: '' // The bucket you want to save into } }); -gun.set({ hello: 'world' }).key('my/first/data'); +gun.put({ hello: 'world' }).key('my/first/data'); var http = require('http'); http.createServer(function(req, res){ - gun.load('my/first/data', function(err, data){ + gun.get('my/first/data', function(err, data){ res.writeHead(200, {'Content-Type': 'application/json'}); res.end(JSON.stringify(data)); }); diff --git a/examples/http.js b/examples/http.js index 21590548..9fabbb68 100644 --- a/examples/http.js +++ b/examples/http.js @@ -1,7 +1,5 @@ var port = process.env.OPENSHIFT_NODEJS_PORT || process.env.VCAP_APP_PORT || process.env.PORT || process.argv[2] || 80; -var http = require('http'); - var Gun = require('gun'); var gun = Gun({ file: 'data.json', @@ -12,10 +10,16 @@ var gun = Gun({ } }); -var server = http.createServer(function(req, res){ - gun.server(req, res); +var server = require('http').createServer(function(req, res){ + if(gun.server(req, res)){ + return; // filters gun requests! + } + require('fs').createReadStream(require('path').join(__dirname, req.url)).on('error',function(){ // static files! + res.writeHead(200, {'Content-Type': 'text/html'}); + res.end(require('fs').readFileSync(require('path').join(__dirname, 'index.html'))); // or default to index + }).pipe(res); // stream }); gun.attach(server); server.listen(port); -console.log('Server started on port ' + port + ' with /gun'); +console.log('Server started on port ' + port + ' with /gun'); \ No newline at end of file diff --git a/examples/index.html b/examples/index.html index 768f47ec..87268da9 100644 --- a/examples/index.html +++ b/examples/index.html @@ -1,6 +1,6 @@ -

Examples Directory

+

Examples Directory

- - - + + + + - + \ No newline at end of file diff --git a/examples/json/index.html b/examples/json/index.html new file mode 100644 index 00000000..3e735b13 --- /dev/null +++ b/examples/json/index.html @@ -0,0 +1,75 @@ + + + + + + + +

Admin JSON Editor

+ This is a live view of your data, you can edit it in realtime or add new field/values. + +
  • + field: + val +
  • + + + + + \ No newline at end of file diff --git a/examples/lists/index.html b/examples/lists/index.html deleted file mode 100644 index 10006115..00000000 --- a/examples/lists/index.html +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - diff --git a/examples/package.json b/examples/package.json index 835312e5..8baedb08 100644 --- a/examples/package.json +++ b/examples/package.json @@ -8,7 +8,7 @@ } , "dependencies": { "express": "~>4.9.0", - "gun": "0.1.5" + "gun": "file:../" } , "scripts": { "start": "node express.js", diff --git a/examples/social/index.html b/examples/social/index.html deleted file mode 100644 index fc8f3a5a..00000000 --- a/examples/social/index.html +++ /dev/null @@ -1,135 +0,0 @@ - - - - - - -

    Social Network

    -
    - - - -
    - - - - - - - - \ No newline at end of file diff --git a/examples/social/server.js b/examples/social/server.js deleted file mode 100644 index c399e0d1..00000000 --- a/examples/social/server.js +++ /dev/null @@ -1,58 +0,0 @@ -var fs = require('fs'); -var http = require('http'); -var qs = require('querystring'); -var Gun = require('gun'); -var gun = Gun({ - peers: 'http://localhost:8888/gun' - ,s3: require('../../test/shotgun') // replace this with your own keys! -}); - -http.route = function(url){ - console.log(url); - var path = __dirname + url; - if(!url){ return http.route } - if(gun.server.regex.test(url)){ - return gun; - } - if(fs.existsSync(path)){ - return ((path = require(path)) && path.server)? path : http.route; - } else - if(url.slice(-3) !== '.js'){ - return http.route(url + '.js'); - } - return http.route; -} -http.route.server = function(req, res){ - console.log("/ no route found"); -} -http.createServer(function(req, res){ - console.log(req.headers); - console.log(req.method, req.url); - var body = {}; - body.length = 0; - body.data = new require('buffer').Buffer(''); - req.on('data', function(buffer){ - if(body.data.length >= body.length + buffer.length){ - buffer.copy(body.data, body.length); - } else { - body.data = Buffer.concat([body.data, buffer]); - } - body.length += buffer.length; - }); - req.on('end', function(x){ - body.text = body.data.toString('utf8'); - try{body.json = JSON.parse(body.text); - }catch(e){} - delete body.data; - req.body = body.json || body.text; - http.route(req.url).server(req, res); - }); - res.on('data', function(data){ - res.write(JSON.stringify(data) + '\n'); - }); - res.on('end', function(data){ - res.end(JSON.stringify(data)); - }); -}).listen(8888); -console.log("listening"); -//process.on("uncaughtException", function(e){console.log('!!!!!!!!!!!!!!!!!!!!!!');console.log(e);console.log('!!!!!!!!!!!!!!!!!!!!!!')}); \ No newline at end of file diff --git a/examples/social/sign.js b/examples/social/sign.js deleted file mode 100644 index a1504431..00000000 --- a/examples/social/sign.js +++ /dev/null @@ -1,77 +0,0 @@ -var sign = {}; -var Gun = require('gun'); -var gun = Gun({ - peers: 'http://localhost:8888/gun' - ,s3: require('../../test/shotgun') // replace this with your own keys! -}); - -sign.user = {} -sign.user.create = function(form, cb, shell){ - sign.crypto(form, function(err, user){ - if(err || !user){ return cb(err) } - user = {key: user.key, salt: user.salt}; - user.account = {email: form.email, registered: new Date().getTime()}; - gun.set(user).key('email/' + user.account.email); - cb(null, user); - }); -}; - -sign.server = function(req, res){ - console.log("sign.server", req.headers, req.body); - if(!req.body || !req.body.email){ return res.emit('end', {err: "That email does not exist."}) } - var user = gun.load('email/' + req.body.email, function(data){ // this callback is called the magazine, since it holds the clip - console.log("data from key", data); - if(!req.body.password){ - return res.emit('end', {ok: 'sign in'}); - } - crypto({password: req.body.password, salt: data.salt }, function(error, valid){ - if(error){ return res.emit('end', {err: "Something went wrong! Try again."}) } - if(data.key === valid.key){ // authorized - return res.emit('end', {ok: 'Signed in!'}); - } else { // unauthorized - return res.emit('end', {err: "Wrong password."}); - } - }); - }).blank(function(){ - if(!req.body.password){ - return res.emit('end', {ok: 'sign up'}); - } - return sign.user.create(req.body, function(err, user){ - if(err || !user){ return res.emit('end', {err: "Something went wrong, please try again."}) } - console.log('yay we made the user', user); - res.emit('end', {err: "Registered!"}); - }, user); - }); -} - -;var crypto = function(context, callback, option){ - option = option || {}; - option.hash = option.hash || 'sha1'; - option.strength = option.strength || 10000; - option.crypto = option.crypto || require('crypto'); - if(!context.password){ - option.crypto.randomBytes(8,function(error, buffer){ - if(error){ return callback(error) } - context.pass = buffer.toString('base64'); - crypto(context, callback); - }); return callback; - } - if(!context.salt){ - option.crypto.randomBytes(64, function(error, buffer){ - if(error){ return callback(error) } - context.salt = buffer.toString('base64'); - crypto(context, callback); - }); - return callback; - } - option.crypto.pbkdf2(context.password, context.salt, option.strength, context.salt.length, function(error, buffer){ - if(!buffer || error){ return callback(error) } - delete context.password; - context.key = buffer.toString('base64'); - callback(null, context); - }); - return callback; -}; -sign.crypto = crypto; - -module.exports = sign; \ No newline at end of file diff --git a/examples/todo/index.html b/examples/todo/index.html index eadcbff6..92dd811c 100644 --- a/examples/todo/index.html +++ b/examples/todo/index.html @@ -10,24 +10,24 @@ // by Forrest Tait! Edited by Mark Nadal. function ready(){ var $ = document.querySelector.bind(document); - var gun = Gun(location.origin + '/gun').load('example/todo/data').set({}); + var gun = Gun(location.origin + '/gun').get('example/todo/data'); gun.on(function renderToDo(val){ var todoHTML = ''; - for(key in val) { - if(!val[key] || key == '_') continue; - todoHTML += '
  • ' + (val[key]||'').toString().replace(/\X
  • '; + for(field in val) { + if(!val[field] || field == '_') continue; + todoHTML += '
  • ' + + (val[field]||'').toString().replace(/\X
  • '; } $("#todos").innerHTML = todoHTML; }); $("#addToDo").onsubmit = function(){ - var id = randomId(); - gun.path(id).set(($("#todoItem").value||'').toString().replace(/\ - + \ No newline at end of file diff --git a/gun.js b/gun.js index f9cebb68..be75f1d0 100644 --- a/gun.js +++ b/gun.js @@ -1,20 +1,21 @@ ;(function(){ function Gun(opt){ var gun = this; - if(!Gun.is(gun)){ - return new Gun(opt); + if(!Gun.is(gun)){ // if this is not a GUN instance, + return new Gun(opt); // then make it so. } gun.opt(opt); } - Gun._ = { + Gun._ = { // some reserved key words, these are not the only ones. soul: '#' ,meta: '_' ,HAM: '>' } - ;(function(Gun){ - Gun.version = 0.1; + ;(function(Gun){ // GUN specific utilities + Util(Gun); // initialize standard utilities + Gun.version = 0.2; // TODO: When Mark (or somebody) does a push/publish, dynamically update package.json Gun.is = function(gun){ return (gun instanceof Gun)? true : false } - Gun.is.value = function(v){ // null, binary, number (!Infinity), text, or a rel. + Gun.is.value = function(v){ // null, binary, number (!Infinity), text, or a rel (soul). if(v === null){ return true } // deletes if(v === Infinity){ return false } // we want this to be, but JSON does not support it, sad face. if(Gun.bi.is(v) @@ -50,12 +51,13 @@ } Gun.is.soul.on = function(n){ return (n && n._ && n._[Gun._.soul]) || false } Gun.is.node = function(node, cb){ + var soul; if(!Gun.obj.is(node)){ return false } - if(Gun.is.soul.on(node)){ + if(soul = Gun.is.soul.on(node)){ return !Gun.obj.map(node, function(value, field){ // need to invert this, because the way we check for this is via a negation. if(field == Gun._.meta){ return } // skip this. if(!Gun.is.value(value)){ return true } // it is true that this is an invalid node. - if(cb){ cb(value, field) } + if(cb){ cb(value, field, node._, soul) } }); } return false; @@ -64,47 +66,115 @@ var exist = false; if(!Gun.obj.is(graph)){ return false } return !Gun.obj.map(graph, function(node, soul){ // need to invert this, because the way we check for this is via a negation. - if(!node || soul !== Gun.is.soul.on(node) || !Gun.is.node(node, fn)){ return true } // it is true that this is an invalid graph. - if(cb){ cb(node, soul) } + if(!node || soul !== Gun.is.soul.on(node) || !Gun.is.node(node, fn)){ return true } // it is true that this is an invalid graph. + (cb || function(){})(node, soul, function(fn){ + if(fn){ Gun.is.node(node, fn) } + }); exist = true; }) && exist; } // Gun.ify // the serializer is too long for right here, it has been relocated towards the bottom. - Gun.union = function(graph, prime){ - var context = Gun.shot(); - context.nodes = {}; - context('done'); - context('change'); - Gun.obj.map(prime, function(node, soul){ - var vertex = graph[soul], env; - if(!vertex){ // disjoint - context.nodes[node._[Gun._.soul]] = graph[node._[Gun._.soul]] = node; - context('change').fire(node); + Gun.union = function(gun, prime, cb){ + var ctx = {count: 0, cb: function(){ cb = cb? cb() && null : null }}; + ctx.graph = gun.__.graph; + if(!ctx.graph){ ctx.err = {err: Gun.log("No graph!") } } + if(!prime){ ctx.err = {err: Gun.log("No data to merge!") } } + if(ctx.soul = Gun.is.soul.on(prime)){ + ctx.tmp = {}; + ctx.tmp[ctx.soul] = prime; + prime = ctx.tmp; + } + Gun.is.graph(prime, null, function(val, field, meta){ + if(!meta || !(meta = meta[Gun._.HAM]) || !Gun.num.is(meta[field])){ + return ctx.err = {err: Gun.log("No state on " + field + "!") } + } + }); + if(ctx.err){ return ctx } + (function union(graph, prime){ + ctx.count += 1; + Gun.obj.map(prime, function(node, soul){ + soul = Gun.is.soul.on(node); + if(!soul){ return } + ctx.count += 1; + var vertex = graph[soul]; + if(!vertex){ // disjoint // TODO: Maybe not correct? BUG, probably. + Gun.on('union').emit(gun, graph[soul] = node); + gun.__.on(soul).emit(node); + ctx.count -= 1; + return; + } + Gun.HAM(vertex, node, function(){}, function(vertex, field, value){ + if(!vertex){ return } + var change = {}; + change._ = change._ || {}; + change._[Gun._.soul] = Gun.is.soul.on(vertex); + if(field){ + change._[Gun._.HAM] = change._[Gun._.HAM] || {}; + vertex[field] = change[field] = value; + (vertex._[Gun._.HAM] = vertex._[Gun._.HAM] || {})[field] = change._[Gun._.HAM][field] = node._[Gun._.HAM][field]; + } + //context.nodes[change._[Gun._.soul]] = change; + //context('change').fire(change); + Gun.on('union').emit(gun, change); + gun.__.on(Gun.is.soul.on(change)).emit(change); + }, function(){})(function(){ + if(!(ctx.count -= 1)){ ctx.cb() } + }); + }); + ctx.count -= 1; + })(ctx.graph, prime); + if(!ctx.count){ ctx.cb() } + return ctx; + } + Gun.union.pseudo = function(soul, graph, vertex){ + var c = 0, s; + ((vertex = vertex || {})._ = {})[Gun._.soul] = soul; + Gun.is.graph(graph, function(node, ss){ + c += 1; s = ss; + Gun.HAM(vertex, node, function(){}, function(vertex, field, value){ + (vertex._[Gun._.HAM] = vertex._[Gun._.HAM] || {})[field] = node._[Gun._.HAM][field]; + vertex[field] = value; + }, function(){}); + }); + if(1 == c){ return } + return vertex; + } + Gun.HAM = function(vertex, delta, lower, now, upper){ + upper.max = -Infinity; + now.end = true; + Gun.obj.map(delta, function update(incoming, field){ + if(field === Gun._.meta){ return } + now.end = false; + var ctx = {incoming: {}, current: {}}, state; + ctx.drift = (ctx.drift = Gun.time.is()) > (Gun.time.now.last || -Infinity)? ctx.drift : Gun.time.now.last; + ctx.incoming.value = Gun.is.soul(incoming) || incoming; + ctx.current.value = Gun.is.soul(vertex[field]) || vertex[field]; + ctx.incoming.state = Gun.num.is(ctx.tmp = ((delta._||{})[Gun._.HAM]||{})[field])? ctx.tmp : -Infinity; + ctx.current.state = Gun.num.is(ctx.tmp = ((vertex._||{})[Gun._.HAM]||{})[field])? ctx.tmp : -Infinity; + upper.max = ctx.incoming.state > upper.max? ctx.incoming.state : upper.max; + state = HAM(ctx.drift, ctx.incoming.state, ctx.current.state, ctx.incoming.value, ctx.current.value); + if(state.err){ + root.console.log(".!HYPOTHETICAL AMNESIA MACHINE ERR!.", state.err); // this error should never happen. return; } - env = Gun.HAM(vertex, node, function(current, field, deltaValue){ - if(!current){ return } - var change = {}; - current[field] = change[field] = deltaValue; // current and vertex are the same - current._[Gun._.HAM][field] = node._[Gun._.HAM][field]; - change._ = current._; - context.nodes[change._[Gun._.soul]] = change; - context('change').fire(change); - }).upper(function(c){ - context.err = c.err; - context.up -= 1; - if(!context.up){ - context('done').fire(context.err, context); - } - }); - context.up += env.up; + if(state.state || state.quarantineState || state.current){ + lower.call(state, vertex, field, incoming); + return; + } + if(state.incoming){ + now.call(state, vertex, field, incoming); + return; + } + if(state.amnesiaQuarantine){ + upper.wait = true; + upper.call(state, vertex, field, incoming); // signals that there are still future modifications. + Gun.schedule(ctx.incoming.state, function(){ + update(incoming, field); + if(ctx.incoming.state === upper.max){ (upper.last || function(){})() } + }); + } }); - if(!context.up){ - context('done').fire(context.err, context); - } - return context; - } - Gun.HAM = function(current, delta, each){ // HAM only handles primitives values, all other data structures need to be built ontop and reduce to HAM. + if(now.end){ now.call({}, vertex) } // TODO: Should HAM handle empty updates? function HAM(machineState, incomingState, currentState, incomingValue, currentValue){ // TODO: Lester's comments on roll backs could be vulnerable to divergence, investigate! if(machineState < incomingState){ // the incoming value is outside the boundary of the machine's state, it must be reprocessed in another state. @@ -139,51 +209,63 @@ } return {err: "you have not properly handled recursion through your data or filtered it as JSON"}; } - var context = Gun.shot(); - context.HAM = {}; - context.states = {}; - context.states.delta = delta._[Gun._.HAM]; - context.states.current = current._[Gun._.HAM] = current._[Gun._.HAM] || {}; - context('lower');context('upper');context.up = context.up || 0; - Gun.obj.map(delta, function update(deltaValue, field){ - if(field === Gun._.meta){ return } - if(!Gun.obj.has(current, field)){ // does not need to be applied through HAM - each.call({incoming: true, converge: true}, current, field, deltaValue); // done synchronously - return; - } - var serverState = Gun.time.is(); - var incomingValue = Gun.is.soul(deltaValue) || deltaValue; - var currentValue = Gun.is.soul(current[field]) || current[field]; - // add more checks? - var state = HAM(serverState, context.states.delta[field], context.states.current[field], incomingValue, currentValue); - //console.log("the server state is",serverState,"with delta:current",context.states.delta[field],context.states.current[field]); - //console.log("having incoming value of",deltaValue,'and',current[field]); - if(state.err){ - root.console.log(".!HYPOTHETICAL AMNESIA MACHINE ERR!.", state.err); // this error should never happen. - return; - } - if(state.state || state.quarantineState || state.current){ - context('lower').fire(context, state, current, field, deltaValue); - return; - } - if(state.incoming){ - each.call(state, current, field, deltaValue); // done synchronously - return; - } - if(state.amnesiaQuarantine){ - context.up += 1; - Gun.schedule(context.states.delta[field], function(){ - update(deltaValue, field); - context.up -= 1; - context('upper').fire(context, state, current, field, deltaValue); - }); - } - }); - if(!context.up){ - context('upper').fire(context, {}); + return function(fn){ + upper.last = fn || function(){}; + if(!upper.wait){ upper.last() } } - return context; } + ;Gun.on = (function(){ + // events are fundamentally different, being synchronously 1 to N fan out, + // than req/res/callback/promise flow, which are asynchronously 1 to 1 into a sink. + function On(where){ return where? (On.event = On.event || On.create())(where) : On.create() } + On.is = function(on){ return (On instanceof on)? true : false } + On.create = function(){ + var chain = new On.chain(); + return chain.$ = function(where){ + chain.where = where; + return chain; + } + } + On.sort = Gun.list.sort('i'); + ;On.chain=(function(){ + function Chain(){ + if(!(this instanceof Chain)){ + return new Chain(); + } + } + Chain.chain = Chain.prototype; + Chain.chain.emit = function(what){ + var me = this + , where = me.where + , args = arguments + , on = (me._ = me._ || {})[where] = me._[where] || []; + if(!(me._[where] = Gun.list.map(on, function(hear, i, map){ + if(!hear || !hear.as){ return } + map(hear); + hear.as.apply(hear, args); + }))){ Gun.obj.del(on, where) } + } + Chain.chain.event = function(as, i){ + if(!as){ return } + var me = this + , where = me.where + , args = arguments + , on = (me._ = me._ || {})[where] = me._[where] || [] + , e = {as: as, i: i || 0, off: function(){ return !(e.as = false) }}; + return on.push(e), on.sort(On.sort), e; + } + Chain.chain.once = function(as, i){ + var me = this + , once = function(){ + this.off(); + as.apply(this, arguments) + } + return me.event(once, i) + } + return Chain; + }()); + return On; + }()); Gun.roulette = function(l, c){ var gun = Gun.is(this)? this : {}; if(gun._ && gun.__.opt && gun.__.opt.uuid){ @@ -196,18 +278,18 @@ } }(Gun)); ;(function(Chain){ - Chain.opt = function(opt, stun){ // idempotently update or set options + Chain.opt = function(opt, stun){ // idempotently update or put options var gun = this; gun._ = gun._ || {}; gun.__ = gun.__ || {}; - gun.shot = Gun.shot('then', 'err'); - gun.shot.next = Gun.next(); if(opt === null){ return gun } opt = opt || {}; gun.__.opt = gun.__.opt || {}; - gun.__.keys = gun.__.keys || {}; + gun.__.flag = gun.__.flag || {start: {}, end: {}}; + gun.__.key = gun.__.key || {s: {}, ed: {}}; gun.__.graph = gun.__.graph || {}; gun.__.on = gun.__.on || Gun.on.create(); + gun.__.meta = gun.__.meta || function(s){ return gun.__.meta[s] = gun.__.meta[s] || {} } if(Gun.text.is(opt)){ opt = {peers: opt} } if(Gun.list.is(opt)){ opt = {peers: opt} } if(Gun.text.is(opt.peers)){ opt.peers = [opt.peers] } @@ -216,7 +298,6 @@ gun.__.opt.uuid = opt.uuid || gun.__.opt.uuid || {}; gun.__.opt.cb = gun.__.opt.cb || function(){}; gun.__.opt.hooks = gun.__.opt.hooks || {}; - gun.__.hook = Gun.shot('then','end'); Gun.obj.map(opt.hooks, function(h, f){ if(!Gun.fns.is(h)){ return } gun.__.opt.hooks[f] = h; @@ -224,172 +305,276 @@ if(!stun){ Gun.on('opt').emit(gun, opt) } return gun; } - Chain.chain = function(from){ + Gun.chain.chain = function(from){ var gun = Gun(null); from = from || this; gun.back = from; gun.__ = from.__; - gun._ = {}; - //Gun.obj.map(from._, function(val, field){ gun._[field] = val }); + gun._ = {on: Gun.on.create()}; + gun._.at = function(e){ + var proxy = function(cb, i, chain){ + var on = gun._.on(e), at; + if(at = ((on = gun._.on(e)).e = on.e || {})[e]){ setTimeout(function(){cb.call(on, at)},0) } + on[chain](function(at){ + cb.call(on, at); + }, i); + } + proxy.event = function(cb, i){ return proxy(cb, i, 'event') }; + proxy.once = function(cb, i){ return proxy(cb, i, 'once') }; + proxy.emit = function(at, on){//setTimeout(function(){ + ((on = gun._.on(e)).e = on.e || {})[e] = at; + gun._.on(e).emit(at); + //},0) + }; + return proxy; + } return gun; } - Chain.load = function(key, cb, opt){ - var gun = this.chain(); - gun.shot.next(function(next){ - opt = opt || {}; - cb = cb || function(){}; cb.c = 0; - cb.soul = Gun.is.soul(key); // is this a key or a soul? - if(cb.soul){ // if a soul... - cb.node = gun.__.graph[cb.soul]; // then attempt to grab it directly from cache in the graph - } else { // if not... - cb.node = gun.__.keys[key]; // attempt to grab it directly from our key cache - (gun._.keys = gun._.keys || {})[key] = cb.node? 1 : 0; // set a key marker on it + Chain.get = function(key, cb, opt){ // get opens up a reference to a node and loads it. + var gun = this.chain(), ctx = {}; + if(!key){ return cb.call(gun, {err: Gun.log("No key or relation to get!") }), gun } + ctx.key = Gun.text.is(key) && key; // if key is text, then key, else false. + ctx.soul = Gun.is.soul(key); // if key is a soul, then the soul, else false. + cb = cb || function(){}; + opt = opt || {}; + + if(opt.force){ load(key) } else + if(ctx.soul){ + if(ctx.node = gun.__.graph[ctx.soul]){ // in memory + (ctx.graph = {})[ctx.soul] = ctx.node; + cb.call(gun, null, Gun.obj.copy(ctx.graph)); + (ctx.graph = {})[ctx.soul] = Gun.union.pseudo(ctx.soul); + cb.call(gun, null, ctx.graph); cb.call(gun, null, {}); // end; + } else { load(key) } // not in memory + ctx.node = gun.__.graph[ctx.soul] = gun.__.graph[ctx.soul] || Gun.union.pseudo(ctx.soul); + gun._.at('soul').emit({soul: ctx.soul, GET: 'SOUL'}); + } else + if(ctx.key){ + function get(soul){ + var graph = gun.__.key.s[ctx.key], end = {}; + Gun.is.graph(graph, function(node, soul){ + end[soul] = Gun.union.pseudo(soul); + gun._.at('soul').emit({soul: soul, key: ctx.key, GET: 'SOUL'}); + }); + cb.call(gun, null, Gun.obj.copy(graph)); cb.call(gun, null, end); cb.call(gun, null, {}); // end. } - if(!opt.force && cb.node){ // if it was in cache, then... - console.log("load via gun"); - gun._.node = cb.node; // assign it to this context - return cb.call(gun, null, Gun.obj.copy(gun._.node)), cb.c++, next(); // frozen copy - } - // missing: hear shots! I now hook this up in other places, but we could get async/latency issues? - // We need to subscribe early? Or the transport layer handle this for us? - if(Gun.fns.is(gun.__.opt.hooks.load)){ - gun.__.opt.hooks.load(key, function(err, data){ - if(cb.c++){ return Gun.log("Warning: Load callback being called", cb.c, "times.") } - if(err){ return cb.call(gun, err) } - if(!data){ return cb.call(gun, null, data), next() } - var context = gun.union(data); // safely transform the data into the current context - if(context.err){ return cb.call(gun, context.err) } // but validate it in case of errors - gun._.node = gun.__.graph[Gun.is.soul.on(data)]; // immediately use the state in cache. - if(!cb.soul){ // and if we had loaded with a key rather than a soul - gun._.keys[key] = 1; // then set a marker that this key matches - gun.__.keys[key] = gun._.node; // and cache a pointer to the node + if(gun.__.key.s[ctx.key]){ get() } // in memory + else if(ctx.flag = gun.__.flag.start[ctx.key]){ // will be in memory + ctx.flag.once(get); + } else { load(key) } // not in memory + } else { cb.call(gun, {err: Gun.log("No key or relation to get!")}) } + + function load(key){ + if(Gun.fns.is(ctx.hook = gun.__.opt.hooks.get)){ + ctx.hook(key, function(err, data){ // will be called multiple times. + console.log("chain.get ", key, "from hook", err, data); + if(err){ return cb.call(gun, err, null) } + if(!data){ + if(ctx.data){ return } + cb.call(gun, null, null); + if(!ctx.key || ctx.soul){ return } // TODO: Maybe want to do a .not on a soul directly? + return gun.__.flag.end[ctx.key] = gun.__.flag.end[ctx.key] || function($){ + // TODO: cover all edge cases, uniqueness? + delete gun.__.flag.end[ctx.key]; + gun._.at('soul').emit($); + }, gun._.at('null').emit({key: ctx.key, GET: 'NULL'}); } - return cb.call(gun, null, Gun.obj.copy(gun._.node)), next(); // frozen copy + var dat = ctx.data = {}; + if(Gun.obj.empty(data)){ return cb.call(gun, null, data) } + if(!Gun.is.graph(data, function(node, soul, map){ + if(err = Gun.union(gun, node).err){ return cb.call(gun, err, data) } + /*dat[soul] = Gun.union.pseudo(soul); map(function(val, field){ + (dat[soul]._[Gun._.HAM] = dat[soul]._[Gun._.HAM] || {})[field] = gun.__.graph[soul]._[Gun._.HAM] + });*/ + if(ctx.key){ (gun.__.key.s[ctx.key] = gun.__.key.s[ctx.key] || {})[soul] = gun.__.graph[soul] } + gun._.at('soul').emit({soul: soul, key: ctx.key, GET: 'SOUL'}); // TODO: Implications? + })){ return cb.call(gun, {err: Gun.log('Not a valid graph!') }, data) } + cb.call(gun, null, data); }, opt); } else { - root.console.log("Warning! You have no persistence layer to load from!"); - return cb.call(gun), cb.c++, next(); + console.Log("Warning! You have no persistence layer to get from!"); + cb.call(gun, null, null); // Technically no error, but no way we can get data. + gun._.at('null').emit({key: ctx.key, GET: 'NULL'}); } - }); + } return gun; } - Chain.key = function(key, cb){ - var gun = this; - if(!gun.back){ // TODO: BUG? Does this maybe introduce bugs other than the test that it fixes? - gun = gun.chain(); // create a new context + Chain.key = function(key, cb, opt){ + var gun = this, ctx = {}; + if(!key){ return cb.call(gun, {err: Gun.log('No key!')}), gun } + if(!gun.back){ gun = gun.chain() } + if(gun.__.key.s[key]){ console.Log("Warning! Key already used!") } // TODO! Have opt that will aggregate. + cb = cb || function(){}; + opt = Gun.text.is(opt)? {soul: opt} : opt || {}; + opt.soul = opt.soul || opt[Gun._.soul]; + if(opt.soul){ // force inject // TODO! BUG! WRITE A TEST FOR THIS! + if(!gun.__.graph[opt.soul]){ + ((gun.__.graph[opt.soul] = {})._ = {})[Gun._.soul] = opt.soul; + } + (gun.__.key.s[key] = gun.__.key.s[key] || {})[opt.soul] = gun.__.graph[opt.soul]; + if(gun.__.flag.end[key]){ // TODO: Ought this be fulfilled from self as well? + gun.__.flag.end[key]({soul: opt.soul}); + } + index({soul: opt.soul}); + } else { // will be injected via a put + (gun.__.flag.start[key] = gun._.at('soul')).once(function($){ + console.log("chain.key"); + (gun.__.key.s[key] = gun.__.key.s[key] || {})[$.soul] = gun.__.graph[$.soul]; + delete gun.__.flag.start[key]; + }, -1); + gun._.at('soul').event(index); } + function index($){ // TODO: once per soul in graph. (?) + if(Gun.fns.is(ctx.hook = gun.__.opt.hooks.key)){ + ctx.hook(key, $.soul, function(err, data){ + return cb.call(gun, err, data); + }, opt); + } else { + console.Log("Warning! You have no key hook!"); + cb.call(gun, null); // This is in memory success, hardly "success" at all. + } + } + return gun; + } + Chain.all = function(key, cb){ + var gun = this.chain(); + return gun; // TODO: BUG! We need to create all! + cb = cb || function(){}; gun.shot.next(function(next){ - cb = cb || function(){}; - if(Gun.obj.is(key)){ // if key is an object then we get the soul directly from it - Gun.obj.map(key, function(soul, field){ return cb.key = field, cb.soul = soul }); - } else { // or else - cb.key = key; // the key is the key - } - if(gun._.node){ // if it is in cache - cb.soul = Gun.is.soul.on(gun._.node); - (gun._.keys = gun._.keys || {})[cb.key] = 1; // clear the marker in this context - (gun.__.keys = gun.__.keys || {})[cb.key] = gun._.node; // and create the pointer - } else { // if it is not in cache - (gun._.keys = gun._.keys || {})[cb.key] = 0; // then set a marker on this context - } - if(Gun.fns.is(gun.__.opt.hooks.key)){ - gun.__.opt.hooks.key(cb.key, cb.soul, function(err, data){ // call the hook - return cb.call(gun, err, data); // and notify how it went. + Gun.obj.map(gun.__.key.s, function(node, key){ // TODO: BUG!! Need to handle souls too! + if(node = Gun.is.soul.on(node)){ + (cb.vode = cb.vode || {})[key] = {}; + cb.vode[key][Gun._.soul] = node; + } + }); + if(cb.vode){ + gun._.node = cb.vode; // assign it to this virtual node. + cb.call(gun, null, Gun.obj.copy(gun._.node)), next(); // frozen copy + } else + if(Gun.fns.is(gun.__.opt.hooks.all)){ + gun.__.opt.hooks.all(function(err, data){ // call the hook + // this is multiple }); } else { - root.console.log("Warning! You have no key hook!"); - cb.call(gun); + console.Log("Warning! You have no all hook!"); + return cb.call(gun), next(); } - next(); // continue regardless }); return gun; } /* - how many different ways can we get something? ONLY THE FIRST ONE IS SUPPORTED, the others might become plugins. + how many different ways can we return something? ONLY THE FIRST ONE IS SUPPORTED, the others might become plugins. Find via a singular path - .path('blah').get(blah); + .path('blah').val(blah); Find via multiple paths with the callback getting called many times - .path('foo', 'bar').get(foorOrBar); + .path('foo', 'bar').val(fooOrBar); Find via multiple paths with the callback getting called once with matching arguments - .path('foo', 'bar').get(foo, bar) + .path('foo', 'bar').val(foo, bar) Find via multiple paths with the result aggregated into an object of pre-given fields - .path('foo', 'bar').get({foo: foo, bar: bar}) || .path({a: 'foo', b: 'bar'}).get({a: foo, b: bar}) + .path('foo', 'bar').val({foo: foo, bar: bar}) || .path({a: 'foo', b: 'bar'}).val({a: foo, b: bar}) Find via multiple paths where the fields and values must match - .path({foo: val, bar: val}).get({}) - Path ultimately should call .get each time, individually, for what it finds. + .path({foo: val, bar: val}).val({}) + Path ultimately should call .val each time, individually, for what it finds. Things that wait and merge many things together should be an abstraction ontop of path. */ - Chain.path = function(path, cb){ // Follow the path into the field. - var gun = this.chain(); // create a new context, changing the focal point. + Chain.path = function(path, cb){ + var gun = this.chain(); cb = cb || function(){}; - path = (Gun.text.ify(path) || '').split('.'); - gun.shot.next(cb.done = function(next){ // let the previous promise resolve. - if(next){ cb.next = next } - if(!cb.next || !cb.back){ return } - cb = cb || function(){}; // fail safe our function. - (function trace(){ // create a recursive function, and immediately call it. - gun._.field = Gun.text.ify(path.shift()); // where are we at? Figure it out. - if(gun._.node && path.length && Gun.is.soul(cb.soul = gun._.node[gun._.field])) { // if we need to recurse more - return gun.load(cb.soul, function(err){ // and the recursion happens to be on a relation, then load it. - if(err){ return cb.call(gun, err) } - trace(gun._ = this._); // follow the context down the chain. + if(!gun.back._.at){ return cb.call(gun, {err: Gun.log("No context!")}), gun } + // TODO: Hmmm once also? figure it out later. + gun.back._.at('soul').event(function($){ + var ctx = {path: (Gun.text.ify(path) || '').split('.')}; + (function trace($){ // TODO: Check for field as well and merge? + if(!ctx.path.length){ return } + var node = gun.__.graph[$.soul], field = Gun.text.ify(ctx.path.shift()), soul, val; + if(ctx.path.length){ + if(soul = Gun.is.soul(val = node[field])){ + gun.get(val, function(err, data){ + data = (data || {})[soul]; + if(err || !data || Gun.obj.empty(data, Gun._.meta)){ return cb.call(gun, err) } + trace({soul: soul}); + }); + } else { + cb.call(gun, null); + } + } else + if(!Gun.obj.has(node, field)){ // TODO: THIS MAY NOT BE CORRECT BEHAVIOR!!!! + cb.call(gun, null, null, field); + gun._.at('soul').emit({soul: $.soul, field: field, PATH: 'SOUL', WAS: 'ON'}); // if .put is after, makes sense. If anything else, makes sense to wait. + } else + if(soul = Gun.is.soul(val = node[field])){ + gun.get(val, function(err, data){ + data = (data || {})[soul]; + cb.call(gun, err, data, field); // TODO: Should we attach field here, does map? }); + ctx.node = gun.__.graph[ctx.soul] = gun.__.graph[ctx.soul] || Gun.union.pseudo(soul); + gun._.at('soul').emit({soul: soul, field: null, from: $.soul, at: field, PATH: 'SOUL'}); + } else { + cb.call(gun, null, val, field); + gun._.at('soul').emit({soul: $.soul, field: field, PATH: 'SOUL'}); } - cb.call(gun, null, Gun.obj.copy(gun._.node), gun._.field); // frozen copy - cb.next(); // and be done, fire our gun with the context. - }(gun._.node = gun.back._.node)); // immediately call trace, setting the new context with the previous node. - }); - gun.back.shot.next(function(next){ - if(gun.back && gun.back._ && gun.back._.field){ - path = [gun.back._.field].concat(path); - } - cb.back = true; - cb.done(); - next(); + }($)); }); + return gun; } - Chain.get = function(cb){ - var gun = this; // keep using the existing context. - gun.shot.next(function(next){ // let the previous promise resolve. - next(); // continue with the chain. - cb = cb || function(){}; // fail safe our function. - if(!gun._.node){ return } // if no node, then abandon and let blank handle it. - var field = Gun.text.ify(gun._.field), val = gun._.node[field]; // else attempt to get the value at the field, if we have a field. - if(field && Gun.is.soul(val)){ // if we have a field, then check to see if it is a relation - return gun.load(val, function(err, value){ // and load it. - if(err || !this._.node){ return } // handle error? - cb.call(this, value, field); // already frozen copy - }); + Chain.val = (function(){ + Gun.on('union').event(function(gun, data){ + if(!Gun.obj.empty(data, Gun._.meta)){ return } + (data = gun.__.meta(Gun.is.soul.on(data))).end = (data.end || 0) + 1; + }); + return function(cb, opt){ + var gun = this, ctx = {}; + cb = cb || root.console.log.bind(root.console); + opt = opt || {}; + + gun.on(function($, delta, on){ + var node = gun.__.graph[$.soul]; + if($.key){ + node = Gun.union.pseudo($.key, gun.__.key.s[$.key]) || node; + } + if($.field){ + if(ctx[$.soul + $.field]){ return } + ctx[$.soul + $.field] = true; // TODO: unregister instead? + return cb.call(gun, node[$.field], $.field || $.at); + } + if(ctx[$.soul] || ($.key && ctx[$.key]) || !gun.__.meta($.soul).end){ return } // TODO: Add opt to change number of terminations. + ctx[$.soul] = ctx[$.key] = true; // TODO: unregister instead? + cb.call(gun, Gun.obj.copy(node), $.field || $.at); + }, {raw: true}); + + return gun; + } + }()); + // .on(fn) gives you back the object, .on(fn, true) gives you delta pair. + Chain.on = function(cb, opt){ + var gun = this, ctx = {}; + opt = Gun.obj.is(opt)? opt : {change: opt}; + cb = cb || function(){}; + + gun._.at('soul').event(function($){ // TODO: once per soul on graph. (?) + if(ctx[$.soul]){ + if(opt.raw){ + ctx[$.soul](gun.__.graph[$.soul], $); // TODO: we get duplicate ons, once here and once from HAM. + } + } else { + (ctx[$.soul] = function(delta, $$){ + var $$ = $$ || $, node = gun.__.graph[$$.soul]; + if(opt.raw){ return cb.call(gun, $$, delta, this) } + if(!opt.end && Gun.obj.empty(delta, Gun._.meta)){ return } + if($$.key){ node = Gun.union.pseudo($.key, gun.__.key.s[$.key]) || node } + cb.call(gun, Gun.obj.copy(opt.change? delta || node : node), $$.field || $$.at); + })(gun.__.graph[$.soul], $); + gun.__.on($.soul).event(ctx[$.soul]); } - cb.call(gun, field? Gun.is.value.as(val) : Gun.obj.copy(gun._.node), field); // frozen copy - }); - return gun; - } - Chain.on = function(cb){ // get and then subscribe to subsequent changes. - var gun = this; // keep using the existing context. - gun.get(function(val, field){ - cb = cb || function(){}; // fail safe our function. - cb.call(gun, val, field); - gun.__.on(Gun.is.soul.on(gun._.node)).event(function(delta){ // then subscribe to subsequent changes. - field = Gun.text.ify(gun._.field); - if(!delta || !gun._.node){ return } - if(!field){ // if we were listening to changes on the node as a whole - return cb.call(gun, Gun.obj.copy(gun._.node)); // frozen copy - } - if(Gun.obj.has(delta, field)){ // else changes on an individual property - delta = delta[field]; // grab it and - cb.call(gun, Gun.obj.is(delta)? Gun.obj.copy(delta) : Gun.is.value.as(delta), field); // frozen copy - // TODO! BUG: If delta is an object, that would suggest it is a relation which needs to get loaded. - } - }); }); + return gun; } /* ACID compliant? Unfortunately the vocabulary is vague, as such the following is an explicit definition: - A - Atomic, if you set a full node, or nodes of nodes, if any value is in error then nothing will be set. - If you want sets to be independent of each other, you need to set each piece of the data individually. + A - Atomic, if you put a full node, or nodes of nodes, if any value is in error then nothing will be put. + If you want puts to be independent of each other, you need to put each piece of the data individually. C - Consistency, if you use any reserved symbols or similar, the operation will be rejected as it could lead to an invalid read and thus an invalid state. I - Isolation, the conflict resolution algorithm guarantees idempotent transactions, across every peer, regardless of any partition, including a peer acting by itself or one having been disconnected from the network. @@ -397,168 +582,145 @@ The live state at point of confirmation may or may not be different than when it was called. If this causes any application-level concern, it can compare against the live data by immediately reading it, or accessing the logs if enabled. */ - Chain.set = function(val, cb, opt){ // TODO: need to turn deserializer into a trampolining function so stackoverflow doesn't happen. - var gun = this; - opt = opt || {}; + Chain.put = function(val, cb, opt){ // TODO: handle case where val is a gun context! + var gun = this.chain(), call = function(){ + gun.back._.at('soul').emit({soul: Gun.is.soul.on(val) || Gun.roulette.call(gun), empty: true, PUT: 'SOUL'}); + }, drift = Gun.time.now(); cb = cb || function(){}; - if(!gun.back){ - gun = gun.chain(); // create a new context + opt = opt || {}; + if(!gun.back.back){ + gun = gun.chain(); + call(); } - gun.shot.next(function(next){ // How many edge cases are there to a set? - if(!gun._.node){ - if(Gun.is.value(val) || !Gun.obj.is(val)){ // 1. Context: null, set: value. Error, no node exists. - return cb.call(gun, {err: Gun.log("No node exists to set " + (typeof val) + " in.")}); - } - if(Gun.obj.is(val)){ // 2. Context: null, set: node. Set. - return set(next); - } - } else { - if(!gun._.field){ - if(Gun.is.value(val) || !Gun.obj.is(val)){ // 3. Context: node, set: value. Error, no field exists. - return cb.call(gun, {err: Gun.log("No field exists to set " + (typeof val) + " on.")}); - } - if(Gun.obj.is(val)){ // 4. Context: node, set: node. Merge. - return set(next); - } + if(gun.back.not){ gun.back.not(call) } + + gun.back._.at('soul').event(function($){ // TODO: maybe once per soul? + var ctx = {}, obj = val, $ = Gun.obj.copy($); + console.log("chain.put", val); + if(Gun.is.value(obj)){ + if($.from && $.at){ + $.soul = $.from; + $.field = $.at; + } // no else! + if(!$.field){ + return cb.call(gun, {err: Gun.log("No field exists for " + (typeof obj) + "!")}); + } else + if(gun.__.graph[$.soul]){ + ctx.tmp = {}; + ctx.tmp[ctx.field = $.field] = obj; + obj = ctx.tmp; } else { - if(Gun.is.value(val) || !Gun.obj.is(val)){ // 5. Context: node and field, set: value. Merge and replace. - var partial = {}; // in case we are doing a set on a field, not on a node. - partial[gun._.field] = val; // we create a blank object with the field/value to be set - val = partial; - return set(next); - } - if(Gun.obj.is(val)){ - if(Gun.is.soul(gun._.node[gun._.field])){ // 6. Context: node and field of relation, set: node. Merge. - return gun.load(gun._.node[gun._.field], function(err){ - if(err){ return cb.call(gun, {err: Gun.log(err)}) } // use gun not this to preserve intent? - set(next, this); - }); - } else { // 7. Context: node and field, set: node. Merge and replace. - var partial = {}; // in case we are doing a set on a field, not on a node. - partial[gun._.field] = val; // we create a blank object with the field/value to be set - val = partial; - return set(next); - } - } + return cb.call(gun, {err: Gun.log("No node exists to put " + (typeof obj) + " in!")}); } } - }); - function set(next, as){ - as = as || gun; - cb.states = Gun.time.is(); - Gun.ify(val, function(raw, context, sub, soul){ - if(val === raw){ return soul(Gun.is.soul.on(as._.node)) } - if(as._.node && sub && sub.path){ - return as.path(sub.path, function(err, node, field){ - if(err){ cb.err = err + " (while doing a set)" } // let .done handle calling this, it may be slower but is more consistent. - if(node = this._.node){ - if(field = this._.field){ - if(field = Gun.is.soul(node[field])){ - return soul(field); - } - } else - if(Gun.is.soul.on(node) !== Gun.is.soul.on(as._.node)){ - if(field = Gun.is.soul.on(node)){ - return soul(field); - } - } + if(Gun.obj.is(obj)){ + if($.field && !ctx.field){ + ctx.tmp = {}; + ctx.tmp[ctx.field = $.field] = obj; + obj = ctx.tmp; + } + Gun.ify(obj, function(env, cb){ + var at; + if(!env || !(at = env.at) || !env.at.node){ return } + if(!at.node._){ + at.node._ = {}; + } + if(!Gun.is.soul.on(at.node)){ + if(obj === at.obj){ + env.graph[at.node._[Gun._.soul] = at.soul = $.soul] = at.node; + cb(at, at.soul); + } else { + function path(err, data){ + if(at.soul){ return } + at.soul = Gun.is.soul.on(data) || Gun.is.soul.on(at.obj) || Gun.roulette.call(gun); + env.graph[at.node._[Gun._.soul] = at.soul] = at.node; + cb(at, at.soul); + }; + $.empty? path() : gun.back.path(at.path.join('.'), path); // TODO: clean this up. } - soul(); // else call it anyways - }); - } soul(); // else call it anyways - }).done(function(err, set){ - // TODO: should be able to handle val being a relation or a gun context or a gun promise. - // TODO: BUG: IF we are setting an object, doing a partial merge, and they are reusing a frozen copy, we need to do a DIFF to update the HAM! Or else we'll get "old" HAM. - cb.root = set.root; - set.err = set.err || cb.err; - if(set.err || !cb.root){ return cb.call(gun, set.err || {err: Gun.log("No root object!")}) } - set = Gun.ify.state(set.nodes, cb.states); // set time state on nodes? - if(set.err){ return cb.call(gun, set.err) } - gun.union(set.nodes); // while this maybe should return a list of the nodes that were changed, we want to send the actual delta - as._.node = as.__.graph[cb.root._[Gun._.soul]] || cb.root; - if(!as._.field){ - Gun.obj.map(as._.keys, function(yes, key){ - if(yes){ return } - as.key(key); // TODO: Feature? what about these callbacks? - }); - } - if(Gun.fns.is(gun.__.opt.hooks.set)){ - gun.__.opt.hooks.set(set.nodes, function(err, data){ // now iterate through those nodes to a persistence layer and get a callback once all are saved - if(err){ return cb.call(gun, err) } - return cb.call(gun, data); - }); - } else { - root.console.log("Warning! You have no persistence layer to save to!"); - return cb.call(gun); - } - next(); - }); - }; + } + if(!at.node._[Gun._.HAM]){ + at.node._[Gun._.HAM] = {}; + } + if(!at.field){ return } + at.node._[Gun._.HAM][at.field] = drift; + })(function(err, ify){ + console.log("chain.put PUT <----", ify.graph); + if(err || ify.err){ return cb.call(gun, err || ify.err) } + if(err = Gun.union(gun, ify.graph).err){ return cb.call(gun, err) } + if($.from = Gun.is.soul(ify.root[$.field])){ $.soul = $.from; $.field = null } + Gun.obj.map(ify.graph, function(node, soul){ Gun.union(gun, Gun.union.pseudo(soul)) }); + gun._.at('soul').emit({soul: $.soul, field: $.field, key: $.key, PUT: 'SOUL', WAS: 'ON'}); // WAS ON + if(Gun.fns.is(ctx.hook = gun.__.opt.hooks.put)){ + ctx.hook(ify.graph, function(err, data){ // now iterate through those nodes to a persistence layer and get a callback once all are saved + if(err){ return cb.call(gun, err) } + return cb.call(gun, null, data); + }, opt); + } else { + console.Log("Warning! You have no persistence layer to save to!"); + cb.call(gun, null); // This is in memory success, hardly "success" at all. + } + }); + } + }); return gun; } Chain.map = function(cb, opt){ - var gun = this; - opt = (Gun.obj.is(opt)? opt : (opt? {all: true} : {})); - gun.get(function(val){ - cb = cb || function(){}; - Gun.obj.map(val, function(val, field){ // by default it only maps over nodes + var gun = this.chain(), ctx = {}; + opt = (Gun.obj.is(opt)? opt : (opt? {node: true} : {})); + cb = cb || function(){}; + + gun.back.on(function(node){ // oo what if this gets TODO: BUG! retriggered? + var soul = Gun.is.soul.on(node); + Gun.obj.map(node, function(val, field){ // maybe filter against known fields. if(Gun._.meta == field){ return } - if(Gun.is.soul(val)){ - gun.load(val).get(function(val){ // should map have support for blank? - cb.call(this, val, field); + var s = Gun.is.soul(val); + if(s){ + gun.get(val, function(err, data){ + data = (data || {})[s]; // TODO: should map have support for `.not`? error? + if(err || !data || Gun.obj.empty(data, Gun._.meta)){ return } + cb.call(this, Gun.obj.copy(data), field); }); + gun.__.graph[s] = gun.__.graph[s] || Gun.union.pseudo(s); + gun._.at('soul').emit({soul: s, field: null, from: soul, at: field, MAP: 'SOUL'}); } else { - if(!opt.all){ return } // {all: true} maps over everything + if(opt.node){ return } // {node: true} maps over only sub nodes. + console.log("trigger next thing", field, val); cb.call(gun, val, field); + gun._.at('soul').emit({soul: soul, field: field, MAP: 'SOUL'}); } }); - }); + }, true); + return gun; } - // Union is different than set. Set casts non-gun style of data into a gun compatible data. - // Union takes already gun compatible data and validates it for a merge. - // Meaning it is more low level, such that even set uses union internally. - Chain.union = function(prime, cb){ - var tmp = {}, gun = this, context = Gun.shot(); + Chain.set = function(val, cb, opt){ + var gun = this, ctx = {}, drift = Gun.time.now(); cb = cb || function(){}; - context.nodes = {}; - if(!prime){ - context.err = {err: Gun.log("No data to merge!")}; - } else - if(Gun.is.soul.on(prime)){ - tmp[prime._[Gun._.soul]] = prime; - prime = tmp; - } - if(!gun || context.err){ - cb(context.err = context.err || {err: Gun.log("No gun instance!"), corrupt: true}, context); - return context; - } - if(!Gun.is.graph(prime, function(node, soul){ - context.nodes[soul] = node; - })){ - cb(context.err = context.err || {err: Gun.log("Invalid graph!"), corrupt: true}, context); - return context; - } - if(context.err){ return cb(context.err, context), context } // if any errors happened in the previous steps, then fail. - Gun.union(gun.__.graph, context.nodes).done(function(err, env){ // now merge prime into the graph - context.err = err || env.err; - cb(context.err, context || {}); - }).change(function(delta){ - if(!Gun.is.soul.on(delta)){ return } - gun.__.on(delta._[Gun._.soul]).emit(Gun.obj.copy(delta)); // this is in reaction to HAM. frozen copy here? - }); - return context; + opt = opt || {}; + + if(!gun.back){ gun = gun.put({}) } + gun = gun.not(function(next, key){ return key? this.put({}).key(key) : this.put({}) }); + if(!val && !Gun.is.value(val)){ return gun } + var obj = {}; + obj['I' + drift + 'R' + Gun.text.random(5)] = val; + return gun.put(obj, cb); } - Chain.blank = function(blank){ - var gun = this; - blank = blank || function(){}; - gun.shot.next(function(next){ - if(gun._.node){ // if it does indeed exist - return next(); // yet fire off the chain - } - blank.call(gun); // call blank - next(); // fire off the chain + Chain.not = function(cb){ + var gun = this, ctx = {}; + cb = cb || function(){}; + + gun._.at('null').once(function(key){ + if(key.soul || gun.__.key.s[key = (key || {}).key]){ return } + // TODO! BUG? Removed a start flag check and tests passed, but is that an edge case? + var kick = function(next){ + if(++c){ return Gun.log("Warning! Multiple `not` resumes!"); } + next._.at('soul').once(function($){ $.N0T = 'KICK SOUL'; gun._.at('soul').emit($) }); + }, chain = gun.chain(), next = cb.call(chain, kick, key), c = -1; + if(Gun.is(next)){ kick(next) } + chain._.at('soul').emit({soul: Gun.roulette.call(chain), empty: true, key: key, N0T: 'SOUL', WAS: 'ON'}); // WAS ON }); + return gun; } Chain.err = function(dud){ // WARNING: dud was depreciated. @@ -566,7 +728,7 @@ return this; } }(Gun.chain = Gun.prototype)); - ;(function(Util){ + ;function Util(Util){ Util.fns = {}; Util.fns.is = function(fn){ return (fn instanceof Function)? true : false } Util.fns.sum = function(done){ // combine with Util.obj.map for some easy parallel async operations! @@ -635,7 +797,14 @@ Util.obj.copy = function(o){ // because http://web.archive.org/web/20140328224025/http://jsperf.com/cloning-an-object/2 return !o? o : JSON.parse(JSON.stringify(o)); // is shockingly faster than anything else, and our data has to be a subset of JSON anyways! } - Util.obj.has = function(o, t){ return Object.prototype.hasOwnProperty.call(o, t) } + Util.obj.has = function(o, t){ return o && Object.prototype.hasOwnProperty.call(o, t) } + Util.obj.empty = function(o, n){ + if(!o){ return true } + return Util.obj.map(o,function(v,i){ + if(n && (i === n || (Util.obj.is(n) && Util.obj.has(n, i)))){ return } + if(i){ return true } + })? false : true; + } Util.obj.map = function(l, c, _){ var u, i = 0, ii = 0, x, r, rr, f = Util.fns.is(c), t = function(k,v){ @@ -667,7 +836,7 @@ } } else { //if(a.test.is(c,l[i])){ return i } // should implement deep equality testing! - if(c === l[i]){ return i } + if(c === l[i]){ return i } // use this for now } } } @@ -675,132 +844,18 @@ } Util.time = {}; Util.time.is = function(t){ return t? t instanceof Date : (+new Date().getTime()) } - }(Gun)); - ;Gun.next = function(){ - var fn = function(cb){ - if(!fn.stack || !fn.stack.length){ - setImmediate(function next(n){ - return (n = (fn.stack||[]).shift() || function(){}), n.back = fn.stack, fn.stack = [], n(function(){ - return (fn.stack = (fn.stack||[]).concat(n.back)), next(); - }); - }); - } if(cb){ - (fn.stack = fn.stack || []).push(cb); - } return fn; - }, setImmediate = setImmediate || function(cb){return setTimeout(cb,0)} - return fn; - } - ;Gun.shot=(function(){ - // I hate the idea of using setTimeouts in my code to do callbacks (promises and sorts) - // as there is no way to guarantee any type of state integrity or the completion of callback. - // However, I have fallen. HAM is suppose to assure side effect free safety of unknown states. - var setImmediate = setImmediate || function(cb){setTimeout(cb,0)} - function Flow(){ - var chain = new Flow.chain(); - chain.$ = function(where){ - (chain._ = chain._ || {})[where] = chain._[where] || []; - chain.$[where] = chain.$[where] || function(fn){ - if(chain.args){ - fn.apply(chain, chain.args); - } else { - (chain._[where]||[]).push(fn); - } - return chain.$; - } - chain.where = where; - return chain; - } - Gun.list.map(Array.prototype.slice.call(arguments), function(where){ chain.$(where) }); - return chain.$; - } - Flow.is = function(flow){ return (Flow instanceof flow)? true : false } - ;Flow.chain=(function(){ - function Chain(){ - if(!(this instanceof Chain)){ - return new Chain(); - } - } - Chain.chain = Chain.prototype; - Chain.chain.pipe = function(a,s,d,f){ - var me = this - , where = me.where - , args = Array.prototype.slice.call(arguments); - setImmediate(function(){ - if(!me || !me._ || !me._[where]){ return } - me.args = args; - while(0 < me._[where].length){ - (me._[where].shift()||function(){}).apply(me, args); - } - // do a done? That would be nice. :) - }); - return me; - } - return Chain; - }()); - return Flow; - }());Gun.shot.chain.chain.fire=Gun.shot.chain.chain.pipe; - ;Gun.on=(function(){ - // events are fundamentally different, being synchronously 1 to N fan out, - // than req/res/callback/promise flow, which are asynchronously 1 to 1 into a sink. - function On(where){ - if(where){ - return (On.event = On.event || On.create())(where); - } - return On.create(); - } - On.is = function(on){ return (On instanceof on)? true : false } - On.create = function(){ - var chain = new On.chain(); - return chain.$ = function(where){ - chain.where = where; - return chain; - } - } - On.sort = Gun.list.sort('i'); - ;On.chain=(function(){ - function Chain(){ - if(!(this instanceof Chain)){ - return new Chain(); - } - } - Chain.chain = Chain.prototype; - Chain.chain.emit = function(what){ - var me = this - , where = me.where - , args = arguments - , on = (me._ = me._ || {})[where] = me._[where] || []; - if(!(me._[where] = Gun.list.map(on, function(hear, i, map){ - if(!hear || !hear.as){ return } - map(hear); - hear.as.apply(hear, args); - }))){ Gun.obj.del(on, where) } - } - Chain.chain.event = function(as, i){ - if(!as){ return } - var me = this - , where = me.where - , args = arguments - , on = (me._ = me._ || {})[where] = me._[where] || [] - , e = {as: as, i: i || 0, off: function(){ return !(e.as = false) }}; - return on.push(e), on.sort(On.sort), e; - } - Chain.chain.once = function(as, i){ - var me = this - , once = function(){ - this.off(); - as.apply(this, arguments) - } - return me.event(once, i) - } - return Chain; - }()); - return On; - }()); + Util.time.now = function(t){ + return (t=t||Util.time.is()) > (Util.time.now.last || -Infinity)? (Util.time.now.last = t) : Util.time.now(t + 1); + return (t = Util.time.is() + Math.random()) > (Util.time.now.last || -Infinity)? + (Util.time.now.last = t) : Util.time.now(); + }; + }; ;(function(schedule){ // maybe use lru-cache schedule.waiting = []; schedule.soonest = Infinity; schedule.sort = Gun.list.sort('when'); schedule.set = function(future){ + if(Infinity <= (schedule.soonest = future)){ return } var now = Gun.time.is(); future = (future <= now)? 0 : (future - now); clearTimeout(schedule.id); @@ -813,7 +868,7 @@ if(!wait){ return } if(wait.when <= now){ if(Gun.fns.is(wait.event)){ - wait.event(); + setTimeout(function(){ wait.event() },0); } } else { soonest = (soonest < wait.when)? soonest : wait.when; @@ -823,141 +878,93 @@ schedule.set(soonest); } Gun.schedule = function(state, cb){ - schedule.waiting.push({when: state, event: cb}); + schedule.waiting.push({when: state, event: cb || function(){}}); if(schedule.soonest < state){ return } schedule.set(state); } }({})); - ;(function(Serializer){ - Gun.ify = function(data, cb){ // TODO: BUG: Modify lists to include HAM state - var gun = Gun.is(this)? this : {} - , nothing, context = Gun.shot(); - context.nodes = {}; - context.seen = []; - context.seen = []; - context('done'); - cb = cb || function(){}; - function ify(data, context, sub){ - sub = sub || {}; - sub.path = sub.path || ''; - context = context || {}; - context.nodes = context.nodes || {}; - if((sub.simple = Gun.is.value(data)) && !(sub._ && Gun.text.is(sub.simple))){ - return data; - } else - if(Gun.obj.is(data)){ - var value = {}, meta = {}, seen - , err = {err: "Metadata does not support external or circular references at " + sub.path, meta: true}; - context.root = context.root || value; - if(seen = ify.seen(context._seen, data)){ - //console.log("seen in _", sub._, sub.path, data); - Gun.log(context.err = err); - return; - } else - if(seen = ify.seen(context.seen, data)){ - //console.log("seen in data", sub._, sub.path, data); - if(sub._){ - Gun.log(context.err = err); - return; - } - meta = Gun.ify.soul.call(gun, meta, seen); - return meta; - } else { - //console.log("seen nowhere", sub._, sub.path, data); - if(sub._){ - context.seen.push({data: data, node: value}); - } else { - value._ = {}; - cb(data, context, sub, context.many.add(function(soul){ - //console.log("What soul did we find?", soul || "random"); - meta[Gun._.soul] = value._[Gun._.soul] = soul = Gun.is.soul.on(data) || soul || Gun.roulette(); - context.nodes[soul] = value; - this.done(); - })); - context.seen.push({data: data, node: value}); - } - } - Gun.obj.map(data, function(val, field){ - var subs = {path: sub.path? sub.path + '.' + field : field, - _: sub._ || (field == Gun._.meta)? true : false }; - val = ify(val, context, subs); - //console.log('>>>>', sub.path + field, 'is', val); - if(context.err){ return true } - if(nothing === val){ return } - // TODO: check field validity - value[field] = val; - }); - if(sub._){ return value } - if(!value._){ return } - return meta; - } else - if(Gun.list.is(data)){ - var unique = {}, edges - , err = {err: "Arrays cause data corruption at " + sub.path, array: true} - edges = Gun.list.map(data, function(val, i, map){ - val = ify(val, context, sub); - if(context.err){ return true } - if(!Gun.obj.is(val)){ - Gun.log(context.err = err); - return true; - } - return Gun.obj.map(val, function(soul, field){ - if(field !== Gun._.soul){ - Gun.log(context.err = err); - return true; - } - if(unique[soul]){ return } - unique[soul] = 1; - map(val); - }); - }); - if(context.err){ return } - return edges; + ;Gun.ify=(function(Serializer){ + function ify(data, cb, opt){ + opt = opt || {}; + cb = cb || function(env, cb){ cb(env.at, Gun.roulette()) }; + var end = function(fn){ + ctx.end = fn || function(){}; + if(ctx.err){ return ctx.end(ctx.err, ctx), ctx.end = function(){} } + unique(ctx); + }, ctx = {}; + if(!data){ return ctx.err = Gun.log('Serializer does not have correct parameters.'), end } + ctx.at = {}; + ctx.root = {}; + ctx.graph = {}; + ctx.queue = []; + ctx.seen = []; + ctx.loop = true; + + ctx.at.path = []; + ctx.at.obj = data; + ctx.at.node = ctx.root; + while(ctx.loop && !ctx.err){ + seen(ctx, ctx.at); + map(ctx, cb); + if(ctx.queue.length){ + ctx.at = ctx.queue.shift(); } else { - context.err = {err: Gun.log("Data type not supported at " + sub.path), invalid: true}; + ctx.loop = false; } } - ify.seen = function(seen, data){ - // unfortunately, using seen[data] = true will cause false-positives for data's children - return Gun.list.map(seen, function(check){ - if(check.data === data){ return check.node } - }); - } - context.many = Gun.fns.sum(function(err){ context('done').fire(context.err, context) }); - context.many.add(function(){ - ify(data, context); - this.done(); - })(); - return context; + return end; } - Gun.ify.state = function(nodes, now){ - var context = {}; - context.nodes = nodes; - context.now = now = (now === 0)? now : now || Gun.time.is(); - Gun.obj.map(context.nodes, function(node, soul){ - if(!node || !soul || !node._ || !node._[Gun._.soul] || node._[Gun._.soul] !== soul){ - return context.err = {err: Gun.log("There is a corruption of nodes and or their souls"), corrupt: true}; - } - var states = node._[Gun._.HAM] = node._[Gun._.HAM] || {}; - Gun.obj.map(node, function(val, field){ - if(field == Gun._.meta){ return } - val = states[field]; - states[field] = (val === 0)? val : val || now; + function map(ctx, cb){ + var rel = function(at, soul){ + at.soul = at.soul || soul; + Gun.list.map(at.back, function(rel){ + rel[Gun._.soul] = at.soul; }); + unique(ctx); // could we remove the setTimeot? + }, it; + Gun.obj.map(ctx.at.obj, function(val, field){ + ctx.at.val = val; + ctx.at.field = field; + it = cb(ctx, rel) || true; + if(field === Gun._.meta){ + ctx.at.node[field] = Gun.obj.copy(val); // TODO: BUG! Is this correct? + return; + } + if(false && notValidField(field)){ // TODO: BUG! Do later for ACID "consistency" guarantee. + return ctx.err = {err: Gun.log('Invalid field name on ' + ctx.at.path.join('.'))}; + } + if(!Gun.is.value(val)){ + var at = {obj: val, node: {}, back: [], path: [field]}, tmp = {}, was; + at.path = (ctx.at.path||[]).concat(at.path || []); + if(!Gun.obj.is(val)){ + return ctx.err = {err: Gun.log('Invalid value at ' + at.path.join('.') + '!' )}; + } + if(was = seen(ctx, at)){ + tmp[Gun._.soul] = Gun.is.soul.on(was.node) || null; + (was.back = was.back || []).push(ctx.at.node[field] = tmp); + } else { + ctx.queue.push(at); + tmp[Gun._.soul] = null; + at.back.push(ctx.at.node[field] = tmp); + } + } else { + ctx.at.node[field] = Gun.obj.copy(val); + } }); - return context; + if(!it){ cb(ctx, rel) } } - Gun.ify.soul = function(to, from){ - var gun = this; - to = to || {}; - if(Gun.is.soul.on(from)){ - to[Gun._.soul] = from._[Gun._.soul]; - return to; - } - to[Gun._.soul] = Gun.roulette.call(gun); - return to; + function unique(ctx){ + if(ctx.err || !Gun.list.map(ctx.seen, function(at){ + if(!at.soul){ return true } + }) && !ctx.loop){ return ctx.end(ctx.err, ctx), ctx.end = function(){} } } - }()); + function seen(ctx, at){ + return Gun.list.map(ctx.seen, function(has){ + if(at.obj === has.obj){ return has } + }) || (ctx.seen.push(at) && false); + } + return ify; + }({})); if(typeof window !== "undefined"){ window.Gun = Gun; } else { @@ -965,125 +972,172 @@ } var root = this || {}; // safe for window, global, root, and 'use strict'. root.console = root.console || {log: function(s){ return s }}; // safe for old browsers - var console = {log: Gun.log = function(s){return (Gun.log.verbose && root.console.log.apply(root.console, arguments)), s}}; + var console = { + log: Gun.log = function(s){return (Gun.log.verbose && root.console.log.apply(root.console, arguments)), s}, + Log: function(s){return (!Gun.log.squelch && root.console.log.apply(root.console, arguments)), s} + }; }({})); ;(function(tab){ if(!this.Gun){ return } if(!window.JSON){ throw new Error("Include JSON first: ajax.cdnjs.com/ajax/libs/json2/20110223/json2.js") } // for old IE use Gun.on('opt').event(function(gun, opt){ - window.tab = tab; // for debugging purposes opt = opt || {}; + var tab = gun.__.tab = gun.__.tab || {}; tab.headers = opt.headers || {}; - tab.headers['gun-sid'] = tab.headers['gun-sid'] || Gun.text.random(); + tab.headers['gun-sid'] = tab.headers['gun-sid'] || Gun.text.random(); // stream id tab.prefix = tab.prefix || opt.prefix || 'gun/'; tab.prekey = tab.prekey || opt.prekey || ''; tab.prenode = tab.prenode || opt.prenode || '_/nodes/'; - tab.load = tab.load || function(key, cb, o){ + window.tab = tab; window.store = store; + tab.get = tab.get || function(key, cb, opt){ if(!key){ return } cb = cb || function(){}; - o = o || {}; - o.url = o.url || {}; - o.headers = Gun.obj.copy(tab.headers); - if(key[Gun._.soul]){ - o.url.query = key; + cb.GET = true; + (opt = opt || {}).url = opt.url || {}; + opt.headers = Gun.obj.copy(tab.headers); + if(Gun.is.soul(key)){ + opt.url.query = key; } else { - o.url.pathname = '/' + key; + opt.url.pathname = '/' + key; } - Gun.log("gun load", key); + Gun.log("tab get --->", key); (function local(key, cb){ - var node, lkey = key[Gun._.soul]? tab.prefix + tab.prenode + key[Gun._.soul] - : tab.prefix + tab.prekey + key - if((node = store.get(lkey)) && node[Gun._.soul]){ return local(node, cb) } - if(cb.node = node){ Gun.log('via cache', key); setTimeout(function(){cb(null, node)},0) } + var path = (path = Gun.is.soul(key))? tab.prefix + tab.prenode + path + : tab.prefix + tab.prekey + key, node = store.get(path), graph, soul; + if(Gun.is.node(node)){ + (cb.graph = cb.graph || {} + )[soul = Gun.is.soul.on(node)] = (graph = {})[soul] = cb.node = node; + cb(null, graph); + (graph = {})[soul] = Gun.union.pseudo(soul); // end. + return cb(null, graph); + } else + if(Gun.obj.is(node)){ + Gun.obj.map(node, function(rel){ if(Gun.is.soul(rel)){ local(rel, cb) } }); + cb(null, {}); + } }(key, cb)); - Gun.obj.map(gun.__.opt.peers, function(peer, url){ - request(url, null, function(err, reply){ - Gun.log('via', url, key, reply.body); - if(err || !reply || (err = reply.body && reply.body.err)){ - cb({err: Gun.log(err || "Error: Load failed through " + url) }); - } else { - if(!key[Gun._.soul] && (cb.soul = Gun.is.soul.on(reply.body))){ - var meta = {}; - meta[Gun._.soul] = cb.soul; - store.set(tab.prefix + tab.prekey + key, meta); + if(!(cb.local = opt.local)){ + Gun.obj.map(opt.peers || gun.__.opt.peers, function(peer, url){ var p = {}; + request(url, null, tab.error(cb, "Error: Get failed through " + url, function(reply){ + if(!p.graph && !Gun.obj.empty(cb.graph)){ // if we have local data + tab.put(p.graph = cb.graph, function(e,r){ // then sync it if we haven't already + Gun.log("Stateless handshake sync:", e, r); + }, {peers: tab.peers(url)}); // to the peer. // TODO: This forces local to flush again, not necessary. + // TODO: What about syncing our keys up? } - if(cb.node){ - if(!cb.graph && (cb.soul = Gun.is.soul.on(cb.node))){ // if we have a cache locally - cb.graph = {}; // we want to make sure we did not go offline while sending updates - cb.graph[cb.soul] = cb.node; // so turn the node into a graph, and sync the latest state. - tab.set(cb.graph, function(e,r){ Gun.log("Stateless handshake sync:", e, r) }); - if(!key[Gun._.soul]){ tab.key(key, cb.soul, function(e,r){}) }//TODO! BUG: this is really bad implicit behavior! - } - return gun.union(reply.body); - } - cb(null, reply.body); - } - }, o); - cb.peers = true; - }); tab.peers(cb); + Gun.is.graph(reply.body, function(node, soul){ // make sure for each received node + if(!Gun.is.soul(key)){ tab.key(key, soul, function(){}, {local: true}) } // that the key points to it. + }); + setTimeout(function(){ tab.put(reply.body, function(){}, {local: true}) },1); // and flush the in memory nodes of this graph to localStorage after we've had a chance to union on it. + }), opt); + cb.peers = true; + }); + } tab.peers(cb); } - tab.key = function(key, soul, cb){ - var meta = {}; - meta[Gun._.soul] = soul = Gun.text.is(soul)? soul : (soul||{})[Gun._.soul]; - if(!soul){ return cb({err: Gun.log("No soul!")}) } - store.set(tab.prefix + tab.prekey + key, meta); - Gun.obj.map(gun.__.opt.peers, function(peer, url){ - request(url, meta, function(err, reply){ - if(err || !reply || (err = reply.body && reply.body.err)){ - // tab.key(key, soul, cb); // naive implementation of retry TODO: BUG: need backoff and anti-infinite-loop! - cb({err: Gun.log(err || "Error: Key failed to be made on " + url) }); - } else { - cb(null, reply.body); - } - }, {url: {pathname: '/' + key }, headers: tab.headers}); - cb.peers = true; - }); tab.peers(cb); - } - tab.set = tab.set || function(nodes, cb){ + tab.put = tab.put || function(graph, cb, opt){ cb = cb || function(){}; - // TODO: batch and throttle later. - // tab.store.set(cb.id = 'send/' + Gun.text.random(), nodes); // TODO: store SENDS until SENT. - Gun.obj.map(nodes, function(node, soul){ - if(!gun || !gun.__ || !gun.__.graph || !gun.__.graph[soul]){ return } - store.set(tab.prefix + tab.prenode + soul, gun.__.graph[soul]); - }); - Gun.obj.map(gun.__.opt.peers, function(peer, url){ - request(url, nodes, function(err, reply){ - if(err || !reply || (err = reply.body && reply.body.err)){ - return cb({err: Gun.log(err || "Error: Set failed on " + url) }); - } else { - cb(null, reply.body); - } - }, {headers: tab.headers}); - cb.peers = true; - }); tab.peers(cb); - Gun.obj.map(nodes, function(node, soul){ - gun.__.on(soul).emit(node); + opt = opt || {}; + Gun.is.graph(graph, function(node, soul){ + if(!opt.local){ gun.__.on(soul).emit(node) } // TODO: Should this be in core? + if(!gun.__.graph[soul]){ return } + store.put(tab.prefix + tab.prenode + soul, gun.__.graph[soul]); }); + if(!(cb.local = opt.local)){ + Gun.obj.map(opt.peers || gun.__.opt.peers, function(peer, url){ + request(url, graph, tab.error(cb, "Error: Put failed on " + url), {headers: tab.headers}); + cb.peers = true; + }); + } tab.peers(cb); } - tab.peers = function(cb){ - if(cb && !cb.peers){ // there are no peers! this is a local only instance - setTimeout(function(){console.log("Warning! You have no peers to connect to!");cb()},1); + tab.key = tab.key || function(key, soul, cb, opt){ + var meta = {}; + opt = opt || {}; + cb = cb || function(){}; + meta[Gun._.soul] = soul = Gun.is.soul(soul) || soul; + if(!soul){ return cb({err: Gun.log("No soul!")}) } + (function(souls){ + (souls = store.get(tab.prefix + tab.prekey + key) || {})[soul] = meta; + store.put(tab.prefix + tab.prekey + key, souls); + }()); + if(!(cb.local = opt.local || opt.soul)){ + Gun.obj.map(opt.peers || gun.__.opt.peers, function(peer, url){ + request(url, meta, tab.error(cb, "Error: Key failed to be made on " + url), {url: {pathname: '/' + key }, headers: tab.headers}); + cb.peers = true; + }); + } tab.peers(cb); + } + tab.error = function(cb, error, fn){ + return function(err, reply){ + reply.body = reply.body || reply.chunk || reply.end || reply.write; + if(err || !reply || (err = reply.body && reply.body.err)){ + return cb({err: Gun.log(err || error) }); + } + if(fn){ fn(reply) } + cb(null, reply.body); } } - tab.set.defer = {}; - request.createServer(function(req, res){ - // Gun.log("client server received request", req); - if(!req.body){ return } - if(Gun.is.node(req.body) || Gun.is.graph(req.body)){ - gun.union(req.body); // TODO: BUG? Interesting, this won't update localStorage because .set isn't called? + tab.peers = function(cb, o){ + if(Gun.text.is(cb)){ return (o = {})[cb] = {}, o } + if(cb && !cb.peers){ setTimeout(function(){ + if(!cb.local){ console.log("Warning! You have no peers to connect to!") } + if(!(cb.graph || cb.node)){ cb() } + },1)} + } + tab.server = tab.server || function(req, res){ + if(!req || !res || !req.url || !req.method){ return } + req.url = req.url.href? req.url : document.createElement('a'); + req.url.href = req.url.href || req.url; + req.url.key = (req.url.pathname||'').replace(tab.server.regex,'').replace(/^\//i,'') || ''; + req.method = req.body? 'put' : 'get'; + if('get' == req.method){ return tab.server.get(req, res) } + if('put' == req.method || 'post' == req.method){ return tab.server.put(req, res) } + } + tab.server.json = 'application/json'; + tab.server.regex = gun.__.opt.route = gun.__.opt.route || opt.route || /^\/gun/i; + tab.server.get = function(){} + tab.server.put = function(req, cb){ + var reply = {headers: {'Content-Type': tab.server.json}}; + if(!req.body){ return cb({headers: reply.headers, body: {err: "No body"}}) } + // TODO: Re-emit message to other peers if we have any non-overlaping ones. + if(tab.server.put.key(req, cb)){ return } + if(Gun.is.node(req.body) || Gun.is.graph(req.body, function(node, soul){ + gun.__.flag.end[soul] = true; // TODO! Put this in CORE not in TAB driver? + })){ + //console.log("tran.put", req.body); + if(req.err = Gun.union(gun, req.body, function(err, ctx){ + if(err){ return cb({headers: reply.headers, body: {err: err || "Union failed."}}) } + var ctx = ctx || {}; ctx.graph = {}; + Gun.is.graph(req.body, function(node, soul){ ctx.graph[soul] = gun.__.graph[soul] }); + gun.__.opt.hooks.put(ctx.graph, function(err, ok){ + if(err){ return cb({headers: reply.headers, body: {err: err || "Failed."}}) } + cb({headers: reply.headers, body: {ok: ok || "Persisted."}}); + }, {local: true}); + }).err){ cb({headers: reply.headers, body: {err: req.err || "Union failed."}}) } } + } + tab.server.put.key = function(req, cb){ + if(!req || !req.url || !req.url.key || !Gun.obj.has(req.body, Gun._.soul)){ return } + var index = req.url.key, soul = Gun.is.soul(req.body); + //console.log("tran.key", index, req.body); + gun.key(index, function(err, reply){ + if(err){ return cb({headers: {'Content-Type': tab.server.json}, body: {err: err}}) } + cb({headers: {'Content-Type': tab.server.json}, body: reply}); // TODO: Fix so we know what the reply is. + }, soul); + return true; + } + Gun.obj.map(gun.__.opt.peers, function(){ // only create server if peers and do it once by returning immediately. + return tab.request = tab.request || request.createServer(tab.server) || true; }); - gun.__.opt.hooks.load = gun.__.opt.hooks.load || tab.load; - gun.__.opt.hooks.set = gun.__.opt.hooks.set || tab.set; + gun.__.opt.hooks.get = gun.__.opt.hooks.get || tab.get; + gun.__.opt.hooks.put = gun.__.opt.hooks.put || tab.put; gun.__.opt.hooks.key = gun.__.opt.hooks.key || tab.key; }); var store = (function(){ function s(){} var store = window.localStorage || {setItem: function(){}, removeItem: function(){}, getItem: function(){}}; - s.set = function(key, val){ return store.setItem(key, Gun.text.ify(val)) } - s.get = function(key){ return Gun.obj.ify(store.getItem(key)) } + s.put = function(key, val){ return store.setItem(key, Gun.text.ify(val)) } + s.get = function(key){ return Gun.obj.ify(store.getItem(key) || null) } s.del = function(key){ return store.removeItem(key) } return s; }()); @@ -1095,15 +1149,20 @@ if(!opt.base){ return } r.transport(opt, cb); } - r.createServer = function(fn){ (r.createServer = fn).on = true } + r.createServer = function(fn){ r.createServer.s.push(fn) } + r.createServer.ing = function(req, cb){ + var i = r.createServer.s.length; + while(i--){ (r.createServer.s[i] || function(){})(req, cb) } + } + r.createServer.s = []; r.transport = function(opt, cb){ //Gun.log("TRANSPORT:", opt); if(r.ws(opt, cb)){ return } r.jsonp(opt, cb); } r.ws = function(opt, cb){ - var ws = window.WebSocket || window.mozWebSocket || window.webkitWebSocket; - if(!ws){ return } + var ws, WS = window.WebSocket || window.mozWebSocket || window.webkitWebSocket; + if(!WS){ return } if(ws = r.ws.peers[opt.base]){ if(!ws.readyState){ return setTimeout(function(){ r.ws(opt, cb) },10), true } var req = {}; @@ -1112,17 +1171,18 @@ if(opt.url){ req.url = opt.url } req.headers = req.headers || {}; r.ws.cbs[req.headers['ws-rid'] = 'WS' + (+ new Date()) + '.' + Math.floor((Math.random()*65535)+1)] = function(err,res){ - delete r.ws.cbs[req.headers['ws-rid']]; + if(res.body || res.end){ delete r.ws.cbs[req.headers['ws-rid']] } cb(err,res); } ws.send(JSON.stringify(req)); return true; } if(ws === false){ return } - ws = r.ws.peers[opt.base] = new WebSocket(opt.base.replace('http','ws')); + ws = r.ws.peers[opt.base] = new WS(opt.base.replace('http','ws')); ws.onopen = function(o){ r.ws(opt, cb) }; - ws.onclose = function(c){ + ws.onclose = window.onbeforeunload = function(c){ if(!c){ return } + if(ws && ws.close instanceof Function){ ws.close() } if(1006 === c.code){ // websockets cannot be used ws = r.ws.peers[opt.base] = false; r.transport(opt, cb); @@ -1139,7 +1199,7 @@ res.headers = res.headers || {}; if(res.headers['ws-rid']){ return (r.ws.cbs[res.headers['ws-rid']]||function(){})(null, res) } Gun.log("We have a pushed message!", res); - if(res.body){ r.createServer(res, function(){}) } // emit extra events. + if(res.body){ r.createServer.ing(res, function(){}) } // emit extra events. }; ws.onerror = function(e){ Gun.log(e); }; return true; @@ -1184,7 +1244,7 @@ while(reply.body && reply.body.length && reply.body.shift){ // we're assuming an array rather than chunk encoding. :( var res = reply.body.shift(); //Gun.log("-- go go go", res); - if(res && res.body){ r.createServer(res, function(){}) } // emit extra events. + if(res && res.body){ r.createServer.ing(res, function(){}) } // emit extra events. } }); }, res.headers.poll); @@ -1229,4 +1289,4 @@ } return r; }()); -}({})); +}({})); \ No newline at end of file diff --git a/index.html b/index.html index 93a1d889..6f22b360 100644 --- a/index.html +++ b/index.html @@ -380,6 +380,11 @@ html, body { Fork me on GitHub + +
    diff --git a/lib/aws.js b/lib/aws.js index bd1daf51..2eed68d1 100644 --- a/lib/aws.js +++ b/lib/aws.js @@ -6,7 +6,6 @@ } var s = this; s.on = a.on.create(); - s.mime = require('mime'); s.AWS = require('aws-sdk'); s.config = {}; opt = opt || {}; @@ -31,14 +30,14 @@ }; s3.id = function(m){ return m.Bucket +'/'+ m.Key } s3.chain = s3.prototype; - s3.chain.put = function(key, o, cb, m){ + s3.chain.PUT = function(key, o, cb, m){ if(!key){ return } m = m || {} m.Bucket = m.Bucket || this.config.bucket; m.Key = m.Key || key; if(a.obj.is(o) || a.list.is(o)){ m.Body = a.text.ify(o); - m.ContentType = this.mime.lookup('json') + m.ContentType = 'application/json'; } else { m.Body = a.text.is(o)? o : a.text.ify(o); } @@ -49,7 +48,7 @@ }); return this; } - s3.chain.get = function(key, cb, o){ + s3.chain.GET = function(key, cb, o){ if(!key){ return } var s = this , m = { @@ -74,7 +73,7 @@ if(e || !r){ return s.on(id).emit(e) } r.Text = r.text = t = (r.Body||r.body||'').toString('utf8'); r.Type = r.type = r.ContentType || (r.headers||{})['content-type']; - if(r.type && 'json' === s.mime.extension(r.type)){ + if(r.type && 'application/json' === r.type){ d = a.obj.ify(t); } m = r.Metadata; @@ -119,7 +118,7 @@ } this.S3().listObjects(m, function(e,r){ //a.log('list',e); - a.list.each((r||{}).Contents, function(v){console.log(v)}); + a.list.map((r||{}).Contents, function(v){console.log(v)}); //a.log('---end list---'); if(!a.fns.is(cb)) return; cb(e,r); diff --git a/lib/file.js b/lib/file.js index 96c5fac2..ff24c27a 100644 --- a/lib/file.js +++ b/lib/file.js @@ -2,39 +2,87 @@ // modified by Mark to be part of core for convenience // twas not designed for production use // only simple local development. +var Gun = require('../gun'), + file = {}; -var Gun = require('../gun'), file = {}; - -Gun.on('opt').event(function(gun, opts){ - if(opts.s3 && opts.s3.key){ return } // don't use this plugin if S3 is being used. - - opts.file = opts.file || 'data.json'; - var fs = require('fs'); - file.raw = file.raw || (fs.existsSync||require('path').existsSync)(opts.file)? fs.readFileSync(opts.file).toString() : null; - var all = file.all = file.all || Gun.obj.ify(file.raw || {nodes: {}, keys: {}}); - +Gun.on('opt').event(function(gun, opts) { + if ((opts.file === false) || (opts.s3 && opts.s3.key)) { + return; // don't use this plugin if S3 is being used. + } + console.log("WARNING! This `file.js` module for gun is intended only for local development testing!") + opts.file = opts.file || 'data.json'; + var fs = require('fs'); + file.raw = file.raw || (fs.existsSync || require('path').existsSync)(opts.file) ? fs.readFileSync(opts.file).toString() : null; + var all = file.all = file.all || Gun.obj.ify(file.raw || { + nodes: {}, + keys: {} + }); + all.keys = all.keys || {}; + all.nodes = all.nodes || {}; gun.opt({hooks: { - load: function(key, cb, options){ - if(Gun.obj.is(key) && key[Gun._.soul]){ - return cb(null, all.nodes[key[Gun._.soul]]); - } - cb(null, all.nodes[all.keys[key]]); - } - ,set: function(graph, cb){ - all.nodes = gun.__.graph; - /*for(n in all.nodes){ // this causes some divergence problems, so removed for now till later when it can be fixed. - for(k in all.nodes[n]){ - if(all.nodes[n][k] === null){ - delete all.nodes[n][k]; + get: function get(key, cb, o){ + var graph, soul; + if(soul = Gun.is.soul(key)){ + if(all.nodes[soul]){ + (graph = {})[soul] = all.nodes[soul]; + cb(null, graph); + (graph = {})[soul] = Gun.union.pseudo(soul); + cb(null, graph); // end. + } + return; + } + Gun.obj.map(all.keys[key], function(rel){ + if(Gun.is.soul(rel)){ get(soul = rel, cb, o) } + }); + return soul? cb(null, {}) : cb(null, null); + }, + put: function(graph, cb, o){ + all.nodes = gun.__.graph; + fs.writeFile(opts.file, Gun.text.ify(all), cb); + }, + key: function(key, soul, cb, o){ + var meta = {}; + meta[Gun._.soul] = soul = Gun.is.soul(soul) || soul; + ((all.keys = all.keys || {})[key] = all.keys[key] || {})[soul] = meta; + fs.writeFile(opts.file, Gun.text.ify(all), cb); + }, + all: function(list, opt, cb) { + opt = opt || {}; + opt.from = opt.from || ''; + opt.start = opt.from + (opt.start || ''); + if(opt.end){ opt.end = opt.from + opt.end } + var match = {}; + cb = cb || function(){}; + Gun.obj.map(list, function(soul, key){ + var end = opt.end || key; + if(key.indexOf(opt.from) === 0 && opt.start <= key && (key <= end || key.indexOf(end) === 0)){ + if(opt.upto){ + if(key.slice(opt.from.length).indexOf(opt.upto) === -1){ + yes(soul, key); + } + } else { + yes(soul, key); } } - }*/ - fs.writeFile(opts.file, Gun.text.ify(all), cb); - } - ,key: function(key, soul, cb){ - all.keys[key] = soul; - fs.writeFile(opts.file, Gun.text.ify(all), cb); - } + }); + function yes(soul, key){ + cb(key); + match[key] = {}; + match[key][Gun._.soul] = soul; + } + return match; + } }}, true); - -}); + gun.all = gun.all || function(url, cb) { + url = require('url').parse(url, true); + var r = gun.__.opt.hooks.all(all.keys, { + from: url.pathname, + upto: url.query['*'], + start: url.query['*>'], + end: url.query['*<'] + }); + console.log("All please", url.pathname, url.query['*'], r); + cb = cb || function() {}; + cb(null, r); + } +}); \ No newline at end of file diff --git a/lib/group.js b/lib/group.js deleted file mode 100644 index 68d0eb8e..00000000 --- a/lib/group.js +++ /dev/null @@ -1,18 +0,0 @@ -var Gun = Gun || require('../gun'); - -Gun.chain.group = function(obj, cb, opt){ - var gun = this; - opt = opt || {}; - cb = cb || function(){}; - gun = gun.set({}); // insert assumes a graph node. So either create it or merge with the existing one. - var error, item = gun.chain().set(obj, function(err){ // create the new item in its own context. - error = err; // if this happens, it should get called before the .get - }).get(function(val){ - if(error){ return cb.call(gun, error) } // which in case it is, allows us to fail fast. - var add = {}, soul = Gun.is.soul.on(val); - if(!soul){ return cb.call(gun, {err: Gun.log("No soul!")}) } - add[soul] = val; // other wise, let's then - gun.set(add, cb); // merge with the graph node. - }); - return gun; -}; \ No newline at end of file diff --git a/lib/http.js b/lib/http.js index af7fd024..68216bb5 100644 --- a/lib/http.js +++ b/lib/http.js @@ -21,7 +21,7 @@ module.exports = function(req, res, next){ res.statusCode = reply.statusCode || reply.status; } if(reply.headers){ - if(!res._headerSent){ + if(!(res.headersSent || res.headerSent || res._headerSent || res._headersSent)){ Gun.obj.map(reply.headers, function(val, field){ res.setHeader(field, val); }); @@ -47,4 +47,4 @@ module.exports = function(req, res, next){ post(null, body); }); form.parse(req); -} +} \ No newline at end of file diff --git a/lib/list.js b/lib/list.js index a433daf7..7967c33e 100644 --- a/lib/list.js +++ b/lib/list.js @@ -3,20 +3,20 @@ var Gun = Gun || require('../gun'); Gun.chain.list = function(cb, opt){ opt = opt || {}; cb = cb || function(){}; - var gun = this.set({}); // insert assumes a graph node. So either create it or merge with the existing one. + var gun = this.put({}); // insert assumes a graph node. So either create it or merge with the existing one. gun.last = function(obj, cb){ var last = gun.path('last'); if(!arguments.length){ return last } - return gun.path('last').set(null).set(obj).get(function(val){ // warning! these are not transactional! They could be. + return gun.path('last').put(null).put(obj).val(function(val){ // warning! these are not transactional! They could be. console.log("last is", val); - last.path('next').set(this._.node, cb); + last.path('next').put(this._.node, cb); }); } gun.first = function(obj, cb){ var first = gun.path('first'); if(!arguments.length){ return first } - return gun.path('first').set(null).set(obj).get(function(){ // warning! these are not transactional! They could be. - first.path('prev').set(this._.node, cb); + return gun.path('first').put(null).put(obj).val(function(){ // warning! these are not transactional! They could be. + first.path('prev').put(this._.node, cb); }); } return gun; @@ -29,24 +29,24 @@ Gun.chain.list = function(cb, opt){ Gun.log.verbose = true; var list = gun.list(); - list.last({name: "Mark Nadal", type: "human", age: 23}).get(function(val){ + list.last({name: "Mark Nadal", type: "human", age: 23}).val(function(val){ //console.log("oh yes?", val, '\n', this.__.graph); }); - list.last({name: "Amber Cazzell", type: "human", age: 23}).get(function(val){ + list.last({name: "Amber Cazzell", type: "human", age: 23}).val(function(val){ //console.log("oh yes?", val, '\n', this.__.graph); }); - list.list().last({name: "Hobbes", type: "kitten", age: 4}).get(function(val){ + list.list().last({name: "Hobbes", type: "kitten", age: 4}).val(function(val){ //console.log("oh yes?", val, '\n', this.__.graph); }); - list.list().last({name: "Skid", type: "kitten", age: 2}).get(function(val){ + list.list().last({name: "Skid", type: "kitten", age: 2}).val(function(val){ //console.log("oh yes?", val, '\n', this.__.graph); }); - setTimeout(function(){ list.get(function(val){ + setTimeout(function(){ list.val(function(val){ console.log("the list!", list.__.graph); return; - list.path('first').get(Gun.log) - .path('next').get(Gun.log) - .path('next').get(Gun.log); + list.path('first').val(Gun.log) + .path('next').val(Gun.log) + .path('next').val(Gun.log); })}, 1000); return; diff --git a/lib/radix.js b/lib/radix.js new file mode 100644 index 00000000..c75dcba0 --- /dev/null +++ b/lib/radix.js @@ -0,0 +1,104 @@ +function radix(r){ + r = r || {}; + var u, n = null, c = 0; + function get(p){ + var v = match(p, r); + return v; + } + function match(path, tree, v){ + if(!Gun.obj.map(tree, function(val, key){ + if(key[0] !== path[0]){ return } + var i = 1; + while(key[i] === path[i] && path[i]){ i++ } + if(key = key.slice(i)){ // recurse + console.log("match", key, i) + v = {sub: tree, pre: path.slice(0, i), val: val, post: key, path: path.slice(i) }; + } else { // replace + console.log("matching", path, key, i); + v = match(path.slice(i), val); + } + return true; + })){ console.log("matched", tree, path); v = {sub: tree, path: path} } // insert + return v; + } + function rebalance(ctx, val){ + console.log("rebalance", ctx, val); + if(!ctx.post){ return ctx.sub[ctx.path] = val } + ctx.sub[ctx.pre] = ctx.sub[ctx.pre] || (ctx.post? {} : ctx.val || {}); + ctx.sub[ctx.pre][ctx.path] = val; + if(ctx.post){ + ctx.sub[ctx.pre][ctx.post] = ctx.val; + delete ctx.sub[ctx.pre + ctx.post]; + } + } + function set(p, val){ + rebalance(match(p, r), val); + console.log('-------------------------'); + return r; + } + return function(p, val){ + return (1 < arguments.length)? set(p, val) : get(p || ''); + } +} // IT WORKS!!!!!! + +var rad = radix({}); + +rad('user/marknadal', {'#': 'asdf'}); +rad('user/ambercazzell', {'#': 'dafs'}); +rad('user/taitforrest', {'#': 'sadf'}); +rad('user/taitveronika', {'#': 'fdsa'}); +rad('user/marknadal', {'#': 'foo'}); + +/* + +function radix(r){ + var u, n = null, c = 0; + r = r || {}; + function get(){ + + } + function match(p, l, cb){ + cb = cb || function(){}; + console.log("LETS DO THIS", p, l); + if(!Gun.obj.map(l, function(v, k){ + if(k[0] === p[0]){ + var i = 1; + while(k[i] === p[i] && p[i]){ i++ } + k = k.slice(i); + if(k){ + cb(p.slice(0, i), v, k, l, p.slice(i)); + } else { + match(p.slice(i), v, cb); + } + return 1; + } + })){ cb(p, l, null, l) } + } + function set(p, val){ + match(p, r, function(pre, data, f, rr, pp){ + if(f === null){ + rr[pre] = val; + return; + } + console.log("Match?", c, pre, data, f); + rr[pre] = r[pre] || (f? {} : data || {}); + rr[pre][pp] = val; + if(f){ + rr[pre][f] = data; + delete rr[pre + f]; + } + }); + return r; + } + return function(p, val){ + return (1 < arguments.length)? set(p, val) : get(p || ''); + } +} // IT WORKS!!!!!! + +var rad = radix({}); + +rad('user/marknadal', {'#': 'asdf'}); +//rad('user/ambercazzell', {'#': 'dafs'}); +//rad('user/taitforrest', {'#': 'sadf'}); +//rad('user/taitveronika', {'#': 'fdsa'}); +*/ \ No newline at end of file diff --git a/lib/s3.js b/lib/s3.js index a8e8d6cb..b2509d9c 100644 --- a/lib/s3.js +++ b/lib/s3.js @@ -12,28 +12,58 @@ gun.__.opt.batch = opt.batch || gun.__.opt.batch || 10; gun.__.opt.throttle = opt.throttle || gun.__.opt.throttle || 15; gun.__.opt.disconnect = opt.disconnect || gun.__.opt.disconnect || 5; - s3.load = s3.load || function(key, cb, opt){ + s3.get = s3.get || function(key, cb, opt){ if(!key){ return } cb = cb || function(){}; - opt = opt || {}; + (opt = opt || {}).ctx = opt.ctx || {}; + opt.ctx.load = opt.ctx.load || {}; if(key[Gun._.soul]){ - key = s3.prefix + s3.prenode + key[Gun._.soul]; + key = s3.prefix + s3.prenode + Gun.is.soul(key); } else { key = s3.prefix + s3.prekey + key; } - s3.get(key, function(err, data, text, meta){ + s3.GET(key, function(err, data, text, meta){ Gun.log('via s3', key, err); - if(meta && meta[Gun._.soul]){ - return s3.load(meta, cb); // SUPER SUPER IMPORTANT TODO!!!! Make this load via GUN in case soul is already cached! - // HUGE HUGE HUGE performance gains could come from the above line being updated! (not that this module is performant). - } if(err && err.statusCode == 404){ err = null; // we want a difference between 'unfound' (data is null) and 'error' (auth is wrong). } - cb(err, data); + // TODO: optimize KEY command to not write data if there is only one soul (which is common). + if(meta && (meta.key || meta[Gun._.soul])){ + if(err){ return cb(err) } + if(meta.key && Gun.obj.is(data) && !Gun.is.node(data)){ + return Gun.obj.map(data, function(rel, soul){ + if(!(soul = Gun.is.soul(rel))){ return } + opt.ctx.load[soul] = false; + s3.get(rel, cb, {next: 's3', ctx: opt.ctx}); // TODO: way faster if you use cache. + }); + } + if(meta[Gun._.soul]){ + return s3.get(meta, cb); // TODO: way faster if you use cache. + } + return cb({err: Gun.log('Cannot determine S3 key data!')}); + } + if(data){ + meta.soul = Gun.is.soul.on(data); + if(!meta.soul){ + err = {err: Gun.log('No soul on node S3 data!')}; + } + } else { + return cb(err, null); + } + if(err){ return cb(err) } + opt.ctx.load[meta.soul] = true; + var graph = {}; + graph[meta.soul] = data; + cb(null, graph); + (graph = {})[meta.soul] = Gun.union.pseudo(meta.soul); + cb(null, graph); + if(Gun.obj.map(opt.ctx.load, function(loaded, soul){ + if(!loaded){ return true } + })){ return } // return IF we have nodes still loading. + cb(null, {}); }); } - s3.set = s3.set || function(nodes, cb){ + s3.put = s3.put || function(nodes, cb){ s3.batching += 1; cb = cb || function(){}; cb.count = 0; @@ -44,7 +74,7 @@ Gun.obj.map(nodes, function(node, soul){ cb.count += 1; batch[soul] = (batch[soul] || 0) + 1; - //Gun.log("set listener for", next + ':' + soul, batch[soul], cb.count); + //Gun.log("put listener for", next + ':' + soul, batch[soul], cb.count); s3.on(next + ':' + soul).event(function(){ cb.count -= 1; //Gun.log("transaction", cb.count); @@ -55,14 +85,14 @@ }); }); if(gun.__.opt.batch < s3.batching){ - return s3.set.now(); + return s3.put.now(); } if(!gun.__.opt.throttle){ - return s3.set.now(); + return s3.put.now(); } - s3.wait = s3.wait || setTimeout(s3.set.now, gun.__.opt.throttle * 1000); // in seconds + s3.wait = s3.wait || setTimeout(s3.put.now, gun.__.opt.throttle * 1000); // in seconds } - s3.set.now = s3.set.now || function(){ + s3.put.now = s3.put.now || function(){ clearTimeout(s3.wait); s3.batching = 0; s3.wait = null; @@ -71,7 +101,7 @@ s3.next = Gun.time.is(); Gun.obj.map(batch, function put(exists, soul){ var node = gun.__.graph[soul]; // the batch does not actually have the nodes, but what happens when we do cold data? Could this be gone? - s3.put(s3.prefix + s3.prenode + soul, node, function(err, reply){ + s3.PUT(s3.prefix + s3.prenode + soul, node, function(err, reply){ Gun.log("s3 put reply", soul, err, reply); if(err || !reply){ put(exists, soul); // naive implementation of retry TODO: BUG: need backoff and anti-infinite-loop! @@ -90,25 +120,31 @@ s3.wait = s3.wait || null; s3.key = s3.key || function(key, soul, cb){ - var meta = {}; - meta[Gun._.soul] = soul = Gun.text.is(soul)? soul : (soul||{})[Gun._.soul]; + if(!key){ + return cb({err: "No key!"}); + } if(!soul){ return cb({err: "No soul!"}); } - s3.put(s3.prefix + s3.prekey + key, '', function(err, reply){ // key is 2 bytes??? Should be smaller. Wait HUH? What did I mean by this? - Gun.log("s3 put reply", soul, err, reply); - if(err || !reply){ - s3.key(key, soul, cb); // naive implementation of retry TODO: BUG: need backoff and anti-infinite-loop! - return; - } - cb(); - }, {Metadata: meta}); + var path = s3.prefix + s3.prekey + key, meta = {key: '0.2'}, rel = {}; + meta[Gun._.soul] = rel[Gun._.soul] = soul = Gun.is.soul(soul) || soul; + s3.GET(path, function(err, data, text, _){ + var souls = data || {}; + souls[soul] = rel; + s3.PUT(path, souls, function(err, reply){ + Gun.log("s3 key reply", soul, err, reply); + if(err || !reply){ + return s3.key(key, soul, cb); // naive implementation of retry TODO: BUG: need backoff and anti-infinite-loop! + } + cb(); + }, {Metadata: meta}); + }); } opt.hooks = opt.hooks || {}; gun.opt({hooks: { - load: opt.hooks.load || s3.load - ,set: opt.hooks.set || s3.set + get: opt.hooks.get || s3.get + ,put: opt.hooks.put || s3.put ,key: opt.hooks.key || s3.key }}, true); }); diff --git a/lib/set.js b/lib/set.js new file mode 100644 index 00000000..6c2596b3 --- /dev/null +++ b/lib/set.js @@ -0,0 +1,35 @@ +var Gun = Gun || require('../gun'); + +/* +Gun.chain.set = function(obj, cb, opt){ + var set = this; + opt = opt || {}; + cb = cb || function(){}; + set = set.put({}); // insert assumes a graph node. So either create it or merge with the existing one. + var error, item = set.chain().put(obj, function(err){ // create the new item in its own context. + error = err; // if this happens, it should get called before the .val + }).val(function(val){ + if(error){ return cb.call(set, error) } // which in case it is, allows us to fail fast. + var add = {}, soul = Gun.is.soul.on(val); + if(!soul){ return cb.call(set, {err: Gun.log("No soul!")}) } + add[soul] = val; // other wise, let's then + set.put(add, cb); // merge with the graph node. + }); + return item; +};*/ + +Gun.chain.set = function(val, cb, opt){ + var gun = this, ctx = {}, drift = Gun.time.now(); + cb = cb || function(){}; + opt = opt || {}; + + if(!gun.back){ gun = gun.put({}) } + gun = gun.not(function(next, key){ + return key? this.put({}).key(key) : this.put({}); + }); + if(!val && !Gun.is.value(val)){ return gun } + + var obj = {}; + obj['I' + drift + 'R' + Gun.text.random(5)] = val; + return gun.put(obj, cb); +} \ No newline at end of file diff --git a/lib/wsp.js b/lib/wsp.js index fdc45017..ea347bf5 100644 --- a/lib/wsp.js +++ b/lib/wsp.js @@ -32,21 +32,21 @@ } return gun; } - gun.server = gun.server || function(req, res, next){ + gun.server = gun.server || function(req, res, next){ // http //Gun.log("\n\n GUN SERVER!", req); next = next || function(){}; - if(!req || !res){ return next() } - if(!req.url){ return next() } - if(!req.method){ return next() } + if(!req || !res){ return next(), false } + if(!req.url){ return next(), false } + if(!req.method){ return next(), false } var msg = {}; msg.url = url.parse(req.url, true); - if(!gun.server.regex.test(msg.url.pathname)){ return next() } + if(!gun.server.regex.test(msg.url.pathname)){ return next(), false } if(msg.url.pathname.replace(gun.server.regex,'').slice(0,3).toLowerCase() === '.js'){ res.writeHead(200, {'Content-Type': 'text/javascript'}); res.end(gun.server.js = gun.server.js || require('fs').readFileSync(__dirname + '/../gun.js')); // gun server is caching the gun library for the client - return; + return true; } - http(req, res, function(req, res){ + return http(req, res, function(req, res){ if(!req){ return next() } var tab, cb = res = require('./jsonp')(req, res); if(req.headers && (tab = req.headers['gun-sid'])){ @@ -72,7 +72,7 @@ } } gun.__.opt.hooks.transport(req, cb); - }); + }), true; } gun.server.on = gun.server.on || Gun.on.create(); gun.__.opt.poll = gun.__.opt.poll || opt.poll || 1; @@ -87,56 +87,94 @@ return req.headers['x-forwarded-for'] || req.connection.remoteAddress || req.socket.remoteAddress || (req.connection.socket || {}).remoteAddress || ''; } */ gun.server.transport = gun.server.transport || (function(){ - // all streams, technically PATCH but implemented as POST, are forwarded to other trusted peers + // all streams, technically PATCH but implemented as PUT or POST, are forwarded to other trusted peers // except for the ones that are listed in the message as having already been sending to. // all states, implemented with GET, are replied to the source that asked for it. function tran(req, cb){ //Gun.log("gun.server", req); - req.method = req.body? 'post' : 'get'; // post or get is based on whether there is a body or not + req.method = req.body? 'put' : 'get'; // put or get is based on whether there is a body or not req.url.key = req.url.pathname.replace(gun.server.regex,'').replace(/^\//i,'') || ''; - if('get' == req.method){ return tran.load(req, cb) } - if('post' == req.method){ return tran.post(req, cb) } + if('get' == req.method){ return tran.get(req, cb) } + if('put' == req.method || 'post' == req.method){ return tran.put(req, cb) } cb({body: {hello: 'world'}}); } - tran.load = function(req, cb){ + tran.get = function(req, cb){ var key = req.url.key , reply = {headers: {'Content-Type': tran.json}}; + //console.log(req); + if(req && req.url && Gun.obj.has(req.url.query, '*')){ + return gun.all(req.url.key + req.url.search, function(err, list){ + cb({headers: reply.headers, body: (err? (err.err? err : {err: err || "Unknown error."}) : list || null ) }) + }); + } if(!key){ if(!Gun.obj.has(req.url.query, Gun._.soul)){ - return cb({headers: reply.headers, body: {err: "No key or soul to load."}}); + return cb({headers: reply.headers, body: {err: "No key or soul to get."}}); } key = {}; key[Gun._.soul] = req.url.query[Gun._.soul]; } - //Gun.log("transport.loading key ->", key, gun.__.graph, gun.__.keys); - gun.load(key, function(err, node){ - //tran.sub.scribe(req.tab, node._[Gun._.soul]); - cb({headers: reply.headers, body: (err? (err.err? err : {err: err || "Unknown error."}) : node || null)}); + console.log("tran.get", key); + gun.get(key, function(err, graph){ + //tran.sub.scribe(req.tab, graph._[Gun._.soul]); + console.log("tran.get", key, "<---", err, graph); + if(err || !graph){ + return cb({headers: reply.headers, body: (err? (err.err? err : {err: err || "Unknown error."}) : null)}); + } + if(Gun.obj.empty(graph)){ return cb({headers: reply.headers, body: graph}) } // we're out of stuff! + // TODO: chunk the graph even if it is already chunked. pseudo code below! + /*Gun.is.graph(graph, function(node, soul){ + if(Object.keys(node).length > 100){ + // split object into many objects that have a fixed size + // iterate over each object + // cb({headers: reply.headers, chunk: {object} ); + } + });*/ + return cb({headers: reply.headers, chunk: graph }); // keep streaming }); } - tran.post = function(req, cb){ - // NOTE: It is highly recommended you do your own POSTs through your own API that then saves to gun manually. + tran.put = function(req, cb){ + // NOTE: It is highly recommended you do your own PUT/POSTs through your own API that then saves to gun manually. // This will give you much more fine-grain control over security, transactions, and what not. var reply = {headers: {'Content-Type': tran.json}}; if(!req.body){ return cb({headers: reply.headers, body: {err: "No body"}}) } - if(tran.post.key(req, cb)){ return } + gun.server.on('network').emit(req); + if(tran.put.key(req, cb)){ return } + // some NEW code that should get revised. + if(Gun.is.node(req.body) || Gun.is.graph(req.body)){ + //console.log("tran.put", req.body); + if(req.err = Gun.union(gun, req.body, function(err, ctx){ // TODO: BUG? Probably should give me ctx.graph + if(err){ return cb({headers: reply.headers, body: {err: err || "Union failed."}}) } + var ctx = ctx || {}; ctx.graph = {}; + Gun.is.graph(req.body, function(node, soul){ + ctx.graph[soul] = gun.__.graph[soul]; // TODO: BUG? Probably should be delta fields + }) + gun.__.opt.hooks.put(ctx.graph, function(err, ok){ + if(err){ return cb({headers: reply.headers, body: {err: err || "Failed."}}) } + cb({headers: reply.headers, body: {ok: ok || "Persisted."}}); + }); + }).err){ cb({headers: reply.headers, body: {err: req.err || "Union failed."}}) } + } + + return; + //return; // saving Gun.obj.map(req.body, function(node, soul){ // iterate over every node if(soul != Gun.is.soul.on(node)){ return this.end("No soul!") } - gun.load(node._, this.add(soul)); // and begin loading it in case it is not cached. + gun.get({'#': soul}, this.add(soul)); // and begin getting it in case it is not cached. }, Gun.fns.sum(function(err){ if(err){ return reply.err = err } - reply.loaded = true; + reply.got = true; })); // could there be a timing error somewhere in here? var setImmediate = setImmediate || setTimeout; // TODO: BUG: This should be cleaned up, but I want Heroku to work. - setImmediate(function(){ // do not do it right away because gun.load above is async, this should be cleaner. - var context = gun.union(req.body, function check(err, ctx){ // check if the body is valid, and get it into cache immediately. + setImmediate(function(){ // do not do it right away because gun.get above is async, this should be cleaner. + var context = Gun.union(gun, req.body, function check(err, ctx){ // check if the body is valid, and get it into cache immediately. context = ctx || context; if(err || reply.err || context.err || !context.nodes){ return cb({headers: reply.headers, body: {err: err || reply.err || context.err || "Union failed." }}) } - if(!Gun.fns.is(gun.__.opt.hooks.set)){ return cb({headers: reply.headers, body: {err: "Persistence not supported." }}) } - if(!reply.loaded){ return setTimeout(check, 2) } // only persist if all nodes have been loaded into cache. - gun.__.opt.hooks.set(context.nodes, function(err, data){ // since we've already manually done the union, we can now directly call the persistence layer. + if(!Gun.fns.is(gun.__.opt.hooks.put)){ return cb({headers: reply.headers, body: {err: "Persistence not supported." }}) } + if(!reply.got){ return setTimeout(check, 2) } // only persist if all nodes are in cache. + gun.__.opt.hooks.put(context.nodes, function(err, data){ // since we've already manually done the union, we can now directly call the persistence layer. if(err){ return cb({headers: reply.headers, body: {err: err || "Persistence failed." }}) } cb({headers: reply.headers, body: {ok: "Persisted."}}); // TODO: Fix so we know what the reply is. }); @@ -144,14 +182,14 @@ }, 0); gun.server.on('network').emit(req); } - tran.post.key = function(req, cb){ // key hook! + tran.put.key = function(req, cb){ // key hook! if(!req || !req.url || !req.url.key || !Gun.obj.has(req.body, Gun._.soul)){ return } - var load = {}, index = {}, soul; - soul = load[Gun._.soul] = index[req.url.key] = req.body[Gun._.soul]; - gun.load(load).key(index, function(err, reply){ + var index = req.url.key, soul = Gun.is.soul(req.body); + console.log("tran.key", index, req.body); + gun.key(index, function(err, reply){ if(err){ return cb({headers: {'Content-Type': tran.json}, body: {err: err}}) } cb({headers: {'Content-Type': tran.json}, body: reply}); // TODO: Fix so we know what the reply is. - }); + }, soul); return true; } gun.server.on('network').event(function(req){ diff --git a/package.json b/package.json index 9a20a41d..0547facb 100644 --- a/package.json +++ b/package.json @@ -1,24 +1,51 @@ -{ - "name": "gun", - "version": "0.1.5", - "author": "Mark Nadal", - "description": "Graph engine.", - "engines": { - "node": "~>0.6.6" - }, - "dependencies": { - "mime": "~>1.2.11", - "aws-sdk": "~>2.0.0", - "formidable": "~>1.0.15", - "ws": "~>0.4.32", - "request": "~>2.39.0" - }, - "devDependencies": { - "mocha": "~>1.9.0" - }, - "scripts": { - "start": "node examples/express.js 8080", - "prestart": "npm install ./examples", - "test": "mocha" - } -} +{ + "name": "gun", + "version": "0.2.0-alpha-1", + "description": "Graph engine", + "main": "index.js", + "scripts": { + "start": "node examples/http.js 8080", + "test": "mocha" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/amark/gun.git" + }, + "keywords": [ + "graph", + "document", + "key", + "value", + "relational", + "datastore", + "database", + "engine", + "realtime", + "decentralized", + "peer-to-peer", + "P2P", + "OSS", + "distributed", + "embedded", + "localstorage", + "S3" + ], + "author": "Mark Nadal", + "license": "(Zlib OR MIT OR Apache-2.0)", + "bugs": { + "url": "https://github.com/amark/gun/issues" + }, + "homepage": "https://github.com/amark/gun#readme", + "engines": { + "node": ">=0.6.6", + "iojs": ">=0.0.1" + }, + "dependencies": { + "aws-sdk": "~>2.0.0", + "formidable": "~>1.0.15", + "ws": "~>0.4.32" + }, + "devDependencies": { + "mocha": "~>1.9.0" + } +} diff --git a/test/abc.js b/test/abc.js new file mode 100644 index 00000000..4a483f99 --- /dev/null +++ b/test/abc.js @@ -0,0 +1,2 @@ +var expect = global.expect = require("./expect"); +require('./common'); \ No newline at end of file diff --git a/test/all.js b/test/all.js index 0ecd8537..0b74c116 100644 --- a/test/all.js +++ b/test/all.js @@ -1,4 +1,79 @@ -console.log("MAKE SURE TO DELETE `data.json` BEFORE RUNNING TESTS!"); - -var expect = global.expect = require("./expect"); -require('./common'); +describe('All', function(){ + return; + var expect = global.expect = require("./expect"); + + var Gun = Gun || require('../gun'); + (typeof window === 'undefined') && require('../lib/file'); + + var gun = Gun({file: 'data.json'}); + + var keys = { + 'emails/aquiva@gmail.com': 'asdf', + 'emails/mark@gunDB.io': 'asdf', + 'user/marknadal': 'asdf', + 'emails/amber@cazzell.com': 'fdsa', + 'user/ambernadal': 'fdsa', + 'user/forrest': 'abcd', + 'emails/banana@gmail.com': 'qwert', + 'user/marknadal/messages/asdf': 'rti', + 'user/marknadal/messages/fobar': 'yuoi', + 'user/marknadal/messages/lol': 'hjkl', + 'user/marknadal/messages/nano': 'vbnm', + 'user/marknadal/messages/sweet': 'xcvb', + 'user/marknadal/posts': 'qvtxz', + 'emails/for@rest.com': 'abcd' + }; + + it('from', function() { + var r = gun.__.opt.hooks.all(keys, {from: 'user/'}); + //console.log(r); + expect(r).to.be.eql({ + 'user/marknadal': { '#': 'asdf' }, + 'user/ambernadal': { '#': 'fdsa' }, + 'user/forrest': { '#': 'abcd' }, + 'user/marknadal/messages/asdf': { '#': 'rti' }, + 'user/marknadal/messages/fobar': { '#': 'yuoi' }, + 'user/marknadal/messages/lol': { '#': 'hjkl' }, + 'user/marknadal/messages/nano': { '#': 'vbnm' }, + 'user/marknadal/messages/sweet': { '#': 'xcvb' }, + 'user/marknadal/posts': { '#': 'qvtxz' } + }); + }); + + it('from and upto', function() { + var r = gun.__.opt.hooks.all(keys, {from: 'user/', upto: '/'}); + //console.log('upto', r); + expect(r).to.be.eql({ + 'user/marknadal': { '#': 'asdf' }, + 'user/ambernadal': { '#': 'fdsa' }, + 'user/forrest': { '#': 'abcd' } + }); + }); + + it('from and upto and start and end', function() { + var r = gun.__.opt.hooks.all(keys, {from: 'user/', upto: '/', start: "c", end: "f"}); + //console.log('upto and start and end', r); + expect(r).to.be.eql({ + 'user/forrest': { '#': 'abcd' } + }); + }); + + it('map', function(done) { return done(); + var users = gun.put({ + a: {name: "Mark Nadal"}, + b: {name: "Amber Nadal"}, + c: {name: "Charlie Chapman"}, + d: {name: "Johnny Depp"}, + e: {name: "Santa Clause"} + }); + //console.log("map:"); + users.map().val(function(user){ + //console.log("each user:", user); + }).path("ohboy"); + return; + users.map(function(){ + + }); + }); + +}); \ No newline at end of file diff --git a/test/common.js b/test/common.js index 77fe7ee9..284e5acf 100644 --- a/test/common.js +++ b/test/common.js @@ -1,10 +1,15 @@ -var Gun = Gun || require('../gun'); -if(typeof window !== 'undefined'){ root = window } +(function(env){ + root = env.window? env.window : root; + env.window && root.localStorage && root.localStorage.clear(); + root.Gun = root.Gun || require('../gun'); +}(this)); +Gun.log.squelch = true; + describe('Gun', function(){ var t = {}; describe('Utility', function(){ - it('verbose console.log debugging', function(done) { + it('verbose console.log debugging', function(done) { console.log("TURN THIS BACK ON the DEBUGGING TEST"); done(); return; var gun = Gun(); var log = root.console.log, counter = 1; @@ -13,11 +18,11 @@ describe('Gun', function(){ //log(a,b,c); } Gun.log.verbose = true; - gun.set('bar', function(err, yay){ // intentionally trigger an error that will get logged. + gun.put('bar', function(err, yay){ // intentionally trigger an error that will get logged. expect(counter).to.be(0); Gun.log.verbose = false; - gun.set('bar', function(err, yay){ // intentionally trigger an error that will get logged. + gun.put('bar', function(err, yay){ // intentionally trigger an error that will get logged. expect(counter).to.be(0); root.console.log = log; @@ -154,6 +159,15 @@ describe('Gun', function(){ it('has',function(){ var obj = {a:1,b:2}; expect(Gun.obj.has(obj,'a')).to.be.ok(); + }); + it('empty',function(){ + expect(Gun.obj.empty()).to.be(true); + expect(Gun.obj.empty({a:false})).to.be(false); + expect(Gun.obj.empty({a:false},'a')).to.be(true); + expect(Gun.obj.empty({a:false},{a:1})).to.be(true); + expect(Gun.obj.empty({a:false,b:1},'a')).to.be(false); + expect(Gun.obj.empty({a:false,b:1},{a:1})).to.be(false); + expect(Gun.obj.empty({1:1},'danger')).to.be(false); }); it('copy',function(){ var obj = {"a":false,"b":1,"c":"d","e":[0,1],"f":{"g":"h"}}; @@ -281,309 +295,1154 @@ describe('Gun', function(){ }); }); - it('ify', function(){ - var data, test; - - data = {a: false, b: true, c: 0, d: 1, e: '', f: 'g', h: null}; - test = Gun.ify(data); - expect(test.err).to.not.be.ok(); - - data = {}; - data.a = {x: 1, y: 2, z: 3} - data.b = {m: 'n', o: 'p', q: 'r', s: 't'}; - data.a.kid = data.b; - data.b.parent = data.a; - data.loop = [data.b, data.a.kid, data]; - test = Gun.ify(data); - expect(test.err).to.not.be.ok(); - - data = {_: {'#': 'shhh', meta: {yay: 1}}, sneak: true}; - test = Gun.ify(data); - expect(test.err).to.not.be.ok(); // metadata needs to be stored, but it can't be used for data. - + describe('ify', function(){ + var test, gun = Gun(); + + it('null', function(done){ + Gun.ify(null)(function(err, ctx){ + expect(err).to.be.ok(); + done(); + }); + }); + + it('basic', function(done){ + var data = {a: false, b: true, c: 0, d: 1, e: '', f: 'g', h: null}; + Gun.ify(data)(function(err, ctx){ + expect(err).to.not.be.ok(); + expect(ctx.err).to.not.be.ok(); + expect(ctx.root).to.eql(data); + expect(ctx.root === data).to.not.ok(); + done(); + }); + }); + + it('basic soul', function(done){ + var data = {_: {'#': 'SOUL'}, a: false, b: true, c: 0, d: 1, e: '', f: 'g', h: null}; + Gun.ify(data)(function(err, ctx){ + expect(err).to.not.be.ok(); + expect(ctx.err).to.not.be.ok(); + + expect(ctx.root).to.eql(data); + expect(ctx.root === data).to.not.be.ok(); + expect(Gun.is.soul.on(ctx.root) === Gun.is.soul.on(data)); + done(); + }); + }); + + it('arrays', function(done){ + var data = {before: {path: 'kill'}, one: {two: {lol: 'troll', three: [9, 8, 7, 6, 5]}}}; + Gun.ify(data)(function(err, ctx){ + expect(err).to.be.ok(); + expect(err.err.indexOf("one.two.three")).to.not.be(-1); + done(); + }); + }); + + it('undefined', function(done){ + var data = {z: undefined, x: 'bye'}; + Gun.ify(data)(function(err, ctx){ + expect(err).to.be.ok(); + done(); + }); + }); + + it('NaN', function(done){ + var data = {a: NaN, b: 2}; + Gun.ify(data)(function(err, ctx){ + expect(err).to.be.ok(); + done(); + }); + }); + + it('Infinity', function(done){ // SAD DAY PANDA BEAR :( :( :(... Mark wants Infinity. JSON won't allow. + var data = {a: 1, b: Infinity}; + Gun.ify(data)(function(err, ctx){ + expect(err).to.be.ok(); + done(); + }); + }); + + it('function', function(done){ + var data = {c: function(){}, d: 'hi'}; + Gun.ify(data)(function(err, ctx){ + expect(err).to.be.ok(); + done(); + }); + }); + + it('extraneous', function(done){ + var data = {_: {'#': 'shhh', meta: {yay: 1}}, sneak: true}; + Gun.ify(data)(function(err, ctx){ + expect(err).to.not.be.ok(); // extraneous metadata needs to be stored, but it can't be used for data. + done(); + }); + }); + + return; // TODO! Fix GUN to handle this! data = {}; data.sneak = false; data.both = {inside: 'meta data'}; data._ = {'#': 'shhh', data: {yay: 1}, spin: data.both}; test = Gun.ify(data); expect(test.err.meta).to.be.ok(); // TODO: Fail: this passes, somehow? Fix ify code! - - data = {one: {two: [9, 8, 7, 6, 5]}}; - test = Gun.ify(data); - expect(test.err.array).to.be.ok(); - - data = {z: undefined, x: 'bye'}; - test = Gun.ify(data); - expect(test.err.invalid).to.be.ok(); - - data = {a: NaN, b: 2}; - test = Gun.ify(data); - expect(test.err.invalid).to.be.ok(); - - data = {a: 1, b: Infinity}; - test = Gun.ify(data); - expect(test.err.invalid).to.be.ok(); - - data = {c: function(){}, d: 'hi'}; - test = Gun.ify(data); - expect(test.err.invalid).to.be.ok(); }); - - it('union', function(){ - var graph, prime; - - graph = Gun.ify({a: false, b: true, c: 0, d: 1, e: '', f: 'g', h: null}).nodes; - prime = Gun.ify({h: 9, i: 'foo', j: 'k', l: 'bar', m: 'Mark', n: 'Nadal'}).nodes; - - Gun.union(graph, prime); // TODO: BUG! Where is the expect??? - }); - - describe('API', function(){ - - (typeof window === 'undefined') && require('../lib/file'); - var gun = Gun({file: 'data.json'}); + + describe('Event Promise Back In Time', function(){ return; // TODO: I think this can be removed entirely now. + /* + var ref = gun.put({field: 'value'}).key('field/value').get('field/value', function(){ + expect() + }); + setTimeout(function(){ + ref.get('field/value', function(){ + expect(); + }); + }, 50); + + A) Synchronous + 1. fake (B) + B) Asychronous + 1. In Memory + DONE + 2. Will be in Memory + LISTEN to something SO WE CAN RESUME + DONE + 3. Not in Memory + Ask others. + DONE + */ + it('A1', function(done){ // this has behavior of a .get(key) where we already have it in memory but need to fake async it. + var graph = {}; + var keys = {}; + graph['soul'] = {foo: 'bar'}; + keys['some/key'] = graph['soul']; + + var ctx = {key: 'some/key'}; + if(ctx.node = keys[ctx.key]){ + console.log("yay we are synchronously in memory!"); + setTimeout(function(){ + expect(ctx.flag).to.be.ok(); + expect(ctx.node.foo).to.be('bar'); + done(); + },0); + ctx.flag = true; + } + }); - it('set key get', function(done){ - gun.set({hello: "world"}).key('hello/world').get(function(val){ + it('B1', function(done){ // this has the behavior a .val() where we don't even know what is going on, we just want context. + var graph = {}; + var keys = {}; + + var ctx = { + promise: function(cb){ + setTimeout(function(){ + graph['soul'] = {foo: 'bar'}; + keys['some/key'] = graph['soul']; + cb('soul'); + },50); + } + }; + if(ctx.node = keys[ctx.key]){ + // see A1 test + } else { + ctx.promise(function(soul){ + if(ctx.node = graph[soul]){ + expect(ctx.node.foo).to.be('bar'); + done(); + } else { + // I don't know + } + }); + } + }); + + it('B2', function(done){ // this is the behavior of a .get(key) which synchronously follows a .put(obj).key(key) which fakes async. + var graph = {}; + var keys = {}; + + var ctx = {}; + (function(data){ // put + setTimeout(function(){ + graph['soul'] = data; + fn(); + },10); + + ctx.promise = function(fn){ + + } + }({field: "value"})); + + (function(key){ // key + keys[key] = true; + ctx.promise(function(){ + keys[key] = node; + }) + }('some/key')); + + (function(ctx){ // get + if(get.node = keys[get.key]){ + + } else + if(get.inbetweenMemory){ + + } else { + loadFromDiskOrPeers(get.key, function(){ + + }); + } + }({key: 'some/key'})); + }); + }); + + describe('Schedule', function(){ + it('one', function(done){ + Gun.schedule(Gun.time.is(), function(){ + expect(true).to.be(true); + done(); //setTimeout(function(){ done() },1); + }); + }); + + it('many', function(done){ + Gun.schedule(Gun.time.is() + 50, function(){ + done.first = true; + }); + Gun.schedule(Gun.time.is() + 100, function(){ + done.second = true; + }); + Gun.schedule(Gun.time.is() + 200, function(){ + done.third = true; + expect(done.first).to.be(true); + expect(done.second).to.be(true); + expect(done.third).to.be(true); + done(); //setTimeout(function(){ done() },1); + }); + }); + }); + + describe('Union', function(){ + var gun = Gun(); + + it('fail', function(){ + var prime = { + 'asdf': { + _: {'#': 'asdf', '>':{ + a: 'cheating' + }}, + a: 0 + } + } + + expect(gun.__.graph['asdf']).to.not.be.ok(); + var ctx = Gun.union(gun, prime); + expect(ctx.err).to.be.ok(); + }); + + it('basic', function(done){ + var prime = { + 'asdf': { + _: {'#': 'asdf', '>':{ + a: Gun.time.is() + }}, + a: 0 + } + } + + expect(gun.__.graph['asdf']).to.not.be.ok(); + var ctx = Gun.union(gun, prime, function(){ + expect(gun.__.graph['asdf'].a).to.be(0); + done(); + }); + }); + + it('disjoint', function(done){ + var prime = { + 'asdf': { + _: {'#': 'asdf', '>':{ + b: Gun.time.is() + }}, + b: 'c' + } + } + + expect(gun.__.graph['asdf'].a).to.be(0); + expect(gun.__.graph['asdf'].b).to.not.be.ok(); + var ctx = Gun.union(gun, prime, function(){ + expect(gun.__.graph['asdf'].a).to.be(0); + expect(gun.__.graph['asdf'].b).to.be('c'); + done(); + }); + }); + + it('mutate', function(done){ + var prime = { + 'asdf': { + _: {'#': 'asdf', '>':{ + b: Gun.time.is() + }}, + b: 'd' + } + } + + expect(gun.__.graph['asdf'].b).to.be('c'); + var ctx = Gun.union(gun, prime, function(){ + expect(gun.__.graph['asdf'].b).to.be('d'); + done(); + }); + }); + + it('disjoint past', function(done){ + var prime = { + 'asdf': { + _: {'#': 'asdf', '>':{ + x: 0 // beginning of time! + }}, + x: 'hi' + } + } + + expect(gun.__.graph['asdf'].x).to.not.be.ok(); + var ctx = Gun.union(gun, prime, function(){ + expect(gun.__.graph['asdf'].x).to.be('hi'); + done(); + }); + }); + + it('past', function(done){ + var prime = { + 'asdf': { + _: {'#': 'asdf', '>':{ + x: Gun.time.is() - (60 * 1000) // above lower boundary, below now or upper boundary. + }}, + x: 'hello' + } + } + + expect(gun.__.graph['asdf'].x).to.be('hi'); + var ctx = Gun.union(gun, prime, function(){ + expect(gun.__.graph['asdf'].x).to.be('hello'); + done(); + }); + }); + + it('future', function(done){ + var prime = { + 'asdf': { + _: {'#': 'asdf', '>':{ + x: Gun.time.is() + (200) // above now or upper boundary, aka future. + }}, + x: 'how are you?' + } + } + + expect(gun.__.graph['asdf'].x).to.be('hello'); + var now = Gun.time.is(); + var ctx = Gun.union(gun, prime, function(){ + expect(Gun.time.is() - now).to.be.above(100); + expect(gun.__.graph['asdf'].x).to.be('how are you?'); + done(); + }); + }); + var to = 5000; + it('disjoint future', function(done){ + var prime = { + 'asdf': { + _: {'#': 'asdf', '>':{ + y: Gun.time.is() + (200) // above now or upper boundary, aka future. + }}, + y: 'goodbye' + } + } + expect(gun.__.graph['asdf'].y).to.not.be.ok(); + var now = Gun.time.is(); + var ctx = Gun.union(gun, prime, function(){ + expect(Gun.time.is() - now).to.be.above(100); + expect(gun.__.graph['asdf'].y).to.be('goodbye'); + done(); + }); + }); + + it('disjoint future max', function(done){ + var prime = { + 'asdf': { + _: {'#': 'asdf', '>':{ + y: Gun.time.is() + (2), // above now or upper boundary, aka future. + z: Gun.time.is() + (200) // above now or upper boundary, aka future. + }}, + y: 'bye', + z: 'who' + } + } + + expect(gun.__.graph['asdf'].y).to.be('goodbye'); + expect(gun.__.graph['asdf'].z).to.not.be.ok(); + var now = Gun.time.is(); + var ctx = Gun.union(gun, prime, function(){ + expect(Gun.time.is() - now).to.be.above(100); + expect(gun.__.graph['asdf'].y).to.be('bye'); + expect(gun.__.graph['asdf'].z).to.be('who'); + done(); //setTimeout(function(){ done() },1); + }); + }); + + it('future max', function(done){ + var prime = { + 'asdf': { + _: {'#': 'asdf', '>':{ + w: Gun.time.is() + (2), // above now or upper boundary, aka future. + x: Gun.time.is() - (60 * 1000), // above now or upper boundary, aka future. + y: Gun.time.is() + (200), // above now or upper boundary, aka future. + z: Gun.time.is() + (50) // above now or upper boundary, aka future. + }}, + w: true, + x: 'nothing', + y: 'farewell', + z: 'doctor who' + } + } + + expect(gun.__.graph['asdf'].w).to.not.be.ok(); + expect(gun.__.graph['asdf'].x).to.be('how are you?'); + expect(gun.__.graph['asdf'].y).to.be('bye'); + expect(gun.__.graph['asdf'].z).to.be('who'); + var now = Gun.time.is(); + var ctx = Gun.union(gun, prime, function(){ + expect(Gun.time.is() - now).to.be.above(100); + expect(gun.__.graph['asdf'].w).to.be(true); + expect(gun.__.graph['asdf'].x).to.be('how are you?'); + expect(gun.__.graph['asdf'].y).to.be('farewell'); + expect(gun.__.graph['asdf'].z).to.be('doctor who'); + done(); //setTimeout(function(){ done() },1); + }); + }); + + it('two nodes', function(done){ // chat app problem where disk dropped the last data, turns out it was a union problem! + var state = Gun.time.is(); + var prime = { + 'sadf': { + _: {'#': 'sadf', '>':{ + 1: state + }}, + 1: {'#': 'fdsa'} + }, + 'fdsa': { + _: {'#': 'fdsa', '>':{ + msg: state + }}, + msg: "Let's chat!" + } + } + + expect(gun.__.graph['sadf']).to.not.be.ok(); + expect(gun.__.graph['fdsa']).to.not.be.ok(); + var ctx = Gun.union(gun, prime, function(){ + expect(gun.__.graph['sadf'][1]).to.be.ok(); + expect(gun.__.graph['fdsa'].msg).to.be("Let's chat!"); + done(); + }); + }); + + it('append third node', function(done){ // chat app problem where disk dropped the last data, turns out it was a union problem! + var state = Gun.time.is(); + var prime = { + 'sadf': { + _: {'#': 'sadf', '>':{ + 2: state + }}, + 2: {'#': 'fads'} + }, + 'fads': { + _: {'#': 'fads', '>':{ + msg: state + }}, + msg: "hi" + } + } + + expect(gun.__.graph['sadf']).to.be.ok(); + expect(gun.__.graph['fdsa']).to.be.ok(); + var ctx = Gun.union(gun, prime, function(){ + expect(gun.__.graph['sadf'][1]).to.be.ok(); + expect(gun.__.graph['sadf'][2]).to.be.ok(); + expect(gun.__.graph['fads'].msg).to.be("hi"); + done(); + }); + }); + + it('pseudo null', function(){ + var node = Gun.union.pseudo('pseudo'); + expect(Gun.is.soul.on(node)).to.be('pseudo'); + }); + + it('pseudo node', function(){ + + var graph = { + 'asdf': { + _: {'#': 'asdf', '>': { + x: Gun.time.is(), + y: Gun.time.is() + }}, + x: 1, + y: 2 + } + } + var node = Gun.union.pseudo('soul', graph); + expect(node).to.not.be.ok(); + }); + + it('pseudo graph', function(){ + + var graph = { + 'asdf': { + _: {'#': 'asdf', '>': { + a: Gun.time.is() - 2, + z: Gun.time.is() - 2 + }}, + a: 1, + z: 1 + }, + 'fdsa': { + _: {'#': 'fdsa', '>': { + b: Gun.time.is() - 1, + z: Gun.time.is() - 1 + }}, + b: 2, + z: 2 + }, + 'sadf': { + _: {'#': 'sadf', '>': { + c: Gun.time.is(), + z: Gun.time.is() - 100 + }}, + c: 3, + z: 3 + } + } + var node = Gun.union.pseudo('soul', graph); + expect(Gun.is.soul.on(node)).to.be('soul'); + expect(node.a).to.be(1); + expect(node.b).to.be(2); + expect(node.c).to.be(3); + expect(node.z).to.be(2); + }); + }); + + describe('API', function(){ + var gun = Gun(); + + it('put', function(done){ + gun.put("hello", function(err){ + expect(err).to.be.ok(); + done(); + }); + }); + + it('put node', function(done){ + gun.put({hello: "world"}, function(err, ok){ + expect(err).to.not.be.ok(); + done(); + }); + }); + + it('put node then value', function(done){ + var ref = gun.put({hello: "world"}); + + ref.put('hello', function(err, ok){ + expect(err).to.be.ok(); + done(); + }); + }); + + it('put node then put', function(done){ + gun.put({hello: "world"}).put({goodbye: "world"}, function(err, ok){ + expect(err).to.not.be.ok(); + done(); + }); + }); + + it('put node key get', function(done){ + gun.put({hello: "key"}).key('yes/key', function(err, ok){ + expect(err).to.not.be.ok(); + }).get('yes/key', function(err, data){ + expect(err).to.not.be.ok(); + var c = 0; + Gun.is.graph(data, function(node){ + expect(c++).to.be(0); + if(Gun.obj.empty(node, Gun._.meta)){ return done(), true } + expect(node.hello).to.be('key'); + }); + }); + }); + + it('put node key gun get', function(done){ + gun.put({hello: "a key"}).key('yes/a/key', function(err, ok){ + expect(err).to.not.be.ok(); + }); + + gun.get('yes/a/key', function(err, data){ + expect(err).to.not.be.ok(); + var c = 0; + Gun.is.graph(data, function(node){ + expect(c++).to.be(0); + if(Gun.obj.empty(node, Gun._.meta)){ return done(), true } + expect(node.hello).to.be('a key'); + }); + }); + }); + + it('gun key', function(){ // Revisit this behavior? + try{ gun.key('fail/key') } + catch(err){ + expect(err).to.be.ok(); + } + }); + + it('get key', function(done){ + gun.get('yes/key', function(err, data){ + expect(err).to.not.be.ok(); + var c = 0; + Gun.is.graph(data, function(node){ + expect(c++).to.be(0); + if(Gun.obj.empty(node, Gun._.meta)){ return } + expect(node.hello).to.be('key'); + }); + }).key('hello/key', function(err, ok){ + expect(err).to.not.be.ok(); + done.key = true; + if(done.yes){ done() } + }).key('yes/hello', function(err, ok){ + expect(err).to.not.be.ok(); + done.yes = true; + if(done.key){ done() } + }); + }); + + it('get key null', function(done){ + gun.get('yes/key').key('', function(err, ok){ + expect(err).to.be.ok(); + done(); + }); + }); + + it('get node put node merge', function(done){ + gun.get('hello/key', function(err, data){ + expect(err).to.not.be.ok(); + var c = 0; + Gun.is.graph(data, function(node){ + expect(c++).to.be(0); + done.soul = Gun.is.soul.on(node); + }); + }).put({hi: 'you'}, function(err, ok){ + expect(err).to.not.be.ok(); + var node = gun.__.graph[done.soul]; + expect(node.hello).to.be('key'); + expect(node.hi).to.be('you'); + done(); + }); + }); + + it('get null put node never', function(done){ // TODO: GET returns nothing, and then doing a PUT? + gun.get(null, function(err, ok){ + expect(err).to.be.ok(); + done.err = true; + }).put({hi: 'you'}, function(err, ok){ + done.flag = true; + }); + setTimeout(function(){ + expect(done.err).to.be.ok(); + expect(done.flag).to.not.be.ok(); + done(); + }, 500); + }); + + /* + it('get key no data put', function(done){ + gun.get('this/key/definitely/does/not/exist', function(err, data){ + expect(err).to.not.be.ok(); + expect(data).to.not.be.ok(); + }).put({testing: 'stuff'}, function(err, ok){ + expect(err).to.not.be.ok(); + var node = gun.__.graph[done.soul]; + expect(node.hello).to.be('key'); + expect(node.hi).to.be('overwritten'); + done(); + }); + }); + */ + + it('get node put node merge conflict', function(done){ + gun.get('hello/key', function(err, data){ + expect(err).to.not.be.ok(); + var c = 0; + Gun.is.graph(data, function(node){ + expect(c++).to.be(0); + if(Gun.obj.empty(node, Gun._.meta)){ return true } + expect(node.hi).to.be('you'); + done.soul = Gun.is.soul.on(node); + }); + }).put({hi: 'overwritten'}, function(err, ok){ + expect(err).to.not.be.ok(); + var node = gun.__.graph[done.soul]; + expect(node.hello).to.be('key'); + expect(node.hi).to.be('overwritten'); + done(); + }); + }); + + it('get node path', function(done){ + gun.get('hello/key').path('hi', function(err, val){ + expect(err).to.not.be.ok(); + expect(val).to.be('overwritten'); + done(); + }); + }); + + it('get node path put value', function(done){ + gun.get('hello/key', function(err, data){ + expect(err).to.not.be.ok(); + var c = 0; + Gun.is.graph(data, function(node){ + expect(c++).to.be(0); + if(Gun.obj.empty(node, Gun._.meta)){ return true } + expect(node.hi).to.be('overwritten'); + done.soul = Gun.is.soul.on(node); + }); + }).path('hi').put('again', function(err, ok){ + expect(err).to.not.be.ok(); + var node = gun.__.graph[done.soul]; + expect(node.hello).to.be('key'); + expect(node.hi).to.be('again'); + done(); + }); + }); + + it('get node path put object', function(done){ + gun.get('hello/key', function(err, data){ + expect(err).to.not.be.ok(); + var c = 0; + Gun.is.graph(data, function(node){ + expect(c++).to.be(0); + if(Gun.obj.empty(node, Gun._.meta)){ return true } + expect(node.hi).to.be('again'); + done.soul = Gun.is.soul.on(node); + }); + }).path('hi').put({yay: "value"}, function(err, ok){ + expect(err).to.not.be.ok(); + var root = gun.__.graph[done.soul]; + expect(root.hello).to.be('key'); + expect(root.yay).to.not.be.ok(); + expect(Gun.is.soul(root.hi)).to.be.ok(); + expect(Gun.is.soul(root.hi)).to.not.be(done.soul); + done(); + }); + }); + + it('get node path put object merge', function(done){ + gun.get('hello/key', function(err, data){ + expect(err).to.not.be.ok(); + var c = 0; + Gun.is.graph(data, function(node){ + expect(c++).to.be(0); + if(Gun.obj.empty(node, Gun._.meta)){ return true } + expect(done.ref = Gun.is.soul(node.hi)).to.be.ok(); + done.soul = Gun.is.soul.on(node); + }); + }).path('hi').put({happy: "faces"}, function(err, ok){ + expect(err).to.not.be.ok(); + var root = gun.__.graph[done.soul]; + var sub = gun.__.graph[done.ref]; + expect(root.hello).to.be('key'); + expect(root.yay).to.not.be.ok(); + expect(Gun.is.soul.on(sub)).to.be(done.ref); + expect(sub.yay).to.be('value'); + expect(sub.happy).to.be('faces'); + done(); + }); + }); + + it('get node path put value conflict relation', function(done){ + gun.get('hello/key', function(err, data){ + expect(err).to.not.be.ok(); + var c = 0; + Gun.is.graph(data, function(node){ + expect(c++).to.be(0); + if(Gun.obj.empty(node, Gun._.meta)){ return true } + expect(done.ref = Gun.is.soul(node.hi)).to.be.ok(); + done.soul = Gun.is.soul.on(node); + }); + }).path('hi').put('crushed', function(err, ok){ + expect(err).to.not.be.ok(); + var root = gun.__.graph[done.soul]; + var sub = gun.__.graph[done.ref]; + expect(root.hello).to.be('key'); + expect(root.yay).to.not.be.ok(); + expect(Gun.is.soul.on(sub)).to.be(done.ref); + expect(sub.yay).to.be('value'); + expect(sub.happy).to.be('faces'); + expect(root.hi).to.be('crushed'); + done(); + }); + }); + + /* + it('put gun node', function(done){ + var mark = gun.put({age: 23, name: "Mark Nadal"}); + var amber = gun.put({age: 23, name: "Amber Nadal"}); + mark.path('wife').put(amber, function(err){ + expect(err).to.not.be.ok(); + expect(false).to.be.ok(); // what whatttt??? + }); + }); + */ + + it('put val', function(done){ + gun.put({hello: "world"}).val(function(val){ expect(val.hello).to.be('world'); done(); }); }); - - it('load', function(done){ - gun.load('hello/world').get(function(val){ + + it('put key val', function(done){ + gun.put({hello: "world"}).key('hello/world').val(function(val){ expect(val.hello).to.be('world'); done(); }); }); - - it('load path', function(done){ - gun.load('hello/world').path('hello').get(function(val){ + + it('get', function(done){ + gun.get('hello/world').val(function(val){ + expect(val.hello).to.be('world'); + done(); + }); + }); + + it('get path', function(done){ + gun.get('hello/world').path('hello').val(function(val){ expect(val).to.be('world'); done(); }); }); - - it('load set path', function(done){ - gun.load('hello/world').set({hello: 'Mark'}).path('hello').get(function(val){ + + it('get put path', function(done){ + gun.get('hello/world').put({hello: 'Mark'}).path('hello').val(function(val){ expect(val).to.be('Mark'); done(); }); }); - - it('load path set', function(done){ - gun.load('hello/world').path('hello').set('World').get(function(val){ + + it('get path put', function(done){ + gun.get('hello/world').path('hello').put('World').val(function(val){ expect(val).to.be('World'); done(); }); }); - it('load path empty set', function(done){ - gun.load('hello/world').path('earth').set('mars').get(function(val){ + it('get path empty put', function(done){ + gun.get('hello/world').path('earth').put('mars').val(function(val){ expect(val).to.be('mars'); done(); }); }); - - it('load path get', function(done){ - gun.load('hello/world').path('earth').get(function(val){ + + it('get path val', function(done){ + gun.get('hello/world').path('earth').val(function(val){ expect(val).to.be('mars'); done(); }); }); - - it('key set get', function(done){ - gun.key('world/hello').set({world: "hello"}).get(function(val){ - expect(val.world).to.be('hello'); - done(); - }); - }); - - it('load again', function(done){ - gun.load('world/hello').get(function(val){ + + /* // CHANGELOG: This behavior is no longer allowed! Sorry peeps. + it('key put val', function(done){ + gun.key('world/hello').put({world: "hello"}).val(function(val){ expect(val.world).to.be('hello'); done(); }); }); - it('load blank kick get', function(done){ // it would be cool with GUN - gun.load("some/empty/thing").blank(function(){ // that if you call blank first - this.set({now: 'exists'}); // you can set stuff - }).get(function(val){ // and THEN still retrieve it. + it('get again', function(done){ + gun.get('world/hello').val(function(val){ + expect(val.world).to.be('hello'); + done(); + }); + }); + */ + + it('get not kick val', function(done){ + gun.get("some/empty/thing").not(function(){ // that if you call not first + return this.put({now: 'exists'}).key("some/empty/thing"); // you can put stuff + }).val(function(val){ // and THEN still retrieve it. expect(val.now).to.be('exists'); done(); }); }); - - it('load blank kick get when it already exists', function(done){ - gun.load("some/empty/thing").blank(function(){ - this.set({now: 'THIS SHOULD NOT HAPPEN'}); - }).get(function(val){ + + it('get not kick val when it already exists', function(done){ + gun.get("some/empty/thing").not(function(){ + return this.put({now: 'THIS SHOULD NOT HAPPEN'}); + }).val(function(val){ expect(val.now).to.be('exists'); done(); }); }); - - it('set path get sub', function(done){ - gun.set({last: {some: 'object'}}).path('last').get(function(val){ + + it('put path val sub', function(done){ + gun.put({last: {some: 'object'}}).path('last').val(function(val){ expect(val.some).to.be('object'); done(); }); }); - it('load set null', function(done){ - gun.set({last: {some: 'object'}}).path('last').get(function(val){ + it('get put null', function(done){ + gun.put({last: {some: 'object'}}).path('last').val(function(val){ expect(val.some).to.be('object'); - }).set(null, function(err){ - //console.log("ERR?", err); - }).get(function(val){ + }).put(null).val(function(val){ expect(val).to.be(null); done(); }); }); - - it('var set key path', function(done){ // contexts should be able to be saved to a variable - var foo = gun.set({foo: 'bar'}).key('foo/bar'); + + it('var put key path', function(done){ // contexts should be able to be saved to a variable + var foo = gun.put({foo: 'bar'}).key('foo/bar'); foo.path('hello.world.nowhere'); // this should become a sub-context, that doesn't alter the original setTimeout(function(){ - foo.path('foo').get(function(val){ // and then the original should be able to be reused later + foo.path('foo').val(function(val){ // and then the original should be able to be reused later expect(val).to.be('bar'); // this should work done(); }); - }, 100); + }, 500); }); - - it('var load path', function(done){ // contexts should be able to be saved to a variable - var foo = gun.load('foo/bar'); + + it('var get path', function(done){ // contexts should be able to be saved to a variable + var foo = gun.get('foo/bar'); foo.path('hello.world.nowhere'); // this should become a sub-context, that doesn't alter the original setTimeout(function(){ - foo.path('foo').get(function(val){ // and then the original should be able to be reused later + foo.path('foo').val(function(val){ // and then the original should be able to be reused later expect(val).to.be('bar'); // this should work done(); }); - }, 100); + }, 500); }); - - it('load blank set get path get', function(done){ // stickies issue - gun.load("examples/list/foobar").blank(function(){ - this.set({ - id: 'foobar', - title: 'awesome title', - todos: {} - }); - }).get(function(data){ + + it('get not put val path val', function(done){ + gun.get("examples/list/foobar").not(function(){ + return this.put({ + id: 'foobar', + title: 'awesome title', + todos: {} + }).key("examples/list/foobar"); + }).val(function(data){ expect(data.id).to.be('foobar'); - }).path('todos').get(function(todos){ + }).path('todos').val(function(todos){ expect(todos).to.not.have.property('id'); done(); }); }); + + it('put circular ref', function(done){ + var data = {}; + data[0] = "DATA!"; + data.a = {c: 'd', e: 1, f: true}; + data.b = {x: 2, y: 'z'}; + data.a.kid = data.b; + data.b.parent = data.a; + gun.put(data, function(err, ok){ + expect(err).to.not.be.ok(); + }).val(function(val){ + var a = gun.__.graph[Gun.is.soul(val.a)]; + var b = gun.__.graph[Gun.is.soul(val.b)]; + expect(Gun.is.soul(val.a)).to.be(Gun.is.soul.on(a)); + expect(Gun.is.soul(val.b)).to.be(Gun.is.soul.on(b)); + expect(Gun.is.soul(a.kid)).to.be(Gun.is.soul.on(b)); + expect(Gun.is.soul(b.parent)).to.be(Gun.is.soul.on(a)); + done(); + }); + }); - it('set partial sub merge', function(done){ - var mark = gun.set({name: "Mark", wife: { name: "Amber" }}).key('person/mark').get(function(mark){ + it('put circular deep', function(done){ + var mark = { + age: 23, + name: "Mark Nadal" + } + var amber = { + age: 23, + name: "Amber Nadal", + phd: true + } + mark.wife = amber; + amber.husband = mark; + var cat = { + age: 3, + name: "Hobbes" + } + mark.pet = cat; + amber.pet = cat; + cat.owner = mark; + cat.master = amber; + gun.put(mark, function(err, ok){ + expect(err).to.not.be.ok(); + }).val(function(val){ + expect(val.age).to.be(23); + expect(val.name).to.be("Mark Nadal"); + expect(Gun.is.soul(val.wife)).to.be.ok(); + expect(Gun.is.soul(val.pet)).to.be.ok(); + }).path('wife.pet.name').val(function(val){ + expect(val).to.be('Hobbes'); + }).back.path('pet.master').val(function(val){ + expect(val.name).to.be("Amber Nadal"); + expect(val.phd).to.be.ok(); + expect(val.age).to.be(23); + expect(Gun.is.soul(val.pet)).to.be.ok(); + done(); + }); + }); + + it('put partial sub merge', function(done){ + var mark = gun.put({name: "Mark", wife: { name: "Amber" }}).key('person/mark').val(function(mark){ expect(mark.name).to.be("Mark"); }); - mark.set({age: 23, wife: {age: 23}}); + mark.put({age: 23, wife: {age: 23}}); setTimeout(function(){ - mark.set({citizen: "USA", wife: {citizen: "USA"}}).get(function(mark){ + mark.put({citizen: "USA", wife: {citizen: "USA"}}).val(function(mark){ expect(mark.name).to.be("Mark"); expect(mark.age).to.be(23); expect(mark.citizen).to.be("USA"); - - this.path('wife').get(function(Amber){ + this.path('wife').val(function(Amber){ expect(Amber.name).to.be("Amber"); expect(Amber.age).to.be(23); expect(Amber.citizen).to.be("USA"); done(); }); }); - }, 50); + }, 500); }); - + it('path path', function(done){ - var deep = gun.set({some: {deeply: {nested: 'value'}}}); - deep.path('some.deeply.nested').get(function(val){ + var deep = gun.put({some: {deeply: {nested: 'value'}}}); + deep.path('some.deeply.nested').val(function(val){ expect(val).to.be('value'); }); - deep.path('some').path('deeply').path('nested').get(function(val){ + deep.path('some').path('deeply').path('nested').val(function(val){ expect(val).to.be('value'); done(); }); }); - - it('context null set value get error', function(done){ - gun.set("oh yes",function(err){ - expect(err).to.be.ok(); - done(); - }); - }); - - var foo; - it('context null set node', function(done){ - foo = gun.set({foo: 'bar'}).get(function(obj){ - expect(obj.foo).to.be('bar'); - done(); - }); - }); - - it('context node set val', function(done){ - foo.set('banana', function(err){ + + it('context null put value val error', function(done){ + gun.put("oh yes", function(err){ expect(err).to.be.ok(); done(); }); }); - it('context node set node', function(done){ - foo.set({bar: {zoo: 'who'}}).get(function(obj){ + var foo; + it('context null put node', function(done){ + foo = gun.put({foo: 'bar'}).val(function(obj){ + expect(obj.foo).to.be('bar'); + done(); //setTimeout(function(){ done() },1); + }); + }); + + it('context node put val', function(done){ + // EFFECTIVELY a TIMEOUT from the previous test. NO LONGER! + foo.put('banana', function(err){ + expect(err).to.be.ok(); + done(); //setTimeout(function(){ done() },1); + }); + }); + + it('context node put node', function(done){ + // EFFECTIVELY a TIMEOUT from the previous test. NO LONGER! + foo.put({bar: {zoo: 'who'}}).val(function(obj){ expect(obj.foo).to.be('bar'); expect(Gun.is.soul(obj.bar)).to.ok(); - done(); + done(); //setTimeout(function(){ done() },1); }); }); - it('context node and field set value', function(done){ + it('context node and field put value', function(done){ + // EFFECTIVELY a TIMEOUT from the previous test. NO LONGER! var tar = foo.path('tar'); - tar.set('zebra').get(function(val){ + tar.put('zebra').val(function(val){ expect(val).to.be('zebra'); - done(); + done(); //setTimeout(function(){ done() },1); }); }); - + var bar; - it('context node and field of relation set node', function(done){ + it('context node and field of relation put node', function(done){ + // EFFECTIVELY a TIMEOUT from the previous test. NO LONGER! bar = foo.path('bar'); - bar.set({combo: 'double'}).get(function(obj){ + bar.put({combo: 'double'}).val(function(obj){ expect(obj.zoo).to.be('who'); expect(obj.combo).to.be('double'); - done(); + done(); //setTimeout(function(){ done() },1); }); }); - - it('context node and field, set node', function(done){ - bar.path('combo').set({another: 'node'}).get(function(obj){ + + it('context node and field, put node', function(done){ + // EFFECTIVELY a TIMEOUT from the previous test. NO LONGER! + bar.path('combo').put({another: 'node'}).val(function(obj){ expect(obj.another).to.be('node'); - bar.get(function(node){ + bar.val(function(node){ expect(Gun.is.soul(node.combo)).to.be.ok(); expect(Gun.is.soul(node.combo)).to.be(Gun.is.soul.on(obj)); - done(); + done(); //setTimeout(function(){ done() },1); }); }); }); - - + + it('val path put val', function(done){ + var gun = Gun(); + + var al = gun.put({gender:'m', age:30, name:'alfred'}).key('user/alfred'); + var beth = gun.put({gender:'f', age:22, name:'beth' }).key('user/beth'); + + al.val(function(a){ + beth.path('friend').put(a).val(function(aa){ + expect(Gun.is.soul.on(a)).to.be(Gun.is.soul.on(aa)); + done(); + }); + }); + + }); + + it('val path put val key', function(done){ // bug discovered from Jose's visualizer // TODO: still timing issues, 0.6! + var gun = Gun(), s = Gun.time.is(), n = function(){ return Gun.time.is() } + this.timeout(5000); + + gun.put({gender:'m', age:30, name:'alfred'}).key('user/alfred'); + gun.put({gender:'f', age:22, name:'beth' }).key('user/beth'); + gun.get('user/alfred').val(function(a){ + gun.get('user/beth').path('friend').put(a); // b - friend_of -> a + gun.get('user/beth').val(function(b){ // TODO: We should have b.friend by now! + gun.get('user/alfred').path('friend').put(b, function(){ // a - friend_of -> b + gun.get('user/beth').path('cat').put({name: "fluffy", age: 3, coat: "tabby"}, function(err, ok){ + + gun.get('user/alfred').path('friend.cat').key('the/cat'); + + gun.get('the/cat').val(function(c){ + expect(c.name).to.be('fluffy'); + expect(c.age).to.be(3); + expect(c.coat).to.be('tabby'); + done(); + }); + }); + }); + }); + }); + }); + it('map', function(done){ - var c = 0, map = gun.set({a: {here: 'you'}, b: {go: 'dear'}, c: {sir: '!'} }); - map.map(function(obj, soul){ + var c = 0, set = gun.put({a: {here: 'you'}, b: {go: 'dear'}, c: {sir: '!'} }); + set.map(function(obj, field){ c++; - if(soul === 'a'){ + if(field === 'a'){ expect(obj.here).to.be('you'); } - if(soul === 'b'){ + if(field === 'b'){ expect(obj.go).to.be('dear'); } - if(soul === 'c'){ + if(field === 'c'){ expect(obj.sir).to.be('!'); } if(c === 3){ @@ -591,5 +1450,429 @@ describe('Gun', function(){ } }) }); + + it('key soul', function(done){ + var gun = Gun(); + gun.key('me', function(err, ok){ + expect(err).to.not.be.ok(); + expect(gun.__.key.s['me']).to.be.ok(); + Gun.is.graph(gun.__.key.s['me'], function(node, soul){ done.soul = soul }); + expect(done.soul).to.be.ok('qwertyasdfzxcv'); + done(); + }, 'qwertyasdfzxcv'); + }); + + it('no false positive null emit', function(done){ + var gun = Gun({hooks: {get: function(key, cb){ + var g = {}; + g[soul] = {_: {'#': soul, '>': {'a': 0}}, + 'a': 'b' + }; + cb(null, g); + g = {}; + g[soul] = {_: {'#': soul, '>': {'c': 0}}, + 'c': 'd' + }; + cb(null, g); + g = {}; + g[soul] = {_: {'#': soul }}; + cb(null, g); + cb(); // false trigger! + }}}), soul = Gun.text.random(); + + gun.get('me').not(function(){ + done.fail = true; + }).val(function(val){ + setTimeout(function(){ + expect(val.a).to.be('b'); + expect(val.c).to.be('d'); + expect(done.fail).to.not.be.ok(); + done(); + },5); + }); + }); + + it('unique val on stream', function(done){ + var gun = Gun({hooks: {get: function(key, cb){ + var g = {}; + g[soul] = {_: {'#': soul, '>': {'a': 0}}, + 'a': 'b' + }; + cb(null, g); + g = {}; + g[soul] = {_: {'#': soul, '>': {'c': 0}}, + 'c': 'd' + }; + cb(null, g); + g = {}; + g[soul] = {_: {'#': soul }}; + cb(null, g); + }}}), soul = Gun.text.random(); + + gun.get('me').val(function(val){ + done.count = (done.count || 0) + 1; + setTimeout(function(){ + expect(val.a).to.be('b'); + expect(val.c).to.be('d'); + expect(done.count).to.be(1); + done(); + },5); + }); + }); + + it('unique path val on stream', function(done){ + var gun = Gun({hooks: {get: function(key, cb){ + var g = {}; + g[soul] = {_: {'#': soul, '>': {'a': 0}}, + 'a': 'a' + }; + cb(null, g); + g = {}; + g[soul] = {_: {'#': soul, '>': {'a': 1}}, + 'a': 'b' + }; + cb(null, g); + g = {}; + g[soul] = {_: {'#': soul }}; + cb(null, g); + }}}), soul = Gun.text.random(); + + gun.get('me').path('a').val(function(val){ + done.count = (done.count || 0) + 1; + setTimeout(function(){ + expect(val).to.be('b'); + expect(done.count).to.be(1); + done(); + },5); + }); + }); + + it('double not', function(done){ // from the thought tutorial + var gun = Gun().get('thoughts').not(function(n, key){ + return this.put({}).key(key); + }); + + setTimeout(function(){ + gun.not(function(){ + console.log("DOUBLE NOT!!!!!!"); + done.not = true; + }).val(function(){ + expect(done.not).to.not.be.ok(); + done(); + }) + }, 10); + }); + + it('set', function(done){ + done.c = 0; + var gun = Gun(); + gun.get('set').set().set().val(function(val){ + done.c += 1; + expect(Gun.obj.empty(val, '_')).to.be.ok(); + setTimeout(function(){ + expect(done.c).to.be(1); + done() + },10) + }); + }); + + it('set multiple', function(done){ + var gun = Gun().get('sets').set(), i = 0; + gun.val(function(val){ + expect(done.soul = Gun.is.soul.on(val)).to.be.ok(); + expect(Gun.obj.empty(val, '_')).to.be.ok(); + }); + + gun.set(1).set(2).set(3).set(4); // if you set an object you'd have to do a `.back` + gun.map().val(function(val){ + i += 1; + expect(val).to.be(i); + if(i % 4 === 0){ + setTimeout(function(){ + done.i = 0; + Gun.obj.map(gun.__.graph, function(){ done.i++ }); + expect(done.i).to.be(1); // make sure there isn't double. + done() + },10); + } + }); + }); + + it('peer 1 get key, peer 2 put key, peer 1 val', function(done){ + var hooks = {get: function(key, cb, opt){ + cb(); + }, put: function(nodes, cb, opt){ + //console.log("put hook", nodes); + Gun.union(gun1, nodes); + cb(); + }, key: function(key, soul, cb, opt){ + //console.log("key hook", key, soul); + gun1.key(key, null, soul); + cb(); + }}, + gun1 = Gun({hooks: {get: hooks.get}}).get('race') + , gun2 = Gun({hooks: hooks}).get('race'); + + setTimeout(function(){ + gun2.put({the: 'data'}).key('race'); + setTimeout(function(){ + gun1.on(function(val){ + expect(val.the).to.be('data'); + done(); + }); + },10); + },10); + }); + + it('get pseudo merge', function(done){ + var gun = Gun(); + + gun.put({a: 1, z: -1}).key('pseudo'); + gun.put({b: 2, z: 0}).key('pseudo'); + + gun.get('pseudo').val(function(val){ + expect(val.a).to.be(1); + expect(val.b).to.be(2); + expect(val.z).to.be(0); + done(); + }); + }); + + it('get pseudo merge on', function(done){ + var gun = Gun(); + + gun.put({a: 1, z: -1}).key('pseudon'); + gun.put({b: 2, z: 0}).key('pseudon'); + + gun.get('pseudon').on(function(val){ + if(done.val){ return } // TODO: Maybe prevent repeat ons where there is no diff? + done.val = val; + expect(val.a).to.be(1); + expect(val.b).to.be(2); + expect(val.z).to.be(0); + done(); + }); + }); + + it('get pseudo merge across peers', function(done){ + Gun.on('opt').event(function(gun, o){ + if(connect){ return } + gun.__.opt.hooks = {get: function(key, cb, opt){ + var other = (o.alice? gun2 : gun1); + if(connect){ + //console.log('connect to peer and get', key); + other.get(key, cb); + } else { + cb(); + } + }, put: function(nodes, cb, opt){ + var other = (o.alice? gun2 : gun1); + if(connect){ + Gun.union(other, nodes); + } + cb(); + }, key: function(key, soul, cb, opt){ + var other = (o.alice? gun2 : gun1); + if(connect){ + other.key(key, null, soul); + } + cb(); + }} + }); + var connect, gun1 = Gun({alice: true}).get('pseudo/merge').not(function(){ + return this.put({hello: "world!"}).key('pseudo/merge'); + }), gun2; + + gun1.val(function(val){ + expect(val.hello).to.be('world!'); + }); + setTimeout(function(){ + gun2 = Gun({bob: true}).get('pseudo/merge').not(function(){ + return this.put({hi: "mars!"}).key('pseudo/merge'); + }); + gun2.val(function(val){ + expect(val.hi).to.be('mars!'); + }); + setTimeout(function(){ + // CONNECT THE TWO PEERS + connect = true; + gun1.get('pseudo/merge', null, {force: true}); // fake a browser refersh, in real world we should auto-reconnect + gun2.get('pseudo/merge', null, {force: true}); // fake a browser refersh, in real world we should auto-reconnect + setTimeout(function(){ + gun1.val(function(val){ + expect(val.hello).to.be('world!'); + expect(val.hi).to.be('mars!'); + done.gun1 = true; + }); + gun2.val(function(val){ + expect(val.hello).to.be('world!'); + expect(val.hi).to.be('mars!'); + expect(done.gun1).to.be.ok(); + Gun({}); + done(); + }); + },10); + },10); + },10); + }); + }); + + describe('Streams', function(){ + var gun = Gun(), g = function(){ + return Gun({hooks: {get: ctx.get}}); + }, ctx = {gen: 5, extra: 45, network: 2}; + + it('prep hook', function(done){ + this.timeout(ctx.gen * ctx.extra); + var peer = Gun(), ref; + ctx.get = function(key, cb){ + var c = 0; + cb = cb || function(){}; + if('big' !== key){ return cb(null, null) } + setTimeout(function badNetwork(){ + c += 1; + var soul = Gun.is.soul.on(ref); + var graph = {}; + var data = graph[soul] = {_: {'#': soul, '>': {}}}; + if(!ref['f' + c]){ + return cb(null, graph), cb(null, {}); + } + data._[Gun._.HAM]['f' + c] = ref._[Gun._.HAM]['f' + c]; + data['f' + c] = ref['f' + c]; + cb(null, graph); + setTimeout(badNetwork, ctx.network); + },ctx.network); + } + ctx.get.fake = {}; + for(var i = 1; i < (ctx.gen) + 1; i++){ + ctx.get.fake['f'+i] = i; + ctx.length = i; + } + var big = peer.put(ctx.get.fake).val(function(val){ + ref = val; + ctx.get('big', function(err, graph){ + if(Gun.obj.empty(graph)){ done() } + }); + gun.opt({hooks: {get: ctx.get}}); + }); + }); + + it('map chain', function(done){ + var set = gun.put({a: {here: 'you'}, b: {go: 'dear'}, c: {sir: '!'} }); + set.map().val(function(obj, field){ + if(obj.here){ + done.a = obj.here; + expect(obj.here).to.be('you'); + } + if(obj.go){ + done.b = obj.go; + expect(obj.go).to.be('dear'); + } + if(obj.sir){ + done.c = obj.sir; + expect(obj.sir).to.be('!'); + } + if(done.a && done.b && done.c){ + done(); + } + }); + }); + + it('map chain path', function(done){ + var set = gun.put({ + a: {name: "Mark", + pet: {coat: "tabby", name: "Hobbes"} + }, b: {name: "Alice", + pet: {coat: "calico", name: "Cali"} + }, c: {name: "Bob", + pet: {coat: "tux", name: "Casper"} + } + }); + set.map().path('pet').val(function(obj, field){ + if(obj.name === 'Hobbes'){ + done.hobbes = obj.name; + expect(obj.name).to.be('Hobbes'); + expect(obj.coat).to.be('tabby'); + } + if(obj.name === 'Cali'){ + done.cali = obj.name; + expect(obj.name).to.be('Cali'); + expect(obj.coat).to.be('calico'); + } + if(obj.name === 'Casper'){ + done.casper = obj.name; + expect(obj.name).to.be('Casper'); + expect(obj.coat).to.be('tux'); + } + if(done.hobbes && done.cali && done.casper){ + done(); + } + }); + }); + + it('get big on', function(done){ + this.timeout(ctx.gen * ctx.extra); + var test = {c: 0, last: 0}; + g().get('big').on(function(val){ + if(test.done){ return console.log("hey yo! you got duplication on your ons!"); } + delete val._; + if(val['f' + (test.last + 1)]){ + test.c += 1; + test.last += 1; + } + var obj = {}; + for(var i = 1; i < test.c + 1; i++){ + obj['f'+i] = i; + } + expect(val).to.eql(obj); + if(test.c === ctx.length){ + test.done = true; + done(); + } + }); + }); + + it('get big on delta', function(done){ + this.timeout(ctx.gen * ctx.extra); + var test = {c: 0, seen: {}}; + g().get('big').on(function(val){ + delete val._; + if(test.seen['f' + test.c]){ return } + test.seen['f' + test.c] = true; + test.c += 1; + var obj = {}; + obj['f' + test.c] = test.c; + expect(val).to.eql(obj); + if(test.c === ctx.length){ + done(); + } + }, true); + }); + + it('get val', function(done){ + this.timeout(ctx.gen * ctx.extra); + g().get('big').val(function(obj){ + delete obj._; + expect(obj.f1).to.be(1); + expect(obj['f' + ctx.length]).to.be(ctx.length); + expect(obj).to.be.eql(ctx.get.fake); + done(); + }); + }); + + it('get big map val', function(done){ + this.timeout(ctx.gen * ctx.extra); + var test = {c: 0, seen: {}}; + g().get('big').map().val(function(val, field){ + if(test.seen[field]){ return } + test.seen[field] = true; + delete val._; + expect(field).to.be('f' + (test.c += 1)); + expect(val).to.be(test.c); + if(test.c === ctx.length){ + done(); + } + }); + }); }); }); \ No newline at end of file diff --git a/test/group.js b/test/group.js deleted file mode 100644 index 1371e2e9..00000000 --- a/test/group.js +++ /dev/null @@ -1,24 +0,0 @@ -(function(){ // group test - - var Gun = require('../index'); - require('../lib/group'); - var gun = Gun({file: 'data.json'}); - - gun.group({ - name: "Mark Nadal", - age: 23, - type: "human" - }).group({ - name: "Amber Nadal", - age: 23, - type: "human" - }).group({ - name: "Hobbes", - age: 4, - type: "kitten" - }).get(function(g){ - //console.log("GOT", g, this.__.graph); - }).map(function(val, id){ - //console.log("map", id, val); - }); -}()); \ No newline at end of file diff --git a/test/json2.js b/test/json2.js new file mode 100644 index 00000000..a0653cc2 --- /dev/null +++ b/test/json2.js @@ -0,0 +1,480 @@ +/* + http://www.JSON.org/json2.js + 2011-02-23 + + Public Domain. + + NO WARRANTY EXPRESSED OR IMPLIED. USE AT YOUR OWN RISK. + + See http://www.JSON.org/js.html + + + This code should be minified before deployment. + See http://javascript.crockford.com/jsmin.html + + USE YOUR OWN COPY. IT IS EXTREMELY UNWISE TO LOAD CODE FROM SERVERS YOU DO + NOT CONTROL. + + + This file creates a global JSON object containing two methods: stringify + and parse. + + JSON.stringify(value, replacer, space) + value any JavaScript value, usually an object or array. + + replacer an optional parameter that determines how object + values are stringified for objects. It can be a + function or an array of strings. + + space an optional parameter that specifies the indentation + of nested structures. If it is omitted, the text will + be packed without extra whitespace. If it is a number, + it will specify the number of spaces to indent at each + level. If it is a string (such as '\t' or ' '), + it contains the characters used to indent at each level. + + This method produces a JSON text from a JavaScript value. + + When an object value is found, if the object contains a toJSON + method, its toJSON method will be called and the result will be + stringified. A toJSON method does not serialize: it returns the + value represented by the name/value pair that should be serialized, + or undefined if nothing should be serialized. The toJSON method + will be passed the key associated with the value, and this will be + bound to the value + + For example, this would serialize Dates as ISO strings. + + Date.prototype.toJSON = function (key) { + function f(n) { + // Format integers to have at least two digits. + return n < 10 ? '0' + n : n; + } + + return this.getUTCFullYear() + '-' + + f(this.getUTCMonth() + 1) + '-' + + f(this.getUTCDate()) + 'T' + + f(this.getUTCHours()) + ':' + + f(this.getUTCMinutes()) + ':' + + f(this.getUTCSeconds()) + 'Z'; + }; + + You can provide an optional replacer method. It will be passed the + key and value of each member, with this bound to the containing + object. The value that is returned from your method will be + serialized. If your method returns undefined, then the member will + be excluded from the serialization. + + If the replacer parameter is an array of strings, then it will be + used to select the members to be serialized. It filters the results + such that only members with keys listed in the replacer array are + stringified. + + Values that do not have JSON representations, such as undefined or + functions, will not be serialized. Such values in objects will be + dropped; in arrays they will be replaced with null. You can use + a replacer function to replace those with JSON values. + JSON.stringify(undefined) returns undefined. + + The optional space parameter produces a stringification of the + value that is filled with line breaks and indentation to make it + easier to read. + + If the space parameter is a non-empty string, then that string will + be used for indentation. If the space parameter is a number, then + the indentation will be that many spaces. + + Example: + + text = JSON.stringify(['e', {pluribus: 'unum'}]); + // text is '["e",{"pluribus":"unum"}]' + + + text = JSON.stringify(['e', {pluribus: 'unum'}], null, '\t'); + // text is '[\n\t"e",\n\t{\n\t\t"pluribus": "unum"\n\t}\n]' + + text = JSON.stringify([new Date()], function (key, value) { + return this[key] instanceof Date ? + 'Date(' + this[key] + ')' : value; + }); + // text is '["Date(---current time---)"]' + + + JSON.parse(text, reviver) + This method parses a JSON text to produce an object or array. + It can throw a SyntaxError exception. + + The optional reviver parameter is a function that can filter and + transform the results. It receives each of the keys and values, + and its return value is used instead of the original value. + If it returns what it received, then the structure is not modified. + If it returns undefined then the member is deleted. + + Example: + + // Parse the text. Values that look like ISO date strings will + // be converted to Date objects. + + myData = JSON.parse(text, function (key, value) { + var a; + if (typeof value === 'string') { + a = +/^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2}(?:\.\d*)?)Z$/.exec(value); + if (a) { + return new Date(Date.UTC(+a[1], +a[2] - 1, +a[3], +a[4], + +a[5], +a[6])); + } + } + return value; + }); + + myData = JSON.parse('["Date(09/09/2001)"]', function (key, value) { + var d; + if (typeof value === 'string' && + value.slice(0, 5) === 'Date(' && + value.slice(-1) === ')') { + d = new Date(value.slice(5, -1)); + if (d) { + return d; + } + } + return value; + }); + + + This is a reference implementation. You are free to copy, modify, or + redistribute. +*/ + +/*jslint evil: true, strict: false, regexp: false */ + +/*members "", "\b", "\t", "\n", "\f", "\r", "\"", JSON, "\\", apply, + call, charCodeAt, getUTCDate, getUTCFullYear, getUTCHours, + getUTCMinutes, getUTCMonth, getUTCSeconds, hasOwnProperty, join, + lastIndex, length, parse, prototype, push, replace, slice, stringify, + test, toJSON, toString, valueOf +*/ + + +// Create a JSON object only if one does not already exist. We create the +// methods in a closure to avoid creating global variables. + +var JSON; +if (!JSON) { + JSON = {}; +} + +(function () { + "use strict"; + + function f(n) { + // Format integers to have at least two digits. + return n < 10 ? '0' + n : n; + } + + if (typeof Date.prototype.toJSON !== 'function') { + + Date.prototype.toJSON = function (key) { + + return isFinite(this.valueOf()) ? + this.getUTCFullYear() + '-' + + f(this.getUTCMonth() + 1) + '-' + + f(this.getUTCDate()) + 'T' + + f(this.getUTCHours()) + ':' + + f(this.getUTCMinutes()) + ':' + + f(this.getUTCSeconds()) + 'Z' : null; + }; + + String.prototype.toJSON = + Number.prototype.toJSON = + Boolean.prototype.toJSON = function (key) { + return this.valueOf(); + }; + } + + var cx = /[\u0000\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g, + escapable = /[\\\"\x00-\x1f\x7f-\x9f\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g, + gap, + indent, + meta = { // table of character substitutions + '\b': '\\b', + '\t': '\\t', + '\n': '\\n', + '\f': '\\f', + '\r': '\\r', + '"' : '\\"', + '\\': '\\\\' + }, + rep; + + + function quote(string) { + +// If the string contains no control characters, no quote characters, and no +// backslash characters, then we can safely slap some quotes around it. +// Otherwise we must also replace the offending characters with safe escape +// sequences. + + escapable.lastIndex = 0; + return escapable.test(string) ? '"' + string.replace(escapable, function (a) { + var c = meta[a]; + return typeof c === 'string' ? c : + '\\u' + ('0000' + a.charCodeAt(0).toString(16)).slice(-4); + }) + '"' : '"' + string + '"'; + } + + + function str(key, holder) { + +// Produce a string from holder[key]. + + var i, // The loop counter. + k, // The member key. + v, // The member value. + length, + mind = gap, + partial, + value = holder[key]; + +// If the value has a toJSON method, call it to obtain a replacement value. + + if (value && typeof value === 'object' && + typeof value.toJSON === 'function') { + value = value.toJSON(key); + } + +// If we were called with a replacer function, then call the replacer to +// obtain a replacement value. + + if (typeof rep === 'function') { + value = rep.call(holder, key, value); + } + +// What happens next depends on the value's type. + + switch (typeof value) { + case 'string': + return quote(value); + + case 'number': + +// JSON numbers must be finite. Encode non-finite numbers as null. + + return isFinite(value) ? String(value) : 'null'; + + case 'boolean': + case 'null': + +// If the value is a boolean or null, convert it to a string. Note: +// typeof null does not produce 'null'. The case is included here in +// the remote chance that this gets fixed someday. + + return String(value); + +// If the type is 'object', we might be dealing with an object or an array or +// null. + + case 'object': + +// Due to a specification blunder in ECMAScript, typeof null is 'object', +// so watch out for that case. + + if (!value) { + return 'null'; + } + +// Make an array to hold the partial results of stringifying this object value. + + gap += indent; + partial = []; + +// Is the value an array? + + if (Object.prototype.toString.apply(value) === '[object Array]') { + +// The value is an array. Stringify every element. Use null as a placeholder +// for non-JSON values. + + length = value.length; + for (i = 0; i < length; i += 1) { + partial[i] = str(i, value) || 'null'; + } + +// Join all of the elements together, separated with commas, and wrap them in +// brackets. + + v = partial.length === 0 ? '[]' : gap ? + '[\n' + gap + partial.join(',\n' + gap) + '\n' + mind + ']' : + '[' + partial.join(',') + ']'; + gap = mind; + return v; + } + +// If the replacer is an array, use it to select the members to be stringified. + + if (rep && typeof rep === 'object') { + length = rep.length; + for (i = 0; i < length; i += 1) { + if (typeof rep[i] === 'string') { + k = rep[i]; + v = str(k, value); + if (v) { + partial.push(quote(k) + (gap ? ': ' : ':') + v); + } + } + } + } else { + +// Otherwise, iterate through all of the keys in the object. + + for (k in value) { + if (Object.prototype.hasOwnProperty.call(value, k)) { + v = str(k, value); + if (v) { + partial.push(quote(k) + (gap ? ': ' : ':') + v); + } + } + } + } + +// Join all of the member texts together, separated with commas, +// and wrap them in braces. + + v = partial.length === 0 ? '{}' : gap ? + '{\n' + gap + partial.join(',\n' + gap) + '\n' + mind + '}' : + '{' + partial.join(',') + '}'; + gap = mind; + return v; + } + } + +// If the JSON object does not yet have a stringify method, give it one. + + if (typeof JSON.stringify !== 'function') { + JSON.stringify = function (value, replacer, space) { + +// The stringify method takes a value and an optional replacer, and an optional +// space parameter, and returns a JSON text. The replacer can be a function +// that can replace values, or an array of strings that will select the keys. +// A default replacer method can be provided. Use of the space parameter can +// produce text that is more easily readable. + + var i; + gap = ''; + indent = ''; + +// If the space parameter is a number, make an indent string containing that +// many spaces. + + if (typeof space === 'number') { + for (i = 0; i < space; i += 1) { + indent += ' '; + } + +// If the space parameter is a string, it will be used as the indent string. + + } else if (typeof space === 'string') { + indent = space; + } + +// If there is a replacer, it must be a function or an array. +// Otherwise, throw an error. + + rep = replacer; + if (replacer && typeof replacer !== 'function' && + (typeof replacer !== 'object' || + typeof replacer.length !== 'number')) { + throw new Error('JSON.stringify'); + } + +// Make a fake root object containing our value under the key of ''. +// Return the result of stringifying the value. + + return str('', {'': value}); + }; + } + + +// If the JSON object does not yet have a parse method, give it one. + + if (typeof JSON.parse !== 'function') { + JSON.parse = function (text, reviver) { + +// The parse method takes a text and an optional reviver function, and returns +// a JavaScript value if the text is a valid JSON text. + + var j; + + function walk(holder, key) { + +// The walk method is used to recursively walk the resulting structure so +// that modifications can be made. + + var k, v, value = holder[key]; + if (value && typeof value === 'object') { + for (k in value) { + if (Object.prototype.hasOwnProperty.call(value, k)) { + v = walk(value, k); + if (v !== undefined) { + value[k] = v; + } else { + delete value[k]; + } + } + } + } + return reviver.call(holder, key, value); + } + + +// Parsing happens in four stages. In the first stage, we replace certain +// Unicode characters with escape sequences. JavaScript handles many characters +// incorrectly, either silently deleting them, or treating them as line endings. + + text = String(text); + cx.lastIndex = 0; + if (cx.test(text)) { + text = text.replace(cx, function (a) { + return '\\u' + + ('0000' + a.charCodeAt(0).toString(16)).slice(-4); + }); + } + +// In the second stage, we run the text against regular expressions that look +// for non-JSON patterns. We are especially concerned with '()' and 'new' +// because they can cause invocation, and '=' because it can cause mutation. +// But just to be safe, we want to reject all unexpected forms. + +// We split the second stage into 4 regexp operations in order to work around +// crippling inefficiencies in IE's and Safari's regexp engines. First we +// replace the JSON backslash pairs with '@' (a non-JSON character). Second, we +// replace all simple value tokens with ']' characters. Third, we delete all +// open brackets that follow a colon or comma or that begin the text. Finally, +// we look to see that the remaining characters are only whitespace or ']' or +// ',' or ':' or '{' or '}'. If that is so, then the text is safe for eval. + + if (/^[\],:{}\s]*$/ + .test(text.replace(/\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})/g, '@') + .replace(/"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g, ']') + .replace(/(?:^|:|,)(?:\s*\[)+/g, ''))) { + +// In the third stage we use the eval function to compile the text into a +// JavaScript structure. The '{' operator is subject to a syntactic ambiguity +// in JavaScript: it can begin a block or an object literal. We wrap the text +// in parens to eliminate the ambiguity. + + j = eval('(' + text + ')'); + +// In the optional fourth stage, we recursively walk the new structure, passing +// each name/value pair to a reviver function for possible transformation. + + return typeof reviver === 'function' ? + walk({'': j}, '') : j; + } + +// If the text is not JSON parseable, then a SyntaxError is thrown. + + throw new SyntaxError('JSON.parse'); + }; + } +}()); \ No newline at end of file diff --git a/test/mocha.html b/test/mocha.html index 46be0264..3815c5d7 100644 --- a/test/mocha.html +++ b/test/mocha.html @@ -2,6 +2,7 @@ Gun Tests + diff --git a/test/set.js b/test/set.js new file mode 100644 index 00000000..912c6ac3 --- /dev/null +++ b/test/set.js @@ -0,0 +1,49 @@ +(function(){ return; + var Gun = require('../gun'); + var done = function(){}; + + var gun = Gun().get('set').set(), i = 0; + gun.val(function(val){ + console.log('t1', val); + }).set(1).set(2).set(3).set(4) // if you set an object you'd have to do a `.back` + .map().val(function(val){ // TODO! BUG? If we do gun.set it immediately calls and we get stale data. Is this wrong? + console.log('t2', val, ++i); + if(4 === i){ + console.log("TODO? BUG! Double soul?", gun.__.graph); + done() + } + }); + + return; // TODO! BUG! Causes tests to crash and burn badly. + + require('../lib/set'); + var gun = Gun(); + + var list = gun.get('thoughts'); + list.set('a'); + list.set('b'); + list.set('c'); + list.set('d').val(function(val){ + console.log('what', val, '\n\n'); + console.log(gun.__.graph); + }) + return; + gun.set({ + name: "Mark Nadal", + age: 23, + type: "human" + }).back.set({ + name: "Amber Nadal", + age: 23, + type: "human" + }).back.set({ + name: "Hobbes", + age: 4, + type: "kitten" + }).back.val(function(g){ + console.log("GOT", g, this.__.graph); + }).map(function(val, id){ + console.log("map", id, val); + }); + +}()); \ No newline at end of file diff --git a/test/tmp.js b/test/tmp.js index 87845334..0298a272 100644 --- a/test/tmp.js +++ b/test/tmp.js @@ -5,15 +5,15 @@ Gun.log.verbose = true; /* - gun.set({foo: "bar"}).get(function(val){ + gun.put({foo: "bar"}).val(function(val){ console.log("POWR HOUSE", val); - this.set({lol: 'pancakes'}).get(function(v){ + this.put({lol: 'pancakes'}).val(function(v){ console.log("YEAH CAKES", v); }) }); */ - gun.load('hello/world').set({hello: 'Mark'}).path('hello').get(function(val){ + gun.get('hello/world').put({hello: 'Mark'}).path('hello').val(function(val){ console.log("YO", val); expect(val).to.be('Mark'); done(); diff --git a/web/2015/15.html b/web/2015/15.html new file mode 100644 index 00000000..5eac6eac --- /dev/null +++ b/web/2015/15.html @@ -0,0 +1,57 @@ + + + + + + + +Fork me on GitHub + +Home + + + + + +
    +

    Do You have Time to Chat?

    +

    + Let's build a chat app. But we're going to do it in a mind boggling way. + Conversations take time to have, therefore rather than storing every message + individually, we are going to store them in time. What does that even mean? +

    +

    + The first requirement is understanding immutable data. + Most database systems overwrite old data with new data when there is an update. + This preserves data in space, but loses its history. + Immutable data is the idea of never changing data, + but appending a new record every time. Such approach preserves history. +

    + +

    + +

    +
    + + + + + + \ No newline at end of file diff --git a/web/dep/codemirror/codemirror.css b/web/dep/codemirror/codemirror.css new file mode 100644 index 00000000..ceacd130 --- /dev/null +++ b/web/dep/codemirror/codemirror.css @@ -0,0 +1,325 @@ +/* BASICS */ + +.CodeMirror { + /* Set height, width, borders, and global font properties here */ + font-family: monospace; + height: 300px; + color: black; +} + +/* PADDING */ + +.CodeMirror-lines { + padding: 4px 0; /* Vertical padding around content */ +} +.CodeMirror pre { + padding: 0 4px; /* Horizontal padding of content */ +} + +.CodeMirror-scrollbar-filler, .CodeMirror-gutter-filler { + background-color: white; /* The little square between H and V scrollbars */ +} + +/* GUTTER */ + +.CodeMirror-gutters { + border-right: 1px solid #ddd; + background-color: #f7f7f7; + white-space: nowrap; +} +.CodeMirror-linenumbers {} +.CodeMirror-linenumber { + padding: 0 3px 0 5px; + min-width: 20px; + text-align: right; + color: #999; + white-space: nowrap; +} + +.CodeMirror-guttermarker { color: black; } +.CodeMirror-guttermarker-subtle { color: #999; } + +/* CURSOR */ + +.CodeMirror div.CodeMirror-cursor { + border-left: 1px solid black; +} +/* Shown when moving in bi-directional text */ +.CodeMirror div.CodeMirror-secondarycursor { + border-left: 1px solid silver; +} +.CodeMirror.cm-fat-cursor div.CodeMirror-cursor { + width: auto; + border: 0; + background: #7e7; +} +.CodeMirror.cm-fat-cursor div.CodeMirror-cursors { + z-index: 1; +} + +.cm-animate-fat-cursor { + width: auto; + border: 0; + -webkit-animation: blink 1.06s steps(1) infinite; + -moz-animation: blink 1.06s steps(1) infinite; + animation: blink 1.06s steps(1) infinite; +} +@-moz-keyframes blink { + 0% { background: #7e7; } + 50% { background: none; } + 100% { background: #7e7; } +} +@-webkit-keyframes blink { + 0% { background: #7e7; } + 50% { background: none; } + 100% { background: #7e7; } +} +@keyframes blink { + 0% { background: #7e7; } + 50% { background: none; } + 100% { background: #7e7; } +} + +/* Can style cursor different in overwrite (non-insert) mode */ +div.CodeMirror-overwrite div.CodeMirror-cursor {} + +.cm-tab { display: inline-block; text-decoration: inherit; } + +.CodeMirror-ruler { + border-left: 1px solid #ccc; + position: absolute; +} + +/* DEFAULT THEME */ + +.cm-s-default .cm-keyword {color: #708;} +.cm-s-default .cm-atom {color: #219;} +.cm-s-default .cm-number {color: #164;} +.cm-s-default .cm-def {color: #00f;} +.cm-s-default .cm-variable, +.cm-s-default .cm-punctuation, +.cm-s-default .cm-property, +.cm-s-default .cm-operator {} +.cm-s-default .cm-variable-2 {color: #05a;} +.cm-s-default .cm-variable-3 {color: #085;} +.cm-s-default .cm-comment {color: #a50;} +.cm-s-default .cm-string {color: #a11;} +.cm-s-default .cm-string-2 {color: #f50;} +.cm-s-default .cm-meta {color: #555;} +.cm-s-default .cm-qualifier {color: #555;} +.cm-s-default .cm-builtin {color: #30a;} +.cm-s-default .cm-bracket {color: #997;} +.cm-s-default .cm-tag {color: #170;} +.cm-s-default .cm-attribute {color: #00c;} +.cm-s-default .cm-header {color: blue;} +.cm-s-default .cm-quote {color: #090;} +.cm-s-default .cm-hr {color: #999;} +.cm-s-default .cm-link {color: #00c;} + +.cm-negative {color: #d44;} +.cm-positive {color: #292;} +.cm-header, .cm-strong {font-weight: bold;} +.cm-em {font-style: italic;} +.cm-link {text-decoration: underline;} +.cm-strikethrough {text-decoration: line-through;} + +.cm-s-default .cm-error {color: #f00;} +.cm-invalidchar {color: #f00;} + +.CodeMirror-composing { border-bottom: 2px solid; } + +/* Default styles for common addons */ + +div.CodeMirror span.CodeMirror-matchingbracket {color: #0f0;} +div.CodeMirror span.CodeMirror-nonmatchingbracket {color: #f22;} +.CodeMirror-matchingtag { background: rgba(255, 150, 0, .3); } +.CodeMirror-activeline-background {background: #e8f2ff;} + +/* STOP */ + +/* The rest of this file contains styles related to the mechanics of + the editor. You probably shouldn't touch them. */ + +.CodeMirror { + position: relative; + overflow: hidden; + background: white; +} + +.CodeMirror-scroll { + overflow: scroll !important; /* Things will break if this is overridden */ + /* 30px is the magic margin used to hide the element's real scrollbars */ + /* See overflow: hidden in .CodeMirror */ + margin-bottom: -30px; margin-right: -30px; + padding-bottom: 30px; + height: 100%; + outline: none; /* Prevent dragging from highlighting the element */ + position: relative; +} +.CodeMirror-sizer { + position: relative; + border-right: 30px solid transparent; +} + +/* The fake, visible scrollbars. Used to force redraw during scrolling + before actuall scrolling happens, thus preventing shaking and + flickering artifacts. */ +.CodeMirror-vscrollbar, .CodeMirror-hscrollbar, .CodeMirror-scrollbar-filler, .CodeMirror-gutter-filler { + position: absolute; + z-index: 6; + display: none; +} +.CodeMirror-vscrollbar { + right: 0; top: 0; + overflow-x: hidden; + overflow-y: scroll; +} +.CodeMirror-hscrollbar { + bottom: 0; left: 0; + overflow-y: hidden; + overflow-x: scroll; +} +.CodeMirror-scrollbar-filler { + right: 0; bottom: 0; +} +.CodeMirror-gutter-filler { + left: 0; bottom: 0; +} + +.CodeMirror-gutters { + position: absolute; left: 0; top: 0; + z-index: 3; +} +.CodeMirror-gutter { + white-space: normal; + height: 100%; + display: inline-block; + margin-bottom: -30px; + /* Hack to make IE7 behave */ + *zoom:1; + *display:inline; +} +.CodeMirror-gutter-wrapper { + position: absolute; + z-index: 4; + height: 100%; +} +.CodeMirror-gutter-elt { + position: absolute; + cursor: default; + z-index: 4; +} +.CodeMirror-gutter-wrapper { + -webkit-user-select: none; + -moz-user-select: none; + user-select: none; +} + +.CodeMirror-lines { + cursor: text; + min-height: 1px; /* prevents collapsing before first draw */ +} +.CodeMirror pre { + /* Reset some styles that the rest of the page might have set */ + -moz-border-radius: 0; -webkit-border-radius: 0; border-radius: 0; + border-width: 0; + background: transparent; + font-family: inherit; + font-size: inherit; + margin: 0; + white-space: pre; + word-wrap: normal; + line-height: inherit; + color: inherit; + z-index: 2; + position: relative; + overflow: visible; + -webkit-tap-highlight-color: transparent; +} +.CodeMirror-wrap pre { + word-wrap: break-word; + white-space: pre-wrap; + word-break: normal; +} + +.CodeMirror-linebackground { + position: absolute; + left: 0; right: 0; top: 0; bottom: 0; + z-index: 0; +} + +.CodeMirror-linewidget { + position: relative; + z-index: 2; + overflow: auto; +} + +.CodeMirror-widget {} + +.CodeMirror-code { + outline: none; +} + +/* Force content-box sizing for the elements where we expect it */ +.CodeMirror-scroll, +.CodeMirror-sizer, +.CodeMirror-gutter, +.CodeMirror-gutters, +.CodeMirror-linenumber { + -moz-box-sizing: content-box; + box-sizing: content-box; +} + +.CodeMirror-measure { + position: absolute; + width: 100%; + height: 0; + overflow: hidden; + visibility: hidden; +} +.CodeMirror-measure pre { position: static; } + +.CodeMirror div.CodeMirror-cursor { + position: absolute; + border-right: none; + width: 0; +} + +div.CodeMirror-cursors { + visibility: hidden; + position: relative; + z-index: 3; +} +.CodeMirror-focused div.CodeMirror-cursors { + visibility: visible; +} + +.CodeMirror-selected { background: #d9d9d9; } +.CodeMirror-focused .CodeMirror-selected { background: #d7d4f0; } +.CodeMirror-crosshair { cursor: crosshair; } +.CodeMirror ::selection { background: #d7d4f0; } +.CodeMirror ::-moz-selection { background: #d7d4f0; } + +.cm-searching { + background: #ffa; + background: rgba(255, 255, 0, .4); +} + +/* IE7 hack to prevent it from returning funny offsetTops on the spans */ +.CodeMirror span { *vertical-align: text-bottom; } + +/* Used to force a border model for a node */ +.cm-force-border { padding-right: .1px; } + +@media print { + /* Hide the cursor when printing */ + .CodeMirror div.CodeMirror-cursors { + visibility: hidden; + } +} + +/* See issue #2901 */ +.cm-tab-wrap-hack:after { content: ''; } + +/* Help users use markselection to safely style text background */ +span.CodeMirror-selectedtext { background: none; } diff --git a/web/dep/codemirror/codemirror.js b/web/dep/codemirror/codemirror.js new file mode 100644 index 00000000..a1dfd0fc --- /dev/null +++ b/web/dep/codemirror/codemirror.js @@ -0,0 +1,8745 @@ +// CodeMirror, copyright (c) by Marijn Haverbeke and others +// Distributed under an MIT license: http://codemirror.net/LICENSE + +// This is CodeMirror (http://codemirror.net), a code editor +// implemented in JavaScript on top of the browser's DOM. +// +// You can find some technical background for some of the code below +// at http://marijnhaverbeke.nl/blog/#cm-internals . + +(function(mod) { + if (typeof exports == "object" && typeof module == "object") // CommonJS + module.exports = mod(); + else if (typeof define == "function" && define.amd) // AMD + return define([], mod); + else // Plain browser env + this.CodeMirror = mod(); +})(function() { + "use strict"; + + // BROWSER SNIFFING + + // Kludges for bugs and behavior differences that can't be feature + // detected are enabled based on userAgent etc sniffing. + + var gecko = /gecko\/\d/i.test(navigator.userAgent); + var ie_upto10 = /MSIE \d/.test(navigator.userAgent); + var ie_11up = /Trident\/(?:[7-9]|\d{2,})\..*rv:(\d+)/.exec(navigator.userAgent); + var ie = ie_upto10 || ie_11up; + var ie_version = ie && (ie_upto10 ? document.documentMode || 6 : ie_11up[1]); + var webkit = /WebKit\//.test(navigator.userAgent); + var qtwebkit = webkit && /Qt\/\d+\.\d+/.test(navigator.userAgent); + var chrome = /Chrome\//.test(navigator.userAgent); + var presto = /Opera\//.test(navigator.userAgent); + var safari = /Apple Computer/.test(navigator.vendor); + var mac_geMountainLion = /Mac OS X 1\d\D([8-9]|\d\d)\D/.test(navigator.userAgent); + var phantom = /PhantomJS/.test(navigator.userAgent); + + var ios = /AppleWebKit/.test(navigator.userAgent) && /Mobile\/\w+/.test(navigator.userAgent); + // This is woefully incomplete. Suggestions for alternative methods welcome. + var mobile = ios || /Android|webOS|BlackBerry|Opera Mini|Opera Mobi|IEMobile/i.test(navigator.userAgent); + var mac = ios || /Mac/.test(navigator.platform); + var windows = /win/i.test(navigator.platform); + + var presto_version = presto && navigator.userAgent.match(/Version\/(\d*\.\d*)/); + if (presto_version) presto_version = Number(presto_version[1]); + if (presto_version && presto_version >= 15) { presto = false; webkit = true; } + // Some browsers use the wrong event properties to signal cmd/ctrl on OS X + var flipCtrlCmd = mac && (qtwebkit || presto && (presto_version == null || presto_version < 12.11)); + var captureRightClick = gecko || (ie && ie_version >= 9); + + // Optimize some code when these features are not used. + var sawReadOnlySpans = false, sawCollapsedSpans = false; + + // EDITOR CONSTRUCTOR + + // A CodeMirror instance represents an editor. This is the object + // that user code is usually dealing with. + + function CodeMirror(place, options) { + if (!(this instanceof CodeMirror)) return new CodeMirror(place, options); + + this.options = options = options ? copyObj(options) : {}; + // Determine effective options based on given values and defaults. + copyObj(defaults, options, false); + setGuttersForLineNumbers(options); + + var doc = options.value; + if (typeof doc == "string") doc = new Doc(doc, options.mode); + this.doc = doc; + + var input = new CodeMirror.inputStyles[options.inputStyle](this); + var display = this.display = new Display(place, doc, input); + display.wrapper.CodeMirror = this; + updateGutters(this); + themeChanged(this); + if (options.lineWrapping) + this.display.wrapper.className += " CodeMirror-wrap"; + if (options.autofocus && !mobile) display.input.focus(); + initScrollbars(this); + + this.state = { + keyMaps: [], // stores maps added by addKeyMap + overlays: [], // highlighting overlays, as added by addOverlay + modeGen: 0, // bumped when mode/overlay changes, used to invalidate highlighting info + overwrite: false, + delayingBlurEvent: false, + focused: false, + suppressEdits: false, // used to disable editing during key handlers when in readOnly mode + pasteIncoming: false, cutIncoming: false, // help recognize paste/cut edits in input.poll + draggingText: false, + highlight: new Delayed(), // stores highlight worker timeout + keySeq: null, // Unfinished key sequence + specialChars: null + }; + + var cm = this; + + // Override magic textarea content restore that IE sometimes does + // on our hidden textarea on reload + if (ie && ie_version < 11) setTimeout(function() { cm.display.input.reset(true); }, 20); + + registerEventHandlers(this); + ensureGlobalHandlers(); + + startOperation(this); + this.curOp.forceUpdate = true; + attachDoc(this, doc); + + if ((options.autofocus && !mobile) || cm.hasFocus()) + setTimeout(bind(onFocus, this), 20); + else + onBlur(this); + + for (var opt in optionHandlers) if (optionHandlers.hasOwnProperty(opt)) + optionHandlers[opt](this, options[opt], Init); + maybeUpdateLineNumberWidth(this); + if (options.finishInit) options.finishInit(this); + for (var i = 0; i < initHooks.length; ++i) initHooks[i](this); + endOperation(this); + // Suppress optimizelegibility in Webkit, since it breaks text + // measuring on line wrapping boundaries. + if (webkit && options.lineWrapping && + getComputedStyle(display.lineDiv).textRendering == "optimizelegibility") + display.lineDiv.style.textRendering = "auto"; + } + + // DISPLAY CONSTRUCTOR + + // The display handles the DOM integration, both for input reading + // and content drawing. It holds references to DOM nodes and + // display-related state. + + function Display(place, doc, input) { + var d = this; + this.input = input; + + // Covers bottom-right square when both scrollbars are present. + d.scrollbarFiller = elt("div", null, "CodeMirror-scrollbar-filler"); + d.scrollbarFiller.setAttribute("cm-not-content", "true"); + // Covers bottom of gutter when coverGutterNextToScrollbar is on + // and h scrollbar is present. + d.gutterFiller = elt("div", null, "CodeMirror-gutter-filler"); + d.gutterFiller.setAttribute("cm-not-content", "true"); + // Will contain the actual code, positioned to cover the viewport. + d.lineDiv = elt("div", null, "CodeMirror-code"); + // Elements are added to these to represent selection and cursors. + d.selectionDiv = elt("div", null, null, "position: relative; z-index: 1"); + d.cursorDiv = elt("div", null, "CodeMirror-cursors"); + // A visibility: hidden element used to find the size of things. + d.measure = elt("div", null, "CodeMirror-measure"); + // When lines outside of the viewport are measured, they are drawn in this. + d.lineMeasure = elt("div", null, "CodeMirror-measure"); + // Wraps everything that needs to exist inside the vertically-padded coordinate system + d.lineSpace = elt("div", [d.measure, d.lineMeasure, d.selectionDiv, d.cursorDiv, d.lineDiv], + null, "position: relative; outline: none"); + // Moved around its parent to cover visible view. + d.mover = elt("div", [elt("div", [d.lineSpace], "CodeMirror-lines")], null, "position: relative"); + // Set to the height of the document, allowing scrolling. + d.sizer = elt("div", [d.mover], "CodeMirror-sizer"); + d.sizerWidth = null; + // Behavior of elts with overflow: auto and padding is + // inconsistent across browsers. This is used to ensure the + // scrollable area is big enough. + d.heightForcer = elt("div", null, null, "position: absolute; height: " + scrollerGap + "px; width: 1px;"); + // Will contain the gutters, if any. + d.gutters = elt("div", null, "CodeMirror-gutters"); + d.lineGutter = null; + // Actual scrollable element. + d.scroller = elt("div", [d.sizer, d.heightForcer, d.gutters], "CodeMirror-scroll"); + d.scroller.setAttribute("tabIndex", "-1"); + // The element in which the editor lives. + d.wrapper = elt("div", [d.scrollbarFiller, d.gutterFiller, d.scroller], "CodeMirror"); + + // Work around IE7 z-index bug (not perfect, hence IE7 not really being supported) + if (ie && ie_version < 8) { d.gutters.style.zIndex = -1; d.scroller.style.paddingRight = 0; } + if (!webkit && !(gecko && mobile)) d.scroller.draggable = true; + + if (place) { + if (place.appendChild) place.appendChild(d.wrapper); + else place(d.wrapper); + } + + // Current rendered range (may be bigger than the view window). + d.viewFrom = d.viewTo = doc.first; + d.reportedViewFrom = d.reportedViewTo = doc.first; + // Information about the rendered lines. + d.view = []; + d.renderedView = null; + // Holds info about a single rendered line when it was rendered + // for measurement, while not in view. + d.externalMeasured = null; + // Empty space (in pixels) above the view + d.viewOffset = 0; + d.lastWrapHeight = d.lastWrapWidth = 0; + d.updateLineNumbers = null; + + d.nativeBarWidth = d.barHeight = d.barWidth = 0; + d.scrollbarsClipped = false; + + // Used to only resize the line number gutter when necessary (when + // the amount of lines crosses a boundary that makes its width change) + d.lineNumWidth = d.lineNumInnerWidth = d.lineNumChars = null; + // Set to true when a non-horizontal-scrolling line widget is + // added. As an optimization, line widget aligning is skipped when + // this is false. + d.alignWidgets = false; + + d.cachedCharWidth = d.cachedTextHeight = d.cachedPaddingH = null; + + // Tracks the maximum line length so that the horizontal scrollbar + // can be kept static when scrolling. + d.maxLine = null; + d.maxLineLength = 0; + d.maxLineChanged = false; + + // Used for measuring wheel scrolling granularity + d.wheelDX = d.wheelDY = d.wheelStartX = d.wheelStartY = null; + + // True when shift is held down. + d.shift = false; + + // Used to track whether anything happened since the context menu + // was opened. + d.selForContextMenu = null; + + d.activeTouch = null; + + input.init(d); + } + + // STATE UPDATES + + // Used to get the editor into a consistent state again when options change. + + function loadMode(cm) { + cm.doc.mode = CodeMirror.getMode(cm.options, cm.doc.modeOption); + resetModeState(cm); + } + + function resetModeState(cm) { + cm.doc.iter(function(line) { + if (line.stateAfter) line.stateAfter = null; + if (line.styles) line.styles = null; + }); + cm.doc.frontier = cm.doc.first; + startWorker(cm, 100); + cm.state.modeGen++; + if (cm.curOp) regChange(cm); + } + + function wrappingChanged(cm) { + if (cm.options.lineWrapping) { + addClass(cm.display.wrapper, "CodeMirror-wrap"); + cm.display.sizer.style.minWidth = ""; + cm.display.sizerWidth = null; + } else { + rmClass(cm.display.wrapper, "CodeMirror-wrap"); + findMaxLine(cm); + } + estimateLineHeights(cm); + regChange(cm); + clearCaches(cm); + setTimeout(function(){updateScrollbars(cm);}, 100); + } + + // Returns a function that estimates the height of a line, to use as + // first approximation until the line becomes visible (and is thus + // properly measurable). + function estimateHeight(cm) { + var th = textHeight(cm.display), wrapping = cm.options.lineWrapping; + var perLine = wrapping && Math.max(5, cm.display.scroller.clientWidth / charWidth(cm.display) - 3); + return function(line) { + if (lineIsHidden(cm.doc, line)) return 0; + + var widgetsHeight = 0; + if (line.widgets) for (var i = 0; i < line.widgets.length; i++) { + if (line.widgets[i].height) widgetsHeight += line.widgets[i].height; + } + + if (wrapping) + return widgetsHeight + (Math.ceil(line.text.length / perLine) || 1) * th; + else + return widgetsHeight + th; + }; + } + + function estimateLineHeights(cm) { + var doc = cm.doc, est = estimateHeight(cm); + doc.iter(function(line) { + var estHeight = est(line); + if (estHeight != line.height) updateLineHeight(line, estHeight); + }); + } + + function themeChanged(cm) { + cm.display.wrapper.className = cm.display.wrapper.className.replace(/\s*cm-s-\S+/g, "") + + cm.options.theme.replace(/(^|\s)\s*/g, " cm-s-"); + clearCaches(cm); + } + + function guttersChanged(cm) { + updateGutters(cm); + regChange(cm); + setTimeout(function(){alignHorizontally(cm);}, 20); + } + + // Rebuild the gutter elements, ensure the margin to the left of the + // code matches their width. + function updateGutters(cm) { + var gutters = cm.display.gutters, specs = cm.options.gutters; + removeChildren(gutters); + for (var i = 0; i < specs.length; ++i) { + var gutterClass = specs[i]; + var gElt = gutters.appendChild(elt("div", null, "CodeMirror-gutter " + gutterClass)); + if (gutterClass == "CodeMirror-linenumbers") { + cm.display.lineGutter = gElt; + gElt.style.width = (cm.display.lineNumWidth || 1) + "px"; + } + } + gutters.style.display = i ? "" : "none"; + updateGutterSpace(cm); + } + + function updateGutterSpace(cm) { + var width = cm.display.gutters.offsetWidth; + cm.display.sizer.style.marginLeft = width + "px"; + } + + // Compute the character length of a line, taking into account + // collapsed ranges (see markText) that might hide parts, and join + // other lines onto it. + function lineLength(line) { + if (line.height == 0) return 0; + var len = line.text.length, merged, cur = line; + while (merged = collapsedSpanAtStart(cur)) { + var found = merged.find(0, true); + cur = found.from.line; + len += found.from.ch - found.to.ch; + } + cur = line; + while (merged = collapsedSpanAtEnd(cur)) { + var found = merged.find(0, true); + len -= cur.text.length - found.from.ch; + cur = found.to.line; + len += cur.text.length - found.to.ch; + } + return len; + } + + // Find the longest line in the document. + function findMaxLine(cm) { + var d = cm.display, doc = cm.doc; + d.maxLine = getLine(doc, doc.first); + d.maxLineLength = lineLength(d.maxLine); + d.maxLineChanged = true; + doc.iter(function(line) { + var len = lineLength(line); + if (len > d.maxLineLength) { + d.maxLineLength = len; + d.maxLine = line; + } + }); + } + + // Make sure the gutters options contains the element + // "CodeMirror-linenumbers" when the lineNumbers option is true. + function setGuttersForLineNumbers(options) { + var found = indexOf(options.gutters, "CodeMirror-linenumbers"); + if (found == -1 && options.lineNumbers) { + options.gutters = options.gutters.concat(["CodeMirror-linenumbers"]); + } else if (found > -1 && !options.lineNumbers) { + options.gutters = options.gutters.slice(0); + options.gutters.splice(found, 1); + } + } + + // SCROLLBARS + + // Prepare DOM reads needed to update the scrollbars. Done in one + // shot to minimize update/measure roundtrips. + function measureForScrollbars(cm) { + var d = cm.display, gutterW = d.gutters.offsetWidth; + var docH = Math.round(cm.doc.height + paddingVert(cm.display)); + return { + clientHeight: d.scroller.clientHeight, + viewHeight: d.wrapper.clientHeight, + scrollWidth: d.scroller.scrollWidth, clientWidth: d.scroller.clientWidth, + viewWidth: d.wrapper.clientWidth, + barLeft: cm.options.fixedGutter ? gutterW : 0, + docHeight: docH, + scrollHeight: docH + scrollGap(cm) + d.barHeight, + nativeBarWidth: d.nativeBarWidth, + gutterWidth: gutterW + }; + } + + function NativeScrollbars(place, scroll, cm) { + this.cm = cm; + var vert = this.vert = elt("div", [elt("div", null, null, "min-width: 1px")], "CodeMirror-vscrollbar"); + var horiz = this.horiz = elt("div", [elt("div", null, null, "height: 100%; min-height: 1px")], "CodeMirror-hscrollbar"); + place(vert); place(horiz); + + on(vert, "scroll", function() { + if (vert.clientHeight) scroll(vert.scrollTop, "vertical"); + }); + on(horiz, "scroll", function() { + if (horiz.clientWidth) scroll(horiz.scrollLeft, "horizontal"); + }); + + this.checkedOverlay = false; + // Need to set a minimum width to see the scrollbar on IE7 (but must not set it on IE8). + if (ie && ie_version < 8) this.horiz.style.minHeight = this.vert.style.minWidth = "18px"; + } + + NativeScrollbars.prototype = copyObj({ + update: function(measure) { + var needsH = measure.scrollWidth > measure.clientWidth + 1; + var needsV = measure.scrollHeight > measure.clientHeight + 1; + var sWidth = measure.nativeBarWidth; + + if (needsV) { + this.vert.style.display = "block"; + this.vert.style.bottom = needsH ? sWidth + "px" : "0"; + var totalHeight = measure.viewHeight - (needsH ? sWidth : 0); + // A bug in IE8 can cause this value to be negative, so guard it. + this.vert.firstChild.style.height = + Math.max(0, measure.scrollHeight - measure.clientHeight + totalHeight) + "px"; + } else { + this.vert.style.display = ""; + this.vert.firstChild.style.height = "0"; + } + + if (needsH) { + this.horiz.style.display = "block"; + this.horiz.style.right = needsV ? sWidth + "px" : "0"; + this.horiz.style.left = measure.barLeft + "px"; + var totalWidth = measure.viewWidth - measure.barLeft - (needsV ? sWidth : 0); + this.horiz.firstChild.style.width = + (measure.scrollWidth - measure.clientWidth + totalWidth) + "px"; + } else { + this.horiz.style.display = ""; + this.horiz.firstChild.style.width = "0"; + } + + if (!this.checkedOverlay && measure.clientHeight > 0) { + if (sWidth == 0) this.overlayHack(); + this.checkedOverlay = true; + } + + return {right: needsV ? sWidth : 0, bottom: needsH ? sWidth : 0}; + }, + setScrollLeft: function(pos) { + if (this.horiz.scrollLeft != pos) this.horiz.scrollLeft = pos; + }, + setScrollTop: function(pos) { + if (this.vert.scrollTop != pos) this.vert.scrollTop = pos; + }, + overlayHack: function() { + var w = mac && !mac_geMountainLion ? "12px" : "18px"; + this.horiz.style.minHeight = this.vert.style.minWidth = w; + var self = this; + var barMouseDown = function(e) { + if (e_target(e) != self.vert && e_target(e) != self.horiz) + operation(self.cm, onMouseDown)(e); + }; + on(this.vert, "mousedown", barMouseDown); + on(this.horiz, "mousedown", barMouseDown); + }, + clear: function() { + var parent = this.horiz.parentNode; + parent.removeChild(this.horiz); + parent.removeChild(this.vert); + } + }, NativeScrollbars.prototype); + + function NullScrollbars() {} + + NullScrollbars.prototype = copyObj({ + update: function() { return {bottom: 0, right: 0}; }, + setScrollLeft: function() {}, + setScrollTop: function() {}, + clear: function() {} + }, NullScrollbars.prototype); + + CodeMirror.scrollbarModel = {"native": NativeScrollbars, "null": NullScrollbars}; + + function initScrollbars(cm) { + if (cm.display.scrollbars) { + cm.display.scrollbars.clear(); + if (cm.display.scrollbars.addClass) + rmClass(cm.display.wrapper, cm.display.scrollbars.addClass); + } + + cm.display.scrollbars = new CodeMirror.scrollbarModel[cm.options.scrollbarStyle](function(node) { + cm.display.wrapper.insertBefore(node, cm.display.scrollbarFiller); + // Prevent clicks in the scrollbars from killing focus + on(node, "mousedown", function() { + if (cm.state.focused) setTimeout(function() { cm.display.input.focus(); }, 0); + }); + node.setAttribute("cm-not-content", "true"); + }, function(pos, axis) { + if (axis == "horizontal") setScrollLeft(cm, pos); + else setScrollTop(cm, pos); + }, cm); + if (cm.display.scrollbars.addClass) + addClass(cm.display.wrapper, cm.display.scrollbars.addClass); + } + + function updateScrollbars(cm, measure) { + if (!measure) measure = measureForScrollbars(cm); + var startWidth = cm.display.barWidth, startHeight = cm.display.barHeight; + updateScrollbarsInner(cm, measure); + for (var i = 0; i < 4 && startWidth != cm.display.barWidth || startHeight != cm.display.barHeight; i++) { + if (startWidth != cm.display.barWidth && cm.options.lineWrapping) + updateHeightsInViewport(cm); + updateScrollbarsInner(cm, measureForScrollbars(cm)); + startWidth = cm.display.barWidth; startHeight = cm.display.barHeight; + } + } + + // Re-synchronize the fake scrollbars with the actual size of the + // content. + function updateScrollbarsInner(cm, measure) { + var d = cm.display; + var sizes = d.scrollbars.update(measure); + + d.sizer.style.paddingRight = (d.barWidth = sizes.right) + "px"; + d.sizer.style.paddingBottom = (d.barHeight = sizes.bottom) + "px"; + + if (sizes.right && sizes.bottom) { + d.scrollbarFiller.style.display = "block"; + d.scrollbarFiller.style.height = sizes.bottom + "px"; + d.scrollbarFiller.style.width = sizes.right + "px"; + } else d.scrollbarFiller.style.display = ""; + if (sizes.bottom && cm.options.coverGutterNextToScrollbar && cm.options.fixedGutter) { + d.gutterFiller.style.display = "block"; + d.gutterFiller.style.height = sizes.bottom + "px"; + d.gutterFiller.style.width = measure.gutterWidth + "px"; + } else d.gutterFiller.style.display = ""; + } + + // Compute the lines that are visible in a given viewport (defaults + // the the current scroll position). viewport may contain top, + // height, and ensure (see op.scrollToPos) properties. + function visibleLines(display, doc, viewport) { + var top = viewport && viewport.top != null ? Math.max(0, viewport.top) : display.scroller.scrollTop; + top = Math.floor(top - paddingTop(display)); + var bottom = viewport && viewport.bottom != null ? viewport.bottom : top + display.wrapper.clientHeight; + + var from = lineAtHeight(doc, top), to = lineAtHeight(doc, bottom); + // Ensure is a {from: {line, ch}, to: {line, ch}} object, and + // forces those lines into the viewport (if possible). + if (viewport && viewport.ensure) { + var ensureFrom = viewport.ensure.from.line, ensureTo = viewport.ensure.to.line; + if (ensureFrom < from) { + from = ensureFrom; + to = lineAtHeight(doc, heightAtLine(getLine(doc, ensureFrom)) + display.wrapper.clientHeight); + } else if (Math.min(ensureTo, doc.lastLine()) >= to) { + from = lineAtHeight(doc, heightAtLine(getLine(doc, ensureTo)) - display.wrapper.clientHeight); + to = ensureTo; + } + } + return {from: from, to: Math.max(to, from + 1)}; + } + + // LINE NUMBERS + + // Re-align line numbers and gutter marks to compensate for + // horizontal scrolling. + function alignHorizontally(cm) { + var display = cm.display, view = display.view; + if (!display.alignWidgets && (!display.gutters.firstChild || !cm.options.fixedGutter)) return; + var comp = compensateForHScroll(display) - display.scroller.scrollLeft + cm.doc.scrollLeft; + var gutterW = display.gutters.offsetWidth, left = comp + "px"; + for (var i = 0; i < view.length; i++) if (!view[i].hidden) { + if (cm.options.fixedGutter && view[i].gutter) + view[i].gutter.style.left = left; + var align = view[i].alignable; + if (align) for (var j = 0; j < align.length; j++) + align[j].style.left = left; + } + if (cm.options.fixedGutter) + display.gutters.style.left = (comp + gutterW) + "px"; + } + + // Used to ensure that the line number gutter is still the right + // size for the current document size. Returns true when an update + // is needed. + function maybeUpdateLineNumberWidth(cm) { + if (!cm.options.lineNumbers) return false; + var doc = cm.doc, last = lineNumberFor(cm.options, doc.first + doc.size - 1), display = cm.display; + if (last.length != display.lineNumChars) { + var test = display.measure.appendChild(elt("div", [elt("div", last)], + "CodeMirror-linenumber CodeMirror-gutter-elt")); + var innerW = test.firstChild.offsetWidth, padding = test.offsetWidth - innerW; + display.lineGutter.style.width = ""; + display.lineNumInnerWidth = Math.max(innerW, display.lineGutter.offsetWidth - padding) + 1; + display.lineNumWidth = display.lineNumInnerWidth + padding; + display.lineNumChars = display.lineNumInnerWidth ? last.length : -1; + display.lineGutter.style.width = display.lineNumWidth + "px"; + updateGutterSpace(cm); + return true; + } + return false; + } + + function lineNumberFor(options, i) { + return String(options.lineNumberFormatter(i + options.firstLineNumber)); + } + + // Computes display.scroller.scrollLeft + display.gutters.offsetWidth, + // but using getBoundingClientRect to get a sub-pixel-accurate + // result. + function compensateForHScroll(display) { + return display.scroller.getBoundingClientRect().left - display.sizer.getBoundingClientRect().left; + } + + // DISPLAY DRAWING + + function DisplayUpdate(cm, viewport, force) { + var display = cm.display; + + this.viewport = viewport; + // Store some values that we'll need later (but don't want to force a relayout for) + this.visible = visibleLines(display, cm.doc, viewport); + this.editorIsHidden = !display.wrapper.offsetWidth; + this.wrapperHeight = display.wrapper.clientHeight; + this.wrapperWidth = display.wrapper.clientWidth; + this.oldDisplayWidth = displayWidth(cm); + this.force = force; + this.dims = getDimensions(cm); + this.events = []; + } + + DisplayUpdate.prototype.signal = function(emitter, type) { + if (hasHandler(emitter, type)) + this.events.push(arguments); + }; + DisplayUpdate.prototype.finish = function() { + for (var i = 0; i < this.events.length; i++) + signal.apply(null, this.events[i]); + }; + + function maybeClipScrollbars(cm) { + var display = cm.display; + if (!display.scrollbarsClipped && display.scroller.offsetWidth) { + display.nativeBarWidth = display.scroller.offsetWidth - display.scroller.clientWidth; + display.heightForcer.style.height = scrollGap(cm) + "px"; + display.sizer.style.marginBottom = -display.nativeBarWidth + "px"; + display.sizer.style.borderRightWidth = scrollGap(cm) + "px"; + display.scrollbarsClipped = true; + } + } + + // Does the actual updating of the line display. Bails out + // (returning false) when there is nothing to be done and forced is + // false. + function updateDisplayIfNeeded(cm, update) { + var display = cm.display, doc = cm.doc; + + if (update.editorIsHidden) { + resetView(cm); + return false; + } + + // Bail out if the visible area is already rendered and nothing changed. + if (!update.force && + update.visible.from >= display.viewFrom && update.visible.to <= display.viewTo && + (display.updateLineNumbers == null || display.updateLineNumbers >= display.viewTo) && + display.renderedView == display.view && countDirtyView(cm) == 0) + return false; + + if (maybeUpdateLineNumberWidth(cm)) { + resetView(cm); + update.dims = getDimensions(cm); + } + + // Compute a suitable new viewport (from & to) + var end = doc.first + doc.size; + var from = Math.max(update.visible.from - cm.options.viewportMargin, doc.first); + var to = Math.min(end, update.visible.to + cm.options.viewportMargin); + if (display.viewFrom < from && from - display.viewFrom < 20) from = Math.max(doc.first, display.viewFrom); + if (display.viewTo > to && display.viewTo - to < 20) to = Math.min(end, display.viewTo); + if (sawCollapsedSpans) { + from = visualLineNo(cm.doc, from); + to = visualLineEndNo(cm.doc, to); + } + + var different = from != display.viewFrom || to != display.viewTo || + display.lastWrapHeight != update.wrapperHeight || display.lastWrapWidth != update.wrapperWidth; + adjustView(cm, from, to); + + display.viewOffset = heightAtLine(getLine(cm.doc, display.viewFrom)); + // Position the mover div to align with the current scroll position + cm.display.mover.style.top = display.viewOffset + "px"; + + var toUpdate = countDirtyView(cm); + if (!different && toUpdate == 0 && !update.force && display.renderedView == display.view && + (display.updateLineNumbers == null || display.updateLineNumbers >= display.viewTo)) + return false; + + // For big changes, we hide the enclosing element during the + // update, since that speeds up the operations on most browsers. + var focused = activeElt(); + if (toUpdate > 4) display.lineDiv.style.display = "none"; + patchDisplay(cm, display.updateLineNumbers, update.dims); + if (toUpdate > 4) display.lineDiv.style.display = ""; + display.renderedView = display.view; + // There might have been a widget with a focused element that got + // hidden or updated, if so re-focus it. + if (focused && activeElt() != focused && focused.offsetHeight) focused.focus(); + + // Prevent selection and cursors from interfering with the scroll + // width and height. + removeChildren(display.cursorDiv); + removeChildren(display.selectionDiv); + display.gutters.style.height = 0; + + if (different) { + display.lastWrapHeight = update.wrapperHeight; + display.lastWrapWidth = update.wrapperWidth; + startWorker(cm, 400); + } + + display.updateLineNumbers = null; + + return true; + } + + function postUpdateDisplay(cm, update) { + var viewport = update.viewport; + for (var first = true;; first = false) { + if (!first || !cm.options.lineWrapping || update.oldDisplayWidth == displayWidth(cm)) { + // Clip forced viewport to actual scrollable area. + if (viewport && viewport.top != null) + viewport = {top: Math.min(cm.doc.height + paddingVert(cm.display) - displayHeight(cm), viewport.top)}; + // Updated line heights might result in the drawn area not + // actually covering the viewport. Keep looping until it does. + update.visible = visibleLines(cm.display, cm.doc, viewport); + if (update.visible.from >= cm.display.viewFrom && update.visible.to <= cm.display.viewTo) + break; + } + if (!updateDisplayIfNeeded(cm, update)) break; + updateHeightsInViewport(cm); + var barMeasure = measureForScrollbars(cm); + updateSelection(cm); + setDocumentHeight(cm, barMeasure); + updateScrollbars(cm, barMeasure); + } + + update.signal(cm, "update", cm); + if (cm.display.viewFrom != cm.display.reportedViewFrom || cm.display.viewTo != cm.display.reportedViewTo) { + update.signal(cm, "viewportChange", cm, cm.display.viewFrom, cm.display.viewTo); + cm.display.reportedViewFrom = cm.display.viewFrom; cm.display.reportedViewTo = cm.display.viewTo; + } + } + + function updateDisplaySimple(cm, viewport) { + var update = new DisplayUpdate(cm, viewport); + if (updateDisplayIfNeeded(cm, update)) { + updateHeightsInViewport(cm); + postUpdateDisplay(cm, update); + var barMeasure = measureForScrollbars(cm); + updateSelection(cm); + setDocumentHeight(cm, barMeasure); + updateScrollbars(cm, barMeasure); + update.finish(); + } + } + + function setDocumentHeight(cm, measure) { + cm.display.sizer.style.minHeight = measure.docHeight + "px"; + var total = measure.docHeight + cm.display.barHeight; + cm.display.heightForcer.style.top = total + "px"; + cm.display.gutters.style.height = Math.max(total + scrollGap(cm), measure.clientHeight) + "px"; + } + + // Read the actual heights of the rendered lines, and update their + // stored heights to match. + function updateHeightsInViewport(cm) { + var display = cm.display; + var prevBottom = display.lineDiv.offsetTop; + for (var i = 0; i < display.view.length; i++) { + var cur = display.view[i], height; + if (cur.hidden) continue; + if (ie && ie_version < 8) { + var bot = cur.node.offsetTop + cur.node.offsetHeight; + height = bot - prevBottom; + prevBottom = bot; + } else { + var box = cur.node.getBoundingClientRect(); + height = box.bottom - box.top; + } + var diff = cur.line.height - height; + if (height < 2) height = textHeight(display); + if (diff > .001 || diff < -.001) { + updateLineHeight(cur.line, height); + updateWidgetHeight(cur.line); + if (cur.rest) for (var j = 0; j < cur.rest.length; j++) + updateWidgetHeight(cur.rest[j]); + } + } + } + + // Read and store the height of line widgets associated with the + // given line. + function updateWidgetHeight(line) { + if (line.widgets) for (var i = 0; i < line.widgets.length; ++i) + line.widgets[i].height = line.widgets[i].node.offsetHeight; + } + + // Do a bulk-read of the DOM positions and sizes needed to draw the + // view, so that we don't interleave reading and writing to the DOM. + function getDimensions(cm) { + var d = cm.display, left = {}, width = {}; + var gutterLeft = d.gutters.clientLeft; + for (var n = d.gutters.firstChild, i = 0; n; n = n.nextSibling, ++i) { + left[cm.options.gutters[i]] = n.offsetLeft + n.clientLeft + gutterLeft; + width[cm.options.gutters[i]] = n.clientWidth; + } + return {fixedPos: compensateForHScroll(d), + gutterTotalWidth: d.gutters.offsetWidth, + gutterLeft: left, + gutterWidth: width, + wrapperWidth: d.wrapper.clientWidth}; + } + + // Sync the actual display DOM structure with display.view, removing + // nodes for lines that are no longer in view, and creating the ones + // that are not there yet, and updating the ones that are out of + // date. + function patchDisplay(cm, updateNumbersFrom, dims) { + var display = cm.display, lineNumbers = cm.options.lineNumbers; + var container = display.lineDiv, cur = container.firstChild; + + function rm(node) { + var next = node.nextSibling; + // Works around a throw-scroll bug in OS X Webkit + if (webkit && mac && cm.display.currentWheelTarget == node) + node.style.display = "none"; + else + node.parentNode.removeChild(node); + return next; + } + + var view = display.view, lineN = display.viewFrom; + // Loop over the elements in the view, syncing cur (the DOM nodes + // in display.lineDiv) with the view as we go. + for (var i = 0; i < view.length; i++) { + var lineView = view[i]; + if (lineView.hidden) { + } else if (!lineView.node || lineView.node.parentNode != container) { // Not drawn yet + var node = buildLineElement(cm, lineView, lineN, dims); + container.insertBefore(node, cur); + } else { // Already drawn + while (cur != lineView.node) cur = rm(cur); + var updateNumber = lineNumbers && updateNumbersFrom != null && + updateNumbersFrom <= lineN && lineView.lineNumber; + if (lineView.changes) { + if (indexOf(lineView.changes, "gutter") > -1) updateNumber = false; + updateLineForChanges(cm, lineView, lineN, dims); + } + if (updateNumber) { + removeChildren(lineView.lineNumber); + lineView.lineNumber.appendChild(document.createTextNode(lineNumberFor(cm.options, lineN))); + } + cur = lineView.node.nextSibling; + } + lineN += lineView.size; + } + while (cur) cur = rm(cur); + } + + // When an aspect of a line changes, a string is added to + // lineView.changes. This updates the relevant part of the line's + // DOM structure. + function updateLineForChanges(cm, lineView, lineN, dims) { + for (var j = 0; j < lineView.changes.length; j++) { + var type = lineView.changes[j]; + if (type == "text") updateLineText(cm, lineView); + else if (type == "gutter") updateLineGutter(cm, lineView, lineN, dims); + else if (type == "class") updateLineClasses(lineView); + else if (type == "widget") updateLineWidgets(cm, lineView, dims); + } + lineView.changes = null; + } + + // Lines with gutter elements, widgets or a background class need to + // be wrapped, and have the extra elements added to the wrapper div + function ensureLineWrapped(lineView) { + if (lineView.node == lineView.text) { + lineView.node = elt("div", null, null, "position: relative"); + if (lineView.text.parentNode) + lineView.text.parentNode.replaceChild(lineView.node, lineView.text); + lineView.node.appendChild(lineView.text); + if (ie && ie_version < 8) lineView.node.style.zIndex = 2; + } + return lineView.node; + } + + function updateLineBackground(lineView) { + var cls = lineView.bgClass ? lineView.bgClass + " " + (lineView.line.bgClass || "") : lineView.line.bgClass; + if (cls) cls += " CodeMirror-linebackground"; + if (lineView.background) { + if (cls) lineView.background.className = cls; + else { lineView.background.parentNode.removeChild(lineView.background); lineView.background = null; } + } else if (cls) { + var wrap = ensureLineWrapped(lineView); + lineView.background = wrap.insertBefore(elt("div", null, cls), wrap.firstChild); + } + } + + // Wrapper around buildLineContent which will reuse the structure + // in display.externalMeasured when possible. + function getLineContent(cm, lineView) { + var ext = cm.display.externalMeasured; + if (ext && ext.line == lineView.line) { + cm.display.externalMeasured = null; + lineView.measure = ext.measure; + return ext.built; + } + return buildLineContent(cm, lineView); + } + + // Redraw the line's text. Interacts with the background and text + // classes because the mode may output tokens that influence these + // classes. + function updateLineText(cm, lineView) { + var cls = lineView.text.className; + var built = getLineContent(cm, lineView); + if (lineView.text == lineView.node) lineView.node = built.pre; + lineView.text.parentNode.replaceChild(built.pre, lineView.text); + lineView.text = built.pre; + if (built.bgClass != lineView.bgClass || built.textClass != lineView.textClass) { + lineView.bgClass = built.bgClass; + lineView.textClass = built.textClass; + updateLineClasses(lineView); + } else if (cls) { + lineView.text.className = cls; + } + } + + function updateLineClasses(lineView) { + updateLineBackground(lineView); + if (lineView.line.wrapClass) + ensureLineWrapped(lineView).className = lineView.line.wrapClass; + else if (lineView.node != lineView.text) + lineView.node.className = ""; + var textClass = lineView.textClass ? lineView.textClass + " " + (lineView.line.textClass || "") : lineView.line.textClass; + lineView.text.className = textClass || ""; + } + + function updateLineGutter(cm, lineView, lineN, dims) { + if (lineView.gutter) { + lineView.node.removeChild(lineView.gutter); + lineView.gutter = null; + } + var markers = lineView.line.gutterMarkers; + if (cm.options.lineNumbers || markers) { + var wrap = ensureLineWrapped(lineView); + var gutterWrap = lineView.gutter = elt("div", null, "CodeMirror-gutter-wrapper", "left: " + + (cm.options.fixedGutter ? dims.fixedPos : -dims.gutterTotalWidth) + + "px; width: " + dims.gutterTotalWidth + "px"); + cm.display.input.setUneditable(gutterWrap); + wrap.insertBefore(gutterWrap, lineView.text); + if (lineView.line.gutterClass) + gutterWrap.className += " " + lineView.line.gutterClass; + if (cm.options.lineNumbers && (!markers || !markers["CodeMirror-linenumbers"])) + lineView.lineNumber = gutterWrap.appendChild( + elt("div", lineNumberFor(cm.options, lineN), + "CodeMirror-linenumber CodeMirror-gutter-elt", + "left: " + dims.gutterLeft["CodeMirror-linenumbers"] + "px; width: " + + cm.display.lineNumInnerWidth + "px")); + if (markers) for (var k = 0; k < cm.options.gutters.length; ++k) { + var id = cm.options.gutters[k], found = markers.hasOwnProperty(id) && markers[id]; + if (found) + gutterWrap.appendChild(elt("div", [found], "CodeMirror-gutter-elt", "left: " + + dims.gutterLeft[id] + "px; width: " + dims.gutterWidth[id] + "px")); + } + } + } + + function updateLineWidgets(cm, lineView, dims) { + if (lineView.alignable) lineView.alignable = null; + for (var node = lineView.node.firstChild, next; node; node = next) { + var next = node.nextSibling; + if (node.className == "CodeMirror-linewidget") + lineView.node.removeChild(node); + } + insertLineWidgets(cm, lineView, dims); + } + + // Build a line's DOM representation from scratch + function buildLineElement(cm, lineView, lineN, dims) { + var built = getLineContent(cm, lineView); + lineView.text = lineView.node = built.pre; + if (built.bgClass) lineView.bgClass = built.bgClass; + if (built.textClass) lineView.textClass = built.textClass; + + updateLineClasses(lineView); + updateLineGutter(cm, lineView, lineN, dims); + insertLineWidgets(cm, lineView, dims); + return lineView.node; + } + + // A lineView may contain multiple logical lines (when merged by + // collapsed spans). The widgets for all of them need to be drawn. + function insertLineWidgets(cm, lineView, dims) { + insertLineWidgetsFor(cm, lineView.line, lineView, dims, true); + if (lineView.rest) for (var i = 0; i < lineView.rest.length; i++) + insertLineWidgetsFor(cm, lineView.rest[i], lineView, dims, false); + } + + function insertLineWidgetsFor(cm, line, lineView, dims, allowAbove) { + if (!line.widgets) return; + var wrap = ensureLineWrapped(lineView); + for (var i = 0, ws = line.widgets; i < ws.length; ++i) { + var widget = ws[i], node = elt("div", [widget.node], "CodeMirror-linewidget"); + if (!widget.handleMouseEvents) node.setAttribute("cm-ignore-events", "true"); + positionLineWidget(widget, node, lineView, dims); + cm.display.input.setUneditable(node); + if (allowAbove && widget.above) + wrap.insertBefore(node, lineView.gutter || lineView.text); + else + wrap.appendChild(node); + signalLater(widget, "redraw"); + } + } + + function positionLineWidget(widget, node, lineView, dims) { + if (widget.noHScroll) { + (lineView.alignable || (lineView.alignable = [])).push(node); + var width = dims.wrapperWidth; + node.style.left = dims.fixedPos + "px"; + if (!widget.coverGutter) { + width -= dims.gutterTotalWidth; + node.style.paddingLeft = dims.gutterTotalWidth + "px"; + } + node.style.width = width + "px"; + } + if (widget.coverGutter) { + node.style.zIndex = 5; + node.style.position = "relative"; + if (!widget.noHScroll) node.style.marginLeft = -dims.gutterTotalWidth + "px"; + } + } + + // POSITION OBJECT + + // A Pos instance represents a position within the text. + var Pos = CodeMirror.Pos = function(line, ch) { + if (!(this instanceof Pos)) return new Pos(line, ch); + this.line = line; this.ch = ch; + }; + + // Compare two positions, return 0 if they are the same, a negative + // number when a is less, and a positive number otherwise. + var cmp = CodeMirror.cmpPos = function(a, b) { return a.line - b.line || a.ch - b.ch; }; + + function copyPos(x) {return Pos(x.line, x.ch);} + function maxPos(a, b) { return cmp(a, b) < 0 ? b : a; } + function minPos(a, b) { return cmp(a, b) < 0 ? a : b; } + + // INPUT HANDLING + + function ensureFocus(cm) { + if (!cm.state.focused) { cm.display.input.focus(); onFocus(cm); } + } + + function isReadOnly(cm) { + return cm.options.readOnly || cm.doc.cantEdit; + } + + // This will be set to an array of strings when copying, so that, + // when pasting, we know what kind of selections the copied text + // was made out of. + var lastCopied = null; + + function applyTextInput(cm, inserted, deleted, sel, origin) { + var doc = cm.doc; + cm.display.shift = false; + if (!sel) sel = doc.sel; + + var textLines = splitLines(inserted), multiPaste = null; + // When pasing N lines into N selections, insert one line per selection + if (cm.state.pasteIncoming && sel.ranges.length > 1) { + if (lastCopied && lastCopied.join("\n") == inserted) + multiPaste = sel.ranges.length % lastCopied.length == 0 && map(lastCopied, splitLines); + else if (textLines.length == sel.ranges.length) + multiPaste = map(textLines, function(l) { return [l]; }); + } + + // Normal behavior is to insert the new text into every selection + for (var i = sel.ranges.length - 1; i >= 0; i--) { + var range = sel.ranges[i]; + var from = range.from(), to = range.to(); + if (range.empty()) { + if (deleted && deleted > 0) // Handle deletion + from = Pos(from.line, from.ch - deleted); + else if (cm.state.overwrite && !cm.state.pasteIncoming) // Handle overwrite + to = Pos(to.line, Math.min(getLine(doc, to.line).text.length, to.ch + lst(textLines).length)); + } + var updateInput = cm.curOp.updateInput; + var changeEvent = {from: from, to: to, text: multiPaste ? multiPaste[i % multiPaste.length] : textLines, + origin: origin || (cm.state.pasteIncoming ? "paste" : cm.state.cutIncoming ? "cut" : "+input")}; + makeChange(cm.doc, changeEvent); + signalLater(cm, "inputRead", cm, changeEvent); + } + if (inserted && !cm.state.pasteIncoming) + triggerElectric(cm, inserted); + + ensureCursorVisible(cm); + cm.curOp.updateInput = updateInput; + cm.curOp.typing = true; + cm.state.pasteIncoming = cm.state.cutIncoming = false; + } + + function triggerElectric(cm, inserted) { + // When an 'electric' character is inserted, immediately trigger a reindent + if (!cm.options.electricChars || !cm.options.smartIndent) return; + var sel = cm.doc.sel; + + for (var i = sel.ranges.length - 1; i >= 0; i--) { + var range = sel.ranges[i]; + if (range.head.ch > 100 || (i && sel.ranges[i - 1].head.line == range.head.line)) continue; + var mode = cm.getModeAt(range.head); + var indented = false; + if (mode.electricChars) { + for (var j = 0; j < mode.electricChars.length; j++) + if (inserted.indexOf(mode.electricChars.charAt(j)) > -1) { + indented = indentLine(cm, range.head.line, "smart"); + break; + } + } else if (mode.electricInput) { + if (mode.electricInput.test(getLine(cm.doc, range.head.line).text.slice(0, range.head.ch))) + indented = indentLine(cm, range.head.line, "smart"); + } + if (indented) signalLater(cm, "electricInput", cm, range.head.line); + } + } + + function copyableRanges(cm) { + var text = [], ranges = []; + for (var i = 0; i < cm.doc.sel.ranges.length; i++) { + var line = cm.doc.sel.ranges[i].head.line; + var lineRange = {anchor: Pos(line, 0), head: Pos(line + 1, 0)}; + ranges.push(lineRange); + text.push(cm.getRange(lineRange.anchor, lineRange.head)); + } + return {text: text, ranges: ranges}; + } + + function disableBrowserMagic(field) { + field.setAttribute("autocorrect", "off"); + field.setAttribute("autocapitalize", "off"); + field.setAttribute("spellcheck", "false"); + } + + // TEXTAREA INPUT STYLE + + function TextareaInput(cm) { + this.cm = cm; + // See input.poll and input.reset + this.prevInput = ""; + + // Flag that indicates whether we expect input to appear real soon + // now (after some event like 'keypress' or 'input') and are + // polling intensively. + this.pollingFast = false; + // Self-resetting timeout for the poller + this.polling = new Delayed(); + // Tracks when input.reset has punted to just putting a short + // string into the textarea instead of the full selection. + this.inaccurateSelection = false; + // Used to work around IE issue with selection being forgotten when focus moves away from textarea + this.hasSelection = false; + this.composing = null; + }; + + function hiddenTextarea() { + var te = elt("textarea", null, null, "position: absolute; padding: 0; width: 1px; height: 1em; outline: none"); + var div = elt("div", [te], null, "overflow: hidden; position: relative; width: 3px; height: 0px;"); + // The textarea is kept positioned near the cursor to prevent the + // fact that it'll be scrolled into view on input from scrolling + // our fake cursor out of view. On webkit, when wrap=off, paste is + // very slow. So make the area wide instead. + if (webkit) te.style.width = "1000px"; + else te.setAttribute("wrap", "off"); + // If border: 0; -- iOS fails to open keyboard (issue #1287) + if (ios) te.style.border = "1px solid black"; + disableBrowserMagic(te); + return div; + } + + TextareaInput.prototype = copyObj({ + init: function(display) { + var input = this, cm = this.cm; + + // Wraps and hides input textarea + var div = this.wrapper = hiddenTextarea(); + // The semihidden textarea that is focused when the editor is + // focused, and receives input. + var te = this.textarea = div.firstChild; + display.wrapper.insertBefore(div, display.wrapper.firstChild); + + // Needed to hide big blue blinking cursor on Mobile Safari (doesn't seem to work in iOS 8 anymore) + if (ios) te.style.width = "0px"; + + on(te, "input", function() { + if (ie && ie_version >= 9 && input.hasSelection) input.hasSelection = null; + input.poll(); + }); + + on(te, "paste", function() { + // Workaround for webkit bug https://bugs.webkit.org/show_bug.cgi?id=90206 + // Add a char to the end of textarea before paste occur so that + // selection doesn't span to the end of textarea. + if (webkit && !cm.state.fakedLastChar && !(new Date - cm.state.lastMiddleDown < 200)) { + var start = te.selectionStart, end = te.selectionEnd; + te.value += "$"; + // The selection end needs to be set before the start, otherwise there + // can be an intermediate non-empty selection between the two, which + // can override the middle-click paste buffer on linux and cause the + // wrong thing to get pasted. + te.selectionEnd = end; + te.selectionStart = start; + cm.state.fakedLastChar = true; + } + cm.state.pasteIncoming = true; + input.fastPoll(); + }); + + function prepareCopyCut(e) { + if (cm.somethingSelected()) { + lastCopied = cm.getSelections(); + if (input.inaccurateSelection) { + input.prevInput = ""; + input.inaccurateSelection = false; + te.value = lastCopied.join("\n"); + selectInput(te); + } + } else if (!cm.options.lineWiseCopyCut) { + return; + } else { + var ranges = copyableRanges(cm); + lastCopied = ranges.text; + if (e.type == "cut") { + cm.setSelections(ranges.ranges, null, sel_dontScroll); + } else { + input.prevInput = ""; + te.value = ranges.text.join("\n"); + selectInput(te); + } + } + if (e.type == "cut") cm.state.cutIncoming = true; + } + on(te, "cut", prepareCopyCut); + on(te, "copy", prepareCopyCut); + + on(display.scroller, "paste", function(e) { + if (eventInWidget(display, e)) return; + cm.state.pasteIncoming = true; + input.focus(); + }); + + // Prevent normal selection in the editor (we handle our own) + on(display.lineSpace, "selectstart", function(e) { + if (!eventInWidget(display, e)) e_preventDefault(e); + }); + + on(te, "compositionstart", function() { + var start = cm.getCursor("from"); + input.composing = { + start: start, + range: cm.markText(start, cm.getCursor("to"), {className: "CodeMirror-composing"}) + }; + }); + on(te, "compositionend", function() { + if (input.composing) { + input.poll(); + input.composing.range.clear(); + input.composing = null; + } + }); + }, + + prepareSelection: function() { + // Redraw the selection and/or cursor + var cm = this.cm, display = cm.display, doc = cm.doc; + var result = prepareSelection(cm); + + // Move the hidden textarea near the cursor to prevent scrolling artifacts + if (cm.options.moveInputWithCursor) { + var headPos = cursorCoords(cm, doc.sel.primary().head, "div"); + var wrapOff = display.wrapper.getBoundingClientRect(), lineOff = display.lineDiv.getBoundingClientRect(); + result.teTop = Math.max(0, Math.min(display.wrapper.clientHeight - 10, + headPos.top + lineOff.top - wrapOff.top)); + result.teLeft = Math.max(0, Math.min(display.wrapper.clientWidth - 10, + headPos.left + lineOff.left - wrapOff.left)); + } + + return result; + }, + + showSelection: function(drawn) { + var cm = this.cm, display = cm.display; + removeChildrenAndAdd(display.cursorDiv, drawn.cursors); + removeChildrenAndAdd(display.selectionDiv, drawn.selection); + if (drawn.teTop != null) { + this.wrapper.style.top = drawn.teTop + "px"; + this.wrapper.style.left = drawn.teLeft + "px"; + } + }, + + // Reset the input to correspond to the selection (or to be empty, + // when not typing and nothing is selected) + reset: function(typing) { + if (this.contextMenuPending) return; + var minimal, selected, cm = this.cm, doc = cm.doc; + if (cm.somethingSelected()) { + this.prevInput = ""; + var range = doc.sel.primary(); + minimal = hasCopyEvent && + (range.to().line - range.from().line > 100 || (selected = cm.getSelection()).length > 1000); + var content = minimal ? "-" : selected || cm.getSelection(); + this.textarea.value = content; + if (cm.state.focused) selectInput(this.textarea); + if (ie && ie_version >= 9) this.hasSelection = content; + } else if (!typing) { + this.prevInput = this.textarea.value = ""; + if (ie && ie_version >= 9) this.hasSelection = null; + } + this.inaccurateSelection = minimal; + }, + + getField: function() { return this.textarea; }, + + supportsTouch: function() { return false; }, + + focus: function() { + if (this.cm.options.readOnly != "nocursor" && (!mobile || activeElt() != this.textarea)) { + try { this.textarea.focus(); } + catch (e) {} // IE8 will throw if the textarea is display: none or not in DOM + } + }, + + blur: function() { this.textarea.blur(); }, + + resetPosition: function() { + this.wrapper.style.top = this.wrapper.style.left = 0; + }, + + receivedFocus: function() { this.slowPoll(); }, + + // Poll for input changes, using the normal rate of polling. This + // runs as long as the editor is focused. + slowPoll: function() { + var input = this; + if (input.pollingFast) return; + input.polling.set(this.cm.options.pollInterval, function() { + input.poll(); + if (input.cm.state.focused) input.slowPoll(); + }); + }, + + // When an event has just come in that is likely to add or change + // something in the input textarea, we poll faster, to ensure that + // the change appears on the screen quickly. + fastPoll: function() { + var missed = false, input = this; + input.pollingFast = true; + function p() { + var changed = input.poll(); + if (!changed && !missed) {missed = true; input.polling.set(60, p);} + else {input.pollingFast = false; input.slowPoll();} + } + input.polling.set(20, p); + }, + + // Read input from the textarea, and update the document to match. + // When something is selected, it is present in the textarea, and + // selected (unless it is huge, in which case a placeholder is + // used). When nothing is selected, the cursor sits after previously + // seen text (can be empty), which is stored in prevInput (we must + // not reset the textarea when typing, because that breaks IME). + poll: function() { + var cm = this.cm, input = this.textarea, prevInput = this.prevInput; + // Since this is called a *lot*, try to bail out as cheaply as + // possible when it is clear that nothing happened. hasSelection + // will be the case when there is a lot of text in the textarea, + // in which case reading its value would be expensive. + if (!cm.state.focused || (hasSelection(input) && !prevInput) || + isReadOnly(cm) || cm.options.disableInput || cm.state.keySeq) + return false; + // See paste handler for more on the fakedLastChar kludge + if (cm.state.pasteIncoming && cm.state.fakedLastChar) { + input.value = input.value.substring(0, input.value.length - 1); + cm.state.fakedLastChar = false; + } + var text = input.value; + // If nothing changed, bail. + if (text == prevInput && !cm.somethingSelected()) return false; + // Work around nonsensical selection resetting in IE9/10, and + // inexplicable appearance of private area unicode characters on + // some key combos in Mac (#2689). + if (ie && ie_version >= 9 && this.hasSelection === text || + mac && /[\uf700-\uf7ff]/.test(text)) { + cm.display.input.reset(); + return false; + } + + if (cm.doc.sel == cm.display.selForContextMenu) { + var first = text.charCodeAt(0); + if (first == 0x200b && !prevInput) prevInput = "\u200b"; + if (first == 0x21da) { this.reset(); return this.cm.execCommand("undo"); } + } + // Find the part of the input that is actually new + var same = 0, l = Math.min(prevInput.length, text.length); + while (same < l && prevInput.charCodeAt(same) == text.charCodeAt(same)) ++same; + + var self = this; + runInOp(cm, function() { + applyTextInput(cm, text.slice(same), prevInput.length - same, + null, self.composing ? "*compose" : null); + + // Don't leave long text in the textarea, since it makes further polling slow + if (text.length > 1000 || text.indexOf("\n") > -1) input.value = self.prevInput = ""; + else self.prevInput = text; + + if (self.composing) { + self.composing.range.clear(); + self.composing.range = cm.markText(self.composing.start, cm.getCursor("to"), + {className: "CodeMirror-composing"}); + } + }); + return true; + }, + + ensurePolled: function() { + if (this.pollingFast && this.poll()) this.pollingFast = false; + }, + + onKeyPress: function() { + if (ie && ie_version >= 9) this.hasSelection = null; + this.fastPoll(); + }, + + onContextMenu: function(e) { + var input = this, cm = input.cm, display = cm.display, te = input.textarea; + var pos = posFromMouse(cm, e), scrollPos = display.scroller.scrollTop; + if (!pos || presto) return; // Opera is difficult. + + // Reset the current text selection only if the click is done outside of the selection + // and 'resetSelectionOnContextMenu' option is true. + var reset = cm.options.resetSelectionOnContextMenu; + if (reset && cm.doc.sel.contains(pos) == -1) + operation(cm, setSelection)(cm.doc, simpleSelection(pos), sel_dontScroll); + + var oldCSS = te.style.cssText; + input.wrapper.style.position = "absolute"; + te.style.cssText = "position: fixed; width: 30px; height: 30px; top: " + (e.clientY - 5) + + "px; left: " + (e.clientX - 5) + "px; z-index: 1000; background: " + + (ie ? "rgba(255, 255, 255, .05)" : "transparent") + + "; outline: none; border-width: 0; outline: none; overflow: hidden; opacity: .05; filter: alpha(opacity=5);"; + if (webkit) var oldScrollY = window.scrollY; // Work around Chrome issue (#2712) + display.input.focus(); + if (webkit) window.scrollTo(null, oldScrollY); + display.input.reset(); + // Adds "Select all" to context menu in FF + if (!cm.somethingSelected()) te.value = input.prevInput = " "; + input.contextMenuPending = true; + display.selForContextMenu = cm.doc.sel; + clearTimeout(display.detectingSelectAll); + + // Select-all will be greyed out if there's nothing to select, so + // this adds a zero-width space so that we can later check whether + // it got selected. + function prepareSelectAllHack() { + if (te.selectionStart != null) { + var selected = cm.somethingSelected(); + var extval = "\u200b" + (selected ? te.value : ""); + te.value = "\u21da"; // Used to catch context-menu undo + te.value = extval; + input.prevInput = selected ? "" : "\u200b"; + te.selectionStart = 1; te.selectionEnd = extval.length; + // Re-set this, in case some other handler touched the + // selection in the meantime. + display.selForContextMenu = cm.doc.sel; + } + } + function rehide() { + input.contextMenuPending = false; + input.wrapper.style.position = "relative"; + te.style.cssText = oldCSS; + if (ie && ie_version < 9) display.scrollbars.setScrollTop(display.scroller.scrollTop = scrollPos); + + // Try to detect the user choosing select-all + if (te.selectionStart != null) { + if (!ie || (ie && ie_version < 9)) prepareSelectAllHack(); + var i = 0, poll = function() { + if (display.selForContextMenu == cm.doc.sel && te.selectionStart == 0 && + te.selectionEnd > 0 && input.prevInput == "\u200b") + operation(cm, commands.selectAll)(cm); + else if (i++ < 10) display.detectingSelectAll = setTimeout(poll, 500); + else display.input.reset(); + }; + display.detectingSelectAll = setTimeout(poll, 200); + } + } + + if (ie && ie_version >= 9) prepareSelectAllHack(); + if (captureRightClick) { + e_stop(e); + var mouseup = function() { + off(window, "mouseup", mouseup); + setTimeout(rehide, 20); + }; + on(window, "mouseup", mouseup); + } else { + setTimeout(rehide, 50); + } + }, + + setUneditable: nothing, + + needsContentAttribute: false + }, TextareaInput.prototype); + + // CONTENTEDITABLE INPUT STYLE + + function ContentEditableInput(cm) { + this.cm = cm; + this.lastAnchorNode = this.lastAnchorOffset = this.lastFocusNode = this.lastFocusOffset = null; + this.polling = new Delayed(); + this.gracePeriod = false; + } + + ContentEditableInput.prototype = copyObj({ + init: function(display) { + var input = this, cm = input.cm; + var div = input.div = display.lineDiv; + div.contentEditable = "true"; + disableBrowserMagic(div); + + on(div, "paste", function(e) { + var pasted = e.clipboardData && e.clipboardData.getData("text/plain"); + if (pasted) { + e.preventDefault(); + cm.replaceSelection(pasted, null, "paste"); + } + }); + + on(div, "compositionstart", function(e) { + var data = e.data; + input.composing = {sel: cm.doc.sel, data: data, startData: data}; + if (!data) return; + var prim = cm.doc.sel.primary(); + var line = cm.getLine(prim.head.line); + var found = line.indexOf(data, Math.max(0, prim.head.ch - data.length)); + if (found > -1 && found <= prim.head.ch) + input.composing.sel = simpleSelection(Pos(prim.head.line, found), + Pos(prim.head.line, found + data.length)); + }); + on(div, "compositionupdate", function(e) { + input.composing.data = e.data; + }); + on(div, "compositionend", function(e) { + var ours = input.composing; + if (!ours) return; + if (e.data != ours.startData && !/\u200b/.test(e.data)) + ours.data = e.data; + // Need a small delay to prevent other code (input event, + // selection polling) from doing damage when fired right after + // compositionend. + setTimeout(function() { + if (!ours.handled) + input.applyComposition(ours); + if (input.composing == ours) + input.composing = null; + }, 50); + }); + + on(div, "touchstart", function() { + input.forceCompositionEnd(); + }); + + on(div, "input", function() { + if (input.composing) return; + if (!input.pollContent()) + runInOp(input.cm, function() {regChange(cm);}); + }); + + function onCopyCut(e) { + if (cm.somethingSelected()) { + lastCopied = cm.getSelections(); + if (e.type == "cut") cm.replaceSelection("", null, "cut"); + } else if (!cm.options.lineWiseCopyCut) { + return; + } else { + var ranges = copyableRanges(cm); + lastCopied = ranges.text; + if (e.type == "cut") { + cm.operation(function() { + cm.setSelections(ranges.ranges, 0, sel_dontScroll); + cm.replaceSelection("", null, "cut"); + }); + } + } + // iOS exposes the clipboard API, but seems to discard content inserted into it + if (e.clipboardData && !ios) { + e.preventDefault(); + e.clipboardData.clearData(); + e.clipboardData.setData("text/plain", lastCopied.join("\n")); + } else { + // Old-fashioned briefly-focus-a-textarea hack + var kludge = hiddenTextarea(), te = kludge.firstChild; + cm.display.lineSpace.insertBefore(kludge, cm.display.lineSpace.firstChild); + te.value = lastCopied.join("\n"); + var hadFocus = document.activeElement; + selectInput(te); + setTimeout(function() { + cm.display.lineSpace.removeChild(kludge); + hadFocus.focus(); + }, 50); + } + } + on(div, "copy", onCopyCut); + on(div, "cut", onCopyCut); + }, + + prepareSelection: function() { + var result = prepareSelection(this.cm, false); + result.focus = this.cm.state.focused; + return result; + }, + + showSelection: function(info) { + if (!info || !this.cm.display.view.length) return; + if (info.focus) this.showPrimarySelection(); + this.showMultipleSelections(info); + }, + + showPrimarySelection: function() { + var sel = window.getSelection(), prim = this.cm.doc.sel.primary(); + var curAnchor = domToPos(this.cm, sel.anchorNode, sel.anchorOffset); + var curFocus = domToPos(this.cm, sel.focusNode, sel.focusOffset); + if (curAnchor && !curAnchor.bad && curFocus && !curFocus.bad && + cmp(minPos(curAnchor, curFocus), prim.from()) == 0 && + cmp(maxPos(curAnchor, curFocus), prim.to()) == 0) + return; + + var start = posToDOM(this.cm, prim.from()); + var end = posToDOM(this.cm, prim.to()); + if (!start && !end) return; + + var view = this.cm.display.view; + var old = sel.rangeCount && sel.getRangeAt(0); + if (!start) { + start = {node: view[0].measure.map[2], offset: 0}; + } else if (!end) { // FIXME dangerously hacky + var measure = view[view.length - 1].measure; + var map = measure.maps ? measure.maps[measure.maps.length - 1] : measure.map; + end = {node: map[map.length - 1], offset: map[map.length - 2] - map[map.length - 3]}; + } + + try { var rng = range(start.node, start.offset, end.offset, end.node); } + catch(e) {} // Our model of the DOM might be outdated, in which case the range we try to set can be impossible + if (rng) { + sel.removeAllRanges(); + sel.addRange(rng); + if (old && sel.anchorNode == null) sel.addRange(old); + else if (gecko) this.startGracePeriod(); + } + this.rememberSelection(); + }, + + startGracePeriod: function() { + var input = this; + clearTimeout(this.gracePeriod); + this.gracePeriod = setTimeout(function() { + input.gracePeriod = false; + if (input.selectionChanged()) + input.cm.operation(function() { input.cm.curOp.selectionChanged = true; }); + }, 20); + }, + + showMultipleSelections: function(info) { + removeChildrenAndAdd(this.cm.display.cursorDiv, info.cursors); + removeChildrenAndAdd(this.cm.display.selectionDiv, info.selection); + }, + + rememberSelection: function() { + var sel = window.getSelection(); + this.lastAnchorNode = sel.anchorNode; this.lastAnchorOffset = sel.anchorOffset; + this.lastFocusNode = sel.focusNode; this.lastFocusOffset = sel.focusOffset; + }, + + selectionInEditor: function() { + var sel = window.getSelection(); + if (!sel.rangeCount) return false; + var node = sel.getRangeAt(0).commonAncestorContainer; + return contains(this.div, node); + }, + + focus: function() { + if (this.cm.options.readOnly != "nocursor") this.div.focus(); + }, + blur: function() { this.div.blur(); }, + getField: function() { return this.div; }, + + supportsTouch: function() { return true; }, + + receivedFocus: function() { + var input = this; + if (this.selectionInEditor()) + this.pollSelection(); + else + runInOp(this.cm, function() { input.cm.curOp.selectionChanged = true; }); + + function poll() { + if (input.cm.state.focused) { + input.pollSelection(); + input.polling.set(input.cm.options.pollInterval, poll); + } + } + this.polling.set(this.cm.options.pollInterval, poll); + }, + + selectionChanged: function() { + var sel = window.getSelection(); + return sel.anchorNode != this.lastAnchorNode || sel.anchorOffset != this.lastAnchorOffset || + sel.focusNode != this.lastFocusNode || sel.focusOffset != this.lastFocusOffset; + }, + + pollSelection: function() { + if (!this.composing && !this.gracePeriod && this.selectionChanged()) { + var sel = window.getSelection(), cm = this.cm; + this.rememberSelection(); + var anchor = domToPos(cm, sel.anchorNode, sel.anchorOffset); + var head = domToPos(cm, sel.focusNode, sel.focusOffset); + if (anchor && head) runInOp(cm, function() { + setSelection(cm.doc, simpleSelection(anchor, head), sel_dontScroll); + if (anchor.bad || head.bad) cm.curOp.selectionChanged = true; + }); + } + }, + + pollContent: function() { + var cm = this.cm, display = cm.display, sel = cm.doc.sel.primary(); + var from = sel.from(), to = sel.to(); + if (from.line < display.viewFrom || to.line > display.viewTo - 1) return false; + + var fromIndex; + if (from.line == display.viewFrom || (fromIndex = findViewIndex(cm, from.line)) == 0) { + var fromLine = lineNo(display.view[0].line); + var fromNode = display.view[0].node; + } else { + var fromLine = lineNo(display.view[fromIndex].line); + var fromNode = display.view[fromIndex - 1].node.nextSibling; + } + var toIndex = findViewIndex(cm, to.line); + if (toIndex == display.view.length - 1) { + var toLine = display.viewTo - 1; + var toNode = display.view[toIndex].node; + } else { + var toLine = lineNo(display.view[toIndex + 1].line) - 1; + var toNode = display.view[toIndex + 1].node.previousSibling; + } + + var newText = splitLines(domTextBetween(cm, fromNode, toNode, fromLine, toLine)); + var oldText = getBetween(cm.doc, Pos(fromLine, 0), Pos(toLine, getLine(cm.doc, toLine).text.length)); + while (newText.length > 1 && oldText.length > 1) { + if (lst(newText) == lst(oldText)) { newText.pop(); oldText.pop(); toLine--; } + else if (newText[0] == oldText[0]) { newText.shift(); oldText.shift(); fromLine++; } + else break; + } + + var cutFront = 0, cutEnd = 0; + var newTop = newText[0], oldTop = oldText[0], maxCutFront = Math.min(newTop.length, oldTop.length); + while (cutFront < maxCutFront && newTop.charCodeAt(cutFront) == oldTop.charCodeAt(cutFront)) + ++cutFront; + var newBot = lst(newText), oldBot = lst(oldText); + var maxCutEnd = Math.min(newBot.length - (newText.length == 1 ? cutFront : 0), + oldBot.length - (oldText.length == 1 ? cutFront : 0)); + while (cutEnd < maxCutEnd && + newBot.charCodeAt(newBot.length - cutEnd - 1) == oldBot.charCodeAt(oldBot.length - cutEnd - 1)) + ++cutEnd; + + newText[newText.length - 1] = newBot.slice(0, newBot.length - cutEnd); + newText[0] = newText[0].slice(cutFront); + + var chFrom = Pos(fromLine, cutFront); + var chTo = Pos(toLine, oldText.length ? lst(oldText).length - cutEnd : 0); + if (newText.length > 1 || newText[0] || cmp(chFrom, chTo)) { + replaceRange(cm.doc, newText, chFrom, chTo, "+input"); + return true; + } + }, + + ensurePolled: function() { + this.forceCompositionEnd(); + }, + reset: function() { + this.forceCompositionEnd(); + }, + forceCompositionEnd: function() { + if (!this.composing || this.composing.handled) return; + this.applyComposition(this.composing); + this.composing.handled = true; + this.div.blur(); + this.div.focus(); + }, + applyComposition: function(composing) { + if (composing.data && composing.data != composing.startData) + operation(this.cm, applyTextInput)(this.cm, composing.data, 0, composing.sel); + }, + + setUneditable: function(node) { + node.setAttribute("contenteditable", "false"); + }, + + onKeyPress: function(e) { + e.preventDefault(); + operation(this.cm, applyTextInput)(this.cm, String.fromCharCode(e.charCode == null ? e.keyCode : e.charCode), 0); + }, + + onContextMenu: nothing, + resetPosition: nothing, + + needsContentAttribute: true + }, ContentEditableInput.prototype); + + function posToDOM(cm, pos) { + var view = findViewForLine(cm, pos.line); + if (!view || view.hidden) return null; + var line = getLine(cm.doc, pos.line); + var info = mapFromLineView(view, line, pos.line); + + var order = getOrder(line), side = "left"; + if (order) { + var partPos = getBidiPartAt(order, pos.ch); + side = partPos % 2 ? "right" : "left"; + } + var result = nodeAndOffsetInLineMap(info.map, pos.ch, side); + result.offset = result.collapse == "right" ? result.end : result.start; + return result; + } + + function badPos(pos, bad) { if (bad) pos.bad = true; return pos; } + + function domToPos(cm, node, offset) { + var lineNode; + if (node == cm.display.lineDiv) { + lineNode = cm.display.lineDiv.childNodes[offset]; + if (!lineNode) return badPos(cm.clipPos(Pos(cm.display.viewTo - 1)), true); + node = null; offset = 0; + } else { + for (lineNode = node;; lineNode = lineNode.parentNode) { + if (!lineNode || lineNode == cm.display.lineDiv) return null; + if (lineNode.parentNode && lineNode.parentNode == cm.display.lineDiv) break; + } + } + for (var i = 0; i < cm.display.view.length; i++) { + var lineView = cm.display.view[i]; + if (lineView.node == lineNode) + return locateNodeInLineView(lineView, node, offset); + } + } + + function locateNodeInLineView(lineView, node, offset) { + var wrapper = lineView.text.firstChild, bad = false; + if (!node || !contains(wrapper, node)) return badPos(Pos(lineNo(lineView.line), 0), true); + if (node == wrapper) { + bad = true; + node = wrapper.childNodes[offset]; + offset = 0; + if (!node) { + var line = lineView.rest ? lst(lineView.rest) : lineView.line; + return badPos(Pos(lineNo(line), line.text.length), bad); + } + } + + var textNode = node.nodeType == 3 ? node : null, topNode = node; + if (!textNode && node.childNodes.length == 1 && node.firstChild.nodeType == 3) { + textNode = node.firstChild; + if (offset) offset = textNode.nodeValue.length; + } + while (topNode.parentNode != wrapper) topNode = topNode.parentNode; + var measure = lineView.measure, maps = measure.maps; + + function find(textNode, topNode, offset) { + for (var i = -1; i < (maps ? maps.length : 0); i++) { + var map = i < 0 ? measure.map : maps[i]; + for (var j = 0; j < map.length; j += 3) { + var curNode = map[j + 2]; + if (curNode == textNode || curNode == topNode) { + var line = lineNo(i < 0 ? lineView.line : lineView.rest[i]); + var ch = map[j] + offset; + if (offset < 0 || curNode != textNode) ch = map[j + (offset ? 1 : 0)]; + return Pos(line, ch); + } + } + } + } + var found = find(textNode, topNode, offset); + if (found) return badPos(found, bad); + + // FIXME this is all really shaky. might handle the few cases it needs to handle, but likely to cause problems + for (var after = topNode.nextSibling, dist = textNode ? textNode.nodeValue.length - offset : 0; after; after = after.nextSibling) { + found = find(after, after.firstChild, 0); + if (found) + return badPos(Pos(found.line, found.ch - dist), bad); + else + dist += after.textContent.length; + } + for (var before = topNode.previousSibling, dist = offset; before; before = before.previousSibling) { + found = find(before, before.firstChild, -1); + if (found) + return badPos(Pos(found.line, found.ch + dist), bad); + else + dist += after.textContent.length; + } + } + + function domTextBetween(cm, from, to, fromLine, toLine) { + var text = "", closing = false; + function recognizeMarker(id) { return function(marker) { return marker.id == id; }; } + function walk(node) { + if (node.nodeType == 1) { + var cmText = node.getAttribute("cm-text"); + if (cmText != null) { + if (cmText == "") cmText = node.textContent.replace(/\u200b/g, ""); + text += cmText; + return; + } + var markerID = node.getAttribute("cm-marker"), range; + if (markerID) { + var found = cm.findMarks(Pos(fromLine, 0), Pos(toLine + 1, 0), recognizeMarker(+markerID)); + if (found.length && (range = found[0].find())) + text += getBetween(cm.doc, range.from, range.to).join("\n"); + return; + } + if (node.getAttribute("contenteditable") == "false") return; + for (var i = 0; i < node.childNodes.length; i++) + walk(node.childNodes[i]); + if (/^(pre|div|p)$/i.test(node.nodeName)) + closing = true; + } else if (node.nodeType == 3) { + var val = node.nodeValue; + if (!val) return; + if (closing) { + text += "\n"; + closing = false; + } + text += val; + } + } + for (;;) { + walk(from); + if (from == to) break; + from = from.nextSibling; + } + return text; + } + + CodeMirror.inputStyles = {"textarea": TextareaInput, "contenteditable": ContentEditableInput}; + + // SELECTION / CURSOR + + // Selection objects are immutable. A new one is created every time + // the selection changes. A selection is one or more non-overlapping + // (and non-touching) ranges, sorted, and an integer that indicates + // which one is the primary selection (the one that's scrolled into + // view, that getCursor returns, etc). + function Selection(ranges, primIndex) { + this.ranges = ranges; + this.primIndex = primIndex; + } + + Selection.prototype = { + primary: function() { return this.ranges[this.primIndex]; }, + equals: function(other) { + if (other == this) return true; + if (other.primIndex != this.primIndex || other.ranges.length != this.ranges.length) return false; + for (var i = 0; i < this.ranges.length; i++) { + var here = this.ranges[i], there = other.ranges[i]; + if (cmp(here.anchor, there.anchor) != 0 || cmp(here.head, there.head) != 0) return false; + } + return true; + }, + deepCopy: function() { + for (var out = [], i = 0; i < this.ranges.length; i++) + out[i] = new Range(copyPos(this.ranges[i].anchor), copyPos(this.ranges[i].head)); + return new Selection(out, this.primIndex); + }, + somethingSelected: function() { + for (var i = 0; i < this.ranges.length; i++) + if (!this.ranges[i].empty()) return true; + return false; + }, + contains: function(pos, end) { + if (!end) end = pos; + for (var i = 0; i < this.ranges.length; i++) { + var range = this.ranges[i]; + if (cmp(end, range.from()) >= 0 && cmp(pos, range.to()) <= 0) + return i; + } + return -1; + } + }; + + function Range(anchor, head) { + this.anchor = anchor; this.head = head; + } + + Range.prototype = { + from: function() { return minPos(this.anchor, this.head); }, + to: function() { return maxPos(this.anchor, this.head); }, + empty: function() { + return this.head.line == this.anchor.line && this.head.ch == this.anchor.ch; + } + }; + + // Take an unsorted, potentially overlapping set of ranges, and + // build a selection out of it. 'Consumes' ranges array (modifying + // it). + function normalizeSelection(ranges, primIndex) { + var prim = ranges[primIndex]; + ranges.sort(function(a, b) { return cmp(a.from(), b.from()); }); + primIndex = indexOf(ranges, prim); + for (var i = 1; i < ranges.length; i++) { + var cur = ranges[i], prev = ranges[i - 1]; + if (cmp(prev.to(), cur.from()) >= 0) { + var from = minPos(prev.from(), cur.from()), to = maxPos(prev.to(), cur.to()); + var inv = prev.empty() ? cur.from() == cur.head : prev.from() == prev.head; + if (i <= primIndex) --primIndex; + ranges.splice(--i, 2, new Range(inv ? to : from, inv ? from : to)); + } + } + return new Selection(ranges, primIndex); + } + + function simpleSelection(anchor, head) { + return new Selection([new Range(anchor, head || anchor)], 0); + } + + // Most of the external API clips given positions to make sure they + // actually exist within the document. + function clipLine(doc, n) {return Math.max(doc.first, Math.min(n, doc.first + doc.size - 1));} + function clipPos(doc, pos) { + if (pos.line < doc.first) return Pos(doc.first, 0); + var last = doc.first + doc.size - 1; + if (pos.line > last) return Pos(last, getLine(doc, last).text.length); + return clipToLen(pos, getLine(doc, pos.line).text.length); + } + function clipToLen(pos, linelen) { + var ch = pos.ch; + if (ch == null || ch > linelen) return Pos(pos.line, linelen); + else if (ch < 0) return Pos(pos.line, 0); + else return pos; + } + function isLine(doc, l) {return l >= doc.first && l < doc.first + doc.size;} + function clipPosArray(doc, array) { + for (var out = [], i = 0; i < array.length; i++) out[i] = clipPos(doc, array[i]); + return out; + } + + // SELECTION UPDATES + + // The 'scroll' parameter given to many of these indicated whether + // the new cursor position should be scrolled into view after + // modifying the selection. + + // If shift is held or the extend flag is set, extends a range to + // include a given position (and optionally a second position). + // Otherwise, simply returns the range between the given positions. + // Used for cursor motion and such. + function extendRange(doc, range, head, other) { + if (doc.cm && doc.cm.display.shift || doc.extend) { + var anchor = range.anchor; + if (other) { + var posBefore = cmp(head, anchor) < 0; + if (posBefore != (cmp(other, anchor) < 0)) { + anchor = head; + head = other; + } else if (posBefore != (cmp(head, other) < 0)) { + head = other; + } + } + return new Range(anchor, head); + } else { + return new Range(other || head, head); + } + } + + // Extend the primary selection range, discard the rest. + function extendSelection(doc, head, other, options) { + setSelection(doc, new Selection([extendRange(doc, doc.sel.primary(), head, other)], 0), options); + } + + // Extend all selections (pos is an array of selections with length + // equal the number of selections) + function extendSelections(doc, heads, options) { + for (var out = [], i = 0; i < doc.sel.ranges.length; i++) + out[i] = extendRange(doc, doc.sel.ranges[i], heads[i], null); + var newSel = normalizeSelection(out, doc.sel.primIndex); + setSelection(doc, newSel, options); + } + + // Updates a single range in the selection. + function replaceOneSelection(doc, i, range, options) { + var ranges = doc.sel.ranges.slice(0); + ranges[i] = range; + setSelection(doc, normalizeSelection(ranges, doc.sel.primIndex), options); + } + + // Reset the selection to a single range. + function setSimpleSelection(doc, anchor, head, options) { + setSelection(doc, simpleSelection(anchor, head), options); + } + + // Give beforeSelectionChange handlers a change to influence a + // selection update. + function filterSelectionChange(doc, sel) { + var obj = { + ranges: sel.ranges, + update: function(ranges) { + this.ranges = []; + for (var i = 0; i < ranges.length; i++) + this.ranges[i] = new Range(clipPos(doc, ranges[i].anchor), + clipPos(doc, ranges[i].head)); + } + }; + signal(doc, "beforeSelectionChange", doc, obj); + if (doc.cm) signal(doc.cm, "beforeSelectionChange", doc.cm, obj); + if (obj.ranges != sel.ranges) return normalizeSelection(obj.ranges, obj.ranges.length - 1); + else return sel; + } + + function setSelectionReplaceHistory(doc, sel, options) { + var done = doc.history.done, last = lst(done); + if (last && last.ranges) { + done[done.length - 1] = sel; + setSelectionNoUndo(doc, sel, options); + } else { + setSelection(doc, sel, options); + } + } + + // Set a new selection. + function setSelection(doc, sel, options) { + setSelectionNoUndo(doc, sel, options); + addSelectionToHistory(doc, doc.sel, doc.cm ? doc.cm.curOp.id : NaN, options); + } + + function setSelectionNoUndo(doc, sel, options) { + if (hasHandler(doc, "beforeSelectionChange") || doc.cm && hasHandler(doc.cm, "beforeSelectionChange")) + sel = filterSelectionChange(doc, sel); + + var bias = options && options.bias || + (cmp(sel.primary().head, doc.sel.primary().head) < 0 ? -1 : 1); + setSelectionInner(doc, skipAtomicInSelection(doc, sel, bias, true)); + + if (!(options && options.scroll === false) && doc.cm) + ensureCursorVisible(doc.cm); + } + + function setSelectionInner(doc, sel) { + if (sel.equals(doc.sel)) return; + + doc.sel = sel; + + if (doc.cm) { + doc.cm.curOp.updateInput = doc.cm.curOp.selectionChanged = true; + signalCursorActivity(doc.cm); + } + signalLater(doc, "cursorActivity", doc); + } + + // Verify that the selection does not partially select any atomic + // marked ranges. + function reCheckSelection(doc) { + setSelectionInner(doc, skipAtomicInSelection(doc, doc.sel, null, false), sel_dontScroll); + } + + // Return a selection that does not partially select any atomic + // ranges. + function skipAtomicInSelection(doc, sel, bias, mayClear) { + var out; + for (var i = 0; i < sel.ranges.length; i++) { + var range = sel.ranges[i]; + var newAnchor = skipAtomic(doc, range.anchor, bias, mayClear); + var newHead = skipAtomic(doc, range.head, bias, mayClear); + if (out || newAnchor != range.anchor || newHead != range.head) { + if (!out) out = sel.ranges.slice(0, i); + out[i] = new Range(newAnchor, newHead); + } + } + return out ? normalizeSelection(out, sel.primIndex) : sel; + } + + // Ensure a given position is not inside an atomic range. + function skipAtomic(doc, pos, bias, mayClear) { + var flipped = false, curPos = pos; + var dir = bias || 1; + doc.cantEdit = false; + search: for (;;) { + var line = getLine(doc, curPos.line); + if (line.markedSpans) { + for (var i = 0; i < line.markedSpans.length; ++i) { + var sp = line.markedSpans[i], m = sp.marker; + if ((sp.from == null || (m.inclusiveLeft ? sp.from <= curPos.ch : sp.from < curPos.ch)) && + (sp.to == null || (m.inclusiveRight ? sp.to >= curPos.ch : sp.to > curPos.ch))) { + if (mayClear) { + signal(m, "beforeCursorEnter"); + if (m.explicitlyCleared) { + if (!line.markedSpans) break; + else {--i; continue;} + } + } + if (!m.atomic) continue; + var newPos = m.find(dir < 0 ? -1 : 1); + if (cmp(newPos, curPos) == 0) { + newPos.ch += dir; + if (newPos.ch < 0) { + if (newPos.line > doc.first) newPos = clipPos(doc, Pos(newPos.line - 1)); + else newPos = null; + } else if (newPos.ch > line.text.length) { + if (newPos.line < doc.first + doc.size - 1) newPos = Pos(newPos.line + 1, 0); + else newPos = null; + } + if (!newPos) { + if (flipped) { + // Driven in a corner -- no valid cursor position found at all + // -- try again *with* clearing, if we didn't already + if (!mayClear) return skipAtomic(doc, pos, bias, true); + // Otherwise, turn off editing until further notice, and return the start of the doc + doc.cantEdit = true; + return Pos(doc.first, 0); + } + flipped = true; newPos = pos; dir = -dir; + } + } + curPos = newPos; + continue search; + } + } + } + return curPos; + } + } + + // SELECTION DRAWING + + function updateSelection(cm) { + cm.display.input.showSelection(cm.display.input.prepareSelection()); + } + + function prepareSelection(cm, primary) { + var doc = cm.doc, result = {}; + var curFragment = result.cursors = document.createDocumentFragment(); + var selFragment = result.selection = document.createDocumentFragment(); + + for (var i = 0; i < doc.sel.ranges.length; i++) { + if (primary === false && i == doc.sel.primIndex) continue; + var range = doc.sel.ranges[i]; + var collapsed = range.empty(); + if (collapsed || cm.options.showCursorWhenSelecting) + drawSelectionCursor(cm, range, curFragment); + if (!collapsed) + drawSelectionRange(cm, range, selFragment); + } + return result; + } + + // Draws a cursor for the given range + function drawSelectionCursor(cm, range, output) { + var pos = cursorCoords(cm, range.head, "div", null, null, !cm.options.singleCursorHeightPerLine); + + var cursor = output.appendChild(elt("div", "\u00a0", "CodeMirror-cursor")); + cursor.style.left = pos.left + "px"; + cursor.style.top = pos.top + "px"; + cursor.style.height = Math.max(0, pos.bottom - pos.top) * cm.options.cursorHeight + "px"; + + if (pos.other) { + // Secondary cursor, shown when on a 'jump' in bi-directional text + var otherCursor = output.appendChild(elt("div", "\u00a0", "CodeMirror-cursor CodeMirror-secondarycursor")); + otherCursor.style.display = ""; + otherCursor.style.left = pos.other.left + "px"; + otherCursor.style.top = pos.other.top + "px"; + otherCursor.style.height = (pos.other.bottom - pos.other.top) * .85 + "px"; + } + } + + // Draws the given range as a highlighted selection + function drawSelectionRange(cm, range, output) { + var display = cm.display, doc = cm.doc; + var fragment = document.createDocumentFragment(); + var padding = paddingH(cm.display), leftSide = padding.left; + var rightSide = Math.max(display.sizerWidth, displayWidth(cm) - display.sizer.offsetLeft) - padding.right; + + function add(left, top, width, bottom) { + if (top < 0) top = 0; + top = Math.round(top); + bottom = Math.round(bottom); + fragment.appendChild(elt("div", null, "CodeMirror-selected", "position: absolute; left: " + left + + "px; top: " + top + "px; width: " + (width == null ? rightSide - left : width) + + "px; height: " + (bottom - top) + "px")); + } + + function drawForLine(line, fromArg, toArg) { + var lineObj = getLine(doc, line); + var lineLen = lineObj.text.length; + var start, end; + function coords(ch, bias) { + return charCoords(cm, Pos(line, ch), "div", lineObj, bias); + } + + iterateBidiSections(getOrder(lineObj), fromArg || 0, toArg == null ? lineLen : toArg, function(from, to, dir) { + var leftPos = coords(from, "left"), rightPos, left, right; + if (from == to) { + rightPos = leftPos; + left = right = leftPos.left; + } else { + rightPos = coords(to - 1, "right"); + if (dir == "rtl") { var tmp = leftPos; leftPos = rightPos; rightPos = tmp; } + left = leftPos.left; + right = rightPos.right; + } + if (fromArg == null && from == 0) left = leftSide; + if (rightPos.top - leftPos.top > 3) { // Different lines, draw top part + add(left, leftPos.top, null, leftPos.bottom); + left = leftSide; + if (leftPos.bottom < rightPos.top) add(left, leftPos.bottom, null, rightPos.top); + } + if (toArg == null && to == lineLen) right = rightSide; + if (!start || leftPos.top < start.top || leftPos.top == start.top && leftPos.left < start.left) + start = leftPos; + if (!end || rightPos.bottom > end.bottom || rightPos.bottom == end.bottom && rightPos.right > end.right) + end = rightPos; + if (left < leftSide + 1) left = leftSide; + add(left, rightPos.top, right - left, rightPos.bottom); + }); + return {start: start, end: end}; + } + + var sFrom = range.from(), sTo = range.to(); + if (sFrom.line == sTo.line) { + drawForLine(sFrom.line, sFrom.ch, sTo.ch); + } else { + var fromLine = getLine(doc, sFrom.line), toLine = getLine(doc, sTo.line); + var singleVLine = visualLine(fromLine) == visualLine(toLine); + var leftEnd = drawForLine(sFrom.line, sFrom.ch, singleVLine ? fromLine.text.length + 1 : null).end; + var rightStart = drawForLine(sTo.line, singleVLine ? 0 : null, sTo.ch).start; + if (singleVLine) { + if (leftEnd.top < rightStart.top - 2) { + add(leftEnd.right, leftEnd.top, null, leftEnd.bottom); + add(leftSide, rightStart.top, rightStart.left, rightStart.bottom); + } else { + add(leftEnd.right, leftEnd.top, rightStart.left - leftEnd.right, leftEnd.bottom); + } + } + if (leftEnd.bottom < rightStart.top) + add(leftSide, leftEnd.bottom, null, rightStart.top); + } + + output.appendChild(fragment); + } + + // Cursor-blinking + function restartBlink(cm) { + if (!cm.state.focused) return; + var display = cm.display; + clearInterval(display.blinker); + var on = true; + display.cursorDiv.style.visibility = ""; + if (cm.options.cursorBlinkRate > 0) + display.blinker = setInterval(function() { + display.cursorDiv.style.visibility = (on = !on) ? "" : "hidden"; + }, cm.options.cursorBlinkRate); + else if (cm.options.cursorBlinkRate < 0) + display.cursorDiv.style.visibility = "hidden"; + } + + // HIGHLIGHT WORKER + + function startWorker(cm, time) { + if (cm.doc.mode.startState && cm.doc.frontier < cm.display.viewTo) + cm.state.highlight.set(time, bind(highlightWorker, cm)); + } + + function highlightWorker(cm) { + var doc = cm.doc; + if (doc.frontier < doc.first) doc.frontier = doc.first; + if (doc.frontier >= cm.display.viewTo) return; + var end = +new Date + cm.options.workTime; + var state = copyState(doc.mode, getStateBefore(cm, doc.frontier)); + var changedLines = []; + + doc.iter(doc.frontier, Math.min(doc.first + doc.size, cm.display.viewTo + 500), function(line) { + if (doc.frontier >= cm.display.viewFrom) { // Visible + var oldStyles = line.styles; + var highlighted = highlightLine(cm, line, state, true); + line.styles = highlighted.styles; + var oldCls = line.styleClasses, newCls = highlighted.classes; + if (newCls) line.styleClasses = newCls; + else if (oldCls) line.styleClasses = null; + var ischange = !oldStyles || oldStyles.length != line.styles.length || + oldCls != newCls && (!oldCls || !newCls || oldCls.bgClass != newCls.bgClass || oldCls.textClass != newCls.textClass); + for (var i = 0; !ischange && i < oldStyles.length; ++i) ischange = oldStyles[i] != line.styles[i]; + if (ischange) changedLines.push(doc.frontier); + line.stateAfter = copyState(doc.mode, state); + } else { + processLine(cm, line.text, state); + line.stateAfter = doc.frontier % 5 == 0 ? copyState(doc.mode, state) : null; + } + ++doc.frontier; + if (+new Date > end) { + startWorker(cm, cm.options.workDelay); + return true; + } + }); + if (changedLines.length) runInOp(cm, function() { + for (var i = 0; i < changedLines.length; i++) + regLineChange(cm, changedLines[i], "text"); + }); + } + + // Finds the line to start with when starting a parse. Tries to + // find a line with a stateAfter, so that it can start with a + // valid state. If that fails, it returns the line with the + // smallest indentation, which tends to need the least context to + // parse correctly. + function findStartLine(cm, n, precise) { + var minindent, minline, doc = cm.doc; + var lim = precise ? -1 : n - (cm.doc.mode.innerMode ? 1000 : 100); + for (var search = n; search > lim; --search) { + if (search <= doc.first) return doc.first; + var line = getLine(doc, search - 1); + if (line.stateAfter && (!precise || search <= doc.frontier)) return search; + var indented = countColumn(line.text, null, cm.options.tabSize); + if (minline == null || minindent > indented) { + minline = search - 1; + minindent = indented; + } + } + return minline; + } + + function getStateBefore(cm, n, precise) { + var doc = cm.doc, display = cm.display; + if (!doc.mode.startState) return true; + var pos = findStartLine(cm, n, precise), state = pos > doc.first && getLine(doc, pos-1).stateAfter; + if (!state) state = startState(doc.mode); + else state = copyState(doc.mode, state); + doc.iter(pos, n, function(line) { + processLine(cm, line.text, state); + var save = pos == n - 1 || pos % 5 == 0 || pos >= display.viewFrom && pos < display.viewTo; + line.stateAfter = save ? copyState(doc.mode, state) : null; + ++pos; + }); + if (precise) doc.frontier = pos; + return state; + } + + // POSITION MEASUREMENT + + function paddingTop(display) {return display.lineSpace.offsetTop;} + function paddingVert(display) {return display.mover.offsetHeight - display.lineSpace.offsetHeight;} + function paddingH(display) { + if (display.cachedPaddingH) return display.cachedPaddingH; + var e = removeChildrenAndAdd(display.measure, elt("pre", "x")); + var style = window.getComputedStyle ? window.getComputedStyle(e) : e.currentStyle; + var data = {left: parseInt(style.paddingLeft), right: parseInt(style.paddingRight)}; + if (!isNaN(data.left) && !isNaN(data.right)) display.cachedPaddingH = data; + return data; + } + + function scrollGap(cm) { return scrollerGap - cm.display.nativeBarWidth; } + function displayWidth(cm) { + return cm.display.scroller.clientWidth - scrollGap(cm) - cm.display.barWidth; + } + function displayHeight(cm) { + return cm.display.scroller.clientHeight - scrollGap(cm) - cm.display.barHeight; + } + + // Ensure the lineView.wrapping.heights array is populated. This is + // an array of bottom offsets for the lines that make up a drawn + // line. When lineWrapping is on, there might be more than one + // height. + function ensureLineHeights(cm, lineView, rect) { + var wrapping = cm.options.lineWrapping; + var curWidth = wrapping && displayWidth(cm); + if (!lineView.measure.heights || wrapping && lineView.measure.width != curWidth) { + var heights = lineView.measure.heights = []; + if (wrapping) { + lineView.measure.width = curWidth; + var rects = lineView.text.firstChild.getClientRects(); + for (var i = 0; i < rects.length - 1; i++) { + var cur = rects[i], next = rects[i + 1]; + if (Math.abs(cur.bottom - next.bottom) > 2) + heights.push((cur.bottom + next.top) / 2 - rect.top); + } + } + heights.push(rect.bottom - rect.top); + } + } + + // Find a line map (mapping character offsets to text nodes) and a + // measurement cache for the given line number. (A line view might + // contain multiple lines when collapsed ranges are present.) + function mapFromLineView(lineView, line, lineN) { + if (lineView.line == line) + return {map: lineView.measure.map, cache: lineView.measure.cache}; + for (var i = 0; i < lineView.rest.length; i++) + if (lineView.rest[i] == line) + return {map: lineView.measure.maps[i], cache: lineView.measure.caches[i]}; + for (var i = 0; i < lineView.rest.length; i++) + if (lineNo(lineView.rest[i]) > lineN) + return {map: lineView.measure.maps[i], cache: lineView.measure.caches[i], before: true}; + } + + // Render a line into the hidden node display.externalMeasured. Used + // when measurement is needed for a line that's not in the viewport. + function updateExternalMeasurement(cm, line) { + line = visualLine(line); + var lineN = lineNo(line); + var view = cm.display.externalMeasured = new LineView(cm.doc, line, lineN); + view.lineN = lineN; + var built = view.built = buildLineContent(cm, view); + view.text = built.pre; + removeChildrenAndAdd(cm.display.lineMeasure, built.pre); + return view; + } + + // Get a {top, bottom, left, right} box (in line-local coordinates) + // for a given character. + function measureChar(cm, line, ch, bias) { + return measureCharPrepared(cm, prepareMeasureForLine(cm, line), ch, bias); + } + + // Find a line view that corresponds to the given line number. + function findViewForLine(cm, lineN) { + if (lineN >= cm.display.viewFrom && lineN < cm.display.viewTo) + return cm.display.view[findViewIndex(cm, lineN)]; + var ext = cm.display.externalMeasured; + if (ext && lineN >= ext.lineN && lineN < ext.lineN + ext.size) + return ext; + } + + // Measurement can be split in two steps, the set-up work that + // applies to the whole line, and the measurement of the actual + // character. Functions like coordsChar, that need to do a lot of + // measurements in a row, can thus ensure that the set-up work is + // only done once. + function prepareMeasureForLine(cm, line) { + var lineN = lineNo(line); + var view = findViewForLine(cm, lineN); + if (view && !view.text) + view = null; + else if (view && view.changes) + updateLineForChanges(cm, view, lineN, getDimensions(cm)); + if (!view) + view = updateExternalMeasurement(cm, line); + + var info = mapFromLineView(view, line, lineN); + return { + line: line, view: view, rect: null, + map: info.map, cache: info.cache, before: info.before, + hasHeights: false + }; + } + + // Given a prepared measurement object, measures the position of an + // actual character (or fetches it from the cache). + function measureCharPrepared(cm, prepared, ch, bias, varHeight) { + if (prepared.before) ch = -1; + var key = ch + (bias || ""), found; + if (prepared.cache.hasOwnProperty(key)) { + found = prepared.cache[key]; + } else { + if (!prepared.rect) + prepared.rect = prepared.view.text.getBoundingClientRect(); + if (!prepared.hasHeights) { + ensureLineHeights(cm, prepared.view, prepared.rect); + prepared.hasHeights = true; + } + found = measureCharInner(cm, prepared, ch, bias); + if (!found.bogus) prepared.cache[key] = found; + } + return {left: found.left, right: found.right, + top: varHeight ? found.rtop : found.top, + bottom: varHeight ? found.rbottom : found.bottom}; + } + + var nullRect = {left: 0, right: 0, top: 0, bottom: 0}; + + function nodeAndOffsetInLineMap(map, ch, bias) { + var node, start, end, collapse; + // First, search the line map for the text node corresponding to, + // or closest to, the target character. + for (var i = 0; i < map.length; i += 3) { + var mStart = map[i], mEnd = map[i + 1]; + if (ch < mStart) { + start = 0; end = 1; + collapse = "left"; + } else if (ch < mEnd) { + start = ch - mStart; + end = start + 1; + } else if (i == map.length - 3 || ch == mEnd && map[i + 3] > ch) { + end = mEnd - mStart; + start = end - 1; + if (ch >= mEnd) collapse = "right"; + } + if (start != null) { + node = map[i + 2]; + if (mStart == mEnd && bias == (node.insertLeft ? "left" : "right")) + collapse = bias; + if (bias == "left" && start == 0) + while (i && map[i - 2] == map[i - 3] && map[i - 1].insertLeft) { + node = map[(i -= 3) + 2]; + collapse = "left"; + } + if (bias == "right" && start == mEnd - mStart) + while (i < map.length - 3 && map[i + 3] == map[i + 4] && !map[i + 5].insertLeft) { + node = map[(i += 3) + 2]; + collapse = "right"; + } + break; + } + } + return {node: node, start: start, end: end, collapse: collapse, coverStart: mStart, coverEnd: mEnd}; + } + + function measureCharInner(cm, prepared, ch, bias) { + var place = nodeAndOffsetInLineMap(prepared.map, ch, bias); + var node = place.node, start = place.start, end = place.end, collapse = place.collapse; + + var rect; + if (node.nodeType == 3) { // If it is a text node, use a range to retrieve the coordinates. + for (var i = 0; i < 4; i++) { // Retry a maximum of 4 times when nonsense rectangles are returned + while (start && isExtendingChar(prepared.line.text.charAt(place.coverStart + start))) --start; + while (place.coverStart + end < place.coverEnd && isExtendingChar(prepared.line.text.charAt(place.coverStart + end))) ++end; + if (ie && ie_version < 9 && start == 0 && end == place.coverEnd - place.coverStart) { + rect = node.parentNode.getBoundingClientRect(); + } else if (ie && cm.options.lineWrapping) { + var rects = range(node, start, end).getClientRects(); + if (rects.length) + rect = rects[bias == "right" ? rects.length - 1 : 0]; + else + rect = nullRect; + } else { + rect = range(node, start, end).getBoundingClientRect() || nullRect; + } + if (rect.left || rect.right || start == 0) break; + end = start; + start = start - 1; + collapse = "right"; + } + if (ie && ie_version < 11) rect = maybeUpdateRectForZooming(cm.display.measure, rect); + } else { // If it is a widget, simply get the box for the whole widget. + if (start > 0) collapse = bias = "right"; + var rects; + if (cm.options.lineWrapping && (rects = node.getClientRects()).length > 1) + rect = rects[bias == "right" ? rects.length - 1 : 0]; + else + rect = node.getBoundingClientRect(); + } + if (ie && ie_version < 9 && !start && (!rect || !rect.left && !rect.right)) { + var rSpan = node.parentNode.getClientRects()[0]; + if (rSpan) + rect = {left: rSpan.left, right: rSpan.left + charWidth(cm.display), top: rSpan.top, bottom: rSpan.bottom}; + else + rect = nullRect; + } + + var rtop = rect.top - prepared.rect.top, rbot = rect.bottom - prepared.rect.top; + var mid = (rtop + rbot) / 2; + var heights = prepared.view.measure.heights; + for (var i = 0; i < heights.length - 1; i++) + if (mid < heights[i]) break; + var top = i ? heights[i - 1] : 0, bot = heights[i]; + var result = {left: (collapse == "right" ? rect.right : rect.left) - prepared.rect.left, + right: (collapse == "left" ? rect.left : rect.right) - prepared.rect.left, + top: top, bottom: bot}; + if (!rect.left && !rect.right) result.bogus = true; + if (!cm.options.singleCursorHeightPerLine) { result.rtop = rtop; result.rbottom = rbot; } + + return result; + } + + // Work around problem with bounding client rects on ranges being + // returned incorrectly when zoomed on IE10 and below. + function maybeUpdateRectForZooming(measure, rect) { + if (!window.screen || screen.logicalXDPI == null || + screen.logicalXDPI == screen.deviceXDPI || !hasBadZoomedRects(measure)) + return rect; + var scaleX = screen.logicalXDPI / screen.deviceXDPI; + var scaleY = screen.logicalYDPI / screen.deviceYDPI; + return {left: rect.left * scaleX, right: rect.right * scaleX, + top: rect.top * scaleY, bottom: rect.bottom * scaleY}; + } + + function clearLineMeasurementCacheFor(lineView) { + if (lineView.measure) { + lineView.measure.cache = {}; + lineView.measure.heights = null; + if (lineView.rest) for (var i = 0; i < lineView.rest.length; i++) + lineView.measure.caches[i] = {}; + } + } + + function clearLineMeasurementCache(cm) { + cm.display.externalMeasure = null; + removeChildren(cm.display.lineMeasure); + for (var i = 0; i < cm.display.view.length; i++) + clearLineMeasurementCacheFor(cm.display.view[i]); + } + + function clearCaches(cm) { + clearLineMeasurementCache(cm); + cm.display.cachedCharWidth = cm.display.cachedTextHeight = cm.display.cachedPaddingH = null; + if (!cm.options.lineWrapping) cm.display.maxLineChanged = true; + cm.display.lineNumChars = null; + } + + function pageScrollX() { return window.pageXOffset || (document.documentElement || document.body).scrollLeft; } + function pageScrollY() { return window.pageYOffset || (document.documentElement || document.body).scrollTop; } + + // Converts a {top, bottom, left, right} box from line-local + // coordinates into another coordinate system. Context may be one of + // "line", "div" (display.lineDiv), "local"/null (editor), "window", + // or "page". + function intoCoordSystem(cm, lineObj, rect, context) { + if (lineObj.widgets) for (var i = 0; i < lineObj.widgets.length; ++i) if (lineObj.widgets[i].above) { + var size = widgetHeight(lineObj.widgets[i]); + rect.top += size; rect.bottom += size; + } + if (context == "line") return rect; + if (!context) context = "local"; + var yOff = heightAtLine(lineObj); + if (context == "local") yOff += paddingTop(cm.display); + else yOff -= cm.display.viewOffset; + if (context == "page" || context == "window") { + var lOff = cm.display.lineSpace.getBoundingClientRect(); + yOff += lOff.top + (context == "window" ? 0 : pageScrollY()); + var xOff = lOff.left + (context == "window" ? 0 : pageScrollX()); + rect.left += xOff; rect.right += xOff; + } + rect.top += yOff; rect.bottom += yOff; + return rect; + } + + // Coverts a box from "div" coords to another coordinate system. + // Context may be "window", "page", "div", or "local"/null. + function fromCoordSystem(cm, coords, context) { + if (context == "div") return coords; + var left = coords.left, top = coords.top; + // First move into "page" coordinate system + if (context == "page") { + left -= pageScrollX(); + top -= pageScrollY(); + } else if (context == "local" || !context) { + var localBox = cm.display.sizer.getBoundingClientRect(); + left += localBox.left; + top += localBox.top; + } + + var lineSpaceBox = cm.display.lineSpace.getBoundingClientRect(); + return {left: left - lineSpaceBox.left, top: top - lineSpaceBox.top}; + } + + function charCoords(cm, pos, context, lineObj, bias) { + if (!lineObj) lineObj = getLine(cm.doc, pos.line); + return intoCoordSystem(cm, lineObj, measureChar(cm, lineObj, pos.ch, bias), context); + } + + // Returns a box for a given cursor position, which may have an + // 'other' property containing the position of the secondary cursor + // on a bidi boundary. + function cursorCoords(cm, pos, context, lineObj, preparedMeasure, varHeight) { + lineObj = lineObj || getLine(cm.doc, pos.line); + if (!preparedMeasure) preparedMeasure = prepareMeasureForLine(cm, lineObj); + function get(ch, right) { + var m = measureCharPrepared(cm, preparedMeasure, ch, right ? "right" : "left", varHeight); + if (right) m.left = m.right; else m.right = m.left; + return intoCoordSystem(cm, lineObj, m, context); + } + function getBidi(ch, partPos) { + var part = order[partPos], right = part.level % 2; + if (ch == bidiLeft(part) && partPos && part.level < order[partPos - 1].level) { + part = order[--partPos]; + ch = bidiRight(part) - (part.level % 2 ? 0 : 1); + right = true; + } else if (ch == bidiRight(part) && partPos < order.length - 1 && part.level < order[partPos + 1].level) { + part = order[++partPos]; + ch = bidiLeft(part) - part.level % 2; + right = false; + } + if (right && ch == part.to && ch > part.from) return get(ch - 1); + return get(ch, right); + } + var order = getOrder(lineObj), ch = pos.ch; + if (!order) return get(ch); + var partPos = getBidiPartAt(order, ch); + var val = getBidi(ch, partPos); + if (bidiOther != null) val.other = getBidi(ch, bidiOther); + return val; + } + + // Used to cheaply estimate the coordinates for a position. Used for + // intermediate scroll updates. + function estimateCoords(cm, pos) { + var left = 0, pos = clipPos(cm.doc, pos); + if (!cm.options.lineWrapping) left = charWidth(cm.display) * pos.ch; + var lineObj = getLine(cm.doc, pos.line); + var top = heightAtLine(lineObj) + paddingTop(cm.display); + return {left: left, right: left, top: top, bottom: top + lineObj.height}; + } + + // Positions returned by coordsChar contain some extra information. + // xRel is the relative x position of the input coordinates compared + // to the found position (so xRel > 0 means the coordinates are to + // the right of the character position, for example). When outside + // is true, that means the coordinates lie outside the line's + // vertical range. + function PosWithInfo(line, ch, outside, xRel) { + var pos = Pos(line, ch); + pos.xRel = xRel; + if (outside) pos.outside = true; + return pos; + } + + // Compute the character position closest to the given coordinates. + // Input must be lineSpace-local ("div" coordinate system). + function coordsChar(cm, x, y) { + var doc = cm.doc; + y += cm.display.viewOffset; + if (y < 0) return PosWithInfo(doc.first, 0, true, -1); + var lineN = lineAtHeight(doc, y), last = doc.first + doc.size - 1; + if (lineN > last) + return PosWithInfo(doc.first + doc.size - 1, getLine(doc, last).text.length, true, 1); + if (x < 0) x = 0; + + var lineObj = getLine(doc, lineN); + for (;;) { + var found = coordsCharInner(cm, lineObj, lineN, x, y); + var merged = collapsedSpanAtEnd(lineObj); + var mergedPos = merged && merged.find(0, true); + if (merged && (found.ch > mergedPos.from.ch || found.ch == mergedPos.from.ch && found.xRel > 0)) + lineN = lineNo(lineObj = mergedPos.to.line); + else + return found; + } + } + + function coordsCharInner(cm, lineObj, lineNo, x, y) { + var innerOff = y - heightAtLine(lineObj); + var wrongLine = false, adjust = 2 * cm.display.wrapper.clientWidth; + var preparedMeasure = prepareMeasureForLine(cm, lineObj); + + function getX(ch) { + var sp = cursorCoords(cm, Pos(lineNo, ch), "line", lineObj, preparedMeasure); + wrongLine = true; + if (innerOff > sp.bottom) return sp.left - adjust; + else if (innerOff < sp.top) return sp.left + adjust; + else wrongLine = false; + return sp.left; + } + + var bidi = getOrder(lineObj), dist = lineObj.text.length; + var from = lineLeft(lineObj), to = lineRight(lineObj); + var fromX = getX(from), fromOutside = wrongLine, toX = getX(to), toOutside = wrongLine; + + if (x > toX) return PosWithInfo(lineNo, to, toOutside, 1); + // Do a binary search between these bounds. + for (;;) { + if (bidi ? to == from || to == moveVisually(lineObj, from, 1) : to - from <= 1) { + var ch = x < fromX || x - fromX <= toX - x ? from : to; + var xDiff = x - (ch == from ? fromX : toX); + while (isExtendingChar(lineObj.text.charAt(ch))) ++ch; + var pos = PosWithInfo(lineNo, ch, ch == from ? fromOutside : toOutside, + xDiff < -1 ? -1 : xDiff > 1 ? 1 : 0); + return pos; + } + var step = Math.ceil(dist / 2), middle = from + step; + if (bidi) { + middle = from; + for (var i = 0; i < step; ++i) middle = moveVisually(lineObj, middle, 1); + } + var middleX = getX(middle); + if (middleX > x) {to = middle; toX = middleX; if (toOutside = wrongLine) toX += 1000; dist = step;} + else {from = middle; fromX = middleX; fromOutside = wrongLine; dist -= step;} + } + } + + var measureText; + // Compute the default text height. + function textHeight(display) { + if (display.cachedTextHeight != null) return display.cachedTextHeight; + if (measureText == null) { + measureText = elt("pre"); + // Measure a bunch of lines, for browsers that compute + // fractional heights. + for (var i = 0; i < 49; ++i) { + measureText.appendChild(document.createTextNode("x")); + measureText.appendChild(elt("br")); + } + measureText.appendChild(document.createTextNode("x")); + } + removeChildrenAndAdd(display.measure, measureText); + var height = measureText.offsetHeight / 50; + if (height > 3) display.cachedTextHeight = height; + removeChildren(display.measure); + return height || 1; + } + + // Compute the default character width. + function charWidth(display) { + if (display.cachedCharWidth != null) return display.cachedCharWidth; + var anchor = elt("span", "xxxxxxxxxx"); + var pre = elt("pre", [anchor]); + removeChildrenAndAdd(display.measure, pre); + var rect = anchor.getBoundingClientRect(), width = (rect.right - rect.left) / 10; + if (width > 2) display.cachedCharWidth = width; + return width || 10; + } + + // OPERATIONS + + // Operations are used to wrap a series of changes to the editor + // state in such a way that each change won't have to update the + // cursor and display (which would be awkward, slow, and + // error-prone). Instead, display updates are batched and then all + // combined and executed at once. + + var operationGroup = null; + + var nextOpId = 0; + // Start a new operation. + function startOperation(cm) { + cm.curOp = { + cm: cm, + viewChanged: false, // Flag that indicates that lines might need to be redrawn + startHeight: cm.doc.height, // Used to detect need to update scrollbar + forceUpdate: false, // Used to force a redraw + updateInput: null, // Whether to reset the input textarea + typing: false, // Whether this reset should be careful to leave existing text (for compositing) + changeObjs: null, // Accumulated changes, for firing change events + cursorActivityHandlers: null, // Set of handlers to fire cursorActivity on + cursorActivityCalled: 0, // Tracks which cursorActivity handlers have been called already + selectionChanged: false, // Whether the selection needs to be redrawn + updateMaxLine: false, // Set when the widest line needs to be determined anew + scrollLeft: null, scrollTop: null, // Intermediate scroll position, not pushed to DOM yet + scrollToPos: null, // Used to scroll to a specific position + focus: false, + id: ++nextOpId // Unique ID + }; + if (operationGroup) { + operationGroup.ops.push(cm.curOp); + } else { + cm.curOp.ownsGroup = operationGroup = { + ops: [cm.curOp], + delayedCallbacks: [] + }; + } + } + + function fireCallbacksForOps(group) { + // Calls delayed callbacks and cursorActivity handlers until no + // new ones appear + var callbacks = group.delayedCallbacks, i = 0; + do { + for (; i < callbacks.length; i++) + callbacks[i](); + for (var j = 0; j < group.ops.length; j++) { + var op = group.ops[j]; + if (op.cursorActivityHandlers) + while (op.cursorActivityCalled < op.cursorActivityHandlers.length) + op.cursorActivityHandlers[op.cursorActivityCalled++](op.cm); + } + } while (i < callbacks.length); + } + + // Finish an operation, updating the display and signalling delayed events + function endOperation(cm) { + var op = cm.curOp, group = op.ownsGroup; + if (!group) return; + + try { fireCallbacksForOps(group); } + finally { + operationGroup = null; + for (var i = 0; i < group.ops.length; i++) + group.ops[i].cm.curOp = null; + endOperations(group); + } + } + + // The DOM updates done when an operation finishes are batched so + // that the minimum number of relayouts are required. + function endOperations(group) { + var ops = group.ops; + for (var i = 0; i < ops.length; i++) // Read DOM + endOperation_R1(ops[i]); + for (var i = 0; i < ops.length; i++) // Write DOM (maybe) + endOperation_W1(ops[i]); + for (var i = 0; i < ops.length; i++) // Read DOM + endOperation_R2(ops[i]); + for (var i = 0; i < ops.length; i++) // Write DOM (maybe) + endOperation_W2(ops[i]); + for (var i = 0; i < ops.length; i++) // Read DOM + endOperation_finish(ops[i]); + } + + function endOperation_R1(op) { + var cm = op.cm, display = cm.display; + maybeClipScrollbars(cm); + if (op.updateMaxLine) findMaxLine(cm); + + op.mustUpdate = op.viewChanged || op.forceUpdate || op.scrollTop != null || + op.scrollToPos && (op.scrollToPos.from.line < display.viewFrom || + op.scrollToPos.to.line >= display.viewTo) || + display.maxLineChanged && cm.options.lineWrapping; + op.update = op.mustUpdate && + new DisplayUpdate(cm, op.mustUpdate && {top: op.scrollTop, ensure: op.scrollToPos}, op.forceUpdate); + } + + function endOperation_W1(op) { + op.updatedDisplay = op.mustUpdate && updateDisplayIfNeeded(op.cm, op.update); + } + + function endOperation_R2(op) { + var cm = op.cm, display = cm.display; + if (op.updatedDisplay) updateHeightsInViewport(cm); + + op.barMeasure = measureForScrollbars(cm); + + // If the max line changed since it was last measured, measure it, + // and ensure the document's width matches it. + // updateDisplay_W2 will use these properties to do the actual resizing + if (display.maxLineChanged && !cm.options.lineWrapping) { + op.adjustWidthTo = measureChar(cm, display.maxLine, display.maxLine.text.length).left + 3; + cm.display.sizerWidth = op.adjustWidthTo; + op.barMeasure.scrollWidth = + Math.max(display.scroller.clientWidth, display.sizer.offsetLeft + op.adjustWidthTo + scrollGap(cm) + cm.display.barWidth); + op.maxScrollLeft = Math.max(0, display.sizer.offsetLeft + op.adjustWidthTo - displayWidth(cm)); + } + + if (op.updatedDisplay || op.selectionChanged) + op.preparedSelection = display.input.prepareSelection(); + } + + function endOperation_W2(op) { + var cm = op.cm; + + if (op.adjustWidthTo != null) { + cm.display.sizer.style.minWidth = op.adjustWidthTo + "px"; + if (op.maxScrollLeft < cm.doc.scrollLeft) + setScrollLeft(cm, Math.min(cm.display.scroller.scrollLeft, op.maxScrollLeft), true); + cm.display.maxLineChanged = false; + } + + if (op.preparedSelection) + cm.display.input.showSelection(op.preparedSelection); + if (op.updatedDisplay) + setDocumentHeight(cm, op.barMeasure); + if (op.updatedDisplay || op.startHeight != cm.doc.height) + updateScrollbars(cm, op.barMeasure); + + if (op.selectionChanged) restartBlink(cm); + + if (cm.state.focused && op.updateInput) + cm.display.input.reset(op.typing); + if (op.focus && op.focus == activeElt()) ensureFocus(op.cm); + } + + function endOperation_finish(op) { + var cm = op.cm, display = cm.display, doc = cm.doc; + + if (op.updatedDisplay) postUpdateDisplay(cm, op.update); + + // Abort mouse wheel delta measurement, when scrolling explicitly + if (display.wheelStartX != null && (op.scrollTop != null || op.scrollLeft != null || op.scrollToPos)) + display.wheelStartX = display.wheelStartY = null; + + // Propagate the scroll position to the actual DOM scroller + if (op.scrollTop != null && (display.scroller.scrollTop != op.scrollTop || op.forceScroll)) { + doc.scrollTop = Math.max(0, Math.min(display.scroller.scrollHeight - display.scroller.clientHeight, op.scrollTop)); + display.scrollbars.setScrollTop(doc.scrollTop); + display.scroller.scrollTop = doc.scrollTop; + } + if (op.scrollLeft != null && (display.scroller.scrollLeft != op.scrollLeft || op.forceScroll)) { + doc.scrollLeft = Math.max(0, Math.min(display.scroller.scrollWidth - displayWidth(cm), op.scrollLeft)); + display.scrollbars.setScrollLeft(doc.scrollLeft); + display.scroller.scrollLeft = doc.scrollLeft; + alignHorizontally(cm); + } + // If we need to scroll a specific position into view, do so. + if (op.scrollToPos) { + var coords = scrollPosIntoView(cm, clipPos(doc, op.scrollToPos.from), + clipPos(doc, op.scrollToPos.to), op.scrollToPos.margin); + if (op.scrollToPos.isCursor && cm.state.focused) maybeScrollWindow(cm, coords); + } + + // Fire events for markers that are hidden/unidden by editing or + // undoing + var hidden = op.maybeHiddenMarkers, unhidden = op.maybeUnhiddenMarkers; + if (hidden) for (var i = 0; i < hidden.length; ++i) + if (!hidden[i].lines.length) signal(hidden[i], "hide"); + if (unhidden) for (var i = 0; i < unhidden.length; ++i) + if (unhidden[i].lines.length) signal(unhidden[i], "unhide"); + + if (display.wrapper.offsetHeight) + doc.scrollTop = cm.display.scroller.scrollTop; + + // Fire change events, and delayed event handlers + if (op.changeObjs) + signal(cm, "changes", cm, op.changeObjs); + if (op.update) + op.update.finish(); + } + + // Run the given function in an operation + function runInOp(cm, f) { + if (cm.curOp) return f(); + startOperation(cm); + try { return f(); } + finally { endOperation(cm); } + } + // Wraps a function in an operation. Returns the wrapped function. + function operation(cm, f) { + return function() { + if (cm.curOp) return f.apply(cm, arguments); + startOperation(cm); + try { return f.apply(cm, arguments); } + finally { endOperation(cm); } + }; + } + // Used to add methods to editor and doc instances, wrapping them in + // operations. + function methodOp(f) { + return function() { + if (this.curOp) return f.apply(this, arguments); + startOperation(this); + try { return f.apply(this, arguments); } + finally { endOperation(this); } + }; + } + function docMethodOp(f) { + return function() { + var cm = this.cm; + if (!cm || cm.curOp) return f.apply(this, arguments); + startOperation(cm); + try { return f.apply(this, arguments); } + finally { endOperation(cm); } + }; + } + + // VIEW TRACKING + + // These objects are used to represent the visible (currently drawn) + // part of the document. A LineView may correspond to multiple + // logical lines, if those are connected by collapsed ranges. + function LineView(doc, line, lineN) { + // The starting line + this.line = line; + // Continuing lines, if any + this.rest = visualLineContinued(line); + // Number of logical lines in this visual line + this.size = this.rest ? lineNo(lst(this.rest)) - lineN + 1 : 1; + this.node = this.text = null; + this.hidden = lineIsHidden(doc, line); + } + + // Create a range of LineView objects for the given lines. + function buildViewArray(cm, from, to) { + var array = [], nextPos; + for (var pos = from; pos < to; pos = nextPos) { + var view = new LineView(cm.doc, getLine(cm.doc, pos), pos); + nextPos = pos + view.size; + array.push(view); + } + return array; + } + + // Updates the display.view data structure for a given change to the + // document. From and to are in pre-change coordinates. Lendiff is + // the amount of lines added or subtracted by the change. This is + // used for changes that span multiple lines, or change the way + // lines are divided into visual lines. regLineChange (below) + // registers single-line changes. + function regChange(cm, from, to, lendiff) { + if (from == null) from = cm.doc.first; + if (to == null) to = cm.doc.first + cm.doc.size; + if (!lendiff) lendiff = 0; + + var display = cm.display; + if (lendiff && to < display.viewTo && + (display.updateLineNumbers == null || display.updateLineNumbers > from)) + display.updateLineNumbers = from; + + cm.curOp.viewChanged = true; + + if (from >= display.viewTo) { // Change after + if (sawCollapsedSpans && visualLineNo(cm.doc, from) < display.viewTo) + resetView(cm); + } else if (to <= display.viewFrom) { // Change before + if (sawCollapsedSpans && visualLineEndNo(cm.doc, to + lendiff) > display.viewFrom) { + resetView(cm); + } else { + display.viewFrom += lendiff; + display.viewTo += lendiff; + } + } else if (from <= display.viewFrom && to >= display.viewTo) { // Full overlap + resetView(cm); + } else if (from <= display.viewFrom) { // Top overlap + var cut = viewCuttingPoint(cm, to, to + lendiff, 1); + if (cut) { + display.view = display.view.slice(cut.index); + display.viewFrom = cut.lineN; + display.viewTo += lendiff; + } else { + resetView(cm); + } + } else if (to >= display.viewTo) { // Bottom overlap + var cut = viewCuttingPoint(cm, from, from, -1); + if (cut) { + display.view = display.view.slice(0, cut.index); + display.viewTo = cut.lineN; + } else { + resetView(cm); + } + } else { // Gap in the middle + var cutTop = viewCuttingPoint(cm, from, from, -1); + var cutBot = viewCuttingPoint(cm, to, to + lendiff, 1); + if (cutTop && cutBot) { + display.view = display.view.slice(0, cutTop.index) + .concat(buildViewArray(cm, cutTop.lineN, cutBot.lineN)) + .concat(display.view.slice(cutBot.index)); + display.viewTo += lendiff; + } else { + resetView(cm); + } + } + + var ext = display.externalMeasured; + if (ext) { + if (to < ext.lineN) + ext.lineN += lendiff; + else if (from < ext.lineN + ext.size) + display.externalMeasured = null; + } + } + + // Register a change to a single line. Type must be one of "text", + // "gutter", "class", "widget" + function regLineChange(cm, line, type) { + cm.curOp.viewChanged = true; + var display = cm.display, ext = cm.display.externalMeasured; + if (ext && line >= ext.lineN && line < ext.lineN + ext.size) + display.externalMeasured = null; + + if (line < display.viewFrom || line >= display.viewTo) return; + var lineView = display.view[findViewIndex(cm, line)]; + if (lineView.node == null) return; + var arr = lineView.changes || (lineView.changes = []); + if (indexOf(arr, type) == -1) arr.push(type); + } + + // Clear the view. + function resetView(cm) { + cm.display.viewFrom = cm.display.viewTo = cm.doc.first; + cm.display.view = []; + cm.display.viewOffset = 0; + } + + // Find the view element corresponding to a given line. Return null + // when the line isn't visible. + function findViewIndex(cm, n) { + if (n >= cm.display.viewTo) return null; + n -= cm.display.viewFrom; + if (n < 0) return null; + var view = cm.display.view; + for (var i = 0; i < view.length; i++) { + n -= view[i].size; + if (n < 0) return i; + } + } + + function viewCuttingPoint(cm, oldN, newN, dir) { + var index = findViewIndex(cm, oldN), diff, view = cm.display.view; + if (!sawCollapsedSpans || newN == cm.doc.first + cm.doc.size) + return {index: index, lineN: newN}; + for (var i = 0, n = cm.display.viewFrom; i < index; i++) + n += view[i].size; + if (n != oldN) { + if (dir > 0) { + if (index == view.length - 1) return null; + diff = (n + view[index].size) - oldN; + index++; + } else { + diff = n - oldN; + } + oldN += diff; newN += diff; + } + while (visualLineNo(cm.doc, newN) != newN) { + if (index == (dir < 0 ? 0 : view.length - 1)) return null; + newN += dir * view[index - (dir < 0 ? 1 : 0)].size; + index += dir; + } + return {index: index, lineN: newN}; + } + + // Force the view to cover a given range, adding empty view element + // or clipping off existing ones as needed. + function adjustView(cm, from, to) { + var display = cm.display, view = display.view; + if (view.length == 0 || from >= display.viewTo || to <= display.viewFrom) { + display.view = buildViewArray(cm, from, to); + display.viewFrom = from; + } else { + if (display.viewFrom > from) + display.view = buildViewArray(cm, from, display.viewFrom).concat(display.view); + else if (display.viewFrom < from) + display.view = display.view.slice(findViewIndex(cm, from)); + display.viewFrom = from; + if (display.viewTo < to) + display.view = display.view.concat(buildViewArray(cm, display.viewTo, to)); + else if (display.viewTo > to) + display.view = display.view.slice(0, findViewIndex(cm, to)); + } + display.viewTo = to; + } + + // Count the number of lines in the view whose DOM representation is + // out of date (or nonexistent). + function countDirtyView(cm) { + var view = cm.display.view, dirty = 0; + for (var i = 0; i < view.length; i++) { + var lineView = view[i]; + if (!lineView.hidden && (!lineView.node || lineView.changes)) ++dirty; + } + return dirty; + } + + // EVENT HANDLERS + + // Attach the necessary event handlers when initializing the editor + function registerEventHandlers(cm) { + var d = cm.display; + on(d.scroller, "mousedown", operation(cm, onMouseDown)); + // Older IE's will not fire a second mousedown for a double click + if (ie && ie_version < 11) + on(d.scroller, "dblclick", operation(cm, function(e) { + if (signalDOMEvent(cm, e)) return; + var pos = posFromMouse(cm, e); + if (!pos || clickInGutter(cm, e) || eventInWidget(cm.display, e)) return; + e_preventDefault(e); + var word = cm.findWordAt(pos); + extendSelection(cm.doc, word.anchor, word.head); + })); + else + on(d.scroller, "dblclick", function(e) { signalDOMEvent(cm, e) || e_preventDefault(e); }); + // Some browsers fire contextmenu *after* opening the menu, at + // which point we can't mess with it anymore. Context menu is + // handled in onMouseDown for these browsers. + if (!captureRightClick) on(d.scroller, "contextmenu", function(e) {onContextMenu(cm, e);}); + + // Used to suppress mouse event handling when a touch happens + var touchFinished, prevTouch = {end: 0}; + function finishTouch() { + if (d.activeTouch) { + touchFinished = setTimeout(function() {d.activeTouch = null;}, 1000); + prevTouch = d.activeTouch; + prevTouch.end = +new Date; + } + }; + function isMouseLikeTouchEvent(e) { + if (e.touches.length != 1) return false; + var touch = e.touches[0]; + return touch.radiusX <= 1 && touch.radiusY <= 1; + } + function farAway(touch, other) { + if (other.left == null) return true; + var dx = other.left - touch.left, dy = other.top - touch.top; + return dx * dx + dy * dy > 20 * 20; + } + on(d.scroller, "touchstart", function(e) { + if (!isMouseLikeTouchEvent(e)) { + clearTimeout(touchFinished); + var now = +new Date; + d.activeTouch = {start: now, moved: false, + prev: now - prevTouch.end <= 300 ? prevTouch : null}; + if (e.touches.length == 1) { + d.activeTouch.left = e.touches[0].pageX; + d.activeTouch.top = e.touches[0].pageY; + } + } + }); + on(d.scroller, "touchmove", function() { + if (d.activeTouch) d.activeTouch.moved = true; + }); + on(d.scroller, "touchend", function(e) { + var touch = d.activeTouch; + if (touch && !eventInWidget(d, e) && touch.left != null && + !touch.moved && new Date - touch.start < 300) { + var pos = cm.coordsChar(d.activeTouch, "page"), range; + if (!touch.prev || farAway(touch, touch.prev)) // Single tap + range = new Range(pos, pos); + else if (!touch.prev.prev || farAway(touch, touch.prev.prev)) // Double tap + range = cm.findWordAt(pos); + else // Triple tap + range = new Range(Pos(pos.line, 0), clipPos(cm.doc, Pos(pos.line + 1, 0))); + cm.setSelection(range.anchor, range.head); + cm.focus(); + e_preventDefault(e); + } + finishTouch(); + }); + on(d.scroller, "touchcancel", finishTouch); + + // Sync scrolling between fake scrollbars and real scrollable + // area, ensure viewport is updated when scrolling. + on(d.scroller, "scroll", function() { + if (d.scroller.clientHeight) { + setScrollTop(cm, d.scroller.scrollTop); + setScrollLeft(cm, d.scroller.scrollLeft, true); + signal(cm, "scroll", cm); + } + }); + + // Listen to wheel events in order to try and update the viewport on time. + on(d.scroller, "mousewheel", function(e){onScrollWheel(cm, e);}); + on(d.scroller, "DOMMouseScroll", function(e){onScrollWheel(cm, e);}); + + // Prevent wrapper from ever scrolling + on(d.wrapper, "scroll", function() { d.wrapper.scrollTop = d.wrapper.scrollLeft = 0; }); + + d.dragFunctions = { + simple: function(e) {if (!signalDOMEvent(cm, e)) e_stop(e);}, + start: function(e){onDragStart(cm, e);}, + drop: operation(cm, onDrop) + }; + + var inp = d.input.getField(); + on(inp, "keyup", function(e) { onKeyUp.call(cm, e); }); + on(inp, "keydown", operation(cm, onKeyDown)); + on(inp, "keypress", operation(cm, onKeyPress)); + on(inp, "focus", bind(onFocus, cm)); + on(inp, "blur", bind(onBlur, cm)); + } + + function dragDropChanged(cm, value, old) { + var wasOn = old && old != CodeMirror.Init; + if (!value != !wasOn) { + var funcs = cm.display.dragFunctions; + var toggle = value ? on : off; + toggle(cm.display.scroller, "dragstart", funcs.start); + toggle(cm.display.scroller, "dragenter", funcs.simple); + toggle(cm.display.scroller, "dragover", funcs.simple); + toggle(cm.display.scroller, "drop", funcs.drop); + } + } + + // Called when the window resizes + function onResize(cm) { + var d = cm.display; + if (d.lastWrapHeight == d.wrapper.clientHeight && d.lastWrapWidth == d.wrapper.clientWidth) + return; + // Might be a text scaling operation, clear size caches. + d.cachedCharWidth = d.cachedTextHeight = d.cachedPaddingH = null; + d.scrollbarsClipped = false; + cm.setSize(); + } + + // MOUSE EVENTS + + // Return true when the given mouse event happened in a widget + function eventInWidget(display, e) { + for (var n = e_target(e); n != display.wrapper; n = n.parentNode) { + if (!n || (n.nodeType == 1 && n.getAttribute("cm-ignore-events") == "true") || + (n.parentNode == display.sizer && n != display.mover)) + return true; + } + } + + // Given a mouse event, find the corresponding position. If liberal + // is false, it checks whether a gutter or scrollbar was clicked, + // and returns null if it was. forRect is used by rectangular + // selections, and tries to estimate a character position even for + // coordinates beyond the right of the text. + function posFromMouse(cm, e, liberal, forRect) { + var display = cm.display; + if (!liberal && e_target(e).getAttribute("cm-not-content") == "true") return null; + + var x, y, space = display.lineSpace.getBoundingClientRect(); + // Fails unpredictably on IE[67] when mouse is dragged around quickly. + try { x = e.clientX - space.left; y = e.clientY - space.top; } + catch (e) { return null; } + var coords = coordsChar(cm, x, y), line; + if (forRect && coords.xRel == 1 && (line = getLine(cm.doc, coords.line).text).length == coords.ch) { + var colDiff = countColumn(line, line.length, cm.options.tabSize) - line.length; + coords = Pos(coords.line, Math.max(0, Math.round((x - paddingH(cm.display).left) / charWidth(cm.display)) - colDiff)); + } + return coords; + } + + // A mouse down can be a single click, double click, triple click, + // start of selection drag, start of text drag, new cursor + // (ctrl-click), rectangle drag (alt-drag), or xwin + // middle-click-paste. Or it might be a click on something we should + // not interfere with, such as a scrollbar or widget. + function onMouseDown(e) { + var cm = this, display = cm.display; + if (display.activeTouch && display.input.supportsTouch() || signalDOMEvent(cm, e)) return; + display.shift = e.shiftKey; + + if (eventInWidget(display, e)) { + if (!webkit) { + // Briefly turn off draggability, to allow widgets to do + // normal dragging things. + display.scroller.draggable = false; + setTimeout(function(){display.scroller.draggable = true;}, 100); + } + return; + } + if (clickInGutter(cm, e)) return; + var start = posFromMouse(cm, e); + window.focus(); + + switch (e_button(e)) { + case 1: + if (start) + leftButtonDown(cm, e, start); + else if (e_target(e) == display.scroller) + e_preventDefault(e); + break; + case 2: + if (webkit) cm.state.lastMiddleDown = +new Date; + if (start) extendSelection(cm.doc, start); + setTimeout(function() {display.input.focus();}, 20); + e_preventDefault(e); + break; + case 3: + if (captureRightClick) onContextMenu(cm, e); + else delayBlurEvent(cm); + break; + } + } + + var lastClick, lastDoubleClick; + function leftButtonDown(cm, e, start) { + if (ie) setTimeout(bind(ensureFocus, cm), 0); + else cm.curOp.focus = activeElt(); + + var now = +new Date, type; + if (lastDoubleClick && lastDoubleClick.time > now - 400 && cmp(lastDoubleClick.pos, start) == 0) { + type = "triple"; + } else if (lastClick && lastClick.time > now - 400 && cmp(lastClick.pos, start) == 0) { + type = "double"; + lastDoubleClick = {time: now, pos: start}; + } else { + type = "single"; + lastClick = {time: now, pos: start}; + } + + var sel = cm.doc.sel, modifier = mac ? e.metaKey : e.ctrlKey, contained; + if (cm.options.dragDrop && dragAndDrop && !isReadOnly(cm) && + type == "single" && (contained = sel.contains(start)) > -1 && + !sel.ranges[contained].empty()) + leftButtonStartDrag(cm, e, start, modifier); + else + leftButtonSelect(cm, e, start, type, modifier); + } + + // Start a text drag. When it ends, see if any dragging actually + // happen, and treat as a click if it didn't. + function leftButtonStartDrag(cm, e, start, modifier) { + var display = cm.display, startTime = +new Date; + var dragEnd = operation(cm, function(e2) { + if (webkit) display.scroller.draggable = false; + cm.state.draggingText = false; + off(document, "mouseup", dragEnd); + off(display.scroller, "drop", dragEnd); + if (Math.abs(e.clientX - e2.clientX) + Math.abs(e.clientY - e2.clientY) < 10) { + e_preventDefault(e2); + if (!modifier && +new Date - 200 < startTime) + extendSelection(cm.doc, start); + // Work around unexplainable focus problem in IE9 (#2127) and Chrome (#3081) + if (webkit || ie && ie_version == 9) + setTimeout(function() {document.body.focus(); display.input.focus();}, 20); + else + display.input.focus(); + } + }); + // Let the drag handler handle this. + if (webkit) display.scroller.draggable = true; + cm.state.draggingText = dragEnd; + // IE's approach to draggable + if (display.scroller.dragDrop) display.scroller.dragDrop(); + on(document, "mouseup", dragEnd); + on(display.scroller, "drop", dragEnd); + } + + // Normal selection, as opposed to text dragging. + function leftButtonSelect(cm, e, start, type, addNew) { + var display = cm.display, doc = cm.doc; + e_preventDefault(e); + + var ourRange, ourIndex, startSel = doc.sel, ranges = startSel.ranges; + if (addNew && !e.shiftKey) { + ourIndex = doc.sel.contains(start); + if (ourIndex > -1) + ourRange = ranges[ourIndex]; + else + ourRange = new Range(start, start); + } else { + ourRange = doc.sel.primary(); + ourIndex = doc.sel.primIndex; + } + + if (e.altKey) { + type = "rect"; + if (!addNew) ourRange = new Range(start, start); + start = posFromMouse(cm, e, true, true); + ourIndex = -1; + } else if (type == "double") { + var word = cm.findWordAt(start); + if (cm.display.shift || doc.extend) + ourRange = extendRange(doc, ourRange, word.anchor, word.head); + else + ourRange = word; + } else if (type == "triple") { + var line = new Range(Pos(start.line, 0), clipPos(doc, Pos(start.line + 1, 0))); + if (cm.display.shift || doc.extend) + ourRange = extendRange(doc, ourRange, line.anchor, line.head); + else + ourRange = line; + } else { + ourRange = extendRange(doc, ourRange, start); + } + + if (!addNew) { + ourIndex = 0; + setSelection(doc, new Selection([ourRange], 0), sel_mouse); + startSel = doc.sel; + } else if (ourIndex == -1) { + ourIndex = ranges.length; + setSelection(doc, normalizeSelection(ranges.concat([ourRange]), ourIndex), + {scroll: false, origin: "*mouse"}); + } else if (ranges.length > 1 && ranges[ourIndex].empty() && type == "single" && !e.shiftKey) { + setSelection(doc, normalizeSelection(ranges.slice(0, ourIndex).concat(ranges.slice(ourIndex + 1)), 0)); + startSel = doc.sel; + } else { + replaceOneSelection(doc, ourIndex, ourRange, sel_mouse); + } + + var lastPos = start; + function extendTo(pos) { + if (cmp(lastPos, pos) == 0) return; + lastPos = pos; + + if (type == "rect") { + var ranges = [], tabSize = cm.options.tabSize; + var startCol = countColumn(getLine(doc, start.line).text, start.ch, tabSize); + var posCol = countColumn(getLine(doc, pos.line).text, pos.ch, tabSize); + var left = Math.min(startCol, posCol), right = Math.max(startCol, posCol); + for (var line = Math.min(start.line, pos.line), end = Math.min(cm.lastLine(), Math.max(start.line, pos.line)); + line <= end; line++) { + var text = getLine(doc, line).text, leftPos = findColumn(text, left, tabSize); + if (left == right) + ranges.push(new Range(Pos(line, leftPos), Pos(line, leftPos))); + else if (text.length > leftPos) + ranges.push(new Range(Pos(line, leftPos), Pos(line, findColumn(text, right, tabSize)))); + } + if (!ranges.length) ranges.push(new Range(start, start)); + setSelection(doc, normalizeSelection(startSel.ranges.slice(0, ourIndex).concat(ranges), ourIndex), + {origin: "*mouse", scroll: false}); + cm.scrollIntoView(pos); + } else { + var oldRange = ourRange; + var anchor = oldRange.anchor, head = pos; + if (type != "single") { + if (type == "double") + var range = cm.findWordAt(pos); + else + var range = new Range(Pos(pos.line, 0), clipPos(doc, Pos(pos.line + 1, 0))); + if (cmp(range.anchor, anchor) > 0) { + head = range.head; + anchor = minPos(oldRange.from(), range.anchor); + } else { + head = range.anchor; + anchor = maxPos(oldRange.to(), range.head); + } + } + var ranges = startSel.ranges.slice(0); + ranges[ourIndex] = new Range(clipPos(doc, anchor), head); + setSelection(doc, normalizeSelection(ranges, ourIndex), sel_mouse); + } + } + + var editorSize = display.wrapper.getBoundingClientRect(); + // Used to ensure timeout re-tries don't fire when another extend + // happened in the meantime (clearTimeout isn't reliable -- at + // least on Chrome, the timeouts still happen even when cleared, + // if the clear happens after their scheduled firing time). + var counter = 0; + + function extend(e) { + var curCount = ++counter; + var cur = posFromMouse(cm, e, true, type == "rect"); + if (!cur) return; + if (cmp(cur, lastPos) != 0) { + cm.curOp.focus = activeElt(); + extendTo(cur); + var visible = visibleLines(display, doc); + if (cur.line >= visible.to || cur.line < visible.from) + setTimeout(operation(cm, function(){if (counter == curCount) extend(e);}), 150); + } else { + var outside = e.clientY < editorSize.top ? -20 : e.clientY > editorSize.bottom ? 20 : 0; + if (outside) setTimeout(operation(cm, function() { + if (counter != curCount) return; + display.scroller.scrollTop += outside; + extend(e); + }), 50); + } + } + + function done(e) { + counter = Infinity; + e_preventDefault(e); + display.input.focus(); + off(document, "mousemove", move); + off(document, "mouseup", up); + doc.history.lastSelOrigin = null; + } + + var move = operation(cm, function(e) { + if (!e_button(e)) done(e); + else extend(e); + }); + var up = operation(cm, done); + on(document, "mousemove", move); + on(document, "mouseup", up); + } + + // Determines whether an event happened in the gutter, and fires the + // handlers for the corresponding event. + function gutterEvent(cm, e, type, prevent, signalfn) { + try { var mX = e.clientX, mY = e.clientY; } + catch(e) { return false; } + if (mX >= Math.floor(cm.display.gutters.getBoundingClientRect().right)) return false; + if (prevent) e_preventDefault(e); + + var display = cm.display; + var lineBox = display.lineDiv.getBoundingClientRect(); + + if (mY > lineBox.bottom || !hasHandler(cm, type)) return e_defaultPrevented(e); + mY -= lineBox.top - display.viewOffset; + + for (var i = 0; i < cm.options.gutters.length; ++i) { + var g = display.gutters.childNodes[i]; + if (g && g.getBoundingClientRect().right >= mX) { + var line = lineAtHeight(cm.doc, mY); + var gutter = cm.options.gutters[i]; + signalfn(cm, type, cm, line, gutter, e); + return e_defaultPrevented(e); + } + } + } + + function clickInGutter(cm, e) { + return gutterEvent(cm, e, "gutterClick", true, signalLater); + } + + // Kludge to work around strange IE behavior where it'll sometimes + // re-fire a series of drag-related events right after the drop (#1551) + var lastDrop = 0; + + function onDrop(e) { + var cm = this; + if (signalDOMEvent(cm, e) || eventInWidget(cm.display, e)) + return; + e_preventDefault(e); + if (ie) lastDrop = +new Date; + var pos = posFromMouse(cm, e, true), files = e.dataTransfer.files; + if (!pos || isReadOnly(cm)) return; + // Might be a file drop, in which case we simply extract the text + // and insert it. + if (files && files.length && window.FileReader && window.File) { + var n = files.length, text = Array(n), read = 0; + var loadFile = function(file, i) { + var reader = new FileReader; + reader.onload = operation(cm, function() { + text[i] = reader.result; + if (++read == n) { + pos = clipPos(cm.doc, pos); + var change = {from: pos, to: pos, text: splitLines(text.join("\n")), origin: "paste"}; + makeChange(cm.doc, change); + setSelectionReplaceHistory(cm.doc, simpleSelection(pos, changeEnd(change))); + } + }); + reader.readAsText(file); + }; + for (var i = 0; i < n; ++i) loadFile(files[i], i); + } else { // Normal drop + // Don't do a replace if the drop happened inside of the selected text. + if (cm.state.draggingText && cm.doc.sel.contains(pos) > -1) { + cm.state.draggingText(e); + // Ensure the editor is re-focused + setTimeout(function() {cm.display.input.focus();}, 20); + return; + } + try { + var text = e.dataTransfer.getData("Text"); + if (text) { + if (cm.state.draggingText && !(mac ? e.altKey : e.ctrlKey)) + var selected = cm.listSelections(); + setSelectionNoUndo(cm.doc, simpleSelection(pos, pos)); + if (selected) for (var i = 0; i < selected.length; ++i) + replaceRange(cm.doc, "", selected[i].anchor, selected[i].head, "drag"); + cm.replaceSelection(text, "around", "paste"); + cm.display.input.focus(); + } + } + catch(e){} + } + } + + function onDragStart(cm, e) { + if (ie && (!cm.state.draggingText || +new Date - lastDrop < 100)) { e_stop(e); return; } + if (signalDOMEvent(cm, e) || eventInWidget(cm.display, e)) return; + + e.dataTransfer.setData("Text", cm.getSelection()); + + // Use dummy image instead of default browsers image. + // Recent Safari (~6.0.2) have a tendency to segfault when this happens, so we don't do it there. + if (e.dataTransfer.setDragImage && !safari) { + var img = elt("img", null, null, "position: fixed; left: 0; top: 0;"); + img.src = "data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw=="; + if (presto) { + img.width = img.height = 1; + cm.display.wrapper.appendChild(img); + // Force a relayout, or Opera won't use our image for some obscure reason + img._top = img.offsetTop; + } + e.dataTransfer.setDragImage(img, 0, 0); + if (presto) img.parentNode.removeChild(img); + } + } + + // SCROLL EVENTS + + // Sync the scrollable area and scrollbars, ensure the viewport + // covers the visible area. + function setScrollTop(cm, val) { + if (Math.abs(cm.doc.scrollTop - val) < 2) return; + cm.doc.scrollTop = val; + if (!gecko) updateDisplaySimple(cm, {top: val}); + if (cm.display.scroller.scrollTop != val) cm.display.scroller.scrollTop = val; + cm.display.scrollbars.setScrollTop(val); + if (gecko) updateDisplaySimple(cm); + startWorker(cm, 100); + } + // Sync scroller and scrollbar, ensure the gutter elements are + // aligned. + function setScrollLeft(cm, val, isScroller) { + if (isScroller ? val == cm.doc.scrollLeft : Math.abs(cm.doc.scrollLeft - val) < 2) return; + val = Math.min(val, cm.display.scroller.scrollWidth - cm.display.scroller.clientWidth); + cm.doc.scrollLeft = val; + alignHorizontally(cm); + if (cm.display.scroller.scrollLeft != val) cm.display.scroller.scrollLeft = val; + cm.display.scrollbars.setScrollLeft(val); + } + + // Since the delta values reported on mouse wheel events are + // unstandardized between browsers and even browser versions, and + // generally horribly unpredictable, this code starts by measuring + // the scroll effect that the first few mouse wheel events have, + // and, from that, detects the way it can convert deltas to pixel + // offsets afterwards. + // + // The reason we want to know the amount a wheel event will scroll + // is that it gives us a chance to update the display before the + // actual scrolling happens, reducing flickering. + + var wheelSamples = 0, wheelPixelsPerUnit = null; + // Fill in a browser-detected starting value on browsers where we + // know one. These don't have to be accurate -- the result of them + // being wrong would just be a slight flicker on the first wheel + // scroll (if it is large enough). + if (ie) wheelPixelsPerUnit = -.53; + else if (gecko) wheelPixelsPerUnit = 15; + else if (chrome) wheelPixelsPerUnit = -.7; + else if (safari) wheelPixelsPerUnit = -1/3; + + var wheelEventDelta = function(e) { + var dx = e.wheelDeltaX, dy = e.wheelDeltaY; + if (dx == null && e.detail && e.axis == e.HORIZONTAL_AXIS) dx = e.detail; + if (dy == null && e.detail && e.axis == e.VERTICAL_AXIS) dy = e.detail; + else if (dy == null) dy = e.wheelDelta; + return {x: dx, y: dy}; + }; + CodeMirror.wheelEventPixels = function(e) { + var delta = wheelEventDelta(e); + delta.x *= wheelPixelsPerUnit; + delta.y *= wheelPixelsPerUnit; + return delta; + }; + + function onScrollWheel(cm, e) { + var delta = wheelEventDelta(e), dx = delta.x, dy = delta.y; + + var display = cm.display, scroll = display.scroller; + // Quit if there's nothing to scroll here + if (!(dx && scroll.scrollWidth > scroll.clientWidth || + dy && scroll.scrollHeight > scroll.clientHeight)) return; + + // Webkit browsers on OS X abort momentum scrolls when the target + // of the scroll event is removed from the scrollable element. + // This hack (see related code in patchDisplay) makes sure the + // element is kept around. + if (dy && mac && webkit) { + outer: for (var cur = e.target, view = display.view; cur != scroll; cur = cur.parentNode) { + for (var i = 0; i < view.length; i++) { + if (view[i].node == cur) { + cm.display.currentWheelTarget = cur; + break outer; + } + } + } + } + + // On some browsers, horizontal scrolling will cause redraws to + // happen before the gutter has been realigned, causing it to + // wriggle around in a most unseemly way. When we have an + // estimated pixels/delta value, we just handle horizontal + // scrolling entirely here. It'll be slightly off from native, but + // better than glitching out. + if (dx && !gecko && !presto && wheelPixelsPerUnit != null) { + if (dy) + setScrollTop(cm, Math.max(0, Math.min(scroll.scrollTop + dy * wheelPixelsPerUnit, scroll.scrollHeight - scroll.clientHeight))); + setScrollLeft(cm, Math.max(0, Math.min(scroll.scrollLeft + dx * wheelPixelsPerUnit, scroll.scrollWidth - scroll.clientWidth))); + e_preventDefault(e); + display.wheelStartX = null; // Abort measurement, if in progress + return; + } + + // 'Project' the visible viewport to cover the area that is being + // scrolled into view (if we know enough to estimate it). + if (dy && wheelPixelsPerUnit != null) { + var pixels = dy * wheelPixelsPerUnit; + var top = cm.doc.scrollTop, bot = top + display.wrapper.clientHeight; + if (pixels < 0) top = Math.max(0, top + pixels - 50); + else bot = Math.min(cm.doc.height, bot + pixels + 50); + updateDisplaySimple(cm, {top: top, bottom: bot}); + } + + if (wheelSamples < 20) { + if (display.wheelStartX == null) { + display.wheelStartX = scroll.scrollLeft; display.wheelStartY = scroll.scrollTop; + display.wheelDX = dx; display.wheelDY = dy; + setTimeout(function() { + if (display.wheelStartX == null) return; + var movedX = scroll.scrollLeft - display.wheelStartX; + var movedY = scroll.scrollTop - display.wheelStartY; + var sample = (movedY && display.wheelDY && movedY / display.wheelDY) || + (movedX && display.wheelDX && movedX / display.wheelDX); + display.wheelStartX = display.wheelStartY = null; + if (!sample) return; + wheelPixelsPerUnit = (wheelPixelsPerUnit * wheelSamples + sample) / (wheelSamples + 1); + ++wheelSamples; + }, 200); + } else { + display.wheelDX += dx; display.wheelDY += dy; + } + } + } + + // KEY EVENTS + + // Run a handler that was bound to a key. + function doHandleBinding(cm, bound, dropShift) { + if (typeof bound == "string") { + bound = commands[bound]; + if (!bound) return false; + } + // Ensure previous input has been read, so that the handler sees a + // consistent view of the document + cm.display.input.ensurePolled(); + var prevShift = cm.display.shift, done = false; + try { + if (isReadOnly(cm)) cm.state.suppressEdits = true; + if (dropShift) cm.display.shift = false; + done = bound(cm) != Pass; + } finally { + cm.display.shift = prevShift; + cm.state.suppressEdits = false; + } + return done; + } + + function lookupKeyForEditor(cm, name, handle) { + for (var i = 0; i < cm.state.keyMaps.length; i++) { + var result = lookupKey(name, cm.state.keyMaps[i], handle, cm); + if (result) return result; + } + return (cm.options.extraKeys && lookupKey(name, cm.options.extraKeys, handle, cm)) + || lookupKey(name, cm.options.keyMap, handle, cm); + } + + var stopSeq = new Delayed; + function dispatchKey(cm, name, e, handle) { + var seq = cm.state.keySeq; + if (seq) { + if (isModifierKey(name)) return "handled"; + stopSeq.set(50, function() { + if (cm.state.keySeq == seq) { + cm.state.keySeq = null; + cm.display.input.reset(); + } + }); + name = seq + " " + name; + } + var result = lookupKeyForEditor(cm, name, handle); + + if (result == "multi") + cm.state.keySeq = name; + if (result == "handled") + signalLater(cm, "keyHandled", cm, name, e); + + if (result == "handled" || result == "multi") { + e_preventDefault(e); + restartBlink(cm); + } + + if (seq && !result && /\'$/.test(name)) { + e_preventDefault(e); + return true; + } + return !!result; + } + + // Handle a key from the keydown event. + function handleKeyBinding(cm, e) { + var name = keyName(e, true); + if (!name) return false; + + if (e.shiftKey && !cm.state.keySeq) { + // First try to resolve full name (including 'Shift-'). Failing + // that, see if there is a cursor-motion command (starting with + // 'go') bound to the keyname without 'Shift-'. + return dispatchKey(cm, "Shift-" + name, e, function(b) {return doHandleBinding(cm, b, true);}) + || dispatchKey(cm, name, e, function(b) { + if (typeof b == "string" ? /^go[A-Z]/.test(b) : b.motion) + return doHandleBinding(cm, b); + }); + } else { + return dispatchKey(cm, name, e, function(b) { return doHandleBinding(cm, b); }); + } + } + + // Handle a key from the keypress event + function handleCharBinding(cm, e, ch) { + return dispatchKey(cm, "'" + ch + "'", e, + function(b) { return doHandleBinding(cm, b, true); }); + } + + var lastStoppedKey = null; + function onKeyDown(e) { + var cm = this; + cm.curOp.focus = activeElt(); + if (signalDOMEvent(cm, e)) return; + // IE does strange things with escape. + if (ie && ie_version < 11 && e.keyCode == 27) e.returnValue = false; + var code = e.keyCode; + cm.display.shift = code == 16 || e.shiftKey; + var handled = handleKeyBinding(cm, e); + if (presto) { + lastStoppedKey = handled ? code : null; + // Opera has no cut event... we try to at least catch the key combo + if (!handled && code == 88 && !hasCopyEvent && (mac ? e.metaKey : e.ctrlKey)) + cm.replaceSelection("", null, "cut"); + } + + // Turn mouse into crosshair when Alt is held on Mac. + if (code == 18 && !/\bCodeMirror-crosshair\b/.test(cm.display.lineDiv.className)) + showCrossHair(cm); + } + + function showCrossHair(cm) { + var lineDiv = cm.display.lineDiv; + addClass(lineDiv, "CodeMirror-crosshair"); + + function up(e) { + if (e.keyCode == 18 || !e.altKey) { + rmClass(lineDiv, "CodeMirror-crosshair"); + off(document, "keyup", up); + off(document, "mouseover", up); + } + } + on(document, "keyup", up); + on(document, "mouseover", up); + } + + function onKeyUp(e) { + if (e.keyCode == 16) this.doc.sel.shift = false; + signalDOMEvent(this, e); + } + + function onKeyPress(e) { + var cm = this; + if (eventInWidget(cm.display, e) || signalDOMEvent(cm, e) || e.ctrlKey && !e.altKey || mac && e.metaKey) return; + var keyCode = e.keyCode, charCode = e.charCode; + if (presto && keyCode == lastStoppedKey) {lastStoppedKey = null; e_preventDefault(e); return;} + if ((presto && (!e.which || e.which < 10)) && handleKeyBinding(cm, e)) return; + var ch = String.fromCharCode(charCode == null ? keyCode : charCode); + if (handleCharBinding(cm, e, ch)) return; + cm.display.input.onKeyPress(e); + } + + // FOCUS/BLUR EVENTS + + function delayBlurEvent(cm) { + cm.state.delayingBlurEvent = true; + setTimeout(function() { + if (cm.state.delayingBlurEvent) { + cm.state.delayingBlurEvent = false; + onBlur(cm); + } + }, 100); + } + + function onFocus(cm) { + if (cm.state.delayingBlurEvent) cm.state.delayingBlurEvent = false; + + if (cm.options.readOnly == "nocursor") return; + if (!cm.state.focused) { + signal(cm, "focus", cm); + cm.state.focused = true; + addClass(cm.display.wrapper, "CodeMirror-focused"); + // This test prevents this from firing when a context + // menu is closed (since the input reset would kill the + // select-all detection hack) + if (!cm.curOp && cm.display.selForContextMenu != cm.doc.sel) { + cm.display.input.reset(); + if (webkit) setTimeout(function() { cm.display.input.reset(true); }, 20); // Issue #1730 + } + cm.display.input.receivedFocus(); + } + restartBlink(cm); + } + function onBlur(cm) { + if (cm.state.delayingBlurEvent) return; + + if (cm.state.focused) { + signal(cm, "blur", cm); + cm.state.focused = false; + rmClass(cm.display.wrapper, "CodeMirror-focused"); + } + clearInterval(cm.display.blinker); + setTimeout(function() {if (!cm.state.focused) cm.display.shift = false;}, 150); + } + + // CONTEXT MENU HANDLING + + // To make the context menu work, we need to briefly unhide the + // textarea (making it as unobtrusive as possible) to let the + // right-click take effect on it. + function onContextMenu(cm, e) { + if (eventInWidget(cm.display, e) || contextMenuInGutter(cm, e)) return; + cm.display.input.onContextMenu(e); + } + + function contextMenuInGutter(cm, e) { + if (!hasHandler(cm, "gutterContextMenu")) return false; + return gutterEvent(cm, e, "gutterContextMenu", false, signal); + } + + // UPDATING + + // Compute the position of the end of a change (its 'to' property + // refers to the pre-change end). + var changeEnd = CodeMirror.changeEnd = function(change) { + if (!change.text) return change.to; + return Pos(change.from.line + change.text.length - 1, + lst(change.text).length + (change.text.length == 1 ? change.from.ch : 0)); + }; + + // Adjust a position to refer to the post-change position of the + // same text, or the end of the change if the change covers it. + function adjustForChange(pos, change) { + if (cmp(pos, change.from) < 0) return pos; + if (cmp(pos, change.to) <= 0) return changeEnd(change); + + var line = pos.line + change.text.length - (change.to.line - change.from.line) - 1, ch = pos.ch; + if (pos.line == change.to.line) ch += changeEnd(change).ch - change.to.ch; + return Pos(line, ch); + } + + function computeSelAfterChange(doc, change) { + var out = []; + for (var i = 0; i < doc.sel.ranges.length; i++) { + var range = doc.sel.ranges[i]; + out.push(new Range(adjustForChange(range.anchor, change), + adjustForChange(range.head, change))); + } + return normalizeSelection(out, doc.sel.primIndex); + } + + function offsetPos(pos, old, nw) { + if (pos.line == old.line) + return Pos(nw.line, pos.ch - old.ch + nw.ch); + else + return Pos(nw.line + (pos.line - old.line), pos.ch); + } + + // Used by replaceSelections to allow moving the selection to the + // start or around the replaced test. Hint may be "start" or "around". + function computeReplacedSel(doc, changes, hint) { + var out = []; + var oldPrev = Pos(doc.first, 0), newPrev = oldPrev; + for (var i = 0; i < changes.length; i++) { + var change = changes[i]; + var from = offsetPos(change.from, oldPrev, newPrev); + var to = offsetPos(changeEnd(change), oldPrev, newPrev); + oldPrev = change.to; + newPrev = to; + if (hint == "around") { + var range = doc.sel.ranges[i], inv = cmp(range.head, range.anchor) < 0; + out[i] = new Range(inv ? to : from, inv ? from : to); + } else { + out[i] = new Range(from, from); + } + } + return new Selection(out, doc.sel.primIndex); + } + + // Allow "beforeChange" event handlers to influence a change + function filterChange(doc, change, update) { + var obj = { + canceled: false, + from: change.from, + to: change.to, + text: change.text, + origin: change.origin, + cancel: function() { this.canceled = true; } + }; + if (update) obj.update = function(from, to, text, origin) { + if (from) this.from = clipPos(doc, from); + if (to) this.to = clipPos(doc, to); + if (text) this.text = text; + if (origin !== undefined) this.origin = origin; + }; + signal(doc, "beforeChange", doc, obj); + if (doc.cm) signal(doc.cm, "beforeChange", doc.cm, obj); + + if (obj.canceled) return null; + return {from: obj.from, to: obj.to, text: obj.text, origin: obj.origin}; + } + + // Apply a change to a document, and add it to the document's + // history, and propagating it to all linked documents. + function makeChange(doc, change, ignoreReadOnly) { + if (doc.cm) { + if (!doc.cm.curOp) return operation(doc.cm, makeChange)(doc, change, ignoreReadOnly); + if (doc.cm.state.suppressEdits) return; + } + + if (hasHandler(doc, "beforeChange") || doc.cm && hasHandler(doc.cm, "beforeChange")) { + change = filterChange(doc, change, true); + if (!change) return; + } + + // Possibly split or suppress the update based on the presence + // of read-only spans in its range. + var split = sawReadOnlySpans && !ignoreReadOnly && removeReadOnlyRanges(doc, change.from, change.to); + if (split) { + for (var i = split.length - 1; i >= 0; --i) + makeChangeInner(doc, {from: split[i].from, to: split[i].to, text: i ? [""] : change.text}); + } else { + makeChangeInner(doc, change); + } + } + + function makeChangeInner(doc, change) { + if (change.text.length == 1 && change.text[0] == "" && cmp(change.from, change.to) == 0) return; + var selAfter = computeSelAfterChange(doc, change); + addChangeToHistory(doc, change, selAfter, doc.cm ? doc.cm.curOp.id : NaN); + + makeChangeSingleDoc(doc, change, selAfter, stretchSpansOverChange(doc, change)); + var rebased = []; + + linkedDocs(doc, function(doc, sharedHist) { + if (!sharedHist && indexOf(rebased, doc.history) == -1) { + rebaseHist(doc.history, change); + rebased.push(doc.history); + } + makeChangeSingleDoc(doc, change, null, stretchSpansOverChange(doc, change)); + }); + } + + // Revert a change stored in a document's history. + function makeChangeFromHistory(doc, type, allowSelectionOnly) { + if (doc.cm && doc.cm.state.suppressEdits) return; + + var hist = doc.history, event, selAfter = doc.sel; + var source = type == "undo" ? hist.done : hist.undone, dest = type == "undo" ? hist.undone : hist.done; + + // Verify that there is a useable event (so that ctrl-z won't + // needlessly clear selection events) + for (var i = 0; i < source.length; i++) { + event = source[i]; + if (allowSelectionOnly ? event.ranges && !event.equals(doc.sel) : !event.ranges) + break; + } + if (i == source.length) return; + hist.lastOrigin = hist.lastSelOrigin = null; + + for (;;) { + event = source.pop(); + if (event.ranges) { + pushSelectionToHistory(event, dest); + if (allowSelectionOnly && !event.equals(doc.sel)) { + setSelection(doc, event, {clearRedo: false}); + return; + } + selAfter = event; + } + else break; + } + + // Build up a reverse change object to add to the opposite history + // stack (redo when undoing, and vice versa). + var antiChanges = []; + pushSelectionToHistory(selAfter, dest); + dest.push({changes: antiChanges, generation: hist.generation}); + hist.generation = event.generation || ++hist.maxGeneration; + + var filter = hasHandler(doc, "beforeChange") || doc.cm && hasHandler(doc.cm, "beforeChange"); + + for (var i = event.changes.length - 1; i >= 0; --i) { + var change = event.changes[i]; + change.origin = type; + if (filter && !filterChange(doc, change, false)) { + source.length = 0; + return; + } + + antiChanges.push(historyChangeFromChange(doc, change)); + + var after = i ? computeSelAfterChange(doc, change) : lst(source); + makeChangeSingleDoc(doc, change, after, mergeOldSpans(doc, change)); + if (!i && doc.cm) doc.cm.scrollIntoView({from: change.from, to: changeEnd(change)}); + var rebased = []; + + // Propagate to the linked documents + linkedDocs(doc, function(doc, sharedHist) { + if (!sharedHist && indexOf(rebased, doc.history) == -1) { + rebaseHist(doc.history, change); + rebased.push(doc.history); + } + makeChangeSingleDoc(doc, change, null, mergeOldSpans(doc, change)); + }); + } + } + + // Sub-views need their line numbers shifted when text is added + // above or below them in the parent document. + function shiftDoc(doc, distance) { + if (distance == 0) return; + doc.first += distance; + doc.sel = new Selection(map(doc.sel.ranges, function(range) { + return new Range(Pos(range.anchor.line + distance, range.anchor.ch), + Pos(range.head.line + distance, range.head.ch)); + }), doc.sel.primIndex); + if (doc.cm) { + regChange(doc.cm, doc.first, doc.first - distance, distance); + for (var d = doc.cm.display, l = d.viewFrom; l < d.viewTo; l++) + regLineChange(doc.cm, l, "gutter"); + } + } + + // More lower-level change function, handling only a single document + // (not linked ones). + function makeChangeSingleDoc(doc, change, selAfter, spans) { + if (doc.cm && !doc.cm.curOp) + return operation(doc.cm, makeChangeSingleDoc)(doc, change, selAfter, spans); + + if (change.to.line < doc.first) { + shiftDoc(doc, change.text.length - 1 - (change.to.line - change.from.line)); + return; + } + if (change.from.line > doc.lastLine()) return; + + // Clip the change to the size of this doc + if (change.from.line < doc.first) { + var shift = change.text.length - 1 - (doc.first - change.from.line); + shiftDoc(doc, shift); + change = {from: Pos(doc.first, 0), to: Pos(change.to.line + shift, change.to.ch), + text: [lst(change.text)], origin: change.origin}; + } + var last = doc.lastLine(); + if (change.to.line > last) { + change = {from: change.from, to: Pos(last, getLine(doc, last).text.length), + text: [change.text[0]], origin: change.origin}; + } + + change.removed = getBetween(doc, change.from, change.to); + + if (!selAfter) selAfter = computeSelAfterChange(doc, change); + if (doc.cm) makeChangeSingleDocInEditor(doc.cm, change, spans); + else updateDoc(doc, change, spans); + setSelectionNoUndo(doc, selAfter, sel_dontScroll); + } + + // Handle the interaction of a change to a document with the editor + // that this document is part of. + function makeChangeSingleDocInEditor(cm, change, spans) { + var doc = cm.doc, display = cm.display, from = change.from, to = change.to; + + var recomputeMaxLength = false, checkWidthStart = from.line; + if (!cm.options.lineWrapping) { + checkWidthStart = lineNo(visualLine(getLine(doc, from.line))); + doc.iter(checkWidthStart, to.line + 1, function(line) { + if (line == display.maxLine) { + recomputeMaxLength = true; + return true; + } + }); + } + + if (doc.sel.contains(change.from, change.to) > -1) + signalCursorActivity(cm); + + updateDoc(doc, change, spans, estimateHeight(cm)); + + if (!cm.options.lineWrapping) { + doc.iter(checkWidthStart, from.line + change.text.length, function(line) { + var len = lineLength(line); + if (len > display.maxLineLength) { + display.maxLine = line; + display.maxLineLength = len; + display.maxLineChanged = true; + recomputeMaxLength = false; + } + }); + if (recomputeMaxLength) cm.curOp.updateMaxLine = true; + } + + // Adjust frontier, schedule worker + doc.frontier = Math.min(doc.frontier, from.line); + startWorker(cm, 400); + + var lendiff = change.text.length - (to.line - from.line) - 1; + // Remember that these lines changed, for updating the display + if (change.full) + regChange(cm); + else if (from.line == to.line && change.text.length == 1 && !isWholeLineUpdate(cm.doc, change)) + regLineChange(cm, from.line, "text"); + else + regChange(cm, from.line, to.line + 1, lendiff); + + var changesHandler = hasHandler(cm, "changes"), changeHandler = hasHandler(cm, "change"); + if (changeHandler || changesHandler) { + var obj = { + from: from, to: to, + text: change.text, + removed: change.removed, + origin: change.origin + }; + if (changeHandler) signalLater(cm, "change", cm, obj); + if (changesHandler) (cm.curOp.changeObjs || (cm.curOp.changeObjs = [])).push(obj); + } + cm.display.selForContextMenu = null; + } + + function replaceRange(doc, code, from, to, origin) { + if (!to) to = from; + if (cmp(to, from) < 0) { var tmp = to; to = from; from = tmp; } + if (typeof code == "string") code = splitLines(code); + makeChange(doc, {from: from, to: to, text: code, origin: origin}); + } + + // SCROLLING THINGS INTO VIEW + + // If an editor sits on the top or bottom of the window, partially + // scrolled out of view, this ensures that the cursor is visible. + function maybeScrollWindow(cm, coords) { + if (signalDOMEvent(cm, "scrollCursorIntoView")) return; + + var display = cm.display, box = display.sizer.getBoundingClientRect(), doScroll = null; + if (coords.top + box.top < 0) doScroll = true; + else if (coords.bottom + box.top > (window.innerHeight || document.documentElement.clientHeight)) doScroll = false; + if (doScroll != null && !phantom) { + var scrollNode = elt("div", "\u200b", null, "position: absolute; top: " + + (coords.top - display.viewOffset - paddingTop(cm.display)) + "px; height: " + + (coords.bottom - coords.top + scrollGap(cm) + display.barHeight) + "px; left: " + + coords.left + "px; width: 2px;"); + cm.display.lineSpace.appendChild(scrollNode); + scrollNode.scrollIntoView(doScroll); + cm.display.lineSpace.removeChild(scrollNode); + } + } + + // Scroll a given position into view (immediately), verifying that + // it actually became visible (as line heights are accurately + // measured, the position of something may 'drift' during drawing). + function scrollPosIntoView(cm, pos, end, margin) { + if (margin == null) margin = 0; + for (var limit = 0; limit < 5; limit++) { + var changed = false, coords = cursorCoords(cm, pos); + var endCoords = !end || end == pos ? coords : cursorCoords(cm, end); + var scrollPos = calculateScrollPos(cm, Math.min(coords.left, endCoords.left), + Math.min(coords.top, endCoords.top) - margin, + Math.max(coords.left, endCoords.left), + Math.max(coords.bottom, endCoords.bottom) + margin); + var startTop = cm.doc.scrollTop, startLeft = cm.doc.scrollLeft; + if (scrollPos.scrollTop != null) { + setScrollTop(cm, scrollPos.scrollTop); + if (Math.abs(cm.doc.scrollTop - startTop) > 1) changed = true; + } + if (scrollPos.scrollLeft != null) { + setScrollLeft(cm, scrollPos.scrollLeft); + if (Math.abs(cm.doc.scrollLeft - startLeft) > 1) changed = true; + } + if (!changed) break; + } + return coords; + } + + // Scroll a given set of coordinates into view (immediately). + function scrollIntoView(cm, x1, y1, x2, y2) { + var scrollPos = calculateScrollPos(cm, x1, y1, x2, y2); + if (scrollPos.scrollTop != null) setScrollTop(cm, scrollPos.scrollTop); + if (scrollPos.scrollLeft != null) setScrollLeft(cm, scrollPos.scrollLeft); + } + + // Calculate a new scroll position needed to scroll the given + // rectangle into view. Returns an object with scrollTop and + // scrollLeft properties. When these are undefined, the + // vertical/horizontal position does not need to be adjusted. + function calculateScrollPos(cm, x1, y1, x2, y2) { + var display = cm.display, snapMargin = textHeight(cm.display); + if (y1 < 0) y1 = 0; + var screentop = cm.curOp && cm.curOp.scrollTop != null ? cm.curOp.scrollTop : display.scroller.scrollTop; + var screen = displayHeight(cm), result = {}; + if (y2 - y1 > screen) y2 = y1 + screen; + var docBottom = cm.doc.height + paddingVert(display); + var atTop = y1 < snapMargin, atBottom = y2 > docBottom - snapMargin; + if (y1 < screentop) { + result.scrollTop = atTop ? 0 : y1; + } else if (y2 > screentop + screen) { + var newTop = Math.min(y1, (atBottom ? docBottom : y2) - screen); + if (newTop != screentop) result.scrollTop = newTop; + } + + var screenleft = cm.curOp && cm.curOp.scrollLeft != null ? cm.curOp.scrollLeft : display.scroller.scrollLeft; + var screenw = displayWidth(cm) - (cm.options.fixedGutter ? display.gutters.offsetWidth : 0); + var tooWide = x2 - x1 > screenw; + if (tooWide) x2 = x1 + screenw; + if (x1 < 10) + result.scrollLeft = 0; + else if (x1 < screenleft) + result.scrollLeft = Math.max(0, x1 - (tooWide ? 0 : 10)); + else if (x2 > screenw + screenleft - 3) + result.scrollLeft = x2 + (tooWide ? 0 : 10) - screenw; + return result; + } + + // Store a relative adjustment to the scroll position in the current + // operation (to be applied when the operation finishes). + function addToScrollPos(cm, left, top) { + if (left != null || top != null) resolveScrollToPos(cm); + if (left != null) + cm.curOp.scrollLeft = (cm.curOp.scrollLeft == null ? cm.doc.scrollLeft : cm.curOp.scrollLeft) + left; + if (top != null) + cm.curOp.scrollTop = (cm.curOp.scrollTop == null ? cm.doc.scrollTop : cm.curOp.scrollTop) + top; + } + + // Make sure that at the end of the operation the current cursor is + // shown. + function ensureCursorVisible(cm) { + resolveScrollToPos(cm); + var cur = cm.getCursor(), from = cur, to = cur; + if (!cm.options.lineWrapping) { + from = cur.ch ? Pos(cur.line, cur.ch - 1) : cur; + to = Pos(cur.line, cur.ch + 1); + } + cm.curOp.scrollToPos = {from: from, to: to, margin: cm.options.cursorScrollMargin, isCursor: true}; + } + + // When an operation has its scrollToPos property set, and another + // scroll action is applied before the end of the operation, this + // 'simulates' scrolling that position into view in a cheap way, so + // that the effect of intermediate scroll commands is not ignored. + function resolveScrollToPos(cm) { + var range = cm.curOp.scrollToPos; + if (range) { + cm.curOp.scrollToPos = null; + var from = estimateCoords(cm, range.from), to = estimateCoords(cm, range.to); + var sPos = calculateScrollPos(cm, Math.min(from.left, to.left), + Math.min(from.top, to.top) - range.margin, + Math.max(from.right, to.right), + Math.max(from.bottom, to.bottom) + range.margin); + cm.scrollTo(sPos.scrollLeft, sPos.scrollTop); + } + } + + // API UTILITIES + + // Indent the given line. The how parameter can be "smart", + // "add"/null, "subtract", or "prev". When aggressive is false + // (typically set to true for forced single-line indents), empty + // lines are not indented, and places where the mode returns Pass + // are left alone. + function indentLine(cm, n, how, aggressive) { + var doc = cm.doc, state; + if (how == null) how = "add"; + if (how == "smart") { + // Fall back to "prev" when the mode doesn't have an indentation + // method. + if (!doc.mode.indent) how = "prev"; + else state = getStateBefore(cm, n); + } + + var tabSize = cm.options.tabSize; + var line = getLine(doc, n), curSpace = countColumn(line.text, null, tabSize); + if (line.stateAfter) line.stateAfter = null; + var curSpaceString = line.text.match(/^\s*/)[0], indentation; + if (!aggressive && !/\S/.test(line.text)) { + indentation = 0; + how = "not"; + } else if (how == "smart") { + indentation = doc.mode.indent(state, line.text.slice(curSpaceString.length), line.text); + if (indentation == Pass || indentation > 150) { + if (!aggressive) return; + how = "prev"; + } + } + if (how == "prev") { + if (n > doc.first) indentation = countColumn(getLine(doc, n-1).text, null, tabSize); + else indentation = 0; + } else if (how == "add") { + indentation = curSpace + cm.options.indentUnit; + } else if (how == "subtract") { + indentation = curSpace - cm.options.indentUnit; + } else if (typeof how == "number") { + indentation = curSpace + how; + } + indentation = Math.max(0, indentation); + + var indentString = "", pos = 0; + if (cm.options.indentWithTabs) + for (var i = Math.floor(indentation / tabSize); i; --i) {pos += tabSize; indentString += "\t";} + if (pos < indentation) indentString += spaceStr(indentation - pos); + + if (indentString != curSpaceString) { + replaceRange(doc, indentString, Pos(n, 0), Pos(n, curSpaceString.length), "+input"); + line.stateAfter = null; + return true; + } else { + // Ensure that, if the cursor was in the whitespace at the start + // of the line, it is moved to the end of that space. + for (var i = 0; i < doc.sel.ranges.length; i++) { + var range = doc.sel.ranges[i]; + if (range.head.line == n && range.head.ch < curSpaceString.length) { + var pos = Pos(n, curSpaceString.length); + replaceOneSelection(doc, i, new Range(pos, pos)); + break; + } + } + } + } + + // Utility for applying a change to a line by handle or number, + // returning the number and optionally registering the line as + // changed. + function changeLine(doc, handle, changeType, op) { + var no = handle, line = handle; + if (typeof handle == "number") line = getLine(doc, clipLine(doc, handle)); + else no = lineNo(handle); + if (no == null) return null; + if (op(line, no) && doc.cm) regLineChange(doc.cm, no, changeType); + return line; + } + + // Helper for deleting text near the selection(s), used to implement + // backspace, delete, and similar functionality. + function deleteNearSelection(cm, compute) { + var ranges = cm.doc.sel.ranges, kill = []; + // Build up a set of ranges to kill first, merging overlapping + // ranges. + for (var i = 0; i < ranges.length; i++) { + var toKill = compute(ranges[i]); + while (kill.length && cmp(toKill.from, lst(kill).to) <= 0) { + var replaced = kill.pop(); + if (cmp(replaced.from, toKill.from) < 0) { + toKill.from = replaced.from; + break; + } + } + kill.push(toKill); + } + // Next, remove those actual ranges. + runInOp(cm, function() { + for (var i = kill.length - 1; i >= 0; i--) + replaceRange(cm.doc, "", kill[i].from, kill[i].to, "+delete"); + ensureCursorVisible(cm); + }); + } + + // Used for horizontal relative motion. Dir is -1 or 1 (left or + // right), unit can be "char", "column" (like char, but doesn't + // cross line boundaries), "word" (across next word), or "group" (to + // the start of next group of word or non-word-non-whitespace + // chars). The visually param controls whether, in right-to-left + // text, direction 1 means to move towards the next index in the + // string, or towards the character to the right of the current + // position. The resulting position will have a hitSide=true + // property if it reached the end of the document. + function findPosH(doc, pos, dir, unit, visually) { + var line = pos.line, ch = pos.ch, origDir = dir; + var lineObj = getLine(doc, line); + var possible = true; + function findNextLine() { + var l = line + dir; + if (l < doc.first || l >= doc.first + doc.size) return (possible = false); + line = l; + return lineObj = getLine(doc, l); + } + function moveOnce(boundToLine) { + var next = (visually ? moveVisually : moveLogically)(lineObj, ch, dir, true); + if (next == null) { + if (!boundToLine && findNextLine()) { + if (visually) ch = (dir < 0 ? lineRight : lineLeft)(lineObj); + else ch = dir < 0 ? lineObj.text.length : 0; + } else return (possible = false); + } else ch = next; + return true; + } + + if (unit == "char") moveOnce(); + else if (unit == "column") moveOnce(true); + else if (unit == "word" || unit == "group") { + var sawType = null, group = unit == "group"; + var helper = doc.cm && doc.cm.getHelper(pos, "wordChars"); + for (var first = true;; first = false) { + if (dir < 0 && !moveOnce(!first)) break; + var cur = lineObj.text.charAt(ch) || "\n"; + var type = isWordChar(cur, helper) ? "w" + : group && cur == "\n" ? "n" + : !group || /\s/.test(cur) ? null + : "p"; + if (group && !first && !type) type = "s"; + if (sawType && sawType != type) { + if (dir < 0) {dir = 1; moveOnce();} + break; + } + + if (type) sawType = type; + if (dir > 0 && !moveOnce(!first)) break; + } + } + var result = skipAtomic(doc, Pos(line, ch), origDir, true); + if (!possible) result.hitSide = true; + return result; + } + + // For relative vertical movement. Dir may be -1 or 1. Unit can be + // "page" or "line". The resulting position will have a hitSide=true + // property if it reached the end of the document. + function findPosV(cm, pos, dir, unit) { + var doc = cm.doc, x = pos.left, y; + if (unit == "page") { + var pageSize = Math.min(cm.display.wrapper.clientHeight, window.innerHeight || document.documentElement.clientHeight); + y = pos.top + dir * (pageSize - (dir < 0 ? 1.5 : .5) * textHeight(cm.display)); + } else if (unit == "line") { + y = dir > 0 ? pos.bottom + 3 : pos.top - 3; + } + for (;;) { + var target = coordsChar(cm, x, y); + if (!target.outside) break; + if (dir < 0 ? y <= 0 : y >= doc.height) { target.hitSide = true; break; } + y += dir * 5; + } + return target; + } + + // EDITOR METHODS + + // The publicly visible API. Note that methodOp(f) means + // 'wrap f in an operation, performed on its `this` parameter'. + + // This is not the complete set of editor methods. Most of the + // methods defined on the Doc type are also injected into + // CodeMirror.prototype, for backwards compatibility and + // convenience. + + CodeMirror.prototype = { + constructor: CodeMirror, + focus: function(){window.focus(); this.display.input.focus();}, + + setOption: function(option, value) { + var options = this.options, old = options[option]; + if (options[option] == value && option != "mode") return; + options[option] = value; + if (optionHandlers.hasOwnProperty(option)) + operation(this, optionHandlers[option])(this, value, old); + }, + + getOption: function(option) {return this.options[option];}, + getDoc: function() {return this.doc;}, + + addKeyMap: function(map, bottom) { + this.state.keyMaps[bottom ? "push" : "unshift"](getKeyMap(map)); + }, + removeKeyMap: function(map) { + var maps = this.state.keyMaps; + for (var i = 0; i < maps.length; ++i) + if (maps[i] == map || maps[i].name == map) { + maps.splice(i, 1); + return true; + } + }, + + addOverlay: methodOp(function(spec, options) { + var mode = spec.token ? spec : CodeMirror.getMode(this.options, spec); + if (mode.startState) throw new Error("Overlays may not be stateful."); + this.state.overlays.push({mode: mode, modeSpec: spec, opaque: options && options.opaque}); + this.state.modeGen++; + regChange(this); + }), + removeOverlay: methodOp(function(spec) { + var overlays = this.state.overlays; + for (var i = 0; i < overlays.length; ++i) { + var cur = overlays[i].modeSpec; + if (cur == spec || typeof spec == "string" && cur.name == spec) { + overlays.splice(i, 1); + this.state.modeGen++; + regChange(this); + return; + } + } + }), + + indentLine: methodOp(function(n, dir, aggressive) { + if (typeof dir != "string" && typeof dir != "number") { + if (dir == null) dir = this.options.smartIndent ? "smart" : "prev"; + else dir = dir ? "add" : "subtract"; + } + if (isLine(this.doc, n)) indentLine(this, n, dir, aggressive); + }), + indentSelection: methodOp(function(how) { + var ranges = this.doc.sel.ranges, end = -1; + for (var i = 0; i < ranges.length; i++) { + var range = ranges[i]; + if (!range.empty()) { + var from = range.from(), to = range.to(); + var start = Math.max(end, from.line); + end = Math.min(this.lastLine(), to.line - (to.ch ? 0 : 1)) + 1; + for (var j = start; j < end; ++j) + indentLine(this, j, how); + var newRanges = this.doc.sel.ranges; + if (from.ch == 0 && ranges.length == newRanges.length && newRanges[i].from().ch > 0) + replaceOneSelection(this.doc, i, new Range(from, newRanges[i].to()), sel_dontScroll); + } else if (range.head.line > end) { + indentLine(this, range.head.line, how, true); + end = range.head.line; + if (i == this.doc.sel.primIndex) ensureCursorVisible(this); + } + } + }), + + // Fetch the parser token for a given character. Useful for hacks + // that want to inspect the mode state (say, for completion). + getTokenAt: function(pos, precise) { + return takeToken(this, pos, precise); + }, + + getLineTokens: function(line, precise) { + return takeToken(this, Pos(line), precise, true); + }, + + getTokenTypeAt: function(pos) { + pos = clipPos(this.doc, pos); + var styles = getLineStyles(this, getLine(this.doc, pos.line)); + var before = 0, after = (styles.length - 1) / 2, ch = pos.ch; + var type; + if (ch == 0) type = styles[2]; + else for (;;) { + var mid = (before + after) >> 1; + if ((mid ? styles[mid * 2 - 1] : 0) >= ch) after = mid; + else if (styles[mid * 2 + 1] < ch) before = mid + 1; + else { type = styles[mid * 2 + 2]; break; } + } + var cut = type ? type.indexOf("cm-overlay ") : -1; + return cut < 0 ? type : cut == 0 ? null : type.slice(0, cut - 1); + }, + + getModeAt: function(pos) { + var mode = this.doc.mode; + if (!mode.innerMode) return mode; + return CodeMirror.innerMode(mode, this.getTokenAt(pos).state).mode; + }, + + getHelper: function(pos, type) { + return this.getHelpers(pos, type)[0]; + }, + + getHelpers: function(pos, type) { + var found = []; + if (!helpers.hasOwnProperty(type)) return found; + var help = helpers[type], mode = this.getModeAt(pos); + if (typeof mode[type] == "string") { + if (help[mode[type]]) found.push(help[mode[type]]); + } else if (mode[type]) { + for (var i = 0; i < mode[type].length; i++) { + var val = help[mode[type][i]]; + if (val) found.push(val); + } + } else if (mode.helperType && help[mode.helperType]) { + found.push(help[mode.helperType]); + } else if (help[mode.name]) { + found.push(help[mode.name]); + } + for (var i = 0; i < help._global.length; i++) { + var cur = help._global[i]; + if (cur.pred(mode, this) && indexOf(found, cur.val) == -1) + found.push(cur.val); + } + return found; + }, + + getStateAfter: function(line, precise) { + var doc = this.doc; + line = clipLine(doc, line == null ? doc.first + doc.size - 1: line); + return getStateBefore(this, line + 1, precise); + }, + + cursorCoords: function(start, mode) { + var pos, range = this.doc.sel.primary(); + if (start == null) pos = range.head; + else if (typeof start == "object") pos = clipPos(this.doc, start); + else pos = start ? range.from() : range.to(); + return cursorCoords(this, pos, mode || "page"); + }, + + charCoords: function(pos, mode) { + return charCoords(this, clipPos(this.doc, pos), mode || "page"); + }, + + coordsChar: function(coords, mode) { + coords = fromCoordSystem(this, coords, mode || "page"); + return coordsChar(this, coords.left, coords.top); + }, + + lineAtHeight: function(height, mode) { + height = fromCoordSystem(this, {top: height, left: 0}, mode || "page").top; + return lineAtHeight(this.doc, height + this.display.viewOffset); + }, + heightAtLine: function(line, mode) { + var end = false, lineObj; + if (typeof line == "number") { + var last = this.doc.first + this.doc.size - 1; + if (line < this.doc.first) line = this.doc.first; + else if (line > last) { line = last; end = true; } + lineObj = getLine(this.doc, line); + } else { + lineObj = line; + } + return intoCoordSystem(this, lineObj, {top: 0, left: 0}, mode || "page").top + + (end ? this.doc.height - heightAtLine(lineObj) : 0); + }, + + defaultTextHeight: function() { return textHeight(this.display); }, + defaultCharWidth: function() { return charWidth(this.display); }, + + setGutterMarker: methodOp(function(line, gutterID, value) { + return changeLine(this.doc, line, "gutter", function(line) { + var markers = line.gutterMarkers || (line.gutterMarkers = {}); + markers[gutterID] = value; + if (!value && isEmpty(markers)) line.gutterMarkers = null; + return true; + }); + }), + + clearGutter: methodOp(function(gutterID) { + var cm = this, doc = cm.doc, i = doc.first; + doc.iter(function(line) { + if (line.gutterMarkers && line.gutterMarkers[gutterID]) { + line.gutterMarkers[gutterID] = null; + regLineChange(cm, i, "gutter"); + if (isEmpty(line.gutterMarkers)) line.gutterMarkers = null; + } + ++i; + }); + }), + + lineInfo: function(line) { + if (typeof line == "number") { + if (!isLine(this.doc, line)) return null; + var n = line; + line = getLine(this.doc, line); + if (!line) return null; + } else { + var n = lineNo(line); + if (n == null) return null; + } + return {line: n, handle: line, text: line.text, gutterMarkers: line.gutterMarkers, + textClass: line.textClass, bgClass: line.bgClass, wrapClass: line.wrapClass, + widgets: line.widgets}; + }, + + getViewport: function() { return {from: this.display.viewFrom, to: this.display.viewTo};}, + + addWidget: function(pos, node, scroll, vert, horiz) { + var display = this.display; + pos = cursorCoords(this, clipPos(this.doc, pos)); + var top = pos.bottom, left = pos.left; + node.style.position = "absolute"; + node.setAttribute("cm-ignore-events", "true"); + this.display.input.setUneditable(node); + display.sizer.appendChild(node); + if (vert == "over") { + top = pos.top; + } else if (vert == "above" || vert == "near") { + var vspace = Math.max(display.wrapper.clientHeight, this.doc.height), + hspace = Math.max(display.sizer.clientWidth, display.lineSpace.clientWidth); + // Default to positioning above (if specified and possible); otherwise default to positioning below + if ((vert == 'above' || pos.bottom + node.offsetHeight > vspace) && pos.top > node.offsetHeight) + top = pos.top - node.offsetHeight; + else if (pos.bottom + node.offsetHeight <= vspace) + top = pos.bottom; + if (left + node.offsetWidth > hspace) + left = hspace - node.offsetWidth; + } + node.style.top = top + "px"; + node.style.left = node.style.right = ""; + if (horiz == "right") { + left = display.sizer.clientWidth - node.offsetWidth; + node.style.right = "0px"; + } else { + if (horiz == "left") left = 0; + else if (horiz == "middle") left = (display.sizer.clientWidth - node.offsetWidth) / 2; + node.style.left = left + "px"; + } + if (scroll) + scrollIntoView(this, left, top, left + node.offsetWidth, top + node.offsetHeight); + }, + + triggerOnKeyDown: methodOp(onKeyDown), + triggerOnKeyPress: methodOp(onKeyPress), + triggerOnKeyUp: onKeyUp, + + execCommand: function(cmd) { + if (commands.hasOwnProperty(cmd)) + return commands[cmd](this); + }, + + triggerElectric: methodOp(function(text) { triggerElectric(this, text); }), + + findPosH: function(from, amount, unit, visually) { + var dir = 1; + if (amount < 0) { dir = -1; amount = -amount; } + for (var i = 0, cur = clipPos(this.doc, from); i < amount; ++i) { + cur = findPosH(this.doc, cur, dir, unit, visually); + if (cur.hitSide) break; + } + return cur; + }, + + moveH: methodOp(function(dir, unit) { + var cm = this; + cm.extendSelectionsBy(function(range) { + if (cm.display.shift || cm.doc.extend || range.empty()) + return findPosH(cm.doc, range.head, dir, unit, cm.options.rtlMoveVisually); + else + return dir < 0 ? range.from() : range.to(); + }, sel_move); + }), + + deleteH: methodOp(function(dir, unit) { + var sel = this.doc.sel, doc = this.doc; + if (sel.somethingSelected()) + doc.replaceSelection("", null, "+delete"); + else + deleteNearSelection(this, function(range) { + var other = findPosH(doc, range.head, dir, unit, false); + return dir < 0 ? {from: other, to: range.head} : {from: range.head, to: other}; + }); + }), + + findPosV: function(from, amount, unit, goalColumn) { + var dir = 1, x = goalColumn; + if (amount < 0) { dir = -1; amount = -amount; } + for (var i = 0, cur = clipPos(this.doc, from); i < amount; ++i) { + var coords = cursorCoords(this, cur, "div"); + if (x == null) x = coords.left; + else coords.left = x; + cur = findPosV(this, coords, dir, unit); + if (cur.hitSide) break; + } + return cur; + }, + + moveV: methodOp(function(dir, unit) { + var cm = this, doc = this.doc, goals = []; + var collapse = !cm.display.shift && !doc.extend && doc.sel.somethingSelected(); + doc.extendSelectionsBy(function(range) { + if (collapse) + return dir < 0 ? range.from() : range.to(); + var headPos = cursorCoords(cm, range.head, "div"); + if (range.goalColumn != null) headPos.left = range.goalColumn; + goals.push(headPos.left); + var pos = findPosV(cm, headPos, dir, unit); + if (unit == "page" && range == doc.sel.primary()) + addToScrollPos(cm, null, charCoords(cm, pos, "div").top - headPos.top); + return pos; + }, sel_move); + if (goals.length) for (var i = 0; i < doc.sel.ranges.length; i++) + doc.sel.ranges[i].goalColumn = goals[i]; + }), + + // Find the word at the given position (as returned by coordsChar). + findWordAt: function(pos) { + var doc = this.doc, line = getLine(doc, pos.line).text; + var start = pos.ch, end = pos.ch; + if (line) { + var helper = this.getHelper(pos, "wordChars"); + if ((pos.xRel < 0 || end == line.length) && start) --start; else ++end; + var startChar = line.charAt(start); + var check = isWordChar(startChar, helper) + ? function(ch) { return isWordChar(ch, helper); } + : /\s/.test(startChar) ? function(ch) {return /\s/.test(ch);} + : function(ch) {return !/\s/.test(ch) && !isWordChar(ch);}; + while (start > 0 && check(line.charAt(start - 1))) --start; + while (end < line.length && check(line.charAt(end))) ++end; + } + return new Range(Pos(pos.line, start), Pos(pos.line, end)); + }, + + toggleOverwrite: function(value) { + if (value != null && value == this.state.overwrite) return; + if (this.state.overwrite = !this.state.overwrite) + addClass(this.display.cursorDiv, "CodeMirror-overwrite"); + else + rmClass(this.display.cursorDiv, "CodeMirror-overwrite"); + + signal(this, "overwriteToggle", this, this.state.overwrite); + }, + hasFocus: function() { return this.display.input.getField() == activeElt(); }, + + scrollTo: methodOp(function(x, y) { + if (x != null || y != null) resolveScrollToPos(this); + if (x != null) this.curOp.scrollLeft = x; + if (y != null) this.curOp.scrollTop = y; + }), + getScrollInfo: function() { + var scroller = this.display.scroller; + return {left: scroller.scrollLeft, top: scroller.scrollTop, + height: scroller.scrollHeight - scrollGap(this) - this.display.barHeight, + width: scroller.scrollWidth - scrollGap(this) - this.display.barWidth, + clientHeight: displayHeight(this), clientWidth: displayWidth(this)}; + }, + + scrollIntoView: methodOp(function(range, margin) { + if (range == null) { + range = {from: this.doc.sel.primary().head, to: null}; + if (margin == null) margin = this.options.cursorScrollMargin; + } else if (typeof range == "number") { + range = {from: Pos(range, 0), to: null}; + } else if (range.from == null) { + range = {from: range, to: null}; + } + if (!range.to) range.to = range.from; + range.margin = margin || 0; + + if (range.from.line != null) { + resolveScrollToPos(this); + this.curOp.scrollToPos = range; + } else { + var sPos = calculateScrollPos(this, Math.min(range.from.left, range.to.left), + Math.min(range.from.top, range.to.top) - range.margin, + Math.max(range.from.right, range.to.right), + Math.max(range.from.bottom, range.to.bottom) + range.margin); + this.scrollTo(sPos.scrollLeft, sPos.scrollTop); + } + }), + + setSize: methodOp(function(width, height) { + var cm = this; + function interpret(val) { + return typeof val == "number" || /^\d+$/.test(String(val)) ? val + "px" : val; + } + if (width != null) cm.display.wrapper.style.width = interpret(width); + if (height != null) cm.display.wrapper.style.height = interpret(height); + if (cm.options.lineWrapping) clearLineMeasurementCache(this); + var lineNo = cm.display.viewFrom; + cm.doc.iter(lineNo, cm.display.viewTo, function(line) { + if (line.widgets) for (var i = 0; i < line.widgets.length; i++) + if (line.widgets[i].noHScroll) { regLineChange(cm, lineNo, "widget"); break; } + ++lineNo; + }); + cm.curOp.forceUpdate = true; + signal(cm, "refresh", this); + }), + + operation: function(f){return runInOp(this, f);}, + + refresh: methodOp(function() { + var oldHeight = this.display.cachedTextHeight; + regChange(this); + this.curOp.forceUpdate = true; + clearCaches(this); + this.scrollTo(this.doc.scrollLeft, this.doc.scrollTop); + updateGutterSpace(this); + if (oldHeight == null || Math.abs(oldHeight - textHeight(this.display)) > .5) + estimateLineHeights(this); + signal(this, "refresh", this); + }), + + swapDoc: methodOp(function(doc) { + var old = this.doc; + old.cm = null; + attachDoc(this, doc); + clearCaches(this); + this.display.input.reset(); + this.scrollTo(doc.scrollLeft, doc.scrollTop); + this.curOp.forceScroll = true; + signalLater(this, "swapDoc", this, old); + return old; + }), + + getInputField: function(){return this.display.input.getField();}, + getWrapperElement: function(){return this.display.wrapper;}, + getScrollerElement: function(){return this.display.scroller;}, + getGutterElement: function(){return this.display.gutters;} + }; + eventMixin(CodeMirror); + + // OPTION DEFAULTS + + // The default configuration options. + var defaults = CodeMirror.defaults = {}; + // Functions to run when options are changed. + var optionHandlers = CodeMirror.optionHandlers = {}; + + function option(name, deflt, handle, notOnInit) { + CodeMirror.defaults[name] = deflt; + if (handle) optionHandlers[name] = + notOnInit ? function(cm, val, old) {if (old != Init) handle(cm, val, old);} : handle; + } + + // Passed to option handlers when there is no old value. + var Init = CodeMirror.Init = {toString: function(){return "CodeMirror.Init";}}; + + // These two are, on init, called from the constructor because they + // have to be initialized before the editor can start at all. + option("value", "", function(cm, val) { + cm.setValue(val); + }, true); + option("mode", null, function(cm, val) { + cm.doc.modeOption = val; + loadMode(cm); + }, true); + + option("indentUnit", 2, loadMode, true); + option("indentWithTabs", false); + option("smartIndent", true); + option("tabSize", 4, function(cm) { + resetModeState(cm); + clearCaches(cm); + regChange(cm); + }, true); + option("specialChars", /[\t\u0000-\u0019\u00ad\u200b-\u200f\u2028\u2029\ufeff]/g, function(cm, val, old) { + cm.state.specialChars = new RegExp(val.source + (val.test("\t") ? "" : "|\t"), "g"); + if (old != CodeMirror.Init) cm.refresh(); + }); + option("specialCharPlaceholder", defaultSpecialCharPlaceholder, function(cm) {cm.refresh();}, true); + option("electricChars", true); + option("inputStyle", mobile ? "contenteditable" : "textarea", function() { + throw new Error("inputStyle can not (yet) be changed in a running editor"); // FIXME + }, true); + option("rtlMoveVisually", !windows); + option("wholeLineUpdateBefore", true); + + option("theme", "default", function(cm) { + themeChanged(cm); + guttersChanged(cm); + }, true); + option("keyMap", "default", function(cm, val, old) { + var next = getKeyMap(val); + var prev = old != CodeMirror.Init && getKeyMap(old); + if (prev && prev.detach) prev.detach(cm, next); + if (next.attach) next.attach(cm, prev || null); + }); + option("extraKeys", null); + + option("lineWrapping", false, wrappingChanged, true); + option("gutters", [], function(cm) { + setGuttersForLineNumbers(cm.options); + guttersChanged(cm); + }, true); + option("fixedGutter", true, function(cm, val) { + cm.display.gutters.style.left = val ? compensateForHScroll(cm.display) + "px" : "0"; + cm.refresh(); + }, true); + option("coverGutterNextToScrollbar", false, function(cm) {updateScrollbars(cm);}, true); + option("scrollbarStyle", "native", function(cm) { + initScrollbars(cm); + updateScrollbars(cm); + cm.display.scrollbars.setScrollTop(cm.doc.scrollTop); + cm.display.scrollbars.setScrollLeft(cm.doc.scrollLeft); + }, true); + option("lineNumbers", false, function(cm) { + setGuttersForLineNumbers(cm.options); + guttersChanged(cm); + }, true); + option("firstLineNumber", 1, guttersChanged, true); + option("lineNumberFormatter", function(integer) {return integer;}, guttersChanged, true); + option("showCursorWhenSelecting", false, updateSelection, true); + + option("resetSelectionOnContextMenu", true); + option("lineWiseCopyCut", true); + + option("readOnly", false, function(cm, val) { + if (val == "nocursor") { + onBlur(cm); + cm.display.input.blur(); + cm.display.disabled = true; + } else { + cm.display.disabled = false; + if (!val) cm.display.input.reset(); + } + }); + option("disableInput", false, function(cm, val) {if (!val) cm.display.input.reset();}, true); + option("dragDrop", true, dragDropChanged); + + option("cursorBlinkRate", 530); + option("cursorScrollMargin", 0); + option("cursorHeight", 1, updateSelection, true); + option("singleCursorHeightPerLine", true, updateSelection, true); + option("workTime", 100); + option("workDelay", 100); + option("flattenSpans", true, resetModeState, true); + option("addModeClass", false, resetModeState, true); + option("pollInterval", 100); + option("undoDepth", 200, function(cm, val){cm.doc.history.undoDepth = val;}); + option("historyEventDelay", 1250); + option("viewportMargin", 10, function(cm){cm.refresh();}, true); + option("maxHighlightLength", 10000, resetModeState, true); + option("moveInputWithCursor", true, function(cm, val) { + if (!val) cm.display.input.resetPosition(); + }); + + option("tabindex", null, function(cm, val) { + cm.display.input.getField().tabIndex = val || ""; + }); + option("autofocus", null); + + // MODE DEFINITION AND QUERYING + + // Known modes, by name and by MIME + var modes = CodeMirror.modes = {}, mimeModes = CodeMirror.mimeModes = {}; + + // Extra arguments are stored as the mode's dependencies, which is + // used by (legacy) mechanisms like loadmode.js to automatically + // load a mode. (Preferred mechanism is the require/define calls.) + CodeMirror.defineMode = function(name, mode) { + if (!CodeMirror.defaults.mode && name != "null") CodeMirror.defaults.mode = name; + if (arguments.length > 2) + mode.dependencies = Array.prototype.slice.call(arguments, 2); + modes[name] = mode; + }; + + CodeMirror.defineMIME = function(mime, spec) { + mimeModes[mime] = spec; + }; + + // Given a MIME type, a {name, ...options} config object, or a name + // string, return a mode config object. + CodeMirror.resolveMode = function(spec) { + if (typeof spec == "string" && mimeModes.hasOwnProperty(spec)) { + spec = mimeModes[spec]; + } else if (spec && typeof spec.name == "string" && mimeModes.hasOwnProperty(spec.name)) { + var found = mimeModes[spec.name]; + if (typeof found == "string") found = {name: found}; + spec = createObj(found, spec); + spec.name = found.name; + } else if (typeof spec == "string" && /^[\w\-]+\/[\w\-]+\+xml$/.test(spec)) { + return CodeMirror.resolveMode("application/xml"); + } + if (typeof spec == "string") return {name: spec}; + else return spec || {name: "null"}; + }; + + // Given a mode spec (anything that resolveMode accepts), find and + // initialize an actual mode object. + CodeMirror.getMode = function(options, spec) { + var spec = CodeMirror.resolveMode(spec); + var mfactory = modes[spec.name]; + if (!mfactory) return CodeMirror.getMode(options, "text/plain"); + var modeObj = mfactory(options, spec); + if (modeExtensions.hasOwnProperty(spec.name)) { + var exts = modeExtensions[spec.name]; + for (var prop in exts) { + if (!exts.hasOwnProperty(prop)) continue; + if (modeObj.hasOwnProperty(prop)) modeObj["_" + prop] = modeObj[prop]; + modeObj[prop] = exts[prop]; + } + } + modeObj.name = spec.name; + if (spec.helperType) modeObj.helperType = spec.helperType; + if (spec.modeProps) for (var prop in spec.modeProps) + modeObj[prop] = spec.modeProps[prop]; + + return modeObj; + }; + + // Minimal default mode. + CodeMirror.defineMode("null", function() { + return {token: function(stream) {stream.skipToEnd();}}; + }); + CodeMirror.defineMIME("text/plain", "null"); + + // This can be used to attach properties to mode objects from + // outside the actual mode definition. + var modeExtensions = CodeMirror.modeExtensions = {}; + CodeMirror.extendMode = function(mode, properties) { + var exts = modeExtensions.hasOwnProperty(mode) ? modeExtensions[mode] : (modeExtensions[mode] = {}); + copyObj(properties, exts); + }; + + // EXTENSIONS + + CodeMirror.defineExtension = function(name, func) { + CodeMirror.prototype[name] = func; + }; + CodeMirror.defineDocExtension = function(name, func) { + Doc.prototype[name] = func; + }; + CodeMirror.defineOption = option; + + var initHooks = []; + CodeMirror.defineInitHook = function(f) {initHooks.push(f);}; + + var helpers = CodeMirror.helpers = {}; + CodeMirror.registerHelper = function(type, name, value) { + if (!helpers.hasOwnProperty(type)) helpers[type] = CodeMirror[type] = {_global: []}; + helpers[type][name] = value; + }; + CodeMirror.registerGlobalHelper = function(type, name, predicate, value) { + CodeMirror.registerHelper(type, name, value); + helpers[type]._global.push({pred: predicate, val: value}); + }; + + // MODE STATE HANDLING + + // Utility functions for working with state. Exported because nested + // modes need to do this for their inner modes. + + var copyState = CodeMirror.copyState = function(mode, state) { + if (state === true) return state; + if (mode.copyState) return mode.copyState(state); + var nstate = {}; + for (var n in state) { + var val = state[n]; + if (val instanceof Array) val = val.concat([]); + nstate[n] = val; + } + return nstate; + }; + + var startState = CodeMirror.startState = function(mode, a1, a2) { + return mode.startState ? mode.startState(a1, a2) : true; + }; + + // Given a mode and a state (for that mode), find the inner mode and + // state at the position that the state refers to. + CodeMirror.innerMode = function(mode, state) { + while (mode.innerMode) { + var info = mode.innerMode(state); + if (!info || info.mode == mode) break; + state = info.state; + mode = info.mode; + } + return info || {mode: mode, state: state}; + }; + + // STANDARD COMMANDS + + // Commands are parameter-less actions that can be performed on an + // editor, mostly used for keybindings. + var commands = CodeMirror.commands = { + selectAll: function(cm) {cm.setSelection(Pos(cm.firstLine(), 0), Pos(cm.lastLine()), sel_dontScroll);}, + singleSelection: function(cm) { + cm.setSelection(cm.getCursor("anchor"), cm.getCursor("head"), sel_dontScroll); + }, + killLine: function(cm) { + deleteNearSelection(cm, function(range) { + if (range.empty()) { + var len = getLine(cm.doc, range.head.line).text.length; + if (range.head.ch == len && range.head.line < cm.lastLine()) + return {from: range.head, to: Pos(range.head.line + 1, 0)}; + else + return {from: range.head, to: Pos(range.head.line, len)}; + } else { + return {from: range.from(), to: range.to()}; + } + }); + }, + deleteLine: function(cm) { + deleteNearSelection(cm, function(range) { + return {from: Pos(range.from().line, 0), + to: clipPos(cm.doc, Pos(range.to().line + 1, 0))}; + }); + }, + delLineLeft: function(cm) { + deleteNearSelection(cm, function(range) { + return {from: Pos(range.from().line, 0), to: range.from()}; + }); + }, + delWrappedLineLeft: function(cm) { + deleteNearSelection(cm, function(range) { + var top = cm.charCoords(range.head, "div").top + 5; + var leftPos = cm.coordsChar({left: 0, top: top}, "div"); + return {from: leftPos, to: range.from()}; + }); + }, + delWrappedLineRight: function(cm) { + deleteNearSelection(cm, function(range) { + var top = cm.charCoords(range.head, "div").top + 5; + var rightPos = cm.coordsChar({left: cm.display.lineDiv.offsetWidth + 100, top: top}, "div"); + return {from: range.from(), to: rightPos }; + }); + }, + undo: function(cm) {cm.undo();}, + redo: function(cm) {cm.redo();}, + undoSelection: function(cm) {cm.undoSelection();}, + redoSelection: function(cm) {cm.redoSelection();}, + goDocStart: function(cm) {cm.extendSelection(Pos(cm.firstLine(), 0));}, + goDocEnd: function(cm) {cm.extendSelection(Pos(cm.lastLine()));}, + goLineStart: function(cm) { + cm.extendSelectionsBy(function(range) { return lineStart(cm, range.head.line); }, + {origin: "+move", bias: 1}); + }, + goLineStartSmart: function(cm) { + cm.extendSelectionsBy(function(range) { + return lineStartSmart(cm, range.head); + }, {origin: "+move", bias: 1}); + }, + goLineEnd: function(cm) { + cm.extendSelectionsBy(function(range) { return lineEnd(cm, range.head.line); }, + {origin: "+move", bias: -1}); + }, + goLineRight: function(cm) { + cm.extendSelectionsBy(function(range) { + var top = cm.charCoords(range.head, "div").top + 5; + return cm.coordsChar({left: cm.display.lineDiv.offsetWidth + 100, top: top}, "div"); + }, sel_move); + }, + goLineLeft: function(cm) { + cm.extendSelectionsBy(function(range) { + var top = cm.charCoords(range.head, "div").top + 5; + return cm.coordsChar({left: 0, top: top}, "div"); + }, sel_move); + }, + goLineLeftSmart: function(cm) { + cm.extendSelectionsBy(function(range) { + var top = cm.charCoords(range.head, "div").top + 5; + var pos = cm.coordsChar({left: 0, top: top}, "div"); + if (pos.ch < cm.getLine(pos.line).search(/\S/)) return lineStartSmart(cm, range.head); + return pos; + }, sel_move); + }, + goLineUp: function(cm) {cm.moveV(-1, "line");}, + goLineDown: function(cm) {cm.moveV(1, "line");}, + goPageUp: function(cm) {cm.moveV(-1, "page");}, + goPageDown: function(cm) {cm.moveV(1, "page");}, + goCharLeft: function(cm) {cm.moveH(-1, "char");}, + goCharRight: function(cm) {cm.moveH(1, "char");}, + goColumnLeft: function(cm) {cm.moveH(-1, "column");}, + goColumnRight: function(cm) {cm.moveH(1, "column");}, + goWordLeft: function(cm) {cm.moveH(-1, "word");}, + goGroupRight: function(cm) {cm.moveH(1, "group");}, + goGroupLeft: function(cm) {cm.moveH(-1, "group");}, + goWordRight: function(cm) {cm.moveH(1, "word");}, + delCharBefore: function(cm) {cm.deleteH(-1, "char");}, + delCharAfter: function(cm) {cm.deleteH(1, "char");}, + delWordBefore: function(cm) {cm.deleteH(-1, "word");}, + delWordAfter: function(cm) {cm.deleteH(1, "word");}, + delGroupBefore: function(cm) {cm.deleteH(-1, "group");}, + delGroupAfter: function(cm) {cm.deleteH(1, "group");}, + indentAuto: function(cm) {cm.indentSelection("smart");}, + indentMore: function(cm) {cm.indentSelection("add");}, + indentLess: function(cm) {cm.indentSelection("subtract");}, + insertTab: function(cm) {cm.replaceSelection("\t");}, + insertSoftTab: function(cm) { + var spaces = [], ranges = cm.listSelections(), tabSize = cm.options.tabSize; + for (var i = 0; i < ranges.length; i++) { + var pos = ranges[i].from(); + var col = countColumn(cm.getLine(pos.line), pos.ch, tabSize); + spaces.push(new Array(tabSize - col % tabSize + 1).join(" ")); + } + cm.replaceSelections(spaces); + }, + defaultTab: function(cm) { + if (cm.somethingSelected()) cm.indentSelection("add"); + else cm.execCommand("insertTab"); + }, + transposeChars: function(cm) { + runInOp(cm, function() { + var ranges = cm.listSelections(), newSel = []; + for (var i = 0; i < ranges.length; i++) { + var cur = ranges[i].head, line = getLine(cm.doc, cur.line).text; + if (line) { + if (cur.ch == line.length) cur = new Pos(cur.line, cur.ch - 1); + if (cur.ch > 0) { + cur = new Pos(cur.line, cur.ch + 1); + cm.replaceRange(line.charAt(cur.ch - 1) + line.charAt(cur.ch - 2), + Pos(cur.line, cur.ch - 2), cur, "+transpose"); + } else if (cur.line > cm.doc.first) { + var prev = getLine(cm.doc, cur.line - 1).text; + if (prev) + cm.replaceRange(line.charAt(0) + "\n" + prev.charAt(prev.length - 1), + Pos(cur.line - 1, prev.length - 1), Pos(cur.line, 1), "+transpose"); + } + } + newSel.push(new Range(cur, cur)); + } + cm.setSelections(newSel); + }); + }, + newlineAndIndent: function(cm) { + runInOp(cm, function() { + var len = cm.listSelections().length; + for (var i = 0; i < len; i++) { + var range = cm.listSelections()[i]; + cm.replaceRange("\n", range.anchor, range.head, "+input"); + cm.indentLine(range.from().line + 1, null, true); + ensureCursorVisible(cm); + } + }); + }, + toggleOverwrite: function(cm) {cm.toggleOverwrite();} + }; + + + // STANDARD KEYMAPS + + var keyMap = CodeMirror.keyMap = {}; + + keyMap.basic = { + "Left": "goCharLeft", "Right": "goCharRight", "Up": "goLineUp", "Down": "goLineDown", + "End": "goLineEnd", "Home": "goLineStartSmart", "PageUp": "goPageUp", "PageDown": "goPageDown", + "Delete": "delCharAfter", "Backspace": "delCharBefore", "Shift-Backspace": "delCharBefore", + "Tab": "defaultTab", "Shift-Tab": "indentAuto", + "Enter": "newlineAndIndent", "Insert": "toggleOverwrite", + "Esc": "singleSelection" + }; + // Note that the save and find-related commands aren't defined by + // default. User code or addons can define them. Unknown commands + // are simply ignored. + keyMap.pcDefault = { + "Ctrl-A": "selectAll", "Ctrl-D": "deleteLine", "Ctrl-Z": "undo", "Shift-Ctrl-Z": "redo", "Ctrl-Y": "redo", + "Ctrl-Home": "goDocStart", "Ctrl-End": "goDocEnd", "Ctrl-Up": "goLineUp", "Ctrl-Down": "goLineDown", + "Ctrl-Left": "goGroupLeft", "Ctrl-Right": "goGroupRight", "Alt-Left": "goLineStart", "Alt-Right": "goLineEnd", + "Ctrl-Backspace": "delGroupBefore", "Ctrl-Delete": "delGroupAfter", "Ctrl-S": "save", "Ctrl-F": "find", + "Ctrl-G": "findNext", "Shift-Ctrl-G": "findPrev", "Shift-Ctrl-F": "replace", "Shift-Ctrl-R": "replaceAll", + "Ctrl-[": "indentLess", "Ctrl-]": "indentMore", + "Ctrl-U": "undoSelection", "Shift-Ctrl-U": "redoSelection", "Alt-U": "redoSelection", + fallthrough: "basic" + }; + // Very basic readline/emacs-style bindings, which are standard on Mac. + keyMap.emacsy = { + "Ctrl-F": "goCharRight", "Ctrl-B": "goCharLeft", "Ctrl-P": "goLineUp", "Ctrl-N": "goLineDown", + "Alt-F": "goWordRight", "Alt-B": "goWordLeft", "Ctrl-A": "goLineStart", "Ctrl-E": "goLineEnd", + "Ctrl-V": "goPageDown", "Shift-Ctrl-V": "goPageUp", "Ctrl-D": "delCharAfter", "Ctrl-H": "delCharBefore", + "Alt-D": "delWordAfter", "Alt-Backspace": "delWordBefore", "Ctrl-K": "killLine", "Ctrl-T": "transposeChars" + }; + keyMap.macDefault = { + "Cmd-A": "selectAll", "Cmd-D": "deleteLine", "Cmd-Z": "undo", "Shift-Cmd-Z": "redo", "Cmd-Y": "redo", + "Cmd-Home": "goDocStart", "Cmd-Up": "goDocStart", "Cmd-End": "goDocEnd", "Cmd-Down": "goDocEnd", "Alt-Left": "goGroupLeft", + "Alt-Right": "goGroupRight", "Cmd-Left": "goLineLeft", "Cmd-Right": "goLineRight", "Alt-Backspace": "delGroupBefore", + "Ctrl-Alt-Backspace": "delGroupAfter", "Alt-Delete": "delGroupAfter", "Cmd-S": "save", "Cmd-F": "find", + "Cmd-G": "findNext", "Shift-Cmd-G": "findPrev", "Cmd-Alt-F": "replace", "Shift-Cmd-Alt-F": "replaceAll", + "Cmd-[": "indentLess", "Cmd-]": "indentMore", "Cmd-Backspace": "delWrappedLineLeft", "Cmd-Delete": "delWrappedLineRight", + "Cmd-U": "undoSelection", "Shift-Cmd-U": "redoSelection", "Ctrl-Up": "goDocStart", "Ctrl-Down": "goDocEnd", + fallthrough: ["basic", "emacsy"] + }; + keyMap["default"] = mac ? keyMap.macDefault : keyMap.pcDefault; + + // KEYMAP DISPATCH + + function normalizeKeyName(name) { + var parts = name.split(/-(?!$)/), name = parts[parts.length - 1]; + var alt, ctrl, shift, cmd; + for (var i = 0; i < parts.length - 1; i++) { + var mod = parts[i]; + if (/^(cmd|meta|m)$/i.test(mod)) cmd = true; + else if (/^a(lt)?$/i.test(mod)) alt = true; + else if (/^(c|ctrl|control)$/i.test(mod)) ctrl = true; + else if (/^s(hift)$/i.test(mod)) shift = true; + else throw new Error("Unrecognized modifier name: " + mod); + } + if (alt) name = "Alt-" + name; + if (ctrl) name = "Ctrl-" + name; + if (cmd) name = "Cmd-" + name; + if (shift) name = "Shift-" + name; + return name; + } + + // This is a kludge to keep keymaps mostly working as raw objects + // (backwards compatibility) while at the same time support features + // like normalization and multi-stroke key bindings. It compiles a + // new normalized keymap, and then updates the old object to reflect + // this. + CodeMirror.normalizeKeyMap = function(keymap) { + var copy = {}; + for (var keyname in keymap) if (keymap.hasOwnProperty(keyname)) { + var value = keymap[keyname]; + if (/^(name|fallthrough|(de|at)tach)$/.test(keyname)) continue; + if (value == "...") { delete keymap[keyname]; continue; } + + var keys = map(keyname.split(" "), normalizeKeyName); + for (var i = 0; i < keys.length; i++) { + var val, name; + if (i == keys.length - 1) { + name = keys.join(" "); + val = value; + } else { + name = keys.slice(0, i + 1).join(" "); + val = "..."; + } + var prev = copy[name]; + if (!prev) copy[name] = val; + else if (prev != val) throw new Error("Inconsistent bindings for " + name); + } + delete keymap[keyname]; + } + for (var prop in copy) keymap[prop] = copy[prop]; + return keymap; + }; + + var lookupKey = CodeMirror.lookupKey = function(key, map, handle, context) { + map = getKeyMap(map); + var found = map.call ? map.call(key, context) : map[key]; + if (found === false) return "nothing"; + if (found === "...") return "multi"; + if (found != null && handle(found)) return "handled"; + + if (map.fallthrough) { + if (Object.prototype.toString.call(map.fallthrough) != "[object Array]") + return lookupKey(key, map.fallthrough, handle, context); + for (var i = 0; i < map.fallthrough.length; i++) { + var result = lookupKey(key, map.fallthrough[i], handle, context); + if (result) return result; + } + } + }; + + // Modifier key presses don't count as 'real' key presses for the + // purpose of keymap fallthrough. + var isModifierKey = CodeMirror.isModifierKey = function(value) { + var name = typeof value == "string" ? value : keyNames[value.keyCode]; + return name == "Ctrl" || name == "Alt" || name == "Shift" || name == "Mod"; + }; + + // Look up the name of a key as indicated by an event object. + var keyName = CodeMirror.keyName = function(event, noShift) { + if (presto && event.keyCode == 34 && event["char"]) return false; + var base = keyNames[event.keyCode], name = base; + if (name == null || event.altGraphKey) return false; + if (event.altKey && base != "Alt") name = "Alt-" + name; + if ((flipCtrlCmd ? event.metaKey : event.ctrlKey) && base != "Ctrl") name = "Ctrl-" + name; + if ((flipCtrlCmd ? event.ctrlKey : event.metaKey) && base != "Cmd") name = "Cmd-" + name; + if (!noShift && event.shiftKey && base != "Shift") name = "Shift-" + name; + return name; + }; + + function getKeyMap(val) { + return typeof val == "string" ? keyMap[val] : val; + } + + // FROMTEXTAREA + + CodeMirror.fromTextArea = function(textarea, options) { + options = options ? copyObj(options) : {}; + options.value = textarea.value; + if (!options.tabindex && textarea.tabIndex) + options.tabindex = textarea.tabIndex; + if (!options.placeholder && textarea.placeholder) + options.placeholder = textarea.placeholder; + // Set autofocus to true if this textarea is focused, or if it has + // autofocus and no other element is focused. + if (options.autofocus == null) { + var hasFocus = activeElt(); + options.autofocus = hasFocus == textarea || + textarea.getAttribute("autofocus") != null && hasFocus == document.body; + } + + function save() {textarea.value = cm.getValue();} + if (textarea.form) { + on(textarea.form, "submit", save); + // Deplorable hack to make the submit method do the right thing. + if (!options.leaveSubmitMethodAlone) { + var form = textarea.form, realSubmit = form.submit; + try { + var wrappedSubmit = form.submit = function() { + save(); + form.submit = realSubmit; + form.submit(); + form.submit = wrappedSubmit; + }; + } catch(e) {} + } + } + + options.finishInit = function(cm) { + cm.save = save; + cm.getTextArea = function() { return textarea; }; + cm.toTextArea = function() { + cm.toTextArea = isNaN; // Prevent this from being ran twice + save(); + textarea.parentNode.removeChild(cm.getWrapperElement()); + textarea.style.display = ""; + if (textarea.form) { + off(textarea.form, "submit", save); + if (typeof textarea.form.submit == "function") + textarea.form.submit = realSubmit; + } + }; + }; + + textarea.style.display = "none"; + var cm = CodeMirror(function(node) { + textarea.parentNode.insertBefore(node, textarea.nextSibling); + }, options); + return cm; + }; + + // STRING STREAM + + // Fed to the mode parsers, provides helper functions to make + // parsers more succinct. + + var StringStream = CodeMirror.StringStream = function(string, tabSize) { + this.pos = this.start = 0; + this.string = string; + this.tabSize = tabSize || 8; + this.lastColumnPos = this.lastColumnValue = 0; + this.lineStart = 0; + }; + + StringStream.prototype = { + eol: function() {return this.pos >= this.string.length;}, + sol: function() {return this.pos == this.lineStart;}, + peek: function() {return this.string.charAt(this.pos) || undefined;}, + next: function() { + if (this.pos < this.string.length) + return this.string.charAt(this.pos++); + }, + eat: function(match) { + var ch = this.string.charAt(this.pos); + if (typeof match == "string") var ok = ch == match; + else var ok = ch && (match.test ? match.test(ch) : match(ch)); + if (ok) {++this.pos; return ch;} + }, + eatWhile: function(match) { + var start = this.pos; + while (this.eat(match)){} + return this.pos > start; + }, + eatSpace: function() { + var start = this.pos; + while (/[\s\u00a0]/.test(this.string.charAt(this.pos))) ++this.pos; + return this.pos > start; + }, + skipToEnd: function() {this.pos = this.string.length;}, + skipTo: function(ch) { + var found = this.string.indexOf(ch, this.pos); + if (found > -1) {this.pos = found; return true;} + }, + backUp: function(n) {this.pos -= n;}, + column: function() { + if (this.lastColumnPos < this.start) { + this.lastColumnValue = countColumn(this.string, this.start, this.tabSize, this.lastColumnPos, this.lastColumnValue); + this.lastColumnPos = this.start; + } + return this.lastColumnValue - (this.lineStart ? countColumn(this.string, this.lineStart, this.tabSize) : 0); + }, + indentation: function() { + return countColumn(this.string, null, this.tabSize) - + (this.lineStart ? countColumn(this.string, this.lineStart, this.tabSize) : 0); + }, + match: function(pattern, consume, caseInsensitive) { + if (typeof pattern == "string") { + var cased = function(str) {return caseInsensitive ? str.toLowerCase() : str;}; + var substr = this.string.substr(this.pos, pattern.length); + if (cased(substr) == cased(pattern)) { + if (consume !== false) this.pos += pattern.length; + return true; + } + } else { + var match = this.string.slice(this.pos).match(pattern); + if (match && match.index > 0) return null; + if (match && consume !== false) this.pos += match[0].length; + return match; + } + }, + current: function(){return this.string.slice(this.start, this.pos);}, + hideFirstChars: function(n, inner) { + this.lineStart += n; + try { return inner(); } + finally { this.lineStart -= n; } + } + }; + + // TEXTMARKERS + + // Created with markText and setBookmark methods. A TextMarker is a + // handle that can be used to clear or find a marked position in the + // document. Line objects hold arrays (markedSpans) containing + // {from, to, marker} object pointing to such marker objects, and + // indicating that such a marker is present on that line. Multiple + // lines may point to the same marker when it spans across lines. + // The spans will have null for their from/to properties when the + // marker continues beyond the start/end of the line. Markers have + // links back to the lines they currently touch. + + var nextMarkerId = 0; + + var TextMarker = CodeMirror.TextMarker = function(doc, type) { + this.lines = []; + this.type = type; + this.doc = doc; + this.id = ++nextMarkerId; + }; + eventMixin(TextMarker); + + // Clear the marker. + TextMarker.prototype.clear = function() { + if (this.explicitlyCleared) return; + var cm = this.doc.cm, withOp = cm && !cm.curOp; + if (withOp) startOperation(cm); + if (hasHandler(this, "clear")) { + var found = this.find(); + if (found) signalLater(this, "clear", found.from, found.to); + } + var min = null, max = null; + for (var i = 0; i < this.lines.length; ++i) { + var line = this.lines[i]; + var span = getMarkedSpanFor(line.markedSpans, this); + if (cm && !this.collapsed) regLineChange(cm, lineNo(line), "text"); + else if (cm) { + if (span.to != null) max = lineNo(line); + if (span.from != null) min = lineNo(line); + } + line.markedSpans = removeMarkedSpan(line.markedSpans, span); + if (span.from == null && this.collapsed && !lineIsHidden(this.doc, line) && cm) + updateLineHeight(line, textHeight(cm.display)); + } + if (cm && this.collapsed && !cm.options.lineWrapping) for (var i = 0; i < this.lines.length; ++i) { + var visual = visualLine(this.lines[i]), len = lineLength(visual); + if (len > cm.display.maxLineLength) { + cm.display.maxLine = visual; + cm.display.maxLineLength = len; + cm.display.maxLineChanged = true; + } + } + + if (min != null && cm && this.collapsed) regChange(cm, min, max + 1); + this.lines.length = 0; + this.explicitlyCleared = true; + if (this.atomic && this.doc.cantEdit) { + this.doc.cantEdit = false; + if (cm) reCheckSelection(cm.doc); + } + if (cm) signalLater(cm, "markerCleared", cm, this); + if (withOp) endOperation(cm); + if (this.parent) this.parent.clear(); + }; + + // Find the position of the marker in the document. Returns a {from, + // to} object by default. Side can be passed to get a specific side + // -- 0 (both), -1 (left), or 1 (right). When lineObj is true, the + // Pos objects returned contain a line object, rather than a line + // number (used to prevent looking up the same line twice). + TextMarker.prototype.find = function(side, lineObj) { + if (side == null && this.type == "bookmark") side = 1; + var from, to; + for (var i = 0; i < this.lines.length; ++i) { + var line = this.lines[i]; + var span = getMarkedSpanFor(line.markedSpans, this); + if (span.from != null) { + from = Pos(lineObj ? line : lineNo(line), span.from); + if (side == -1) return from; + } + if (span.to != null) { + to = Pos(lineObj ? line : lineNo(line), span.to); + if (side == 1) return to; + } + } + return from && {from: from, to: to}; + }; + + // Signals that the marker's widget changed, and surrounding layout + // should be recomputed. + TextMarker.prototype.changed = function() { + var pos = this.find(-1, true), widget = this, cm = this.doc.cm; + if (!pos || !cm) return; + runInOp(cm, function() { + var line = pos.line, lineN = lineNo(pos.line); + var view = findViewForLine(cm, lineN); + if (view) { + clearLineMeasurementCacheFor(view); + cm.curOp.selectionChanged = cm.curOp.forceUpdate = true; + } + cm.curOp.updateMaxLine = true; + if (!lineIsHidden(widget.doc, line) && widget.height != null) { + var oldHeight = widget.height; + widget.height = null; + var dHeight = widgetHeight(widget) - oldHeight; + if (dHeight) + updateLineHeight(line, line.height + dHeight); + } + }); + }; + + TextMarker.prototype.attachLine = function(line) { + if (!this.lines.length && this.doc.cm) { + var op = this.doc.cm.curOp; + if (!op.maybeHiddenMarkers || indexOf(op.maybeHiddenMarkers, this) == -1) + (op.maybeUnhiddenMarkers || (op.maybeUnhiddenMarkers = [])).push(this); + } + this.lines.push(line); + }; + TextMarker.prototype.detachLine = function(line) { + this.lines.splice(indexOf(this.lines, line), 1); + if (!this.lines.length && this.doc.cm) { + var op = this.doc.cm.curOp; + (op.maybeHiddenMarkers || (op.maybeHiddenMarkers = [])).push(this); + } + }; + + // Collapsed markers have unique ids, in order to be able to order + // them, which is needed for uniquely determining an outer marker + // when they overlap (they may nest, but not partially overlap). + var nextMarkerId = 0; + + // Create a marker, wire it up to the right lines, and + function markText(doc, from, to, options, type) { + // Shared markers (across linked documents) are handled separately + // (markTextShared will call out to this again, once per + // document). + if (options && options.shared) return markTextShared(doc, from, to, options, type); + // Ensure we are in an operation. + if (doc.cm && !doc.cm.curOp) return operation(doc.cm, markText)(doc, from, to, options, type); + + var marker = new TextMarker(doc, type), diff = cmp(from, to); + if (options) copyObj(options, marker, false); + // Don't connect empty markers unless clearWhenEmpty is false + if (diff > 0 || diff == 0 && marker.clearWhenEmpty !== false) + return marker; + if (marker.replacedWith) { + // Showing up as a widget implies collapsed (widget replaces text) + marker.collapsed = true; + marker.widgetNode = elt("span", [marker.replacedWith], "CodeMirror-widget"); + if (!options.handleMouseEvents) marker.widgetNode.setAttribute("cm-ignore-events", "true"); + if (options.insertLeft) marker.widgetNode.insertLeft = true; + } + if (marker.collapsed) { + if (conflictingCollapsedRange(doc, from.line, from, to, marker) || + from.line != to.line && conflictingCollapsedRange(doc, to.line, from, to, marker)) + throw new Error("Inserting collapsed marker partially overlapping an existing one"); + sawCollapsedSpans = true; + } + + if (marker.addToHistory) + addChangeToHistory(doc, {from: from, to: to, origin: "markText"}, doc.sel, NaN); + + var curLine = from.line, cm = doc.cm, updateMaxLine; + doc.iter(curLine, to.line + 1, function(line) { + if (cm && marker.collapsed && !cm.options.lineWrapping && visualLine(line) == cm.display.maxLine) + updateMaxLine = true; + if (marker.collapsed && curLine != from.line) updateLineHeight(line, 0); + addMarkedSpan(line, new MarkedSpan(marker, + curLine == from.line ? from.ch : null, + curLine == to.line ? to.ch : null)); + ++curLine; + }); + // lineIsHidden depends on the presence of the spans, so needs a second pass + if (marker.collapsed) doc.iter(from.line, to.line + 1, function(line) { + if (lineIsHidden(doc, line)) updateLineHeight(line, 0); + }); + + if (marker.clearOnEnter) on(marker, "beforeCursorEnter", function() { marker.clear(); }); + + if (marker.readOnly) { + sawReadOnlySpans = true; + if (doc.history.done.length || doc.history.undone.length) + doc.clearHistory(); + } + if (marker.collapsed) { + marker.id = ++nextMarkerId; + marker.atomic = true; + } + if (cm) { + // Sync editor state + if (updateMaxLine) cm.curOp.updateMaxLine = true; + if (marker.collapsed) + regChange(cm, from.line, to.line + 1); + else if (marker.className || marker.title || marker.startStyle || marker.endStyle || marker.css) + for (var i = from.line; i <= to.line; i++) regLineChange(cm, i, "text"); + if (marker.atomic) reCheckSelection(cm.doc); + signalLater(cm, "markerAdded", cm, marker); + } + return marker; + } + + // SHARED TEXTMARKERS + + // A shared marker spans multiple linked documents. It is + // implemented as a meta-marker-object controlling multiple normal + // markers. + var SharedTextMarker = CodeMirror.SharedTextMarker = function(markers, primary) { + this.markers = markers; + this.primary = primary; + for (var i = 0; i < markers.length; ++i) + markers[i].parent = this; + }; + eventMixin(SharedTextMarker); + + SharedTextMarker.prototype.clear = function() { + if (this.explicitlyCleared) return; + this.explicitlyCleared = true; + for (var i = 0; i < this.markers.length; ++i) + this.markers[i].clear(); + signalLater(this, "clear"); + }; + SharedTextMarker.prototype.find = function(side, lineObj) { + return this.primary.find(side, lineObj); + }; + + function markTextShared(doc, from, to, options, type) { + options = copyObj(options); + options.shared = false; + var markers = [markText(doc, from, to, options, type)], primary = markers[0]; + var widget = options.widgetNode; + linkedDocs(doc, function(doc) { + if (widget) options.widgetNode = widget.cloneNode(true); + markers.push(markText(doc, clipPos(doc, from), clipPos(doc, to), options, type)); + for (var i = 0; i < doc.linked.length; ++i) + if (doc.linked[i].isParent) return; + primary = lst(markers); + }); + return new SharedTextMarker(markers, primary); + } + + function findSharedMarkers(doc) { + return doc.findMarks(Pos(doc.first, 0), doc.clipPos(Pos(doc.lastLine())), + function(m) { return m.parent; }); + } + + function copySharedMarkers(doc, markers) { + for (var i = 0; i < markers.length; i++) { + var marker = markers[i], pos = marker.find(); + var mFrom = doc.clipPos(pos.from), mTo = doc.clipPos(pos.to); + if (cmp(mFrom, mTo)) { + var subMark = markText(doc, mFrom, mTo, marker.primary, marker.primary.type); + marker.markers.push(subMark); + subMark.parent = marker; + } + } + } + + function detachSharedMarkers(markers) { + for (var i = 0; i < markers.length; i++) { + var marker = markers[i], linked = [marker.primary.doc];; + linkedDocs(marker.primary.doc, function(d) { linked.push(d); }); + for (var j = 0; j < marker.markers.length; j++) { + var subMarker = marker.markers[j]; + if (indexOf(linked, subMarker.doc) == -1) { + subMarker.parent = null; + marker.markers.splice(j--, 1); + } + } + } + } + + // TEXTMARKER SPANS + + function MarkedSpan(marker, from, to) { + this.marker = marker; + this.from = from; this.to = to; + } + + // Search an array of spans for a span matching the given marker. + function getMarkedSpanFor(spans, marker) { + if (spans) for (var i = 0; i < spans.length; ++i) { + var span = spans[i]; + if (span.marker == marker) return span; + } + } + // Remove a span from an array, returning undefined if no spans are + // left (we don't store arrays for lines without spans). + function removeMarkedSpan(spans, span) { + for (var r, i = 0; i < spans.length; ++i) + if (spans[i] != span) (r || (r = [])).push(spans[i]); + return r; + } + // Add a span to a line. + function addMarkedSpan(line, span) { + line.markedSpans = line.markedSpans ? line.markedSpans.concat([span]) : [span]; + span.marker.attachLine(line); + } + + // Used for the algorithm that adjusts markers for a change in the + // document. These functions cut an array of spans at a given + // character position, returning an array of remaining chunks (or + // undefined if nothing remains). + function markedSpansBefore(old, startCh, isInsert) { + if (old) for (var i = 0, nw; i < old.length; ++i) { + var span = old[i], marker = span.marker; + var startsBefore = span.from == null || (marker.inclusiveLeft ? span.from <= startCh : span.from < startCh); + if (startsBefore || span.from == startCh && marker.type == "bookmark" && (!isInsert || !span.marker.insertLeft)) { + var endsAfter = span.to == null || (marker.inclusiveRight ? span.to >= startCh : span.to > startCh); + (nw || (nw = [])).push(new MarkedSpan(marker, span.from, endsAfter ? null : span.to)); + } + } + return nw; + } + function markedSpansAfter(old, endCh, isInsert) { + if (old) for (var i = 0, nw; i < old.length; ++i) { + var span = old[i], marker = span.marker; + var endsAfter = span.to == null || (marker.inclusiveRight ? span.to >= endCh : span.to > endCh); + if (endsAfter || span.from == endCh && marker.type == "bookmark" && (!isInsert || span.marker.insertLeft)) { + var startsBefore = span.from == null || (marker.inclusiveLeft ? span.from <= endCh : span.from < endCh); + (nw || (nw = [])).push(new MarkedSpan(marker, startsBefore ? null : span.from - endCh, + span.to == null ? null : span.to - endCh)); + } + } + return nw; + } + + // Given a change object, compute the new set of marker spans that + // cover the line in which the change took place. Removes spans + // entirely within the change, reconnects spans belonging to the + // same marker that appear on both sides of the change, and cuts off + // spans partially within the change. Returns an array of span + // arrays with one element for each line in (after) the change. + function stretchSpansOverChange(doc, change) { + if (change.full) return null; + var oldFirst = isLine(doc, change.from.line) && getLine(doc, change.from.line).markedSpans; + var oldLast = isLine(doc, change.to.line) && getLine(doc, change.to.line).markedSpans; + if (!oldFirst && !oldLast) return null; + + var startCh = change.from.ch, endCh = change.to.ch, isInsert = cmp(change.from, change.to) == 0; + // Get the spans that 'stick out' on both sides + var first = markedSpansBefore(oldFirst, startCh, isInsert); + var last = markedSpansAfter(oldLast, endCh, isInsert); + + // Next, merge those two ends + var sameLine = change.text.length == 1, offset = lst(change.text).length + (sameLine ? startCh : 0); + if (first) { + // Fix up .to properties of first + for (var i = 0; i < first.length; ++i) { + var span = first[i]; + if (span.to == null) { + var found = getMarkedSpanFor(last, span.marker); + if (!found) span.to = startCh; + else if (sameLine) span.to = found.to == null ? null : found.to + offset; + } + } + } + if (last) { + // Fix up .from in last (or move them into first in case of sameLine) + for (var i = 0; i < last.length; ++i) { + var span = last[i]; + if (span.to != null) span.to += offset; + if (span.from == null) { + var found = getMarkedSpanFor(first, span.marker); + if (!found) { + span.from = offset; + if (sameLine) (first || (first = [])).push(span); + } + } else { + span.from += offset; + if (sameLine) (first || (first = [])).push(span); + } + } + } + // Make sure we didn't create any zero-length spans + if (first) first = clearEmptySpans(first); + if (last && last != first) last = clearEmptySpans(last); + + var newMarkers = [first]; + if (!sameLine) { + // Fill gap with whole-line-spans + var gap = change.text.length - 2, gapMarkers; + if (gap > 0 && first) + for (var i = 0; i < first.length; ++i) + if (first[i].to == null) + (gapMarkers || (gapMarkers = [])).push(new MarkedSpan(first[i].marker, null, null)); + for (var i = 0; i < gap; ++i) + newMarkers.push(gapMarkers); + newMarkers.push(last); + } + return newMarkers; + } + + // Remove spans that are empty and don't have a clearWhenEmpty + // option of false. + function clearEmptySpans(spans) { + for (var i = 0; i < spans.length; ++i) { + var span = spans[i]; + if (span.from != null && span.from == span.to && span.marker.clearWhenEmpty !== false) + spans.splice(i--, 1); + } + if (!spans.length) return null; + return spans; + } + + // Used for un/re-doing changes from the history. Combines the + // result of computing the existing spans with the set of spans that + // existed in the history (so that deleting around a span and then + // undoing brings back the span). + function mergeOldSpans(doc, change) { + var old = getOldSpans(doc, change); + var stretched = stretchSpansOverChange(doc, change); + if (!old) return stretched; + if (!stretched) return old; + + for (var i = 0; i < old.length; ++i) { + var oldCur = old[i], stretchCur = stretched[i]; + if (oldCur && stretchCur) { + spans: for (var j = 0; j < stretchCur.length; ++j) { + var span = stretchCur[j]; + for (var k = 0; k < oldCur.length; ++k) + if (oldCur[k].marker == span.marker) continue spans; + oldCur.push(span); + } + } else if (stretchCur) { + old[i] = stretchCur; + } + } + return old; + } + + // Used to 'clip' out readOnly ranges when making a change. + function removeReadOnlyRanges(doc, from, to) { + var markers = null; + doc.iter(from.line, to.line + 1, function(line) { + if (line.markedSpans) for (var i = 0; i < line.markedSpans.length; ++i) { + var mark = line.markedSpans[i].marker; + if (mark.readOnly && (!markers || indexOf(markers, mark) == -1)) + (markers || (markers = [])).push(mark); + } + }); + if (!markers) return null; + var parts = [{from: from, to: to}]; + for (var i = 0; i < markers.length; ++i) { + var mk = markers[i], m = mk.find(0); + for (var j = 0; j < parts.length; ++j) { + var p = parts[j]; + if (cmp(p.to, m.from) < 0 || cmp(p.from, m.to) > 0) continue; + var newParts = [j, 1], dfrom = cmp(p.from, m.from), dto = cmp(p.to, m.to); + if (dfrom < 0 || !mk.inclusiveLeft && !dfrom) + newParts.push({from: p.from, to: m.from}); + if (dto > 0 || !mk.inclusiveRight && !dto) + newParts.push({from: m.to, to: p.to}); + parts.splice.apply(parts, newParts); + j += newParts.length - 1; + } + } + return parts; + } + + // Connect or disconnect spans from a line. + function detachMarkedSpans(line) { + var spans = line.markedSpans; + if (!spans) return; + for (var i = 0; i < spans.length; ++i) + spans[i].marker.detachLine(line); + line.markedSpans = null; + } + function attachMarkedSpans(line, spans) { + if (!spans) return; + for (var i = 0; i < spans.length; ++i) + spans[i].marker.attachLine(line); + line.markedSpans = spans; + } + + // Helpers used when computing which overlapping collapsed span + // counts as the larger one. + function extraLeft(marker) { return marker.inclusiveLeft ? -1 : 0; } + function extraRight(marker) { return marker.inclusiveRight ? 1 : 0; } + + // Returns a number indicating which of two overlapping collapsed + // spans is larger (and thus includes the other). Falls back to + // comparing ids when the spans cover exactly the same range. + function compareCollapsedMarkers(a, b) { + var lenDiff = a.lines.length - b.lines.length; + if (lenDiff != 0) return lenDiff; + var aPos = a.find(), bPos = b.find(); + var fromCmp = cmp(aPos.from, bPos.from) || extraLeft(a) - extraLeft(b); + if (fromCmp) return -fromCmp; + var toCmp = cmp(aPos.to, bPos.to) || extraRight(a) - extraRight(b); + if (toCmp) return toCmp; + return b.id - a.id; + } + + // Find out whether a line ends or starts in a collapsed span. If + // so, return the marker for that span. + function collapsedSpanAtSide(line, start) { + var sps = sawCollapsedSpans && line.markedSpans, found; + if (sps) for (var sp, i = 0; i < sps.length; ++i) { + sp = sps[i]; + if (sp.marker.collapsed && (start ? sp.from : sp.to) == null && + (!found || compareCollapsedMarkers(found, sp.marker) < 0)) + found = sp.marker; + } + return found; + } + function collapsedSpanAtStart(line) { return collapsedSpanAtSide(line, true); } + function collapsedSpanAtEnd(line) { return collapsedSpanAtSide(line, false); } + + // Test whether there exists a collapsed span that partially + // overlaps (covers the start or end, but not both) of a new span. + // Such overlap is not allowed. + function conflictingCollapsedRange(doc, lineNo, from, to, marker) { + var line = getLine(doc, lineNo); + var sps = sawCollapsedSpans && line.markedSpans; + if (sps) for (var i = 0; i < sps.length; ++i) { + var sp = sps[i]; + if (!sp.marker.collapsed) continue; + var found = sp.marker.find(0); + var fromCmp = cmp(found.from, from) || extraLeft(sp.marker) - extraLeft(marker); + var toCmp = cmp(found.to, to) || extraRight(sp.marker) - extraRight(marker); + if (fromCmp >= 0 && toCmp <= 0 || fromCmp <= 0 && toCmp >= 0) continue; + if (fromCmp <= 0 && (cmp(found.to, from) > 0 || (sp.marker.inclusiveRight && marker.inclusiveLeft)) || + fromCmp >= 0 && (cmp(found.from, to) < 0 || (sp.marker.inclusiveLeft && marker.inclusiveRight))) + return true; + } + } + + // A visual line is a line as drawn on the screen. Folding, for + // example, can cause multiple logical lines to appear on the same + // visual line. This finds the start of the visual line that the + // given line is part of (usually that is the line itself). + function visualLine(line) { + var merged; + while (merged = collapsedSpanAtStart(line)) + line = merged.find(-1, true).line; + return line; + } + + // Returns an array of logical lines that continue the visual line + // started by the argument, or undefined if there are no such lines. + function visualLineContinued(line) { + var merged, lines; + while (merged = collapsedSpanAtEnd(line)) { + line = merged.find(1, true).line; + (lines || (lines = [])).push(line); + } + return lines; + } + + // Get the line number of the start of the visual line that the + // given line number is part of. + function visualLineNo(doc, lineN) { + var line = getLine(doc, lineN), vis = visualLine(line); + if (line == vis) return lineN; + return lineNo(vis); + } + // Get the line number of the start of the next visual line after + // the given line. + function visualLineEndNo(doc, lineN) { + if (lineN > doc.lastLine()) return lineN; + var line = getLine(doc, lineN), merged; + if (!lineIsHidden(doc, line)) return lineN; + while (merged = collapsedSpanAtEnd(line)) + line = merged.find(1, true).line; + return lineNo(line) + 1; + } + + // Compute whether a line is hidden. Lines count as hidden when they + // are part of a visual line that starts with another line, or when + // they are entirely covered by collapsed, non-widget span. + function lineIsHidden(doc, line) { + var sps = sawCollapsedSpans && line.markedSpans; + if (sps) for (var sp, i = 0; i < sps.length; ++i) { + sp = sps[i]; + if (!sp.marker.collapsed) continue; + if (sp.from == null) return true; + if (sp.marker.widgetNode) continue; + if (sp.from == 0 && sp.marker.inclusiveLeft && lineIsHiddenInner(doc, line, sp)) + return true; + } + } + function lineIsHiddenInner(doc, line, span) { + if (span.to == null) { + var end = span.marker.find(1, true); + return lineIsHiddenInner(doc, end.line, getMarkedSpanFor(end.line.markedSpans, span.marker)); + } + if (span.marker.inclusiveRight && span.to == line.text.length) + return true; + for (var sp, i = 0; i < line.markedSpans.length; ++i) { + sp = line.markedSpans[i]; + if (sp.marker.collapsed && !sp.marker.widgetNode && sp.from == span.to && + (sp.to == null || sp.to != span.from) && + (sp.marker.inclusiveLeft || span.marker.inclusiveRight) && + lineIsHiddenInner(doc, line, sp)) return true; + } + } + + // LINE WIDGETS + + // Line widgets are block elements displayed above or below a line. + + var LineWidget = CodeMirror.LineWidget = function(doc, node, options) { + if (options) for (var opt in options) if (options.hasOwnProperty(opt)) + this[opt] = options[opt]; + this.doc = doc; + this.node = node; + }; + eventMixin(LineWidget); + + function adjustScrollWhenAboveVisible(cm, line, diff) { + if (heightAtLine(line) < ((cm.curOp && cm.curOp.scrollTop) || cm.doc.scrollTop)) + addToScrollPos(cm, null, diff); + } + + LineWidget.prototype.clear = function() { + var cm = this.doc.cm, ws = this.line.widgets, line = this.line, no = lineNo(line); + if (no == null || !ws) return; + for (var i = 0; i < ws.length; ++i) if (ws[i] == this) ws.splice(i--, 1); + if (!ws.length) line.widgets = null; + var height = widgetHeight(this); + updateLineHeight(line, Math.max(0, line.height - height)); + if (cm) runInOp(cm, function() { + adjustScrollWhenAboveVisible(cm, line, -height); + regLineChange(cm, no, "widget"); + }); + }; + LineWidget.prototype.changed = function() { + var oldH = this.height, cm = this.doc.cm, line = this.line; + this.height = null; + var diff = widgetHeight(this) - oldH; + if (!diff) return; + updateLineHeight(line, line.height + diff); + if (cm) runInOp(cm, function() { + cm.curOp.forceUpdate = true; + adjustScrollWhenAboveVisible(cm, line, diff); + }); + }; + + function widgetHeight(widget) { + if (widget.height != null) return widget.height; + var cm = widget.doc.cm; + if (!cm) return 0; + if (!contains(document.body, widget.node)) { + var parentStyle = "position: relative;"; + if (widget.coverGutter) + parentStyle += "margin-left: -" + cm.display.gutters.offsetWidth + "px;"; + if (widget.noHScroll) + parentStyle += "width: " + cm.display.wrapper.clientWidth + "px;"; + removeChildrenAndAdd(cm.display.measure, elt("div", [widget.node], null, parentStyle)); + } + return widget.height = widget.node.offsetHeight; + } + + function addLineWidget(doc, handle, node, options) { + var widget = new LineWidget(doc, node, options); + var cm = doc.cm; + if (cm && widget.noHScroll) cm.display.alignWidgets = true; + changeLine(doc, handle, "widget", function(line) { + var widgets = line.widgets || (line.widgets = []); + if (widget.insertAt == null) widgets.push(widget); + else widgets.splice(Math.min(widgets.length - 1, Math.max(0, widget.insertAt)), 0, widget); + widget.line = line; + if (cm && !lineIsHidden(doc, line)) { + var aboveVisible = heightAtLine(line) < doc.scrollTop; + updateLineHeight(line, line.height + widgetHeight(widget)); + if (aboveVisible) addToScrollPos(cm, null, widget.height); + cm.curOp.forceUpdate = true; + } + return true; + }); + return widget; + } + + // LINE DATA STRUCTURE + + // Line objects. These hold state related to a line, including + // highlighting info (the styles array). + var Line = CodeMirror.Line = function(text, markedSpans, estimateHeight) { + this.text = text; + attachMarkedSpans(this, markedSpans); + this.height = estimateHeight ? estimateHeight(this) : 1; + }; + eventMixin(Line); + Line.prototype.lineNo = function() { return lineNo(this); }; + + // Change the content (text, markers) of a line. Automatically + // invalidates cached information and tries to re-estimate the + // line's height. + function updateLine(line, text, markedSpans, estimateHeight) { + line.text = text; + if (line.stateAfter) line.stateAfter = null; + if (line.styles) line.styles = null; + if (line.order != null) line.order = null; + detachMarkedSpans(line); + attachMarkedSpans(line, markedSpans); + var estHeight = estimateHeight ? estimateHeight(line) : 1; + if (estHeight != line.height) updateLineHeight(line, estHeight); + } + + // Detach a line from the document tree and its markers. + function cleanUpLine(line) { + line.parent = null; + detachMarkedSpans(line); + } + + function extractLineClasses(type, output) { + if (type) for (;;) { + var lineClass = type.match(/(?:^|\s+)line-(background-)?(\S+)/); + if (!lineClass) break; + type = type.slice(0, lineClass.index) + type.slice(lineClass.index + lineClass[0].length); + var prop = lineClass[1] ? "bgClass" : "textClass"; + if (output[prop] == null) + output[prop] = lineClass[2]; + else if (!(new RegExp("(?:^|\s)" + lineClass[2] + "(?:$|\s)")).test(output[prop])) + output[prop] += " " + lineClass[2]; + } + return type; + } + + function callBlankLine(mode, state) { + if (mode.blankLine) return mode.blankLine(state); + if (!mode.innerMode) return; + var inner = CodeMirror.innerMode(mode, state); + if (inner.mode.blankLine) return inner.mode.blankLine(inner.state); + } + + function readToken(mode, stream, state, inner) { + for (var i = 0; i < 10; i++) { + if (inner) inner[0] = CodeMirror.innerMode(mode, state).mode; + var style = mode.token(stream, state); + if (stream.pos > stream.start) return style; + } + throw new Error("Mode " + mode.name + " failed to advance stream."); + } + + // Utility for getTokenAt and getLineTokens + function takeToken(cm, pos, precise, asArray) { + function getObj(copy) { + return {start: stream.start, end: stream.pos, + string: stream.current(), + type: style || null, + state: copy ? copyState(doc.mode, state) : state}; + } + + var doc = cm.doc, mode = doc.mode, style; + pos = clipPos(doc, pos); + var line = getLine(doc, pos.line), state = getStateBefore(cm, pos.line, precise); + var stream = new StringStream(line.text, cm.options.tabSize), tokens; + if (asArray) tokens = []; + while ((asArray || stream.pos < pos.ch) && !stream.eol()) { + stream.start = stream.pos; + style = readToken(mode, stream, state); + if (asArray) tokens.push(getObj(true)); + } + return asArray ? tokens : getObj(); + } + + // Run the given mode's parser over a line, calling f for each token. + function runMode(cm, text, mode, state, f, lineClasses, forceToEnd) { + var flattenSpans = mode.flattenSpans; + if (flattenSpans == null) flattenSpans = cm.options.flattenSpans; + var curStart = 0, curStyle = null; + var stream = new StringStream(text, cm.options.tabSize), style; + var inner = cm.options.addModeClass && [null]; + if (text == "") extractLineClasses(callBlankLine(mode, state), lineClasses); + while (!stream.eol()) { + if (stream.pos > cm.options.maxHighlightLength) { + flattenSpans = false; + if (forceToEnd) processLine(cm, text, state, stream.pos); + stream.pos = text.length; + style = null; + } else { + style = extractLineClasses(readToken(mode, stream, state, inner), lineClasses); + } + if (inner) { + var mName = inner[0].name; + if (mName) style = "m-" + (style ? mName + " " + style : mName); + } + if (!flattenSpans || curStyle != style) { + while (curStart < stream.start) { + curStart = Math.min(stream.start, curStart + 50000); + f(curStart, curStyle); + } + curStyle = style; + } + stream.start = stream.pos; + } + while (curStart < stream.pos) { + // Webkit seems to refuse to render text nodes longer than 57444 characters + var pos = Math.min(stream.pos, curStart + 50000); + f(pos, curStyle); + curStart = pos; + } + } + + // Compute a style array (an array starting with a mode generation + // -- for invalidation -- followed by pairs of end positions and + // style strings), which is used to highlight the tokens on the + // line. + function highlightLine(cm, line, state, forceToEnd) { + // A styles array always starts with a number identifying the + // mode/overlays that it is based on (for easy invalidation). + var st = [cm.state.modeGen], lineClasses = {}; + // Compute the base array of styles + runMode(cm, line.text, cm.doc.mode, state, function(end, style) { + st.push(end, style); + }, lineClasses, forceToEnd); + + // Run overlays, adjust style array. + for (var o = 0; o < cm.state.overlays.length; ++o) { + var overlay = cm.state.overlays[o], i = 1, at = 0; + runMode(cm, line.text, overlay.mode, true, function(end, style) { + var start = i; + // Ensure there's a token end at the current position, and that i points at it + while (at < end) { + var i_end = st[i]; + if (i_end > end) + st.splice(i, 1, end, st[i+1], i_end); + i += 2; + at = Math.min(end, i_end); + } + if (!style) return; + if (overlay.opaque) { + st.splice(start, i - start, end, "cm-overlay " + style); + i = start + 2; + } else { + for (; start < i; start += 2) { + var cur = st[start+1]; + st[start+1] = (cur ? cur + " " : "") + "cm-overlay " + style; + } + } + }, lineClasses); + } + + return {styles: st, classes: lineClasses.bgClass || lineClasses.textClass ? lineClasses : null}; + } + + function getLineStyles(cm, line, updateFrontier) { + if (!line.styles || line.styles[0] != cm.state.modeGen) { + var result = highlightLine(cm, line, line.stateAfter = getStateBefore(cm, lineNo(line))); + line.styles = result.styles; + if (result.classes) line.styleClasses = result.classes; + else if (line.styleClasses) line.styleClasses = null; + if (updateFrontier === cm.doc.frontier) cm.doc.frontier++; + } + return line.styles; + } + + // Lightweight form of highlight -- proceed over this line and + // update state, but don't save a style array. Used for lines that + // aren't currently visible. + function processLine(cm, text, state, startAt) { + var mode = cm.doc.mode; + var stream = new StringStream(text, cm.options.tabSize); + stream.start = stream.pos = startAt || 0; + if (text == "") callBlankLine(mode, state); + while (!stream.eol() && stream.pos <= cm.options.maxHighlightLength) { + readToken(mode, stream, state); + stream.start = stream.pos; + } + } + + // Convert a style as returned by a mode (either null, or a string + // containing one or more styles) to a CSS style. This is cached, + // and also looks for line-wide styles. + var styleToClassCache = {}, styleToClassCacheWithMode = {}; + function interpretTokenStyle(style, options) { + if (!style || /^\s*$/.test(style)) return null; + var cache = options.addModeClass ? styleToClassCacheWithMode : styleToClassCache; + return cache[style] || + (cache[style] = style.replace(/\S+/g, "cm-$&")); + } + + // Render the DOM representation of the text of a line. Also builds + // up a 'line map', which points at the DOM nodes that represent + // specific stretches of text, and is used by the measuring code. + // The returned object contains the DOM node, this map, and + // information about line-wide styles that were set by the mode. + function buildLineContent(cm, lineView) { + // The padding-right forces the element to have a 'border', which + // is needed on Webkit to be able to get line-level bounding + // rectangles for it (in measureChar). + var content = elt("span", null, null, webkit ? "padding-right: .1px" : null); + var builder = {pre: elt("pre", [content]), content: content, + col: 0, pos: 0, cm: cm, + splitSpaces: (ie || webkit) && cm.getOption("lineWrapping")}; + lineView.measure = {}; + + // Iterate over the logical lines that make up this visual line. + for (var i = 0; i <= (lineView.rest ? lineView.rest.length : 0); i++) { + var line = i ? lineView.rest[i - 1] : lineView.line, order; + builder.pos = 0; + builder.addToken = buildToken; + // Optionally wire in some hacks into the token-rendering + // algorithm, to deal with browser quirks. + if (hasBadBidiRects(cm.display.measure) && (order = getOrder(line))) + builder.addToken = buildTokenBadBidi(builder.addToken, order); + builder.map = []; + var allowFrontierUpdate = lineView != cm.display.externalMeasured && lineNo(line); + insertLineContent(line, builder, getLineStyles(cm, line, allowFrontierUpdate)); + if (line.styleClasses) { + if (line.styleClasses.bgClass) + builder.bgClass = joinClasses(line.styleClasses.bgClass, builder.bgClass || ""); + if (line.styleClasses.textClass) + builder.textClass = joinClasses(line.styleClasses.textClass, builder.textClass || ""); + } + + // Ensure at least a single node is present, for measuring. + if (builder.map.length == 0) + builder.map.push(0, 0, builder.content.appendChild(zeroWidthElement(cm.display.measure))); + + // Store the map and a cache object for the current logical line + if (i == 0) { + lineView.measure.map = builder.map; + lineView.measure.cache = {}; + } else { + (lineView.measure.maps || (lineView.measure.maps = [])).push(builder.map); + (lineView.measure.caches || (lineView.measure.caches = [])).push({}); + } + } + + // See issue #2901 + if (webkit && /\bcm-tab\b/.test(builder.content.lastChild.className)) + builder.content.className = "cm-tab-wrap-hack"; + + signal(cm, "renderLine", cm, lineView.line, builder.pre); + if (builder.pre.className) + builder.textClass = joinClasses(builder.pre.className, builder.textClass || ""); + + return builder; + } + + function defaultSpecialCharPlaceholder(ch) { + var token = elt("span", "\u2022", "cm-invalidchar"); + token.title = "\\u" + ch.charCodeAt(0).toString(16); + token.setAttribute("aria-label", token.title); + return token; + } + + // Build up the DOM representation for a single token, and add it to + // the line map. Takes care to render special characters separately. + function buildToken(builder, text, style, startStyle, endStyle, title, css) { + if (!text) return; + var displayText = builder.splitSpaces ? text.replace(/ {3,}/g, splitSpaces) : text; + var special = builder.cm.state.specialChars, mustWrap = false; + if (!special.test(text)) { + builder.col += text.length; + var content = document.createTextNode(displayText); + builder.map.push(builder.pos, builder.pos + text.length, content); + if (ie && ie_version < 9) mustWrap = true; + builder.pos += text.length; + } else { + var content = document.createDocumentFragment(), pos = 0; + while (true) { + special.lastIndex = pos; + var m = special.exec(text); + var skipped = m ? m.index - pos : text.length - pos; + if (skipped) { + var txt = document.createTextNode(displayText.slice(pos, pos + skipped)); + if (ie && ie_version < 9) content.appendChild(elt("span", [txt])); + else content.appendChild(txt); + builder.map.push(builder.pos, builder.pos + skipped, txt); + builder.col += skipped; + builder.pos += skipped; + } + if (!m) break; + pos += skipped + 1; + if (m[0] == "\t") { + var tabSize = builder.cm.options.tabSize, tabWidth = tabSize - builder.col % tabSize; + var txt = content.appendChild(elt("span", spaceStr(tabWidth), "cm-tab")); + txt.setAttribute("role", "presentation"); + txt.setAttribute("cm-text", "\t"); + builder.col += tabWidth; + } else { + var txt = builder.cm.options.specialCharPlaceholder(m[0]); + txt.setAttribute("cm-text", m[0]); + if (ie && ie_version < 9) content.appendChild(elt("span", [txt])); + else content.appendChild(txt); + builder.col += 1; + } + builder.map.push(builder.pos, builder.pos + 1, txt); + builder.pos++; + } + } + if (style || startStyle || endStyle || mustWrap || css) { + var fullStyle = style || ""; + if (startStyle) fullStyle += startStyle; + if (endStyle) fullStyle += endStyle; + var token = elt("span", [content], fullStyle, css); + if (title) token.title = title; + return builder.content.appendChild(token); + } + builder.content.appendChild(content); + } + + function splitSpaces(old) { + var out = " "; + for (var i = 0; i < old.length - 2; ++i) out += i % 2 ? " " : "\u00a0"; + out += " "; + return out; + } + + // Work around nonsense dimensions being reported for stretches of + // right-to-left text. + function buildTokenBadBidi(inner, order) { + return function(builder, text, style, startStyle, endStyle, title, css) { + style = style ? style + " cm-force-border" : "cm-force-border"; + var start = builder.pos, end = start + text.length; + for (;;) { + // Find the part that overlaps with the start of this text + for (var i = 0; i < order.length; i++) { + var part = order[i]; + if (part.to > start && part.from <= start) break; + } + if (part.to >= end) return inner(builder, text, style, startStyle, endStyle, title, css); + inner(builder, text.slice(0, part.to - start), style, startStyle, null, title, css); + startStyle = null; + text = text.slice(part.to - start); + start = part.to; + } + }; + } + + function buildCollapsedSpan(builder, size, marker, ignoreWidget) { + var widget = !ignoreWidget && marker.widgetNode; + if (widget) builder.map.push(builder.pos, builder.pos + size, widget); + if (!ignoreWidget && builder.cm.display.input.needsContentAttribute) { + if (!widget) + widget = builder.content.appendChild(document.createElement("span")); + widget.setAttribute("cm-marker", marker.id); + } + if (widget) { + builder.cm.display.input.setUneditable(widget); + builder.content.appendChild(widget); + } + builder.pos += size; + } + + // Outputs a number of spans to make up a line, taking highlighting + // and marked text into account. + function insertLineContent(line, builder, styles) { + var spans = line.markedSpans, allText = line.text, at = 0; + if (!spans) { + for (var i = 1; i < styles.length; i+=2) + builder.addToken(builder, allText.slice(at, at = styles[i]), interpretTokenStyle(styles[i+1], builder.cm.options)); + return; + } + + var len = allText.length, pos = 0, i = 1, text = "", style, css; + var nextChange = 0, spanStyle, spanEndStyle, spanStartStyle, title, collapsed; + for (;;) { + if (nextChange == pos) { // Update current marker set + spanStyle = spanEndStyle = spanStartStyle = title = css = ""; + collapsed = null; nextChange = Infinity; + var foundBookmarks = []; + for (var j = 0; j < spans.length; ++j) { + var sp = spans[j], m = sp.marker; + if (m.type == "bookmark" && sp.from == pos && m.widgetNode) { + foundBookmarks.push(m); + } else if (sp.from <= pos && (sp.to == null || sp.to > pos || m.collapsed && sp.to == pos && sp.from == pos)) { + if (sp.to != null && sp.to != pos && nextChange > sp.to) { + nextChange = sp.to; + spanEndStyle = ""; + } + if (m.className) spanStyle += " " + m.className; + if (m.css) css = m.css; + if (m.startStyle && sp.from == pos) spanStartStyle += " " + m.startStyle; + if (m.endStyle && sp.to == nextChange) spanEndStyle += " " + m.endStyle; + if (m.title && !title) title = m.title; + if (m.collapsed && (!collapsed || compareCollapsedMarkers(collapsed.marker, m) < 0)) + collapsed = sp; + } else if (sp.from > pos && nextChange > sp.from) { + nextChange = sp.from; + } + } + if (collapsed && (collapsed.from || 0) == pos) { + buildCollapsedSpan(builder, (collapsed.to == null ? len + 1 : collapsed.to) - pos, + collapsed.marker, collapsed.from == null); + if (collapsed.to == null) return; + if (collapsed.to == pos) collapsed = false; + } + if (!collapsed && foundBookmarks.length) for (var j = 0; j < foundBookmarks.length; ++j) + buildCollapsedSpan(builder, 0, foundBookmarks[j]); + } + if (pos >= len) break; + + var upto = Math.min(len, nextChange); + while (true) { + if (text) { + var end = pos + text.length; + if (!collapsed) { + var tokenText = end > upto ? text.slice(0, upto - pos) : text; + builder.addToken(builder, tokenText, style ? style + spanStyle : spanStyle, + spanStartStyle, pos + tokenText.length == nextChange ? spanEndStyle : "", title, css); + } + if (end >= upto) {text = text.slice(upto - pos); pos = upto; break;} + pos = end; + spanStartStyle = ""; + } + text = allText.slice(at, at = styles[i++]); + style = interpretTokenStyle(styles[i++], builder.cm.options); + } + } + } + + // DOCUMENT DATA STRUCTURE + + // By default, updates that start and end at the beginning of a line + // are treated specially, in order to make the association of line + // widgets and marker elements with the text behave more intuitive. + function isWholeLineUpdate(doc, change) { + return change.from.ch == 0 && change.to.ch == 0 && lst(change.text) == "" && + (!doc.cm || doc.cm.options.wholeLineUpdateBefore); + } + + // Perform a change on the document data structure. + function updateDoc(doc, change, markedSpans, estimateHeight) { + function spansFor(n) {return markedSpans ? markedSpans[n] : null;} + function update(line, text, spans) { + updateLine(line, text, spans, estimateHeight); + signalLater(line, "change", line, change); + } + function linesFor(start, end) { + for (var i = start, result = []; i < end; ++i) + result.push(new Line(text[i], spansFor(i), estimateHeight)); + return result; + } + + var from = change.from, to = change.to, text = change.text; + var firstLine = getLine(doc, from.line), lastLine = getLine(doc, to.line); + var lastText = lst(text), lastSpans = spansFor(text.length - 1), nlines = to.line - from.line; + + // Adjust the line structure + if (change.full) { + doc.insert(0, linesFor(0, text.length)); + doc.remove(text.length, doc.size - text.length); + } else if (isWholeLineUpdate(doc, change)) { + // This is a whole-line replace. Treated specially to make + // sure line objects move the way they are supposed to. + var added = linesFor(0, text.length - 1); + update(lastLine, lastLine.text, lastSpans); + if (nlines) doc.remove(from.line, nlines); + if (added.length) doc.insert(from.line, added); + } else if (firstLine == lastLine) { + if (text.length == 1) { + update(firstLine, firstLine.text.slice(0, from.ch) + lastText + firstLine.text.slice(to.ch), lastSpans); + } else { + var added = linesFor(1, text.length - 1); + added.push(new Line(lastText + firstLine.text.slice(to.ch), lastSpans, estimateHeight)); + update(firstLine, firstLine.text.slice(0, from.ch) + text[0], spansFor(0)); + doc.insert(from.line + 1, added); + } + } else if (text.length == 1) { + update(firstLine, firstLine.text.slice(0, from.ch) + text[0] + lastLine.text.slice(to.ch), spansFor(0)); + doc.remove(from.line + 1, nlines); + } else { + update(firstLine, firstLine.text.slice(0, from.ch) + text[0], spansFor(0)); + update(lastLine, lastText + lastLine.text.slice(to.ch), lastSpans); + var added = linesFor(1, text.length - 1); + if (nlines > 1) doc.remove(from.line + 1, nlines - 1); + doc.insert(from.line + 1, added); + } + + signalLater(doc, "change", doc, change); + } + + // The document is represented as a BTree consisting of leaves, with + // chunk of lines in them, and branches, with up to ten leaves or + // other branch nodes below them. The top node is always a branch + // node, and is the document object itself (meaning it has + // additional methods and properties). + // + // All nodes have parent links. The tree is used both to go from + // line numbers to line objects, and to go from objects to numbers. + // It also indexes by height, and is used to convert between height + // and line object, and to find the total height of the document. + // + // See also http://marijnhaverbeke.nl/blog/codemirror-line-tree.html + + function LeafChunk(lines) { + this.lines = lines; + this.parent = null; + for (var i = 0, height = 0; i < lines.length; ++i) { + lines[i].parent = this; + height += lines[i].height; + } + this.height = height; + } + + LeafChunk.prototype = { + chunkSize: function() { return this.lines.length; }, + // Remove the n lines at offset 'at'. + removeInner: function(at, n) { + for (var i = at, e = at + n; i < e; ++i) { + var line = this.lines[i]; + this.height -= line.height; + cleanUpLine(line); + signalLater(line, "delete"); + } + this.lines.splice(at, n); + }, + // Helper used to collapse a small branch into a single leaf. + collapse: function(lines) { + lines.push.apply(lines, this.lines); + }, + // Insert the given array of lines at offset 'at', count them as + // having the given height. + insertInner: function(at, lines, height) { + this.height += height; + this.lines = this.lines.slice(0, at).concat(lines).concat(this.lines.slice(at)); + for (var i = 0; i < lines.length; ++i) lines[i].parent = this; + }, + // Used to iterate over a part of the tree. + iterN: function(at, n, op) { + for (var e = at + n; at < e; ++at) + if (op(this.lines[at])) return true; + } + }; + + function BranchChunk(children) { + this.children = children; + var size = 0, height = 0; + for (var i = 0; i < children.length; ++i) { + var ch = children[i]; + size += ch.chunkSize(); height += ch.height; + ch.parent = this; + } + this.size = size; + this.height = height; + this.parent = null; + } + + BranchChunk.prototype = { + chunkSize: function() { return this.size; }, + removeInner: function(at, n) { + this.size -= n; + for (var i = 0; i < this.children.length; ++i) { + var child = this.children[i], sz = child.chunkSize(); + if (at < sz) { + var rm = Math.min(n, sz - at), oldHeight = child.height; + child.removeInner(at, rm); + this.height -= oldHeight - child.height; + if (sz == rm) { this.children.splice(i--, 1); child.parent = null; } + if ((n -= rm) == 0) break; + at = 0; + } else at -= sz; + } + // If the result is smaller than 25 lines, ensure that it is a + // single leaf node. + if (this.size - n < 25 && + (this.children.length > 1 || !(this.children[0] instanceof LeafChunk))) { + var lines = []; + this.collapse(lines); + this.children = [new LeafChunk(lines)]; + this.children[0].parent = this; + } + }, + collapse: function(lines) { + for (var i = 0; i < this.children.length; ++i) this.children[i].collapse(lines); + }, + insertInner: function(at, lines, height) { + this.size += lines.length; + this.height += height; + for (var i = 0; i < this.children.length; ++i) { + var child = this.children[i], sz = child.chunkSize(); + if (at <= sz) { + child.insertInner(at, lines, height); + if (child.lines && child.lines.length > 50) { + while (child.lines.length > 50) { + var spilled = child.lines.splice(child.lines.length - 25, 25); + var newleaf = new LeafChunk(spilled); + child.height -= newleaf.height; + this.children.splice(i + 1, 0, newleaf); + newleaf.parent = this; + } + this.maybeSpill(); + } + break; + } + at -= sz; + } + }, + // When a node has grown, check whether it should be split. + maybeSpill: function() { + if (this.children.length <= 10) return; + var me = this; + do { + var spilled = me.children.splice(me.children.length - 5, 5); + var sibling = new BranchChunk(spilled); + if (!me.parent) { // Become the parent node + var copy = new BranchChunk(me.children); + copy.parent = me; + me.children = [copy, sibling]; + me = copy; + } else { + me.size -= sibling.size; + me.height -= sibling.height; + var myIndex = indexOf(me.parent.children, me); + me.parent.children.splice(myIndex + 1, 0, sibling); + } + sibling.parent = me.parent; + } while (me.children.length > 10); + me.parent.maybeSpill(); + }, + iterN: function(at, n, op) { + for (var i = 0; i < this.children.length; ++i) { + var child = this.children[i], sz = child.chunkSize(); + if (at < sz) { + var used = Math.min(n, sz - at); + if (child.iterN(at, used, op)) return true; + if ((n -= used) == 0) break; + at = 0; + } else at -= sz; + } + } + }; + + var nextDocId = 0; + var Doc = CodeMirror.Doc = function(text, mode, firstLine) { + if (!(this instanceof Doc)) return new Doc(text, mode, firstLine); + if (firstLine == null) firstLine = 0; + + BranchChunk.call(this, [new LeafChunk([new Line("", null)])]); + this.first = firstLine; + this.scrollTop = this.scrollLeft = 0; + this.cantEdit = false; + this.cleanGeneration = 1; + this.frontier = firstLine; + var start = Pos(firstLine, 0); + this.sel = simpleSelection(start); + this.history = new History(null); + this.id = ++nextDocId; + this.modeOption = mode; + + if (typeof text == "string") text = splitLines(text); + updateDoc(this, {from: start, to: start, text: text}); + setSelection(this, simpleSelection(start), sel_dontScroll); + }; + + Doc.prototype = createObj(BranchChunk.prototype, { + constructor: Doc, + // Iterate over the document. Supports two forms -- with only one + // argument, it calls that for each line in the document. With + // three, it iterates over the range given by the first two (with + // the second being non-inclusive). + iter: function(from, to, op) { + if (op) this.iterN(from - this.first, to - from, op); + else this.iterN(this.first, this.first + this.size, from); + }, + + // Non-public interface for adding and removing lines. + insert: function(at, lines) { + var height = 0; + for (var i = 0; i < lines.length; ++i) height += lines[i].height; + this.insertInner(at - this.first, lines, height); + }, + remove: function(at, n) { this.removeInner(at - this.first, n); }, + + // From here, the methods are part of the public interface. Most + // are also available from CodeMirror (editor) instances. + + getValue: function(lineSep) { + var lines = getLines(this, this.first, this.first + this.size); + if (lineSep === false) return lines; + return lines.join(lineSep || "\n"); + }, + setValue: docMethodOp(function(code) { + var top = Pos(this.first, 0), last = this.first + this.size - 1; + makeChange(this, {from: top, to: Pos(last, getLine(this, last).text.length), + text: splitLines(code), origin: "setValue", full: true}, true); + setSelection(this, simpleSelection(top)); + }), + replaceRange: function(code, from, to, origin) { + from = clipPos(this, from); + to = to ? clipPos(this, to) : from; + replaceRange(this, code, from, to, origin); + }, + getRange: function(from, to, lineSep) { + var lines = getBetween(this, clipPos(this, from), clipPos(this, to)); + if (lineSep === false) return lines; + return lines.join(lineSep || "\n"); + }, + + getLine: function(line) {var l = this.getLineHandle(line); return l && l.text;}, + + getLineHandle: function(line) {if (isLine(this, line)) return getLine(this, line);}, + getLineNumber: function(line) {return lineNo(line);}, + + getLineHandleVisualStart: function(line) { + if (typeof line == "number") line = getLine(this, line); + return visualLine(line); + }, + + lineCount: function() {return this.size;}, + firstLine: function() {return this.first;}, + lastLine: function() {return this.first + this.size - 1;}, + + clipPos: function(pos) {return clipPos(this, pos);}, + + getCursor: function(start) { + var range = this.sel.primary(), pos; + if (start == null || start == "head") pos = range.head; + else if (start == "anchor") pos = range.anchor; + else if (start == "end" || start == "to" || start === false) pos = range.to(); + else pos = range.from(); + return pos; + }, + listSelections: function() { return this.sel.ranges; }, + somethingSelected: function() {return this.sel.somethingSelected();}, + + setCursor: docMethodOp(function(line, ch, options) { + setSimpleSelection(this, clipPos(this, typeof line == "number" ? Pos(line, ch || 0) : line), null, options); + }), + setSelection: docMethodOp(function(anchor, head, options) { + setSimpleSelection(this, clipPos(this, anchor), clipPos(this, head || anchor), options); + }), + extendSelection: docMethodOp(function(head, other, options) { + extendSelection(this, clipPos(this, head), other && clipPos(this, other), options); + }), + extendSelections: docMethodOp(function(heads, options) { + extendSelections(this, clipPosArray(this, heads, options)); + }), + extendSelectionsBy: docMethodOp(function(f, options) { + extendSelections(this, map(this.sel.ranges, f), options); + }), + setSelections: docMethodOp(function(ranges, primary, options) { + if (!ranges.length) return; + for (var i = 0, out = []; i < ranges.length; i++) + out[i] = new Range(clipPos(this, ranges[i].anchor), + clipPos(this, ranges[i].head)); + if (primary == null) primary = Math.min(ranges.length - 1, this.sel.primIndex); + setSelection(this, normalizeSelection(out, primary), options); + }), + addSelection: docMethodOp(function(anchor, head, options) { + var ranges = this.sel.ranges.slice(0); + ranges.push(new Range(clipPos(this, anchor), clipPos(this, head || anchor))); + setSelection(this, normalizeSelection(ranges, ranges.length - 1), options); + }), + + getSelection: function(lineSep) { + var ranges = this.sel.ranges, lines; + for (var i = 0; i < ranges.length; i++) { + var sel = getBetween(this, ranges[i].from(), ranges[i].to()); + lines = lines ? lines.concat(sel) : sel; + } + if (lineSep === false) return lines; + else return lines.join(lineSep || "\n"); + }, + getSelections: function(lineSep) { + var parts = [], ranges = this.sel.ranges; + for (var i = 0; i < ranges.length; i++) { + var sel = getBetween(this, ranges[i].from(), ranges[i].to()); + if (lineSep !== false) sel = sel.join(lineSep || "\n"); + parts[i] = sel; + } + return parts; + }, + replaceSelection: function(code, collapse, origin) { + var dup = []; + for (var i = 0; i < this.sel.ranges.length; i++) + dup[i] = code; + this.replaceSelections(dup, collapse, origin || "+input"); + }, + replaceSelections: docMethodOp(function(code, collapse, origin) { + var changes = [], sel = this.sel; + for (var i = 0; i < sel.ranges.length; i++) { + var range = sel.ranges[i]; + changes[i] = {from: range.from(), to: range.to(), text: splitLines(code[i]), origin: origin}; + } + var newSel = collapse && collapse != "end" && computeReplacedSel(this, changes, collapse); + for (var i = changes.length - 1; i >= 0; i--) + makeChange(this, changes[i]); + if (newSel) setSelectionReplaceHistory(this, newSel); + else if (this.cm) ensureCursorVisible(this.cm); + }), + undo: docMethodOp(function() {makeChangeFromHistory(this, "undo");}), + redo: docMethodOp(function() {makeChangeFromHistory(this, "redo");}), + undoSelection: docMethodOp(function() {makeChangeFromHistory(this, "undo", true);}), + redoSelection: docMethodOp(function() {makeChangeFromHistory(this, "redo", true);}), + + setExtending: function(val) {this.extend = val;}, + getExtending: function() {return this.extend;}, + + historySize: function() { + var hist = this.history, done = 0, undone = 0; + for (var i = 0; i < hist.done.length; i++) if (!hist.done[i].ranges) ++done; + for (var i = 0; i < hist.undone.length; i++) if (!hist.undone[i].ranges) ++undone; + return {undo: done, redo: undone}; + }, + clearHistory: function() {this.history = new History(this.history.maxGeneration);}, + + markClean: function() { + this.cleanGeneration = this.changeGeneration(true); + }, + changeGeneration: function(forceSplit) { + if (forceSplit) + this.history.lastOp = this.history.lastSelOp = this.history.lastOrigin = null; + return this.history.generation; + }, + isClean: function (gen) { + return this.history.generation == (gen || this.cleanGeneration); + }, + + getHistory: function() { + return {done: copyHistoryArray(this.history.done), + undone: copyHistoryArray(this.history.undone)}; + }, + setHistory: function(histData) { + var hist = this.history = new History(this.history.maxGeneration); + hist.done = copyHistoryArray(histData.done.slice(0), null, true); + hist.undone = copyHistoryArray(histData.undone.slice(0), null, true); + }, + + addLineClass: docMethodOp(function(handle, where, cls) { + return changeLine(this, handle, where == "gutter" ? "gutter" : "class", function(line) { + var prop = where == "text" ? "textClass" + : where == "background" ? "bgClass" + : where == "gutter" ? "gutterClass" : "wrapClass"; + if (!line[prop]) line[prop] = cls; + else if (classTest(cls).test(line[prop])) return false; + else line[prop] += " " + cls; + return true; + }); + }), + removeLineClass: docMethodOp(function(handle, where, cls) { + return changeLine(this, handle, where == "gutter" ? "gutter" : "class", function(line) { + var prop = where == "text" ? "textClass" + : where == "background" ? "bgClass" + : where == "gutter" ? "gutterClass" : "wrapClass"; + var cur = line[prop]; + if (!cur) return false; + else if (cls == null) line[prop] = null; + else { + var found = cur.match(classTest(cls)); + if (!found) return false; + var end = found.index + found[0].length; + line[prop] = cur.slice(0, found.index) + (!found.index || end == cur.length ? "" : " ") + cur.slice(end) || null; + } + return true; + }); + }), + + addLineWidget: docMethodOp(function(handle, node, options) { + return addLineWidget(this, handle, node, options); + }), + removeLineWidget: function(widget) { widget.clear(); }, + + markText: function(from, to, options) { + return markText(this, clipPos(this, from), clipPos(this, to), options, "range"); + }, + setBookmark: function(pos, options) { + var realOpts = {replacedWith: options && (options.nodeType == null ? options.widget : options), + insertLeft: options && options.insertLeft, + clearWhenEmpty: false, shared: options && options.shared, + handleMouseEvents: options && options.handleMouseEvents}; + pos = clipPos(this, pos); + return markText(this, pos, pos, realOpts, "bookmark"); + }, + findMarksAt: function(pos) { + pos = clipPos(this, pos); + var markers = [], spans = getLine(this, pos.line).markedSpans; + if (spans) for (var i = 0; i < spans.length; ++i) { + var span = spans[i]; + if ((span.from == null || span.from <= pos.ch) && + (span.to == null || span.to >= pos.ch)) + markers.push(span.marker.parent || span.marker); + } + return markers; + }, + findMarks: function(from, to, filter) { + from = clipPos(this, from); to = clipPos(this, to); + var found = [], lineNo = from.line; + this.iter(from.line, to.line + 1, function(line) { + var spans = line.markedSpans; + if (spans) for (var i = 0; i < spans.length; i++) { + var span = spans[i]; + if (!(lineNo == from.line && from.ch > span.to || + span.from == null && lineNo != from.line|| + lineNo == to.line && span.from > to.ch) && + (!filter || filter(span.marker))) + found.push(span.marker.parent || span.marker); + } + ++lineNo; + }); + return found; + }, + getAllMarks: function() { + var markers = []; + this.iter(function(line) { + var sps = line.markedSpans; + if (sps) for (var i = 0; i < sps.length; ++i) + if (sps[i].from != null) markers.push(sps[i].marker); + }); + return markers; + }, + + posFromIndex: function(off) { + var ch, lineNo = this.first; + this.iter(function(line) { + var sz = line.text.length + 1; + if (sz > off) { ch = off; return true; } + off -= sz; + ++lineNo; + }); + return clipPos(this, Pos(lineNo, ch)); + }, + indexFromPos: function (coords) { + coords = clipPos(this, coords); + var index = coords.ch; + if (coords.line < this.first || coords.ch < 0) return 0; + this.iter(this.first, coords.line, function (line) { + index += line.text.length + 1; + }); + return index; + }, + + copy: function(copyHistory) { + var doc = new Doc(getLines(this, this.first, this.first + this.size), this.modeOption, this.first); + doc.scrollTop = this.scrollTop; doc.scrollLeft = this.scrollLeft; + doc.sel = this.sel; + doc.extend = false; + if (copyHistory) { + doc.history.undoDepth = this.history.undoDepth; + doc.setHistory(this.getHistory()); + } + return doc; + }, + + linkedDoc: function(options) { + if (!options) options = {}; + var from = this.first, to = this.first + this.size; + if (options.from != null && options.from > from) from = options.from; + if (options.to != null && options.to < to) to = options.to; + var copy = new Doc(getLines(this, from, to), options.mode || this.modeOption, from); + if (options.sharedHist) copy.history = this.history; + (this.linked || (this.linked = [])).push({doc: copy, sharedHist: options.sharedHist}); + copy.linked = [{doc: this, isParent: true, sharedHist: options.sharedHist}]; + copySharedMarkers(copy, findSharedMarkers(this)); + return copy; + }, + unlinkDoc: function(other) { + if (other instanceof CodeMirror) other = other.doc; + if (this.linked) for (var i = 0; i < this.linked.length; ++i) { + var link = this.linked[i]; + if (link.doc != other) continue; + this.linked.splice(i, 1); + other.unlinkDoc(this); + detachSharedMarkers(findSharedMarkers(this)); + break; + } + // If the histories were shared, split them again + if (other.history == this.history) { + var splitIds = [other.id]; + linkedDocs(other, function(doc) {splitIds.push(doc.id);}, true); + other.history = new History(null); + other.history.done = copyHistoryArray(this.history.done, splitIds); + other.history.undone = copyHistoryArray(this.history.undone, splitIds); + } + }, + iterLinkedDocs: function(f) {linkedDocs(this, f);}, + + getMode: function() {return this.mode;}, + getEditor: function() {return this.cm;} + }); + + // Public alias. + Doc.prototype.eachLine = Doc.prototype.iter; + + // Set up methods on CodeMirror's prototype to redirect to the editor's document. + var dontDelegate = "iter insert remove copy getEditor".split(" "); + for (var prop in Doc.prototype) if (Doc.prototype.hasOwnProperty(prop) && indexOf(dontDelegate, prop) < 0) + CodeMirror.prototype[prop] = (function(method) { + return function() {return method.apply(this.doc, arguments);}; + })(Doc.prototype[prop]); + + eventMixin(Doc); + + // Call f for all linked documents. + function linkedDocs(doc, f, sharedHistOnly) { + function propagate(doc, skip, sharedHist) { + if (doc.linked) for (var i = 0; i < doc.linked.length; ++i) { + var rel = doc.linked[i]; + if (rel.doc == skip) continue; + var shared = sharedHist && rel.sharedHist; + if (sharedHistOnly && !shared) continue; + f(rel.doc, shared); + propagate(rel.doc, doc, shared); + } + } + propagate(doc, null, true); + } + + // Attach a document to an editor. + function attachDoc(cm, doc) { + if (doc.cm) throw new Error("This document is already in use."); + cm.doc = doc; + doc.cm = cm; + estimateLineHeights(cm); + loadMode(cm); + if (!cm.options.lineWrapping) findMaxLine(cm); + cm.options.mode = doc.modeOption; + regChange(cm); + } + + // LINE UTILITIES + + // Find the line object corresponding to the given line number. + function getLine(doc, n) { + n -= doc.first; + if (n < 0 || n >= doc.size) throw new Error("There is no line " + (n + doc.first) + " in the document."); + for (var chunk = doc; !chunk.lines;) { + for (var i = 0;; ++i) { + var child = chunk.children[i], sz = child.chunkSize(); + if (n < sz) { chunk = child; break; } + n -= sz; + } + } + return chunk.lines[n]; + } + + // Get the part of a document between two positions, as an array of + // strings. + function getBetween(doc, start, end) { + var out = [], n = start.line; + doc.iter(start.line, end.line + 1, function(line) { + var text = line.text; + if (n == end.line) text = text.slice(0, end.ch); + if (n == start.line) text = text.slice(start.ch); + out.push(text); + ++n; + }); + return out; + } + // Get the lines between from and to, as array of strings. + function getLines(doc, from, to) { + var out = []; + doc.iter(from, to, function(line) { out.push(line.text); }); + return out; + } + + // Update the height of a line, propagating the height change + // upwards to parent nodes. + function updateLineHeight(line, height) { + var diff = height - line.height; + if (diff) for (var n = line; n; n = n.parent) n.height += diff; + } + + // Given a line object, find its line number by walking up through + // its parent links. + function lineNo(line) { + if (line.parent == null) return null; + var cur = line.parent, no = indexOf(cur.lines, line); + for (var chunk = cur.parent; chunk; cur = chunk, chunk = chunk.parent) { + for (var i = 0;; ++i) { + if (chunk.children[i] == cur) break; + no += chunk.children[i].chunkSize(); + } + } + return no + cur.first; + } + + // Find the line at the given vertical position, using the height + // information in the document tree. + function lineAtHeight(chunk, h) { + var n = chunk.first; + outer: do { + for (var i = 0; i < chunk.children.length; ++i) { + var child = chunk.children[i], ch = child.height; + if (h < ch) { chunk = child; continue outer; } + h -= ch; + n += child.chunkSize(); + } + return n; + } while (!chunk.lines); + for (var i = 0; i < chunk.lines.length; ++i) { + var line = chunk.lines[i], lh = line.height; + if (h < lh) break; + h -= lh; + } + return n + i; + } + + + // Find the height above the given line. + function heightAtLine(lineObj) { + lineObj = visualLine(lineObj); + + var h = 0, chunk = lineObj.parent; + for (var i = 0; i < chunk.lines.length; ++i) { + var line = chunk.lines[i]; + if (line == lineObj) break; + else h += line.height; + } + for (var p = chunk.parent; p; chunk = p, p = chunk.parent) { + for (var i = 0; i < p.children.length; ++i) { + var cur = p.children[i]; + if (cur == chunk) break; + else h += cur.height; + } + } + return h; + } + + // Get the bidi ordering for the given line (and cache it). Returns + // false for lines that are fully left-to-right, and an array of + // BidiSpan objects otherwise. + function getOrder(line) { + var order = line.order; + if (order == null) order = line.order = bidiOrdering(line.text); + return order; + } + + // HISTORY + + function History(startGen) { + // Arrays of change events and selections. Doing something adds an + // event to done and clears undo. Undoing moves events from done + // to undone, redoing moves them in the other direction. + this.done = []; this.undone = []; + this.undoDepth = Infinity; + // Used to track when changes can be merged into a single undo + // event + this.lastModTime = this.lastSelTime = 0; + this.lastOp = this.lastSelOp = null; + this.lastOrigin = this.lastSelOrigin = null; + // Used by the isClean() method + this.generation = this.maxGeneration = startGen || 1; + } + + // Create a history change event from an updateDoc-style change + // object. + function historyChangeFromChange(doc, change) { + var histChange = {from: copyPos(change.from), to: changeEnd(change), text: getBetween(doc, change.from, change.to)}; + attachLocalSpans(doc, histChange, change.from.line, change.to.line + 1); + linkedDocs(doc, function(doc) {attachLocalSpans(doc, histChange, change.from.line, change.to.line + 1);}, true); + return histChange; + } + + // Pop all selection events off the end of a history array. Stop at + // a change event. + function clearSelectionEvents(array) { + while (array.length) { + var last = lst(array); + if (last.ranges) array.pop(); + else break; + } + } + + // Find the top change event in the history. Pop off selection + // events that are in the way. + function lastChangeEvent(hist, force) { + if (force) { + clearSelectionEvents(hist.done); + return lst(hist.done); + } else if (hist.done.length && !lst(hist.done).ranges) { + return lst(hist.done); + } else if (hist.done.length > 1 && !hist.done[hist.done.length - 2].ranges) { + hist.done.pop(); + return lst(hist.done); + } + } + + // Register a change in the history. Merges changes that are within + // a single operation, ore are close together with an origin that + // allows merging (starting with "+") into a single event. + function addChangeToHistory(doc, change, selAfter, opId) { + var hist = doc.history; + hist.undone.length = 0; + var time = +new Date, cur; + + if ((hist.lastOp == opId || + hist.lastOrigin == change.origin && change.origin && + ((change.origin.charAt(0) == "+" && doc.cm && hist.lastModTime > time - doc.cm.options.historyEventDelay) || + change.origin.charAt(0) == "*")) && + (cur = lastChangeEvent(hist, hist.lastOp == opId))) { + // Merge this change into the last event + var last = lst(cur.changes); + if (cmp(change.from, change.to) == 0 && cmp(change.from, last.to) == 0) { + // Optimized case for simple insertion -- don't want to add + // new changesets for every character typed + last.to = changeEnd(change); + } else { + // Add new sub-event + cur.changes.push(historyChangeFromChange(doc, change)); + } + } else { + // Can not be merged, start a new event. + var before = lst(hist.done); + if (!before || !before.ranges) + pushSelectionToHistory(doc.sel, hist.done); + cur = {changes: [historyChangeFromChange(doc, change)], + generation: hist.generation}; + hist.done.push(cur); + while (hist.done.length > hist.undoDepth) { + hist.done.shift(); + if (!hist.done[0].ranges) hist.done.shift(); + } + } + hist.done.push(selAfter); + hist.generation = ++hist.maxGeneration; + hist.lastModTime = hist.lastSelTime = time; + hist.lastOp = hist.lastSelOp = opId; + hist.lastOrigin = hist.lastSelOrigin = change.origin; + + if (!last) signal(doc, "historyAdded"); + } + + function selectionEventCanBeMerged(doc, origin, prev, sel) { + var ch = origin.charAt(0); + return ch == "*" || + ch == "+" && + prev.ranges.length == sel.ranges.length && + prev.somethingSelected() == sel.somethingSelected() && + new Date - doc.history.lastSelTime <= (doc.cm ? doc.cm.options.historyEventDelay : 500); + } + + // Called whenever the selection changes, sets the new selection as + // the pending selection in the history, and pushes the old pending + // selection into the 'done' array when it was significantly + // different (in number of selected ranges, emptiness, or time). + function addSelectionToHistory(doc, sel, opId, options) { + var hist = doc.history, origin = options && options.origin; + + // A new event is started when the previous origin does not match + // the current, or the origins don't allow matching. Origins + // starting with * are always merged, those starting with + are + // merged when similar and close together in time. + if (opId == hist.lastSelOp || + (origin && hist.lastSelOrigin == origin && + (hist.lastModTime == hist.lastSelTime && hist.lastOrigin == origin || + selectionEventCanBeMerged(doc, origin, lst(hist.done), sel)))) + hist.done[hist.done.length - 1] = sel; + else + pushSelectionToHistory(sel, hist.done); + + hist.lastSelTime = +new Date; + hist.lastSelOrigin = origin; + hist.lastSelOp = opId; + if (options && options.clearRedo !== false) + clearSelectionEvents(hist.undone); + } + + function pushSelectionToHistory(sel, dest) { + var top = lst(dest); + if (!(top && top.ranges && top.equals(sel))) + dest.push(sel); + } + + // Used to store marked span information in the history. + function attachLocalSpans(doc, change, from, to) { + var existing = change["spans_" + doc.id], n = 0; + doc.iter(Math.max(doc.first, from), Math.min(doc.first + doc.size, to), function(line) { + if (line.markedSpans) + (existing || (existing = change["spans_" + doc.id] = {}))[n] = line.markedSpans; + ++n; + }); + } + + // When un/re-doing restores text containing marked spans, those + // that have been explicitly cleared should not be restored. + function removeClearedSpans(spans) { + if (!spans) return null; + for (var i = 0, out; i < spans.length; ++i) { + if (spans[i].marker.explicitlyCleared) { if (!out) out = spans.slice(0, i); } + else if (out) out.push(spans[i]); + } + return !out ? spans : out.length ? out : null; + } + + // Retrieve and filter the old marked spans stored in a change event. + function getOldSpans(doc, change) { + var found = change["spans_" + doc.id]; + if (!found) return null; + for (var i = 0, nw = []; i < change.text.length; ++i) + nw.push(removeClearedSpans(found[i])); + return nw; + } + + // Used both to provide a JSON-safe object in .getHistory, and, when + // detaching a document, to split the history in two + function copyHistoryArray(events, newGroup, instantiateSel) { + for (var i = 0, copy = []; i < events.length; ++i) { + var event = events[i]; + if (event.ranges) { + copy.push(instantiateSel ? Selection.prototype.deepCopy.call(event) : event); + continue; + } + var changes = event.changes, newChanges = []; + copy.push({changes: newChanges}); + for (var j = 0; j < changes.length; ++j) { + var change = changes[j], m; + newChanges.push({from: change.from, to: change.to, text: change.text}); + if (newGroup) for (var prop in change) if (m = prop.match(/^spans_(\d+)$/)) { + if (indexOf(newGroup, Number(m[1])) > -1) { + lst(newChanges)[prop] = change[prop]; + delete change[prop]; + } + } + } + } + return copy; + } + + // Rebasing/resetting history to deal with externally-sourced changes + + function rebaseHistSelSingle(pos, from, to, diff) { + if (to < pos.line) { + pos.line += diff; + } else if (from < pos.line) { + pos.line = from; + pos.ch = 0; + } + } + + // Tries to rebase an array of history events given a change in the + // document. If the change touches the same lines as the event, the + // event, and everything 'behind' it, is discarded. If the change is + // before the event, the event's positions are updated. Uses a + // copy-on-write scheme for the positions, to avoid having to + // reallocate them all on every rebase, but also avoid problems with + // shared position objects being unsafely updated. + function rebaseHistArray(array, from, to, diff) { + for (var i = 0; i < array.length; ++i) { + var sub = array[i], ok = true; + if (sub.ranges) { + if (!sub.copied) { sub = array[i] = sub.deepCopy(); sub.copied = true; } + for (var j = 0; j < sub.ranges.length; j++) { + rebaseHistSelSingle(sub.ranges[j].anchor, from, to, diff); + rebaseHistSelSingle(sub.ranges[j].head, from, to, diff); + } + continue; + } + for (var j = 0; j < sub.changes.length; ++j) { + var cur = sub.changes[j]; + if (to < cur.from.line) { + cur.from = Pos(cur.from.line + diff, cur.from.ch); + cur.to = Pos(cur.to.line + diff, cur.to.ch); + } else if (from <= cur.to.line) { + ok = false; + break; + } + } + if (!ok) { + array.splice(0, i + 1); + i = 0; + } + } + } + + function rebaseHist(hist, change) { + var from = change.from.line, to = change.to.line, diff = change.text.length - (to - from) - 1; + rebaseHistArray(hist.done, from, to, diff); + rebaseHistArray(hist.undone, from, to, diff); + } + + // EVENT UTILITIES + + // Due to the fact that we still support jurassic IE versions, some + // compatibility wrappers are needed. + + var e_preventDefault = CodeMirror.e_preventDefault = function(e) { + if (e.preventDefault) e.preventDefault(); + else e.returnValue = false; + }; + var e_stopPropagation = CodeMirror.e_stopPropagation = function(e) { + if (e.stopPropagation) e.stopPropagation(); + else e.cancelBubble = true; + }; + function e_defaultPrevented(e) { + return e.defaultPrevented != null ? e.defaultPrevented : e.returnValue == false; + } + var e_stop = CodeMirror.e_stop = function(e) {e_preventDefault(e); e_stopPropagation(e);}; + + function e_target(e) {return e.target || e.srcElement;} + function e_button(e) { + var b = e.which; + if (b == null) { + if (e.button & 1) b = 1; + else if (e.button & 2) b = 3; + else if (e.button & 4) b = 2; + } + if (mac && e.ctrlKey && b == 1) b = 3; + return b; + } + + // EVENT HANDLING + + // Lightweight event framework. on/off also work on DOM nodes, + // registering native DOM handlers. + + var on = CodeMirror.on = function(emitter, type, f) { + if (emitter.addEventListener) + emitter.addEventListener(type, f, false); + else if (emitter.attachEvent) + emitter.attachEvent("on" + type, f); + else { + var map = emitter._handlers || (emitter._handlers = {}); + var arr = map[type] || (map[type] = []); + arr.push(f); + } + }; + + var off = CodeMirror.off = function(emitter, type, f) { + if (emitter.removeEventListener) + emitter.removeEventListener(type, f, false); + else if (emitter.detachEvent) + emitter.detachEvent("on" + type, f); + else { + var arr = emitter._handlers && emitter._handlers[type]; + if (!arr) return; + for (var i = 0; i < arr.length; ++i) + if (arr[i] == f) { arr.splice(i, 1); break; } + } + }; + + var signal = CodeMirror.signal = function(emitter, type /*, values...*/) { + var arr = emitter._handlers && emitter._handlers[type]; + if (!arr) return; + var args = Array.prototype.slice.call(arguments, 2); + for (var i = 0; i < arr.length; ++i) arr[i].apply(null, args); + }; + + var orphanDelayedCallbacks = null; + + // Often, we want to signal events at a point where we are in the + // middle of some work, but don't want the handler to start calling + // other methods on the editor, which might be in an inconsistent + // state or simply not expect any other events to happen. + // signalLater looks whether there are any handlers, and schedules + // them to be executed when the last operation ends, or, if no + // operation is active, when a timeout fires. + function signalLater(emitter, type /*, values...*/) { + var arr = emitter._handlers && emitter._handlers[type]; + if (!arr) return; + var args = Array.prototype.slice.call(arguments, 2), list; + if (operationGroup) { + list = operationGroup.delayedCallbacks; + } else if (orphanDelayedCallbacks) { + list = orphanDelayedCallbacks; + } else { + list = orphanDelayedCallbacks = []; + setTimeout(fireOrphanDelayed, 0); + } + function bnd(f) {return function(){f.apply(null, args);};}; + for (var i = 0; i < arr.length; ++i) + list.push(bnd(arr[i])); + } + + function fireOrphanDelayed() { + var delayed = orphanDelayedCallbacks; + orphanDelayedCallbacks = null; + for (var i = 0; i < delayed.length; ++i) delayed[i](); + } + + // The DOM events that CodeMirror handles can be overridden by + // registering a (non-DOM) handler on the editor for the event name, + // and preventDefault-ing the event in that handler. + function signalDOMEvent(cm, e, override) { + if (typeof e == "string") + e = {type: e, preventDefault: function() { this.defaultPrevented = true; }}; + signal(cm, override || e.type, cm, e); + return e_defaultPrevented(e) || e.codemirrorIgnore; + } + + function signalCursorActivity(cm) { + var arr = cm._handlers && cm._handlers.cursorActivity; + if (!arr) return; + var set = cm.curOp.cursorActivityHandlers || (cm.curOp.cursorActivityHandlers = []); + for (var i = 0; i < arr.length; ++i) if (indexOf(set, arr[i]) == -1) + set.push(arr[i]); + } + + function hasHandler(emitter, type) { + var arr = emitter._handlers && emitter._handlers[type]; + return arr && arr.length > 0; + } + + // Add on and off methods to a constructor's prototype, to make + // registering events on such objects more convenient. + function eventMixin(ctor) { + ctor.prototype.on = function(type, f) {on(this, type, f);}; + ctor.prototype.off = function(type, f) {off(this, type, f);}; + } + + // MISC UTILITIES + + // Number of pixels added to scroller and sizer to hide scrollbar + var scrollerGap = 30; + + // Returned or thrown by various protocols to signal 'I'm not + // handling this'. + var Pass = CodeMirror.Pass = {toString: function(){return "CodeMirror.Pass";}}; + + // Reused option objects for setSelection & friends + var sel_dontScroll = {scroll: false}, sel_mouse = {origin: "*mouse"}, sel_move = {origin: "+move"}; + + function Delayed() {this.id = null;} + Delayed.prototype.set = function(ms, f) { + clearTimeout(this.id); + this.id = setTimeout(f, ms); + }; + + // Counts the column offset in a string, taking tabs into account. + // Used mostly to find indentation. + var countColumn = CodeMirror.countColumn = function(string, end, tabSize, startIndex, startValue) { + if (end == null) { + end = string.search(/[^\s\u00a0]/); + if (end == -1) end = string.length; + } + for (var i = startIndex || 0, n = startValue || 0;;) { + var nextTab = string.indexOf("\t", i); + if (nextTab < 0 || nextTab >= end) + return n + (end - i); + n += nextTab - i; + n += tabSize - (n % tabSize); + i = nextTab + 1; + } + }; + + // The inverse of countColumn -- find the offset that corresponds to + // a particular column. + function findColumn(string, goal, tabSize) { + for (var pos = 0, col = 0;;) { + var nextTab = string.indexOf("\t", pos); + if (nextTab == -1) nextTab = string.length; + var skipped = nextTab - pos; + if (nextTab == string.length || col + skipped >= goal) + return pos + Math.min(skipped, goal - col); + col += nextTab - pos; + col += tabSize - (col % tabSize); + pos = nextTab + 1; + if (col >= goal) return pos; + } + } + + var spaceStrs = [""]; + function spaceStr(n) { + while (spaceStrs.length <= n) + spaceStrs.push(lst(spaceStrs) + " "); + return spaceStrs[n]; + } + + function lst(arr) { return arr[arr.length-1]; } + + var selectInput = function(node) { node.select(); }; + if (ios) // Mobile Safari apparently has a bug where select() is broken. + selectInput = function(node) { node.selectionStart = 0; node.selectionEnd = node.value.length; }; + else if (ie) // Suppress mysterious IE10 errors + selectInput = function(node) { try { node.select(); } catch(_e) {} }; + + function indexOf(array, elt) { + for (var i = 0; i < array.length; ++i) + if (array[i] == elt) return i; + return -1; + } + function map(array, f) { + var out = []; + for (var i = 0; i < array.length; i++) out[i] = f(array[i], i); + return out; + } + + function nothing() {} + + function createObj(base, props) { + var inst; + if (Object.create) { + inst = Object.create(base); + } else { + nothing.prototype = base; + inst = new nothing(); + } + if (props) copyObj(props, inst); + return inst; + }; + + function copyObj(obj, target, overwrite) { + if (!target) target = {}; + for (var prop in obj) + if (obj.hasOwnProperty(prop) && (overwrite !== false || !target.hasOwnProperty(prop))) + target[prop] = obj[prop]; + return target; + } + + function bind(f) { + var args = Array.prototype.slice.call(arguments, 1); + return function(){return f.apply(null, args);}; + } + + var nonASCIISingleCaseWordChar = /[\u00df\u0587\u0590-\u05f4\u0600-\u06ff\u3040-\u309f\u30a0-\u30ff\u3400-\u4db5\u4e00-\u9fcc\uac00-\ud7af]/; + var isWordCharBasic = CodeMirror.isWordChar = function(ch) { + return /\w/.test(ch) || ch > "\x80" && + (ch.toUpperCase() != ch.toLowerCase() || nonASCIISingleCaseWordChar.test(ch)); + }; + function isWordChar(ch, helper) { + if (!helper) return isWordCharBasic(ch); + if (helper.source.indexOf("\\w") > -1 && isWordCharBasic(ch)) return true; + return helper.test(ch); + } + + function isEmpty(obj) { + for (var n in obj) if (obj.hasOwnProperty(n) && obj[n]) return false; + return true; + } + + // Extending unicode characters. A series of a non-extending char + + // any number of extending chars is treated as a single unit as far + // as editing and measuring is concerned. This is not fully correct, + // since some scripts/fonts/browsers also treat other configurations + // of code points as a group. + var extendingChars = /[\u0300-\u036f\u0483-\u0489\u0591-\u05bd\u05bf\u05c1\u05c2\u05c4\u05c5\u05c7\u0610-\u061a\u064b-\u065e\u0670\u06d6-\u06dc\u06de-\u06e4\u06e7\u06e8\u06ea-\u06ed\u0711\u0730-\u074a\u07a6-\u07b0\u07eb-\u07f3\u0816-\u0819\u081b-\u0823\u0825-\u0827\u0829-\u082d\u0900-\u0902\u093c\u0941-\u0948\u094d\u0951-\u0955\u0962\u0963\u0981\u09bc\u09be\u09c1-\u09c4\u09cd\u09d7\u09e2\u09e3\u0a01\u0a02\u0a3c\u0a41\u0a42\u0a47\u0a48\u0a4b-\u0a4d\u0a51\u0a70\u0a71\u0a75\u0a81\u0a82\u0abc\u0ac1-\u0ac5\u0ac7\u0ac8\u0acd\u0ae2\u0ae3\u0b01\u0b3c\u0b3e\u0b3f\u0b41-\u0b44\u0b4d\u0b56\u0b57\u0b62\u0b63\u0b82\u0bbe\u0bc0\u0bcd\u0bd7\u0c3e-\u0c40\u0c46-\u0c48\u0c4a-\u0c4d\u0c55\u0c56\u0c62\u0c63\u0cbc\u0cbf\u0cc2\u0cc6\u0ccc\u0ccd\u0cd5\u0cd6\u0ce2\u0ce3\u0d3e\u0d41-\u0d44\u0d4d\u0d57\u0d62\u0d63\u0dca\u0dcf\u0dd2-\u0dd4\u0dd6\u0ddf\u0e31\u0e34-\u0e3a\u0e47-\u0e4e\u0eb1\u0eb4-\u0eb9\u0ebb\u0ebc\u0ec8-\u0ecd\u0f18\u0f19\u0f35\u0f37\u0f39\u0f71-\u0f7e\u0f80-\u0f84\u0f86\u0f87\u0f90-\u0f97\u0f99-\u0fbc\u0fc6\u102d-\u1030\u1032-\u1037\u1039\u103a\u103d\u103e\u1058\u1059\u105e-\u1060\u1071-\u1074\u1082\u1085\u1086\u108d\u109d\u135f\u1712-\u1714\u1732-\u1734\u1752\u1753\u1772\u1773\u17b7-\u17bd\u17c6\u17c9-\u17d3\u17dd\u180b-\u180d\u18a9\u1920-\u1922\u1927\u1928\u1932\u1939-\u193b\u1a17\u1a18\u1a56\u1a58-\u1a5e\u1a60\u1a62\u1a65-\u1a6c\u1a73-\u1a7c\u1a7f\u1b00-\u1b03\u1b34\u1b36-\u1b3a\u1b3c\u1b42\u1b6b-\u1b73\u1b80\u1b81\u1ba2-\u1ba5\u1ba8\u1ba9\u1c2c-\u1c33\u1c36\u1c37\u1cd0-\u1cd2\u1cd4-\u1ce0\u1ce2-\u1ce8\u1ced\u1dc0-\u1de6\u1dfd-\u1dff\u200c\u200d\u20d0-\u20f0\u2cef-\u2cf1\u2de0-\u2dff\u302a-\u302f\u3099\u309a\ua66f-\ua672\ua67c\ua67d\ua6f0\ua6f1\ua802\ua806\ua80b\ua825\ua826\ua8c4\ua8e0-\ua8f1\ua926-\ua92d\ua947-\ua951\ua980-\ua982\ua9b3\ua9b6-\ua9b9\ua9bc\uaa29-\uaa2e\uaa31\uaa32\uaa35\uaa36\uaa43\uaa4c\uaab0\uaab2-\uaab4\uaab7\uaab8\uaabe\uaabf\uaac1\uabe5\uabe8\uabed\udc00-\udfff\ufb1e\ufe00-\ufe0f\ufe20-\ufe26\uff9e\uff9f]/; + function isExtendingChar(ch) { return ch.charCodeAt(0) >= 768 && extendingChars.test(ch); } + + // DOM UTILITIES + + function elt(tag, content, className, style) { + var e = document.createElement(tag); + if (className) e.className = className; + if (style) e.style.cssText = style; + if (typeof content == "string") e.appendChild(document.createTextNode(content)); + else if (content) for (var i = 0; i < content.length; ++i) e.appendChild(content[i]); + return e; + } + + var range; + if (document.createRange) range = function(node, start, end, endNode) { + var r = document.createRange(); + r.setEnd(endNode || node, end); + r.setStart(node, start); + return r; + }; + else range = function(node, start, end) { + var r = document.body.createTextRange(); + try { r.moveToElementText(node.parentNode); } + catch(e) { return r; } + r.collapse(true); + r.moveEnd("character", end); + r.moveStart("character", start); + return r; + }; + + function removeChildren(e) { + for (var count = e.childNodes.length; count > 0; --count) + e.removeChild(e.firstChild); + return e; + } + + function removeChildrenAndAdd(parent, e) { + return removeChildren(parent).appendChild(e); + } + + var contains = CodeMirror.contains = function(parent, child) { + if (child.nodeType == 3) // Android browser always returns false when child is a textnode + child = child.parentNode; + if (parent.contains) + return parent.contains(child); + do { + if (child.nodeType == 11) child = child.host; + if (child == parent) return true; + } while (child = child.parentNode); + }; + + function activeElt() { return document.activeElement; } + // Older versions of IE throws unspecified error when touching + // document.activeElement in some cases (during loading, in iframe) + if (ie && ie_version < 11) activeElt = function() { + try { return document.activeElement; } + catch(e) { return document.body; } + }; + + function classTest(cls) { return new RegExp("(^|\\s)" + cls + "(?:$|\\s)\\s*"); } + var rmClass = CodeMirror.rmClass = function(node, cls) { + var current = node.className; + var match = classTest(cls).exec(current); + if (match) { + var after = current.slice(match.index + match[0].length); + node.className = current.slice(0, match.index) + (after ? match[1] + after : ""); + } + }; + var addClass = CodeMirror.addClass = function(node, cls) { + var current = node.className; + if (!classTest(cls).test(current)) node.className += (current ? " " : "") + cls; + }; + function joinClasses(a, b) { + var as = a.split(" "); + for (var i = 0; i < as.length; i++) + if (as[i] && !classTest(as[i]).test(b)) b += " " + as[i]; + return b; + } + + // WINDOW-WIDE EVENTS + + // These must be handled carefully, because naively registering a + // handler for each editor will cause the editors to never be + // garbage collected. + + function forEachCodeMirror(f) { + if (!document.body.getElementsByClassName) return; + var byClass = document.body.getElementsByClassName("CodeMirror"); + for (var i = 0; i < byClass.length; i++) { + var cm = byClass[i].CodeMirror; + if (cm) f(cm); + } + } + + var globalsRegistered = false; + function ensureGlobalHandlers() { + if (globalsRegistered) return; + registerGlobalHandlers(); + globalsRegistered = true; + } + function registerGlobalHandlers() { + // When the window resizes, we need to refresh active editors. + var resizeTimer; + on(window, "resize", function() { + if (resizeTimer == null) resizeTimer = setTimeout(function() { + resizeTimer = null; + forEachCodeMirror(onResize); + }, 100); + }); + // When the window loses focus, we want to show the editor as blurred + on(window, "blur", function() { + forEachCodeMirror(onBlur); + }); + } + + // FEATURE DETECTION + + // Detect drag-and-drop + var dragAndDrop = function() { + // There is *some* kind of drag-and-drop support in IE6-8, but I + // couldn't get it to work yet. + if (ie && ie_version < 9) return false; + var div = elt('div'); + return "draggable" in div || "dragDrop" in div; + }(); + + var zwspSupported; + function zeroWidthElement(measure) { + if (zwspSupported == null) { + var test = elt("span", "\u200b"); + removeChildrenAndAdd(measure, elt("span", [test, document.createTextNode("x")])); + if (measure.firstChild.offsetHeight != 0) + zwspSupported = test.offsetWidth <= 1 && test.offsetHeight > 2 && !(ie && ie_version < 8); + } + var node = zwspSupported ? elt("span", "\u200b") : + elt("span", "\u00a0", null, "display: inline-block; width: 1px; margin-right: -1px"); + node.setAttribute("cm-text", ""); + return node; + } + + // Feature-detect IE's crummy client rect reporting for bidi text + var badBidiRects; + function hasBadBidiRects(measure) { + if (badBidiRects != null) return badBidiRects; + var txt = removeChildrenAndAdd(measure, document.createTextNode("A\u062eA")); + var r0 = range(txt, 0, 1).getBoundingClientRect(); + if (!r0 || r0.left == r0.right) return false; // Safari returns null in some cases (#2780) + var r1 = range(txt, 1, 2).getBoundingClientRect(); + return badBidiRects = (r1.right - r0.right < 3); + } + + // See if "".split is the broken IE version, if so, provide an + // alternative way to split lines. + var splitLines = CodeMirror.splitLines = "\n\nb".split(/\n/).length != 3 ? function(string) { + var pos = 0, result = [], l = string.length; + while (pos <= l) { + var nl = string.indexOf("\n", pos); + if (nl == -1) nl = string.length; + var line = string.slice(pos, string.charAt(nl - 1) == "\r" ? nl - 1 : nl); + var rt = line.indexOf("\r"); + if (rt != -1) { + result.push(line.slice(0, rt)); + pos += rt + 1; + } else { + result.push(line); + pos = nl + 1; + } + } + return result; + } : function(string){return string.split(/\r\n?|\n/);}; + + var hasSelection = window.getSelection ? function(te) { + try { return te.selectionStart != te.selectionEnd; } + catch(e) { return false; } + } : function(te) { + try {var range = te.ownerDocument.selection.createRange();} + catch(e) {} + if (!range || range.parentElement() != te) return false; + return range.compareEndPoints("StartToEnd", range) != 0; + }; + + var hasCopyEvent = (function() { + var e = elt("div"); + if ("oncopy" in e) return true; + e.setAttribute("oncopy", "return;"); + return typeof e.oncopy == "function"; + })(); + + var badZoomedRects = null; + function hasBadZoomedRects(measure) { + if (badZoomedRects != null) return badZoomedRects; + var node = removeChildrenAndAdd(measure, elt("span", "x")); + var normal = node.getBoundingClientRect(); + var fromRange = range(node, 0, 1).getBoundingClientRect(); + return badZoomedRects = Math.abs(normal.left - fromRange.left) > 1; + } + + // KEY NAMES + + var keyNames = {3: "Enter", 8: "Backspace", 9: "Tab", 13: "Enter", 16: "Shift", 17: "Ctrl", 18: "Alt", + 19: "Pause", 20: "CapsLock", 27: "Esc", 32: "Space", 33: "PageUp", 34: "PageDown", 35: "End", + 36: "Home", 37: "Left", 38: "Up", 39: "Right", 40: "Down", 44: "PrintScrn", 45: "Insert", + 46: "Delete", 59: ";", 61: "=", 91: "Mod", 92: "Mod", 93: "Mod", 107: "=", 109: "-", 127: "Delete", + 173: "-", 186: ";", 187: "=", 188: ",", 189: "-", 190: ".", 191: "/", 192: "`", 219: "[", 220: "\\", + 221: "]", 222: "'", 63232: "Up", 63233: "Down", 63234: "Left", 63235: "Right", 63272: "Delete", + 63273: "Home", 63275: "End", 63276: "PageUp", 63277: "PageDown", 63302: "Insert"}; + CodeMirror.keyNames = keyNames; + (function() { + // Number keys + for (var i = 0; i < 10; i++) keyNames[i + 48] = keyNames[i + 96] = String(i); + // Alphabetic keys + for (var i = 65; i <= 90; i++) keyNames[i] = String.fromCharCode(i); + // Function keys + for (var i = 1; i <= 12; i++) keyNames[i + 111] = keyNames[i + 63235] = "F" + i; + })(); + + // BIDI HELPERS + + function iterateBidiSections(order, from, to, f) { + if (!order) return f(from, to, "ltr"); + var found = false; + for (var i = 0; i < order.length; ++i) { + var part = order[i]; + if (part.from < to && part.to > from || from == to && part.to == from) { + f(Math.max(part.from, from), Math.min(part.to, to), part.level == 1 ? "rtl" : "ltr"); + found = true; + } + } + if (!found) f(from, to, "ltr"); + } + + function bidiLeft(part) { return part.level % 2 ? part.to : part.from; } + function bidiRight(part) { return part.level % 2 ? part.from : part.to; } + + function lineLeft(line) { var order = getOrder(line); return order ? bidiLeft(order[0]) : 0; } + function lineRight(line) { + var order = getOrder(line); + if (!order) return line.text.length; + return bidiRight(lst(order)); + } + + function lineStart(cm, lineN) { + var line = getLine(cm.doc, lineN); + var visual = visualLine(line); + if (visual != line) lineN = lineNo(visual); + var order = getOrder(visual); + var ch = !order ? 0 : order[0].level % 2 ? lineRight(visual) : lineLeft(visual); + return Pos(lineN, ch); + } + function lineEnd(cm, lineN) { + var merged, line = getLine(cm.doc, lineN); + while (merged = collapsedSpanAtEnd(line)) { + line = merged.find(1, true).line; + lineN = null; + } + var order = getOrder(line); + var ch = !order ? line.text.length : order[0].level % 2 ? lineLeft(line) : lineRight(line); + return Pos(lineN == null ? lineNo(line) : lineN, ch); + } + function lineStartSmart(cm, pos) { + var start = lineStart(cm, pos.line); + var line = getLine(cm.doc, start.line); + var order = getOrder(line); + if (!order || order[0].level == 0) { + var firstNonWS = Math.max(0, line.text.search(/\S/)); + var inWS = pos.line == start.line && pos.ch <= firstNonWS && pos.ch; + return Pos(start.line, inWS ? 0 : firstNonWS); + } + return start; + } + + function compareBidiLevel(order, a, b) { + var linedir = order[0].level; + if (a == linedir) return true; + if (b == linedir) return false; + return a < b; + } + var bidiOther; + function getBidiPartAt(order, pos) { + bidiOther = null; + for (var i = 0, found; i < order.length; ++i) { + var cur = order[i]; + if (cur.from < pos && cur.to > pos) return i; + if ((cur.from == pos || cur.to == pos)) { + if (found == null) { + found = i; + } else if (compareBidiLevel(order, cur.level, order[found].level)) { + if (cur.from != cur.to) bidiOther = found; + return i; + } else { + if (cur.from != cur.to) bidiOther = i; + return found; + } + } + } + return found; + } + + function moveInLine(line, pos, dir, byUnit) { + if (!byUnit) return pos + dir; + do pos += dir; + while (pos > 0 && isExtendingChar(line.text.charAt(pos))); + return pos; + } + + // This is needed in order to move 'visually' through bi-directional + // text -- i.e., pressing left should make the cursor go left, even + // when in RTL text. The tricky part is the 'jumps', where RTL and + // LTR text touch each other. This often requires the cursor offset + // to move more than one unit, in order to visually move one unit. + function moveVisually(line, start, dir, byUnit) { + var bidi = getOrder(line); + if (!bidi) return moveLogically(line, start, dir, byUnit); + var pos = getBidiPartAt(bidi, start), part = bidi[pos]; + var target = moveInLine(line, start, part.level % 2 ? -dir : dir, byUnit); + + for (;;) { + if (target > part.from && target < part.to) return target; + if (target == part.from || target == part.to) { + if (getBidiPartAt(bidi, target) == pos) return target; + part = bidi[pos += dir]; + return (dir > 0) == part.level % 2 ? part.to : part.from; + } else { + part = bidi[pos += dir]; + if (!part) return null; + if ((dir > 0) == part.level % 2) + target = moveInLine(line, part.to, -1, byUnit); + else + target = moveInLine(line, part.from, 1, byUnit); + } + } + } + + function moveLogically(line, start, dir, byUnit) { + var target = start + dir; + if (byUnit) while (target > 0 && isExtendingChar(line.text.charAt(target))) target += dir; + return target < 0 || target > line.text.length ? null : target; + } + + // Bidirectional ordering algorithm + // See http://unicode.org/reports/tr9/tr9-13.html for the algorithm + // that this (partially) implements. + + // One-char codes used for character types: + // L (L): Left-to-Right + // R (R): Right-to-Left + // r (AL): Right-to-Left Arabic + // 1 (EN): European Number + // + (ES): European Number Separator + // % (ET): European Number Terminator + // n (AN): Arabic Number + // , (CS): Common Number Separator + // m (NSM): Non-Spacing Mark + // b (BN): Boundary Neutral + // s (B): Paragraph Separator + // t (S): Segment Separator + // w (WS): Whitespace + // N (ON): Other Neutrals + + // Returns null if characters are ordered as they appear + // (left-to-right), or an array of sections ({from, to, level} + // objects) in the order in which they occur visually. + var bidiOrdering = (function() { + // Character types for codepoints 0 to 0xff + var lowTypes = "bbbbbbbbbtstwsbbbbbbbbbbbbbbssstwNN%%%NNNNNN,N,N1111111111NNNNNNNLLLLLLLLLLLLLLLLLLLLLLLLLLNNNNNNLLLLLLLLLLLLLLLLLLLLLLLLLLNNNNbbbbbbsbbbbbbbbbbbbbbbbbbbbbbbbbb,N%%%%NNNNLNNNNN%%11NLNNN1LNNNNNLLLLLLLLLLLLLLLLLLLLLLLNLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLN"; + // Character types for codepoints 0x600 to 0x6ff + var arabicTypes = "rrrrrrrrrrrr,rNNmmmmmmrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrmmmmmmmmmmmmmmrrrrrrrnnnnnnnnnn%nnrrrmrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrmmmmmmmmmmmmmmmmmmmNmmmm"; + function charType(code) { + if (code <= 0xf7) return lowTypes.charAt(code); + else if (0x590 <= code && code <= 0x5f4) return "R"; + else if (0x600 <= code && code <= 0x6ed) return arabicTypes.charAt(code - 0x600); + else if (0x6ee <= code && code <= 0x8ac) return "r"; + else if (0x2000 <= code && code <= 0x200b) return "w"; + else if (code == 0x200c) return "b"; + else return "L"; + } + + var bidiRE = /[\u0590-\u05f4\u0600-\u06ff\u0700-\u08ac]/; + var isNeutral = /[stwN]/, isStrong = /[LRr]/, countsAsLeft = /[Lb1n]/, countsAsNum = /[1n]/; + // Browsers seem to always treat the boundaries of block elements as being L. + var outerType = "L"; + + function BidiSpan(level, from, to) { + this.level = level; + this.from = from; this.to = to; + } + + return function(str) { + if (!bidiRE.test(str)) return false; + var len = str.length, types = []; + for (var i = 0, type; i < len; ++i) + types.push(type = charType(str.charCodeAt(i))); + + // W1. Examine each non-spacing mark (NSM) in the level run, and + // change the type of the NSM to the type of the previous + // character. If the NSM is at the start of the level run, it will + // get the type of sor. + for (var i = 0, prev = outerType; i < len; ++i) { + var type = types[i]; + if (type == "m") types[i] = prev; + else prev = type; + } + + // W2. Search backwards from each instance of a European number + // until the first strong type (R, L, AL, or sor) is found. If an + // AL is found, change the type of the European number to Arabic + // number. + // W3. Change all ALs to R. + for (var i = 0, cur = outerType; i < len; ++i) { + var type = types[i]; + if (type == "1" && cur == "r") types[i] = "n"; + else if (isStrong.test(type)) { cur = type; if (type == "r") types[i] = "R"; } + } + + // W4. A single European separator between two European numbers + // changes to a European number. A single common separator between + // two numbers of the same type changes to that type. + for (var i = 1, prev = types[0]; i < len - 1; ++i) { + var type = types[i]; + if (type == "+" && prev == "1" && types[i+1] == "1") types[i] = "1"; + else if (type == "," && prev == types[i+1] && + (prev == "1" || prev == "n")) types[i] = prev; + prev = type; + } + + // W5. A sequence of European terminators adjacent to European + // numbers changes to all European numbers. + // W6. Otherwise, separators and terminators change to Other + // Neutral. + for (var i = 0; i < len; ++i) { + var type = types[i]; + if (type == ",") types[i] = "N"; + else if (type == "%") { + for (var end = i + 1; end < len && types[end] == "%"; ++end) {} + var replace = (i && types[i-1] == "!") || (end < len && types[end] == "1") ? "1" : "N"; + for (var j = i; j < end; ++j) types[j] = replace; + i = end - 1; + } + } + + // W7. Search backwards from each instance of a European number + // until the first strong type (R, L, or sor) is found. If an L is + // found, then change the type of the European number to L. + for (var i = 0, cur = outerType; i < len; ++i) { + var type = types[i]; + if (cur == "L" && type == "1") types[i] = "L"; + else if (isStrong.test(type)) cur = type; + } + + // N1. A sequence of neutrals takes the direction of the + // surrounding strong text if the text on both sides has the same + // direction. European and Arabic numbers act as if they were R in + // terms of their influence on neutrals. Start-of-level-run (sor) + // and end-of-level-run (eor) are used at level run boundaries. + // N2. Any remaining neutrals take the embedding direction. + for (var i = 0; i < len; ++i) { + if (isNeutral.test(types[i])) { + for (var end = i + 1; end < len && isNeutral.test(types[end]); ++end) {} + var before = (i ? types[i-1] : outerType) == "L"; + var after = (end < len ? types[end] : outerType) == "L"; + var replace = before || after ? "L" : "R"; + for (var j = i; j < end; ++j) types[j] = replace; + i = end - 1; + } + } + + // Here we depart from the documented algorithm, in order to avoid + // building up an actual levels array. Since there are only three + // levels (0, 1, 2) in an implementation that doesn't take + // explicit embedding into account, we can build up the order on + // the fly, without following the level-based algorithm. + var order = [], m; + for (var i = 0; i < len;) { + if (countsAsLeft.test(types[i])) { + var start = i; + for (++i; i < len && countsAsLeft.test(types[i]); ++i) {} + order.push(new BidiSpan(0, start, i)); + } else { + var pos = i, at = order.length; + for (++i; i < len && types[i] != "L"; ++i) {} + for (var j = pos; j < i;) { + if (countsAsNum.test(types[j])) { + if (pos < j) order.splice(at, 0, new BidiSpan(1, pos, j)); + var nstart = j; + for (++j; j < i && countsAsNum.test(types[j]); ++j) {} + order.splice(at, 0, new BidiSpan(2, nstart, j)); + pos = j; + } else ++j; + } + if (pos < i) order.splice(at, 0, new BidiSpan(1, pos, i)); + } + } + if (order[0].level == 1 && (m = str.match(/^\s+/))) { + order[0].from = m[0].length; + order.unshift(new BidiSpan(0, 0, m[0].length)); + } + if (lst(order).level == 1 && (m = str.match(/\s+$/))) { + lst(order).to -= m[0].length; + order.push(new BidiSpan(0, len - m[0].length, len)); + } + if (order[0].level == 2) + order.unshift(new BidiSpan(1, order[0].to, order[0].to)); + if (order[0].level != lst(order).level) + order.push(new BidiSpan(order[0].level, len, len)); + + return order; + }; + })(); + + // THE END + + CodeMirror.version = "5.3.0"; + + return CodeMirror; +}); diff --git a/web/dep/codemirror/colorforth.css b/web/dep/codemirror/colorforth.css new file mode 100644 index 00000000..36fe525c --- /dev/null +++ b/web/dep/codemirror/colorforth.css @@ -0,0 +1,33 @@ +.cm-s-colorforth.CodeMirror { background: #000000; color: #f8f8f8; } +.cm-s-colorforth .CodeMirror-gutters { background: #0a001f; border-right: 1px solid #aaa; } +.cm-s-colorforth .CodeMirror-guttermarker { color: #FFBD40; } +.cm-s-colorforth .CodeMirror-guttermarker-subtle { color: #78846f; } +.cm-s-colorforth .CodeMirror-linenumber { color: #bababa; } +.cm-s-colorforth .CodeMirror-cursor { border-left: 1px solid white !important; } + +.cm-s-colorforth span.cm-comment { color: #ededed; } +.cm-s-colorforth span.cm-def { color: #ff1c1c; font-weight:bold; } +.cm-s-colorforth span.cm-keyword { color: #ffd900; } +.cm-s-colorforth span.cm-builtin { color: #00d95a; } +.cm-s-colorforth span.cm-variable { color: #73ff00; } +.cm-s-colorforth span.cm-string { color: #007bff; } +.cm-s-colorforth span.cm-number { color: #00c4ff; } +.cm-s-colorforth span.cm-atom { color: #606060; } + +.cm-s-colorforth span.cm-variable-2 { color: #EEE; } +.cm-s-colorforth span.cm-variable-3 { color: #DDD; } +.cm-s-colorforth span.cm-property {} +.cm-s-colorforth span.cm-operator {} + +.cm-s-colorforth span.cm-meta { color: yellow; } +.cm-s-colorforth span.cm-qualifier { color: #FFF700; } +.cm-s-colorforth span.cm-tag { color: lime; } +.cm-s-colorforth span.cm-bracket { color: green; } +.cm-s-colorforth span.cm-attribute { color: #FFF700; } +.cm-s-colorforth span.cm-error { color: #f00; } + +.cm-s-colorforth .CodeMirror-selected { background: #333d53 !important; } + +.cm-s-colorforth span.cm-compilation { background: rgba(255, 255, 255, 0.12); } + +.cm-s-colorforth .CodeMirror-activeline-background {background: #253540 !important;} diff --git a/web/dep/codemirror/colorforth_.css b/web/dep/codemirror/colorforth_.css new file mode 100644 index 00000000..73fbf808 --- /dev/null +++ b/web/dep/codemirror/colorforth_.css @@ -0,0 +1,33 @@ +.cm-s-colorforth.CodeMirror { background: #000000; color: #f8f8f8; } +.cm-s-colorforth .CodeMirror-gutters { background: #0a001f; border-right: 1px solid #aaa; } +.cm-s-colorforth .CodeMirror-guttermarker { color: #FFBD40; } +.cm-s-colorforth .CodeMirror-guttermarker-subtle { color: #78846f; } +.cm-s-colorforth .CodeMirror-linenumber { color: #bababa; } +.cm-s-colorforth .CodeMirror-cursor { border-left: 1px solid white !important; } + +.cm-s-colorforth span.cm-comment { color: #ededed; } +.cm-s-colorforth span.cm-def { color: #ff1c1c; font-weight:bold; } +.cm-s-colorforth span.cm-keyword { color: #ffd900; } +.cm-s-colorforth span.cm-builtin { color: #00d95a; } +.cm-s-colorforth span.cm-variable { color: #73ff00; } +.cm-s-colorforth span.cm-string { color: #007bff; } +.cm-s-colorforth span.cm-number { color: #00c4ff; } +.cm-s-colorforth span.cm-atom { color: #606060; } + +.cm-s-colorforth span.cm-variable-2 { color: #EEE; } +.cm-s-colorforth span.cm-variable-3 { color: #DDD; } +.cm-s-colorforth span.cm-property {} +.cm-s-colorforth span.cm-operator {} + +.cm-s-colorforth span.cm-meta { color: yellow; } +.cm-s-colorforth span.cm-qualifier { color: #FFF700; } +.cm-s-colorforth span.cm-bracket { color: #cc7; } +.cm-s-colorforth span.cm-tag { color: #FFBD40; } +.cm-s-colorforth span.cm-attribute { color: #FFF700; } +.cm-s-colorforth span.cm-error { color: #f00; } + +.cm-s-colorforth .CodeMirror-selected { background: #333d53 !important; } + +.cm-s-colorforth span.cm-compilation { background: rgba(255, 255, 255, 0.12); } + +.cm-s-colorforth .CodeMirror-activeline-background {background: #253540 !important;} diff --git a/web/dep/codemirror/css.js b/web/dep/codemirror/css.js new file mode 100644 index 00000000..1e6d2ddb --- /dev/null +++ b/web/dep/codemirror/css.js @@ -0,0 +1,754 @@ +// CodeMirror, copyright (c) by Marijn Haverbeke and others +// Distributed under an MIT license: http://codemirror.net/LICENSE + +(function(mod) { + if (typeof exports == "object" && typeof module == "object") // CommonJS + mod(require("../../lib/codemirror")); + else if (typeof define == "function" && define.amd) // AMD + define(["../../lib/codemirror"], mod); + else // Plain browser env + mod(CodeMirror); +})(function(CodeMirror) { +"use strict"; + +CodeMirror.defineMode("css", function(config, parserConfig) { + if (!parserConfig.propertyKeywords) parserConfig = CodeMirror.resolveMode("text/css"); + + var indentUnit = config.indentUnit, + tokenHooks = parserConfig.tokenHooks, + documentTypes = parserConfig.documentTypes || {}, + mediaTypes = parserConfig.mediaTypes || {}, + mediaFeatures = parserConfig.mediaFeatures || {}, + propertyKeywords = parserConfig.propertyKeywords || {}, + nonStandardPropertyKeywords = parserConfig.nonStandardPropertyKeywords || {}, + fontProperties = parserConfig.fontProperties || {}, + counterDescriptors = parserConfig.counterDescriptors || {}, + colorKeywords = parserConfig.colorKeywords || {}, + valueKeywords = parserConfig.valueKeywords || {}, + allowNested = parserConfig.allowNested; + + var type, override; + function ret(style, tp) { type = tp; return style; } + + // Tokenizers + + function tokenBase(stream, state) { + var ch = stream.next(); + if (tokenHooks[ch]) { + var result = tokenHooks[ch](stream, state); + if (result !== false) return result; + } + if (ch == "@") { + stream.eatWhile(/[\w\\\-]/); + return ret("def", stream.current()); + } else if (ch == "=" || (ch == "~" || ch == "|") && stream.eat("=")) { + return ret(null, "compare"); + } else if (ch == "\"" || ch == "'") { + state.tokenize = tokenString(ch); + return state.tokenize(stream, state); + } else if (ch == "#") { + stream.eatWhile(/[\w\\\-]/); + return ret("atom", "hash"); + } else if (ch == "!") { + stream.match(/^\s*\w*/); + return ret("keyword", "important"); + } else if (/\d/.test(ch) || ch == "." && stream.eat(/\d/)) { + stream.eatWhile(/[\w.%]/); + return ret("number", "unit"); + } else if (ch === "-") { + if (/[\d.]/.test(stream.peek())) { + stream.eatWhile(/[\w.%]/); + return ret("number", "unit"); + } else if (stream.match(/^-[\w\\\-]+/)) { + stream.eatWhile(/[\w\\\-]/); + if (stream.match(/^\s*:/, false)) + return ret("variable-2", "variable-definition"); + return ret("variable-2", "variable"); + } else if (stream.match(/^\w+-/)) { + return ret("meta", "meta"); + } + } else if (/[,+>*\/]/.test(ch)) { + return ret(null, "select-op"); + } else if (ch == "." && stream.match(/^-?[_a-z][_a-z0-9-]*/i)) { + return ret("qualifier", "qualifier"); + } else if (/[:;{}\[\]\(\)]/.test(ch)) { + return ret(null, ch); + } else if ((ch == "u" && stream.match(/rl(-prefix)?\(/)) || + (ch == "d" && stream.match("omain(")) || + (ch == "r" && stream.match("egexp("))) { + stream.backUp(1); + state.tokenize = tokenParenthesized; + return ret("property", "word"); + } else if (/[\w\\\-]/.test(ch)) { + stream.eatWhile(/[\w\\\-]/); + return ret("property", "word"); + } else { + return ret(null, null); + } + } + + function tokenString(quote) { + return function(stream, state) { + var escaped = false, ch; + while ((ch = stream.next()) != null) { + if (ch == quote && !escaped) { + if (quote == ")") stream.backUp(1); + break; + } + escaped = !escaped && ch == "\\"; + } + if (ch == quote || !escaped && quote != ")") state.tokenize = null; + return ret("string", "string"); + }; + } + + function tokenParenthesized(stream, state) { + stream.next(); // Must be '(' + if (!stream.match(/\s*[\"\')]/, false)) + state.tokenize = tokenString(")"); + else + state.tokenize = null; + return ret(null, "("); + } + + // Context management + + function Context(type, indent, prev) { + this.type = type; + this.indent = indent; + this.prev = prev; + } + + function pushContext(state, stream, type) { + state.context = new Context(type, stream.indentation() + indentUnit, state.context); + return type; + } + + function popContext(state) { + state.context = state.context.prev; + return state.context.type; + } + + function pass(type, stream, state) { + return states[state.context.type](type, stream, state); + } + function popAndPass(type, stream, state, n) { + for (var i = n || 1; i > 0; i--) + state.context = state.context.prev; + return pass(type, stream, state); + } + + // Parser + + function wordAsValue(stream) { + var word = stream.current().toLowerCase(); + if (valueKeywords.hasOwnProperty(word)) + override = "atom"; + else if (colorKeywords.hasOwnProperty(word)) + override = "keyword"; + else + override = "variable"; + } + + var states = {}; + + states.top = function(type, stream, state) { + if (type == "{") { + return pushContext(state, stream, "block"); + } else if (type == "}" && state.context.prev) { + return popContext(state); + } else if (/@(media|supports|(-moz-)?document)/.test(type)) { + return pushContext(state, stream, "atBlock"); + } else if (/@(font-face|counter-style)/.test(type)) { + state.stateArg = type; + return "restricted_atBlock_before"; + } else if (/^@(-(moz|ms|o|webkit)-)?keyframes$/.test(type)) { + return "keyframes"; + } else if (type && type.charAt(0) == "@") { + return pushContext(state, stream, "at"); + } else if (type == "hash") { + override = "builtin"; + } else if (type == "word") { + override = "tag"; + } else if (type == "variable-definition") { + return "maybeprop"; + } else if (type == "interpolation") { + return pushContext(state, stream, "interpolation"); + } else if (type == ":") { + return "pseudo"; + } else if (allowNested && type == "(") { + return pushContext(state, stream, "parens"); + } + return state.context.type; + }; + + states.block = function(type, stream, state) { + if (type == "word") { + var word = stream.current().toLowerCase(); + if (propertyKeywords.hasOwnProperty(word)) { + override = "property"; + return "maybeprop"; + } else if (nonStandardPropertyKeywords.hasOwnProperty(word)) { + override = "string-2"; + return "maybeprop"; + } else if (allowNested) { + override = stream.match(/^\s*:(?:\s|$)/, false) ? "property" : "tag"; + return "block"; + } else { + override += " error"; + return "maybeprop"; + } + } else if (type == "meta") { + return "block"; + } else if (!allowNested && (type == "hash" || type == "qualifier")) { + override = "error"; + return "block"; + } else { + return states.top(type, stream, state); + } + }; + + states.maybeprop = function(type, stream, state) { + if (type == ":") return pushContext(state, stream, "prop"); + return pass(type, stream, state); + }; + + states.prop = function(type, stream, state) { + if (type == ";") return popContext(state); + if (type == "{" && allowNested) return pushContext(state, stream, "propBlock"); + if (type == "}" || type == "{") return popAndPass(type, stream, state); + if (type == "(") return pushContext(state, stream, "parens"); + + if (type == "hash" && !/^#([0-9a-fA-f]{3}|[0-9a-fA-f]{6})$/.test(stream.current())) { + override += " error"; + } else if (type == "word") { + wordAsValue(stream); + } else if (type == "interpolation") { + return pushContext(state, stream, "interpolation"); + } + return "prop"; + }; + + states.propBlock = function(type, _stream, state) { + if (type == "}") return popContext(state); + if (type == "word") { override = "property"; return "maybeprop"; } + return state.context.type; + }; + + states.parens = function(type, stream, state) { + if (type == "{" || type == "}") return popAndPass(type, stream, state); + if (type == ")") return popContext(state); + if (type == "(") return pushContext(state, stream, "parens"); + if (type == "interpolation") return pushContext(state, stream, "interpolation"); + if (type == "word") wordAsValue(stream); + return "parens"; + }; + + states.pseudo = function(type, stream, state) { + if (type == "word") { + override = "variable-3"; + return state.context.type; + } + return pass(type, stream, state); + }; + + states.atBlock = function(type, stream, state) { + if (type == "(") return pushContext(state, stream, "atBlock_parens"); + if (type == "}") return popAndPass(type, stream, state); + if (type == "{") return popContext(state) && pushContext(state, stream, allowNested ? "block" : "top"); + + if (type == "word") { + var word = stream.current().toLowerCase(); + if (word == "only" || word == "not" || word == "and" || word == "or") + override = "keyword"; + else if (documentTypes.hasOwnProperty(word)) + override = "tag"; + else if (mediaTypes.hasOwnProperty(word)) + override = "attribute"; + else if (mediaFeatures.hasOwnProperty(word)) + override = "property"; + else if (propertyKeywords.hasOwnProperty(word)) + override = "property"; + else if (nonStandardPropertyKeywords.hasOwnProperty(word)) + override = "string-2"; + else if (valueKeywords.hasOwnProperty(word)) + override = "atom"; + else + override = "error"; + } + return state.context.type; + }; + + states.atBlock_parens = function(type, stream, state) { + if (type == ")") return popContext(state); + if (type == "{" || type == "}") return popAndPass(type, stream, state, 2); + return states.atBlock(type, stream, state); + }; + + states.restricted_atBlock_before = function(type, stream, state) { + if (type == "{") + return pushContext(state, stream, "restricted_atBlock"); + if (type == "word" && state.stateArg == "@counter-style") { + override = "variable"; + return "restricted_atBlock_before"; + } + return pass(type, stream, state); + }; + + states.restricted_atBlock = function(type, stream, state) { + if (type == "}") { + state.stateArg = null; + return popContext(state); + } + if (type == "word") { + if ((state.stateArg == "@font-face" && !fontProperties.hasOwnProperty(stream.current().toLowerCase())) || + (state.stateArg == "@counter-style" && !counterDescriptors.hasOwnProperty(stream.current().toLowerCase()))) + override = "error"; + else + override = "property"; + return "maybeprop"; + } + return "restricted_atBlock"; + }; + + states.keyframes = function(type, stream, state) { + if (type == "word") { override = "variable"; return "keyframes"; } + if (type == "{") return pushContext(state, stream, "top"); + return pass(type, stream, state); + }; + + states.at = function(type, stream, state) { + if (type == ";") return popContext(state); + if (type == "{" || type == "}") return popAndPass(type, stream, state); + if (type == "word") override = "tag"; + else if (type == "hash") override = "builtin"; + return "at"; + }; + + states.interpolation = function(type, stream, state) { + if (type == "}") return popContext(state); + if (type == "{" || type == ";") return popAndPass(type, stream, state); + if (type == "word") override = "variable"; + else if (type != "variable" && type != "(" && type != ")") override = "error"; + return "interpolation"; + }; + + return { + startState: function(base) { + return {tokenize: null, + state: "top", + stateArg: null, + context: new Context("top", base || 0, null)}; + }, + + token: function(stream, state) { + if (!state.tokenize && stream.eatSpace()) return null; + var style = (state.tokenize || tokenBase)(stream, state); + if (style && typeof style == "object") { + type = style[1]; + style = style[0]; + } + override = style; + state.state = states[state.state](type, stream, state); + return override; + }, + + indent: function(state, textAfter) { + var cx = state.context, ch = textAfter && textAfter.charAt(0); + var indent = cx.indent; + if (cx.type == "prop" && (ch == "}" || ch == ")")) cx = cx.prev; + if (cx.prev && + (ch == "}" && (cx.type == "block" || cx.type == "top" || cx.type == "interpolation" || cx.type == "restricted_atBlock") || + ch == ")" && (cx.type == "parens" || cx.type == "atBlock_parens") || + ch == "{" && (cx.type == "at" || cx.type == "atBlock"))) { + indent = cx.indent - indentUnit; + cx = cx.prev; + } + return indent; + }, + + electricChars: "}", + blockCommentStart: "/*", + blockCommentEnd: "*/", + fold: "brace" + }; +}); + + function keySet(array) { + var keys = {}; + for (var i = 0; i < array.length; ++i) { + keys[array[i]] = true; + } + return keys; + } + + var documentTypes_ = [ + "domain", "regexp", "url", "url-prefix" + ], documentTypes = keySet(documentTypes_); + + var mediaTypes_ = [ + "all", "aural", "braille", "handheld", "print", "projection", "screen", + "tty", "tv", "embossed" + ], mediaTypes = keySet(mediaTypes_); + + var mediaFeatures_ = [ + "width", "min-width", "max-width", "height", "min-height", "max-height", + "device-width", "min-device-width", "max-device-width", "device-height", + "min-device-height", "max-device-height", "aspect-ratio", + "min-aspect-ratio", "max-aspect-ratio", "device-aspect-ratio", + "min-device-aspect-ratio", "max-device-aspect-ratio", "color", "min-color", + "max-color", "color-index", "min-color-index", "max-color-index", + "monochrome", "min-monochrome", "max-monochrome", "resolution", + "min-resolution", "max-resolution", "scan", "grid" + ], mediaFeatures = keySet(mediaFeatures_); + + var propertyKeywords_ = [ + "align-content", "align-items", "align-self", "alignment-adjust", + "alignment-baseline", "anchor-point", "animation", "animation-delay", + "animation-direction", "animation-duration", "animation-fill-mode", + "animation-iteration-count", "animation-name", "animation-play-state", + "animation-timing-function", "appearance", "azimuth", "backface-visibility", + "background", "background-attachment", "background-clip", "background-color", + "background-image", "background-origin", "background-position", + "background-repeat", "background-size", "baseline-shift", "binding", + "bleed", "bookmark-label", "bookmark-level", "bookmark-state", + "bookmark-target", "border", "border-bottom", "border-bottom-color", + "border-bottom-left-radius", "border-bottom-right-radius", + "border-bottom-style", "border-bottom-width", "border-collapse", + "border-color", "border-image", "border-image-outset", + "border-image-repeat", "border-image-slice", "border-image-source", + "border-image-width", "border-left", "border-left-color", + "border-left-style", "border-left-width", "border-radius", "border-right", + "border-right-color", "border-right-style", "border-right-width", + "border-spacing", "border-style", "border-top", "border-top-color", + "border-top-left-radius", "border-top-right-radius", "border-top-style", + "border-top-width", "border-width", "bottom", "box-decoration-break", + "box-shadow", "box-sizing", "break-after", "break-before", "break-inside", + "caption-side", "clear", "clip", "color", "color-profile", "column-count", + "column-fill", "column-gap", "column-rule", "column-rule-color", + "column-rule-style", "column-rule-width", "column-span", "column-width", + "columns", "content", "counter-increment", "counter-reset", "crop", "cue", + "cue-after", "cue-before", "cursor", "direction", "display", + "dominant-baseline", "drop-initial-after-adjust", + "drop-initial-after-align", "drop-initial-before-adjust", + "drop-initial-before-align", "drop-initial-size", "drop-initial-value", + "elevation", "empty-cells", "fit", "fit-position", "flex", "flex-basis", + "flex-direction", "flex-flow", "flex-grow", "flex-shrink", "flex-wrap", + "float", "float-offset", "flow-from", "flow-into", "font", "font-feature-settings", + "font-family", "font-kerning", "font-language-override", "font-size", "font-size-adjust", + "font-stretch", "font-style", "font-synthesis", "font-variant", + "font-variant-alternates", "font-variant-caps", "font-variant-east-asian", + "font-variant-ligatures", "font-variant-numeric", "font-variant-position", + "font-weight", "grid", "grid-area", "grid-auto-columns", "grid-auto-flow", + "grid-auto-position", "grid-auto-rows", "grid-column", "grid-column-end", + "grid-column-start", "grid-row", "grid-row-end", "grid-row-start", + "grid-template", "grid-template-areas", "grid-template-columns", + "grid-template-rows", "hanging-punctuation", "height", "hyphens", + "icon", "image-orientation", "image-rendering", "image-resolution", + "inline-box-align", "justify-content", "left", "letter-spacing", + "line-break", "line-height", "line-stacking", "line-stacking-ruby", + "line-stacking-shift", "line-stacking-strategy", "list-style", + "list-style-image", "list-style-position", "list-style-type", "margin", + "margin-bottom", "margin-left", "margin-right", "margin-top", + "marker-offset", "marks", "marquee-direction", "marquee-loop", + "marquee-play-count", "marquee-speed", "marquee-style", "max-height", + "max-width", "min-height", "min-width", "move-to", "nav-down", "nav-index", + "nav-left", "nav-right", "nav-up", "object-fit", "object-position", + "opacity", "order", "orphans", "outline", + "outline-color", "outline-offset", "outline-style", "outline-width", + "overflow", "overflow-style", "overflow-wrap", "overflow-x", "overflow-y", + "padding", "padding-bottom", "padding-left", "padding-right", "padding-top", + "page", "page-break-after", "page-break-before", "page-break-inside", + "page-policy", "pause", "pause-after", "pause-before", "perspective", + "perspective-origin", "pitch", "pitch-range", "play-during", "position", + "presentation-level", "punctuation-trim", "quotes", "region-break-after", + "region-break-before", "region-break-inside", "region-fragment", + "rendering-intent", "resize", "rest", "rest-after", "rest-before", "richness", + "right", "rotation", "rotation-point", "ruby-align", "ruby-overhang", + "ruby-position", "ruby-span", "shape-image-threshold", "shape-inside", "shape-margin", + "shape-outside", "size", "speak", "speak-as", "speak-header", + "speak-numeral", "speak-punctuation", "speech-rate", "stress", "string-set", + "tab-size", "table-layout", "target", "target-name", "target-new", + "target-position", "text-align", "text-align-last", "text-decoration", + "text-decoration-color", "text-decoration-line", "text-decoration-skip", + "text-decoration-style", "text-emphasis", "text-emphasis-color", + "text-emphasis-position", "text-emphasis-style", "text-height", + "text-indent", "text-justify", "text-outline", "text-overflow", "text-shadow", + "text-size-adjust", "text-space-collapse", "text-transform", "text-underline-position", + "text-wrap", "top", "transform", "transform-origin", "transform-style", + "transition", "transition-delay", "transition-duration", + "transition-property", "transition-timing-function", "unicode-bidi", + "vertical-align", "visibility", "voice-balance", "voice-duration", + "voice-family", "voice-pitch", "voice-range", "voice-rate", "voice-stress", + "voice-volume", "volume", "white-space", "widows", "width", "word-break", + "word-spacing", "word-wrap", "z-index", + // SVG-specific + "clip-path", "clip-rule", "mask", "enable-background", "filter", "flood-color", + "flood-opacity", "lighting-color", "stop-color", "stop-opacity", "pointer-events", + "color-interpolation", "color-interpolation-filters", + "color-rendering", "fill", "fill-opacity", "fill-rule", "image-rendering", + "marker", "marker-end", "marker-mid", "marker-start", "shape-rendering", "stroke", + "stroke-dasharray", "stroke-dashoffset", "stroke-linecap", "stroke-linejoin", + "stroke-miterlimit", "stroke-opacity", "stroke-width", "text-rendering", + "baseline-shift", "dominant-baseline", "glyph-orientation-horizontal", + "glyph-orientation-vertical", "text-anchor", "writing-mode" + ], propertyKeywords = keySet(propertyKeywords_); + + var nonStandardPropertyKeywords_ = [ + "scrollbar-arrow-color", "scrollbar-base-color", "scrollbar-dark-shadow-color", + "scrollbar-face-color", "scrollbar-highlight-color", "scrollbar-shadow-color", + "scrollbar-3d-light-color", "scrollbar-track-color", "shape-inside", + "searchfield-cancel-button", "searchfield-decoration", "searchfield-results-button", + "searchfield-results-decoration", "zoom" + ], nonStandardPropertyKeywords = keySet(nonStandardPropertyKeywords_); + + var fontProperties_ = [ + "font-family", "src", "unicode-range", "font-variant", "font-feature-settings", + "font-stretch", "font-weight", "font-style" + ], fontProperties = keySet(fontProperties_); + + var counterDescriptors_ = [ + "additive-symbols", "fallback", "negative", "pad", "prefix", "range", + "speak-as", "suffix", "symbols", "system" + ], counterDescriptors = keySet(counterDescriptors_); + + var colorKeywords_ = [ + "aliceblue", "antiquewhite", "aqua", "aquamarine", "azure", "beige", + "bisque", "black", "blanchedalmond", "blue", "blueviolet", "brown", + "burlywood", "cadetblue", "chartreuse", "chocolate", "coral", "cornflowerblue", + "cornsilk", "crimson", "cyan", "darkblue", "darkcyan", "darkgoldenrod", + "darkgray", "darkgreen", "darkkhaki", "darkmagenta", "darkolivegreen", + "darkorange", "darkorchid", "darkred", "darksalmon", "darkseagreen", + "darkslateblue", "darkslategray", "darkturquoise", "darkviolet", + "deeppink", "deepskyblue", "dimgray", "dodgerblue", "firebrick", + "floralwhite", "forestgreen", "fuchsia", "gainsboro", "ghostwhite", + "gold", "goldenrod", "gray", "grey", "green", "greenyellow", "honeydew", + "hotpink", "indianred", "indigo", "ivory", "khaki", "lavender", + "lavenderblush", "lawngreen", "lemonchiffon", "lightblue", "lightcoral", + "lightcyan", "lightgoldenrodyellow", "lightgray", "lightgreen", "lightpink", + "lightsalmon", "lightseagreen", "lightskyblue", "lightslategray", + "lightsteelblue", "lightyellow", "lime", "limegreen", "linen", "magenta", + "maroon", "mediumaquamarine", "mediumblue", "mediumorchid", "mediumpurple", + "mediumseagreen", "mediumslateblue", "mediumspringgreen", "mediumturquoise", + "mediumvioletred", "midnightblue", "mintcream", "mistyrose", "moccasin", + "navajowhite", "navy", "oldlace", "olive", "olivedrab", "orange", "orangered", + "orchid", "palegoldenrod", "palegreen", "paleturquoise", "palevioletred", + "papayawhip", "peachpuff", "peru", "pink", "plum", "powderblue", + "purple", "rebeccapurple", "red", "rosybrown", "royalblue", "saddlebrown", + "salmon", "sandybrown", "seagreen", "seashell", "sienna", "silver", "skyblue", + "slateblue", "slategray", "snow", "springgreen", "steelblue", "tan", + "teal", "thistle", "tomato", "turquoise", "violet", "wheat", "white", + "whitesmoke", "yellow", "yellowgreen" + ], colorKeywords = keySet(colorKeywords_); + + var valueKeywords_ = [ + "above", "absolute", "activeborder", "additive", "activecaption", "afar", + "after-white-space", "ahead", "alias", "all", "all-scroll", "alphabetic", "alternate", + "always", "amharic", "amharic-abegede", "antialiased", "appworkspace", + "arabic-indic", "armenian", "asterisks", "attr", "auto", "avoid", "avoid-column", "avoid-page", + "avoid-region", "background", "backwards", "baseline", "below", "bidi-override", "binary", + "bengali", "blink", "block", "block-axis", "bold", "bolder", "border", "border-box", + "both", "bottom", "break", "break-all", "break-word", "bullets", "button", "button-bevel", + "buttonface", "buttonhighlight", "buttonshadow", "buttontext", "calc", "cambodian", + "capitalize", "caps-lock-indicator", "caption", "captiontext", "caret", + "cell", "center", "checkbox", "circle", "cjk-decimal", "cjk-earthly-branch", + "cjk-heavenly-stem", "cjk-ideographic", "clear", "clip", "close-quote", + "col-resize", "collapse", "column", "compact", "condensed", "contain", "content", + "content-box", "context-menu", "continuous", "copy", "counter", "counters", "cover", "crop", + "cross", "crosshair", "currentcolor", "cursive", "cyclic", "dashed", "decimal", + "decimal-leading-zero", "default", "default-button", "destination-atop", + "destination-in", "destination-out", "destination-over", "devanagari", + "disc", "discard", "disclosure-closed", "disclosure-open", "document", + "dot-dash", "dot-dot-dash", + "dotted", "double", "down", "e-resize", "ease", "ease-in", "ease-in-out", "ease-out", + "element", "ellipse", "ellipsis", "embed", "end", "ethiopic", "ethiopic-abegede", + "ethiopic-abegede-am-et", "ethiopic-abegede-gez", "ethiopic-abegede-ti-er", + "ethiopic-abegede-ti-et", "ethiopic-halehame-aa-er", + "ethiopic-halehame-aa-et", "ethiopic-halehame-am-et", + "ethiopic-halehame-gez", "ethiopic-halehame-om-et", + "ethiopic-halehame-sid-et", "ethiopic-halehame-so-et", + "ethiopic-halehame-ti-er", "ethiopic-halehame-ti-et", "ethiopic-halehame-tig", + "ethiopic-numeric", "ew-resize", "expanded", "extends", "extra-condensed", + "extra-expanded", "fantasy", "fast", "fill", "fixed", "flat", "flex", "footnotes", + "forwards", "from", "geometricPrecision", "georgian", "graytext", "groove", + "gujarati", "gurmukhi", "hand", "hangul", "hangul-consonant", "hebrew", + "help", "hidden", "hide", "higher", "highlight", "highlighttext", + "hiragana", "hiragana-iroha", "horizontal", "hsl", "hsla", "icon", "ignore", + "inactiveborder", "inactivecaption", "inactivecaptiontext", "infinite", + "infobackground", "infotext", "inherit", "initial", "inline", "inline-axis", + "inline-block", "inline-flex", "inline-table", "inset", "inside", "intrinsic", "invert", + "italic", "japanese-formal", "japanese-informal", "justify", "kannada", + "katakana", "katakana-iroha", "keep-all", "khmer", + "korean-hangul-formal", "korean-hanja-formal", "korean-hanja-informal", + "landscape", "lao", "large", "larger", "left", "level", "lighter", + "line-through", "linear", "linear-gradient", "lines", "list-item", "listbox", "listitem", + "local", "logical", "loud", "lower", "lower-alpha", "lower-armenian", + "lower-greek", "lower-hexadecimal", "lower-latin", "lower-norwegian", + "lower-roman", "lowercase", "ltr", "malayalam", "match", "matrix", "matrix3d", + "media-controls-background", "media-current-time-display", + "media-fullscreen-button", "media-mute-button", "media-play-button", + "media-return-to-realtime-button", "media-rewind-button", + "media-seek-back-button", "media-seek-forward-button", "media-slider", + "media-sliderthumb", "media-time-remaining-display", "media-volume-slider", + "media-volume-slider-container", "media-volume-sliderthumb", "medium", + "menu", "menulist", "menulist-button", "menulist-text", + "menulist-textfield", "menutext", "message-box", "middle", "min-intrinsic", + "mix", "mongolian", "monospace", "move", "multiple", "myanmar", "n-resize", + "narrower", "ne-resize", "nesw-resize", "no-close-quote", "no-drop", + "no-open-quote", "no-repeat", "none", "normal", "not-allowed", "nowrap", + "ns-resize", "numbers", "numeric", "nw-resize", "nwse-resize", "oblique", "octal", "open-quote", + "optimizeLegibility", "optimizeSpeed", "oriya", "oromo", "outset", + "outside", "outside-shape", "overlay", "overline", "padding", "padding-box", + "painted", "page", "paused", "persian", "perspective", "plus-darker", "plus-lighter", + "pointer", "polygon", "portrait", "pre", "pre-line", "pre-wrap", "preserve-3d", + "progress", "push-button", "radial-gradient", "radio", "read-only", + "read-write", "read-write-plaintext-only", "rectangle", "region", + "relative", "repeat", "repeating-linear-gradient", + "repeating-radial-gradient", "repeat-x", "repeat-y", "reset", "reverse", + "rgb", "rgba", "ridge", "right", "rotate", "rotate3d", "rotateX", "rotateY", + "rotateZ", "round", "row-resize", "rtl", "run-in", "running", + "s-resize", "sans-serif", "scale", "scale3d", "scaleX", "scaleY", "scaleZ", + "scroll", "scrollbar", "se-resize", "searchfield", + "searchfield-cancel-button", "searchfield-decoration", + "searchfield-results-button", "searchfield-results-decoration", + "semi-condensed", "semi-expanded", "separate", "serif", "show", "sidama", + "simp-chinese-formal", "simp-chinese-informal", "single", + "skew", "skewX", "skewY", "skip-white-space", "slide", "slider-horizontal", + "slider-vertical", "sliderthumb-horizontal", "sliderthumb-vertical", "slow", + "small", "small-caps", "small-caption", "smaller", "solid", "somali", + "source-atop", "source-in", "source-out", "source-over", "space", "spell-out", "square", + "square-button", "start", "static", "status-bar", "stretch", "stroke", "sub", + "subpixel-antialiased", "super", "sw-resize", "symbolic", "symbols", "table", + "table-caption", "table-cell", "table-column", "table-column-group", + "table-footer-group", "table-header-group", "table-row", "table-row-group", + "tamil", + "telugu", "text", "text-bottom", "text-top", "textarea", "textfield", "thai", + "thick", "thin", "threeddarkshadow", "threedface", "threedhighlight", + "threedlightshadow", "threedshadow", "tibetan", "tigre", "tigrinya-er", + "tigrinya-er-abegede", "tigrinya-et", "tigrinya-et-abegede", "to", "top", + "trad-chinese-formal", "trad-chinese-informal", + "translate", "translate3d", "translateX", "translateY", "translateZ", + "transparent", "ultra-condensed", "ultra-expanded", "underline", "up", + "upper-alpha", "upper-armenian", "upper-greek", "upper-hexadecimal", + "upper-latin", "upper-norwegian", "upper-roman", "uppercase", "urdu", "url", + "var", "vertical", "vertical-text", "visible", "visibleFill", "visiblePainted", + "visibleStroke", "visual", "w-resize", "wait", "wave", "wider", + "window", "windowframe", "windowtext", "words", "x-large", "x-small", "xor", + "xx-large", "xx-small" + ], valueKeywords = keySet(valueKeywords_); + + var allWords = documentTypes_.concat(mediaTypes_).concat(mediaFeatures_).concat(propertyKeywords_) + .concat(nonStandardPropertyKeywords_).concat(colorKeywords_).concat(valueKeywords_); + CodeMirror.registerHelper("hintWords", "css", allWords); + + function tokenCComment(stream, state) { + var maybeEnd = false, ch; + while ((ch = stream.next()) != null) { + if (maybeEnd && ch == "/") { + state.tokenize = null; + break; + } + maybeEnd = (ch == "*"); + } + return ["comment", "comment"]; + } + + CodeMirror.defineMIME("text/css", { + documentTypes: documentTypes, + mediaTypes: mediaTypes, + mediaFeatures: mediaFeatures, + propertyKeywords: propertyKeywords, + nonStandardPropertyKeywords: nonStandardPropertyKeywords, + fontProperties: fontProperties, + counterDescriptors: counterDescriptors, + colorKeywords: colorKeywords, + valueKeywords: valueKeywords, + tokenHooks: { + "/": function(stream, state) { + if (!stream.eat("*")) return false; + state.tokenize = tokenCComment; + return tokenCComment(stream, state); + } + }, + name: "css" + }); + + CodeMirror.defineMIME("text/x-scss", { + mediaTypes: mediaTypes, + mediaFeatures: mediaFeatures, + propertyKeywords: propertyKeywords, + nonStandardPropertyKeywords: nonStandardPropertyKeywords, + colorKeywords: colorKeywords, + valueKeywords: valueKeywords, + fontProperties: fontProperties, + allowNested: true, + tokenHooks: { + "/": function(stream, state) { + if (stream.eat("/")) { + stream.skipToEnd(); + return ["comment", "comment"]; + } else if (stream.eat("*")) { + state.tokenize = tokenCComment; + return tokenCComment(stream, state); + } else { + return ["operator", "operator"]; + } + }, + ":": function(stream) { + if (stream.match(/\s*\{/)) + return [null, "{"]; + return false; + }, + "$": function(stream) { + stream.match(/^[\w-]+/); + if (stream.match(/^\s*:/, false)) + return ["variable-2", "variable-definition"]; + return ["variable-2", "variable"]; + }, + "#": function(stream) { + if (!stream.eat("{")) return false; + return [null, "interpolation"]; + } + }, + name: "css", + helperType: "scss" + }); + + CodeMirror.defineMIME("text/x-less", { + mediaTypes: mediaTypes, + mediaFeatures: mediaFeatures, + propertyKeywords: propertyKeywords, + nonStandardPropertyKeywords: nonStandardPropertyKeywords, + colorKeywords: colorKeywords, + valueKeywords: valueKeywords, + fontProperties: fontProperties, + allowNested: true, + tokenHooks: { + "/": function(stream, state) { + if (stream.eat("/")) { + stream.skipToEnd(); + return ["comment", "comment"]; + } else if (stream.eat("*")) { + state.tokenize = tokenCComment; + return tokenCComment(stream, state); + } else { + return ["operator", "operator"]; + } + }, + "@": function(stream) { + if (stream.eat("{")) return [null, "interpolation"]; + if (stream.match(/^(charset|document|font-face|import|(-(moz|ms|o|webkit)-)?keyframes|media|namespace|page|supports)\b/, false)) return false; + stream.eatWhile(/[\w\\\-]/); + if (stream.match(/^\s*:/, false)) + return ["variable-2", "variable-definition"]; + return ["variable-2", "variable"]; + }, + "&": function() { + return ["atom", "atom"]; + } + }, + name: "css", + helperType: "less" + }); + +}); diff --git a/web/dep/codemirror/htmlmixed.js b/web/dep/codemirror/htmlmixed.js new file mode 100644 index 00000000..24552e2d --- /dev/null +++ b/web/dep/codemirror/htmlmixed.js @@ -0,0 +1,121 @@ +// CodeMirror, copyright (c) by Marijn Haverbeke and others +// Distributed under an MIT license: http://codemirror.net/LICENSE + +(function(mod) { + if (typeof exports == "object" && typeof module == "object") // CommonJS + mod(require("../../lib/codemirror"), require("../xml/xml"), require("../javascript/javascript"), require("../css/css")); + else if (typeof define == "function" && define.amd) // AMD + define(["../../lib/codemirror", "../xml/xml", "../javascript/javascript", "../css/css"], mod); + else // Plain browser env + mod(CodeMirror); +})(function(CodeMirror) { +"use strict"; + +CodeMirror.defineMode("htmlmixed", function(config, parserConfig) { + var htmlMode = CodeMirror.getMode(config, {name: "xml", + htmlMode: true, + multilineTagIndentFactor: parserConfig.multilineTagIndentFactor, + multilineTagIndentPastTag: parserConfig.multilineTagIndentPastTag}); + var cssMode = CodeMirror.getMode(config, "css"); + + var scriptTypes = [], scriptTypesConf = parserConfig && parserConfig.scriptTypes; + scriptTypes.push({matches: /^(?:text|application)\/(?:x-)?(?:java|ecma)script$|^$/i, + mode: CodeMirror.getMode(config, "javascript")}); + if (scriptTypesConf) for (var i = 0; i < scriptTypesConf.length; ++i) { + var conf = scriptTypesConf[i]; + scriptTypes.push({matches: conf.matches, mode: conf.mode && CodeMirror.getMode(config, conf.mode)}); + } + scriptTypes.push({matches: /./, + mode: CodeMirror.getMode(config, "text/plain")}); + + function html(stream, state) { + var tagName = state.htmlState.tagName; + if (tagName) tagName = tagName.toLowerCase(); + var style = htmlMode.token(stream, state.htmlState); + if (tagName == "script" && /\btag\b/.test(style) && stream.current() == ">") { + // Script block: mode to change to depends on type attribute + var scriptType = stream.string.slice(Math.max(0, stream.pos - 100), stream.pos).match(/\btype\s*=\s*("[^"]+"|'[^']+'|\S+)[^<]*$/i); + scriptType = scriptType ? scriptType[1] : ""; + if (scriptType && /[\"\']/.test(scriptType.charAt(0))) scriptType = scriptType.slice(1, scriptType.length - 1); + for (var i = 0; i < scriptTypes.length; ++i) { + var tp = scriptTypes[i]; + if (typeof tp.matches == "string" ? scriptType == tp.matches : tp.matches.test(scriptType)) { + if (tp.mode) { + state.token = script; + state.localMode = tp.mode; + state.localState = tp.mode.startState && tp.mode.startState(htmlMode.indent(state.htmlState, "")); + } + break; + } + } + } else if (tagName == "style" && /\btag\b/.test(style) && stream.current() == ">") { + state.token = css; + state.localMode = cssMode; + state.localState = cssMode.startState(htmlMode.indent(state.htmlState, "")); + } + return style; + } + function maybeBackup(stream, pat, style) { + var cur = stream.current(); + var close = cur.search(pat); + if (close > -1) stream.backUp(cur.length - close); + else if (cur.match(/<\/?$/)) { + stream.backUp(cur.length); + if (!stream.match(pat, false)) stream.match(cur); + } + return style; + } + function script(stream, state) { + if (stream.match(/^<\/\s*script\s*>/i, false)) { + state.token = html; + state.localState = state.localMode = null; + return null; + } + return maybeBackup(stream, /<\/\s*script\s*>/, + state.localMode.token(stream, state.localState)); + } + function css(stream, state) { + if (stream.match(/^<\/\s*style\s*>/i, false)) { + state.token = html; + state.localState = state.localMode = null; + return null; + } + return maybeBackup(stream, /<\/\s*style\s*>/, + cssMode.token(stream, state.localState)); + } + + return { + startState: function() { + var state = htmlMode.startState(); + return {token: html, localMode: null, localState: null, htmlState: state}; + }, + + copyState: function(state) { + if (state.localState) + var local = CodeMirror.copyState(state.localMode, state.localState); + return {token: state.token, localMode: state.localMode, localState: local, + htmlState: CodeMirror.copyState(htmlMode, state.htmlState)}; + }, + + token: function(stream, state) { + return state.token(stream, state); + }, + + indent: function(state, textAfter) { + if (!state.localMode || /^\s*<\//.test(textAfter)) + return htmlMode.indent(state.htmlState, textAfter); + else if (state.localMode.indent) + return state.localMode.indent(state.localState, textAfter); + else + return CodeMirror.Pass; + }, + + innerMode: function(state) { + return {state: state.localState || state.htmlState, mode: state.localMode || htmlMode}; + } + }; +}, "xml", "javascript", "css"); + +CodeMirror.defineMIME("text/html", "htmlmixed"); + +}); diff --git a/web/dep/codemirror/javascript.js b/web/dep/codemirror/javascript.js new file mode 100644 index 00000000..ef018478 --- /dev/null +++ b/web/dep/codemirror/javascript.js @@ -0,0 +1,701 @@ +// CodeMirror, copyright (c) by Marijn Haverbeke and others +// Distributed under an MIT license: http://codemirror.net/LICENSE + +// TODO actually recognize syntax of TypeScript constructs + +(function(mod) { + if (typeof exports == "object" && typeof module == "object") // CommonJS + mod(require("../../lib/codemirror")); + else if (typeof define == "function" && define.amd) // AMD + define(["../../lib/codemirror"], mod); + else // Plain browser env + mod(CodeMirror); +})(function(CodeMirror) { +"use strict"; + +CodeMirror.defineMode("javascript", function(config, parserConfig) { + var indentUnit = config.indentUnit; + var statementIndent = parserConfig.statementIndent; + var jsonldMode = parserConfig.jsonld; + var jsonMode = parserConfig.json || jsonldMode; + var isTS = parserConfig.typescript; + var wordRE = parserConfig.wordCharacters || /[\w$\xa1-\uffff]/; + + // Tokenizer + + var keywords = function(){ + function kw(type) {return {type: type, style: "keyword"};} + var A = kw("keyword a"), B = kw("keyword b"), C = kw("keyword c"); + var operator = kw("operator"), atom = {type: "atom", style: "atom"}; + + var jsKeywords = { + "if": kw("if"), "while": A, "with": A, "else": B, "do": B, "try": B, "finally": B, + "return": C, "break": C, "continue": C, "new": C, "delete": C, "throw": C, "debugger": C, + "var": kw("var"), "const": kw("var"), "let": kw("var"), + "function": kw("function"), "catch": kw("catch"), + "for": kw("for"), "switch": kw("switch"), "case": kw("case"), "default": kw("default"), + "in": operator, "typeof": operator, "instanceof": operator, + "true": atom, "false": atom, "null": atom, "undefined": atom, "NaN": atom, "Infinity": atom, + "this": kw("this"), "module": kw("module"), "class": kw("class"), "super": kw("atom"), + "yield": C, "export": kw("export"), "import": kw("import"), "extends": C + }; + + // Extend the 'normal' keywords with the TypeScript language extensions + if (isTS) { + var type = {type: "variable", style: "variable-3"}; + var tsKeywords = { + // object-like things + "interface": kw("interface"), + "extends": kw("extends"), + "constructor": kw("constructor"), + + // scope modifiers + "public": kw("public"), + "private": kw("private"), + "protected": kw("protected"), + "static": kw("static"), + + // types + "string": type, "number": type, "bool": type, "any": type + }; + + for (var attr in tsKeywords) { + jsKeywords[attr] = tsKeywords[attr]; + } + } + + return jsKeywords; + }(); + + var isOperatorChar = /[+\-*&%=<>!?|~^]/; + var isJsonldKeyword = /^@(context|id|value|language|type|container|list|set|reverse|index|base|vocab|graph)"/; + + function readRegexp(stream) { + var escaped = false, next, inSet = false; + while ((next = stream.next()) != null) { + if (!escaped) { + if (next == "/" && !inSet) return; + if (next == "[") inSet = true; + else if (inSet && next == "]") inSet = false; + } + escaped = !escaped && next == "\\"; + } + } + + // Used as scratch variables to communicate multiple values without + // consing up tons of objects. + var type, content; + function ret(tp, style, cont) { + type = tp; content = cont; + return style; + } + function tokenBase(stream, state) { + var ch = stream.next(); + if (ch == '"' || ch == "'") { + state.tokenize = tokenString(ch); + return state.tokenize(stream, state); + } else if (ch == "." && stream.match(/^\d+(?:[eE][+\-]?\d+)?/)) { + return ret("number", "number"); + } else if (ch == "." && stream.match("..")) { + return ret("spread", "meta"); + } else if (/[\[\]{}\(\),;\:\.]/.test(ch)) { + return ret(ch); + } else if (ch == "=" && stream.eat(">")) { + return ret("=>", "operator"); + } else if (ch == "0" && stream.eat(/x/i)) { + stream.eatWhile(/[\da-f]/i); + return ret("number", "number"); + } else if (/\d/.test(ch)) { + stream.match(/^\d*(?:\.\d*)?(?:[eE][+\-]?\d+)?/); + return ret("number", "number"); + } else if (ch == "/") { + if (stream.eat("*")) { + state.tokenize = tokenComment; + return tokenComment(stream, state); + } else if (stream.eat("/")) { + stream.skipToEnd(); + return ret("comment", "comment"); + } else if (state.lastType == "operator" || state.lastType == "keyword c" || + state.lastType == "sof" || /^[\[{}\(,;:]$/.test(state.lastType)) { + readRegexp(stream); + stream.match(/^\b(([gimyu])(?![gimyu]*\2))+\b/); + return ret("regexp", "string-2"); + } else { + stream.eatWhile(isOperatorChar); + return ret("operator", "operator", stream.current()); + } + } else if (ch == "`") { + state.tokenize = tokenQuasi; + return tokenQuasi(stream, state); + } else if (ch == "#") { + stream.skipToEnd(); + return ret("error", "error"); + } else if (isOperatorChar.test(ch)) { + stream.eatWhile(isOperatorChar); + return ret("operator", "operator", stream.current()); + } else if (wordRE.test(ch)) { + stream.eatWhile(wordRE); + var word = stream.current(), known = keywords.propertyIsEnumerable(word) && keywords[word]; + return (known && state.lastType != ".") ? ret(known.type, known.style, word) : + ret("variable", "variable", word); + } + } + + function tokenString(quote) { + return function(stream, state) { + var escaped = false, next; + if (jsonldMode && stream.peek() == "@" && stream.match(isJsonldKeyword)){ + state.tokenize = tokenBase; + return ret("jsonld-keyword", "meta"); + } + while ((next = stream.next()) != null) { + if (next == quote && !escaped) break; + escaped = !escaped && next == "\\"; + } + if (!escaped) state.tokenize = tokenBase; + return ret("string", "string"); + }; + } + + function tokenComment(stream, state) { + var maybeEnd = false, ch; + while (ch = stream.next()) { + if (ch == "/" && maybeEnd) { + state.tokenize = tokenBase; + break; + } + maybeEnd = (ch == "*"); + } + return ret("comment", "comment"); + } + + function tokenQuasi(stream, state) { + var escaped = false, next; + while ((next = stream.next()) != null) { + if (!escaped && (next == "`" || next == "$" && stream.eat("{"))) { + state.tokenize = tokenBase; + break; + } + escaped = !escaped && next == "\\"; + } + return ret("quasi", "string-2", stream.current()); + } + + var brackets = "([{}])"; + // This is a crude lookahead trick to try and notice that we're + // parsing the argument patterns for a fat-arrow function before we + // actually hit the arrow token. It only works if the arrow is on + // the same line as the arguments and there's no strange noise + // (comments) in between. Fallback is to only notice when we hit the + // arrow, and not declare the arguments as locals for the arrow + // body. + function findFatArrow(stream, state) { + if (state.fatArrowAt) state.fatArrowAt = null; + var arrow = stream.string.indexOf("=>", stream.start); + if (arrow < 0) return; + + var depth = 0, sawSomething = false; + for (var pos = arrow - 1; pos >= 0; --pos) { + var ch = stream.string.charAt(pos); + var bracket = brackets.indexOf(ch); + if (bracket >= 0 && bracket < 3) { + if (!depth) { ++pos; break; } + if (--depth == 0) break; + } else if (bracket >= 3 && bracket < 6) { + ++depth; + } else if (wordRE.test(ch)) { + sawSomething = true; + } else if (/["'\/]/.test(ch)) { + return; + } else if (sawSomething && !depth) { + ++pos; + break; + } + } + if (sawSomething && !depth) state.fatArrowAt = pos; + } + + // Parser + + var atomicTypes = {"atom": true, "number": true, "variable": true, "string": true, "regexp": true, "this": true, "jsonld-keyword": true}; + + function JSLexical(indented, column, type, align, prev, info) { + this.indented = indented; + this.column = column; + this.type = type; + this.prev = prev; + this.info = info; + if (align != null) this.align = align; + } + + function inScope(state, varname) { + for (var v = state.localVars; v; v = v.next) + if (v.name == varname) return true; + for (var cx = state.context; cx; cx = cx.prev) { + for (var v = cx.vars; v; v = v.next) + if (v.name == varname) return true; + } + } + + function parseJS(state, style, type, content, stream) { + var cc = state.cc; + // Communicate our context to the combinators. + // (Less wasteful than consing up a hundred closures on every call.) + cx.state = state; cx.stream = stream; cx.marked = null, cx.cc = cc; cx.style = style; + + if (!state.lexical.hasOwnProperty("align")) + state.lexical.align = true; + + while(true) { + var combinator = cc.length ? cc.pop() : jsonMode ? expression : statement; + if (combinator(type, content)) { + while(cc.length && cc[cc.length - 1].lex) + cc.pop()(); + if (cx.marked) return cx.marked; + if (type == "variable" && inScope(state, content)) return "variable-2"; + return style; + } + } + } + + // Combinator utils + + var cx = {state: null, column: null, marked: null, cc: null}; + function pass() { + for (var i = arguments.length - 1; i >= 0; i--) cx.cc.push(arguments[i]); + } + function cont() { + pass.apply(null, arguments); + return true; + } + function register(varname) { + function inList(list) { + for (var v = list; v; v = v.next) + if (v.name == varname) return true; + return false; + } + var state = cx.state; + if (state.context) { + cx.marked = "def"; + if (inList(state.localVars)) return; + state.localVars = {name: varname, next: state.localVars}; + } else { + if (inList(state.globalVars)) return; + if (parserConfig.globalVars) + state.globalVars = {name: varname, next: state.globalVars}; + } + } + + // Combinators + + var defaultVars = {name: "this", next: {name: "arguments"}}; + function pushcontext() { + cx.state.context = {prev: cx.state.context, vars: cx.state.localVars}; + cx.state.localVars = defaultVars; + } + function popcontext() { + cx.state.localVars = cx.state.context.vars; + cx.state.context = cx.state.context.prev; + } + function pushlex(type, info) { + var result = function() { + var state = cx.state, indent = state.indented; + if (state.lexical.type == "stat") indent = state.lexical.indented; + else for (var outer = state.lexical; outer && outer.type == ")" && outer.align; outer = outer.prev) + indent = outer.indented; + state.lexical = new JSLexical(indent, cx.stream.column(), type, null, state.lexical, info); + }; + result.lex = true; + return result; + } + function poplex() { + var state = cx.state; + if (state.lexical.prev) { + if (state.lexical.type == ")") + state.indented = state.lexical.indented; + state.lexical = state.lexical.prev; + } + } + poplex.lex = true; + + function expect(wanted) { + function exp(type) { + if (type == wanted) return cont(); + else if (wanted == ";") return pass(); + else return cont(exp); + }; + return exp; + } + + function statement(type, value) { + if (type == "var") return cont(pushlex("vardef", value.length), vardef, expect(";"), poplex); + if (type == "keyword a") return cont(pushlex("form"), expression, statement, poplex); + if (type == "keyword b") return cont(pushlex("form"), statement, poplex); + if (type == "{") return cont(pushlex("}"), block, poplex); + if (type == ";") return cont(); + if (type == "if") { + if (cx.state.lexical.info == "else" && cx.state.cc[cx.state.cc.length - 1] == poplex) + cx.state.cc.pop()(); + return cont(pushlex("form"), expression, statement, poplex, maybeelse); + } + if (type == "function") return cont(functiondef); + if (type == "for") return cont(pushlex("form"), forspec, statement, poplex); + if (type == "variable") return cont(pushlex("stat"), maybelabel); + if (type == "switch") return cont(pushlex("form"), expression, pushlex("}", "switch"), expect("{"), + block, poplex, poplex); + if (type == "case") return cont(expression, expect(":")); + if (type == "default") return cont(expect(":")); + if (type == "catch") return cont(pushlex("form"), pushcontext, expect("("), funarg, expect(")"), + statement, poplex, popcontext); + if (type == "module") return cont(pushlex("form"), pushcontext, afterModule, popcontext, poplex); + if (type == "class") return cont(pushlex("form"), className, poplex); + if (type == "export") return cont(pushlex("form"), afterExport, poplex); + if (type == "import") return cont(pushlex("form"), afterImport, poplex); + return pass(pushlex("stat"), expression, expect(";"), poplex); + } + function expression(type) { + return expressionInner(type, false); + } + function expressionNoComma(type) { + return expressionInner(type, true); + } + function expressionInner(type, noComma) { + if (cx.state.fatArrowAt == cx.stream.start) { + var body = noComma ? arrowBodyNoComma : arrowBody; + if (type == "(") return cont(pushcontext, pushlex(")"), commasep(pattern, ")"), poplex, expect("=>"), body, popcontext); + else if (type == "variable") return pass(pushcontext, pattern, expect("=>"), body, popcontext); + } + + var maybeop = noComma ? maybeoperatorNoComma : maybeoperatorComma; + if (atomicTypes.hasOwnProperty(type)) return cont(maybeop); + if (type == "function") return cont(functiondef, maybeop); + if (type == "keyword c") return cont(noComma ? maybeexpressionNoComma : maybeexpression); + if (type == "(") return cont(pushlex(")"), maybeexpression, comprehension, expect(")"), poplex, maybeop); + if (type == "operator" || type == "spread") return cont(noComma ? expressionNoComma : expression); + if (type == "[") return cont(pushlex("]"), arrayLiteral, poplex, maybeop); + if (type == "{") return contCommasep(objprop, "}", null, maybeop); + if (type == "quasi") { return pass(quasi, maybeop); } + return cont(); + } + function maybeexpression(type) { + if (type.match(/[;\}\)\],]/)) return pass(); + return pass(expression); + } + function maybeexpressionNoComma(type) { + if (type.match(/[;\}\)\],]/)) return pass(); + return pass(expressionNoComma); + } + + function maybeoperatorComma(type, value) { + if (type == ",") return cont(expression); + return maybeoperatorNoComma(type, value, false); + } + function maybeoperatorNoComma(type, value, noComma) { + var me = noComma == false ? maybeoperatorComma : maybeoperatorNoComma; + var expr = noComma == false ? expression : expressionNoComma; + if (type == "=>") return cont(pushcontext, noComma ? arrowBodyNoComma : arrowBody, popcontext); + if (type == "operator") { + if (/\+\+|--/.test(value)) return cont(me); + if (value == "?") return cont(expression, expect(":"), expr); + return cont(expr); + } + if (type == "quasi") { return pass(quasi, me); } + if (type == ";") return; + if (type == "(") return contCommasep(expressionNoComma, ")", "call", me); + if (type == ".") return cont(property, me); + if (type == "[") return cont(pushlex("]"), maybeexpression, expect("]"), poplex, me); + } + function quasi(type, value) { + if (type != "quasi") return pass(); + if (value.slice(value.length - 2) != "${") return cont(quasi); + return cont(expression, continueQuasi); + } + function continueQuasi(type) { + if (type == "}") { + cx.marked = "string-2"; + cx.state.tokenize = tokenQuasi; + return cont(quasi); + } + } + function arrowBody(type) { + findFatArrow(cx.stream, cx.state); + return pass(type == "{" ? statement : expression); + } + function arrowBodyNoComma(type) { + findFatArrow(cx.stream, cx.state); + return pass(type == "{" ? statement : expressionNoComma); + } + function maybelabel(type) { + if (type == ":") return cont(poplex, statement); + return pass(maybeoperatorComma, expect(";"), poplex); + } + function property(type) { + if (type == "variable") {cx.marked = "property"; return cont();} + } + function objprop(type, value) { + if (type == "variable" || cx.style == "keyword") { + cx.marked = "property"; + if (value == "get" || value == "set") return cont(getterSetter); + return cont(afterprop); + } else if (type == "number" || type == "string") { + cx.marked = jsonldMode ? "property" : (cx.style + " property"); + return cont(afterprop); + } else if (type == "jsonld-keyword") { + return cont(afterprop); + } else if (type == "[") { + return cont(expression, expect("]"), afterprop); + } + } + function getterSetter(type) { + if (type != "variable") return pass(afterprop); + cx.marked = "property"; + return cont(functiondef); + } + function afterprop(type) { + if (type == ":") return cont(expressionNoComma); + if (type == "(") return pass(functiondef); + } + function commasep(what, end) { + function proceed(type) { + if (type == ",") { + var lex = cx.state.lexical; + if (lex.info == "call") lex.pos = (lex.pos || 0) + 1; + return cont(what, proceed); + } + if (type == end) return cont(); + return cont(expect(end)); + } + return function(type) { + if (type == end) return cont(); + return pass(what, proceed); + }; + } + function contCommasep(what, end, info) { + for (var i = 3; i < arguments.length; i++) + cx.cc.push(arguments[i]); + return cont(pushlex(end, info), commasep(what, end), poplex); + } + function block(type) { + if (type == "}") return cont(); + return pass(statement, block); + } + function maybetype(type) { + if (isTS && type == ":") return cont(typedef); + } + function typedef(type) { + if (type == "variable"){cx.marked = "variable-3"; return cont();} + } + function vardef() { + return pass(pattern, maybetype, maybeAssign, vardefCont); + } + function pattern(type, value) { + if (type == "variable") { register(value); return cont(); } + if (type == "[") return contCommasep(pattern, "]"); + if (type == "{") return contCommasep(proppattern, "}"); + } + function proppattern(type, value) { + if (type == "variable" && !cx.stream.match(/^\s*:/, false)) { + register(value); + return cont(maybeAssign); + } + if (type == "variable") cx.marked = "property"; + return cont(expect(":"), pattern, maybeAssign); + } + function maybeAssign(_type, value) { + if (value == "=") return cont(expressionNoComma); + } + function vardefCont(type) { + if (type == ",") return cont(vardef); + } + function maybeelse(type, value) { + if (type == "keyword b" && value == "else") return cont(pushlex("form", "else"), statement, poplex); + } + function forspec(type) { + if (type == "(") return cont(pushlex(")"), forspec1, expect(")"), poplex); + } + function forspec1(type) { + if (type == "var") return cont(vardef, expect(";"), forspec2); + if (type == ";") return cont(forspec2); + if (type == "variable") return cont(formaybeinof); + return pass(expression, expect(";"), forspec2); + } + function formaybeinof(_type, value) { + if (value == "in" || value == "of") { cx.marked = "keyword"; return cont(expression); } + return cont(maybeoperatorComma, forspec2); + } + function forspec2(type, value) { + if (type == ";") return cont(forspec3); + if (value == "in" || value == "of") { cx.marked = "keyword"; return cont(expression); } + return pass(expression, expect(";"), forspec3); + } + function forspec3(type) { + if (type != ")") cont(expression); + } + function functiondef(type, value) { + if (value == "*") {cx.marked = "keyword"; return cont(functiondef);} + if (type == "variable") {register(value); return cont(functiondef);} + if (type == "(") return cont(pushcontext, pushlex(")"), commasep(funarg, ")"), poplex, statement, popcontext); + } + function funarg(type) { + if (type == "spread") return cont(funarg); + return pass(pattern, maybetype); + } + function className(type, value) { + if (type == "variable") {register(value); return cont(classNameAfter);} + } + function classNameAfter(type, value) { + if (value == "extends") return cont(expression, classNameAfter); + if (type == "{") return cont(pushlex("}"), classBody, poplex); + } + function classBody(type, value) { + if (type == "variable" || cx.style == "keyword") { + if (value == "static") { + cx.marked = "keyword"; + return cont(classBody); + } + cx.marked = "property"; + if (value == "get" || value == "set") return cont(classGetterSetter, functiondef, classBody); + return cont(functiondef, classBody); + } + if (value == "*") { + cx.marked = "keyword"; + return cont(classBody); + } + if (type == ";") return cont(classBody); + if (type == "}") return cont(); + } + function classGetterSetter(type) { + if (type != "variable") return pass(); + cx.marked = "property"; + return cont(); + } + function afterModule(type, value) { + if (type == "string") return cont(statement); + if (type == "variable") { register(value); return cont(maybeFrom); } + } + function afterExport(_type, value) { + if (value == "*") { cx.marked = "keyword"; return cont(maybeFrom, expect(";")); } + if (value == "default") { cx.marked = "keyword"; return cont(expression, expect(";")); } + return pass(statement); + } + function afterImport(type) { + if (type == "string") return cont(); + return pass(importSpec, maybeFrom); + } + function importSpec(type, value) { + if (type == "{") return contCommasep(importSpec, "}"); + if (type == "variable") register(value); + if (value == "*") cx.marked = "keyword"; + return cont(maybeAs); + } + function maybeAs(_type, value) { + if (value == "as") { cx.marked = "keyword"; return cont(importSpec); } + } + function maybeFrom(_type, value) { + if (value == "from") { cx.marked = "keyword"; return cont(expression); } + } + function arrayLiteral(type) { + if (type == "]") return cont(); + return pass(expressionNoComma, maybeArrayComprehension); + } + function maybeArrayComprehension(type) { + if (type == "for") return pass(comprehension, expect("]")); + if (type == ",") return cont(commasep(maybeexpressionNoComma, "]")); + return pass(commasep(expressionNoComma, "]")); + } + function comprehension(type) { + if (type == "for") return cont(forspec, comprehension); + if (type == "if") return cont(expression, comprehension); + } + + function isContinuedStatement(state, textAfter) { + return state.lastType == "operator" || state.lastType == "," || + isOperatorChar.test(textAfter.charAt(0)) || + /[,.]/.test(textAfter.charAt(0)); + } + + // Interface + + return { + startState: function(basecolumn) { + var state = { + tokenize: tokenBase, + lastType: "sof", + cc: [], + lexical: new JSLexical((basecolumn || 0) - indentUnit, 0, "block", false), + localVars: parserConfig.localVars, + context: parserConfig.localVars && {vars: parserConfig.localVars}, + indented: 0 + }; + if (parserConfig.globalVars && typeof parserConfig.globalVars == "object") + state.globalVars = parserConfig.globalVars; + return state; + }, + + token: function(stream, state) { + if (stream.sol()) { + if (!state.lexical.hasOwnProperty("align")) + state.lexical.align = false; + state.indented = stream.indentation(); + findFatArrow(stream, state); + } + if (state.tokenize != tokenComment && stream.eatSpace()) return null; + var style = state.tokenize(stream, state); + if (type == "comment") return style; + state.lastType = type == "operator" && (content == "++" || content == "--") ? "incdec" : type; + return parseJS(state, style, type, content, stream); + }, + + indent: function(state, textAfter) { + if (state.tokenize == tokenComment) return CodeMirror.Pass; + if (state.tokenize != tokenBase) return 0; + var firstChar = textAfter && textAfter.charAt(0), lexical = state.lexical; + // Kludge to prevent 'maybelse' from blocking lexical scope pops + if (!/^\s*else\b/.test(textAfter)) for (var i = state.cc.length - 1; i >= 0; --i) { + var c = state.cc[i]; + if (c == poplex) lexical = lexical.prev; + else if (c != maybeelse) break; + } + if (lexical.type == "stat" && firstChar == "}") lexical = lexical.prev; + if (statementIndent && lexical.type == ")" && lexical.prev.type == "stat") + lexical = lexical.prev; + var type = lexical.type, closing = firstChar == type; + + if (type == "vardef") return lexical.indented + (state.lastType == "operator" || state.lastType == "," ? lexical.info + 1 : 0); + else if (type == "form" && firstChar == "{") return lexical.indented; + else if (type == "form") return lexical.indented + indentUnit; + else if (type == "stat") + return lexical.indented + (isContinuedStatement(state, textAfter) ? statementIndent || indentUnit : 0); + else if (lexical.info == "switch" && !closing && parserConfig.doubleIndentSwitch != false) + return lexical.indented + (/^(?:case|default)\b/.test(textAfter) ? indentUnit : 2 * indentUnit); + else if (lexical.align) return lexical.column + (closing ? 0 : 1); + else return lexical.indented + (closing ? 0 : indentUnit); + }, + + electricInput: /^\s*(?:case .*?:|default:|\{|\})$/, + blockCommentStart: jsonMode ? null : "/*", + blockCommentEnd: jsonMode ? null : "*/", + lineComment: jsonMode ? null : "//", + fold: "brace", + closeBrackets: "()[]{}''\"\"``", + + helperType: jsonMode ? "json" : "javascript", + jsonldMode: jsonldMode, + jsonMode: jsonMode + }; +}); + +CodeMirror.registerHelper("wordChars", "javascript", /[\w$]/); + +CodeMirror.defineMIME("text/javascript", "javascript"); +CodeMirror.defineMIME("text/ecmascript", "javascript"); +CodeMirror.defineMIME("application/javascript", "javascript"); +CodeMirror.defineMIME("application/x-javascript", "javascript"); +CodeMirror.defineMIME("application/ecmascript", "javascript"); +CodeMirror.defineMIME("application/json", {name: "javascript", json: true}); +CodeMirror.defineMIME("application/x-json", {name: "javascript", json: true}); +CodeMirror.defineMIME("application/ld+json", {name: "javascript", jsonld: true}); +CodeMirror.defineMIME("text/typescript", { name: "javascript", typescript: true }); +CodeMirror.defineMIME("application/typescript", { name: "javascript", typescript: true }); + +}); diff --git a/web/dep/codemirror/matchbrackets.js b/web/dep/codemirror/matchbrackets.js new file mode 100644 index 00000000..70e1ae18 --- /dev/null +++ b/web/dep/codemirror/matchbrackets.js @@ -0,0 +1,120 @@ +// CodeMirror, copyright (c) by Marijn Haverbeke and others +// Distributed under an MIT license: http://codemirror.net/LICENSE + +(function(mod) { + if (typeof exports == "object" && typeof module == "object") // CommonJS + mod(require("../../lib/codemirror")); + else if (typeof define == "function" && define.amd) // AMD + define(["../../lib/codemirror"], mod); + else // Plain browser env + mod(CodeMirror); +})(function(CodeMirror) { + var ie_lt8 = /MSIE \d/.test(navigator.userAgent) && + (document.documentMode == null || document.documentMode < 8); + + var Pos = CodeMirror.Pos; + + var matching = {"(": ")>", ")": "(<", "[": "]>", "]": "[<", "{": "}>", "}": "{<"}; + + function findMatchingBracket(cm, where, strict, config) { + var line = cm.getLineHandle(where.line), pos = where.ch - 1; + var match = (pos >= 0 && matching[line.text.charAt(pos)]) || matching[line.text.charAt(++pos)]; + if (!match) return null; + var dir = match.charAt(1) == ">" ? 1 : -1; + if (strict && (dir > 0) != (pos == where.ch)) return null; + var style = cm.getTokenTypeAt(Pos(where.line, pos + 1)); + + var found = scanForBracket(cm, Pos(where.line, pos + (dir > 0 ? 1 : 0)), dir, style || null, config); + if (found == null) return null; + return {from: Pos(where.line, pos), to: found && found.pos, + match: found && found.ch == match.charAt(0), forward: dir > 0}; + } + + // bracketRegex is used to specify which type of bracket to scan + // should be a regexp, e.g. /[[\]]/ + // + // Note: If "where" is on an open bracket, then this bracket is ignored. + // + // Returns false when no bracket was found, null when it reached + // maxScanLines and gave up + function scanForBracket(cm, where, dir, style, config) { + var maxScanLen = (config && config.maxScanLineLength) || 10000; + var maxScanLines = (config && config.maxScanLines) || 1000; + + var stack = []; + var re = config && config.bracketRegex ? config.bracketRegex : /[(){}[\]]/; + var lineEnd = dir > 0 ? Math.min(where.line + maxScanLines, cm.lastLine() + 1) + : Math.max(cm.firstLine() - 1, where.line - maxScanLines); + for (var lineNo = where.line; lineNo != lineEnd; lineNo += dir) { + var line = cm.getLine(lineNo); + if (!line) continue; + var pos = dir > 0 ? 0 : line.length - 1, end = dir > 0 ? line.length : -1; + if (line.length > maxScanLen) continue; + if (lineNo == where.line) pos = where.ch - (dir < 0 ? 1 : 0); + for (; pos != end; pos += dir) { + var ch = line.charAt(pos); + if (re.test(ch) && (style === undefined || cm.getTokenTypeAt(Pos(lineNo, pos + 1)) == style)) { + var match = matching[ch]; + if ((match.charAt(1) == ">") == (dir > 0)) stack.push(ch); + else if (!stack.length) return {pos: Pos(lineNo, pos), ch: ch}; + else stack.pop(); + } + } + } + return lineNo - dir == (dir > 0 ? cm.lastLine() : cm.firstLine()) ? false : null; + } + + function matchBrackets(cm, autoclear, config) { + // Disable brace matching in long lines, since it'll cause hugely slow updates + var maxHighlightLen = cm.state.matchBrackets.maxHighlightLineLength || 1000; + var marks = [], ranges = cm.listSelections(); + for (var i = 0; i < ranges.length; i++) { + var match = ranges[i].empty() && findMatchingBracket(cm, ranges[i].head, false, config); + if (match && cm.getLine(match.from.line).length <= maxHighlightLen) { + var style = match.match ? "CodeMirror-matchingbracket" : "CodeMirror-nonmatchingbracket"; + marks.push(cm.markText(match.from, Pos(match.from.line, match.from.ch + 1), {className: style})); + if (match.to && cm.getLine(match.to.line).length <= maxHighlightLen) + marks.push(cm.markText(match.to, Pos(match.to.line, match.to.ch + 1), {className: style})); + } + } + + if (marks.length) { + // Kludge to work around the IE bug from issue #1193, where text + // input stops going to the textare whever this fires. + if (ie_lt8 && cm.state.focused) cm.focus(); + + var clear = function() { + cm.operation(function() { + for (var i = 0; i < marks.length; i++) marks[i].clear(); + }); + }; + if (autoclear) setTimeout(clear, 800); + else return clear; + } + } + + var currentlyHighlighted = null; + function doMatchBrackets(cm) { + cm.operation(function() { + if (currentlyHighlighted) {currentlyHighlighted(); currentlyHighlighted = null;} + currentlyHighlighted = matchBrackets(cm, false, cm.state.matchBrackets); + }); + } + + CodeMirror.defineOption("matchBrackets", false, function(cm, val, old) { + if (old && old != CodeMirror.Init) + cm.off("cursorActivity", doMatchBrackets); + if (val) { + cm.state.matchBrackets = typeof val == "object" ? val : {}; + cm.on("cursorActivity", doMatchBrackets); + } + }); + + CodeMirror.defineExtension("matchBrackets", function() {matchBrackets(this, true);}); + CodeMirror.defineExtension("findMatchingBracket", function(pos, strict, config){ + return findMatchingBracket(this, pos, strict, config); + }); + CodeMirror.defineExtension("scanForBracket", function(pos, dir, style, config){ + return scanForBracket(this, pos, dir, style, config); + }); +}); diff --git a/web/dep/codemirror/xml.js b/web/dep/codemirror/xml.js new file mode 100644 index 00000000..2f3b8f87 --- /dev/null +++ b/web/dep/codemirror/xml.js @@ -0,0 +1,384 @@ +// CodeMirror, copyright (c) by Marijn Haverbeke and others +// Distributed under an MIT license: http://codemirror.net/LICENSE + +(function(mod) { + if (typeof exports == "object" && typeof module == "object") // CommonJS + mod(require("../../lib/codemirror")); + else if (typeof define == "function" && define.amd) // AMD + define(["../../lib/codemirror"], mod); + else // Plain browser env + mod(CodeMirror); +})(function(CodeMirror) { +"use strict"; + +CodeMirror.defineMode("xml", function(config, parserConfig) { + var indentUnit = config.indentUnit; + var multilineTagIndentFactor = parserConfig.multilineTagIndentFactor || 1; + var multilineTagIndentPastTag = parserConfig.multilineTagIndentPastTag; + if (multilineTagIndentPastTag == null) multilineTagIndentPastTag = true; + + var Kludges = parserConfig.htmlMode ? { + autoSelfClosers: {'area': true, 'base': true, 'br': true, 'col': true, 'command': true, + 'embed': true, 'frame': true, 'hr': true, 'img': true, 'input': true, + 'keygen': true, 'link': true, 'meta': true, 'param': true, 'source': true, + 'track': true, 'wbr': true, 'menuitem': true}, + implicitlyClosed: {'dd': true, 'li': true, 'optgroup': true, 'option': true, 'p': true, + 'rp': true, 'rt': true, 'tbody': true, 'td': true, 'tfoot': true, + 'th': true, 'tr': true}, + contextGrabbers: { + 'dd': {'dd': true, 'dt': true}, + 'dt': {'dd': true, 'dt': true}, + 'li': {'li': true}, + 'option': {'option': true, 'optgroup': true}, + 'optgroup': {'optgroup': true}, + 'p': {'address': true, 'article': true, 'aside': true, 'blockquote': true, 'dir': true, + 'div': true, 'dl': true, 'fieldset': true, 'footer': true, 'form': true, + 'h1': true, 'h2': true, 'h3': true, 'h4': true, 'h5': true, 'h6': true, + 'header': true, 'hgroup': true, 'hr': true, 'menu': true, 'nav': true, 'ol': true, + 'p': true, 'pre': true, 'section': true, 'table': true, 'ul': true}, + 'rp': {'rp': true, 'rt': true}, + 'rt': {'rp': true, 'rt': true}, + 'tbody': {'tbody': true, 'tfoot': true}, + 'td': {'td': true, 'th': true}, + 'tfoot': {'tbody': true}, + 'th': {'td': true, 'th': true}, + 'thead': {'tbody': true, 'tfoot': true}, + 'tr': {'tr': true} + }, + doNotIndent: {"pre": true}, + allowUnquoted: true, + allowMissing: true, + caseFold: true + } : { + autoSelfClosers: {}, + implicitlyClosed: {}, + contextGrabbers: {}, + doNotIndent: {}, + allowUnquoted: false, + allowMissing: false, + caseFold: false + }; + var alignCDATA = parserConfig.alignCDATA; + + // Return variables for tokenizers + var type, setStyle; + + function inText(stream, state) { + function chain(parser) { + state.tokenize = parser; + return parser(stream, state); + } + + var ch = stream.next(); + if (ch == "<") { + if (stream.eat("!")) { + if (stream.eat("[")) { + if (stream.match("CDATA[")) return chain(inBlock("atom", "]]>")); + else return null; + } else if (stream.match("--")) { + return chain(inBlock("comment", "-->")); + } else if (stream.match("DOCTYPE", true, true)) { + stream.eatWhile(/[\w\._\-]/); + return chain(doctype(1)); + } else { + return null; + } + } else if (stream.eat("?")) { + stream.eatWhile(/[\w\._\-]/); + state.tokenize = inBlock("meta", "?>"); + return "meta"; + } else { + type = stream.eat("/") ? "closeTag" : "openTag"; + state.tokenize = inTag; + return "tag bracket"; + } + } else if (ch == "&") { + var ok; + if (stream.eat("#")) { + if (stream.eat("x")) { + ok = stream.eatWhile(/[a-fA-F\d]/) && stream.eat(";"); + } else { + ok = stream.eatWhile(/[\d]/) && stream.eat(";"); + } + } else { + ok = stream.eatWhile(/[\w\.\-:]/) && stream.eat(";"); + } + return ok ? "atom" : "error"; + } else { + stream.eatWhile(/[^&<]/); + return null; + } + } + + function inTag(stream, state) { + var ch = stream.next(); + if (ch == ">" || (ch == "/" && stream.eat(">"))) { + state.tokenize = inText; + type = ch == ">" ? "endTag" : "selfcloseTag"; + return "tag bracket"; + } else if (ch == "=") { + type = "equals"; + return null; + } else if (ch == "<") { + state.tokenize = inText; + state.state = baseState; + state.tagName = state.tagStart = null; + var next = state.tokenize(stream, state); + return next ? next + " tag error" : "tag error"; + } else if (/[\'\"]/.test(ch)) { + state.tokenize = inAttribute(ch); + state.stringStartCol = stream.column(); + return state.tokenize(stream, state); + } else { + stream.match(/^[^\s\u00a0=<>\"\']*[^\s\u00a0=<>\"\'\/]/); + return "word"; + } + } + + function inAttribute(quote) { + var closure = function(stream, state) { + while (!stream.eol()) { + if (stream.next() == quote) { + state.tokenize = inTag; + break; + } + } + return "string"; + }; + closure.isInAttribute = true; + return closure; + } + + function inBlock(style, terminator) { + return function(stream, state) { + while (!stream.eol()) { + if (stream.match(terminator)) { + state.tokenize = inText; + break; + } + stream.next(); + } + return style; + }; + } + function doctype(depth) { + return function(stream, state) { + var ch; + while ((ch = stream.next()) != null) { + if (ch == "<") { + state.tokenize = doctype(depth + 1); + return state.tokenize(stream, state); + } else if (ch == ">") { + if (depth == 1) { + state.tokenize = inText; + break; + } else { + state.tokenize = doctype(depth - 1); + return state.tokenize(stream, state); + } + } + } + return "meta"; + }; + } + + function Context(state, tagName, startOfLine) { + this.prev = state.context; + this.tagName = tagName; + this.indent = state.indented; + this.startOfLine = startOfLine; + if (Kludges.doNotIndent.hasOwnProperty(tagName) || (state.context && state.context.noIndent)) + this.noIndent = true; + } + function popContext(state) { + if (state.context) state.context = state.context.prev; + } + function maybePopContext(state, nextTagName) { + var parentTagName; + while (true) { + if (!state.context) { + return; + } + parentTagName = state.context.tagName; + if (!Kludges.contextGrabbers.hasOwnProperty(parentTagName) || + !Kludges.contextGrabbers[parentTagName].hasOwnProperty(nextTagName)) { + return; + } + popContext(state); + } + } + + function baseState(type, stream, state) { + if (type == "openTag") { + state.tagStart = stream.column(); + return tagNameState; + } else if (type == "closeTag") { + return closeTagNameState; + } else { + return baseState; + } + } + function tagNameState(type, stream, state) { + if (type == "word") { + state.tagName = stream.current(); + setStyle = "tag"; + return attrState; + } else { + setStyle = "error"; + return tagNameState; + } + } + function closeTagNameState(type, stream, state) { + if (type == "word") { + var tagName = stream.current(); + if (state.context && state.context.tagName != tagName && + Kludges.implicitlyClosed.hasOwnProperty(state.context.tagName)) + popContext(state); + if (state.context && state.context.tagName == tagName) { + setStyle = "tag"; + return closeState; + } else { + setStyle = "tag error"; + return closeStateErr; + } + } else { + setStyle = "error"; + return closeStateErr; + } + } + + function closeState(type, _stream, state) { + if (type != "endTag") { + setStyle = "error"; + return closeState; + } + popContext(state); + return baseState; + } + function closeStateErr(type, stream, state) { + setStyle = "error"; + return closeState(type, stream, state); + } + + function attrState(type, _stream, state) { + if (type == "word") { + setStyle = "attribute"; + return attrEqState; + } else if (type == "endTag" || type == "selfcloseTag") { + var tagName = state.tagName, tagStart = state.tagStart; + state.tagName = state.tagStart = null; + if (type == "selfcloseTag" || + Kludges.autoSelfClosers.hasOwnProperty(tagName)) { + maybePopContext(state, tagName); + } else { + maybePopContext(state, tagName); + state.context = new Context(state, tagName, tagStart == state.indented); + } + return baseState; + } + setStyle = "error"; + return attrState; + } + function attrEqState(type, stream, state) { + if (type == "equals") return attrValueState; + if (!Kludges.allowMissing) setStyle = "error"; + return attrState(type, stream, state); + } + function attrValueState(type, stream, state) { + if (type == "string") return attrContinuedState; + if (type == "word" && Kludges.allowUnquoted) {setStyle = "string"; return attrState;} + setStyle = "error"; + return attrState(type, stream, state); + } + function attrContinuedState(type, stream, state) { + if (type == "string") return attrContinuedState; + return attrState(type, stream, state); + } + + return { + startState: function() { + return {tokenize: inText, + state: baseState, + indented: 0, + tagName: null, tagStart: null, + context: null}; + }, + + token: function(stream, state) { + if (!state.tagName && stream.sol()) + state.indented = stream.indentation(); + + if (stream.eatSpace()) return null; + type = null; + var style = state.tokenize(stream, state); + if ((style || type) && style != "comment") { + setStyle = null; + state.state = state.state(type || style, stream, state); + if (setStyle) + style = setStyle == "error" ? style + " error" : setStyle; + } + return style; + }, + + indent: function(state, textAfter, fullLine) { + var context = state.context; + // Indent multi-line strings (e.g. css). + if (state.tokenize.isInAttribute) { + if (state.tagStart == state.indented) + return state.stringStartCol + 1; + else + return state.indented + indentUnit; + } + if (context && context.noIndent) return CodeMirror.Pass; + if (state.tokenize != inTag && state.tokenize != inText) + return fullLine ? fullLine.match(/^(\s*)/)[0].length : 0; + // Indent the starts of attribute names. + if (state.tagName) { + if (multilineTagIndentPastTag) + return state.tagStart + state.tagName.length + 2; + else + return state.tagStart + indentUnit * multilineTagIndentFactor; + } + if (alignCDATA && /$/, + blockCommentStart: "", + + configuration: parserConfig.htmlMode ? "html" : "xml", + helperType: parserConfig.htmlMode ? "html" : "xml" + }; +}); + +CodeMirror.defineMIME("text/xml", "xml"); +CodeMirror.defineMIME("application/xml", "xml"); +if (!CodeMirror.mimeModes.hasOwnProperty("text/html")) + CodeMirror.defineMIME("text/html", {name: "xml", htmlMode: true}); + +}); diff --git a/web/dep/jquery.js b/web/dep/jquery.js new file mode 100644 index 00000000..fdd413a6 --- /dev/null +++ b/web/dep/jquery.js @@ -0,0 +1,5 @@ +/*! jQuery v1.11.3 | (c) 2005, 2015 jQuery Foundation, Inc. | jquery.org/license */ +!function(a,b){"object"==typeof module&&"object"==typeof module.exports?module.exports=a.document?b(a,!0):function(a){if(!a.document)throw new Error("jQuery requires a window with a document");return b(a)}:b(a)}("undefined"!=typeof window?window:this,function(a,b){var c=[],d=c.slice,e=c.concat,f=c.push,g=c.indexOf,h={},i=h.toString,j=h.hasOwnProperty,k={},l="1.11.3",m=function(a,b){return new m.fn.init(a,b)},n=/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g,o=/^-ms-/,p=/-([\da-z])/gi,q=function(a,b){return b.toUpperCase()};m.fn=m.prototype={jquery:l,constructor:m,selector:"",length:0,toArray:function(){return d.call(this)},get:function(a){return null!=a?0>a?this[a+this.length]:this[a]:d.call(this)},pushStack:function(a){var b=m.merge(this.constructor(),a);return b.prevObject=this,b.context=this.context,b},each:function(a,b){return m.each(this,a,b)},map:function(a){return this.pushStack(m.map(this,function(b,c){return a.call(b,c,b)}))},slice:function(){return this.pushStack(d.apply(this,arguments))},first:function(){return this.eq(0)},last:function(){return this.eq(-1)},eq:function(a){var b=this.length,c=+a+(0>a?b:0);return this.pushStack(c>=0&&b>c?[this[c]]:[])},end:function(){return this.prevObject||this.constructor(null)},push:f,sort:c.sort,splice:c.splice},m.extend=m.fn.extend=function(){var a,b,c,d,e,f,g=arguments[0]||{},h=1,i=arguments.length,j=!1;for("boolean"==typeof g&&(j=g,g=arguments[h]||{},h++),"object"==typeof g||m.isFunction(g)||(g={}),h===i&&(g=this,h--);i>h;h++)if(null!=(e=arguments[h]))for(d in e)a=g[d],c=e[d],g!==c&&(j&&c&&(m.isPlainObject(c)||(b=m.isArray(c)))?(b?(b=!1,f=a&&m.isArray(a)?a:[]):f=a&&m.isPlainObject(a)?a:{},g[d]=m.extend(j,f,c)):void 0!==c&&(g[d]=c));return g},m.extend({expando:"jQuery"+(l+Math.random()).replace(/\D/g,""),isReady:!0,error:function(a){throw new Error(a)},noop:function(){},isFunction:function(a){return"function"===m.type(a)},isArray:Array.isArray||function(a){return"array"===m.type(a)},isWindow:function(a){return null!=a&&a==a.window},isNumeric:function(a){return!m.isArray(a)&&a-parseFloat(a)+1>=0},isEmptyObject:function(a){var b;for(b in a)return!1;return!0},isPlainObject:function(a){var b;if(!a||"object"!==m.type(a)||a.nodeType||m.isWindow(a))return!1;try{if(a.constructor&&!j.call(a,"constructor")&&!j.call(a.constructor.prototype,"isPrototypeOf"))return!1}catch(c){return!1}if(k.ownLast)for(b in a)return j.call(a,b);for(b in a);return void 0===b||j.call(a,b)},type:function(a){return null==a?a+"":"object"==typeof a||"function"==typeof a?h[i.call(a)]||"object":typeof a},globalEval:function(b){b&&m.trim(b)&&(a.execScript||function(b){a.eval.call(a,b)})(b)},camelCase:function(a){return a.replace(o,"ms-").replace(p,q)},nodeName:function(a,b){return a.nodeName&&a.nodeName.toLowerCase()===b.toLowerCase()},each:function(a,b,c){var d,e=0,f=a.length,g=r(a);if(c){if(g){for(;f>e;e++)if(d=b.apply(a[e],c),d===!1)break}else for(e in a)if(d=b.apply(a[e],c),d===!1)break}else if(g){for(;f>e;e++)if(d=b.call(a[e],e,a[e]),d===!1)break}else for(e in a)if(d=b.call(a[e],e,a[e]),d===!1)break;return a},trim:function(a){return null==a?"":(a+"").replace(n,"")},makeArray:function(a,b){var c=b||[];return null!=a&&(r(Object(a))?m.merge(c,"string"==typeof a?[a]:a):f.call(c,a)),c},inArray:function(a,b,c){var d;if(b){if(g)return g.call(b,a,c);for(d=b.length,c=c?0>c?Math.max(0,d+c):c:0;d>c;c++)if(c in b&&b[c]===a)return c}return-1},merge:function(a,b){var c=+b.length,d=0,e=a.length;while(c>d)a[e++]=b[d++];if(c!==c)while(void 0!==b[d])a[e++]=b[d++];return a.length=e,a},grep:function(a,b,c){for(var d,e=[],f=0,g=a.length,h=!c;g>f;f++)d=!b(a[f],f),d!==h&&e.push(a[f]);return e},map:function(a,b,c){var d,f=0,g=a.length,h=r(a),i=[];if(h)for(;g>f;f++)d=b(a[f],f,c),null!=d&&i.push(d);else for(f in a)d=b(a[f],f,c),null!=d&&i.push(d);return e.apply([],i)},guid:1,proxy:function(a,b){var c,e,f;return"string"==typeof b&&(f=a[b],b=a,a=f),m.isFunction(a)?(c=d.call(arguments,2),e=function(){return a.apply(b||this,c.concat(d.call(arguments)))},e.guid=a.guid=a.guid||m.guid++,e):void 0},now:function(){return+new Date},support:k}),m.each("Boolean Number String Function Array Date RegExp Object Error".split(" "),function(a,b){h["[object "+b+"]"]=b.toLowerCase()});function r(a){var b="length"in a&&a.length,c=m.type(a);return"function"===c||m.isWindow(a)?!1:1===a.nodeType&&b?!0:"array"===c||0===b||"number"==typeof b&&b>0&&b-1 in a}var s=function(a){var b,c,d,e,f,g,h,i,j,k,l,m,n,o,p,q,r,s,t,u="sizzle"+1*new Date,v=a.document,w=0,x=0,y=ha(),z=ha(),A=ha(),B=function(a,b){return a===b&&(l=!0),0},C=1<<31,D={}.hasOwnProperty,E=[],F=E.pop,G=E.push,H=E.push,I=E.slice,J=function(a,b){for(var c=0,d=a.length;d>c;c++)if(a[c]===b)return c;return-1},K="checked|selected|async|autofocus|autoplay|controls|defer|disabled|hidden|ismap|loop|multiple|open|readonly|required|scoped",L="[\\x20\\t\\r\\n\\f]",M="(?:\\\\.|[\\w-]|[^\\x00-\\xa0])+",N=M.replace("w","w#"),O="\\["+L+"*("+M+")(?:"+L+"*([*^$|!~]?=)"+L+"*(?:'((?:\\\\.|[^\\\\'])*)'|\"((?:\\\\.|[^\\\\\"])*)\"|("+N+"))|)"+L+"*\\]",P=":("+M+")(?:\\((('((?:\\\\.|[^\\\\'])*)'|\"((?:\\\\.|[^\\\\\"])*)\")|((?:\\\\.|[^\\\\()[\\]]|"+O+")*)|.*)\\)|)",Q=new RegExp(L+"+","g"),R=new RegExp("^"+L+"+|((?:^|[^\\\\])(?:\\\\.)*)"+L+"+$","g"),S=new RegExp("^"+L+"*,"+L+"*"),T=new RegExp("^"+L+"*([>+~]|"+L+")"+L+"*"),U=new RegExp("="+L+"*([^\\]'\"]*?)"+L+"*\\]","g"),V=new RegExp(P),W=new RegExp("^"+N+"$"),X={ID:new RegExp("^#("+M+")"),CLASS:new RegExp("^\\.("+M+")"),TAG:new RegExp("^("+M.replace("w","w*")+")"),ATTR:new RegExp("^"+O),PSEUDO:new RegExp("^"+P),CHILD:new RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\("+L+"*(even|odd|(([+-]|)(\\d*)n|)"+L+"*(?:([+-]|)"+L+"*(\\d+)|))"+L+"*\\)|)","i"),bool:new RegExp("^(?:"+K+")$","i"),needsContext:new RegExp("^"+L+"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\("+L+"*((?:-\\d)?\\d*)"+L+"*\\)|)(?=[^-]|$)","i")},Y=/^(?:input|select|textarea|button)$/i,Z=/^h\d$/i,$=/^[^{]+\{\s*\[native \w/,_=/^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,aa=/[+~]/,ba=/'|\\/g,ca=new RegExp("\\\\([\\da-f]{1,6}"+L+"?|("+L+")|.)","ig"),da=function(a,b,c){var d="0x"+b-65536;return d!==d||c?b:0>d?String.fromCharCode(d+65536):String.fromCharCode(d>>10|55296,1023&d|56320)},ea=function(){m()};try{H.apply(E=I.call(v.childNodes),v.childNodes),E[v.childNodes.length].nodeType}catch(fa){H={apply:E.length?function(a,b){G.apply(a,I.call(b))}:function(a,b){var c=a.length,d=0;while(a[c++]=b[d++]);a.length=c-1}}}function ga(a,b,d,e){var f,h,j,k,l,o,r,s,w,x;if((b?b.ownerDocument||b:v)!==n&&m(b),b=b||n,d=d||[],k=b.nodeType,"string"!=typeof a||!a||1!==k&&9!==k&&11!==k)return d;if(!e&&p){if(11!==k&&(f=_.exec(a)))if(j=f[1]){if(9===k){if(h=b.getElementById(j),!h||!h.parentNode)return d;if(h.id===j)return d.push(h),d}else if(b.ownerDocument&&(h=b.ownerDocument.getElementById(j))&&t(b,h)&&h.id===j)return d.push(h),d}else{if(f[2])return H.apply(d,b.getElementsByTagName(a)),d;if((j=f[3])&&c.getElementsByClassName)return H.apply(d,b.getElementsByClassName(j)),d}if(c.qsa&&(!q||!q.test(a))){if(s=r=u,w=b,x=1!==k&&a,1===k&&"object"!==b.nodeName.toLowerCase()){o=g(a),(r=b.getAttribute("id"))?s=r.replace(ba,"\\$&"):b.setAttribute("id",s),s="[id='"+s+"'] ",l=o.length;while(l--)o[l]=s+ra(o[l]);w=aa.test(a)&&pa(b.parentNode)||b,x=o.join(",")}if(x)try{return H.apply(d,w.querySelectorAll(x)),d}catch(y){}finally{r||b.removeAttribute("id")}}}return i(a.replace(R,"$1"),b,d,e)}function ha(){var a=[];function b(c,e){return a.push(c+" ")>d.cacheLength&&delete b[a.shift()],b[c+" "]=e}return b}function ia(a){return a[u]=!0,a}function ja(a){var b=n.createElement("div");try{return!!a(b)}catch(c){return!1}finally{b.parentNode&&b.parentNode.removeChild(b),b=null}}function ka(a,b){var c=a.split("|"),e=a.length;while(e--)d.attrHandle[c[e]]=b}function la(a,b){var c=b&&a,d=c&&1===a.nodeType&&1===b.nodeType&&(~b.sourceIndex||C)-(~a.sourceIndex||C);if(d)return d;if(c)while(c=c.nextSibling)if(c===b)return-1;return a?1:-1}function ma(a){return function(b){var c=b.nodeName.toLowerCase();return"input"===c&&b.type===a}}function na(a){return function(b){var c=b.nodeName.toLowerCase();return("input"===c||"button"===c)&&b.type===a}}function oa(a){return ia(function(b){return b=+b,ia(function(c,d){var e,f=a([],c.length,b),g=f.length;while(g--)c[e=f[g]]&&(c[e]=!(d[e]=c[e]))})})}function pa(a){return a&&"undefined"!=typeof a.getElementsByTagName&&a}c=ga.support={},f=ga.isXML=function(a){var b=a&&(a.ownerDocument||a).documentElement;return b?"HTML"!==b.nodeName:!1},m=ga.setDocument=function(a){var b,e,g=a?a.ownerDocument||a:v;return g!==n&&9===g.nodeType&&g.documentElement?(n=g,o=g.documentElement,e=g.defaultView,e&&e!==e.top&&(e.addEventListener?e.addEventListener("unload",ea,!1):e.attachEvent&&e.attachEvent("onunload",ea)),p=!f(g),c.attributes=ja(function(a){return a.className="i",!a.getAttribute("className")}),c.getElementsByTagName=ja(function(a){return a.appendChild(g.createComment("")),!a.getElementsByTagName("*").length}),c.getElementsByClassName=$.test(g.getElementsByClassName),c.getById=ja(function(a){return o.appendChild(a).id=u,!g.getElementsByName||!g.getElementsByName(u).length}),c.getById?(d.find.ID=function(a,b){if("undefined"!=typeof b.getElementById&&p){var c=b.getElementById(a);return c&&c.parentNode?[c]:[]}},d.filter.ID=function(a){var b=a.replace(ca,da);return function(a){return a.getAttribute("id")===b}}):(delete d.find.ID,d.filter.ID=function(a){var b=a.replace(ca,da);return function(a){var c="undefined"!=typeof a.getAttributeNode&&a.getAttributeNode("id");return c&&c.value===b}}),d.find.TAG=c.getElementsByTagName?function(a,b){return"undefined"!=typeof b.getElementsByTagName?b.getElementsByTagName(a):c.qsa?b.querySelectorAll(a):void 0}:function(a,b){var c,d=[],e=0,f=b.getElementsByTagName(a);if("*"===a){while(c=f[e++])1===c.nodeType&&d.push(c);return d}return f},d.find.CLASS=c.getElementsByClassName&&function(a,b){return p?b.getElementsByClassName(a):void 0},r=[],q=[],(c.qsa=$.test(g.querySelectorAll))&&(ja(function(a){o.appendChild(a).innerHTML="",a.querySelectorAll("[msallowcapture^='']").length&&q.push("[*^$]="+L+"*(?:''|\"\")"),a.querySelectorAll("[selected]").length||q.push("\\["+L+"*(?:value|"+K+")"),a.querySelectorAll("[id~="+u+"-]").length||q.push("~="),a.querySelectorAll(":checked").length||q.push(":checked"),a.querySelectorAll("a#"+u+"+*").length||q.push(".#.+[+~]")}),ja(function(a){var b=g.createElement("input");b.setAttribute("type","hidden"),a.appendChild(b).setAttribute("name","D"),a.querySelectorAll("[name=d]").length&&q.push("name"+L+"*[*^$|!~]?="),a.querySelectorAll(":enabled").length||q.push(":enabled",":disabled"),a.querySelectorAll("*,:x"),q.push(",.*:")})),(c.matchesSelector=$.test(s=o.matches||o.webkitMatchesSelector||o.mozMatchesSelector||o.oMatchesSelector||o.msMatchesSelector))&&ja(function(a){c.disconnectedMatch=s.call(a,"div"),s.call(a,"[s!='']:x"),r.push("!=",P)}),q=q.length&&new RegExp(q.join("|")),r=r.length&&new RegExp(r.join("|")),b=$.test(o.compareDocumentPosition),t=b||$.test(o.contains)?function(a,b){var c=9===a.nodeType?a.documentElement:a,d=b&&b.parentNode;return a===d||!(!d||1!==d.nodeType||!(c.contains?c.contains(d):a.compareDocumentPosition&&16&a.compareDocumentPosition(d)))}:function(a,b){if(b)while(b=b.parentNode)if(b===a)return!0;return!1},B=b?function(a,b){if(a===b)return l=!0,0;var d=!a.compareDocumentPosition-!b.compareDocumentPosition;return d?d:(d=(a.ownerDocument||a)===(b.ownerDocument||b)?a.compareDocumentPosition(b):1,1&d||!c.sortDetached&&b.compareDocumentPosition(a)===d?a===g||a.ownerDocument===v&&t(v,a)?-1:b===g||b.ownerDocument===v&&t(v,b)?1:k?J(k,a)-J(k,b):0:4&d?-1:1)}:function(a,b){if(a===b)return l=!0,0;var c,d=0,e=a.parentNode,f=b.parentNode,h=[a],i=[b];if(!e||!f)return a===g?-1:b===g?1:e?-1:f?1:k?J(k,a)-J(k,b):0;if(e===f)return la(a,b);c=a;while(c=c.parentNode)h.unshift(c);c=b;while(c=c.parentNode)i.unshift(c);while(h[d]===i[d])d++;return d?la(h[d],i[d]):h[d]===v?-1:i[d]===v?1:0},g):n},ga.matches=function(a,b){return ga(a,null,null,b)},ga.matchesSelector=function(a,b){if((a.ownerDocument||a)!==n&&m(a),b=b.replace(U,"='$1']"),!(!c.matchesSelector||!p||r&&r.test(b)||q&&q.test(b)))try{var d=s.call(a,b);if(d||c.disconnectedMatch||a.document&&11!==a.document.nodeType)return d}catch(e){}return ga(b,n,null,[a]).length>0},ga.contains=function(a,b){return(a.ownerDocument||a)!==n&&m(a),t(a,b)},ga.attr=function(a,b){(a.ownerDocument||a)!==n&&m(a);var e=d.attrHandle[b.toLowerCase()],f=e&&D.call(d.attrHandle,b.toLowerCase())?e(a,b,!p):void 0;return void 0!==f?f:c.attributes||!p?a.getAttribute(b):(f=a.getAttributeNode(b))&&f.specified?f.value:null},ga.error=function(a){throw new Error("Syntax error, unrecognized expression: "+a)},ga.uniqueSort=function(a){var b,d=[],e=0,f=0;if(l=!c.detectDuplicates,k=!c.sortStable&&a.slice(0),a.sort(B),l){while(b=a[f++])b===a[f]&&(e=d.push(f));while(e--)a.splice(d[e],1)}return k=null,a},e=ga.getText=function(a){var b,c="",d=0,f=a.nodeType;if(f){if(1===f||9===f||11===f){if("string"==typeof a.textContent)return a.textContent;for(a=a.firstChild;a;a=a.nextSibling)c+=e(a)}else if(3===f||4===f)return a.nodeValue}else while(b=a[d++])c+=e(b);return c},d=ga.selectors={cacheLength:50,createPseudo:ia,match:X,attrHandle:{},find:{},relative:{">":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(a){return a[1]=a[1].replace(ca,da),a[3]=(a[3]||a[4]||a[5]||"").replace(ca,da),"~="===a[2]&&(a[3]=" "+a[3]+" "),a.slice(0,4)},CHILD:function(a){return a[1]=a[1].toLowerCase(),"nth"===a[1].slice(0,3)?(a[3]||ga.error(a[0]),a[4]=+(a[4]?a[5]+(a[6]||1):2*("even"===a[3]||"odd"===a[3])),a[5]=+(a[7]+a[8]||"odd"===a[3])):a[3]&&ga.error(a[0]),a},PSEUDO:function(a){var b,c=!a[6]&&a[2];return X.CHILD.test(a[0])?null:(a[3]?a[2]=a[4]||a[5]||"":c&&V.test(c)&&(b=g(c,!0))&&(b=c.indexOf(")",c.length-b)-c.length)&&(a[0]=a[0].slice(0,b),a[2]=c.slice(0,b)),a.slice(0,3))}},filter:{TAG:function(a){var b=a.replace(ca,da).toLowerCase();return"*"===a?function(){return!0}:function(a){return a.nodeName&&a.nodeName.toLowerCase()===b}},CLASS:function(a){var b=y[a+" "];return b||(b=new RegExp("(^|"+L+")"+a+"("+L+"|$)"))&&y(a,function(a){return b.test("string"==typeof a.className&&a.className||"undefined"!=typeof a.getAttribute&&a.getAttribute("class")||"")})},ATTR:function(a,b,c){return function(d){var e=ga.attr(d,a);return null==e?"!="===b:b?(e+="","="===b?e===c:"!="===b?e!==c:"^="===b?c&&0===e.indexOf(c):"*="===b?c&&e.indexOf(c)>-1:"$="===b?c&&e.slice(-c.length)===c:"~="===b?(" "+e.replace(Q," ")+" ").indexOf(c)>-1:"|="===b?e===c||e.slice(0,c.length+1)===c+"-":!1):!0}},CHILD:function(a,b,c,d,e){var f="nth"!==a.slice(0,3),g="last"!==a.slice(-4),h="of-type"===b;return 1===d&&0===e?function(a){return!!a.parentNode}:function(b,c,i){var j,k,l,m,n,o,p=f!==g?"nextSibling":"previousSibling",q=b.parentNode,r=h&&b.nodeName.toLowerCase(),s=!i&&!h;if(q){if(f){while(p){l=b;while(l=l[p])if(h?l.nodeName.toLowerCase()===r:1===l.nodeType)return!1;o=p="only"===a&&!o&&"nextSibling"}return!0}if(o=[g?q.firstChild:q.lastChild],g&&s){k=q[u]||(q[u]={}),j=k[a]||[],n=j[0]===w&&j[1],m=j[0]===w&&j[2],l=n&&q.childNodes[n];while(l=++n&&l&&l[p]||(m=n=0)||o.pop())if(1===l.nodeType&&++m&&l===b){k[a]=[w,n,m];break}}else if(s&&(j=(b[u]||(b[u]={}))[a])&&j[0]===w)m=j[1];else while(l=++n&&l&&l[p]||(m=n=0)||o.pop())if((h?l.nodeName.toLowerCase()===r:1===l.nodeType)&&++m&&(s&&((l[u]||(l[u]={}))[a]=[w,m]),l===b))break;return m-=e,m===d||m%d===0&&m/d>=0}}},PSEUDO:function(a,b){var c,e=d.pseudos[a]||d.setFilters[a.toLowerCase()]||ga.error("unsupported pseudo: "+a);return e[u]?e(b):e.length>1?(c=[a,a,"",b],d.setFilters.hasOwnProperty(a.toLowerCase())?ia(function(a,c){var d,f=e(a,b),g=f.length;while(g--)d=J(a,f[g]),a[d]=!(c[d]=f[g])}):function(a){return e(a,0,c)}):e}},pseudos:{not:ia(function(a){var b=[],c=[],d=h(a.replace(R,"$1"));return d[u]?ia(function(a,b,c,e){var f,g=d(a,null,e,[]),h=a.length;while(h--)(f=g[h])&&(a[h]=!(b[h]=f))}):function(a,e,f){return b[0]=a,d(b,null,f,c),b[0]=null,!c.pop()}}),has:ia(function(a){return function(b){return ga(a,b).length>0}}),contains:ia(function(a){return a=a.replace(ca,da),function(b){return(b.textContent||b.innerText||e(b)).indexOf(a)>-1}}),lang:ia(function(a){return W.test(a||"")||ga.error("unsupported lang: "+a),a=a.replace(ca,da).toLowerCase(),function(b){var c;do if(c=p?b.lang:b.getAttribute("xml:lang")||b.getAttribute("lang"))return c=c.toLowerCase(),c===a||0===c.indexOf(a+"-");while((b=b.parentNode)&&1===b.nodeType);return!1}}),target:function(b){var c=a.location&&a.location.hash;return c&&c.slice(1)===b.id},root:function(a){return a===o},focus:function(a){return a===n.activeElement&&(!n.hasFocus||n.hasFocus())&&!!(a.type||a.href||~a.tabIndex)},enabled:function(a){return a.disabled===!1},disabled:function(a){return a.disabled===!0},checked:function(a){var b=a.nodeName.toLowerCase();return"input"===b&&!!a.checked||"option"===b&&!!a.selected},selected:function(a){return a.parentNode&&a.parentNode.selectedIndex,a.selected===!0},empty:function(a){for(a=a.firstChild;a;a=a.nextSibling)if(a.nodeType<6)return!1;return!0},parent:function(a){return!d.pseudos.empty(a)},header:function(a){return Z.test(a.nodeName)},input:function(a){return Y.test(a.nodeName)},button:function(a){var b=a.nodeName.toLowerCase();return"input"===b&&"button"===a.type||"button"===b},text:function(a){var b;return"input"===a.nodeName.toLowerCase()&&"text"===a.type&&(null==(b=a.getAttribute("type"))||"text"===b.toLowerCase())},first:oa(function(){return[0]}),last:oa(function(a,b){return[b-1]}),eq:oa(function(a,b,c){return[0>c?c+b:c]}),even:oa(function(a,b){for(var c=0;b>c;c+=2)a.push(c);return a}),odd:oa(function(a,b){for(var c=1;b>c;c+=2)a.push(c);return a}),lt:oa(function(a,b,c){for(var d=0>c?c+b:c;--d>=0;)a.push(d);return a}),gt:oa(function(a,b,c){for(var d=0>c?c+b:c;++db;b++)d+=a[b].value;return d}function sa(a,b,c){var d=b.dir,e=c&&"parentNode"===d,f=x++;return b.first?function(b,c,f){while(b=b[d])if(1===b.nodeType||e)return a(b,c,f)}:function(b,c,g){var h,i,j=[w,f];if(g){while(b=b[d])if((1===b.nodeType||e)&&a(b,c,g))return!0}else while(b=b[d])if(1===b.nodeType||e){if(i=b[u]||(b[u]={}),(h=i[d])&&h[0]===w&&h[1]===f)return j[2]=h[2];if(i[d]=j,j[2]=a(b,c,g))return!0}}}function ta(a){return a.length>1?function(b,c,d){var e=a.length;while(e--)if(!a[e](b,c,d))return!1;return!0}:a[0]}function ua(a,b,c){for(var d=0,e=b.length;e>d;d++)ga(a,b[d],c);return c}function va(a,b,c,d,e){for(var f,g=[],h=0,i=a.length,j=null!=b;i>h;h++)(f=a[h])&&(!c||c(f,d,e))&&(g.push(f),j&&b.push(h));return g}function wa(a,b,c,d,e,f){return d&&!d[u]&&(d=wa(d)),e&&!e[u]&&(e=wa(e,f)),ia(function(f,g,h,i){var j,k,l,m=[],n=[],o=g.length,p=f||ua(b||"*",h.nodeType?[h]:h,[]),q=!a||!f&&b?p:va(p,m,a,h,i),r=c?e||(f?a:o||d)?[]:g:q;if(c&&c(q,r,h,i),d){j=va(r,n),d(j,[],h,i),k=j.length;while(k--)(l=j[k])&&(r[n[k]]=!(q[n[k]]=l))}if(f){if(e||a){if(e){j=[],k=r.length;while(k--)(l=r[k])&&j.push(q[k]=l);e(null,r=[],j,i)}k=r.length;while(k--)(l=r[k])&&(j=e?J(f,l):m[k])>-1&&(f[j]=!(g[j]=l))}}else r=va(r===g?r.splice(o,r.length):r),e?e(null,g,r,i):H.apply(g,r)})}function xa(a){for(var b,c,e,f=a.length,g=d.relative[a[0].type],h=g||d.relative[" "],i=g?1:0,k=sa(function(a){return a===b},h,!0),l=sa(function(a){return J(b,a)>-1},h,!0),m=[function(a,c,d){var e=!g&&(d||c!==j)||((b=c).nodeType?k(a,c,d):l(a,c,d));return b=null,e}];f>i;i++)if(c=d.relative[a[i].type])m=[sa(ta(m),c)];else{if(c=d.filter[a[i].type].apply(null,a[i].matches),c[u]){for(e=++i;f>e;e++)if(d.relative[a[e].type])break;return wa(i>1&&ta(m),i>1&&ra(a.slice(0,i-1).concat({value:" "===a[i-2].type?"*":""})).replace(R,"$1"),c,e>i&&xa(a.slice(i,e)),f>e&&xa(a=a.slice(e)),f>e&&ra(a))}m.push(c)}return ta(m)}function ya(a,b){var c=b.length>0,e=a.length>0,f=function(f,g,h,i,k){var l,m,o,p=0,q="0",r=f&&[],s=[],t=j,u=f||e&&d.find.TAG("*",k),v=w+=null==t?1:Math.random()||.1,x=u.length;for(k&&(j=g!==n&&g);q!==x&&null!=(l=u[q]);q++){if(e&&l){m=0;while(o=a[m++])if(o(l,g,h)){i.push(l);break}k&&(w=v)}c&&((l=!o&&l)&&p--,f&&r.push(l))}if(p+=q,c&&q!==p){m=0;while(o=b[m++])o(r,s,g,h);if(f){if(p>0)while(q--)r[q]||s[q]||(s[q]=F.call(i));s=va(s)}H.apply(i,s),k&&!f&&s.length>0&&p+b.length>1&&ga.uniqueSort(i)}return k&&(w=v,j=t),r};return c?ia(f):f}return h=ga.compile=function(a,b){var c,d=[],e=[],f=A[a+" "];if(!f){b||(b=g(a)),c=b.length;while(c--)f=xa(b[c]),f[u]?d.push(f):e.push(f);f=A(a,ya(e,d)),f.selector=a}return f},i=ga.select=function(a,b,e,f){var i,j,k,l,m,n="function"==typeof a&&a,o=!f&&g(a=n.selector||a);if(e=e||[],1===o.length){if(j=o[0]=o[0].slice(0),j.length>2&&"ID"===(k=j[0]).type&&c.getById&&9===b.nodeType&&p&&d.relative[j[1].type]){if(b=(d.find.ID(k.matches[0].replace(ca,da),b)||[])[0],!b)return e;n&&(b=b.parentNode),a=a.slice(j.shift().value.length)}i=X.needsContext.test(a)?0:j.length;while(i--){if(k=j[i],d.relative[l=k.type])break;if((m=d.find[l])&&(f=m(k.matches[0].replace(ca,da),aa.test(j[0].type)&&pa(b.parentNode)||b))){if(j.splice(i,1),a=f.length&&ra(j),!a)return H.apply(e,f),e;break}}}return(n||h(a,o))(f,b,!p,e,aa.test(a)&&pa(b.parentNode)||b),e},c.sortStable=u.split("").sort(B).join("")===u,c.detectDuplicates=!!l,m(),c.sortDetached=ja(function(a){return 1&a.compareDocumentPosition(n.createElement("div"))}),ja(function(a){return a.innerHTML="","#"===a.firstChild.getAttribute("href")})||ka("type|href|height|width",function(a,b,c){return c?void 0:a.getAttribute(b,"type"===b.toLowerCase()?1:2)}),c.attributes&&ja(function(a){return a.innerHTML="",a.firstChild.setAttribute("value",""),""===a.firstChild.getAttribute("value")})||ka("value",function(a,b,c){return c||"input"!==a.nodeName.toLowerCase()?void 0:a.defaultValue}),ja(function(a){return null==a.getAttribute("disabled")})||ka(K,function(a,b,c){var d;return c?void 0:a[b]===!0?b.toLowerCase():(d=a.getAttributeNode(b))&&d.specified?d.value:null}),ga}(a);m.find=s,m.expr=s.selectors,m.expr[":"]=m.expr.pseudos,m.unique=s.uniqueSort,m.text=s.getText,m.isXMLDoc=s.isXML,m.contains=s.contains;var t=m.expr.match.needsContext,u=/^<(\w+)\s*\/?>(?:<\/\1>|)$/,v=/^.[^:#\[\.,]*$/;function w(a,b,c){if(m.isFunction(b))return m.grep(a,function(a,d){return!!b.call(a,d,a)!==c});if(b.nodeType)return m.grep(a,function(a){return a===b!==c});if("string"==typeof b){if(v.test(b))return m.filter(b,a,c);b=m.filter(b,a)}return m.grep(a,function(a){return m.inArray(a,b)>=0!==c})}m.filter=function(a,b,c){var d=b[0];return c&&(a=":not("+a+")"),1===b.length&&1===d.nodeType?m.find.matchesSelector(d,a)?[d]:[]:m.find.matches(a,m.grep(b,function(a){return 1===a.nodeType}))},m.fn.extend({find:function(a){var b,c=[],d=this,e=d.length;if("string"!=typeof a)return this.pushStack(m(a).filter(function(){for(b=0;e>b;b++)if(m.contains(d[b],this))return!0}));for(b=0;e>b;b++)m.find(a,d[b],c);return c=this.pushStack(e>1?m.unique(c):c),c.selector=this.selector?this.selector+" "+a:a,c},filter:function(a){return this.pushStack(w(this,a||[],!1))},not:function(a){return this.pushStack(w(this,a||[],!0))},is:function(a){return!!w(this,"string"==typeof a&&t.test(a)?m(a):a||[],!1).length}});var x,y=a.document,z=/^(?:\s*(<[\w\W]+>)[^>]*|#([\w-]*))$/,A=m.fn.init=function(a,b){var c,d;if(!a)return this;if("string"==typeof a){if(c="<"===a.charAt(0)&&">"===a.charAt(a.length-1)&&a.length>=3?[null,a,null]:z.exec(a),!c||!c[1]&&b)return!b||b.jquery?(b||x).find(a):this.constructor(b).find(a);if(c[1]){if(b=b instanceof m?b[0]:b,m.merge(this,m.parseHTML(c[1],b&&b.nodeType?b.ownerDocument||b:y,!0)),u.test(c[1])&&m.isPlainObject(b))for(c in b)m.isFunction(this[c])?this[c](b[c]):this.attr(c,b[c]);return this}if(d=y.getElementById(c[2]),d&&d.parentNode){if(d.id!==c[2])return x.find(a);this.length=1,this[0]=d}return this.context=y,this.selector=a,this}return a.nodeType?(this.context=this[0]=a,this.length=1,this):m.isFunction(a)?"undefined"!=typeof x.ready?x.ready(a):a(m):(void 0!==a.selector&&(this.selector=a.selector,this.context=a.context),m.makeArray(a,this))};A.prototype=m.fn,x=m(y);var B=/^(?:parents|prev(?:Until|All))/,C={children:!0,contents:!0,next:!0,prev:!0};m.extend({dir:function(a,b,c){var d=[],e=a[b];while(e&&9!==e.nodeType&&(void 0===c||1!==e.nodeType||!m(e).is(c)))1===e.nodeType&&d.push(e),e=e[b];return d},sibling:function(a,b){for(var c=[];a;a=a.nextSibling)1===a.nodeType&&a!==b&&c.push(a);return c}}),m.fn.extend({has:function(a){var b,c=m(a,this),d=c.length;return this.filter(function(){for(b=0;d>b;b++)if(m.contains(this,c[b]))return!0})},closest:function(a,b){for(var c,d=0,e=this.length,f=[],g=t.test(a)||"string"!=typeof a?m(a,b||this.context):0;e>d;d++)for(c=this[d];c&&c!==b;c=c.parentNode)if(c.nodeType<11&&(g?g.index(c)>-1:1===c.nodeType&&m.find.matchesSelector(c,a))){f.push(c);break}return this.pushStack(f.length>1?m.unique(f):f)},index:function(a){return a?"string"==typeof a?m.inArray(this[0],m(a)):m.inArray(a.jquery?a[0]:a,this):this[0]&&this[0].parentNode?this.first().prevAll().length:-1},add:function(a,b){return this.pushStack(m.unique(m.merge(this.get(),m(a,b))))},addBack:function(a){return this.add(null==a?this.prevObject:this.prevObject.filter(a))}});function D(a,b){do a=a[b];while(a&&1!==a.nodeType);return a}m.each({parent:function(a){var b=a.parentNode;return b&&11!==b.nodeType?b:null},parents:function(a){return m.dir(a,"parentNode")},parentsUntil:function(a,b,c){return m.dir(a,"parentNode",c)},next:function(a){return D(a,"nextSibling")},prev:function(a){return D(a,"previousSibling")},nextAll:function(a){return m.dir(a,"nextSibling")},prevAll:function(a){return m.dir(a,"previousSibling")},nextUntil:function(a,b,c){return m.dir(a,"nextSibling",c)},prevUntil:function(a,b,c){return m.dir(a,"previousSibling",c)},siblings:function(a){return m.sibling((a.parentNode||{}).firstChild,a)},children:function(a){return m.sibling(a.firstChild)},contents:function(a){return m.nodeName(a,"iframe")?a.contentDocument||a.contentWindow.document:m.merge([],a.childNodes)}},function(a,b){m.fn[a]=function(c,d){var e=m.map(this,b,c);return"Until"!==a.slice(-5)&&(d=c),d&&"string"==typeof d&&(e=m.filter(d,e)),this.length>1&&(C[a]||(e=m.unique(e)),B.test(a)&&(e=e.reverse())),this.pushStack(e)}});var E=/\S+/g,F={};function G(a){var b=F[a]={};return m.each(a.match(E)||[],function(a,c){b[c]=!0}),b}m.Callbacks=function(a){a="string"==typeof a?F[a]||G(a):m.extend({},a);var b,c,d,e,f,g,h=[],i=!a.once&&[],j=function(l){for(c=a.memory&&l,d=!0,f=g||0,g=0,e=h.length,b=!0;h&&e>f;f++)if(h[f].apply(l[0],l[1])===!1&&a.stopOnFalse){c=!1;break}b=!1,h&&(i?i.length&&j(i.shift()):c?h=[]:k.disable())},k={add:function(){if(h){var d=h.length;!function f(b){m.each(b,function(b,c){var d=m.type(c);"function"===d?a.unique&&k.has(c)||h.push(c):c&&c.length&&"string"!==d&&f(c)})}(arguments),b?e=h.length:c&&(g=d,j(c))}return this},remove:function(){return h&&m.each(arguments,function(a,c){var d;while((d=m.inArray(c,h,d))>-1)h.splice(d,1),b&&(e>=d&&e--,f>=d&&f--)}),this},has:function(a){return a?m.inArray(a,h)>-1:!(!h||!h.length)},empty:function(){return h=[],e=0,this},disable:function(){return h=i=c=void 0,this},disabled:function(){return!h},lock:function(){return i=void 0,c||k.disable(),this},locked:function(){return!i},fireWith:function(a,c){return!h||d&&!i||(c=c||[],c=[a,c.slice?c.slice():c],b?i.push(c):j(c)),this},fire:function(){return k.fireWith(this,arguments),this},fired:function(){return!!d}};return k},m.extend({Deferred:function(a){var b=[["resolve","done",m.Callbacks("once memory"),"resolved"],["reject","fail",m.Callbacks("once memory"),"rejected"],["notify","progress",m.Callbacks("memory")]],c="pending",d={state:function(){return c},always:function(){return e.done(arguments).fail(arguments),this},then:function(){var a=arguments;return m.Deferred(function(c){m.each(b,function(b,f){var g=m.isFunction(a[b])&&a[b];e[f[1]](function(){var a=g&&g.apply(this,arguments);a&&m.isFunction(a.promise)?a.promise().done(c.resolve).fail(c.reject).progress(c.notify):c[f[0]+"With"](this===d?c.promise():this,g?[a]:arguments)})}),a=null}).promise()},promise:function(a){return null!=a?m.extend(a,d):d}},e={};return d.pipe=d.then,m.each(b,function(a,f){var g=f[2],h=f[3];d[f[1]]=g.add,h&&g.add(function(){c=h},b[1^a][2].disable,b[2][2].lock),e[f[0]]=function(){return e[f[0]+"With"](this===e?d:this,arguments),this},e[f[0]+"With"]=g.fireWith}),d.promise(e),a&&a.call(e,e),e},when:function(a){var b=0,c=d.call(arguments),e=c.length,f=1!==e||a&&m.isFunction(a.promise)?e:0,g=1===f?a:m.Deferred(),h=function(a,b,c){return function(e){b[a]=this,c[a]=arguments.length>1?d.call(arguments):e,c===i?g.notifyWith(b,c):--f||g.resolveWith(b,c)}},i,j,k;if(e>1)for(i=new Array(e),j=new Array(e),k=new Array(e);e>b;b++)c[b]&&m.isFunction(c[b].promise)?c[b].promise().done(h(b,k,c)).fail(g.reject).progress(h(b,j,i)):--f;return f||g.resolveWith(k,c),g.promise()}});var H;m.fn.ready=function(a){return m.ready.promise().done(a),this},m.extend({isReady:!1,readyWait:1,holdReady:function(a){a?m.readyWait++:m.ready(!0)},ready:function(a){if(a===!0?!--m.readyWait:!m.isReady){if(!y.body)return setTimeout(m.ready);m.isReady=!0,a!==!0&&--m.readyWait>0||(H.resolveWith(y,[m]),m.fn.triggerHandler&&(m(y).triggerHandler("ready"),m(y).off("ready")))}}});function I(){y.addEventListener?(y.removeEventListener("DOMContentLoaded",J,!1),a.removeEventListener("load",J,!1)):(y.detachEvent("onreadystatechange",J),a.detachEvent("onload",J))}function J(){(y.addEventListener||"load"===event.type||"complete"===y.readyState)&&(I(),m.ready())}m.ready.promise=function(b){if(!H)if(H=m.Deferred(),"complete"===y.readyState)setTimeout(m.ready);else if(y.addEventListener)y.addEventListener("DOMContentLoaded",J,!1),a.addEventListener("load",J,!1);else{y.attachEvent("onreadystatechange",J),a.attachEvent("onload",J);var c=!1;try{c=null==a.frameElement&&y.documentElement}catch(d){}c&&c.doScroll&&!function e(){if(!m.isReady){try{c.doScroll("left")}catch(a){return setTimeout(e,50)}I(),m.ready()}}()}return H.promise(b)};var K="undefined",L;for(L in m(k))break;k.ownLast="0"!==L,k.inlineBlockNeedsLayout=!1,m(function(){var a,b,c,d;c=y.getElementsByTagName("body")[0],c&&c.style&&(b=y.createElement("div"),d=y.createElement("div"),d.style.cssText="position:absolute;border:0;width:0;height:0;top:0;left:-9999px",c.appendChild(d).appendChild(b),typeof b.style.zoom!==K&&(b.style.cssText="display:inline;margin:0;border:0;padding:1px;width:1px;zoom:1",k.inlineBlockNeedsLayout=a=3===b.offsetWidth,a&&(c.style.zoom=1)),c.removeChild(d))}),function(){var a=y.createElement("div");if(null==k.deleteExpando){k.deleteExpando=!0;try{delete a.test}catch(b){k.deleteExpando=!1}}a=null}(),m.acceptData=function(a){var b=m.noData[(a.nodeName+" ").toLowerCase()],c=+a.nodeType||1;return 1!==c&&9!==c?!1:!b||b!==!0&&a.getAttribute("classid")===b};var M=/^(?:\{[\w\W]*\}|\[[\w\W]*\])$/,N=/([A-Z])/g;function O(a,b,c){if(void 0===c&&1===a.nodeType){var d="data-"+b.replace(N,"-$1").toLowerCase();if(c=a.getAttribute(d),"string"==typeof c){try{c="true"===c?!0:"false"===c?!1:"null"===c?null:+c+""===c?+c:M.test(c)?m.parseJSON(c):c}catch(e){}m.data(a,b,c)}else c=void 0}return c}function P(a){var b;for(b in a)if(("data"!==b||!m.isEmptyObject(a[b]))&&"toJSON"!==b)return!1; + +return!0}function Q(a,b,d,e){if(m.acceptData(a)){var f,g,h=m.expando,i=a.nodeType,j=i?m.cache:a,k=i?a[h]:a[h]&&h;if(k&&j[k]&&(e||j[k].data)||void 0!==d||"string"!=typeof b)return k||(k=i?a[h]=c.pop()||m.guid++:h),j[k]||(j[k]=i?{}:{toJSON:m.noop}),("object"==typeof b||"function"==typeof b)&&(e?j[k]=m.extend(j[k],b):j[k].data=m.extend(j[k].data,b)),g=j[k],e||(g.data||(g.data={}),g=g.data),void 0!==d&&(g[m.camelCase(b)]=d),"string"==typeof b?(f=g[b],null==f&&(f=g[m.camelCase(b)])):f=g,f}}function R(a,b,c){if(m.acceptData(a)){var d,e,f=a.nodeType,g=f?m.cache:a,h=f?a[m.expando]:m.expando;if(g[h]){if(b&&(d=c?g[h]:g[h].data)){m.isArray(b)?b=b.concat(m.map(b,m.camelCase)):b in d?b=[b]:(b=m.camelCase(b),b=b in d?[b]:b.split(" ")),e=b.length;while(e--)delete d[b[e]];if(c?!P(d):!m.isEmptyObject(d))return}(c||(delete g[h].data,P(g[h])))&&(f?m.cleanData([a],!0):k.deleteExpando||g!=g.window?delete g[h]:g[h]=null)}}}m.extend({cache:{},noData:{"applet ":!0,"embed ":!0,"object ":"clsid:D27CDB6E-AE6D-11cf-96B8-444553540000"},hasData:function(a){return a=a.nodeType?m.cache[a[m.expando]]:a[m.expando],!!a&&!P(a)},data:function(a,b,c){return Q(a,b,c)},removeData:function(a,b){return R(a,b)},_data:function(a,b,c){return Q(a,b,c,!0)},_removeData:function(a,b){return R(a,b,!0)}}),m.fn.extend({data:function(a,b){var c,d,e,f=this[0],g=f&&f.attributes;if(void 0===a){if(this.length&&(e=m.data(f),1===f.nodeType&&!m._data(f,"parsedAttrs"))){c=g.length;while(c--)g[c]&&(d=g[c].name,0===d.indexOf("data-")&&(d=m.camelCase(d.slice(5)),O(f,d,e[d])));m._data(f,"parsedAttrs",!0)}return e}return"object"==typeof a?this.each(function(){m.data(this,a)}):arguments.length>1?this.each(function(){m.data(this,a,b)}):f?O(f,a,m.data(f,a)):void 0},removeData:function(a){return this.each(function(){m.removeData(this,a)})}}),m.extend({queue:function(a,b,c){var d;return a?(b=(b||"fx")+"queue",d=m._data(a,b),c&&(!d||m.isArray(c)?d=m._data(a,b,m.makeArray(c)):d.push(c)),d||[]):void 0},dequeue:function(a,b){b=b||"fx";var c=m.queue(a,b),d=c.length,e=c.shift(),f=m._queueHooks(a,b),g=function(){m.dequeue(a,b)};"inprogress"===e&&(e=c.shift(),d--),e&&("fx"===b&&c.unshift("inprogress"),delete f.stop,e.call(a,g,f)),!d&&f&&f.empty.fire()},_queueHooks:function(a,b){var c=b+"queueHooks";return m._data(a,c)||m._data(a,c,{empty:m.Callbacks("once memory").add(function(){m._removeData(a,b+"queue"),m._removeData(a,c)})})}}),m.fn.extend({queue:function(a,b){var c=2;return"string"!=typeof a&&(b=a,a="fx",c--),arguments.lengthh;h++)b(a[h],c,g?d:d.call(a[h],h,b(a[h],c)));return e?a:j?b.call(a):i?b(a[0],c):f},W=/^(?:checkbox|radio)$/i;!function(){var a=y.createElement("input"),b=y.createElement("div"),c=y.createDocumentFragment();if(b.innerHTML="
    a",k.leadingWhitespace=3===b.firstChild.nodeType,k.tbody=!b.getElementsByTagName("tbody").length,k.htmlSerialize=!!b.getElementsByTagName("link").length,k.html5Clone="<:nav>"!==y.createElement("nav").cloneNode(!0).outerHTML,a.type="checkbox",a.checked=!0,c.appendChild(a),k.appendChecked=a.checked,b.innerHTML="",k.noCloneChecked=!!b.cloneNode(!0).lastChild.defaultValue,c.appendChild(b),b.innerHTML="",k.checkClone=b.cloneNode(!0).cloneNode(!0).lastChild.checked,k.noCloneEvent=!0,b.attachEvent&&(b.attachEvent("onclick",function(){k.noCloneEvent=!1}),b.cloneNode(!0).click()),null==k.deleteExpando){k.deleteExpando=!0;try{delete b.test}catch(d){k.deleteExpando=!1}}}(),function(){var b,c,d=y.createElement("div");for(b in{submit:!0,change:!0,focusin:!0})c="on"+b,(k[b+"Bubbles"]=c in a)||(d.setAttribute(c,"t"),k[b+"Bubbles"]=d.attributes[c].expando===!1);d=null}();var X=/^(?:input|select|textarea)$/i,Y=/^key/,Z=/^(?:mouse|pointer|contextmenu)|click/,$=/^(?:focusinfocus|focusoutblur)$/,_=/^([^.]*)(?:\.(.+)|)$/;function aa(){return!0}function ba(){return!1}function ca(){try{return y.activeElement}catch(a){}}m.event={global:{},add:function(a,b,c,d,e){var f,g,h,i,j,k,l,n,o,p,q,r=m._data(a);if(r){c.handler&&(i=c,c=i.handler,e=i.selector),c.guid||(c.guid=m.guid++),(g=r.events)||(g=r.events={}),(k=r.handle)||(k=r.handle=function(a){return typeof m===K||a&&m.event.triggered===a.type?void 0:m.event.dispatch.apply(k.elem,arguments)},k.elem=a),b=(b||"").match(E)||[""],h=b.length;while(h--)f=_.exec(b[h])||[],o=q=f[1],p=(f[2]||"").split(".").sort(),o&&(j=m.event.special[o]||{},o=(e?j.delegateType:j.bindType)||o,j=m.event.special[o]||{},l=m.extend({type:o,origType:q,data:d,handler:c,guid:c.guid,selector:e,needsContext:e&&m.expr.match.needsContext.test(e),namespace:p.join(".")},i),(n=g[o])||(n=g[o]=[],n.delegateCount=0,j.setup&&j.setup.call(a,d,p,k)!==!1||(a.addEventListener?a.addEventListener(o,k,!1):a.attachEvent&&a.attachEvent("on"+o,k))),j.add&&(j.add.call(a,l),l.handler.guid||(l.handler.guid=c.guid)),e?n.splice(n.delegateCount++,0,l):n.push(l),m.event.global[o]=!0);a=null}},remove:function(a,b,c,d,e){var f,g,h,i,j,k,l,n,o,p,q,r=m.hasData(a)&&m._data(a);if(r&&(k=r.events)){b=(b||"").match(E)||[""],j=b.length;while(j--)if(h=_.exec(b[j])||[],o=q=h[1],p=(h[2]||"").split(".").sort(),o){l=m.event.special[o]||{},o=(d?l.delegateType:l.bindType)||o,n=k[o]||[],h=h[2]&&new RegExp("(^|\\.)"+p.join("\\.(?:.*\\.|)")+"(\\.|$)"),i=f=n.length;while(f--)g=n[f],!e&&q!==g.origType||c&&c.guid!==g.guid||h&&!h.test(g.namespace)||d&&d!==g.selector&&("**"!==d||!g.selector)||(n.splice(f,1),g.selector&&n.delegateCount--,l.remove&&l.remove.call(a,g));i&&!n.length&&(l.teardown&&l.teardown.call(a,p,r.handle)!==!1||m.removeEvent(a,o,r.handle),delete k[o])}else for(o in k)m.event.remove(a,o+b[j],c,d,!0);m.isEmptyObject(k)&&(delete r.handle,m._removeData(a,"events"))}},trigger:function(b,c,d,e){var f,g,h,i,k,l,n,o=[d||y],p=j.call(b,"type")?b.type:b,q=j.call(b,"namespace")?b.namespace.split("."):[];if(h=l=d=d||y,3!==d.nodeType&&8!==d.nodeType&&!$.test(p+m.event.triggered)&&(p.indexOf(".")>=0&&(q=p.split("."),p=q.shift(),q.sort()),g=p.indexOf(":")<0&&"on"+p,b=b[m.expando]?b:new m.Event(p,"object"==typeof b&&b),b.isTrigger=e?2:3,b.namespace=q.join("."),b.namespace_re=b.namespace?new RegExp("(^|\\.)"+q.join("\\.(?:.*\\.|)")+"(\\.|$)"):null,b.result=void 0,b.target||(b.target=d),c=null==c?[b]:m.makeArray(c,[b]),k=m.event.special[p]||{},e||!k.trigger||k.trigger.apply(d,c)!==!1)){if(!e&&!k.noBubble&&!m.isWindow(d)){for(i=k.delegateType||p,$.test(i+p)||(h=h.parentNode);h;h=h.parentNode)o.push(h),l=h;l===(d.ownerDocument||y)&&o.push(l.defaultView||l.parentWindow||a)}n=0;while((h=o[n++])&&!b.isPropagationStopped())b.type=n>1?i:k.bindType||p,f=(m._data(h,"events")||{})[b.type]&&m._data(h,"handle"),f&&f.apply(h,c),f=g&&h[g],f&&f.apply&&m.acceptData(h)&&(b.result=f.apply(h,c),b.result===!1&&b.preventDefault());if(b.type=p,!e&&!b.isDefaultPrevented()&&(!k._default||k._default.apply(o.pop(),c)===!1)&&m.acceptData(d)&&g&&d[p]&&!m.isWindow(d)){l=d[g],l&&(d[g]=null),m.event.triggered=p;try{d[p]()}catch(r){}m.event.triggered=void 0,l&&(d[g]=l)}return b.result}},dispatch:function(a){a=m.event.fix(a);var b,c,e,f,g,h=[],i=d.call(arguments),j=(m._data(this,"events")||{})[a.type]||[],k=m.event.special[a.type]||{};if(i[0]=a,a.delegateTarget=this,!k.preDispatch||k.preDispatch.call(this,a)!==!1){h=m.event.handlers.call(this,a,j),b=0;while((f=h[b++])&&!a.isPropagationStopped()){a.currentTarget=f.elem,g=0;while((e=f.handlers[g++])&&!a.isImmediatePropagationStopped())(!a.namespace_re||a.namespace_re.test(e.namespace))&&(a.handleObj=e,a.data=e.data,c=((m.event.special[e.origType]||{}).handle||e.handler).apply(f.elem,i),void 0!==c&&(a.result=c)===!1&&(a.preventDefault(),a.stopPropagation()))}return k.postDispatch&&k.postDispatch.call(this,a),a.result}},handlers:function(a,b){var c,d,e,f,g=[],h=b.delegateCount,i=a.target;if(h&&i.nodeType&&(!a.button||"click"!==a.type))for(;i!=this;i=i.parentNode||this)if(1===i.nodeType&&(i.disabled!==!0||"click"!==a.type)){for(e=[],f=0;h>f;f++)d=b[f],c=d.selector+" ",void 0===e[c]&&(e[c]=d.needsContext?m(c,this).index(i)>=0:m.find(c,this,null,[i]).length),e[c]&&e.push(d);e.length&&g.push({elem:i,handlers:e})}return h]","i"),ha=/^\s+/,ia=/<(?!area|br|col|embed|hr|img|input|link|meta|param)(([\w:]+)[^>]*)\/>/gi,ja=/<([\w:]+)/,ka=/\s*$/g,ra={option:[1,""],legend:[1,"
    ","
    "],area:[1,"",""],param:[1,"",""],thead:[1,"","
    "],tr:[2,"","
    "],col:[2,"","
    "],td:[3,"","
    "],_default:k.htmlSerialize?[0,"",""]:[1,"X
    ","
    "]},sa=da(y),ta=sa.appendChild(y.createElement("div"));ra.optgroup=ra.option,ra.tbody=ra.tfoot=ra.colgroup=ra.caption=ra.thead,ra.th=ra.td;function ua(a,b){var c,d,e=0,f=typeof a.getElementsByTagName!==K?a.getElementsByTagName(b||"*"):typeof a.querySelectorAll!==K?a.querySelectorAll(b||"*"):void 0;if(!f)for(f=[],c=a.childNodes||a;null!=(d=c[e]);e++)!b||m.nodeName(d,b)?f.push(d):m.merge(f,ua(d,b));return void 0===b||b&&m.nodeName(a,b)?m.merge([a],f):f}function va(a){W.test(a.type)&&(a.defaultChecked=a.checked)}function wa(a,b){return m.nodeName(a,"table")&&m.nodeName(11!==b.nodeType?b:b.firstChild,"tr")?a.getElementsByTagName("tbody")[0]||a.appendChild(a.ownerDocument.createElement("tbody")):a}function xa(a){return a.type=(null!==m.find.attr(a,"type"))+"/"+a.type,a}function ya(a){var b=pa.exec(a.type);return b?a.type=b[1]:a.removeAttribute("type"),a}function za(a,b){for(var c,d=0;null!=(c=a[d]);d++)m._data(c,"globalEval",!b||m._data(b[d],"globalEval"))}function Aa(a,b){if(1===b.nodeType&&m.hasData(a)){var c,d,e,f=m._data(a),g=m._data(b,f),h=f.events;if(h){delete g.handle,g.events={};for(c in h)for(d=0,e=h[c].length;e>d;d++)m.event.add(b,c,h[c][d])}g.data&&(g.data=m.extend({},g.data))}}function Ba(a,b){var c,d,e;if(1===b.nodeType){if(c=b.nodeName.toLowerCase(),!k.noCloneEvent&&b[m.expando]){e=m._data(b);for(d in e.events)m.removeEvent(b,d,e.handle);b.removeAttribute(m.expando)}"script"===c&&b.text!==a.text?(xa(b).text=a.text,ya(b)):"object"===c?(b.parentNode&&(b.outerHTML=a.outerHTML),k.html5Clone&&a.innerHTML&&!m.trim(b.innerHTML)&&(b.innerHTML=a.innerHTML)):"input"===c&&W.test(a.type)?(b.defaultChecked=b.checked=a.checked,b.value!==a.value&&(b.value=a.value)):"option"===c?b.defaultSelected=b.selected=a.defaultSelected:("input"===c||"textarea"===c)&&(b.defaultValue=a.defaultValue)}}m.extend({clone:function(a,b,c){var d,e,f,g,h,i=m.contains(a.ownerDocument,a);if(k.html5Clone||m.isXMLDoc(a)||!ga.test("<"+a.nodeName+">")?f=a.cloneNode(!0):(ta.innerHTML=a.outerHTML,ta.removeChild(f=ta.firstChild)),!(k.noCloneEvent&&k.noCloneChecked||1!==a.nodeType&&11!==a.nodeType||m.isXMLDoc(a)))for(d=ua(f),h=ua(a),g=0;null!=(e=h[g]);++g)d[g]&&Ba(e,d[g]);if(b)if(c)for(h=h||ua(a),d=d||ua(f),g=0;null!=(e=h[g]);g++)Aa(e,d[g]);else Aa(a,f);return d=ua(f,"script"),d.length>0&&za(d,!i&&ua(a,"script")),d=h=e=null,f},buildFragment:function(a,b,c,d){for(var e,f,g,h,i,j,l,n=a.length,o=da(b),p=[],q=0;n>q;q++)if(f=a[q],f||0===f)if("object"===m.type(f))m.merge(p,f.nodeType?[f]:f);else if(la.test(f)){h=h||o.appendChild(b.createElement("div")),i=(ja.exec(f)||["",""])[1].toLowerCase(),l=ra[i]||ra._default,h.innerHTML=l[1]+f.replace(ia,"<$1>")+l[2],e=l[0];while(e--)h=h.lastChild;if(!k.leadingWhitespace&&ha.test(f)&&p.push(b.createTextNode(ha.exec(f)[0])),!k.tbody){f="table"!==i||ka.test(f)?""!==l[1]||ka.test(f)?0:h:h.firstChild,e=f&&f.childNodes.length;while(e--)m.nodeName(j=f.childNodes[e],"tbody")&&!j.childNodes.length&&f.removeChild(j)}m.merge(p,h.childNodes),h.textContent="";while(h.firstChild)h.removeChild(h.firstChild);h=o.lastChild}else p.push(b.createTextNode(f));h&&o.removeChild(h),k.appendChecked||m.grep(ua(p,"input"),va),q=0;while(f=p[q++])if((!d||-1===m.inArray(f,d))&&(g=m.contains(f.ownerDocument,f),h=ua(o.appendChild(f),"script"),g&&za(h),c)){e=0;while(f=h[e++])oa.test(f.type||"")&&c.push(f)}return h=null,o},cleanData:function(a,b){for(var d,e,f,g,h=0,i=m.expando,j=m.cache,l=k.deleteExpando,n=m.event.special;null!=(d=a[h]);h++)if((b||m.acceptData(d))&&(f=d[i],g=f&&j[f])){if(g.events)for(e in g.events)n[e]?m.event.remove(d,e):m.removeEvent(d,e,g.handle);j[f]&&(delete j[f],l?delete d[i]:typeof d.removeAttribute!==K?d.removeAttribute(i):d[i]=null,c.push(f))}}}),m.fn.extend({text:function(a){return V(this,function(a){return void 0===a?m.text(this):this.empty().append((this[0]&&this[0].ownerDocument||y).createTextNode(a))},null,a,arguments.length)},append:function(){return this.domManip(arguments,function(a){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var b=wa(this,a);b.appendChild(a)}})},prepend:function(){return this.domManip(arguments,function(a){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var b=wa(this,a);b.insertBefore(a,b.firstChild)}})},before:function(){return this.domManip(arguments,function(a){this.parentNode&&this.parentNode.insertBefore(a,this)})},after:function(){return this.domManip(arguments,function(a){this.parentNode&&this.parentNode.insertBefore(a,this.nextSibling)})},remove:function(a,b){for(var c,d=a?m.filter(a,this):this,e=0;null!=(c=d[e]);e++)b||1!==c.nodeType||m.cleanData(ua(c)),c.parentNode&&(b&&m.contains(c.ownerDocument,c)&&za(ua(c,"script")),c.parentNode.removeChild(c));return this},empty:function(){for(var a,b=0;null!=(a=this[b]);b++){1===a.nodeType&&m.cleanData(ua(a,!1));while(a.firstChild)a.removeChild(a.firstChild);a.options&&m.nodeName(a,"select")&&(a.options.length=0)}return this},clone:function(a,b){return a=null==a?!1:a,b=null==b?a:b,this.map(function(){return m.clone(this,a,b)})},html:function(a){return V(this,function(a){var b=this[0]||{},c=0,d=this.length;if(void 0===a)return 1===b.nodeType?b.innerHTML.replace(fa,""):void 0;if(!("string"!=typeof a||ma.test(a)||!k.htmlSerialize&&ga.test(a)||!k.leadingWhitespace&&ha.test(a)||ra[(ja.exec(a)||["",""])[1].toLowerCase()])){a=a.replace(ia,"<$1>");try{for(;d>c;c++)b=this[c]||{},1===b.nodeType&&(m.cleanData(ua(b,!1)),b.innerHTML=a);b=0}catch(e){}}b&&this.empty().append(a)},null,a,arguments.length)},replaceWith:function(){var a=arguments[0];return this.domManip(arguments,function(b){a=this.parentNode,m.cleanData(ua(this)),a&&a.replaceChild(b,this)}),a&&(a.length||a.nodeType)?this:this.remove()},detach:function(a){return this.remove(a,!0)},domManip:function(a,b){a=e.apply([],a);var c,d,f,g,h,i,j=0,l=this.length,n=this,o=l-1,p=a[0],q=m.isFunction(p);if(q||l>1&&"string"==typeof p&&!k.checkClone&&na.test(p))return this.each(function(c){var d=n.eq(c);q&&(a[0]=p.call(this,c,d.html())),d.domManip(a,b)});if(l&&(i=m.buildFragment(a,this[0].ownerDocument,!1,this),c=i.firstChild,1===i.childNodes.length&&(i=c),c)){for(g=m.map(ua(i,"script"),xa),f=g.length;l>j;j++)d=i,j!==o&&(d=m.clone(d,!0,!0),f&&m.merge(g,ua(d,"script"))),b.call(this[j],d,j);if(f)for(h=g[g.length-1].ownerDocument,m.map(g,ya),j=0;f>j;j++)d=g[j],oa.test(d.type||"")&&!m._data(d,"globalEval")&&m.contains(h,d)&&(d.src?m._evalUrl&&m._evalUrl(d.src):m.globalEval((d.text||d.textContent||d.innerHTML||"").replace(qa,"")));i=c=null}return this}}),m.each({appendTo:"append",prependTo:"prepend",insertBefore:"before",insertAfter:"after",replaceAll:"replaceWith"},function(a,b){m.fn[a]=function(a){for(var c,d=0,e=[],g=m(a),h=g.length-1;h>=d;d++)c=d===h?this:this.clone(!0),m(g[d])[b](c),f.apply(e,c.get());return this.pushStack(e)}});var Ca,Da={};function Ea(b,c){var d,e=m(c.createElement(b)).appendTo(c.body),f=a.getDefaultComputedStyle&&(d=a.getDefaultComputedStyle(e[0]))?d.display:m.css(e[0],"display");return e.detach(),f}function Fa(a){var b=y,c=Da[a];return c||(c=Ea(a,b),"none"!==c&&c||(Ca=(Ca||m(" + + + + + + + + + \ No newline at end of file diff --git a/web/img/Thumbs.db b/web/img/Thumbs.db new file mode 100644 index 00000000..2699a75f Binary files /dev/null and b/web/img/Thumbs.db differ diff --git a/web/img/knockout.jpg b/web/img/knockout.jpg new file mode 100644 index 00000000..252400d2 Binary files /dev/null and b/web/img/knockout.jpg differ diff --git a/web/intro.html b/web/intro.html new file mode 100644 index 00000000..e25f706f --- /dev/null +++ b/web/intro.html @@ -0,0 +1,65 @@ +
    +
    +
    GAME OVER
    + + ? +
    +
    + +
    +

    "You don't remember what happened, do you?"

    +

    Violence is a jarring thing. Imagine waking up and having no memory. No notion of who you are, where you came from, or why you were chosen.

    +

    Your identity, erased. This is your story, and you are about to discover how.

    +

    Hi, I'm Plublious. Let me be your guide.

    +

    "You've got a dreadful case of amnesia. I guess you're just gonna have to trust me."

    +

    "So hey, what's your name?"

    +
    or .
    +

    "You don't know? Well then, let's call you Bob."

    +

    "Hi, !"

    +
    +

    "What is this all about?" asks. +

    or .
    +
    +

    "Distributed systems."

    +

    "What?" gawks. +

    "Distributed systems."

    +
    +
    +
    + + + + \ No newline at end of file diff --git a/web/notes-keys.txt b/web/notes-keys.txt new file mode 100644 index 00000000..ff53e997 --- /dev/null +++ b/web/notes-keys.txt @@ -0,0 +1,127 @@ +Alice comes online and does + +`var todo = gun.get('todo')` + +However she is the first peer, objectively, to be around. + +Therefore, very quickly her query returns empty. So when she + +`todo.put({first: "buy groceries"}).key('todo')` + +the put has to generate a soul and GUN is instructed to associate 'todo' with that soul. + +A few hours later, Bob comes online and does + +`var todo = gun.get('todo')` + +and thankfully he was connected to Alice so he gets her soul and node. So when he + +`todo.put({last: "sell leftovers"}).key('todo')` + +this was the expected and intended result, producing the following graph: + +```{ + 'ASDF': { + _: {'#': 'ASDF', '>': { + first: 1, + last: 2 + }}, + first: "buy groceries", + last: "sell leftovers" + } +}``` + +For purposes of clarity, we are using states as if they were linearizable (this is not actually the case though). + +Then Carl comes online and tries to + +`var todo = gun.get('todo')` + +But since he is not connected to Alice or Bob, gets an empty result. + +Carl does nothing with it, meaning no mutation, no soul, no generation. + +Then Dave comes online and does the same as everyone else: + +`var todo = gun.get('todo')` + +But Dave is only connected to Carl as a peer, therefore his get is empty. Like Alice, he then + +`todo.put({remember: "eat food!", last: "no leftovers!"}).key('todo')` + +Which unfortunately causes a new soul to be generated. Meanwhile, everybody then does the following: + +`todo.on(function(val){ console.log(val) })` + +Alice and Bob immediately get: + +```{ + _: {'#': 'ASDF', '>': { + first: 1, + last: 2 + }}, + first: "buy groceries", + last: "sell leftovers" +}``` + +But Carl and Dave immediately get: + +```{ + _: {'#': 'FDSA', '>': { + remember: 3, + last: 3 + }}, + remember: "eat food!", + last: "no leftovers!" +}``` + +However, a few hours later everybody gets connected. This is the graph everyone then has: + +```{ + 'ASDF': { + _: {'#': 'ASDF', '>': { + first: 1, + last: 2 + }}, + first: "buy groceries", + last: "sell leftovers" + }, + 'FDSA': { + _: {'#': 'FDSA', '>': { + remember: 3, + last: 3 + }}, + remember: "eat food!", + last: "no leftovers!" + } +}``` + +But their `on` function triggers again, with the following `val` locally for everyone: + +```{ + first: "buy groceries", + remember: "eat food!", + last: "no leftovers!" +}``` + +GUN merges all the nodes with matching keys into a temporary in-memory object. + +This way it is safe to get empty results and still put data into it, + +Everyone will see a unified view of the data when they do get connected, as intended. + +This solves the null, singular, and plural problems for get all together. + +However, if we intentionally do not want to see a potentially conflicting unified view, any peer can: + +`var todos = gun.all('todo')` + +And have the discrete data via: + +`todos.map(function(todo, soul){ console.log(todo) })` + +Which would currently get called twice, with the distinct nodes of 'ASDF' and 'FDSA'. + +The only thing that this does not address is how write operations (put/key) would effect `get` nodes. + +However, I feel like finding the answer to that question will be much easier than trying to solve `get` in any other way. \ No newline at end of file diff --git a/web/notes.txt b/web/notes.txt index f4014f36..41d02b2e 100644 --- a/web/notes.txt +++ b/web/notes.txt @@ -1,7 +1,7 @@ HOOKS: - TRANSPORT: - Has to basically re-implement set/load/key over some transport. - REMEMBER that you have to subscribe on all set/load/key (especially key). + Has to basically re-implement set/get/key/all over some transport. + REMEMBER that you have to subscribe on all set/get/key/all (especially key). Notes: @@ -60,6 +60,16 @@ These are reserved for gun, to include meta-data on the data itself. . represents walking the path through a node's scope _ represents non-human data in the graph that the amnesia machine uses to process stuff > and < are dedicated to comparing timestamp states, timelessly to avoid malicious abuse (this should always come last, because requests will be rejected without it) +~ is who +* relates to key prefixes +*> lexical constraint +*< lexical constraint +@ is where it should be delivered to. +@> is where it has been or will be rebroadcasted through. +% is for byte constraints. +: is time +$ is value +' is for escaping characters contained within ' { who: {} @@ -85,9 +95,41 @@ How do we do memory management / clean up then of old dead bad data? Init You initialize with a node(s) you want to connect to. -Evolution -var node = gun.create('person/joe', {}); // this creates a 'new' node with an identifier to this node -node('name', 'joe'); // changes properties -node('age', 24); -node('eyes', {}); // this internally creates a new node within the same subset as joe, -node('eyes.color', 'blue'); \ No newline at end of file +DISK +We might want to compact everything into a journal, with this type of format: +#'ASDF'.'hello'>12743921$"world" +Over the wire though it would include more information, like the ordering of where it has/will-be traversed through. +@>'random','gunjs.herokuapp.com','127.0.0.1:8080':7894657#'ASDF'.'hello'>12743921$"world" +What about ACKs? +@'random':7894657#'ASDF'.'hello'>12743921 + +There is a limited amount of space on a machine, where we assume it is considered ephemeral as a user viewing device. +Such machines are the default requiring no configuration, machines that are permanent nodes can configure caps. +For instance, the browser would be a looping ephemeral device, where the 5mb localStorage gets recycled. +The localhost server of that device would be permanent and not loop, but halt when storage runs out. +By thus virtue of this, clients should be able to throttle (backpressure) their data intake. +The size of primitive values is fundamentally outside our control and left to the developer or multiparting. +If they decide to have a string that exceeds memory or local disk then their app will always fail regardless of multiparting. +However it is our duty to make sure that the rest of the structured data is streamable, including JSON. +Therefore networks and snapshotting should throttle to a size limit specified by the requesting agent. +Resuming these requests will be based soley on a lexical cursor included in the delivery, to avoid remembering state. +The asynchronicity of these lexical cursor could pose potential concurrency issues, and have to be considered ad hoc. +That is to say, if you are dealing with a large data set, you should always subscribe to updates it and never merely "get" it. +For the edge case of the singleton "get", the lexical cursor will progress until it receives the same or no cursor back from the server and terminate. + +total over a range of time: {"alice", "bob", "carl", "david", "ed", "fred", "gary", "harry", "ian", "jake", "kim"} +client requests "users" at limit of 30 characters, server replies with (rounded down) {"alice", "bob", "carl"}. +after a while the client has processed this and goes for the next step, requesting /users?*>=carl&%=30 and {"david", "ed", "fred", "gary"} is returned. +then again /user?*>gary&%=30 with {"harry", "ian", "jake", "kim"} response. +then again /user?*>kim&%=30 with {} response. No subsequent cursor is possible, end. +Note: I do not know yet if the lexical cursor should be open-closed boundary, open-open, closed-closed, or closed-open - decide later. + + + + + + + + + + diff --git a/web/think.html b/web/think.html new file mode 100644 index 00000000..5fdaf5bd --- /dev/null +++ b/web/think.html @@ -0,0 +1,406 @@ + + + + + + + + + +

    Before we can start building anything interesting, we should have a way to jot down our thoughts. Therefore the first thing we will build is a tool to keep track of what needs to be done. The infamous To-Do app, allowing us to keep temporary notes.

    + +

    So what are the requirements? The ability to add a note, read our notes, and to clear them off. We will also need a space to keep these notes in, and a web page to access them through. Let's start with the page! You can edit the code below, which will update the live preview.

    + +
    ... loading editor and preview ... + +
    + + + + +
    +

    What does this do? HTML is how we code the layout of a web page.

    +
      +
    • We first must wrap all our code in an open and closing html tag so our computer knows it is a web page.
    • +
    • The body tag tells it to display the contents enclosed within.
    • +
    • h1 is one of many semantic tags for declaring a title, others include h2, h3 and so on of different sizes.
    • +
    • A form is a container for getting information from a user.
    • +
    • Forms have inputs which let the user type data in, it is a self-closing tag.
    • +
    • The button can be pressed, causing an action that we code to happen.
    • +
    • ul is an unordered list which we will display our thoughts inside of.
    • +
    +

    Now, try changing the h1 text in the editor from "Title" to the name of our app, "Thoughts".

    + + + +
    + +
    +

    HTML controls the layout, but how do we control what happens when a user presses the 'add' button? This is done with javascript. But using raw javascript quickly becomes verbose, so to keep things concise we will use a popular tool called jQuery. We also need a tool to store data, so we will include GUN as well.

    +

    Insert the following between the ending ul tag and the ending body tag, replacing the comment line:

    + +
      +
    • The script tag tells the browser to use some javascript code, and src is where to load it from.
    • +
    • We can then test to see if our code worked with an alert message, which pops up and forces you to press ok.
    • +
    • In javascript, we denote text by wrapping it inside quotation marks, double "" or single ''.
    • +
    • We instruct the computer to notify us with that text by calling the alert function using parenthesis ().
    • +
    • A function is just a fancy word for a reusable piece of code that does something when we call its name, such as alert. +
    • A semicolon ; marks the end of a javascript sentence in the same way a period marks the end of a sentence.
    • +
    + + + +
    + +
    +

    Wonderful! You should have gotten the alert message, this means writing code works! Let's replace the alert line entirely with code that responds to user input.

    + +

    What's going on here?

    +
      +
    • jQuery is a function like alert, its name is $ which can be called with parenthesis ().
    • +
    • Calling $ with 'form' as the input gives us a reference to the corresponding HTML form tag.
    • +
    • We then call on with two inputs. First the text name of an event we want to react to, and then a function we create. +
        +
      • Events are predefined ways we can interact with a user, such as 'mousemove' or a 'keypress'.
      • +
      • We use 'submit' because it responds to both a button 'click' and hitting enter on a form.
      • +
      • Our function will get called with the event every time the user does that action, allowing us to react to their input.
      • +
      +
    • The default behavior of a form is to cause the browser to change pages which is annoying, we prevent that by calling preventDefault on the event.
    • +
    • Finally, calling $ with 'input' will reference the HTML input tag which we then call val on, giving us the text the user typed in.
    • +
    + + + +
    + +
    +

    Now that users can jot down their thoughts, we need a place to save them. Let's start using GUN for just that.

    + +
      +
    • The variable keyword tells javascript that we want to create a reference named gun that we can reuse.
    • +
    • We call Gun to start the database, which only needs to be done once per page load.
    • +
    • Now we want to open up a reference to some data, so we call get with the name of the data we want.
    • +
    • However, no data has been saved to 'thoughts' yet! Let's fix that in the next step by using gun.
    • +
    + + +
    + +
    +

    Replace the alert line in the submit function with the following:

    + +
      +
    • We're telling gun to add the value of the input as an item in a set of thoughts.
    • +
    • Then we also want the input's value to become empty text, so we can add new thoughts later.
    • +
    + + +
    + +
    +

    Fantastic! Now that we can successfully store data, we want show the data! Replace the comment line in the editor with the following:

    + +
      +
    • In the same way $'s on reacts to events, so does gun. It responds to any update on 'thoughts'.
    • +
    • map calls the function you input into it for each item in the set, one at a time.
    • +
    • We get the thought value itself and a unique identifier for the item in the set.
    • +
    • This next line looks scary, but read it like this, "make variable li equal to X or Y". +
        +
      • The X part asks $ to find the id in the HTML and get it.
      • +
      • In javascript, || means 'or', such that javascript will use X if it exist or it will use Y.
      • +
      • The Y part asks $ to create a new <li> HTML tag, set its id attribute to our id and append it to the end of the HTML ul list.
      • +
    • +
    • Finally, the javascript if statement either asks $ to make thought be the text of the li if thought exists, else hide the li from being displayed.
    • +
    • Altogether it says "Create or reuse the HTML list item and make sure it is in the HTML list, then update the text or hide the item if there is no text".
    • +
    + + +
    + +
    +

    Finally we want to be able to clear off our thoughts when we are done with them. The interface for this could be done in many different ways, but for simplicity we will use a double tap as the gesture to clear it off. Replace the comment line in the editor with this code.

    + +
      +
    • In order to react to any 'dblclick' event rather than a specific one, we call on on the page's 'body' as a whole.
    • +
    • But we want to filter the events to ones that happened only on any 'li' tag. Fortunately, we can call on with an optional second input of 'li' which does just that.
    • +
    • Inside a function we get a special this keyword in javascript, which $ uses as a reference to the original HTML tag that caused the event.
    • +
    • Calling path tells gun to filter its data to just the id of the thought we want to clear off.
    • +
    • Then calling put on that tells gun to update that thought to null, so we no longer have the thought.
    • +
    • And whenever an update happens, gun's on function from the previous step gets called again, which then hides the corresponding HTML list item. +
    + + +
    + +
    +

    Congratulations! You are all done, you have built your first GUN app!

    +

    In the next tutorial we will use GUN to synchronize data in realtime across multiple devices. We'll start by copying the app we made here and modifying it to become a chat app.

    + Coming Soon! +
    + + + + \ No newline at end of file diff --git a/web/wire.txt b/web/wire.txt new file mode 100644 index 00000000..fd1a9fee --- /dev/null +++ b/web/wire.txt @@ -0,0 +1,65 @@ +WIRE PROTOCOL + +save/set/create/put/post/delete/update/change/mutate + +get/open/load/call/location/address + +name/reference/key/index/point + + +/gun {data: 'yay', #: "soul"} + +/gun/key {#: 'soul'} + +/gun/key + +/gun/key?*=/&*<=a&*>=c + + +Reads are a GET and do not contain a body. +Writes have a body. + +Reads may call the callback multiple times, and will end with an empty object. + +Reads are either a singular pathname or pound query to get a key or soul's individual node. +Or a read is to retrieve information on many key's and their corresponding soul. +Query formats are allowed as: +? + * = / + star means the peer should reply with all the KEYS it has up to the character in the value of the query. + Ex. "/users/?*=/" would return {"users/marknadal": {"#": "ASDF"}, "users/ambernadal": {"#": "FDSA"}} + If there is no up to character, then all subkeys would be returned as well. + The peer does not have to reply with all of its keys, however if there are more keys for the receiving peer, it should give it a lexical cursor. (how?) + Ordering is not guaranteed, however if there is ordering, it should be lexical. Therefore things like limit or skip do not necessarily apply. + *> = a + *< = c + star greater than and star less than are lexical constraints on returning all peers KEYS. + Ex. "/users/?*=/&*>=a&*<=c" asks the peer to return users that start and end between 'a' and 'c', + thus {"users/alice": {"#": "DSAF"}, "users/bob": {"#": "DAFS"}, "user/carl": {"#": "SAFD"}} + # = ASDF + pound means the peer should reply with the node that has this exact soul. There should be no key prefixed path on this type of request. A special case of "#=*" indicates to literally dump the entire graph, as is, to the client. Lexical carets are okay. + % = 30 + percent means byte constraint requested by the peer asking for data. + > = 1426007247399 + < = 1426007248941 + greater than and less than are time/state constraints, allowing a peer to request historical data or future data. + the peer processing the request ought to reply with such state data if it has it, but has no requirement to keep such data (but is encouraged to do so). + +Using query constraints is generally not advised, as it is better to do all computation at the point of data, or have all data at the point of computation, not inbetween. +This protocol should work over HTTP, or the JSONP fallback specification, and emulated over WS or WebRTC. + +On a separate note, it might be advantageous to chunk node objects into pieces as well, in case they grow too big or especially contain too many field/value pairs. If this was built into the behavior, to handle better streaming and lower memory use cases, it would also become ideal for groups of data without any extra logic or behavior. + +There are three types of grouped related data: + +1. Dependent Causality (videos, audio, etc.) +2. Relative Ordering (age, height, users, etc.) +3. Independent + +Obviously independent data is fairly uninteresting, as the HAM can handle convergence just on states alone. Relative ordering requires only application specific logic to sort data for the view, or computing averages and such. They can always be streamed in efficiently by creating indices on the desired properties and then doing lexical loading. Any conflict in ordering on one property can be handled by cascading into other properties, like from sort order to then creation date. By this means, all data should be explicitly recorded, not implicit. + +Dependent Causality is the most interesting, and unfortunately not conducive towards efficient means of streaming. This means the data must be sorted in a particular direction, and even if you receive a "later" chunk, you should not reveal it until all earlier chunks have been digested. Which might means you have to discard it from memory if space runs out, and then pull it back in again at the right time. Let's look at some differences here with some concrete examples. + +If we have 4 people in the back room, and we want to sort them by height as they come on to stage, then order in which they come out from behind does not matter. Say their heights are 4, 5, 6, and 7 feet tall. When the first one comes out we do not need to do anything, where the 6 is the first one. Then the second one comes out and is 4, so we put them to the left of the first. Then third comes out the 7, and we put them to the right of the first. Finally, the fourth comes out as the 5, and we put them inbetween the first and second. This method allows us to incrementally sort and always see the correct ordering without waiting. + +However, lets say we have collaborative text. We have the initial text of "Hello" and an editor named Alice adds ", World!" in which each letter (' ', ',', 'W', 'o', 'r', 'l', 'd', '!') is streamed, potentially out of order, to Bob. We are going to assert that relative ordering does not work because, for the sake of explanation, any individual letter could be lost in the mail as they are sent. The last thing we want is Bob receiving the three characters 'old', which has correct relative ordering but wrong dependency, and thinking Alice is insulting him for being senile. Therefore every letter should specify the message that comes before it, the letter that it depends upon. \ No newline at end of file