diff --git a/demo/.jshintrc b/demo/.jshintrc
new file mode 100644
index 000000000..5eb122474
--- /dev/null
+++ b/demo/.jshintrc
@@ -0,0 +1,8 @@
+{
+  "esnext": true,
+  "browser": true,
+  "globals": {
+    "sigma": true,
+    "console": true
+  }
+}
diff --git a/demo/static/cypher/.gitattributes b/demo/static/cypher/.gitattributes
new file mode 100644
index 000000000..f8bdd60f4
--- /dev/null
+++ b/demo/static/cypher/.gitattributes
@@ -0,0 +1,8 @@
+*.txt   text
+*.js    text
+*.html  text
+*.md    text
+*.json  text
+*.yml   text
+*.css   text
+*.svg   text
diff --git a/demo/static/cypher/.gitignore b/demo/static/cypher/.gitignore
new file mode 100644
index 000000000..f91c241f2
--- /dev/null
+++ b/demo/static/cypher/.gitignore
@@ -0,0 +1,8 @@
+/node_modules
+/npm-debug.log
+/test*.html
+.tern-*
+*~
+*.swp
+.idea
+*.iml
diff --git a/demo/static/cypher/.npmignore b/demo/static/cypher/.npmignore
new file mode 100644
index 000000000..5ed053f89
--- /dev/null
+++ b/demo/static/cypher/.npmignore
@@ -0,0 +1,10 @@
+/node_modules
+/demo
+/doc
+/test
+/test*.html
+/index.html
+/mode/*/*test.js
+/mode/*/*.html
+/mode/index.html
+.*
diff --git a/demo/static/cypher/.travis.yml b/demo/static/cypher/.travis.yml
new file mode 100644
index 000000000..52b8b8159
--- /dev/null
+++ b/demo/static/cypher/.travis.yml
@@ -0,0 +1,4 @@
+language: node_js
+node_js:
+  - stable
+sudo: false
diff --git a/demo/static/cypher/codemirror.css b/demo/static/cypher/codemirror.css
new file mode 100644
index 000000000..ebfe4f697
--- /dev/null
+++ b/demo/static/cypher/codemirror.css
@@ -0,0 +1,335 @@
+/* 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-cursor {
+  border-left: 1px solid black;
+  border-right: none;
+  width: 0;
+}
+/* Shown when moving in bi-directional text */
+.CodeMirror div.CodeMirror-secondarycursor {
+  border-left: 1px solid silver;
+}
+.cm-fat-cursor .CodeMirror-cursor {
+  width: auto;
+  border: 0;
+  background: #7e7;
+}
+.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;
+  background-color: #7e7;
+}
+@-moz-keyframes blink {
+  0% {}
+  50% { background-color: transparent; }
+  100% {}
+}
+@-webkit-keyframes blink {
+  0% {}
+  50% { background-color: transparent; }
+  100% {}
+}
+@keyframes blink {
+  0% {}
+  50% { background-color: transparent; }
+  100% {}
+}
+
+/* Can style cursor different in overwrite (non-insert) mode */
+.CodeMirror-overwrite .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-header {color: blue;}
+.cm-s-default .cm-quote {color: #090;}
+.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-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-hr {color: #999;}
+.cm-s-default .cm-link {color: #00c;}
+
+.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 actual 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;
+  vertical-align: top;
+  margin-bottom: -30px;
+  /* Hack to make IE7 behave */
+  *zoom:1;
+  *display:inline;
+}
+.CodeMirror-gutter-wrapper {
+  position: absolute;
+  z-index: 4;
+  background: none !important;
+  border: none !important;
+}
+.CodeMirror-gutter-background {
+  position: absolute;
+  top: 0; bottom: 0;
+  z-index: 4;
+}
+.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-cursor { position: absolute; }
+.CodeMirror-measure pre { position: static; }
+
+div.CodeMirror-cursors {
+  visibility: hidden;
+  position: relative;
+  z-index: 3;
+}
+div.CodeMirror-dragcursors {
+  visibility: visible;
+}
+
+.CodeMirror-focused div.CodeMirror-cursors {
+  visibility: visible;
+}
+
+.CodeMirror-selected { background: #d9d9d9; }
+.CodeMirror-focused .CodeMirror-selected { background: #d7d4f0; }
+.CodeMirror-crosshair { cursor: crosshair; }
+.CodeMirror-line::selection, .CodeMirror-line > span::selection, .CodeMirror-line > span > span::selection { background: #d7d4f0; }
+.CodeMirror-line::-moz-selection, .CodeMirror-line > span::-moz-selection, .CodeMirror-line > span > span::-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/demo/static/cypher/codemirror.js b/demo/static/cypher/codemirror.js
new file mode 100644
index 000000000..e803211bc
--- /dev/null
+++ b/demo/static/cypher/codemirror.js
@@ -0,0 +1,8892 @@
+// 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 || window).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 userAgent = navigator.userAgent;
+  var platform = navigator.platform;
+
+  var gecko = /gecko\/\d/i.test(userAgent);
+  var ie_upto10 = /MSIE \d/.test(userAgent);
+  var ie_11up = /Trident\/(?:[7-9]|\d{2,})\..*rv:(\d+)/.exec(userAgent);
+  var ie = ie_upto10 || ie_11up;
+  var ie_version = ie && (ie_upto10 ? document.documentMode || 6 : ie_11up[1]);
+  var webkit = /WebKit\//.test(userAgent);
+  var qtwebkit = webkit && /Qt\/\d+\.\d+/.test(userAgent);
+  var chrome = /Chrome\//.test(userAgent);
+  var presto = /Opera\//.test(userAgent);
+  var safari = /Apple Computer/.test(navigator.vendor);
+  var mac_geMountainLion = /Mac OS X 1\d\D([8-9]|\d\d)\D/.test(userAgent);
+  var phantom = /PhantomJS/.test(userAgent);
+
+  var ios = /AppleWebKit/.test(userAgent) && /Mobile\/\w+/.test(userAgent);
+  // This is woefully incomplete. Suggestions for alternative methods welcome.
+  var mobile = ios || /Android|webOS|BlackBerry|Opera Mini|Opera Mobi|IEMobile/i.test(userAgent);
+  var mac = ios || /Mac/.test(platform);
+  var windows = /win/i.test(platform);
+
+  var presto_version = presto && 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, null, options.lineSeparator);
+    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
+      selectingText: false,
+      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.checkedZeroWidth = 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.checkedZeroWidth && measure.clientHeight > 0) {
+        if (sWidth == 0) this.zeroWidthHack();
+        this.checkedZeroWidth = true;
+      }
+
+      return {right: needsV ? sWidth : 0, bottom: needsH ? sWidth : 0};
+    },
+    setScrollLeft: function(pos) {
+      if (this.horiz.scrollLeft != pos) this.horiz.scrollLeft = pos;
+      if (this.disableHoriz) this.enableZeroWidthBar(this.horiz, this.disableHoriz);
+    },
+    setScrollTop: function(pos) {
+      if (this.vert.scrollTop != pos) this.vert.scrollTop = pos;
+      if (this.disableVert) this.enableZeroWidthBar(this.vert, this.disableVert);
+    },
+    zeroWidthHack: function() {
+      var w = mac && !mac_geMountainLion ? "12px" : "18px";
+      this.horiz.style.height = this.vert.style.width = w;
+      this.horiz.style.pointerEvents = this.vert.style.pointerEvents = "none";
+      this.disableHoriz = new Delayed;
+      this.disableVert = new Delayed;
+    },
+    enableZeroWidthBar: function(bar, delay) {
+      bar.style.pointerEvents = "auto";
+      function maybeDisable() {
+        // To find out whether the scrollbar is still visible, we
+        // check whether the element under the pixel in the bottom
+        // left corner of the scrollbar box is the scrollbar box
+        // itself (when the bar is still visible) or its filler child
+        // (when the bar is hidden). If it is still visible, we keep
+        // it enabled, if it's hidden, we disable pointer events.
+        var box = bar.getBoundingClientRect();
+        var elt = document.elementFromPoint(box.left + 1, box.bottom - 1);
+        if (elt != bar) bar.style.pointerEvents = "none";
+        else delay.set(1000, maybeDisable);
+      }
+      delay.set(1000, maybeDisable);
+    },
+    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";
+    d.heightForcer.style.borderBottom = sizes.bottom + "px solid transparent"
+
+    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 = display.sizer.style.minHeight = 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";
+    cm.display.heightForcer.style.top = measure.docHeight + "px";
+    cm.display.gutters.style.height = Math.max(measure.docHeight + cm.display.barHeight + 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.parentNode.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;
+    }
+    if (lineView.gutterBackground) {
+      lineView.node.removeChild(lineView.gutterBackground);
+      lineView.gutterBackground = null;
+    }
+    if (lineView.line.gutterClass) {
+      var wrap = ensureLineWrapped(lineView);
+      lineView.gutterBackground = elt("div", null, "CodeMirror-gutter-background " + lineView.line.gutterClass,
+                                      "left: " + (cm.options.fixedGutter ? dims.fixedPos : -dims.gutterTotalWidth) +
+                                      "px; width: " + dims.gutterTotalWidth + "px");
+      wrap.insertBefore(lineView.gutterBackground, lineView.text);
+    }
+    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");
+      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); }
+  }
+
+  // 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 paste = cm.state.pasteIncoming || origin == "paste";
+    var textLines = doc.splitLines(inserted), multiPaste = null;
+    // When pasing N lines into N selections, insert one line per selection
+    if (paste && sel.ranges.length > 1) {
+      if (lastCopied && lastCopied.join("\n") == inserted) {
+        if (sel.ranges.length % lastCopied.length == 0) {
+          multiPaste = [];
+          for (var i = 0; i < lastCopied.length; i++)
+            multiPaste.push(doc.splitLines(lastCopied[i]));
+        }
+      } 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 && !paste) // 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 || (paste ? "paste" : cm.state.cutIncoming ? "cut" : "+input")};
+      makeChange(cm.doc, changeEvent);
+      signalLater(cm, "inputRead", cm, changeEvent);
+    }
+    if (inserted && !paste)
+      triggerElectric(cm, inserted);
+
+    ensureCursorVisible(cm);
+    cm.curOp.updateInput = updateInput;
+    cm.curOp.typing = true;
+    cm.state.pasteIncoming = cm.state.cutIncoming = false;
+  }
+
+  function handlePaste(e, cm) {
+    var pasted = e.clipboardData && e.clipboardData.getData("text/plain");
+    if (pasted) {
+      e.preventDefault();
+      if (!cm.isReadOnly() && !cm.options.disableInput)
+        runInOp(cm, function() { applyTextInput(cm, pasted, 0, null, "paste"); });
+      return true;
+    }
+  }
+
+  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(e) {
+        if (signalDOMEvent(cm, e) || handlePaste(e, cm)) return
+
+        cm.state.pasteIncoming = true;
+        input.fastPoll();
+      });
+
+      function prepareCopyCut(e) {
+        if (signalDOMEvent(cm, e)) return
+        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) || signalDOMEvent(cm, 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");
+        if (input.composing) input.composing.range.clear()
+        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 (this.contextMenuPending || !cm.state.focused ||
+          (hasSelection(input) && !prevInput && !this.composing) ||
+          cm.isReadOnly() || cm.options.disableInput || cm.state.keySeq)
+        return 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, oldWrapperCSS = input.wrapper.style.cssText;
+      input.wrapper.style.cssText = "position: absolute"
+      var wrapperBox = input.wrapper.getBoundingClientRect()
+      te.style.cssText = "position: absolute; width: 30px; height: 30px; top: " + (e.clientY - wrapperBox.top - 5) +
+        "px; left: " + (e.clientX - wrapperBox.left - 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.cssText = oldWrapperCSS
+        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);
+      }
+    },
+
+    readOnlyChanged: function(val) {
+      if (!val) this.reset();
+    },
+
+    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;
+      disableBrowserMagic(div);
+
+      on(div, "paste", function(e) {
+        if (!signalDOMEvent(cm, e)) handlePaste(e, cm);
+      })
+
+      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 (cm.isReadOnly() || !input.pollContent())
+          runInOp(input.cm, function() {regChange(cm);});
+      });
+
+      function onCopyCut(e) {
+        if (signalDOMEvent(cm, e)) return
+        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) {
+        if (!gecko && this.cm.state.focused) {
+          sel.collapse(start.node, start.offset);
+          if (!rng.collapsed) sel.addRange(rng);
+        } else {
+          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.lineDiv.lastChild;
+      } else {
+        var toLine = lineNo(display.view[toIndex + 1].line) - 1;
+        var toNode = display.view[toIndex + 1].node.previousSibling;
+      }
+
+      var newText = cm.doc.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 (this.cm.isReadOnly())
+        operation(this.cm, regChange)(this.cm)
+      else if (composing.data && composing.data != composing.startData)
+        operation(this.cm, applyTextInput)(this.cm, composing.data, 0, composing.sel);
+    },
+
+    setUneditable: function(node) {
+      node.contentEditable = "false"
+    },
+
+    onKeyPress: function(e) {
+      e.preventDefault();
+      if (!this.cm.isReadOnly())
+        operation(this.cm, applyTextInput)(this.cm, String.fromCharCode(e.charCode == null ? e.keyCode : e.charCode), 0);
+    },
+
+    readOnlyChanged: function(val) {
+      this.div.contentEditable = String(val != "nocursor")
+    },
+
+    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, lineSep = cm.doc.lineSeparator();
+    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(lineSep);
+          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 += lineSep;
+          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, options) {
+    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));
+      },
+      origin: options && options.origin
+    };
+    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, options);
+
+    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 old = sel.ranges.length == doc.sel.ranges.length && doc.sel.ranges[i];
+      var newAnchor = skipAtomic(doc, range.anchor, old && old.anchor, bias, mayClear);
+      var newHead = skipAtomic(doc, range.head, old && old.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;
+  }
+
+  function skipAtomicInner(doc, pos, oldPos, dir, mayClear) {
+    var line = getLine(doc, pos.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 <= pos.ch : sp.from < pos.ch)) &&
+          (sp.to == null || (m.inclusiveRight ? sp.to >= pos.ch : sp.to > pos.ch))) {
+        if (mayClear) {
+          signal(m, "beforeCursorEnter");
+          if (m.explicitlyCleared) {
+            if (!line.markedSpans) break;
+            else {--i; continue;}
+          }
+        }
+        if (!m.atomic) continue;
+
+        if (oldPos) {
+          var near = m.find(dir < 0 ? 1 : -1), diff;
+          if (dir < 0 ? m.inclusiveRight : m.inclusiveLeft) near = movePos(doc, near, -dir, line);
+          if (near && near.line == pos.line && (diff = cmp(near, oldPos)) && (dir < 0 ? diff < 0 : diff > 0))
+            return skipAtomicInner(doc, near, pos, dir, mayClear);
+        }
+
+        var far = m.find(dir < 0 ? -1 : 1);
+        if (dir < 0 ? m.inclusiveLeft : m.inclusiveRight) far = movePos(doc, far, dir, line);
+        return far ? skipAtomicInner(doc, far, pos, dir, mayClear) : null;
+      }
+    }
+    return pos;
+  }
+
+  // Ensure a given position is not inside an atomic range.
+  function skipAtomic(doc, pos, oldPos, bias, mayClear) {
+    var dir = bias || 1;
+    var found = skipAtomicInner(doc, pos, oldPos, dir, mayClear) ||
+        (!mayClear && skipAtomicInner(doc, pos, oldPos, dir, true)) ||
+        skipAtomicInner(doc, pos, oldPos, -dir, mayClear) ||
+        (!mayClear && skipAtomicInner(doc, pos, oldPos, -dir, true));
+    if (!found) {
+      doc.cantEdit = true;
+      return Pos(doc.first, 0);
+    }
+    return found;
+  }
+
+  function movePos(doc, pos, dir, line) {
+    if (dir < 0 && pos.ch == 0) {
+      if (pos.line > doc.first) return clipPos(doc, Pos(pos.line - 1));
+      else return null;
+    } else if (dir > 0 && pos.ch == (line || getLine(doc, pos.line)).text.length) {
+      if (pos.line < doc.first + doc.size - 1) return Pos(pos.line + 1, 0);
+      else return null;
+    } else {
+      return new Pos(pos.line, pos.ch + dir);
+    }
+  }
+
+  // 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.head, curFragment);
+      if (!collapsed)
+        drawSelectionRange(cm, range, selFragment);
+    }
+    return result;
+  }
+
+  // Draws a cursor for the given range
+  function drawSelectionCursor(cm, head, output) {
+    var pos = cursorCoords(cm, 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, tooLong = line.text.length > cm.options.maxHighlightLength;
+        var highlighted = highlightLine(cm, line, tooLong ? copyState(doc.mode, state) : 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 = tooLong ? state : copyState(doc.mode, state);
+      } else {
+        if (line.text.length <= cm.options.maxHighlightLength)
+          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));
+      cm.curOp.forceUpdate = true;
+    }
+    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].call(null);
+      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++].call(null, 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() && (!document.hasFocus || document.hasFocus()))
+      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 - display.scroller.clientWidth, 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 (!signalDOMEvent(cm, e) && !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 = {
+      enter: function(e) {if (!signalDOMEvent(cm, e)) e_stop(e);},
+      over: function(e) {if (!signalDOMEvent(cm, e)) { onDragOver(cm, e); e_stop(e); }},
+      start: function(e){onDragStart(cm, e);},
+      drop: operation(cm, onDrop),
+      leave: function() {clearDragCursor(cm);}
+    };
+
+    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.enter);
+      toggle(cm.display.scroller, "dragover", funcs.over);
+      toggle(cm.display.scroller, "dragleave", funcs.leave);
+      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 (signalDOMEvent(cm, e) || display.activeTouch && display.input.supportsTouch()) 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:
+      // #3261: make sure, that we're not starting a second selection
+      if (cm.state.selectingText)
+        cm.state.selectingText(e);
+      else 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 && !cm.isReadOnly() &&
+        type == "single" && (contained = sel.contains(start)) > -1 &&
+        (cmp((contained = sel.ranges[contained]).from(), start) < 0 || start.xRel > 0) &&
+        (cmp(contained.to(), start) > 0 || start.xRel < 0))
+      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),
+                   {scroll: false, origin: "*mouse"});
+      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) {
+      cm.state.selectingText = false;
+      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);
+    cm.state.selectingText = up;
+    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) {
+    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];
+        signal(cm, type, cm, line, gutter, e);
+        return e_defaultPrevented(e);
+      }
+    }
+  }
+
+  function clickInGutter(cm, e) {
+    return gutterEvent(cm, e, "gutterClick", true);
+  }
+
+  // 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;
+    clearDragCursor(cm);
+    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 || cm.isReadOnly()) 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) {
+        if (cm.options.allowDropFileTypes &&
+            indexOf(cm.options.allowDropFileTypes, file.type) == -1)
+          return;
+
+        var reader = new FileReader;
+        reader.onload = operation(cm, function() {
+          var content = reader.result;
+          if (/[\x00-\x08\x0e-\x1f]{2}/.test(content)) content = "";
+          text[i] = content;
+          if (++read == n) {
+            pos = clipPos(cm.doc, pos);
+            var change = {from: pos, to: pos,
+                          text: cm.doc.splitLines(text.join(cm.doc.lineSeparator())),
+                          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);
+    }
+  }
+
+  function onDragOver(cm, e) {
+    var pos = posFromMouse(cm, e);
+    if (!pos) return;
+    var frag = document.createDocumentFragment();
+    drawSelectionCursor(cm, pos, frag);
+    if (!cm.display.dragCursor) {
+      cm.display.dragCursor = elt("div", null, "CodeMirror-cursors CodeMirror-dragcursors");
+      cm.display.lineSpace.insertBefore(cm.display.dragCursor, cm.display.cursorDiv);
+    }
+    removeChildrenAndAdd(cm.display.dragCursor, frag);
+  }
+
+  function clearDragCursor(cm) {
+    if (cm.display.dragCursor) {
+      cm.display.lineSpace.removeChild(cm.display.dragCursor);
+      cm.display.dragCursor = null;
+    }
+  }
+
+  // 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
+    var canScrollX = scroll.scrollWidth > scroll.clientWidth;
+    var canScrollY = scroll.scrollHeight > scroll.clientHeight;
+    if (!(dx && canScrollX || dy && canScrollY)) 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 && canScrollY)
+        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)));
+      // Only prevent default scrolling if vertical scrolling is
+      // actually possible. Otherwise, it causes vertical scroll
+      // jitter on OSX trackpads when deltaX is small and deltaY
+      // is large (issue #3579)
+      if (!dy || (dy && canScrollY))
+        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 (cm.isReadOnly()) 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;
+    if (signalDOMEvent(cm, e, "contextmenu")) return;
+    cm.display.input.onContextMenu(e);
+  }
+
+  function contextMenuInGutter(cm, e) {
+    if (!hasHandler(cm, "gutterContextMenu")) return false;
+    return gutterEvent(cm, e, "gutterContextMenu", false);
+  }
+
+  // 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 = doc.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);
+    function findNextLine() {
+      var l = line + dir;
+      if (l < doc.first || l >= doc.first + doc.size) return 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 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), pos, origDir, true);
+    if (!cmp(pos, result)) 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].call(null, 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(); },
+    isReadOnly: function() { return !!(this.options.readOnly || this.doc.cantEdit); },
+
+    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("lineSeparator", null, function(cm, val) {
+    cm.doc.lineSep = val;
+    if (!val) return;
+    var newBreaks = [], lineNo = cm.doc.first;
+    cm.doc.iter(function(line) {
+      for (var pos = 0;;) {
+        var found = line.text.indexOf(val, pos);
+        if (found == -1) break;
+        pos = found + val.length;
+        newBreaks.push(Pos(lineNo, found));
+      }
+      lineNo++;
+    });
+    for (var i = newBreaks.length - 1; i >= 0; i--)
+      replaceRange(cm.doc, val, newBreaks[i], Pos(newBreaks[i].line, newBreaks[i].ch + val.length))
+  });
+  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;
+    }
+    cm.display.input.readOnlyChanged(val)
+  });
+  option("disableInput", false, function(cm, val) {if (!val) cm.display.input.reset();}, true);
+  option("dragDrop", true, dragDropChanged);
+  option("allowDropFileTypes", null);
+
+  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) + cm.doc.lineSeparator() +
+                                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(cm.doc.lineSeparator(), 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.parentNode.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 state = getStateBefore(cm, lineNo(line));
+      var result = highlightLine(cm, line, line.text.length > cm.options.maxHighlightLength ? copyState(cm.doc.mode, state) : state);
+      line.stateAfter = state;
+      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()) {
+      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], "CodeMirror-line"), 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 if (m[0] == "\r" || m[0] == "\n") {
+          var txt = content.appendChild(elt("span", m[0] == "\r" ? "\u240d" : "\u2424", "cm-invalidchar"));
+          txt.setAttribute("cm-text", m[0]);
+          builder.col += 1;
+        } 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 = [], endStyles
+        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 = (css ? css + ";" : "") + m.css;
+            if (m.startStyle && sp.from == pos) spanStartStyle += " " + m.startStyle;
+            if (m.endStyle && sp.to == nextChange) (endStyles || (endStyles = [])).push(m.endStyle, sp.to)
+            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 (endStyles) for (var j = 0; j < endStyles.length; j += 2)
+          if (endStyles[j + 1] == nextChange) spanEndStyle += " " + endStyles[j]
+
+        if (!collapsed || collapsed.from == pos) for (var j = 0; j < foundBookmarks.length; ++j)
+          buildCollapsedSpan(builder, 0, foundBookmarks[j]);
+        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 (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, lineSep) {
+    if (!(this instanceof Doc)) return new Doc(text, mode, firstLine, lineSep);
+    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;
+    this.lineSep = lineSep;
+    this.extend = false;
+
+    if (typeof text == "string") text = this.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 || this.lineSeparator());
+    },
+    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: this.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 || this.lineSeparator());
+    },
+
+    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) {
+      var heads = map(this.sel.ranges, f);
+      extendSelections(this, clipPosArray(this, heads), 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 || this.lineSeparator());
+    },
+    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 || this.lineSeparator());
+        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: this.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, options && options.type || "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, this.lineSep);
+      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, this.lineSep);
+      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;},
+
+    splitLines: function(str) {
+      if (this.lineSep) return str.split(this.lineSep);
+      return splitLinesAuto(str);
+    },
+    lineSeparator: function() { return this.lineSep || "\n"; }
+  });
+
+  // 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 constructor".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 noHandlers = []
+  function getHandlers(emitter, type, copy) {
+    var arr = emitter._handlers && emitter._handlers[type]
+    if (copy) return arr && arr.length > 0 ? arr.slice() : noHandlers
+    else return arr || noHandlers
+  }
+
+  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 handlers = getHandlers(emitter, type, false)
+      for (var i = 0; i < handlers.length; ++i)
+        if (handlers[i] == f) { handlers.splice(i, 1); break; }
+    }
+  };
+
+  var signal = CodeMirror.signal = function(emitter, type /*, values...*/) {
+    var handlers = getHandlers(emitter, type, true)
+    if (!handlers.length) return;
+    var args = Array.prototype.slice.call(arguments, 2);
+    for (var i = 0; i < handlers.length; ++i) handlers[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 = getHandlers(emitter, type, false)
+    if (!arr.length) 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) {
+    return getHandlers(emitter, type).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.
+  var findColumn = CodeMirror.findColumn = function(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() {
+    var activeElement = document.activeElement;
+    while (activeElement && activeElement.root && activeElement.root.activeElement)
+      activeElement = activeElement.root.activeElement;
+    return 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 splitLinesAuto = 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 = CodeMirror.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",
+    106: "*", 107: "=", 109: "-", 110: ".", 111: "/", 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"
+  };
+  (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.12.0";
+
+  return CodeMirror;
+});
diff --git a/demo/static/cypher/cypher.js b/demo/static/cypher/cypher.js
new file mode 100644
index 000000000..107e4f6d2
--- /dev/null
+++ b/demo/static/cypher/cypher.js
@@ -0,0 +1,146 @@
+// CodeMirror, copyright (c) by Marijn Haverbeke and others
+// Distributed under an MIT license: http://codemirror.net/LICENSE
+
+// By the Neo4j Team and contributors.
+// https://github.com/neo4j-contrib/CodeMirror
+
+(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";
+  var wordRegexp = function(words) {
+    return new RegExp("^(?:" + words.join("|") + ")$", "i");
+  };
+
+  CodeMirror.defineMode("cypher", function(config) {
+    var tokenBase = function(stream/*, state*/) {
+      var ch = stream.next();
+      if (ch === "\"" || ch === "'") {
+        stream.match(/.+?["']/);
+        return "string";
+      }
+      if (/[{}\(\),\.;\[\]]/.test(ch)) {
+        curPunc = ch;
+        return "node";
+      } else if (ch === "/" && stream.eat("/")) {
+        stream.skipToEnd();
+        return "comment";
+      } else if (operatorChars.test(ch)) {
+        stream.eatWhile(operatorChars);
+        return null;
+      } else {
+        stream.eatWhile(/[_\w\d]/);
+        if (stream.eat(":")) {
+          stream.eatWhile(/[\w\d_\-]/);
+          return "atom";
+        }
+        var word = stream.current();
+        if (funcs.test(word)) return "builtin";
+        if (preds.test(word)) return "def";
+        if (keywords.test(word)) return "keyword";
+        return "variable";
+      }
+    };
+    var pushContext = function(state, type, col) {
+      return state.context = {
+        prev: state.context,
+        indent: state.indent,
+        col: col,
+        type: type
+      };
+    };
+    var popContext = function(state) {
+      state.indent = state.context.indent;
+      return state.context = state.context.prev;
+    };
+    var indentUnit = config.indentUnit;
+    var curPunc;
+    var funcs = wordRegexp(["abs", "acos", "allShortestPaths", "asin", "atan", "atan2", "avg", "ceil", "coalesce", "collect", "cos", "cot", "count", "degrees", "e", "endnode", "exp", "extract", "filter", "floor", "haversin", "head", "id", "keys", "labels", "last", "left", "length", "log", "log10", "lower", "ltrim", "max", "min", "node", "nodes", "percentileCont", "percentileDisc", "pi", "radians", "rand", "range", "reduce", "rel", "relationship", "relationships", "replace", "reverse", "right", "round", "rtrim", "shortestPath", "sign", "sin", "size", "split", "sqrt", "startnode", "stdev", "stdevp", "str", "substring", "sum", "tail", "tan", "timestamp", "toFloat", "toInt", "toString", "trim", "type", "upper"]);
+    var preds = wordRegexp(["all", "and", "any", "contains", "exists", "has", "in", "none", "not", "or", "single", "xor"]);
+    var keywords = wordRegexp(["as", "asc", "ascending", "assert", "by", "case", "commit", "constraint", "create", "csv", "cypher", "delete", "desc", "descending", "detach", "distinct", "drop", "else", "end", "ends", "explain", "false", "fieldterminator", "foreach", "from", "headers", "in", "index", "is", "join", "limit", "load", "match", "merge", "null", "on", "optional", "order", "periodic", "profile", "remove", "return", "scan", "set", "skip", "start", "starts", "then", "true", "union", "unique", "unwind", "using", "when", "where", "with"]);
+    var operatorChars = /[*+\-<>=&|~%^]/;
+
+    return {
+      startState: function(/*base*/) {
+        return {
+          tokenize: tokenBase,
+          context: null,
+          indent: 0,
+          col: 0
+        };
+      },
+      token: function(stream, state) {
+        if (stream.sol()) {
+          if (state.context && (state.context.align == null)) {
+            state.context.align = false;
+          }
+          state.indent = stream.indentation();
+        }
+        if (stream.eatSpace()) {
+          return null;
+        }
+        var style = state.tokenize(stream, state);
+        if (style !== "comment" && state.context && (state.context.align == null) && state.context.type !== "pattern") {
+          state.context.align = true;
+        }
+        if (curPunc === "(") {
+          pushContext(state, ")", stream.column());
+        } else if (curPunc === "[") {
+          pushContext(state, "]", stream.column());
+        } else if (curPunc === "{") {
+          pushContext(state, "}", stream.column());
+        } else if (/[\]\}\)]/.test(curPunc)) {
+          while (state.context && state.context.type === "pattern") {
+            popContext(state);
+          }
+          if (state.context && curPunc === state.context.type) {
+            popContext(state);
+          }
+        } else if (curPunc === "." && state.context && state.context.type === "pattern") {
+          popContext(state);
+        } else if (/atom|string|variable/.test(style) && state.context) {
+          if (/[\}\]]/.test(state.context.type)) {
+            pushContext(state, "pattern", stream.column());
+          } else if (state.context.type === "pattern" && !state.context.align) {
+            state.context.align = true;
+            state.context.col = stream.column();
+          }
+        }
+        return style;
+      },
+      indent: function(state, textAfter) {
+        var firstChar = textAfter && textAfter.charAt(0);
+        var context = state.context;
+        if (/[\]\}]/.test(firstChar)) {
+          while (context && context.type === "pattern") {
+            context = context.prev;
+          }
+        }
+        var closing = context && firstChar === context.type;
+        if (!context) return 0;
+        if (context.type === "keywords") return CodeMirror.commands.newlineAndIndent;
+        if (context.align) return context.col + (closing ? 0 : 1);
+        return context.indent + (closing ? 0 : indentUnit);
+      }
+    };
+  });
+
+  CodeMirror.modeExtensions["cypher"] = {
+    autoFormatLineBreaks: function(text) {
+      var i, lines, reProcessedPortion;
+      var lines = text.split("\n");
+      var reProcessedPortion = /\s+\b(return|where|order by|match|with|skip|limit|create|delete|set)\b\s/g;
+      for (var i = 0; i < lines.length; i++)
+        lines[i] = lines[i].replace(reProcessedPortion, " \n$1 ").trim();
+      return lines.join("\n");
+    }
+  };
+
+  CodeMirror.defineMIME("application/x-cypher-query", "cypher");
+
+});
diff --git a/demo/static/cypher/neo.css b/demo/static/cypher/neo.css
new file mode 100644
index 000000000..b28d5c65f
--- /dev/null
+++ b/demo/static/cypher/neo.css
@@ -0,0 +1,43 @@
+/* neo theme for codemirror */
+
+/* Color scheme */
+
+.cm-s-neo.CodeMirror {
+  background-color:#ffffff;
+  color:#2e383c;
+  line-height:1.4375;
+}
+.cm-s-neo .cm-comment { color:#75787b; }
+.cm-s-neo .cm-keyword, .cm-s-neo .cm-property { color:#1d75b3; }
+.cm-s-neo .cm-atom,.cm-s-neo .cm-number { color:#75438a; }
+.cm-s-neo .cm-node,.cm-s-neo .cm-tag { color:#9c3328; }
+.cm-s-neo .cm-string { color:#b35e14; }
+.cm-s-neo .cm-variable,.cm-s-neo .cm-qualifier { color:#047d65; }
+
+
+/* Editor styling */
+
+.cm-s-neo pre {
+  padding:0;
+}
+
+.cm-s-neo .CodeMirror-gutters {
+  border:none;
+  border-right:10px solid transparent;
+  background-color:transparent;
+}
+
+.cm-s-neo .CodeMirror-linenumber {
+  padding:0;
+  color:#e0e2e5;
+}
+
+.cm-s-neo .CodeMirror-guttermarker { color: #1d75b3; }
+.cm-s-neo .CodeMirror-guttermarker-subtle { color: #e0e2e5; }
+
+.cm-s-neo .CodeMirror-cursor {
+  width: auto;
+  border: 0;
+  background: rgba(155,157,162,0.37);
+  z-index: 1;
+}
diff --git a/demo/static/demo.css b/demo/static/demo.css
new file mode 100644
index 000000000..abc32f8d2
--- /dev/null
+++ b/demo/static/demo.css
@@ -0,0 +1,17 @@
+
+body {
+  background-color: #eee;
+}
+
+.w100 {
+  width: 100%;
+}
+
+.CodeMirror {
+  border-top: 1px solid #eee;
+  height: auto;
+}
+
+svg, text {
+  font-size: 28;
+}
diff --git a/demo/static/demo.html b/demo/static/demo.html
new file mode 100644
index 000000000..f02dd437c
--- /dev/null
+++ b/demo/static/demo.html
@@ -0,0 +1,142 @@
+<!doctype html>
+<html lang="en">
+<head>
+  <meta charset="utf-8">
+  <title>Memgraph Demo</title>
+
+  <link rel="stylesheet" href="demo.css">
+  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/materialize/0.97.5/css/materialize.min.css">
+  <link rel="stylesheet" href="cypher/codemirror.css">
+  <link rel="stylesheet" href="cypher/neo.css">
+
+  <link rel="stylesheet" href="https://cdn.rawgit.com/novus/nvd3/v1.8.1/build/nv.d3.css">
+  <link rel="stylesheet" href="demo.css">
+</head>
+
+<body>
+  <div class="row">
+    <div class="valign-wrapper">
+      <h1 class="valign center w100">Memgraph Benchmark</h1>
+    </div>
+  </div>
+  <div class="row">
+    <div class="col s2">
+      <div id="q1" class="card">
+        <div class="qps valign-wrapper">
+          <div class="col s6">
+            <h3 id="text" class="valign center w100">25734</h3>
+          </div>
+          <div class="col s6">
+            <canvas id="gauge" width="150" height="60"
+              class="valign center"></canvas>
+          </div>
+        </div>
+        <textarea id="query">MATCH (n:Person) RETURN n</textarea>
+      </div>
+      <div id="q2" class="card">
+        <div class="qps valign-wrapper">
+          <div class="col s6">
+            <h3 id="text" class="valign center w100">25734</h3>
+          </div>
+          <div class="col s6">
+            <canvas id="gauge" width="150" height="60"
+              class="valign center"></canvas>
+          </div>
+        </div>
+        <textarea id="query">MATCH (n:Person) RETURN n</textarea>
+      </div>
+      <div id="q3" class="card">
+        <div class="qps valign-wrapper">
+          <div class="col s6">
+            <h3 id="text" class="valign center w100">25734</h3>
+          </div>
+          <div class="col s6">
+            <canvas id="gauge" width="150" height="60"
+              class="valign center"></canvas>
+          </div>
+        </div>
+        <textarea id="query">MATCH (n:Person) RETURN n</textarea>
+      </div>
+      <div id="q4" class="card">
+        <div class="qps valign-wrapper">
+          <div class="col s6">
+            <h3 id="text" class="valign center w100">25734</h3>
+          </div>
+          <div class="col s6">
+            <canvas id="gauge" width="150" height="60"
+              class="valign center"></canvas>
+          </div>
+        </div>
+        <textarea id="query">MATCH (n:Person) RETURN n</textarea>
+      </div>
+    </div>
+    <div class="col s8">
+      <div class="card">
+        <div class="card-content">
+          <div id="chart" style="height: 600px;">
+            <svg></svg>
+          </div>
+        </div>
+      </div>
+    </div>
+    <div class="col s2">
+      <div id="q5" class="card">
+        <div class="qps valign-wrapper">
+          <div class="col s6">
+            <h3 id="text" class="valign center w100">25734</h3>
+          </div>
+          <div class="col s6">
+            <canvas id="gauge" width="150" height="60"
+              class="valign center"></canvas>
+          </div>
+        </div>
+        <textarea id="query">MATCH (n:Person) RETURN n</textarea>
+      </div>
+      <div id="q6" class="card">
+        <div class="qps valign-wrapper">
+          <div class="col s6">
+            <h3 id="text" class="valign center w100">25734</h3>
+          </div>
+          <div class="col s6">
+            <canvas id="gauge" width="150" height="60"
+              class="valign center"></canvas>
+          </div>
+        </div>
+        <textarea id="query">MATCH (n:Person) RETURN n</textarea>
+      </div>
+      <div id="q7" class="card">
+        <div class="qps valign-wrapper">
+          <div class="col s6">
+            <h3 id="text" class="valign center w100">25734</h3>
+          </div>
+          <div class="col s6">
+            <canvas id="gauge" width="150" height="60"
+              class="valign center"></canvas>
+          </div>
+        </div>
+        <textarea id="query">MATCH (n:Person) RETURN n</textarea>
+      </div>
+      <div id="q8" class="card">
+        <div class="qps valign-wrapper">
+          <div class="col s6">
+            <h3 id="text" class="valign center w100">25734</h3>
+          </div>
+          <div class="col s6">
+            <canvas id="gauge" width="150" height="60"
+              class="valign center"></canvas>
+          </div>
+        </div>
+        <textarea id="query">MATCH (n:Person) RETURN n</textarea>
+      </div>
+    </div>
+  </div>
+  <script type="text/javascript" src="https://code.jquery.com/jquery-2.1.1.min.js"></script>
+  <script src="https://cdnjs.cloudflare.com/ajax/libs/materialize/0.97.5/js/materialize.min.js"></script>
+
+  <script type="text/javascript" src="http://bernii.github.io/gauge.js/dist/gauge.min.js"></script>
+  <script src="http://d3js.org/d3.v3.min.js" charset="utf-8"></script>
+  <script src="https://cdn.rawgit.com/novus/nvd3/v1.8.1/build/nv.d3.min.js"></script>
+  <script src="cypher/codemirror.js"></script>
+  <script src="cypher/cypher.js"></script>
+  <script src="demo.js"></script>
+</body>
diff --git a/demo/static/demo.js b/demo/static/demo.js
new file mode 100644
index 000000000..8294b302e
--- /dev/null
+++ b/demo/static/demo.js
@@ -0,0 +1,186 @@
+(() => {
+  'use strict';
+
+  class QpsGauge {
+    constructor(canvas) {
+      this.gauge = new Gauge(canvas).setOptions({
+        lines: 12, // The number of lines to draw
+        angle: 0.07, // The length of each line
+        lineWidth: 0.27, // The line thickness
+        pointer: {
+          length: 0.82, // The radius of the inner circle
+          strokeWidth: 0.073, // The rotation offset
+          color: '#000' // Fill color
+        },
+        // If true, the pointer will not go past the end of the gauge
+        limitMax: 'true',   
+        strokeColor: '#e0e0e0', // to see which ones work best for you
+        generateGradient: true,
+        percentColors: [[0.0, '#d32f2f'], [0.5, '#ffee58'], [1.0, '#388e3c']],
+      });
+
+      this.gauge.animationSpeed = 1;
+    }
+
+    set(value) {
+      this.gauge.set(value);
+    }
+
+    setMax(value) {
+      this.gauge.maxValue = this.maxValue = value;
+    }
+
+    getMax() {
+      return this.maxValue;
+    }
+  }
+
+  class CypherQuery {
+    constructor(textarea) {
+      this.textarea = textarea;
+      this.editor = CodeMirror.fromTextArea(this.textarea, {
+        height: 0,
+        lineNumbers: true,
+        mode: 'application/x-cypher-query',
+        indentWithTabs: false,
+        smartIndent: true,
+        matchBrackets: true,
+        theme: 'neo',
+        viewportMargin: Infinity
+      });
+    }
+
+    set(value) {
+      this.editor.getDoc().setValue(value);
+    }
+
+    get(separator) {
+      separator = separator ? separator : ' ';
+      this.editor.getDoc().getValue(separator);
+    }
+  }
+
+  class QpsText {
+    constructor(element) {
+      this.element = element;
+    }
+
+    get() {
+      $(this.element).text();
+    }
+
+    set(text) {
+      $(this.element).text(text);
+    }
+  }
+
+  class QueryCard {
+    constructor(card, maxQps, value) {
+      this.card = card;
+      this.maxQps = maxQps;
+
+      value = value ? value : 1;
+      
+      this.text = new QpsText($(card).find('#text')[0]);
+      this.gauge = new QpsGauge($(card).find('#gauge')[0]);
+      this.query = new CypherQuery($(card).find('#query')[0]);
+
+      this.gauge.setMax(maxQps + 1);
+      this.gauge.animationSpeed = 1;
+      this.gauge.set(value);
+    }
+
+    set(value) {
+      this.text.set(value);
+      this.gauge.set(value);
+    }
+  }
+
+  let value = 0;
+  let maxQps = 5000;
+
+  let card1 = new QueryCard($('#q1')[0], maxQps);
+  let card2 = new QueryCard($('#q2')[0], maxQps);
+  let card3 = new QueryCard($('#q3')[0], maxQps);
+  let card4 = new QueryCard($('#q4')[0], maxQps);
+  let card5 = new QueryCard($('#q5')[0], maxQps);
+  let card6 = new QueryCard($('#q6')[0], maxQps);
+  let card7 = new QueryCard($('#q7')[0], maxQps);
+  let card8 = new QueryCard($('#q8')[0], maxQps);
+
+  function run() {
+    setTimeout(() => {
+      value += 10;
+
+      if(value >= maxQps)
+        value = 0;
+        
+      card1.set(value);
+      card2.set(value);
+      card3.set(value);
+      card4.set(value);
+      card5.set(value);
+      card6.set(value);
+      card7.set(value);
+      card8.set(value);
+
+      run();
+    }, 20);
+  }
+
+  run();
+
+
+  nv.addGraph(function() {
+    var chart = nv.models.lineChart()
+                  .useInteractiveGuideline(true)
+                  .showLegend(true)
+                  .showYAxis(true)
+                  .showXAxis(true);
+
+    chart.xAxis
+         .axisLabel('Time (s)')
+         .tickFormat(d3.format(',r'));
+
+    chart.yAxis
+         .axisLabel('QPS')
+         .tickFormat(d3.format(',r'));
+
+    var myData = sinAndCos();
+
+    d3.select('#chart svg')
+      .datum(myData)
+      .call(chart);
+
+    chart.update();
+    nv.utils.windowResize(function() { chart.update(); });
+    return chart;
+  });
+
+  function sinAndCos() {
+    var sin = [],sin2 = [],
+        cos = [];
+
+    for (var i = 0; i < 100; i++) {
+      sin.push({x: i, y: Math.sin(i/10)});
+      sin2.push({x: i, y: Math.sin(i/10) *0.25 + 0.5});
+      cos.push({x: i, y: 0.5 * Math.cos(i/10)});
+    }
+
+    return [{
+        values: sin,
+        key: 'Sine Wave',
+        color: '#ff7f0e'
+      }, {
+        values: cos,
+        key: 'Cosine Wave',
+        color: '#2ca02c'
+      }, {
+        values: sin2,
+        key: 'Another sine wave',
+        color: '#7777ff',
+        area: true
+      }];
+  }
+
+})();
diff --git a/demo/static/demo.py b/demo/static/demo.py
new file mode 100644
index 000000000..183f5d88b
--- /dev/null
+++ b/demo/static/demo.py
@@ -0,0 +1,6 @@
+
+
+class Chromosome(object):
+
+    def __init__(self, k):
+        pass
diff --git a/demo/static/sigmajs/captors/sigma.captors.mouse.js b/demo/static/sigmajs/captors/sigma.captors.mouse.js
new file mode 100644
index 000000000..d3a270c9f
--- /dev/null
+++ b/demo/static/sigmajs/captors/sigma.captors.mouse.js
@@ -0,0 +1,395 @@
+;(function(undefined) {
+  'use strict';
+
+  if (typeof sigma === 'undefined')
+    throw 'sigma is not declared';
+
+  // Initialize packages:
+  sigma.utils.pkg('sigma.captors');
+
+  /**
+   * The user inputs default captor. It deals with mouse events, keyboards
+   * events and touch events.
+   *
+   * @param  {DOMElement}   target   The DOM element where the listeners will be
+   *                                 bound.
+   * @param  {camera}       camera   The camera related to the target.
+   * @param  {configurable} settings The settings function.
+   * @return {sigma.captor}          The fresh new captor instance.
+   */
+  sigma.captors.mouse = function(target, camera, settings) {
+    var _self = this,
+        _target = target,
+        _camera = camera,
+        _settings = settings,
+
+        // CAMERA MANAGEMENT:
+        // ******************
+        // The camera position when the user starts dragging:
+        _startCameraX,
+        _startCameraY,
+        _startCameraAngle,
+
+        // The latest stage position:
+        _lastCameraX,
+        _lastCameraY,
+        _lastCameraAngle,
+        _lastCameraRatio,
+
+        // MOUSE MANAGEMENT:
+        // *****************
+        // The mouse position when the user starts dragging:
+        _startMouseX,
+        _startMouseY,
+
+        _isMouseDown,
+        _isMoving,
+        _hasDragged,
+        _downStartTime,
+        _movingTimeoutId;
+
+    sigma.classes.dispatcher.extend(this);
+
+    sigma.utils.doubleClick(_target, 'click', _doubleClickHandler);
+    _target.addEventListener('DOMMouseScroll', _wheelHandler, false);
+    _target.addEventListener('mousewheel', _wheelHandler, false);
+    _target.addEventListener('mousemove', _moveHandler, false);
+    _target.addEventListener('mousedown', _downHandler, false);
+    _target.addEventListener('click', _clickHandler, false);
+    _target.addEventListener('mouseout', _outHandler, false);
+    document.addEventListener('mouseup', _upHandler, false);
+
+
+
+
+    /**
+     * This method unbinds every handlers that makes the captor work.
+     */
+    this.kill = function() {
+      sigma.utils.unbindDoubleClick(_target, 'click');
+      _target.removeEventListener('DOMMouseScroll', _wheelHandler);
+      _target.removeEventListener('mousewheel', _wheelHandler);
+      _target.removeEventListener('mousemove', _moveHandler);
+      _target.removeEventListener('mousedown', _downHandler);
+      _target.removeEventListener('click', _clickHandler);
+      _target.removeEventListener('mouseout', _outHandler);
+      document.removeEventListener('mouseup', _upHandler);
+    };
+
+
+
+
+    // MOUSE EVENTS:
+    // *************
+
+    /**
+     * The handler listening to the 'move' mouse event. It will effectively
+     * drag the graph.
+     *
+     * @param {event} e A mouse event.
+     */
+    function _moveHandler(e) {
+      var x,
+          y,
+          pos;
+
+      // Dispatch event:
+      if (_settings('mouseEnabled'))
+        _self.dispatchEvent('mousemove', {
+          x: sigma.utils.getX(e) - sigma.utils.getWidth(e) / 2,
+          y: sigma.utils.getY(e) - sigma.utils.getHeight(e) / 2,
+          clientX: e.clientX,
+          clientY: e.clientY,
+          ctrlKey: e.ctrlKey,
+          metaKey: e.metaKey,
+          altKey: e.altKey,
+          shiftKey: e.shiftKey
+        });
+
+      if (_settings('mouseEnabled') && _isMouseDown) {
+        _isMoving = true;
+        _hasDragged = true;
+
+        if (_movingTimeoutId)
+          clearTimeout(_movingTimeoutId);
+
+        _movingTimeoutId = setTimeout(function() {
+          _isMoving = false;
+        }, _settings('dragTimeout'));
+
+        sigma.misc.animation.killAll(_camera);
+
+        _camera.isMoving = true;
+        pos = _camera.cameraPosition(
+          sigma.utils.getX(e) - _startMouseX,
+          sigma.utils.getY(e) - _startMouseY,
+          true
+        );
+
+        x = _startCameraX - pos.x;
+        y = _startCameraY - pos.y;
+
+        if (x !== _camera.x || y !== _camera.y) {
+          _lastCameraX = _camera.x;
+          _lastCameraY = _camera.y;
+
+          _camera.goTo({
+            x: x,
+            y: y
+          });
+        }
+
+        if (e.preventDefault)
+          e.preventDefault();
+        else
+          e.returnValue = false;
+
+        e.stopPropagation();
+        return false;
+      }
+    }
+
+    /**
+     * The handler listening to the 'up' mouse event. It will stop dragging the
+     * graph.
+     *
+     * @param {event} e A mouse event.
+     */
+    function _upHandler(e) {
+      if (_settings('mouseEnabled') && _isMouseDown) {
+        _isMouseDown = false;
+        if (_movingTimeoutId)
+          clearTimeout(_movingTimeoutId);
+
+        _camera.isMoving = false;
+
+        var x = sigma.utils.getX(e),
+            y = sigma.utils.getY(e);
+
+        if (_isMoving) {
+          sigma.misc.animation.killAll(_camera);
+          sigma.misc.animation.camera(
+            _camera,
+            {
+              x: _camera.x +
+                _settings('mouseInertiaRatio') * (_camera.x - _lastCameraX),
+              y: _camera.y +
+                _settings('mouseInertiaRatio') * (_camera.y - _lastCameraY)
+            },
+            {
+              easing: 'quadraticOut',
+              duration: _settings('mouseInertiaDuration')
+            }
+          );
+        } else if (
+          _startMouseX !== x ||
+          _startMouseY !== y
+        )
+          _camera.goTo({
+            x: _camera.x,
+            y: _camera.y
+          });
+
+        _self.dispatchEvent('mouseup', {
+          x: x - sigma.utils.getWidth(e) / 2,
+          y: y - sigma.utils.getHeight(e) / 2,
+          clientX: e.clientX,
+          clientY: e.clientY,
+          ctrlKey: e.ctrlKey,
+          metaKey: e.metaKey,
+          altKey: e.altKey,
+          shiftKey: e.shiftKey
+        });
+
+        // Update _isMoving flag:
+        _isMoving = false;
+      }
+    }
+
+    /**
+     * The handler listening to the 'down' mouse event. It will start observing
+     * the mouse position for dragging the graph.
+     *
+     * @param {event} e A mouse event.
+     */
+    function _downHandler(e) {
+      if (_settings('mouseEnabled')) {
+        _startCameraX = _camera.x;
+        _startCameraY = _camera.y;
+
+        _lastCameraX = _camera.x;
+        _lastCameraY = _camera.y;
+
+        _startMouseX = sigma.utils.getX(e);
+        _startMouseY = sigma.utils.getY(e);
+
+        _hasDragged = false;
+        _downStartTime = (new Date()).getTime();
+
+        switch (e.which) {
+          case 2:
+            // Middle mouse button pressed
+            // Do nothing.
+            break;
+          case 3:
+            // Right mouse button pressed
+            _self.dispatchEvent('rightclick', {
+              x: _startMouseX - sigma.utils.getWidth(e) / 2,
+              y: _startMouseY - sigma.utils.getHeight(e) / 2,
+              clientX: e.clientX,
+              clientY: e.clientY,
+              ctrlKey: e.ctrlKey,
+              metaKey: e.metaKey,
+              altKey: e.altKey,
+              shiftKey: e.shiftKey
+            });
+            break;
+          // case 1:
+          default:
+            // Left mouse button pressed
+            _isMouseDown = true;
+
+            _self.dispatchEvent('mousedown', {
+              x: _startMouseX - sigma.utils.getWidth(e) / 2,
+              y: _startMouseY - sigma.utils.getHeight(e) / 2,
+              clientX: e.clientX,
+              clientY: e.clientY,
+              ctrlKey: e.ctrlKey,
+              metaKey: e.metaKey,
+              altKey: e.altKey,
+              shiftKey: e.shiftKey
+            });
+        }
+      }
+    }
+
+    /**
+     * The handler listening to the 'out' mouse event. It will just redispatch
+     * the event.
+     *
+     * @param {event} e A mouse event.
+     */
+    function _outHandler(e) {
+      if (_settings('mouseEnabled'))
+        _self.dispatchEvent('mouseout');
+    }
+
+    /**
+     * The handler listening to the 'click' mouse event. It will redispatch the
+     * click event, but with normalized X and Y coordinates.
+     *
+     * @param {event} e A mouse event.
+     */
+    function _clickHandler(e) {
+      if (_settings('mouseEnabled'))
+        _self.dispatchEvent('click', {
+          x: sigma.utils.getX(e) - sigma.utils.getWidth(e) / 2,
+          y: sigma.utils.getY(e) - sigma.utils.getHeight(e) / 2,
+          clientX: e.clientX,
+          clientY: e.clientY,
+          ctrlKey: e.ctrlKey,
+          metaKey: e.metaKey,
+          altKey: e.altKey,
+          shiftKey: e.shiftKey,
+          isDragging:
+            (((new Date()).getTime() - _downStartTime) > 100) &&
+            _hasDragged
+        });
+
+      if (e.preventDefault)
+        e.preventDefault();
+      else
+        e.returnValue = false;
+
+      e.stopPropagation();
+      return false;
+    }
+
+    /**
+     * The handler listening to the double click custom event. It will
+     * basically zoom into the graph.
+     *
+     * @param {event} e A mouse event.
+     */
+    function _doubleClickHandler(e) {
+      var pos,
+          ratio,
+          animation;
+
+      if (_settings('mouseEnabled')) {
+        ratio = 1 / _settings('doubleClickZoomingRatio');
+
+        _self.dispatchEvent('doubleclick', {
+          x: _startMouseX - sigma.utils.getWidth(e) / 2,
+          y: _startMouseY - sigma.utils.getHeight(e) / 2,
+          clientX: e.clientX,
+          clientY: e.clientY,
+          ctrlKey: e.ctrlKey,
+          metaKey: e.metaKey,
+          altKey: e.altKey,
+          shiftKey: e.shiftKey
+        });
+
+        if (_settings('doubleClickEnabled')) {
+          pos = _camera.cameraPosition(
+            sigma.utils.getX(e) - sigma.utils.getWidth(e) / 2,
+            sigma.utils.getY(e) - sigma.utils.getHeight(e) / 2,
+            true
+          );
+
+          animation = {
+            duration: _settings('doubleClickZoomDuration')
+          };
+
+          sigma.utils.zoomTo(_camera, pos.x, pos.y, ratio, animation);
+        }
+
+        if (e.preventDefault)
+          e.preventDefault();
+        else
+          e.returnValue = false;
+
+        e.stopPropagation();
+        return false;
+      }
+    }
+
+    /**
+     * The handler listening to the 'wheel' mouse event. It will basically zoom
+     * in or not into the graph.
+     *
+     * @param {event} e A mouse event.
+     */
+    function _wheelHandler(e) {
+      var pos,
+          ratio,
+          animation;
+
+      if (_settings('mouseEnabled') && _settings('mouseWheelEnabled')) {
+        ratio = sigma.utils.getDelta(e) > 0 ?
+          1 / _settings('zoomingRatio') :
+          _settings('zoomingRatio');
+
+        pos = _camera.cameraPosition(
+          sigma.utils.getX(e) - sigma.utils.getWidth(e) / 2,
+          sigma.utils.getY(e) - sigma.utils.getHeight(e) / 2,
+          true
+        );
+
+        animation = {
+          duration: _settings('mouseZoomDuration')
+        };
+
+        sigma.utils.zoomTo(_camera, pos.x, pos.y, ratio, animation);
+
+        if (e.preventDefault)
+          e.preventDefault();
+        else
+          e.returnValue = false;
+
+        e.stopPropagation();
+        return false;
+      }
+    }
+  };
+}).call(this);
diff --git a/demo/static/sigmajs/captors/sigma.captors.touch.js b/demo/static/sigmajs/captors/sigma.captors.touch.js
new file mode 100644
index 000000000..b63b50aed
--- /dev/null
+++ b/demo/static/sigmajs/captors/sigma.captors.touch.js
@@ -0,0 +1,430 @@
+;(function(undefined) {
+  'use strict';
+
+  if (typeof sigma === 'undefined')
+    throw 'sigma is not declared';
+
+  // Initialize packages:
+  sigma.utils.pkg('sigma.captors');
+
+  /**
+   * The user inputs default captor. It deals with mouse events, keyboards
+   * events and touch events.
+   *
+   * @param  {DOMElement}   target   The DOM element where the listeners will be
+   *                                 bound.
+   * @param  {camera}       camera   The camera related to the target.
+   * @param  {configurable} settings The settings function.
+   * @return {sigma.captor}          The fresh new captor instance.
+   */
+  sigma.captors.touch = function(target, camera, settings) {
+    var _self = this,
+        _target = target,
+        _camera = camera,
+        _settings = settings,
+
+        // CAMERA MANAGEMENT:
+        // ******************
+        // The camera position when the user starts dragging:
+        _startCameraX,
+        _startCameraY,
+        _startCameraAngle,
+        _startCameraRatio,
+
+        // The latest stage position:
+        _lastCameraX,
+        _lastCameraY,
+        _lastCameraAngle,
+        _lastCameraRatio,
+
+        // TOUCH MANAGEMENT:
+        // *****************
+        // Touches that are down:
+        _downTouches = [],
+
+        _startTouchX0,
+        _startTouchY0,
+        _startTouchX1,
+        _startTouchY1,
+        _startTouchAngle,
+        _startTouchDistance,
+
+        _touchMode,
+
+        _isMoving,
+        _doubleTap,
+        _movingTimeoutId;
+
+    sigma.classes.dispatcher.extend(this);
+
+    sigma.utils.doubleClick(_target, 'touchstart', _doubleTapHandler);
+    _target.addEventListener('touchstart', _handleStart, false);
+    _target.addEventListener('touchend', _handleLeave, false);
+    _target.addEventListener('touchcancel', _handleLeave, false);
+    _target.addEventListener('touchleave', _handleLeave, false);
+    _target.addEventListener('touchmove', _handleMove, false);
+
+    function position(e) {
+      var offset = sigma.utils.getOffset(_target);
+
+      return {
+        x: e.pageX - offset.left,
+        y: e.pageY - offset.top
+      };
+    }
+
+
+
+
+    /**
+     * This method unbinds every handlers that makes the captor work.
+     */
+    this.kill = function() {
+      sigma.utils.unbindDoubleClick(_target, 'touchstart');
+      _target.addEventListener('touchstart', _handleStart);
+      _target.addEventListener('touchend', _handleLeave);
+      _target.addEventListener('touchcancel', _handleLeave);
+      _target.addEventListener('touchleave', _handleLeave);
+      _target.addEventListener('touchmove', _handleMove);
+    };
+
+
+
+
+    // TOUCH EVENTS:
+    // *************
+    /**
+     * The handler listening to the 'touchstart' event. It will set the touch
+     * mode ("_touchMode") and start observing the user touch moves.
+     *
+     * @param {event} e A touch event.
+     */
+    function _handleStart(e) {
+      if (_settings('touchEnabled')) {
+        var x0,
+            x1,
+            y0,
+            y1,
+            pos0,
+            pos1;
+
+        _downTouches = e.touches;
+
+        switch (_downTouches.length) {
+          case 1:
+            _camera.isMoving = true;
+            _touchMode = 1;
+
+            _startCameraX = _camera.x;
+            _startCameraY = _camera.y;
+
+            _lastCameraX = _camera.x;
+            _lastCameraY = _camera.y;
+
+            pos0 = position(_downTouches[0]);
+            _startTouchX0 = pos0.x;
+            _startTouchY0 = pos0.y;
+
+            break;
+          case 2:
+            _camera.isMoving = true;
+            _touchMode = 2;
+
+            pos0 = position(_downTouches[0]);
+            pos1 = position(_downTouches[1]);
+            x0 = pos0.x;
+            y0 = pos0.y;
+            x1 = pos1.x;
+            y1 = pos1.y;
+
+            _lastCameraX = _camera.x;
+            _lastCameraY = _camera.y;
+
+            _startCameraAngle = _camera.angle;
+            _startCameraRatio = _camera.ratio;
+
+            _startCameraX = _camera.x;
+            _startCameraY = _camera.y;
+
+            _startTouchX0 = x0;
+            _startTouchY0 = y0;
+            _startTouchX1 = x1;
+            _startTouchY1 = y1;
+
+            _startTouchAngle = Math.atan2(
+              _startTouchY1 - _startTouchY0,
+              _startTouchX1 - _startTouchX0
+            );
+            _startTouchDistance = Math.sqrt(
+              Math.pow(_startTouchY1 - _startTouchY0, 2) +
+              Math.pow(_startTouchX1 - _startTouchX0, 2)
+            );
+
+            e.preventDefault();
+            return false;
+        }
+      }
+    }
+
+    /**
+     * The handler listening to the 'touchend', 'touchcancel' and 'touchleave'
+     * event. It will update the touch mode if there are still at least one
+     * finger, and stop dragging else.
+     *
+     * @param {event} e A touch event.
+     */
+    function _handleLeave(e) {
+      if (_settings('touchEnabled')) {
+        _downTouches = e.touches;
+        var inertiaRatio = _settings('touchInertiaRatio');
+
+        if (_movingTimeoutId) {
+          _isMoving = false;
+          clearTimeout(_movingTimeoutId);
+        }
+
+        switch (_touchMode) {
+          case 2:
+            if (e.touches.length === 1) {
+              _handleStart(e);
+
+              e.preventDefault();
+              break;
+            }
+            /* falls through */
+          case 1:
+            _camera.isMoving = false;
+            _self.dispatchEvent('stopDrag');
+
+            if (_isMoving) {
+              _doubleTap = false;
+              sigma.misc.animation.camera(
+                _camera,
+                {
+                  x: _camera.x +
+                    inertiaRatio * (_camera.x - _lastCameraX),
+                  y: _camera.y +
+                    inertiaRatio * (_camera.y - _lastCameraY)
+                },
+                {
+                  easing: 'quadraticOut',
+                  duration: _settings('touchInertiaDuration')
+                }
+              );
+            }
+
+            _isMoving = false;
+            _touchMode = 0;
+            break;
+        }
+      }
+    }
+
+    /**
+     * The handler listening to the 'touchmove' event. It will effectively drag
+     * the graph, and eventually zooms and turn it if the user is using two
+     * fingers.
+     *
+     * @param {event} e A touch event.
+     */
+    function _handleMove(e) {
+      if (!_doubleTap && _settings('touchEnabled')) {
+        var x0,
+            x1,
+            y0,
+            y1,
+            cos,
+            sin,
+            end,
+            pos0,
+            pos1,
+            diff,
+            start,
+            dAngle,
+            dRatio,
+            newStageX,
+            newStageY,
+            newStageRatio,
+            newStageAngle;
+
+        _downTouches = e.touches;
+        _isMoving = true;
+
+        if (_movingTimeoutId)
+          clearTimeout(_movingTimeoutId);
+
+        _movingTimeoutId = setTimeout(function() {
+          _isMoving = false;
+        }, _settings('dragTimeout'));
+
+        switch (_touchMode) {
+          case 1:
+            pos0 = position(_downTouches[0]);
+            x0 = pos0.x;
+            y0 = pos0.y;
+
+            diff = _camera.cameraPosition(
+              x0 - _startTouchX0,
+              y0 - _startTouchY0,
+              true
+            );
+
+            newStageX = _startCameraX - diff.x;
+            newStageY = _startCameraY - diff.y;
+
+            if (newStageX !== _camera.x || newStageY !== _camera.y) {
+              _lastCameraX = _camera.x;
+              _lastCameraY = _camera.y;
+
+              _camera.goTo({
+                x: newStageX,
+                y: newStageY
+              });
+
+              _self.dispatchEvent('mousemove', {
+                x: pos0.x - sigma.utils.getWidth(e) / 2,
+                y: pos0.y - sigma.utils.getHeight(e) / 2,
+                clientX: e.clientX,
+                clientY: e.clientY,
+                ctrlKey: e.ctrlKey,
+                metaKey: e.metaKey,
+                altKey: e.altKey,
+                shiftKey: e.shiftKey
+              });
+
+              _self.dispatchEvent('drag');
+            }
+            break;
+          case 2:
+            pos0 = position(_downTouches[0]);
+            pos1 = position(_downTouches[1]);
+            x0 = pos0.x;
+            y0 = pos0.y;
+            x1 = pos1.x;
+            y1 = pos1.y;
+
+            start = _camera.cameraPosition(
+              (_startTouchX0 + _startTouchX1) / 2 -
+                sigma.utils.getWidth(e) / 2,
+              (_startTouchY0 + _startTouchY1) / 2 -
+                sigma.utils.getHeight(e) / 2,
+              true
+            );
+            end = _camera.cameraPosition(
+              (x0 + x1) / 2 - sigma.utils.getWidth(e) / 2,
+              (y0 + y1) / 2 - sigma.utils.getHeight(e) / 2,
+              true
+            );
+
+            dAngle = Math.atan2(y1 - y0, x1 - x0) - _startTouchAngle;
+            dRatio = Math.sqrt(
+              Math.pow(y1 - y0, 2) + Math.pow(x1 - x0, 2)
+            ) / _startTouchDistance;
+
+            // Translation:
+            x0 = start.x;
+            y0 = start.y;
+
+            // Homothetic transformation:
+            newStageRatio = _startCameraRatio / dRatio;
+            x0 = x0 * dRatio;
+            y0 = y0 * dRatio;
+
+            // Rotation:
+            newStageAngle = _startCameraAngle - dAngle;
+            cos = Math.cos(-dAngle);
+            sin = Math.sin(-dAngle);
+            x1 = x0 * cos + y0 * sin;
+            y1 = y0 * cos - x0 * sin;
+            x0 = x1;
+            y0 = y1;
+
+            // Finalize:
+            newStageX = x0 - end.x + _startCameraX;
+            newStageY = y0 - end.y + _startCameraY;
+
+            if (
+              newStageRatio !== _camera.ratio ||
+              newStageAngle !== _camera.angle ||
+              newStageX !== _camera.x ||
+              newStageY !== _camera.y
+            ) {
+              _lastCameraX = _camera.x;
+              _lastCameraY = _camera.y;
+              _lastCameraAngle = _camera.angle;
+              _lastCameraRatio = _camera.ratio;
+
+              _camera.goTo({
+                x: newStageX,
+                y: newStageY,
+                angle: newStageAngle,
+                ratio: newStageRatio
+              });
+
+              _self.dispatchEvent('drag');
+            }
+
+            break;
+        }
+
+        e.preventDefault();
+        return false;
+      }
+    }
+
+    /**
+     * The handler listening to the double tap custom event. It will
+     * basically zoom into the graph.
+     *
+     * @param {event} e A touch event.
+     */
+    function _doubleTapHandler(e) {
+      var pos,
+          ratio,
+          animation;
+
+      if (e.touches && e.touches.length === 1 && _settings('touchEnabled')) {
+        _doubleTap = true;
+
+        ratio = 1 / _settings('doubleClickZoomingRatio');
+
+        pos = position(e.touches[0]);
+        _self.dispatchEvent('doubleclick', {
+          x: pos.x - sigma.utils.getWidth(e) / 2,
+          y: pos.y - sigma.utils.getHeight(e) / 2,
+          clientX: e.clientX,
+          clientY: e.clientY,
+          ctrlKey: e.ctrlKey,
+          metaKey: e.metaKey,
+          altKey: e.altKey,
+          shiftKey: e.shiftKey
+        });
+
+        if (_settings('doubleClickEnabled')) {
+          pos = _camera.cameraPosition(
+            pos.x - sigma.utils.getWidth(e) / 2,
+            pos.y - sigma.utils.getHeight(e) / 2,
+            true
+          );
+
+          animation = {
+            duration: _settings('doubleClickZoomDuration'),
+            onComplete: function() {
+              _doubleTap = false;
+            }
+          };
+
+          sigma.utils.zoomTo(_camera, pos.x, pos.y, ratio, animation);
+        }
+
+        if (e.preventDefault)
+          e.preventDefault();
+        else
+          e.returnValue = false;
+
+        e.stopPropagation();
+        return false;
+      }
+    }
+  };
+}).call(this);
diff --git a/demo/static/sigmajs/classes/sigma.classes.camera.js b/demo/static/sigmajs/classes/sigma.classes.camera.js
new file mode 100644
index 000000000..f4a86b1de
--- /dev/null
+++ b/demo/static/sigmajs/classes/sigma.classes.camera.js
@@ -0,0 +1,240 @@
+;(function(undefined) {
+  'use strict';
+
+  if (typeof sigma === 'undefined')
+    throw 'sigma is not declared';
+
+  sigma.utils.pkg('sigma.classes');
+
+  /**
+   * The camera constructor. It just initializes its attributes and methods.
+   *
+   * @param  {string}       id       The id.
+   * @param  {sigma.classes.graph}  graph    The graph.
+   * @param  {configurable} settings The settings function.
+   * @param  {?object}      options  Eventually some overriding options.
+   * @return {camera}                Returns the fresh new camera instance.
+   */
+  sigma.classes.camera = function(id, graph, settings, options) {
+    sigma.classes.dispatcher.extend(this);
+
+    Object.defineProperty(this, 'graph', {
+      value: graph
+    });
+    Object.defineProperty(this, 'id', {
+      value: id
+    });
+    Object.defineProperty(this, 'readPrefix', {
+      value: 'read_cam' + id + ':'
+    });
+    Object.defineProperty(this, 'prefix', {
+      value: 'cam' + id + ':'
+    });
+
+    this.x = 0;
+    this.y = 0;
+    this.ratio = 1;
+    this.angle = 0;
+    this.isAnimated = false;
+    this.settings = (typeof options === 'object' && options) ?
+      settings.embedObject(options) :
+      settings;
+  };
+
+  /**
+   * Updates the camera position.
+   *
+   * @param  {object} coordinates The new coordinates object.
+   * @return {camera}             Returns the camera.
+   */
+  sigma.classes.camera.prototype.goTo = function(coordinates) {
+    if (!this.settings('enableCamera'))
+      return this;
+
+    var i,
+        l,
+        c = coordinates || {},
+        keys = ['x', 'y', 'ratio', 'angle'];
+
+    for (i = 0, l = keys.length; i < l; i++)
+      if (c[keys[i]] !== undefined) {
+        if (typeof c[keys[i]] === 'number' && !isNaN(c[keys[i]]))
+          this[keys[i]] = c[keys[i]];
+        else
+          throw 'Value for "' + keys[i] + '" is not a number.';
+      }
+
+    this.dispatchEvent('coordinatesUpdated');
+    return this;
+  };
+
+  /**
+   * This method takes a graph and computes for each node and edges its
+   * coordinates relatively to the center of the camera. Basically, it will
+   * compute the coordinates that will be used by the graphic renderers.
+   *
+   * Since it should be possible to use different cameras and different
+   * renderers, it is possible to specify a prefix to put before the new
+   * coordinates (to get something like "node.camera1_x")
+   *
+   * @param  {?string} read    The prefix of the coordinates to read.
+   * @param  {?string} write   The prefix of the coordinates to write.
+   * @param  {?object} options Eventually an object of options. Those can be:
+   *                           - A restricted nodes array.
+   *                           - A restricted edges array.
+   *                           - A width.
+   *                           - A height.
+   * @return {camera}        Returns the camera.
+   */
+  sigma.classes.camera.prototype.applyView = function(read, write, options) {
+    options = options || {};
+    write = write !== undefined ? write : this.prefix;
+    read = read !== undefined ? read : this.readPrefix;
+
+    var nodes = options.nodes || this.graph.nodes(),
+        edges = options.edges || this.graph.edges();
+
+    var i,
+        l,
+        node,
+        cos = Math.cos(this.angle),
+        sin = Math.sin(this.angle),
+        nodeRatio = Math.pow(this.ratio, this.settings('nodesPowRatio')),
+        edgeRatio = Math.pow(this.ratio, this.settings('edgesPowRatio'));
+
+    for (i = 0, l = nodes.length; i < l; i++) {
+      node = nodes[i];
+      node[write + 'x'] =
+        (
+          ((node[read + 'x'] || 0) - this.x) * cos +
+          ((node[read + 'y'] || 0) - this.y) * sin
+        ) / this.ratio + (options.width || 0) / 2;
+      node[write + 'y'] =
+        (
+          ((node[read + 'y'] || 0) - this.y) * cos -
+          ((node[read + 'x'] || 0) - this.x) * sin
+        ) / this.ratio + (options.height || 0) / 2;
+      node[write + 'size'] =
+        (node[read + 'size'] || 0) /
+        nodeRatio;
+    }
+
+    for (i = 0, l = edges.length; i < l; i++) {
+      edges[i][write + 'size'] =
+        (edges[i][read + 'size'] || 0) /
+        edgeRatio;
+    }
+
+    return this;
+  };
+
+  /**
+   * This function converts the coordinates of a point from the frame of the
+   * camera to the frame of the graph.
+   *
+   * @param  {number} x The X coordinate of the point in the frame of the
+   *                    camera.
+   * @param  {number} y The Y coordinate of the point in the frame of the
+   *                    camera.
+   * @return {object}   The point coordinates in the frame of the graph.
+   */
+  sigma.classes.camera.prototype.graphPosition = function(x, y, vector) {
+    var X = 0,
+        Y = 0,
+        cos = Math.cos(this.angle),
+        sin = Math.sin(this.angle);
+
+    // Revert the origin differential vector:
+    if (!vector) {
+      X = - (this.x * cos + this.y * sin) / this.ratio;
+      Y = - (this.y * cos - this.x * sin) / this.ratio;
+    }
+
+    return {
+      x: (x * cos + y * sin) / this.ratio + X,
+      y: (y * cos - x * sin) / this.ratio + Y
+    };
+  };
+
+  /**
+   * This function converts the coordinates of a point from the frame of the
+   * graph to the frame of the camera.
+   *
+   * @param  {number} x The X coordinate of the point in the frame of the
+   *                    graph.
+   * @param  {number} y The Y coordinate of the point in the frame of the
+   *                    graph.
+   * @return {object}   The point coordinates in the frame of the camera.
+   */
+  sigma.classes.camera.prototype.cameraPosition = function(x, y, vector) {
+    var X = 0,
+        Y = 0,
+        cos = Math.cos(this.angle),
+        sin = Math.sin(this.angle);
+
+    // Revert the origin differential vector:
+    if (!vector) {
+      X = - (this.x * cos + this.y * sin) / this.ratio;
+      Y = - (this.y * cos - this.x * sin) / this.ratio;
+    }
+
+    return {
+      x: ((x - X) * cos - (y - Y) * sin) * this.ratio,
+      y: ((y - Y) * cos + (x - X) * sin) * this.ratio
+    };
+  };
+
+  /**
+   * This method returns the transformation matrix of the camera. This is
+   * especially useful to apply the camera view directly in shaders, in case of
+   * WebGL rendering.
+   *
+   * @return {array} The transformation matrix.
+   */
+  sigma.classes.camera.prototype.getMatrix = function() {
+    var scale = sigma.utils.matrices.scale(1 / this.ratio),
+        rotation = sigma.utils.matrices.rotation(this.angle),
+        translation = sigma.utils.matrices.translation(-this.x, -this.y),
+        matrix = sigma.utils.matrices.multiply(
+          translation,
+          sigma.utils.matrices.multiply(
+            rotation,
+            scale
+          )
+        );
+
+    return matrix;
+  };
+
+  /**
+   * Taking a width and a height as parameters, this method returns the
+   * coordinates of the rectangle representing the camera on screen, in the
+   * graph's referentiel.
+   *
+   * To keep displaying labels of nodes going out of the screen, the method
+   * keeps a margin around the screen in the returned rectangle.
+   *
+   * @param  {number} width  The width of the screen.
+   * @param  {number} height The height of the screen.
+   * @return {object}        The rectangle as x1, y1, x2 and y2, representing
+   *                         two opposite points.
+   */
+  sigma.classes.camera.prototype.getRectangle = function(width, height) {
+    var widthVect = this.cameraPosition(width, 0, true),
+        heightVect = this.cameraPosition(0, height, true),
+        centerVect = this.cameraPosition(width / 2, height / 2, true),
+        marginX = this.cameraPosition(width / 4, 0, true).x,
+        marginY = this.cameraPosition(0, height / 4, true).y;
+
+    return {
+      x1: this.x - centerVect.x - marginX,
+      y1: this.y - centerVect.y - marginY,
+      x2: this.x - centerVect.x + marginX + widthVect.x,
+      y2: this.y - centerVect.y - marginY + widthVect.y,
+      height: Math.sqrt(
+        Math.pow(heightVect.x, 2) +
+        Math.pow(heightVect.y + 2 * marginY, 2)
+      )
+    };
+  };
+}).call(this);
diff --git a/demo/static/sigmajs/classes/sigma.classes.configurable.js b/demo/static/sigmajs/classes/sigma.classes.configurable.js
new file mode 100644
index 000000000..09ce1f796
--- /dev/null
+++ b/demo/static/sigmajs/classes/sigma.classes.configurable.js
@@ -0,0 +1,116 @@
+;(function() {
+  'use strict';
+
+  /**
+   * This utils aims to facilitate the manipulation of each instance setting.
+   * Using a function instead of an object brings two main advantages: First,
+   * it will be easier in the future to catch settings updates through a
+   * function than an object. Second, giving it a full object will "merge" it
+   * to the settings object properly, keeping us to have to always add a loop.
+   *
+   * @return {configurable} The "settings" function.
+   */
+  var configurable = function() {
+    var i,
+        l,
+        data = {},
+        datas = Array.prototype.slice.call(arguments, 0);
+
+    /**
+     * The method to use to set or get any property of this instance.
+     *
+     * @param  {string|object}    a1 If it is a string and if a2 is undefined,
+     *                               then it will return the corresponding
+     *                               property. If it is a string and if a2 is
+     *                               set, then it will set a2 as the property
+     *                               corresponding to a1, and return this. If
+     *                               it is an object, then each pair string +
+     *                               object(or any other type) will be set as a
+     *                               property.
+     * @param  {*?}               a2 The new property corresponding to a1 if a1
+     *                               is a string.
+     * @return {*|configurable}      Returns itself or the corresponding
+     *                               property.
+     *
+     * Polymorphism:
+     * *************
+     * Here are some basic use examples:
+     *
+     *  > settings = new configurable();
+     *  > settings('mySetting', 42);
+     *  > settings('mySetting'); // Logs: 42
+     *  > settings('mySetting', 123);
+     *  > settings('mySetting'); // Logs: 123
+     *  > settings({mySetting: 456});
+     *  > settings('mySetting'); // Logs: 456
+     *
+     * Also, it is possible to use the function as a fallback:
+     *  > settings({mySetting: 'abc'}, 'mySetting');  // Logs: 'abc'
+     *  > settings({hisSetting: 'abc'}, 'mySetting'); // Logs: 456
+     */
+    var settings = function(a1, a2) {
+      var o,
+          i,
+          l,
+          k;
+
+      if (arguments.length === 1 && typeof a1 === 'string') {
+        if (data[a1] !== undefined)
+          return data[a1];
+        for (i = 0, l = datas.length; i < l; i++)
+          if (datas[i][a1] !== undefined)
+            return datas[i][a1];
+        return undefined;
+      } else if (typeof a1 === 'object' && typeof a2 === 'string') {
+        return (a1 || {})[a2] !== undefined ? a1[a2] : settings(a2);
+      } else {
+        o = (typeof a1 === 'object' && a2 === undefined) ? a1 : {};
+
+        if (typeof a1 === 'string')
+          o[a1] = a2;
+
+        for (i = 0, k = Object.keys(o), l = k.length; i < l; i++)
+          data[k[i]] = o[k[i]];
+
+        return this;
+      }
+    };
+
+    /**
+     * This method returns a new configurable function, with new objects
+     *
+     * @param  {object*}  Any number of objects to search in.
+     * @return {function} Returns the function. Check its documentation to know
+     *                    more about how it works.
+     */
+    settings.embedObjects = function() {
+      var args = datas.concat(
+        data
+      ).concat(
+        Array.prototype.splice.call(arguments, 0)
+      );
+
+      return configurable.apply({}, args);
+    };
+
+    // Initialize
+    for (i = 0, l = arguments.length; i < l; i++)
+      settings(arguments[i]);
+
+    return settings;
+  };
+
+  /**
+   * EXPORT:
+   * *******
+   */
+  if (typeof this.sigma !== 'undefined') {
+    this.sigma.classes = this.sigma.classes || {};
+    this.sigma.classes.configurable = configurable;
+  } else if (typeof exports !== 'undefined') {
+    if (typeof module !== 'undefined' && module.exports)
+      exports = module.exports = configurable;
+    exports.configurable = configurable;
+  } else
+    this.configurable = configurable;
+}).call(this);
diff --git a/demo/static/sigmajs/classes/sigma.classes.dispatcher.js b/demo/static/sigmajs/classes/sigma.classes.dispatcher.js
new file mode 100644
index 000000000..0ce7dca66
--- /dev/null
+++ b/demo/static/sigmajs/classes/sigma.classes.dispatcher.js
@@ -0,0 +1,204 @@
+;(function() {
+  'use strict';
+
+  /**
+   * Dispatcher constructor.
+   *
+   * @return {dispatcher} The new dispatcher instance.
+   */
+  var dispatcher = function() {
+    Object.defineProperty(this, '_handlers', {
+      value: {}
+    });
+  };
+
+
+
+
+  /**
+   * Will execute the handler everytime that the indicated event (or the
+   * indicated events) will be triggered.
+   *
+   * @param  {string}           events  The name of the event (or the events
+   *                                    separated by spaces).
+   * @param  {function(Object)} handler The handler to bind.
+   * @return {dispatcher}               Returns the instance itself.
+   */
+  dispatcher.prototype.bind = function(events, handler) {
+    var i,
+        l,
+        event,
+        eArray;
+
+    if (
+      arguments.length === 1 &&
+      typeof arguments[0] === 'object'
+    )
+      for (events in arguments[0])
+        this.bind(events, arguments[0][events]);
+    else if (
+      arguments.length === 2 &&
+      typeof arguments[1] === 'function'
+    ) {
+      eArray = typeof events === 'string' ? events.split(' ') : events;
+
+      for (i = 0, l = eArray.length; i !== l; i += 1) {
+        event = eArray[i];
+
+        // Check that event is not '':
+        if (!event)
+          continue;
+
+        if (!this._handlers[event])
+          this._handlers[event] = [];
+
+        // Using an object instead of directly the handler will make possible
+        // later to add flags
+        this._handlers[event].push({
+          handler: handler
+        });
+      }
+    } else
+      throw 'bind: Wrong arguments.';
+
+    return this;
+  };
+
+  /**
+   * Removes the handler from a specified event (or specified events).
+   *
+   * @param  {?string}           events  The name of the event (or the events
+   *                                     separated by spaces). If undefined,
+   *                                     then all handlers are removed.
+   * @param  {?function(object)} handler The handler to unbind. If undefined,
+   *                                     each handler bound to the event or the
+   *                                     events will be removed.
+   * @return {dispatcher}                Returns the instance itself.
+   */
+  dispatcher.prototype.unbind = function(events, handler) {
+    var i,
+        n,
+        j,
+        m,
+        k,
+        a,
+        event,
+        eArray = typeof events === 'string' ? events.split(' ') : events;
+
+    if (!arguments.length) {
+      for (k in this._handlers)
+        delete this._handlers[k];
+      return this;
+    }
+
+    if (handler) {
+      for (i = 0, n = eArray.length; i !== n; i += 1) {
+        event = eArray[i];
+        if (this._handlers[event]) {
+          a = [];
+          for (j = 0, m = this._handlers[event].length; j !== m; j += 1)
+            if (this._handlers[event][j].handler !== handler)
+              a.push(this._handlers[event][j]);
+
+          this._handlers[event] = a;
+        }
+
+        if (this._handlers[event] && this._handlers[event].length === 0)
+          delete this._handlers[event];
+      }
+    } else
+      for (i = 0, n = eArray.length; i !== n; i += 1)
+        delete this._handlers[eArray[i]];
+
+    return this;
+  };
+
+  /**
+   * Executes each handler bound to the event
+   *
+   * @param  {string}     events The name of the event (or the events separated
+   *                             by spaces).
+   * @param  {?object}    data   The content of the event (optional).
+   * @return {dispatcher}        Returns the instance itself.
+   */
+  dispatcher.prototype.dispatchEvent = function(events, data) {
+    var i,
+        n,
+        j,
+        m,
+        a,
+        event,
+        eventName,
+        self = this,
+        eArray = typeof events === 'string' ? events.split(' ') : events;
+
+    data = data === undefined ? {} : data;
+
+    for (i = 0, n = eArray.length; i !== n; i += 1) {
+      eventName = eArray[i];
+
+      if (this._handlers[eventName]) {
+        event = self.getEvent(eventName, data);
+        a = [];
+
+        for (j = 0, m = this._handlers[eventName].length; j !== m; j += 1) {
+          this._handlers[eventName][j].handler(event);
+          if (!this._handlers[eventName][j].one)
+            a.push(this._handlers[eventName][j]);
+        }
+
+        this._handlers[eventName] = a;
+      }
+    }
+
+    return this;
+  };
+
+  /**
+   * Return an event object.
+   *
+   * @param  {string}  events The name of the event.
+   * @param  {?object} data   The content of the event (optional).
+   * @return {object}         Returns the instance itself.
+   */
+  dispatcher.prototype.getEvent = function(event, data) {
+    return {
+      type: event,
+      data: data || {},
+      target: this
+    };
+  };
+
+  /**
+   * A useful function to deal with inheritance. It will make the target
+   * inherit the prototype of the class dispatcher as well as its constructor.
+   *
+   * @param {object} target The target.
+   */
+  dispatcher.extend = function(target, args) {
+    var k;
+
+    for (k in dispatcher.prototype)
+      if (dispatcher.prototype.hasOwnProperty(k))
+        target[k] = dispatcher.prototype[k];
+
+    dispatcher.apply(target, args);
+  };
+
+
+
+
+  /**
+   * EXPORT:
+   * *******
+   */
+  if (typeof this.sigma !== 'undefined') {
+    this.sigma.classes = this.sigma.classes || {};
+    this.sigma.classes.dispatcher = dispatcher;
+  } else if (typeof exports !== 'undefined') {
+    if (typeof module !== 'undefined' && module.exports)
+      exports = module.exports = dispatcher;
+    exports.dispatcher = dispatcher;
+  } else
+    this.dispatcher = dispatcher;
+}).call(this);
diff --git a/demo/static/sigmajs/classes/sigma.classes.edgequad.js b/demo/static/sigmajs/classes/sigma.classes.edgequad.js
new file mode 100644
index 000000000..fbc5b7fc3
--- /dev/null
+++ b/demo/static/sigmajs/classes/sigma.classes.edgequad.js
@@ -0,0 +1,832 @@
+;(function(undefined) {
+  'use strict';
+
+  /**
+   * Sigma Quadtree Module for edges
+   * ===============================
+   *
+   * Author: Sébastien Heymann,
+   *   from the quad of Guillaume Plique (Yomguithereal)
+   * Version: 0.2
+   */
+
+
+
+  /**
+   * Quad Geometric Operations
+   * -------------------------
+   *
+   * A useful batch of geometric operations used by the quadtree.
+   */
+
+  var _geom = {
+
+    /**
+     * Transforms a graph node with x, y and size into an
+     * axis-aligned square.
+     *
+     * @param  {object} A graph node with at least a point (x, y) and a size.
+     * @return {object} A square: two points (x1, y1), (x2, y2) and height.
+     */
+    pointToSquare: function(n) {
+      return {
+        x1: n.x - n.size,
+        y1: n.y - n.size,
+        x2: n.x + n.size,
+        y2: n.y - n.size,
+        height: n.size * 2
+      };
+    },
+
+    /**
+     * Transforms a graph edge with x1, y1, x2, y2 and size into an
+     * axis-aligned square.
+     *
+     * @param  {object} A graph edge with at least two points
+     *                  (x1, y1), (x2, y2) and a size.
+     * @return {object} A square: two points (x1, y1), (x2, y2) and height.
+     */
+    lineToSquare: function(e) {
+      if (e.y1 < e.y2) {
+        // (e.x1, e.y1) on top
+        if (e.x1 < e.x2) {
+          // (e.x1, e.y1) on left
+          return {
+            x1: e.x1 - e.size,
+            y1: e.y1 - e.size,
+            x2: e.x2 + e.size,
+            y2: e.y1 - e.size,
+            height: e.y2 - e.y1 + e.size * 2
+          };
+        }
+        // (e.x1, e.y1) on right
+        return {
+          x1: e.x2 - e.size,
+          y1: e.y1 - e.size,
+          x2: e.x1 + e.size,
+          y2: e.y1 - e.size,
+          height: e.y2 - e.y1 + e.size * 2
+        };
+      }
+
+      // (e.x2, e.y2) on top
+      if (e.x1 < e.x2) {
+        // (e.x1, e.y1) on left
+        return {
+          x1: e.x1 - e.size,
+          y1: e.y2 - e.size,
+          x2: e.x2 + e.size,
+          y2: e.y2 - e.size,
+          height: e.y1 - e.y2 + e.size * 2
+        };
+      }
+      // (e.x2, e.y2) on right
+      return {
+        x1: e.x2 - e.size,
+        y1: e.y2 - e.size,
+        x2: e.x1 + e.size,
+        y2: e.y2 - e.size,
+        height: e.y1 - e.y2 + e.size * 2
+      };
+    },
+
+    /**
+     * Transforms a graph edge of type 'curve' with x1, y1, x2, y2,
+     * control point and size into an axis-aligned square.
+     *
+     * @param  {object} e  A graph edge with at least two points
+     *                     (x1, y1), (x2, y2) and a size.
+     * @param  {object} cp A control point (x,y).
+     * @return {object}    A square: two points (x1, y1), (x2, y2) and height.
+     */
+    quadraticCurveToSquare: function(e, cp) {
+      var pt = sigma.utils.getPointOnQuadraticCurve(
+        0.5,
+        e.x1,
+        e.y1,
+        e.x2,
+        e.y2,
+        cp.x,
+        cp.y
+      );
+
+      // Bounding box of the two points and the point at the middle of the
+      // curve:
+      var minX = Math.min(e.x1, e.x2, pt.x),
+          maxX = Math.max(e.x1, e.x2, pt.x),
+          minY = Math.min(e.y1, e.y2, pt.y),
+          maxY = Math.max(e.y1, e.y2, pt.y);
+
+      return {
+        x1: minX - e.size,
+        y1: minY - e.size,
+        x2: maxX + e.size,
+        y2: minY - e.size,
+        height: maxY - minY + e.size * 2
+      };
+    },
+
+    /**
+     * Transforms a graph self loop into an axis-aligned square.
+     *
+     * @param  {object} n A graph node with a point (x, y) and a size.
+     * @return {object}   A square: two points (x1, y1), (x2, y2) and height.
+     */
+    selfLoopToSquare: function(n) {
+      // Fitting to the curve is too costly, we compute a larger bounding box
+      // using the control points:
+      var cp = sigma.utils.getSelfLoopControlPoints(n.x, n.y, n.size);
+
+      // Bounding box of the point and the two control points:
+      var minX = Math.min(n.x, cp.x1, cp.x2),
+          maxX = Math.max(n.x, cp.x1, cp.x2),
+          minY = Math.min(n.y, cp.y1, cp.y2),
+          maxY = Math.max(n.y, cp.y1, cp.y2);
+
+      return {
+        x1: minX - n.size,
+        y1: minY - n.size,
+        x2: maxX + n.size,
+        y2: minY - n.size,
+        height: maxY - minY + n.size * 2
+      };
+    },
+
+    /**
+     * Checks whether a rectangle is axis-aligned.
+     *
+     * @param  {object}  A rectangle defined by two points
+     *                   (x1, y1) and (x2, y2).
+     * @return {boolean} True if the rectangle is axis-aligned.
+     */
+    isAxisAligned: function(r) {
+      return r.x1 === r.x2 || r.y1 === r.y2;
+    },
+
+    /**
+     * Compute top points of an axis-aligned rectangle. This is useful in
+     * cases when the rectangle has been rotated (left, right or bottom up) and
+     * later operations need to know the top points.
+     *
+     * @param  {object} An axis-aligned rectangle defined by two points
+     *                  (x1, y1), (x2, y2) and height.
+     * @return {object} A rectangle: two points (x1, y1), (x2, y2) and height.
+     */
+    axisAlignedTopPoints: function(r) {
+
+      // Basic
+      if (r.y1 === r.y2 && r.x1 < r.x2)
+        return r;
+
+      // Rotated to right
+      if (r.x1 === r.x2 && r.y2 > r.y1)
+        return {
+          x1: r.x1 - r.height, y1: r.y1,
+          x2: r.x1, y2: r.y1,
+          height: r.height
+        };
+
+      // Rotated to left
+      if (r.x1 === r.x2 && r.y2 < r.y1)
+        return {
+          x1: r.x1, y1: r.y2,
+          x2: r.x2 + r.height, y2: r.y2,
+          height: r.height
+        };
+
+      // Bottom's up
+      return {
+        x1: r.x2, y1: r.y1 - r.height,
+        x2: r.x1, y2: r.y1 - r.height,
+        height: r.height
+      };
+    },
+
+    /**
+     * Get coordinates of a rectangle's lower left corner from its top points.
+     *
+     * @param  {object} A rectangle defined by two points (x1, y1) and (x2, y2).
+     * @return {object} Coordinates of the corner (x, y).
+     */
+    lowerLeftCoor: function(r) {
+      var width = (
+        Math.sqrt(
+          Math.pow(r.x2 - r.x1, 2) +
+          Math.pow(r.y2 - r.y1, 2)
+        )
+      );
+
+      return {
+        x: r.x1 - (r.y2 - r.y1) * r.height / width,
+        y: r.y1 + (r.x2 - r.x1) * r.height / width
+      };
+    },
+
+    /**
+     * Get coordinates of a rectangle's lower right corner from its top points
+     * and its lower left corner.
+     *
+     * @param  {object} A rectangle defined by two points (x1, y1) and (x2, y2).
+     * @param  {object} A corner's coordinates (x, y).
+     * @return {object} Coordinates of the corner (x, y).
+     */
+    lowerRightCoor: function(r, llc) {
+      return {
+        x: llc.x - r.x1 + r.x2,
+        y: llc.y - r.y1 + r.y2
+      };
+    },
+
+    /**
+     * Get the coordinates of all the corners of a rectangle from its top point.
+     *
+     * @param  {object} A rectangle defined by two points (x1, y1) and (x2, y2).
+     * @return {array}  An array of the four corners' coordinates (x, y).
+     */
+    rectangleCorners: function(r) {
+      var llc = this.lowerLeftCoor(r),
+          lrc = this.lowerRightCoor(r, llc);
+
+      return [
+        {x: r.x1, y: r.y1},
+        {x: r.x2, y: r.y2},
+        {x: llc.x, y: llc.y},
+        {x: lrc.x, y: lrc.y}
+      ];
+    },
+
+    /**
+     * Split a square defined by its boundaries into four.
+     *
+     * @param  {object} Boundaries of the square (x, y, width, height).
+     * @return {array}  An array containing the four new squares, themselves
+     *                  defined by an array of their four corners (x, y).
+     */
+    splitSquare: function(b) {
+      return [
+        [
+          {x: b.x, y: b.y},
+          {x: b.x + b.width / 2, y: b.y},
+          {x: b.x, y: b.y + b.height / 2},
+          {x: b.x + b.width / 2, y: b.y + b.height / 2}
+        ],
+        [
+          {x: b.x + b.width / 2, y: b.y},
+          {x: b.x + b.width, y: b.y},
+          {x: b.x + b.width / 2, y: b.y + b.height / 2},
+          {x: b.x + b.width, y: b.y + b.height / 2}
+        ],
+        [
+          {x: b.x, y: b.y + b.height / 2},
+          {x: b.x + b.width / 2, y: b.y + b.height / 2},
+          {x: b.x, y: b.y + b.height},
+          {x: b.x + b.width / 2, y: b.y + b.height}
+        ],
+        [
+          {x: b.x + b.width / 2, y: b.y + b.height / 2},
+          {x: b.x + b.width, y: b.y + b.height / 2},
+          {x: b.x + b.width / 2, y: b.y + b.height},
+          {x: b.x + b.width, y: b.y + b.height}
+        ]
+      ];
+    },
+
+    /**
+     * Compute the four axis between corners of rectangle A and corners of
+     * rectangle B. This is needed later to check an eventual collision.
+     *
+     * @param  {array} An array of rectangle A's four corners (x, y).
+     * @param  {array} An array of rectangle B's four corners (x, y).
+     * @return {array} An array of four axis defined by their coordinates (x,y).
+     */
+    axis: function(c1, c2) {
+      return [
+        {x: c1[1].x - c1[0].x, y: c1[1].y - c1[0].y},
+        {x: c1[1].x - c1[3].x, y: c1[1].y - c1[3].y},
+        {x: c2[0].x - c2[2].x, y: c2[0].y - c2[2].y},
+        {x: c2[0].x - c2[1].x, y: c2[0].y - c2[1].y}
+      ];
+    },
+
+    /**
+     * Project a rectangle's corner on an axis.
+     *
+     * @param  {object} Coordinates of a corner (x, y).
+     * @param  {object} Coordinates of an axis (x, y).
+     * @return {object} The projection defined by coordinates (x, y).
+     */
+    projection: function(c, a) {
+      var l = (
+        (c.x * a.x + c.y * a.y) /
+        (Math.pow(a.x, 2) + Math.pow(a.y, 2))
+      );
+
+      return {
+        x: l * a.x,
+        y: l * a.y
+      };
+    },
+
+    /**
+     * Check whether two rectangles collide on one particular axis.
+     *
+     * @param  {object}   An axis' coordinates (x, y).
+     * @param  {array}    Rectangle A's corners.
+     * @param  {array}    Rectangle B's corners.
+     * @return {boolean}  True if the rectangles collide on the axis.
+     */
+    axisCollision: function(a, c1, c2) {
+      var sc1 = [],
+          sc2 = [];
+
+      for (var ci = 0; ci < 4; ci++) {
+        var p1 = this.projection(c1[ci], a),
+            p2 = this.projection(c2[ci], a);
+
+        sc1.push(p1.x * a.x + p1.y * a.y);
+        sc2.push(p2.x * a.x + p2.y * a.y);
+      }
+
+      var maxc1 = Math.max.apply(Math, sc1),
+          maxc2 = Math.max.apply(Math, sc2),
+          minc1 = Math.min.apply(Math, sc1),
+          minc2 = Math.min.apply(Math, sc2);
+
+      return (minc2 <= maxc1 && maxc2 >= minc1);
+    },
+
+    /**
+     * Check whether two rectangles collide on each one of their four axis. If
+     * all axis collide, then the two rectangles do collide on the plane.
+     *
+     * @param  {array}    Rectangle A's corners.
+     * @param  {array}    Rectangle B's corners.
+     * @return {boolean}  True if the rectangles collide.
+     */
+    collision: function(c1, c2) {
+      var axis = this.axis(c1, c2),
+          col = true;
+
+      for (var i = 0; i < 4; i++)
+        col = col && this.axisCollision(axis[i], c1, c2);
+
+      return col;
+    }
+  };
+
+
+  /**
+   * Quad Functions
+   * ------------
+   *
+   * The Quadtree functions themselves.
+   * For each of those functions, we consider that in a splitted quad, the
+   * index of each node is the following:
+   * 0: top left
+   * 1: top right
+   * 2: bottom left
+   * 3: bottom right
+   *
+   * Moreover, the hereafter quad's philosophy is to consider that if an element
+   * collides with more than one nodes, this element belongs to each of the
+   * nodes it collides with where other would let it lie on a higher node.
+   */
+
+  /**
+   * Get the index of the node containing the point in the quad
+   *
+   * @param  {object}  point      A point defined by coordinates (x, y).
+   * @param  {object}  quadBounds Boundaries of the quad (x, y, width, heigth).
+   * @return {integer}            The index of the node containing the point.
+   */
+  function _quadIndex(point, quadBounds) {
+    var xmp = quadBounds.x + quadBounds.width / 2,
+        ymp = quadBounds.y + quadBounds.height / 2,
+        top = (point.y < ymp),
+        left = (point.x < xmp);
+
+    if (top) {
+      if (left)
+        return 0;
+      else
+        return 1;
+    }
+    else {
+      if (left)
+        return 2;
+      else
+        return 3;
+    }
+  }
+
+  /**
+   * Get a list of indexes of nodes containing an axis-aligned rectangle
+   *
+   * @param  {object}  rectangle   A rectangle defined by two points (x1, y1),
+   *                               (x2, y2) and height.
+   * @param  {array}   quadCorners An array of the quad nodes' corners.
+   * @return {array}               An array of indexes containing one to
+   *                               four integers.
+   */
+  function _quadIndexes(rectangle, quadCorners) {
+    var indexes = [];
+
+    // Iterating through quads
+    for (var i = 0; i < 4; i++)
+      if ((rectangle.x2 >= quadCorners[i][0].x) &&
+          (rectangle.x1 <= quadCorners[i][1].x) &&
+          (rectangle.y1 + rectangle.height >= quadCorners[i][0].y) &&
+          (rectangle.y1 <= quadCorners[i][2].y))
+        indexes.push(i);
+
+    return indexes;
+  }
+
+  /**
+   * Get a list of indexes of nodes containing a non-axis-aligned rectangle
+   *
+   * @param  {array}  corners      An array containing each corner of the
+   *                               rectangle defined by its coordinates (x, y).
+   * @param  {array}  quadCorners  An array of the quad nodes' corners.
+   * @return {array}               An array of indexes containing one to
+   *                               four integers.
+   */
+  function _quadCollision(corners, quadCorners) {
+    var indexes = [];
+
+    // Iterating through quads
+    for (var i = 0; i < 4; i++)
+      if (_geom.collision(corners, quadCorners[i]))
+        indexes.push(i);
+
+    return indexes;
+  }
+
+  /**
+   * Subdivide a quad by creating a node at a precise index. The function does
+   * not generate all four nodes not to potentially create unused nodes.
+   *
+   * @param  {integer}  index The index of the node to create.
+   * @param  {object}   quad  The quad object to subdivide.
+   * @return {object}         A new quad representing the node created.
+   */
+  function _quadSubdivide(index, quad) {
+    var next = quad.level + 1,
+        subw = Math.round(quad.bounds.width / 2),
+        subh = Math.round(quad.bounds.height / 2),
+        qx = Math.round(quad.bounds.x),
+        qy = Math.round(quad.bounds.y),
+        x,
+        y;
+
+    switch (index) {
+      case 0:
+        x = qx;
+        y = qy;
+        break;
+      case 1:
+        x = qx + subw;
+        y = qy;
+        break;
+      case 2:
+        x = qx;
+        y = qy + subh;
+        break;
+      case 3:
+        x = qx + subw;
+        y = qy + subh;
+        break;
+    }
+
+    return _quadTree(
+      {x: x, y: y, width: subw, height: subh},
+      next,
+      quad.maxElements,
+      quad.maxLevel
+    );
+  }
+
+  /**
+   * Recursively insert an element into the quadtree. Only points
+   * with size, i.e. axis-aligned squares, may be inserted with this
+   * method.
+   *
+   * @param  {object}  el         The element to insert in the quadtree.
+   * @param  {object}  sizedPoint A sized point defined by two top points
+   *                              (x1, y1), (x2, y2) and height.
+   * @param  {object}  quad       The quad in which to insert the element.
+   * @return {undefined}          The function does not return anything.
+   */
+  function _quadInsert(el, sizedPoint, quad) {
+    if (quad.level < quad.maxLevel) {
+
+      // Searching appropriate quads
+      var indexes = _quadIndexes(sizedPoint, quad.corners);
+
+      // Iterating
+      for (var i = 0, l = indexes.length; i < l; i++) {
+
+        // Subdividing if necessary
+        if (quad.nodes[indexes[i]] === undefined)
+          quad.nodes[indexes[i]] = _quadSubdivide(indexes[i], quad);
+
+        // Recursion
+        _quadInsert(el, sizedPoint, quad.nodes[indexes[i]]);
+      }
+    }
+    else {
+
+      // Pushing the element in a leaf node
+      quad.elements.push(el);
+    }
+  }
+
+  /**
+   * Recursively retrieve every elements held by the node containing the
+   * searched point.
+   *
+   * @param  {object}  point The searched point (x, y).
+   * @param  {object}  quad  The searched quad.
+   * @return {array}         An array of elements contained in the relevant
+   *                         node.
+   */
+  function _quadRetrievePoint(point, quad) {
+    if (quad.level < quad.maxLevel) {
+      var index = _quadIndex(point, quad.bounds);
+
+      // If node does not exist we return an empty list
+      if (quad.nodes[index] !== undefined) {
+        return _quadRetrievePoint(point, quad.nodes[index]);
+      }
+      else {
+        return [];
+      }
+    }
+    else {
+      return quad.elements;
+    }
+  }
+
+  /**
+   * Recursively retrieve every elements contained within an rectangular area
+   * that may or may not be axis-aligned.
+   *
+   * @param  {object|array} rectData       The searched area defined either by
+   *                                       an array of four corners (x, y) in
+   *                                       the case of a non-axis-aligned
+   *                                       rectangle or an object with two top
+   *                                       points (x1, y1), (x2, y2) and height.
+   * @param  {object}       quad           The searched quad.
+   * @param  {function}     collisionFunc  The collision function used to search
+   *                                       for node indexes.
+   * @param  {array?}       els            The retrieved elements.
+   * @return {array}                       An array of elements contained in the
+   *                                       area.
+   */
+  function _quadRetrieveArea(rectData, quad, collisionFunc, els) {
+    els = els || {};
+
+    if (quad.level < quad.maxLevel) {
+      var indexes = collisionFunc(rectData, quad.corners);
+
+      for (var i = 0, l = indexes.length; i < l; i++)
+        if (quad.nodes[indexes[i]] !== undefined)
+          _quadRetrieveArea(
+            rectData,
+            quad.nodes[indexes[i]],
+            collisionFunc,
+            els
+          );
+    } else
+      for (var j = 0, m = quad.elements.length; j < m; j++)
+        if (els[quad.elements[j].id] === undefined)
+          els[quad.elements[j].id] = quad.elements[j];
+
+    return els;
+  }
+
+  /**
+   * Creates the quadtree object itself.
+   *
+   * @param  {object}   bounds       The boundaries of the quad defined by an
+   *                                 origin (x, y), width and heigth.
+   * @param  {integer}  level        The level of the quad in the tree.
+   * @param  {integer}  maxElements  The max number of element in a leaf node.
+   * @param  {integer}  maxLevel     The max recursion level of the tree.
+   * @return {object}                The quadtree object.
+   */
+  function _quadTree(bounds, level, maxElements, maxLevel) {
+    return {
+      level: level || 0,
+      bounds: bounds,
+      corners: _geom.splitSquare(bounds),
+      maxElements: maxElements || 40,
+      maxLevel: maxLevel || 8,
+      elements: [],
+      nodes: []
+    };
+  }
+
+
+  /**
+   * Sigma Quad Constructor
+   * ----------------------
+   *
+   * The edgequad API as exposed to sigma.
+   */
+
+  /**
+   * The edgequad core that will become the sigma interface with the quadtree.
+   *
+   * property {object} _tree     Property holding the quadtree object.
+   * property {object} _geom     Exposition of the _geom namespace for testing.
+   * property {object} _cache    Cache for the area method.
+   * property {boolean} _enabled Can index and retreive elements.
+   */
+  var edgequad = function() {
+    this._geom = _geom;
+    this._tree = null;
+    this._cache = {
+      query: false,
+      result: false
+    };
+    this._enabled = true;
+  };
+
+  /**
+   * Index a graph by inserting its edges into the quadtree.
+   *
+   * @param  {object} graph   A graph instance.
+   * @param  {object} params  An object of parameters with at least the quad
+   *                          bounds.
+   * @return {object}         The quadtree object.
+   *
+   * Parameters:
+   * ----------
+   * bounds:      {object}   boundaries of the quad defined by its origin (x, y)
+   *                         width and heigth.
+   * prefix:      {string?}  a prefix for edge geometric attributes.
+   * maxElements: {integer?} the max number of elements in a leaf node.
+   * maxLevel:    {integer?} the max recursion level of the tree.
+   */
+  edgequad.prototype.index = function(graph, params) {
+    if (!this._enabled)
+      return this._tree;
+
+    // Enforcing presence of boundaries
+    if (!params.bounds)
+      throw 'sigma.classes.edgequad.index: bounds information not given.';
+
+    // Prefix
+    var prefix = params.prefix || '',
+        cp,
+        source,
+        target,
+        n,
+        e;
+
+    // Building the tree
+    this._tree = _quadTree(
+      params.bounds,
+      0,
+      params.maxElements,
+      params.maxLevel
+    );
+
+    var edges = graph.edges();
+
+    // Inserting graph edges into the tree
+    for (var i = 0, l = edges.length; i < l; i++) {
+      source = graph.nodes(edges[i].source);
+      target = graph.nodes(edges[i].target);
+      e = {
+        x1: source[prefix + 'x'],
+        y1: source[prefix + 'y'],
+        x2: target[prefix + 'x'],
+        y2: target[prefix + 'y'],
+        size: edges[i][prefix + 'size'] || 0
+      };
+
+      // Inserting edge
+      if (edges[i].type === 'curve' || edges[i].type === 'curvedArrow') {
+        if (source.id === target.id) {
+          n = {
+            x: source[prefix + 'x'],
+            y: source[prefix + 'y'],
+            size: source[prefix + 'size'] || 0
+          };
+          _quadInsert(
+            edges[i],
+            _geom.selfLoopToSquare(n),
+            this._tree);
+        }
+        else {
+          cp = sigma.utils.getQuadraticControlPoint(e.x1, e.y1, e.x2, e.y2);
+          _quadInsert(
+            edges[i],
+            _geom.quadraticCurveToSquare(e, cp),
+            this._tree);
+        }
+      }
+      else {
+        _quadInsert(
+          edges[i],
+          _geom.lineToSquare(e),
+          this._tree);
+      }
+    }
+
+    // Reset cache:
+    this._cache = {
+      query: false,
+      result: false
+    };
+
+    // remove?
+    return this._tree;
+  };
+
+  /**
+   * Retrieve every graph edges held by the quadtree node containing the
+   * searched point.
+   *
+   * @param  {number} x of the point.
+   * @param  {number} y of the point.
+   * @return {array}  An array of edges retrieved.
+   */
+  edgequad.prototype.point = function(x, y) {
+    if (!this._enabled)
+      return [];
+
+    return this._tree ?
+      _quadRetrievePoint({x: x, y: y}, this._tree) || [] :
+      [];
+  };
+
+  /**
+   * Retrieve every graph edges within a rectangular area. The methods keep the
+   * last area queried in cache for optimization reason and will act differently
+   * for the same reason if the area is axis-aligned or not.
+   *
+   * @param  {object} A rectangle defined by two top points (x1, y1), (x2, y2)
+   *                  and height.
+   * @return {array}  An array of edges retrieved.
+   */
+  edgequad.prototype.area = function(rect) {
+    if (!this._enabled)
+      return [];
+
+    var serialized = JSON.stringify(rect),
+        collisionFunc,
+        rectData;
+
+    // Returning cache?
+    if (this._cache.query === serialized)
+      return this._cache.result;
+
+    // Axis aligned ?
+    if (_geom.isAxisAligned(rect)) {
+      collisionFunc = _quadIndexes;
+      rectData = _geom.axisAlignedTopPoints(rect);
+    }
+    else {
+      collisionFunc = _quadCollision;
+      rectData = _geom.rectangleCorners(rect);
+    }
+
+    // Retrieving edges
+    var edges = this._tree ?
+      _quadRetrieveArea(
+        rectData,
+        this._tree,
+        collisionFunc
+      ) :
+      [];
+
+    // Object to array
+    var edgesArray = [];
+    for (var i in edges)
+      edgesArray.push(edges[i]);
+
+    // Caching
+    this._cache.query = serialized;
+    this._cache.result = edgesArray;
+
+    return edgesArray;
+  };
+
+
+  /**
+   * EXPORT:
+   * *******
+   */
+  if (typeof this.sigma !== 'undefined') {
+    this.sigma.classes = this.sigma.classes || {};
+    this.sigma.classes.edgequad = edgequad;
+  } else if (typeof exports !== 'undefined') {
+    if (typeof module !== 'undefined' && module.exports)
+      exports = module.exports = edgequad;
+    exports.edgequad = edgequad;
+  } else
+    this.edgequad = edgequad;
+}).call(this);
diff --git a/demo/static/sigmajs/classes/sigma.classes.graph.js b/demo/static/sigmajs/classes/sigma.classes.graph.js
new file mode 100644
index 000000000..c7ebe9dd4
--- /dev/null
+++ b/demo/static/sigmajs/classes/sigma.classes.graph.js
@@ -0,0 +1,859 @@
+;(function(undefined) {
+  'use strict';
+
+  var _methods = Object.create(null),
+      _indexes = Object.create(null),
+      _initBindings = Object.create(null),
+      _methodBindings = Object.create(null),
+      _methodBeforeBindings = Object.create(null),
+      _defaultSettings = {
+        immutable: true,
+        clone: true
+      },
+      _defaultSettingsFunction = function(key) {
+        return _defaultSettings[key];
+      };
+
+  /**
+   * The graph constructor. It initializes the data and the indexes, and binds
+   * the custom indexes and methods to its own scope.
+   *
+   * Recognized parameters:
+   * **********************
+   * Here is the exhaustive list of every accepted parameters in the settings
+   * object:
+   *
+   *   {boolean} clone     Indicates if the data have to be cloned in methods
+   *                       to add nodes or edges.
+   *   {boolean} immutable Indicates if nodes "id" values and edges "id",
+   *                       "source" and "target" values must be set as
+   *                       immutable.
+   *
+   * @param  {?configurable} settings Eventually a settings function.
+   * @return {graph}                  The new graph instance.
+   */
+  var graph = function(settings) {
+    var k,
+        fn,
+        data;
+
+    /**
+     * DATA:
+     * *****
+     * Every data that is callable from graph methods are stored in this "data"
+     * object. This object will be served as context for all these methods,
+     * and it is possible to add other type of data in it.
+     */
+    data = {
+      /**
+       * SETTINGS FUNCTION:
+       * ******************
+       */
+      settings: settings || _defaultSettingsFunction,
+
+      /**
+       * MAIN DATA:
+       * **********
+       */
+      nodesArray: [],
+      edgesArray: [],
+
+      /**
+       * GLOBAL INDEXES:
+       * ***************
+       * These indexes just index data by ids.
+       */
+      nodesIndex: Object.create(null),
+      edgesIndex: Object.create(null),
+
+      /**
+       * LOCAL INDEXES:
+       * **************
+       * These indexes refer from node to nodes. Each key is an id, and each
+       * value is the array of the ids of related nodes.
+       */
+      inNeighborsIndex: Object.create(null),
+      outNeighborsIndex: Object.create(null),
+      allNeighborsIndex: Object.create(null),
+
+      inNeighborsCount: Object.create(null),
+      outNeighborsCount: Object.create(null),
+      allNeighborsCount: Object.create(null)
+    };
+
+    // Execute bindings:
+    for (k in _initBindings)
+      _initBindings[k].call(data);
+
+    // Add methods to both the scope and the data objects:
+    for (k in _methods) {
+      fn = __bindGraphMethod(k, data, _methods[k]);
+      this[k] = fn;
+      data[k] = fn;
+    }
+  };
+
+
+
+
+  /**
+   * A custom tool to bind methods such that function that are bound to it will
+   * be executed anytime the method is called.
+   *
+   * @param  {string}   methodName The name of the method to bind.
+   * @param  {object}   scope      The scope where the method must be executed.
+   * @param  {function} fn         The method itself.
+   * @return {function}            The new method.
+   */
+  function __bindGraphMethod(methodName, scope, fn) {
+    var result = function() {
+      var k,
+          res;
+
+      // Execute "before" bound functions:
+      for (k in _methodBeforeBindings[methodName])
+        _methodBeforeBindings[methodName][k].apply(scope, arguments);
+
+      // Apply the method:
+      res = fn.apply(scope, arguments);
+
+      // Execute bound functions:
+      for (k in _methodBindings[methodName])
+        _methodBindings[methodName][k].apply(scope, arguments);
+
+      // Return res:
+      return res;
+    };
+
+    return result;
+  }
+
+  /**
+   * This custom tool function removes every pair key/value from an hash. The
+   * goal is to avoid creating a new object while some other references are
+   * still hanging in some scopes...
+   *
+   * @param  {object} obj The object to empty.
+   * @return {object}     The empty object.
+   */
+  function __emptyObject(obj) {
+    var k;
+
+    for (k in obj)
+      if (!('hasOwnProperty' in obj) || obj.hasOwnProperty(k))
+        delete obj[k];
+
+    return obj;
+  }
+
+
+
+
+  /**
+   * This global method adds a method that will be bound to the futurly created
+   * graph instances.
+   *
+   * Since these methods will be bound to their scope when the instances are
+   * created, it does not use the prototype. Because of that, methods have to
+   * be added before instances are created to make them available.
+   *
+   * Here is an example:
+   *
+   *  > graph.addMethod('getNodesCount', function() {
+   *  >   return this.nodesArray.length;
+   *  > });
+   *  >
+   *  > var myGraph = new graph();
+   *  > console.log(myGraph.getNodesCount()); // outputs 0
+   *
+   * @param  {string}   methodName The name of the method.
+   * @param  {function} fn         The method itself.
+   * @return {object}              The global graph constructor.
+   */
+  graph.addMethod = function(methodName, fn) {
+    if (
+      typeof methodName !== 'string' ||
+      typeof fn !== 'function' ||
+      arguments.length !== 2
+    )
+      throw 'addMethod: Wrong arguments.';
+
+    if (_methods[methodName] || graph[methodName])
+      throw 'The method "' + methodName + '" already exists.';
+
+    _methods[methodName] = fn;
+    _methodBindings[methodName] = Object.create(null);
+    _methodBeforeBindings[methodName] = Object.create(null);
+
+    return this;
+  };
+
+  /**
+   * This global method returns true if the method has already been added, and
+   * false else.
+   *
+   * Here are some examples:
+   *
+   *  > graph.hasMethod('addNode'); // returns true
+   *  > graph.hasMethod('hasMethod'); // returns true
+   *  > graph.hasMethod('unexistingMethod'); // returns false
+   *
+   * @param  {string}  methodName The name of the method.
+   * @return {boolean}            The result.
+   */
+  graph.hasMethod = function(methodName) {
+    return !!(_methods[methodName] || graph[methodName]);
+  };
+
+  /**
+   * This global methods attaches a function to a method. Anytime the specified
+   * method is called, the attached function is called right after, with the
+   * same arguments and in the same scope. The attached function is called
+   * right before if the last argument is true, unless the method is the graph
+   * constructor.
+   *
+   * To attach a function to the graph constructor, use 'constructor' as the
+   * method name (first argument).
+   *
+   * The main idea is to have a clean way to keep custom indexes up to date,
+   * for instance:
+   *
+   *  > var timesAddNodeCalled = 0;
+   *  > graph.attach('addNode', 'timesAddNodeCalledInc', function() {
+   *  >   timesAddNodeCalled++;
+   *  > });
+   *  >
+   *  > var myGraph = new graph();
+   *  > console.log(timesAddNodeCalled); // outputs 0
+   *  >
+   *  > myGraph.addNode({ id: '1' }).addNode({ id: '2' });
+   *  > console.log(timesAddNodeCalled); // outputs 2
+   *
+   * The idea for calling a function before is to provide pre-processors, for
+   * instance:
+   *
+   *  > var colorPalette = { Person: '#C3CBE1', Place: '#9BDEBD' };
+   *  > graph.attach('addNode', 'applyNodeColorPalette', function(n) {
+   *  >   n.color = colorPalette[n.category];
+   *  > }, true);
+   *  >
+   *  > var myGraph = new graph();
+   *  > myGraph.addNode({ id: 'n0', category: 'Person' });
+   *  > console.log(myGraph.nodes('n0').color); // outputs '#C3CBE1'
+   *
+   * @param  {string}   methodName The name of the related method or
+   *                               "constructor".
+   * @param  {string}   key        The key to identify the function to attach.
+   * @param  {function} fn         The function to bind.
+   * @param  {boolean}  before     If true the function is called right before.
+   * @return {object}              The global graph constructor.
+   */
+  graph.attach = function(methodName, key, fn, before) {
+    if (
+      typeof methodName !== 'string' ||
+      typeof key !== 'string' ||
+      typeof fn !== 'function' ||
+      arguments.length < 3 ||
+      arguments.length > 4
+    )
+      throw 'attach: Wrong arguments.';
+
+    var bindings;
+
+    if (methodName === 'constructor')
+      bindings = _initBindings;
+    else {
+      if (before) {
+        if (!_methodBeforeBindings[methodName])
+        throw 'The method "' + methodName + '" does not exist.';
+
+        bindings = _methodBeforeBindings[methodName];
+      }
+      else {
+        if (!_methodBindings[methodName])
+          throw 'The method "' + methodName + '" does not exist.';
+
+        bindings = _methodBindings[methodName];
+      }
+    }
+
+    if (bindings[key])
+      throw 'A function "' + key + '" is already attached ' +
+            'to the method "' + methodName + '".';
+
+    bindings[key] = fn;
+
+    return this;
+  };
+
+  /**
+   * Alias of attach(methodName, key, fn, true).
+   */
+  graph.attachBefore = function(methodName, key, fn) {
+    return this.attach(methodName, key, fn, true);
+  };
+
+  /**
+   * This methods is just an helper to deal with custom indexes. It takes as
+   * arguments the name of the index and an object containing all the different
+   * functions to bind to the methods.
+   *
+   * Here is a basic example, that creates an index to keep the number of nodes
+   * in the current graph. It also adds a method to provide a getter on that
+   * new index:
+   *
+   *  > sigma.classes.graph.addIndex('nodesCount', {
+   *  >   constructor: function() {
+   *  >     this.nodesCount = 0;
+   *  >   },
+   *  >   addNode: function() {
+   *  >     this.nodesCount++;
+   *  >   },
+   *  >   dropNode: function() {
+   *  >     this.nodesCount--;
+   *  >   }
+   *  > });
+   *  >
+   *  > sigma.classes.graph.addMethod('getNodesCount', function() {
+   *  >   return this.nodesCount;
+   *  > });
+   *  >
+   *  > var myGraph = new sigma.classes.graph();
+   *  > console.log(myGraph.getNodesCount()); // outputs 0
+   *  >
+   *  > myGraph.addNode({ id: '1' }).addNode({ id: '2' });
+   *  > console.log(myGraph.getNodesCount()); // outputs 2
+   *
+   * @param  {string} name     The name of the index.
+   * @param  {object} bindings The object containing the functions to bind.
+   * @return {object}          The global graph constructor.
+   */
+  graph.addIndex = function(name, bindings) {
+    if (
+      typeof name !== 'string' ||
+      Object(bindings) !== bindings ||
+      arguments.length !== 2
+    )
+      throw 'addIndex: Wrong arguments.';
+
+    if (_indexes[name])
+      throw 'The index "' + name + '" already exists.';
+
+    var k;
+
+    // Store the bindings:
+    _indexes[name] = bindings;
+
+    // Attach the bindings:
+    for (k in bindings)
+      if (typeof bindings[k] !== 'function')
+        throw 'The bindings must be functions.';
+      else
+        graph.attach(k, name, bindings[k]);
+
+    return this;
+  };
+
+
+
+
+  /**
+   * This method adds a node to the graph. The node must be an object, with a
+   * string under the key "id". Except for this, it is possible to add any
+   * other attribute, that will be preserved all along the manipulations.
+   *
+   * If the graph option "clone" has a truthy value, the node will be cloned
+   * when added to the graph. Also, if the graph option "immutable" has a
+   * truthy value, its id will be defined as immutable.
+   *
+   * @param  {object} node The node to add.
+   * @return {object}      The graph instance.
+   */
+  graph.addMethod('addNode', function(node) {
+    // Check that the node is an object and has an id:
+    if (Object(node) !== node || arguments.length !== 1)
+      throw 'addNode: Wrong arguments.';
+
+    if (typeof node.id !== 'string' && typeof node.id !== 'number')
+      throw 'The node must have a string or number id.';
+
+    if (this.nodesIndex[node.id])
+      throw 'The node "' + node.id + '" already exists.';
+
+    var k,
+        id = node.id,
+        validNode = Object.create(null);
+
+    // Check the "clone" option:
+    if (this.settings('clone')) {
+      for (k in node)
+        if (k !== 'id')
+          validNode[k] = node[k];
+    } else
+      validNode = node;
+
+    // Check the "immutable" option:
+    if (this.settings('immutable'))
+      Object.defineProperty(validNode, 'id', {
+        value: id,
+        enumerable: true
+      });
+    else
+      validNode.id = id;
+
+    // Add empty containers for edges indexes:
+    this.inNeighborsIndex[id] = Object.create(null);
+    this.outNeighborsIndex[id] = Object.create(null);
+    this.allNeighborsIndex[id] = Object.create(null);
+
+    this.inNeighborsCount[id] = 0;
+    this.outNeighborsCount[id] = 0;
+    this.allNeighborsCount[id] = 0;
+
+    // Add the node to indexes:
+    this.nodesArray.push(validNode);
+    this.nodesIndex[validNode.id] = validNode;
+
+    // Return the current instance:
+    return this;
+  });
+
+  /**
+   * This method adds an edge to the graph. The edge must be an object, with a
+   * string under the key "id", and strings under the keys "source" and
+   * "target" that design existing nodes. Except for this, it is possible to
+   * add any other attribute, that will be preserved all along the
+   * manipulations.
+   *
+   * If the graph option "clone" has a truthy value, the edge will be cloned
+   * when added to the graph. Also, if the graph option "immutable" has a
+   * truthy value, its id, source and target will be defined as immutable.
+   *
+   * @param  {object} edge The edge to add.
+   * @return {object}      The graph instance.
+   */
+  graph.addMethod('addEdge', function(edge) {
+    // Check that the edge is an object and has an id:
+    if (Object(edge) !== edge || arguments.length !== 1)
+      throw 'addEdge: Wrong arguments.';
+
+    if (typeof edge.id !== 'string' && typeof edge.id !== 'number')
+      throw 'The edge must have a string or number id.';
+
+    if ((typeof edge.source !== 'string' && typeof edge.source !== 'number') ||
+        !this.nodesIndex[edge.source])
+      throw 'The edge source must have an existing node id.';
+
+    if ((typeof edge.target !== 'string' && typeof edge.target !== 'number') ||
+        !this.nodesIndex[edge.target])
+      throw 'The edge target must have an existing node id.';
+
+    if (this.edgesIndex[edge.id])
+      throw 'The edge "' + edge.id + '" already exists.';
+
+    var k,
+        validEdge = Object.create(null);
+
+    // Check the "clone" option:
+    if (this.settings('clone')) {
+      for (k in edge)
+        if (k !== 'id' && k !== 'source' && k !== 'target')
+          validEdge[k] = edge[k];
+    } else
+      validEdge = edge;
+
+    // Check the "immutable" option:
+    if (this.settings('immutable')) {
+      Object.defineProperty(validEdge, 'id', {
+        value: edge.id,
+        enumerable: true
+      });
+
+      Object.defineProperty(validEdge, 'source', {
+        value: edge.source,
+        enumerable: true
+      });
+
+      Object.defineProperty(validEdge, 'target', {
+        value: edge.target,
+        enumerable: true
+      });
+    } else {
+      validEdge.id = edge.id;
+      validEdge.source = edge.source;
+      validEdge.target = edge.target;
+    }
+
+    // Add the edge to indexes:
+    this.edgesArray.push(validEdge);
+    this.edgesIndex[validEdge.id] = validEdge;
+
+    if (!this.inNeighborsIndex[validEdge.target][validEdge.source])
+      this.inNeighborsIndex[validEdge.target][validEdge.source] =
+        Object.create(null);
+    this.inNeighborsIndex[validEdge.target][validEdge.source][validEdge.id] =
+      validEdge;
+
+    if (!this.outNeighborsIndex[validEdge.source][validEdge.target])
+      this.outNeighborsIndex[validEdge.source][validEdge.target] =
+        Object.create(null);
+    this.outNeighborsIndex[validEdge.source][validEdge.target][validEdge.id] =
+      validEdge;
+
+    if (!this.allNeighborsIndex[validEdge.source][validEdge.target])
+      this.allNeighborsIndex[validEdge.source][validEdge.target] =
+        Object.create(null);
+    this.allNeighborsIndex[validEdge.source][validEdge.target][validEdge.id] =
+      validEdge;
+
+    if (validEdge.target !== validEdge.source) {
+      if (!this.allNeighborsIndex[validEdge.target][validEdge.source])
+        this.allNeighborsIndex[validEdge.target][validEdge.source] =
+          Object.create(null);
+      this.allNeighborsIndex[validEdge.target][validEdge.source][validEdge.id] =
+        validEdge;
+    }
+
+    // Keep counts up to date:
+    this.inNeighborsCount[validEdge.target]++;
+    this.outNeighborsCount[validEdge.source]++;
+    this.allNeighborsCount[validEdge.target]++;
+    this.allNeighborsCount[validEdge.source]++;
+
+    return this;
+  });
+
+  /**
+   * This method drops a node from the graph. It also removes each edge that is
+   * bound to it, through the dropEdge method. An error is thrown if the node
+   * does not exist.
+   *
+   * @param  {string} id The node id.
+   * @return {object}    The graph instance.
+   */
+  graph.addMethod('dropNode', function(id) {
+    // Check that the arguments are valid:
+    if ((typeof id !== 'string' && typeof id !== 'number') ||
+        arguments.length !== 1)
+      throw 'dropNode: Wrong arguments.';
+
+    if (!this.nodesIndex[id])
+      throw 'The node "' + id + '" does not exist.';
+
+    var i, k, l;
+
+    // Remove the node from indexes:
+    delete this.nodesIndex[id];
+    for (i = 0, l = this.nodesArray.length; i < l; i++)
+      if (this.nodesArray[i].id === id) {
+        this.nodesArray.splice(i, 1);
+        break;
+      }
+
+    // Remove related edges:
+    for (i = this.edgesArray.length - 1; i >= 0; i--)
+      if (this.edgesArray[i].source === id || this.edgesArray[i].target === id)
+        this.dropEdge(this.edgesArray[i].id);
+
+    // Remove related edge indexes:
+    delete this.inNeighborsIndex[id];
+    delete this.outNeighborsIndex[id];
+    delete this.allNeighborsIndex[id];
+
+    delete this.inNeighborsCount[id];
+    delete this.outNeighborsCount[id];
+    delete this.allNeighborsCount[id];
+
+    for (k in this.nodesIndex) {
+      delete this.inNeighborsIndex[k][id];
+      delete this.outNeighborsIndex[k][id];
+      delete this.allNeighborsIndex[k][id];
+    }
+
+    return this;
+  });
+
+  /**
+   * This method drops an edge from the graph. An error is thrown if the edge
+   * does not exist.
+   *
+   * @param  {string} id The edge id.
+   * @return {object}    The graph instance.
+   */
+  graph.addMethod('dropEdge', function(id) {
+    // Check that the arguments are valid:
+    if ((typeof id !== 'string' && typeof id !== 'number') ||
+        arguments.length !== 1)
+      throw 'dropEdge: Wrong arguments.';
+
+    if (!this.edgesIndex[id])
+      throw 'The edge "' + id + '" does not exist.';
+
+    var i, l, edge;
+
+    // Remove the edge from indexes:
+    edge = this.edgesIndex[id];
+    delete this.edgesIndex[id];
+    for (i = 0, l = this.edgesArray.length; i < l; i++)
+      if (this.edgesArray[i].id === id) {
+        this.edgesArray.splice(i, 1);
+        break;
+      }
+
+    delete this.inNeighborsIndex[edge.target][edge.source][edge.id];
+    if (!Object.keys(this.inNeighborsIndex[edge.target][edge.source]).length)
+      delete this.inNeighborsIndex[edge.target][edge.source];
+
+    delete this.outNeighborsIndex[edge.source][edge.target][edge.id];
+    if (!Object.keys(this.outNeighborsIndex[edge.source][edge.target]).length)
+      delete this.outNeighborsIndex[edge.source][edge.target];
+
+    delete this.allNeighborsIndex[edge.source][edge.target][edge.id];
+    if (!Object.keys(this.allNeighborsIndex[edge.source][edge.target]).length)
+      delete this.allNeighborsIndex[edge.source][edge.target];
+
+    if (edge.target !== edge.source) {
+      delete this.allNeighborsIndex[edge.target][edge.source][edge.id];
+      if (!Object.keys(this.allNeighborsIndex[edge.target][edge.source]).length)
+        delete this.allNeighborsIndex[edge.target][edge.source];
+    }
+
+    this.inNeighborsCount[edge.target]--;
+    this.outNeighborsCount[edge.source]--;
+    this.allNeighborsCount[edge.source]--;
+    this.allNeighborsCount[edge.target]--;
+
+    return this;
+  });
+
+  /**
+   * This method destroys the current instance. It basically empties each index
+   * and methods attached to the graph.
+   */
+  graph.addMethod('kill', function() {
+    // Delete arrays:
+    this.nodesArray.length = 0;
+    this.edgesArray.length = 0;
+    delete this.nodesArray;
+    delete this.edgesArray;
+
+    // Delete indexes:
+    delete this.nodesIndex;
+    delete this.edgesIndex;
+    delete this.inNeighborsIndex;
+    delete this.outNeighborsIndex;
+    delete this.allNeighborsIndex;
+    delete this.inNeighborsCount;
+    delete this.outNeighborsCount;
+    delete this.allNeighborsCount;
+  });
+
+  /**
+   * This method empties the nodes and edges arrays, as well as the different
+   * indexes.
+   *
+   * @return {object} The graph instance.
+   */
+  graph.addMethod('clear', function() {
+    this.nodesArray.length = 0;
+    this.edgesArray.length = 0;
+
+    // Due to GC issues, I prefer not to create new object. These objects are
+    // only available from the methods and attached functions, but still, it is
+    // better to prevent ghost references to unrelevant data...
+    __emptyObject(this.nodesIndex);
+    __emptyObject(this.edgesIndex);
+    __emptyObject(this.nodesIndex);
+    __emptyObject(this.inNeighborsIndex);
+    __emptyObject(this.outNeighborsIndex);
+    __emptyObject(this.allNeighborsIndex);
+    __emptyObject(this.inNeighborsCount);
+    __emptyObject(this.outNeighborsCount);
+    __emptyObject(this.allNeighborsCount);
+
+    return this;
+  });
+
+  /**
+   * This method reads an object and adds the nodes and edges, through the
+   * proper methods "addNode" and "addEdge".
+   *
+   * Here is an example:
+   *
+   *  > var myGraph = new graph();
+   *  > myGraph.read({
+   *  >   nodes: [
+   *  >     { id: 'n0' },
+   *  >     { id: 'n1' }
+   *  >   ],
+   *  >   edges: [
+   *  >     {
+   *  >       id: 'e0',
+   *  >       source: 'n0',
+   *  >       target: 'n1'
+   *  >     }
+   *  >   ]
+   *  > });
+   *  >
+   *  > console.log(
+   *  >   myGraph.nodes().length,
+   *  >   myGraph.edges().length
+   *  > ); // outputs 2 1
+   *
+   * @param  {object} g The graph object.
+   * @return {object}   The graph instance.
+   */
+  graph.addMethod('read', function(g) {
+    var i,
+        a,
+        l;
+
+    a = g.nodes || [];
+    for (i = 0, l = a.length; i < l; i++)
+      this.addNode(a[i]);
+
+    a = g.edges || [];
+    for (i = 0, l = a.length; i < l; i++)
+      this.addEdge(a[i]);
+
+    return this;
+  });
+
+  /**
+   * This methods returns one or several nodes, depending on how it is called.
+   *
+   * To get the array of nodes, call "nodes" without argument. To get a
+   * specific node, call it with the id of the node. The get multiple node,
+   * call it with an array of ids, and it will return the array of nodes, in
+   * the same order.
+   *
+   * @param  {?(string|array)} v Eventually one id, an array of ids.
+   * @return {object|array}      The related node or array of nodes.
+   */
+  graph.addMethod('nodes', function(v) {
+    // Clone the array of nodes and return it:
+    if (!arguments.length)
+      return this.nodesArray.slice(0);
+
+    // Return the related node:
+    if (arguments.length === 1 &&
+        (typeof v === 'string' || typeof v === 'number'))
+      return this.nodesIndex[v];
+
+    // Return an array of the related node:
+    if (
+      arguments.length === 1 &&
+      Object.prototype.toString.call(v) === '[object Array]'
+    ) {
+      var i,
+          l,
+          a = [];
+
+      for (i = 0, l = v.length; i < l; i++)
+        if (typeof v[i] === 'string' || typeof v[i] === 'number')
+          a.push(this.nodesIndex[v[i]]);
+        else
+          throw 'nodes: Wrong arguments.';
+
+      return a;
+    }
+
+    throw 'nodes: Wrong arguments.';
+  });
+
+  /**
+   * This methods returns the degree of one or several nodes, depending on how
+   * it is called. It is also possible to get incoming or outcoming degrees
+   * instead by specifying 'in' or 'out' as a second argument.
+   *
+   * @param  {string|array} v     One id, an array of ids.
+   * @param  {?string}      which Which degree is required. Values are 'in',
+   *                              'out', and by default the normal degree.
+   * @return {number|array}       The related degree or array of degrees.
+   */
+  graph.addMethod('degree', function(v, which) {
+    // Check which degree is required:
+    which = {
+      'in': this.inNeighborsCount,
+      'out': this.outNeighborsCount
+    }[which || ''] || this.allNeighborsCount;
+
+    // Return the related node:
+    if (typeof v === 'string' || typeof v === 'number')
+      return which[v];
+
+    // Return an array of the related node:
+    if (Object.prototype.toString.call(v) === '[object Array]') {
+      var i,
+          l,
+          a = [];
+
+      for (i = 0, l = v.length; i < l; i++)
+        if (typeof v[i] === 'string' || typeof v[i] === 'number')
+          a.push(which[v[i]]);
+        else
+          throw 'degree: Wrong arguments.';
+
+      return a;
+    }
+
+    throw 'degree: Wrong arguments.';
+  });
+
+  /**
+   * This methods returns one or several edges, depending on how it is called.
+   *
+   * To get the array of edges, call "edges" without argument. To get a
+   * specific edge, call it with the id of the edge. The get multiple edge,
+   * call it with an array of ids, and it will return the array of edges, in
+   * the same order.
+   *
+   * @param  {?(string|array)} v Eventually one id, an array of ids.
+   * @return {object|array}      The related edge or array of edges.
+   */
+  graph.addMethod('edges', function(v) {
+    // Clone the array of edges and return it:
+    if (!arguments.length)
+      return this.edgesArray.slice(0);
+
+    // Return the related edge:
+    if (arguments.length === 1 &&
+        (typeof v === 'string' || typeof v === 'number'))
+      return this.edgesIndex[v];
+
+    // Return an array of the related edge:
+    if (
+      arguments.length === 1 &&
+      Object.prototype.toString.call(v) === '[object Array]'
+    ) {
+      var i,
+          l,
+          a = [];
+
+      for (i = 0, l = v.length; i < l; i++)
+        if (typeof v[i] === 'string' || typeof v[i] === 'number')
+          a.push(this.edgesIndex[v[i]]);
+        else
+          throw 'edges: Wrong arguments.';
+
+      return a;
+    }
+
+    throw 'edges: Wrong arguments.';
+  });
+
+
+  /**
+   * EXPORT:
+   * *******
+   */
+  if (typeof sigma !== 'undefined') {
+    sigma.classes = sigma.classes || Object.create(null);
+    sigma.classes.graph = graph;
+  } else if (typeof exports !== 'undefined') {
+    if (typeof module !== 'undefined' && module.exports)
+      exports = module.exports = graph;
+    exports.graph = graph;
+  } else
+    this.graph = graph;
+}).call(this);
diff --git a/demo/static/sigmajs/classes/sigma.classes.quad.js b/demo/static/sigmajs/classes/sigma.classes.quad.js
new file mode 100644
index 000000000..fb1138636
--- /dev/null
+++ b/demo/static/sigmajs/classes/sigma.classes.quad.js
@@ -0,0 +1,674 @@
+;(function(undefined) {
+  'use strict';
+
+  /**
+   * Sigma Quadtree Module
+   * =====================
+   *
+   * Author: Guillaume Plique (Yomguithereal)
+   * Version: 0.2
+   */
+
+
+
+  /**
+   * Quad Geometric Operations
+   * -------------------------
+   *
+   * A useful batch of geometric operations used by the quadtree.
+   */
+
+  var _geom = {
+
+    /**
+     * Transforms a graph node with x, y and size into an
+     * axis-aligned square.
+     *
+     * @param  {object} A graph node with at least a point (x, y) and a size.
+     * @return {object} A square: two points (x1, y1), (x2, y2) and height.
+     */
+    pointToSquare: function(n) {
+      return {
+        x1: n.x - n.size,
+        y1: n.y - n.size,
+        x2: n.x + n.size,
+        y2: n.y - n.size,
+        height: n.size * 2
+      };
+    },
+
+    /**
+     * Checks whether a rectangle is axis-aligned.
+     *
+     * @param  {object}  A rectangle defined by two points
+     *                   (x1, y1) and (x2, y2).
+     * @return {boolean} True if the rectangle is axis-aligned.
+     */
+    isAxisAligned: function(r) {
+      return r.x1 === r.x2 || r.y1 === r.y2;
+    },
+
+    /**
+     * Compute top points of an axis-aligned rectangle. This is useful in
+     * cases when the rectangle has been rotated (left, right or bottom up) and
+     * later operations need to know the top points.
+     *
+     * @param  {object} An axis-aligned rectangle defined by two points
+     *                  (x1, y1), (x2, y2) and height.
+     * @return {object} A rectangle: two points (x1, y1), (x2, y2) and height.
+     */
+    axisAlignedTopPoints: function(r) {
+
+      // Basic
+      if (r.y1 === r.y2 && r.x1 < r.x2)
+        return r;
+
+      // Rotated to right
+      if (r.x1 === r.x2 && r.y2 > r.y1)
+        return {
+          x1: r.x1 - r.height, y1: r.y1,
+          x2: r.x1, y2: r.y1,
+          height: r.height
+        };
+
+      // Rotated to left
+      if (r.x1 === r.x2 && r.y2 < r.y1)
+        return {
+          x1: r.x1, y1: r.y2,
+          x2: r.x2 + r.height, y2: r.y2,
+          height: r.height
+        };
+
+      // Bottom's up
+      return {
+        x1: r.x2, y1: r.y1 - r.height,
+        x2: r.x1, y2: r.y1 - r.height,
+        height: r.height
+      };
+    },
+
+    /**
+     * Get coordinates of a rectangle's lower left corner from its top points.
+     *
+     * @param  {object} A rectangle defined by two points (x1, y1) and (x2, y2).
+     * @return {object} Coordinates of the corner (x, y).
+     */
+    lowerLeftCoor: function(r) {
+      var width = (
+        Math.sqrt(
+          Math.pow(r.x2 - r.x1, 2) +
+          Math.pow(r.y2 - r.y1, 2)
+        )
+      );
+
+      return {
+        x: r.x1 - (r.y2 - r.y1) * r.height / width,
+        y: r.y1 + (r.x2 - r.x1) * r.height / width
+      };
+    },
+
+    /**
+     * Get coordinates of a rectangle's lower right corner from its top points
+     * and its lower left corner.
+     *
+     * @param  {object} A rectangle defined by two points (x1, y1) and (x2, y2).
+     * @param  {object} A corner's coordinates (x, y).
+     * @return {object} Coordinates of the corner (x, y).
+     */
+    lowerRightCoor: function(r, llc) {
+      return {
+        x: llc.x - r.x1 + r.x2,
+        y: llc.y - r.y1 + r.y2
+      };
+    },
+
+    /**
+     * Get the coordinates of all the corners of a rectangle from its top point.
+     *
+     * @param  {object} A rectangle defined by two points (x1, y1) and (x2, y2).
+     * @return {array}  An array of the four corners' coordinates (x, y).
+     */
+    rectangleCorners: function(r) {
+      var llc = this.lowerLeftCoor(r),
+          lrc = this.lowerRightCoor(r, llc);
+
+      return [
+        {x: r.x1, y: r.y1},
+        {x: r.x2, y: r.y2},
+        {x: llc.x, y: llc.y},
+        {x: lrc.x, y: lrc.y}
+      ];
+    },
+
+    /**
+     * Split a square defined by its boundaries into four.
+     *
+     * @param  {object} Boundaries of the square (x, y, width, height).
+     * @return {array}  An array containing the four new squares, themselves
+     *                  defined by an array of their four corners (x, y).
+     */
+    splitSquare: function(b) {
+      return [
+        [
+          {x: b.x, y: b.y},
+          {x: b.x + b.width / 2, y: b.y},
+          {x: b.x, y: b.y + b.height / 2},
+          {x: b.x + b.width / 2, y: b.y + b.height / 2}
+        ],
+        [
+          {x: b.x + b.width / 2, y: b.y},
+          {x: b.x + b.width, y: b.y},
+          {x: b.x + b.width / 2, y: b.y + b.height / 2},
+          {x: b.x + b.width, y: b.y + b.height / 2}
+        ],
+        [
+          {x: b.x, y: b.y + b.height / 2},
+          {x: b.x + b.width / 2, y: b.y + b.height / 2},
+          {x: b.x, y: b.y + b.height},
+          {x: b.x + b.width / 2, y: b.y + b.height}
+        ],
+        [
+          {x: b.x + b.width / 2, y: b.y + b.height / 2},
+          {x: b.x + b.width, y: b.y + b.height / 2},
+          {x: b.x + b.width / 2, y: b.y + b.height},
+          {x: b.x + b.width, y: b.y + b.height}
+        ]
+      ];
+    },
+
+    /**
+     * Compute the four axis between corners of rectangle A and corners of
+     * rectangle B. This is needed later to check an eventual collision.
+     *
+     * @param  {array} An array of rectangle A's four corners (x, y).
+     * @param  {array} An array of rectangle B's four corners (x, y).
+     * @return {array} An array of four axis defined by their coordinates (x,y).
+     */
+    axis: function(c1, c2) {
+      return [
+        {x: c1[1].x - c1[0].x, y: c1[1].y - c1[0].y},
+        {x: c1[1].x - c1[3].x, y: c1[1].y - c1[3].y},
+        {x: c2[0].x - c2[2].x, y: c2[0].y - c2[2].y},
+        {x: c2[0].x - c2[1].x, y: c2[0].y - c2[1].y}
+      ];
+    },
+
+    /**
+     * Project a rectangle's corner on an axis.
+     *
+     * @param  {object} Coordinates of a corner (x, y).
+     * @param  {object} Coordinates of an axis (x, y).
+     * @return {object} The projection defined by coordinates (x, y).
+     */
+    projection: function(c, a) {
+      var l = (
+        (c.x * a.x + c.y * a.y) /
+        (Math.pow(a.x, 2) + Math.pow(a.y, 2))
+      );
+
+      return {
+        x: l * a.x,
+        y: l * a.y
+      };
+    },
+
+    /**
+     * Check whether two rectangles collide on one particular axis.
+     *
+     * @param  {object}   An axis' coordinates (x, y).
+     * @param  {array}    Rectangle A's corners.
+     * @param  {array}    Rectangle B's corners.
+     * @return {boolean}  True if the rectangles collide on the axis.
+     */
+    axisCollision: function(a, c1, c2) {
+      var sc1 = [],
+          sc2 = [];
+
+      for (var ci = 0; ci < 4; ci++) {
+        var p1 = this.projection(c1[ci], a),
+            p2 = this.projection(c2[ci], a);
+
+        sc1.push(p1.x * a.x + p1.y * a.y);
+        sc2.push(p2.x * a.x + p2.y * a.y);
+      }
+
+      var maxc1 = Math.max.apply(Math, sc1),
+          maxc2 = Math.max.apply(Math, sc2),
+          minc1 = Math.min.apply(Math, sc1),
+          minc2 = Math.min.apply(Math, sc2);
+
+      return (minc2 <= maxc1 && maxc2 >= minc1);
+    },
+
+    /**
+     * Check whether two rectangles collide on each one of their four axis. If
+     * all axis collide, then the two rectangles do collide on the plane.
+     *
+     * @param  {array}    Rectangle A's corners.
+     * @param  {array}    Rectangle B's corners.
+     * @return {boolean}  True if the rectangles collide.
+     */
+    collision: function(c1, c2) {
+      var axis = this.axis(c1, c2),
+          col = true;
+
+      for (var i = 0; i < 4; i++)
+        col = col && this.axisCollision(axis[i], c1, c2);
+
+      return col;
+    }
+  };
+
+
+  /**
+   * Quad Functions
+   * ------------
+   *
+   * The Quadtree functions themselves.
+   * For each of those functions, we consider that in a splitted quad, the
+   * index of each node is the following:
+   * 0: top left
+   * 1: top right
+   * 2: bottom left
+   * 3: bottom right
+   *
+   * Moreover, the hereafter quad's philosophy is to consider that if an element
+   * collides with more than one nodes, this element belongs to each of the
+   * nodes it collides with where other would let it lie on a higher node.
+   */
+
+  /**
+   * Get the index of the node containing the point in the quad
+   *
+   * @param  {object}  point      A point defined by coordinates (x, y).
+   * @param  {object}  quadBounds Boundaries of the quad (x, y, width, heigth).
+   * @return {integer}            The index of the node containing the point.
+   */
+  function _quadIndex(point, quadBounds) {
+    var xmp = quadBounds.x + quadBounds.width / 2,
+        ymp = quadBounds.y + quadBounds.height / 2,
+        top = (point.y < ymp),
+        left = (point.x < xmp);
+
+    if (top) {
+      if (left)
+        return 0;
+      else
+        return 1;
+    }
+    else {
+      if (left)
+        return 2;
+      else
+        return 3;
+    }
+  }
+
+  /**
+   * Get a list of indexes of nodes containing an axis-aligned rectangle
+   *
+   * @param  {object}  rectangle   A rectangle defined by two points (x1, y1),
+   *                               (x2, y2) and height.
+   * @param  {array}   quadCorners An array of the quad nodes' corners.
+   * @return {array}               An array of indexes containing one to
+   *                               four integers.
+   */
+  function _quadIndexes(rectangle, quadCorners) {
+    var indexes = [];
+
+    // Iterating through quads
+    for (var i = 0; i < 4; i++)
+      if ((rectangle.x2 >= quadCorners[i][0].x) &&
+          (rectangle.x1 <= quadCorners[i][1].x) &&
+          (rectangle.y1 + rectangle.height >= quadCorners[i][0].y) &&
+          (rectangle.y1 <= quadCorners[i][2].y))
+        indexes.push(i);
+
+    return indexes;
+  }
+
+  /**
+   * Get a list of indexes of nodes containing a non-axis-aligned rectangle
+   *
+   * @param  {array}  corners      An array containing each corner of the
+   *                               rectangle defined by its coordinates (x, y).
+   * @param  {array}  quadCorners  An array of the quad nodes' corners.
+   * @return {array}               An array of indexes containing one to
+   *                               four integers.
+   */
+  function _quadCollision(corners, quadCorners) {
+    var indexes = [];
+
+    // Iterating through quads
+    for (var i = 0; i < 4; i++)
+      if (_geom.collision(corners, quadCorners[i]))
+        indexes.push(i);
+
+    return indexes;
+  }
+
+  /**
+   * Subdivide a quad by creating a node at a precise index. The function does
+   * not generate all four nodes not to potentially create unused nodes.
+   *
+   * @param  {integer}  index The index of the node to create.
+   * @param  {object}   quad  The quad object to subdivide.
+   * @return {object}         A new quad representing the node created.
+   */
+  function _quadSubdivide(index, quad) {
+    var next = quad.level + 1,
+        subw = Math.round(quad.bounds.width / 2),
+        subh = Math.round(quad.bounds.height / 2),
+        qx = Math.round(quad.bounds.x),
+        qy = Math.round(quad.bounds.y),
+        x,
+        y;
+
+    switch (index) {
+      case 0:
+        x = qx;
+        y = qy;
+        break;
+      case 1:
+        x = qx + subw;
+        y = qy;
+        break;
+      case 2:
+        x = qx;
+        y = qy + subh;
+        break;
+      case 3:
+        x = qx + subw;
+        y = qy + subh;
+        break;
+    }
+
+    return _quadTree(
+      {x: x, y: y, width: subw, height: subh},
+      next,
+      quad.maxElements,
+      quad.maxLevel
+    );
+  }
+
+  /**
+   * Recursively insert an element into the quadtree. Only points
+   * with size, i.e. axis-aligned squares, may be inserted with this
+   * method.
+   *
+   * @param  {object}  el         The element to insert in the quadtree.
+   * @param  {object}  sizedPoint A sized point defined by two top points
+   *                              (x1, y1), (x2, y2) and height.
+   * @param  {object}  quad       The quad in which to insert the element.
+   * @return {undefined}          The function does not return anything.
+   */
+  function _quadInsert(el, sizedPoint, quad) {
+    if (quad.level < quad.maxLevel) {
+
+      // Searching appropriate quads
+      var indexes = _quadIndexes(sizedPoint, quad.corners);
+
+      // Iterating
+      for (var i = 0, l = indexes.length; i < l; i++) {
+
+        // Subdividing if necessary
+        if (quad.nodes[indexes[i]] === undefined)
+          quad.nodes[indexes[i]] = _quadSubdivide(indexes[i], quad);
+
+        // Recursion
+        _quadInsert(el, sizedPoint, quad.nodes[indexes[i]]);
+      }
+    }
+    else {
+
+      // Pushing the element in a leaf node
+      quad.elements.push(el);
+    }
+  }
+
+  /**
+   * Recursively retrieve every elements held by the node containing the
+   * searched point.
+   *
+   * @param  {object}  point The searched point (x, y).
+   * @param  {object}  quad  The searched quad.
+   * @return {array}         An array of elements contained in the relevant
+   *                         node.
+   */
+  function _quadRetrievePoint(point, quad) {
+    if (quad.level < quad.maxLevel) {
+      var index = _quadIndex(point, quad.bounds);
+
+      // If node does not exist we return an empty list
+      if (quad.nodes[index] !== undefined) {
+        return _quadRetrievePoint(point, quad.nodes[index]);
+      }
+      else {
+        return [];
+      }
+    }
+    else {
+      return quad.elements;
+    }
+  }
+
+  /**
+   * Recursively retrieve every elements contained within an rectangular area
+   * that may or may not be axis-aligned.
+   *
+   * @param  {object|array} rectData       The searched area defined either by
+   *                                       an array of four corners (x, y) in
+   *                                       the case of a non-axis-aligned
+   *                                       rectangle or an object with two top
+   *                                       points (x1, y1), (x2, y2) and height.
+   * @param  {object}       quad           The searched quad.
+   * @param  {function}     collisionFunc  The collision function used to search
+   *                                       for node indexes.
+   * @param  {array?}       els            The retrieved elements.
+   * @return {array}                       An array of elements contained in the
+   *                                       area.
+   */
+  function _quadRetrieveArea(rectData, quad, collisionFunc, els) {
+    els = els || {};
+
+    if (quad.level < quad.maxLevel) {
+      var indexes = collisionFunc(rectData, quad.corners);
+
+      for (var i = 0, l = indexes.length; i < l; i++)
+        if (quad.nodes[indexes[i]] !== undefined)
+          _quadRetrieveArea(
+            rectData,
+            quad.nodes[indexes[i]],
+            collisionFunc,
+            els
+          );
+    } else
+      for (var j = 0, m = quad.elements.length; j < m; j++)
+        if (els[quad.elements[j].id] === undefined)
+          els[quad.elements[j].id] = quad.elements[j];
+
+    return els;
+  }
+
+  /**
+   * Creates the quadtree object itself.
+   *
+   * @param  {object}   bounds       The boundaries of the quad defined by an
+   *                                 origin (x, y), width and heigth.
+   * @param  {integer}  level        The level of the quad in the tree.
+   * @param  {integer}  maxElements  The max number of element in a leaf node.
+   * @param  {integer}  maxLevel     The max recursion level of the tree.
+   * @return {object}                The quadtree object.
+   */
+  function _quadTree(bounds, level, maxElements, maxLevel) {
+    return {
+      level: level || 0,
+      bounds: bounds,
+      corners: _geom.splitSquare(bounds),
+      maxElements: maxElements || 20,
+      maxLevel: maxLevel || 4,
+      elements: [],
+      nodes: []
+    };
+  }
+
+
+  /**
+   * Sigma Quad Constructor
+   * ----------------------
+   *
+   * The quad API as exposed to sigma.
+   */
+
+  /**
+   * The quad core that will become the sigma interface with the quadtree.
+   *
+   * property {object} _tree  Property holding the quadtree object.
+   * property {object} _geom  Exposition of the _geom namespace for testing.
+   * property {object} _cache Cache for the area method.
+   */
+  var quad = function() {
+    this._geom = _geom;
+    this._tree = null;
+    this._cache = {
+      query: false,
+      result: false
+    };
+  };
+
+  /**
+   * Index a graph by inserting its nodes into the quadtree.
+   *
+   * @param  {array}  nodes   An array of nodes to index.
+   * @param  {object} params  An object of parameters with at least the quad
+   *                          bounds.
+   * @return {object}         The quadtree object.
+   *
+   * Parameters:
+   * ----------
+   * bounds:      {object}   boundaries of the quad defined by its origin (x, y)
+   *                         width and heigth.
+   * prefix:      {string?}  a prefix for node geometric attributes.
+   * maxElements: {integer?} the max number of elements in a leaf node.
+   * maxLevel:    {integer?} the max recursion level of the tree.
+   */
+  quad.prototype.index = function(nodes, params) {
+
+    // Enforcing presence of boundaries
+    if (!params.bounds)
+      throw 'sigma.classes.quad.index: bounds information not given.';
+
+    // Prefix
+    var prefix = params.prefix || '';
+
+    // Building the tree
+    this._tree = _quadTree(
+      params.bounds,
+      0,
+      params.maxElements,
+      params.maxLevel
+    );
+
+    // Inserting graph nodes into the tree
+    for (var i = 0, l = nodes.length; i < l; i++) {
+
+      // Inserting node
+      _quadInsert(
+        nodes[i],
+        _geom.pointToSquare({
+          x: nodes[i][prefix + 'x'],
+          y: nodes[i][prefix + 'y'],
+          size: nodes[i][prefix + 'size']
+        }),
+        this._tree
+      );
+    }
+
+    // Reset cache:
+    this._cache = {
+      query: false,
+      result: false
+    };
+
+    // remove?
+    return this._tree;
+  };
+
+  /**
+   * Retrieve every graph nodes held by the quadtree node containing the
+   * searched point.
+   *
+   * @param  {number} x of the point.
+   * @param  {number} y of the point.
+   * @return {array}  An array of nodes retrieved.
+   */
+  quad.prototype.point = function(x, y) {
+    return this._tree ?
+      _quadRetrievePoint({x: x, y: y}, this._tree) || [] :
+      [];
+  };
+
+  /**
+   * Retrieve every graph nodes within a rectangular area. The methods keep the
+   * last area queried in cache for optimization reason and will act differently
+   * for the same reason if the area is axis-aligned or not.
+   *
+   * @param  {object} A rectangle defined by two top points (x1, y1), (x2, y2)
+   *                  and height.
+   * @return {array}  An array of nodes retrieved.
+   */
+  quad.prototype.area = function(rect) {
+    var serialized = JSON.stringify(rect),
+        collisionFunc,
+        rectData;
+
+    // Returning cache?
+    if (this._cache.query === serialized)
+      return this._cache.result;
+
+    // Axis aligned ?
+    if (_geom.isAxisAligned(rect)) {
+      collisionFunc = _quadIndexes;
+      rectData = _geom.axisAlignedTopPoints(rect);
+    }
+    else {
+      collisionFunc = _quadCollision;
+      rectData = _geom.rectangleCorners(rect);
+    }
+
+    // Retrieving nodes
+    var nodes = this._tree ?
+      _quadRetrieveArea(
+        rectData,
+        this._tree,
+        collisionFunc
+      ) :
+      [];
+
+    // Object to array
+    var nodesArray = [];
+    for (var i in nodes)
+      nodesArray.push(nodes[i]);
+
+    // Caching
+    this._cache.query = serialized;
+    this._cache.result = nodesArray;
+
+    return nodesArray;
+  };
+
+
+  /**
+   * EXPORT:
+   * *******
+   */
+  if (typeof this.sigma !== 'undefined') {
+    this.sigma.classes = this.sigma.classes || {};
+    this.sigma.classes.quad = quad;
+  } else if (typeof exports !== 'undefined') {
+    if (typeof module !== 'undefined' && module.exports)
+      exports = module.exports = quad;
+    exports.quad = quad;
+  } else
+    this.quad = quad;
+}).call(this);
diff --git a/demo/static/sigmajs/conrad.js b/demo/static/sigmajs/conrad.js
new file mode 100644
index 000000000..09560d5f6
--- /dev/null
+++ b/demo/static/sigmajs/conrad.js
@@ -0,0 +1,984 @@
+/**
+ * conrad.js is a tiny JavaScript jobs scheduler,
+ *
+ * Version: 0.1.0
+ * Sources: http://github.com/jacomyal/conrad.js
+ * Doc:     http://github.com/jacomyal/conrad.js#readme
+ *
+ * License:
+ * --------
+ * Copyright © 2013 Alexis Jacomy, Sciences-Po médialab
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to
+ * deal in the Software without restriction, including without limitation the
+ * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+ * sell copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * The Software is provided "as is", without warranty of any kind, express or
+ * implied, including but not limited to the warranties of merchantability,
+ * fitness for a particular purpose and noninfringement. In no event shall the
+ * authors or copyright holders be liable for any claim, damages or other
+ * liability, whether in an action of contract, tort or otherwise, arising
+ * from, out of or in connection with the software or the use or other dealings
+ * in the Software.
+ */
+(function(global) {
+  'use strict';
+
+  // Check that conrad.js has not been loaded yet:
+  if (global.conrad)
+    throw new Error('conrad already exists');
+
+
+  /**
+   * PRIVATE VARIABLES:
+   * ******************
+   */
+
+  /**
+   * A flag indicating whether conrad is running or not.
+   *
+   * @type {Number}
+   */
+  var _lastFrameTime;
+
+  /**
+   * A flag indicating whether conrad is running or not.
+   *
+   * @type {Boolean}
+   */
+  var _isRunning = false;
+
+  /**
+   * The hash of registered jobs. Each job must at least have a unique ID
+   * under the key "id" and a function under the key "job". This hash
+   * contains each running job and each waiting job.
+   *
+   * @type {Object}
+   */
+  var _jobs = {};
+
+  /**
+   * The hash of currently running jobs.
+   *
+   * @type {Object}
+   */
+  var _runningJobs = {};
+
+  /**
+   * The array of currently running jobs, sorted by priority.
+   *
+   * @type {Array}
+   */
+  var _sortedByPriorityJobs = [];
+
+  /**
+   * The array of currently waiting jobs.
+   *
+   * @type {Object}
+   */
+  var _waitingJobs = {};
+
+  /**
+   * The array of finished jobs. They are stored in an array, since two jobs
+   * with the same "id" can happen at two different times.
+   *
+   * @type {Array}
+   */
+  var _doneJobs = [];
+
+  /**
+   * A dirty flag to keep conrad from starting: Indeed, when addJob() is called
+   * with several jobs, conrad must be started only at the end. This flag keeps
+   * me from duplicating the code that effectively adds a job.
+   *
+   * @type {Boolean}
+   */
+  var _noStart = false;
+
+  /**
+   * An hash containing some global settings about how conrad.js should
+   * behave.
+   *
+   * @type {Object}
+   */
+  var _parameters = {
+    frameDuration: 20,
+    history: true
+  };
+
+  /**
+   * This object contains every handlers bound to conrad events. It does not
+   * requirea any DOM implementation, since the events are all JavaScript.
+   *
+   * @type {Object}
+   */
+  var _handlers = Object.create(null);
+
+
+  /**
+   * PRIVATE FUNCTIONS:
+   * ******************
+   */
+
+  /**
+   * Will execute the handler everytime that the indicated event (or the
+   * indicated events) will be triggered.
+   *
+   * @param  {string|array|object} events  The name of the event (or the events
+   *                                       separated by spaces).
+   * @param  {function(Object)}    handler The handler to bind.
+   * @return {Object}                      Returns conrad.
+   */
+  function _bind(events, handler) {
+    var i,
+        i_end,
+        event,
+        eArray;
+
+    if (!arguments.length)
+      return;
+    else if (
+      arguments.length === 1 &&
+      Object(arguments[0]) === arguments[0]
+    )
+      for (events in arguments[0])
+        _bind(events, arguments[0][events]);
+    else if (arguments.length > 1) {
+      eArray =
+        Array.isArray(events) ?
+          events :
+          events.split(/ /);
+
+      for (i = 0, i_end = eArray.length; i !== i_end; i += 1) {
+        event = eArray[i];
+
+        if (!_handlers[event])
+          _handlers[event] = [];
+
+        // Using an object instead of directly the handler will make possible
+        // later to add flags
+        _handlers[event].push({
+          handler: handler
+        });
+      }
+    }
+  }
+
+  /**
+   * Removes the handler from a specified event (or specified events).
+   *
+   * @param  {?string}           events  The name of the event (or the events
+   *                                     separated by spaces). If undefined,
+   *                                     then all handlers are removed.
+   * @param  {?function(Object)} handler The handler to unbind. If undefined,
+   *                                     each handler bound to the event or the
+   *                                     events will be removed.
+   * @return {Object}            Returns conrad.
+   */
+  function _unbind(events, handler) {
+    var i,
+        i_end,
+        j,
+        j_end,
+        a,
+        event,
+        eArray = Array.isArray(events) ?
+                   events :
+                   events.split(/ /);
+
+    if (!arguments.length)
+      _handlers = Object.create(null);
+    else if (handler) {
+      for (i = 0, i_end = eArray.length; i !== i_end; i += 1) {
+        event = eArray[i];
+        if (_handlers[event]) {
+          a = [];
+          for (j = 0, j_end = _handlers[event].length; j !== j_end; j += 1)
+            if (_handlers[event][j].handler !== handler)
+              a.push(_handlers[event][j]);
+
+          _handlers[event] = a;
+        }
+
+        if (_handlers[event] && _handlers[event].length === 0)
+          delete _handlers[event];
+      }
+    } else
+      for (i = 0, i_end = eArray.length; i !== i_end; i += 1)
+        delete _handlers[eArray[i]];
+  }
+
+  /**
+   * Executes each handler bound to the event.
+   *
+   * @param  {string}  events The name of the event (or the events separated
+   *                          by spaces).
+   * @param  {?Object} data   The content of the event (optional).
+   * @return {Object}         Returns conrad.
+   */
+  function _dispatch(events, data) {
+    var i,
+        j,
+        i_end,
+        j_end,
+        event,
+        eventName,
+        eArray = Array.isArray(events) ?
+                   events :
+                   events.split(/ /);
+
+    data = data === undefined ? {} : data;
+
+    for (i = 0, i_end = eArray.length; i !== i_end; i += 1) {
+      eventName = eArray[i];
+
+      if (_handlers[eventName]) {
+        event = {
+          type: eventName,
+          data: data || {}
+        };
+
+        for (j = 0, j_end = _handlers[eventName].length; j !== j_end; j += 1)
+          try {
+            _handlers[eventName][j].handler(event);
+          } catch (e) {}
+      }
+    }
+  }
+
+  /**
+   * Executes the most prioritary job once, and deals with filling the stats
+   * (done, time, averageTime, currentTime, etc...).
+   *
+   * @return {?Object} Returns the job object if it has to be killed, null else.
+   */
+  function _executeFirstJob() {
+    var i,
+        l,
+        test,
+        kill,
+        pushed = false,
+        time = __dateNow(),
+        job = _sortedByPriorityJobs.shift();
+
+    // Execute the job and look at the result:
+    test = job.job();
+
+    // Deal with stats:
+    time = __dateNow() - time;
+    job.done++;
+    job.time += time;
+    job.currentTime += time;
+    job.weightTime = job.currentTime / (job.weight || 1);
+    job.averageTime = job.time / job.done;
+
+    // Check if the job has to be killed:
+    kill = job.count ? (job.count <= job.done) : !test;
+
+    // Reset priorities:
+    if (!kill) {
+      for (i = 0, l = _sortedByPriorityJobs.length; i < l; i++)
+        if (_sortedByPriorityJobs[i].weightTime > job.weightTime) {
+          _sortedByPriorityJobs.splice(i, 0, job);
+          pushed = true;
+          break;
+        }
+
+      if (!pushed)
+        _sortedByPriorityJobs.push(job);
+    }
+
+    return kill ? job : null;
+  }
+
+  /**
+   * Activates a job, by adding it to the _runningJobs object and the
+   * _sortedByPriorityJobs array. It also initializes its currentTime value.
+   *
+   * @param  {Object} job The job to activate.
+   */
+  function _activateJob(job) {
+    var l = _sortedByPriorityJobs.length;
+
+    // Add the job to the running jobs:
+    _runningJobs[job.id] = job;
+    job.status = 'running';
+
+    // Add the job to the priorities:
+    if (l) {
+      job.weightTime = _sortedByPriorityJobs[l - 1].weightTime;
+      job.currentTime = job.weightTime * (job.weight || 1);
+    }
+
+    // Initialize the job and dispatch:
+    job.startTime = __dateNow();
+    _dispatch('jobStarted', __clone(job));
+
+    _sortedByPriorityJobs.push(job);
+  }
+
+  /**
+   * The main loop of conrad.js:
+   *  . It executes job such that they all occupate the same processing time.
+   *  . It stops jobs that do not need to be executed anymore.
+   *  . It triggers callbacks when it is relevant.
+   *  . It starts waiting jobs when they need to be started.
+   *  . It injects frames to keep a constant frapes per second ratio.
+   *  . It stops itself when there are no more jobs to execute.
+   */
+  function _loop() {
+    var k,
+        o,
+        l,
+        job,
+        time,
+        deadJob;
+
+    // Deal with the newly added jobs (the _jobs object):
+    for (k in _jobs) {
+      job = _jobs[k];
+
+      if (job.after)
+        _waitingJobs[k] = job;
+      else
+        _activateJob(job);
+
+      delete _jobs[k];
+    }
+
+    // Set the _isRunning flag to false if there are no running job:
+    _isRunning = !!_sortedByPriorityJobs.length;
+
+    // Deal with the running jobs (the _runningJobs object):
+    while (
+      _sortedByPriorityJobs.length &&
+      __dateNow() - _lastFrameTime < _parameters.frameDuration
+    ) {
+      deadJob = _executeFirstJob();
+
+      // Deal with the case where the job has ended:
+      if (deadJob) {
+        _killJob(deadJob.id);
+
+        // Check for waiting jobs:
+        for (k in _waitingJobs)
+          if (_waitingJobs[k].after === deadJob.id) {
+            _activateJob(_waitingJobs[k]);
+            delete _waitingJobs[k];
+          }
+      }
+    }
+
+    // Check if conrad still has jobs to deal with, and kill it if not:
+    if (_isRunning) {
+      // Update the _lastFrameTime:
+      _lastFrameTime = __dateNow();
+
+      _dispatch('enterFrame');
+      setTimeout(_loop, 0);
+    } else
+      _dispatch('stop');
+  }
+
+  /**
+   * Adds one or more jobs, and starts the loop if no job was running before. A
+   * job is at least a unique string "id" and a function, and there are some
+   * parameters that you can specify for each job to modify the way conrad will
+   * execute it. If a job is added with the "id" of another job that is waiting
+   * or still running, an error will be thrown.
+   *
+   * When a job is added, it is referenced in the _jobs object, by its id.
+   * Then, if it has to be executed right now, it will be also referenced in
+   * the _runningJobs object. If it has to wait, then it will be added into the
+   * _waitingJobs object, until it can start.
+   *
+   * Keep reading this documentation to see how to call this method.
+   *
+   * @return {Object} Returns conrad.
+   *
+   * Adding one job:
+   * ***************
+   * Basically, a job is defined by its string id and a function (the job). It
+   * is also possible to add some parameters:
+   *
+   *  > conrad.addJob('myJobId', myJobFunction);
+   *  > conrad.addJob('myJobId', {
+   *  >   job: myJobFunction,
+   *  >   someParameter: someValue
+   *  > });
+   *  > conrad.addJob({
+   *  >   id: 'myJobId',
+   *  >   job: myJobFunction,
+   *  >   someParameter: someValue
+   *  > });
+   *
+   * Adding several jobs:
+   * ********************
+   * When adding several jobs at the same time, it is possible to specify
+   * parameters for each one individually or for all:
+   *
+   *  > conrad.addJob([
+   *  >   {
+   *  >     id: 'myJobId1',
+   *  >     job: myJobFunction1,
+   *  >     someParameter1: someValue1
+   *  >   },
+   *  >   {
+   *  >     id: 'myJobId2',
+   *  >     job: myJobFunction2,
+   *  >     someParameter2: someValue2
+   *  >   }
+   *  > ], {
+   *  >   someCommonParameter: someCommonValue
+   *  > });
+   *  > conrad.addJob({
+   *  >   myJobId1: {,
+   *  >     job: myJobFunction1,
+   *  >     someParameter1: someValue1
+   *  >   },
+   *  >   myJobId2: {,
+   *  >     job: myJobFunction2,
+   *  >     someParameter2: someValue2
+   *  >   }
+   *  > }, {
+   *  >   someCommonParameter: someCommonValue
+   *  > });
+   *  > conrad.addJob({
+   *  >   myJobId1: myJobFunction1,
+   *  >   myJobId2: myJobFunction2
+   *  > }, {
+   *  >   someCommonParameter: someCommonValue
+   *  > });
+   *
+   *  Recognized parameters:
+   *  **********************
+   *  Here is the exhaustive list of every accepted parameters:
+   *
+   *    {?Function} end      A callback to execute when the job is ended. It is
+   *                         not executed if the job is killed instead of ended
+   *                         "naturally".
+   *    {?Integer}  count    The number of time the job has to be executed.
+   *    {?Number}   weight   If specified, the job will be executed as it was
+   *                         added "weight" times.
+   *    {?String}   after    The id of another job (eventually not added yet).
+   *                         If specified, this job will start only when the
+   *                         specified "after" job is ended.
+   */
+  function _addJob(v1, v2) {
+    var i,
+        l,
+        o;
+
+    // Array of jobs:
+    if (Array.isArray(v1)) {
+      // Keep conrad to start until the last job is added:
+      _noStart = true;
+
+      for (i = 0, l = v1.length; i < l; i++)
+        _addJob(v1[i].id, __extend(v1[i], v2));
+
+      _noStart = false;
+      if (!_isRunning) {
+        // Update the _lastFrameTime:
+        _lastFrameTime = __dateNow();
+
+        _dispatch('start');
+        _loop();
+      }
+    } else if (typeof v1 === 'object') {
+      // One job (object):
+      if (typeof v1.id === 'string')
+        _addJob(v1.id, v1);
+
+      // Hash of jobs:
+      else {
+        // Keep conrad to start until the last job is added:
+        _noStart = true;
+
+        for (i in v1)
+          if (typeof v1[i] === 'function')
+            _addJob(i, __extend({
+              job: v1[i]
+            }, v2));
+          else
+            _addJob(i, __extend(v1[i], v2));
+
+        _noStart = false;
+        if (!_isRunning) {
+          // Update the _lastFrameTime:
+          _lastFrameTime = __dateNow();
+
+          _dispatch('start');
+          _loop();
+        }
+      }
+
+    // One job (string, *):
+    } else if (typeof v1 === 'string') {
+      if (_hasJob(v1))
+        throw new Error(
+          '[conrad.addJob] Job with id "' + v1 + '" already exists.'
+        );
+
+      // One job (string, function):
+      if (typeof v2 === 'function') {
+        o = {
+          id: v1,
+          done: 0,
+          time: 0,
+          status: 'waiting',
+          currentTime: 0,
+          averageTime: 0,
+          weightTime: 0,
+          job: v2
+        };
+
+      // One job (string, object):
+      } else if (typeof v2 === 'object') {
+        o = __extend(
+          {
+            id: v1,
+            done: 0,
+            time: 0,
+            status: 'waiting',
+            currentTime: 0,
+            averageTime: 0,
+            weightTime: 0
+          },
+          v2
+        );
+
+      // If none of those cases, throw an error:
+      } else
+        throw new Error('[conrad.addJob] Wrong arguments.');
+
+      // Effectively add the job:
+      _jobs[v1] = o;
+      _dispatch('jobAdded', __clone(o));
+
+      // Check if the loop has to be started:
+      if (!_isRunning && !_noStart) {
+        // Update the _lastFrameTime:
+        _lastFrameTime = __dateNow();
+
+        _dispatch('start');
+        _loop();
+      }
+
+    // If none of those cases, throw an error:
+    } else
+      throw new Error('[conrad.addJob] Wrong arguments.');
+
+    return this;
+  }
+
+  /**
+   * Kills one or more jobs, indicated by their ids. It is only possible to
+   * kill running jobs or waiting jobs. If you try to kill a job that does not
+   * exists or that is already killed, a warning will be thrown.
+   *
+   * @param  {Array|String} v1 A string job id or an array of job ids.
+   * @return {Object}       Returns conrad.
+   */
+  function _killJob(v1) {
+    var i,
+        l,
+        k,
+        a,
+        job,
+        found = false;
+
+    // Array of job ids:
+    if (Array.isArray(v1))
+      for (i = 0, l = v1.length; i < l; i++)
+        _killJob(v1[i]);
+
+    // One job's id:
+    else if (typeof v1 === 'string') {
+      a = [_runningJobs, _waitingJobs, _jobs];
+
+      // Remove the job from the hashes:
+      for (i = 0, l = a.length; i < l; i++)
+        if (v1 in a[i]) {
+          job = a[i][v1];
+
+          if (_parameters.history) {
+            job.status = 'done';
+            _doneJobs.push(job);
+          }
+
+          _dispatch('jobEnded', __clone(job));
+          delete a[i][v1];
+
+          if (typeof job.end === 'function')
+            job.end();
+
+          found = true;
+        }
+
+      // Remove the priorities array:
+      a = _sortedByPriorityJobs;
+      for (i = 0, l = a.length; i < l; i++)
+        if (a[i].id === v1) {
+          a.splice(i, 1);
+          break;
+        }
+
+      if (!found)
+        throw new Error('[conrad.killJob] Job "' + v1 + '" not found.');
+
+    // If none of those cases, throw an error:
+    } else
+      throw new Error('[conrad.killJob] Wrong arguments.');
+
+    return this;
+  }
+
+  /**
+   * Kills every running, waiting, and just added jobs.
+   *
+   * @return {Object} Returns conrad.
+   */
+  function _killAll() {
+    var k,
+        jobs = __extend(_jobs, _runningJobs, _waitingJobs);
+
+    // Take every jobs and push them into the _doneJobs object:
+    if (_parameters.history)
+      for (k in jobs) {
+        jobs[k].status = 'done';
+        _doneJobs.push(jobs[k]);
+
+        if (typeof jobs[k].end === 'function')
+          jobs[k].end();
+      }
+
+    // Reinitialize the different jobs lists:
+    _jobs = {};
+    _waitingJobs = {};
+    _runningJobs = {};
+    _sortedByPriorityJobs = [];
+
+    // In case some jobs are added right after the kill:
+    _isRunning = false;
+
+    return this;
+  }
+
+  /**
+   * Returns true if a job with the specified id is currently running or
+   * waiting, and false else.
+   *
+   * @param  {String}  id The id of the job.
+   * @return {?Object} Returns the job object if it exists.
+   */
+  function _hasJob(id) {
+    var job = _jobs[id] || _runningJobs[id] || _waitingJobs[id];
+    return job ? __extend(job) : null;
+  }
+
+  /**
+   * This method will set the setting specified by "v1" to the value specified
+   * by "v2" if both are given, and else return the current value of the
+   * settings "v1".
+   *
+   * @param  {String}   v1 The name of the property.
+   * @param  {?*}       v2 Eventually, a value to set to the specified
+   *                       property.
+   * @return {Object|*} Returns the specified settings value if "v2" is not
+   *                    given, and conrad else.
+   */
+  function _settings(v1, v2) {
+    var o;
+
+    if (typeof a1 === 'string' && arguments.length === 1)
+      return _parameters[a1];
+    else {
+      o = (typeof a1 === 'object' && arguments.length === 1) ?
+        a1 || {} :
+        {};
+      if (typeof a1 === 'string')
+        o[a1] = a2;
+
+      for (var k in o)
+        if (o[k] !== undefined)
+          _parameters[k] = o[k];
+        else
+          delete _parameters[k];
+
+      return this;
+    }
+  }
+
+  /**
+   * Returns true if conrad is currently running, and false else.
+   *
+   * @return {Boolean} Returns _isRunning.
+   */
+  function _getIsRunning() {
+    return _isRunning;
+  }
+
+  /**
+   * Unreference every jobs that are stored in the _doneJobs object. It will
+   * not be possible anymore to get stats about these jobs, but it will release
+   * the memory.
+   *
+   * @return {Object} Returns conrad.
+   */
+  function _clearHistory() {
+    _doneJobs = [];
+    return this;
+  }
+
+  /**
+   * Returns a snapshot of every data about jobs that wait to be started, are
+   * currently running or are done.
+   *
+   * It is possible to get only running, waiting or done jobs by giving
+   * "running", "waiting" or "done" as fist argument.
+   *
+   * It is also possible to get every job with a specified id by giving it as
+   * first argument. Also, using a RegExp instead of an id will return every
+   * jobs whose ids match the RegExp. And these two last use cases work as well
+   * by giving before "running", "waiting" or "done".
+   *
+   * @return {Array} The array of the matching jobs.
+   *
+   * Some call examples:
+   * *******************
+   *  > conrad.getStats('running')
+   *  > conrad.getStats('waiting')
+   *  > conrad.getStats('done')
+   *  > conrad.getStats('myJob')
+   *  > conrad.getStats(/test/)
+   *  > conrad.getStats('running', 'myRunningJob')
+   *  > conrad.getStats('running', /test/)
+   */
+  function _getStats(v1, v2) {
+    var a,
+        k,
+        i,
+        l,
+        stats,
+        pattern,
+        isPatternString;
+
+    if (!arguments.length) {
+      stats = [];
+
+      for (k in _jobs)
+        stats.push(_jobs[k]);
+
+      for (k in _waitingJobs)
+        stats.push(_waitingJobs[k]);
+
+      for (k in _runningJobs)
+        stats.push(_runningJobs[k]);
+
+      stats = stats.concat(_doneJobs);
+    }
+
+    if (typeof v1 === 'string')
+      switch (v1) {
+        case 'waiting':
+          stats = __objectValues(_waitingJobs);
+          break;
+        case 'running':
+          stats = __objectValues(_runningJobs);
+          break;
+        case 'done':
+          stats = _doneJobs;
+          break;
+        default:
+          pattern = v1;
+      }
+
+    if (v1 instanceof RegExp)
+      pattern = v1;
+
+    if (!pattern && (typeof v2 === 'string' || v2 instanceof RegExp))
+      pattern = v2;
+
+    // Filter jobs if a pattern is given:
+    if (pattern) {
+      isPatternString = typeof pattern === 'string';
+
+      if (stats instanceof Array) {
+        a = stats;
+      } else if (typeof stats === 'object') {
+        a = [];
+
+        for (k in stats)
+          a = a.concat(stats[k]);
+      } else {
+        a = [];
+
+        for (k in _jobs)
+          a.push(_jobs[k]);
+
+        for (k in _waitingJobs)
+          a.push(_waitingJobs[k]);
+
+        for (k in _runningJobs)
+          a.push(_runningJobs[k]);
+
+        a = a.concat(_doneJobs);
+      }
+
+      stats = [];
+      for (i = 0, l = a.length; i < l; i++)
+        if (isPatternString ? a[i].id === pattern : a[i].id.match(pattern))
+          stats.push(a[i]);
+    }
+
+    return __clone(stats);
+  }
+
+
+  /**
+   * TOOLS FUNCTIONS:
+   * ****************
+   */
+
+  /**
+   * This function takes any number of objects as arguments, copies from each
+   * of these objects each pair key/value into a new object, and finally
+   * returns this object.
+   *
+   * The arguments are parsed from the last one to the first one, such that
+   * when two objects have keys in common, the "earliest" object wins.
+   *
+   * Example:
+   * ********
+   *  > var o1 = {
+   *  >       a: 1,
+   *  >       b: 2,
+   *  >       c: '3'
+   *  >     },
+   *  >     o2 = {
+   *  >       c: '4',
+   *  >       d: [ 5 ]
+   *  >     };
+   *  > __extend(o1, o2);
+   *  > // Returns: {
+   *  > //   a: 1,
+   *  > //   b: 2,
+   *  > //   c: '3',
+   *  > //   d: [ 5 ]
+   *  > // };
+   *
+   * @param  {Object+} Any number of objects.
+   * @return {Object}  The merged object.
+   */
+  function __extend() {
+    var i,
+        k,
+        res = {},
+        l = arguments.length;
+
+    for (i = l - 1; i >= 0; i--)
+      for (k in arguments[i])
+        res[k] = arguments[i][k];
+
+    return res;
+  }
+
+  /**
+   * This function simply clones an object. This object must contain only
+   * objects, arrays and immutable values. Since it is not public, it does not
+   * deal with cyclic references, DOM elements and instantiated objects - so
+   * use it carefully.
+   *
+   * @param  {Object} The object to clone.
+   * @return {Object} The clone.
+   */
+  function __clone(item) {
+    var result, i, k, l;
+
+    if (!item)
+      return item;
+
+    if (Array.isArray(item)) {
+      result = [];
+      for (i = 0, l = item.length; i < l; i++)
+        result.push(__clone(item[i]));
+    } else if (typeof item === 'object') {
+      result = {};
+      for (i in item)
+        result[i] = __clone(item[i]);
+    } else
+      result = item;
+
+    return result;
+  }
+
+  /**
+   * Returns an array containing the values of an object.
+   *
+   * @param  {Object} The object.
+   * @return {Array}  The array of values.
+   */
+  function __objectValues(o) {
+    var k,
+        a = [];
+
+    for (k in o)
+      a.push(o[k]);
+
+    return a;
+  }
+
+  /**
+   * A short "Date.now()" polyfill.
+   *
+   * @return {Number} The current time (in ms).
+   */
+  function __dateNow() {
+    return Date.now ? Date.now() : new Date().getTime();
+  }
+
+  /**
+   * Polyfill for the Array.isArray function:
+   */
+  if (!Array.isArray)
+    Array.isArray = function(v) {
+      return Object.prototype.toString.call(v) === '[object Array]';
+    };
+
+
+  /**
+   * EXPORT PUBLIC API:
+   * ******************
+   */
+  var conrad = {
+    hasJob: _hasJob,
+    addJob: _addJob,
+    killJob: _killJob,
+    killAll: _killAll,
+    settings: _settings,
+    getStats: _getStats,
+    isRunning: _getIsRunning,
+    clearHistory: _clearHistory,
+
+    // Events management:
+    bind: _bind,
+    unbind: _unbind,
+
+    // Version:
+    version: '0.1.0'
+  };
+
+  if (typeof exports !== 'undefined') {
+    if (typeof module !== 'undefined' && module.exports)
+      exports = module.exports = conrad;
+    exports.conrad = conrad;
+  }
+  global.conrad = conrad;
+})(this);
diff --git a/demo/static/sigmajs/middlewares/sigma.middlewares.copy.js b/demo/static/sigmajs/middlewares/sigma.middlewares.copy.js
new file mode 100644
index 000000000..8f1b2a947
--- /dev/null
+++ b/demo/static/sigmajs/middlewares/sigma.middlewares.copy.js
@@ -0,0 +1,35 @@
+;(function(undefined) {
+  'use strict';
+
+  if (typeof sigma === 'undefined')
+    throw 'sigma is not declared';
+
+  // Initialize packages:
+  sigma.utils.pkg('sigma.middlewares');
+
+  /**
+   * This middleware will just copy the graphic properties.
+   *
+   * @param {?string} readPrefix  The read prefix.
+   * @param {?string} writePrefix The write prefix.
+   */
+  sigma.middlewares.copy = function(readPrefix, writePrefix) {
+    var i,
+        l,
+        a;
+
+    if (writePrefix + '' === readPrefix + '')
+      return;
+
+    a = this.graph.nodes();
+    for (i = 0, l = a.length; i < l; i++) {
+      a[i][writePrefix + 'x'] = a[i][readPrefix + 'x'];
+      a[i][writePrefix + 'y'] = a[i][readPrefix + 'y'];
+      a[i][writePrefix + 'size'] = a[i][readPrefix + 'size'];
+    }
+
+    a = this.graph.edges();
+    for (i = 0, l = a.length; i < l; i++)
+      a[i][writePrefix + 'size'] = a[i][readPrefix + 'size'];
+  };
+}).call(this);
diff --git a/demo/static/sigmajs/middlewares/sigma.middlewares.rescale.js b/demo/static/sigmajs/middlewares/sigma.middlewares.rescale.js
new file mode 100644
index 000000000..85460ec2b
--- /dev/null
+++ b/demo/static/sigmajs/middlewares/sigma.middlewares.rescale.js
@@ -0,0 +1,189 @@
+;(function(undefined) {
+  'use strict';
+
+  if (typeof sigma === 'undefined')
+    throw 'sigma is not declared';
+
+  // Initialize packages:
+  sigma.utils.pkg('sigma.middlewares');
+  sigma.utils.pkg('sigma.utils');
+
+  /**
+   * This middleware will rescale the graph such that it takes an optimal space
+   * on the renderer.
+   *
+   * As each middleware, this function is executed in the scope of the sigma
+   * instance.
+   *
+   * @param {?string} readPrefix  The read prefix.
+   * @param {?string} writePrefix The write prefix.
+   * @param {object}  options     The parameters.
+   */
+  sigma.middlewares.rescale = function(readPrefix, writePrefix, options) {
+    var i,
+        l,
+        a,
+        b,
+        c,
+        d,
+        scale,
+        margin,
+        n = this.graph.nodes(),
+        e = this.graph.edges(),
+        settings = this.settings.embedObjects(options || {}),
+        bounds = settings('bounds') || sigma.utils.getBoundaries(
+          this.graph,
+          readPrefix,
+          true
+        ),
+        minX = bounds.minX,
+        minY = bounds.minY,
+        maxX = bounds.maxX,
+        maxY = bounds.maxY,
+        sizeMax = bounds.sizeMax,
+        weightMax = bounds.weightMax,
+        w = settings('width') || 1,
+        h = settings('height') || 1,
+        rescaleSettings = settings('autoRescale'),
+        validSettings = {
+          nodePosition: 1,
+          nodeSize: 1,
+          edgeSize: 1
+        };
+
+    /**
+     * What elements should we rescale?
+     */
+    if (!(rescaleSettings instanceof Array))
+      rescaleSettings = ['nodePosition', 'nodeSize', 'edgeSize'];
+
+    for (i = 0, l = rescaleSettings.length; i < l; i++)
+      if (!validSettings[rescaleSettings[i]])
+        throw new Error(
+          'The rescale setting "' + rescaleSettings[i] + '" is not recognized.'
+        );
+
+    var np = ~rescaleSettings.indexOf('nodePosition'),
+        ns = ~rescaleSettings.indexOf('nodeSize'),
+        es = ~rescaleSettings.indexOf('edgeSize');
+
+    /**
+     * First, we compute the scaling ratio, without considering the sizes
+     * of the nodes : Each node will have its center in the canvas, but might
+     * be partially out of it.
+     */
+    scale = settings('scalingMode') === 'outside' ?
+      Math.max(
+        w / Math.max(maxX - minX, 1),
+        h / Math.max(maxY - minY, 1)
+      ) :
+      Math.min(
+        w / Math.max(maxX - minX, 1),
+        h / Math.max(maxY - minY, 1)
+      );
+
+    /**
+     * Then, we correct that scaling ratio considering a margin, which is
+     * basically the size of the biggest node.
+     * This has to be done as a correction since to compare the size of the
+     * biggest node to the X and Y values, we have to first get an
+     * approximation of the scaling ratio.
+     **/
+    margin =
+      (
+        settings('rescaleIgnoreSize') ?
+          0 :
+          (settings('maxNodeSize') || sizeMax) / scale
+      ) +
+      (settings('sideMargin') || 0);
+    maxX += margin;
+    minX -= margin;
+    maxY += margin;
+    minY -= margin;
+
+    // Fix the scaling with the new extrema:
+    scale = settings('scalingMode') === 'outside' ?
+      Math.max(
+        w / Math.max(maxX - minX, 1),
+        h / Math.max(maxY - minY, 1)
+      ) :
+      Math.min(
+        w / Math.max(maxX - minX, 1),
+        h / Math.max(maxY - minY, 1)
+      );
+
+    // Size homothetic parameters:
+    if (!settings('maxNodeSize') && !settings('minNodeSize')) {
+      a = 1;
+      b = 0;
+    } else if (settings('maxNodeSize') === settings('minNodeSize')) {
+      a = 0;
+      b = +settings('maxNodeSize');
+    } else {
+      a = (settings('maxNodeSize') - settings('minNodeSize')) / sizeMax;
+      b = +settings('minNodeSize');
+    }
+
+    if (!settings('maxEdgeSize') && !settings('minEdgeSize')) {
+      c = 1;
+      d = 0;
+    } else if (settings('maxEdgeSize') === settings('minEdgeSize')) {
+      c = 0;
+      d = +settings('minEdgeSize');
+    } else {
+      c = (settings('maxEdgeSize') - settings('minEdgeSize')) / weightMax;
+      d = +settings('minEdgeSize');
+    }
+
+    // Rescale the nodes and edges:
+    for (i = 0, l = e.length; i < l; i++)
+      e[i][writePrefix + 'size'] =
+        e[i][readPrefix + 'size'] * (es ? c : 1) + (es ? d : 0);
+
+    for (i = 0, l = n.length; i < l; i++) {
+      n[i][writePrefix + 'size'] =
+        n[i][readPrefix + 'size'] * (ns ? a : 1) + (ns ? b : 0);
+      n[i][writePrefix + 'x'] =
+        (n[i][readPrefix + 'x'] - (maxX + minX) / 2) * (np ? scale : 1);
+      n[i][writePrefix + 'y'] =
+        (n[i][readPrefix + 'y'] - (maxY + minY) / 2) * (np ? scale : 1);
+    }
+  };
+
+  sigma.utils.getBoundaries = function(graph, prefix, doEdges) {
+    var i,
+        l,
+        e = graph.edges(),
+        n = graph.nodes(),
+        weightMax = -Infinity,
+        sizeMax = -Infinity,
+        minX = Infinity,
+        minY = Infinity,
+        maxX = -Infinity,
+        maxY = -Infinity;
+
+    if (doEdges)
+      for (i = 0, l = e.length; i < l; i++)
+        weightMax = Math.max(e[i][prefix + 'size'], weightMax);
+
+    for (i = 0, l = n.length; i < l; i++) {
+      sizeMax = Math.max(n[i][prefix + 'size'], sizeMax);
+      maxX = Math.max(n[i][prefix + 'x'], maxX);
+      minX = Math.min(n[i][prefix + 'x'], minX);
+      maxY = Math.max(n[i][prefix + 'y'], maxY);
+      minY = Math.min(n[i][prefix + 'y'], minY);
+    }
+
+    weightMax = weightMax || 1;
+    sizeMax = sizeMax || 1;
+
+    return {
+      weightMax: weightMax,
+      sizeMax: sizeMax,
+      minX: minX,
+      minY: minY,
+      maxX: maxX,
+      maxY: maxY
+    };
+  };
+}).call(this);
diff --git a/demo/static/sigmajs/misc/sigma.misc.animation.js b/demo/static/sigmajs/misc/sigma.misc.animation.js
new file mode 100644
index 000000000..299f00fe1
--- /dev/null
+++ b/demo/static/sigmajs/misc/sigma.misc.animation.js
@@ -0,0 +1,239 @@
+;(function(undefined) {
+  'use strict';
+
+  if (typeof sigma === 'undefined')
+    throw 'sigma is not declared';
+
+  // Initialize packages:
+  sigma.utils.pkg('sigma.misc.animation.running');
+
+  /**
+   * Generates a unique ID for the animation.
+   *
+   * @return {string} Returns the new ID.
+   */
+  var _getID = (function() {
+    var id = 0;
+    return function() {
+      return '' + (++id);
+    };
+  })();
+
+  /**
+   * This function animates a camera. It has to be called with the camera to
+   * animate, the values of the coordinates to reach and eventually some
+   * options. It returns a number id, that you can use to kill the animation,
+   * with the method sigma.misc.animation.kill(id).
+   *
+   * The available options are:
+   *
+   *   {?number}            duration   The duration of the animation.
+   *   {?function}          onNewFrame A callback to execute when the animation
+   *                                   enter a new frame.
+   *   {?function}          onComplete A callback to execute when the animation
+   *                                   is completed or killed.
+   *   {?(string|function)} easing     The name of a function from the package
+   *                                   sigma.utils.easings, or a custom easing
+   *                                   function.
+   *
+   * @param  {camera}  camera  The camera to animate.
+   * @param  {object}  target  The coordinates to reach.
+   * @param  {?object} options Eventually an object to specify some options to
+   *                           the function. The available options are
+   *                           presented in the description of the function.
+   * @return {number}          The animation id, to make it easy to kill
+   *                           through the method "sigma.misc.animation.kill".
+   */
+  sigma.misc.animation.camera = function(camera, val, options) {
+    if (
+      !(camera instanceof sigma.classes.camera) ||
+      typeof val !== 'object' ||
+      !val
+    )
+      throw 'animation.camera: Wrong arguments.';
+
+    if (
+      typeof val.x !== 'number' &&
+      typeof val.y !== 'number' &&
+      typeof val.ratio !== 'number' &&
+      typeof val.angle !== 'number'
+    )
+      throw 'There must be at least one valid coordinate in the given val.';
+
+    var fn,
+        id,
+        anim,
+        easing,
+        duration,
+        initialVal,
+        o = options || {},
+        start = sigma.utils.dateNow();
+
+    // Store initial values:
+    initialVal = {
+      x: camera.x,
+      y: camera.y,
+      ratio: camera.ratio,
+      angle: camera.angle
+    };
+
+    duration = o.duration;
+    easing = typeof o.easing !== 'function' ?
+      sigma.utils.easings[o.easing || 'quadraticInOut'] :
+      o.easing;
+
+    fn = function() {
+      var coef,
+          t = o.duration ? (sigma.utils.dateNow() - start) / o.duration : 1;
+
+      // If the animation is over:
+      if (t >= 1) {
+        camera.isAnimated = false;
+        camera.goTo({
+          x: val.x !== undefined ? val.x : initialVal.x,
+          y: val.y !== undefined ? val.y : initialVal.y,
+          ratio: val.ratio !== undefined ? val.ratio : initialVal.ratio,
+          angle: val.angle !== undefined ? val.angle : initialVal.angle
+        });
+
+        cancelAnimationFrame(id);
+        delete sigma.misc.animation.running[id];
+
+        // Check callbacks:
+        if (typeof o.onComplete === 'function')
+          o.onComplete();
+
+      // Else, let's keep going:
+      } else {
+        coef = easing(t);
+        camera.isAnimated = true;
+        camera.goTo({
+          x: val.x !== undefined ?
+            initialVal.x + (val.x - initialVal.x) * coef :
+            initialVal.x,
+          y: val.y !== undefined ?
+            initialVal.y + (val.y - initialVal.y) * coef :
+            initialVal.y,
+          ratio: val.ratio !== undefined ?
+            initialVal.ratio + (val.ratio - initialVal.ratio) * coef :
+            initialVal.ratio,
+          angle: val.angle !== undefined ?
+            initialVal.angle + (val.angle - initialVal.angle) * coef :
+            initialVal.angle
+        });
+
+        // Check callbacks:
+        if (typeof o.onNewFrame === 'function')
+          o.onNewFrame();
+
+        anim.frameId = requestAnimationFrame(fn);
+      }
+    };
+
+    id = _getID();
+    anim = {
+      frameId: requestAnimationFrame(fn),
+      target: camera,
+      type: 'camera',
+      options: o,
+      fn: fn
+    };
+    sigma.misc.animation.running[id] = anim;
+
+    return id;
+  };
+
+  /**
+   * Kills a running animation. It triggers the eventual onComplete callback.
+   *
+   * @param  {number} id  The id of the animation to kill.
+   * @return {object}     Returns the sigma.misc.animation package.
+   */
+  sigma.misc.animation.kill = function(id) {
+    if (arguments.length !== 1 || typeof id !== 'number')
+      throw 'animation.kill: Wrong arguments.';
+
+    var o = sigma.misc.animation.running[id];
+
+    if (o) {
+      cancelAnimationFrame(id);
+      delete sigma.misc.animation.running[o.frameId];
+
+      if (o.type === 'camera')
+        o.target.isAnimated = false;
+
+      // Check callbacks:
+      if (typeof (o.options || {}).onComplete === 'function')
+        o.options.onComplete();
+    }
+
+    return this;
+  };
+
+  /**
+   * Kills every running animations, or only the one with the specified type,
+   * if a string parameter is given.
+   *
+   * @param  {?(string|object)} filter A string to filter the animations to kill
+   *                                   on their type (example: "camera"), or an
+   *                                   object to filter on their target.
+   * @return {number}                  Returns the number of animations killed
+   *                                   that way.
+   */
+  sigma.misc.animation.killAll = function(filter) {
+    var o,
+        id,
+        count = 0,
+        type = typeof filter === 'string' ? filter : null,
+        target = typeof filter === 'object' ? filter : null,
+        running = sigma.misc.animation.running;
+
+    for (id in running)
+      if (
+        (!type || running[id].type === type) &&
+        (!target || running[id].target === target)
+      ) {
+        o = sigma.misc.animation.running[id];
+        cancelAnimationFrame(o.frameId);
+        delete sigma.misc.animation.running[id];
+
+        if (o.type === 'camera')
+          o.target.isAnimated = false;
+
+        // Increment counter:
+        count++;
+
+        // Check callbacks:
+        if (typeof (o.options || {}).onComplete === 'function')
+          o.options.onComplete();
+      }
+
+    return count;
+  };
+
+  /**
+   * Returns "true" if any animation that is currently still running matches
+   * the filter given to the function.
+   *
+   * @param  {string|object} filter A string to filter the animations to kill
+   *                                on their type (example: "camera"), or an
+   *                                object to filter on their target.
+   * @return {boolean}              Returns true if any running animation
+   *                                matches.
+   */
+  sigma.misc.animation.has = function(filter) {
+    var id,
+        type = typeof filter === 'string' ? filter : null,
+        target = typeof filter === 'object' ? filter : null,
+        running = sigma.misc.animation.running;
+
+    for (id in running)
+      if (
+        (!type || running[id].type === type) &&
+        (!target || running[id].target === target)
+      )
+        return true;
+
+    return false;
+  };
+}).call(this);
diff --git a/demo/static/sigmajs/misc/sigma.misc.bindDOMEvents.js b/demo/static/sigmajs/misc/sigma.misc.bindDOMEvents.js
new file mode 100644
index 000000000..1e758e848
--- /dev/null
+++ b/demo/static/sigmajs/misc/sigma.misc.bindDOMEvents.js
@@ -0,0 +1,156 @@
+;(function(undefined) {
+  'use strict';
+
+  if (typeof sigma === 'undefined')
+    throw 'sigma is not declared';
+
+  // Initialize packages:
+  sigma.utils.pkg('sigma.misc');
+
+  /**
+   * This helper will bind any DOM renderer (for instance svg)
+   * to its captors, to properly dispatch the good events to the sigma instance
+   * to manage clicking, hovering etc...
+   *
+   * It has to be called in the scope of the related renderer.
+   */
+  sigma.misc.bindDOMEvents = function(container) {
+    var self = this,
+        graph = this.graph;
+
+    // DOMElement abstraction
+    function Element(domElement) {
+
+      // Helpers
+      this.attr = function(attrName) {
+        return domElement.getAttributeNS(null, attrName);
+      };
+
+      // Properties
+      this.tag = domElement.tagName;
+      this.class = this.attr('class');
+      this.id = this.attr('id');
+
+      // Methods
+      this.isNode = function() {
+        return !!~this.class.indexOf(self.settings('classPrefix') + '-node');
+      };
+
+      this.isEdge = function() {
+        return !!~this.class.indexOf(self.settings('classPrefix') + '-edge');
+      };
+
+      this.isHover = function() {
+        return !!~this.class.indexOf(self.settings('classPrefix') + '-hover');
+      };
+    }
+
+    // Click
+    function click(e) {
+      if (!self.settings('eventsEnabled'))
+        return;
+
+      // Generic event
+      self.dispatchEvent('click', e);
+
+      // Are we on a node?
+      var element = new Element(e.target);
+
+      if (element.isNode())
+        self.dispatchEvent('clickNode', {
+          node: graph.nodes(element.attr('data-node-id'))
+        });
+      else
+        self.dispatchEvent('clickStage');
+
+      e.preventDefault();
+      e.stopPropagation();
+    }
+
+    // Double click
+    function doubleClick(e) {
+      if (!self.settings('eventsEnabled'))
+        return;
+
+      // Generic event
+      self.dispatchEvent('doubleClick', e);
+
+      // Are we on a node?
+      var element = new Element(e.target);
+
+      if (element.isNode())
+        self.dispatchEvent('doubleClickNode', {
+          node: graph.nodes(element.attr('data-node-id'))
+        });
+      else
+        self.dispatchEvent('doubleClickStage');
+
+      e.preventDefault();
+      e.stopPropagation();
+    }
+
+    // On over
+    function onOver(e) {
+      var target = e.toElement || e.target;
+
+      if (!self.settings('eventsEnabled') || !target)
+        return;
+
+      var el = new Element(target);
+
+      if (el.isNode()) {
+        self.dispatchEvent('overNode', {
+          node: graph.nodes(el.attr('data-node-id'))
+        });
+      }
+      else if (el.isEdge()) {
+        var edge = graph.edges(el.attr('data-edge-id'));
+        self.dispatchEvent('overEdge', {
+          edge: edge,
+          source: graph.nodes(edge.source),
+          target: graph.nodes(edge.target)
+        });
+      }
+    }
+
+    // On out
+    function onOut(e) {
+      var target = e.fromElement || e.originalTarget;
+
+      if (!self.settings('eventsEnabled'))
+        return;
+
+      var el = new Element(target);
+
+      if (el.isNode()) {
+        self.dispatchEvent('outNode', {
+          node: graph.nodes(el.attr('data-node-id'))
+        });
+      }
+      else if (el.isEdge()) {
+        var edge = graph.edges(el.attr('data-edge-id'));
+        self.dispatchEvent('outEdge', {
+          edge: edge,
+          source: graph.nodes(edge.source),
+          target: graph.nodes(edge.target)
+        });
+      }
+    }
+
+    // Registering Events:
+
+    // Click
+    container.addEventListener('click', click, false);
+    sigma.utils.doubleClick(container, 'click', doubleClick);
+
+    // Touch counterparts
+    container.addEventListener('touchstart', click, false);
+    sigma.utils.doubleClick(container, 'touchstart', doubleClick);
+
+    // Mouseover
+    container.addEventListener('mouseover', onOver, true);
+
+    // Mouseout
+    container.addEventListener('mouseout', onOut, true);
+  };
+}).call(this);
diff --git a/demo/static/sigmajs/misc/sigma.misc.bindEvents.js b/demo/static/sigmajs/misc/sigma.misc.bindEvents.js
new file mode 100644
index 000000000..e87ad028d
--- /dev/null
+++ b/demo/static/sigmajs/misc/sigma.misc.bindEvents.js
@@ -0,0 +1,509 @@
+;(function(undefined) {
+  'use strict';
+
+  if (typeof sigma === 'undefined')
+    throw 'sigma is not declared';
+
+  // Initialize packages:
+  sigma.utils.pkg('sigma.misc');
+
+  /**
+   * This helper will bind any no-DOM renderer (for instance canvas or WebGL)
+   * to its captors, to properly dispatch the good events to the sigma instance
+   * to manage clicking, hovering etc...
+   *
+   * It has to be called in the scope of the related renderer.
+   */
+  sigma.misc.bindEvents = function(prefix) {
+    var i,
+        l,
+        mX,
+        mY,
+        captor,
+        self = this;
+
+    function getNodes(e) {
+      if (e) {
+        mX = 'x' in e.data ? e.data.x : mX;
+        mY = 'y' in e.data ? e.data.y : mY;
+      }
+
+      var i,
+          j,
+          l,
+          n,
+          x,
+          y,
+          s,
+          inserted,
+          selected = [],
+          modifiedX = mX + self.width / 2,
+          modifiedY = mY + self.height / 2,
+          point = self.camera.cameraPosition(
+            mX,
+            mY
+          ),
+          nodes = self.camera.quadtree.point(
+            point.x,
+            point.y
+          );
+
+      if (nodes.length)
+        for (i = 0, l = nodes.length; i < l; i++) {
+          n = nodes[i];
+          x = n[prefix + 'x'];
+          y = n[prefix + 'y'];
+          s = n[prefix + 'size'];
+
+          if (
+            !n.hidden &&
+            modifiedX > x - s &&
+            modifiedX < x + s &&
+            modifiedY > y - s &&
+            modifiedY < y + s &&
+            Math.sqrt(
+              Math.pow(modifiedX - x, 2) +
+              Math.pow(modifiedY - y, 2)
+            ) < s
+          ) {
+            // Insert the node:
+            inserted = false;
+
+            for (j = 0; j < selected.length; j++)
+              if (n.size > selected[j].size) {
+                selected.splice(j, 0, n);
+                inserted = true;
+                break;
+              }
+
+            if (!inserted)
+              selected.push(n);
+          }
+        }
+
+      return selected;
+    }
+
+
+    function getEdges(e) {
+      if (!self.settings('enableEdgeHovering')) {
+        // No event if the setting is off:
+        return [];
+      }
+
+      var isCanvas = (
+        sigma.renderers.canvas && self instanceof sigma.renderers.canvas);
+
+      if (!isCanvas) {
+        // A quick hardcoded rule to prevent people from using this feature
+        // with the WebGL renderer (which is not good enough at the moment):
+        throw new Error(
+          'The edge events feature is not compatible with the WebGL renderer'
+        );
+      }
+
+      if (e) {
+        mX = 'x' in e.data ? e.data.x : mX;
+        mY = 'y' in e.data ? e.data.y : mY;
+      }
+
+      var i,
+          j,
+          l,
+          a,
+          edge,
+          s,
+          maxEpsilon = self.settings('edgeHoverPrecision'),
+          source,
+          target,
+          cp,
+          nodeIndex = {},
+          inserted,
+          selected = [],
+          modifiedX = mX + self.width / 2,
+          modifiedY = mY + self.height / 2,
+          point = self.camera.cameraPosition(
+            mX,
+            mY
+          ),
+          edges = [];
+
+      if (isCanvas) {
+        var nodesOnScreen = self.camera.quadtree.area(
+          self.camera.getRectangle(self.width, self.height)
+        );
+        for (a = nodesOnScreen, i = 0, l = a.length; i < l; i++)
+          nodeIndex[a[i].id] = a[i];
+      }
+
+      if (self.camera.edgequadtree !== undefined) {
+        edges = self.camera.edgequadtree.point(
+          point.x,
+          point.y
+        );
+      }
+
+      function insertEdge(selected, edge) {
+        inserted = false;
+
+        for (j = 0; j < selected.length; j++)
+          if (edge.size > selected[j].size) {
+            selected.splice(j, 0, edge);
+            inserted = true;
+            break;
+          }
+
+        if (!inserted)
+          selected.push(edge);
+      }
+
+      if (edges.length)
+        for (i = 0, l = edges.length; i < l; i++) {
+          edge = edges[i];
+          source = self.graph.nodes(edge.source);
+          target = self.graph.nodes(edge.target);
+          // (HACK) we can't get edge[prefix + 'size'] on WebGL renderer:
+          s = edge[prefix + 'size'] ||
+              edge['read_' + prefix + 'size'];
+
+          // First, let's identify which edges are drawn. To do this, we keep
+          // every edges that have at least one extremity displayed according to
+          // the quadtree and the "hidden" attribute. We also do not keep hidden
+          // edges.
+          // Then, let's check if the mouse is on the edge (we suppose that it
+          // is a line segment).
+
+          if (
+            !edge.hidden &&
+            !source.hidden && !target.hidden &&
+            (!isCanvas ||
+              (nodeIndex[edge.source] || nodeIndex[edge.target])) &&
+            sigma.utils.getDistance(
+              source[prefix + 'x'],
+              source[prefix + 'y'],
+              modifiedX,
+              modifiedY) > source[prefix + 'size'] &&
+            sigma.utils.getDistance(
+              target[prefix + 'x'],
+              target[prefix + 'y'],
+              modifiedX,
+              modifiedY) > target[prefix + 'size']
+          ) {
+            if (edge.type == 'curve' || edge.type == 'curvedArrow') {
+              if (source.id === target.id) {
+                cp = sigma.utils.getSelfLoopControlPoints(
+                  source[prefix + 'x'],
+                  source[prefix + 'y'],
+                  source[prefix + 'size']
+                );
+                if (
+                  sigma.utils.isPointOnBezierCurve(
+                  modifiedX,
+                  modifiedY,
+                  source[prefix + 'x'],
+                  source[prefix + 'y'],
+                  target[prefix + 'x'],
+                  target[prefix + 'y'],
+                  cp.x1,
+                  cp.y1,
+                  cp.x2,
+                  cp.y2,
+                  Math.max(s, maxEpsilon)
+                )) {
+                  insertEdge(selected, edge);
+                }
+              }
+              else {
+                cp = sigma.utils.getQuadraticControlPoint(
+                  source[prefix + 'x'],
+                  source[prefix + 'y'],
+                  target[prefix + 'x'],
+                  target[prefix + 'y']);
+                if (
+                  sigma.utils.isPointOnQuadraticCurve(
+                  modifiedX,
+                  modifiedY,
+                  source[prefix + 'x'],
+                  source[prefix + 'y'],
+                  target[prefix + 'x'],
+                  target[prefix + 'y'],
+                  cp.x,
+                  cp.y,
+                  Math.max(s, maxEpsilon)
+                )) {
+                  insertEdge(selected, edge);
+                }
+              }
+            } else if (
+                sigma.utils.isPointOnSegment(
+                modifiedX,
+                modifiedY,
+                source[prefix + 'x'],
+                source[prefix + 'y'],
+                target[prefix + 'x'],
+                target[prefix + 'y'],
+                Math.max(s, maxEpsilon)
+              )) {
+              insertEdge(selected, edge);
+            }
+          }
+        }
+
+      return selected;
+    }
+
+
+    function bindCaptor(captor) {
+      var nodes,
+          edges,
+          overNodes = {},
+          overEdges = {};
+
+      function onClick(e) {
+        if (!self.settings('eventsEnabled'))
+          return;
+
+        self.dispatchEvent('click', e.data);
+
+        nodes = getNodes(e);
+        edges = getEdges(e);
+
+        if (nodes.length) {
+          self.dispatchEvent('clickNode', {
+            node: nodes[0],
+            captor: e.data
+          });
+          self.dispatchEvent('clickNodes', {
+            node: nodes,
+            captor: e.data
+          });
+        } else if (edges.length) {
+          self.dispatchEvent('clickEdge', {
+            edge: edges[0],
+            captor: e.data
+          });
+          self.dispatchEvent('clickEdges', {
+            edge: edges,
+            captor: e.data
+          });
+        } else
+          self.dispatchEvent('clickStage', {captor: e.data});
+      }
+
+      function onDoubleClick(e) {
+        if (!self.settings('eventsEnabled'))
+          return;
+
+        self.dispatchEvent('doubleClick', e.data);
+
+        nodes = getNodes(e);
+        edges = getEdges(e);
+
+        if (nodes.length) {
+          self.dispatchEvent('doubleClickNode', {
+            node: nodes[0],
+            captor: e.data
+          });
+          self.dispatchEvent('doubleClickNodes', {
+            node: nodes,
+            captor: e.data
+          });
+        } else if (edges.length) {
+          self.dispatchEvent('doubleClickEdge', {
+            edge: edges[0],
+            captor: e.data
+          });
+          self.dispatchEvent('doubleClickEdges', {
+            edge: edges,
+            captor: e.data
+          });
+        } else
+          self.dispatchEvent('doubleClickStage', {captor: e.data});
+      }
+
+      function onRightClick(e) {
+        if (!self.settings('eventsEnabled'))
+          return;
+
+        self.dispatchEvent('rightClick', e.data);
+
+        nodes = getNodes(e);
+        edges = getEdges(e);
+
+        if (nodes.length) {
+          self.dispatchEvent('rightClickNode', {
+            node: nodes[0],
+            captor: e.data
+          });
+          self.dispatchEvent('rightClickNodes', {
+            node: nodes,
+            captor: e.data
+          });
+        } else if (edges.length) {
+          self.dispatchEvent('rightClickEdge', {
+            edge: edges[0],
+            captor: e.data
+          });
+          self.dispatchEvent('rightClickEdges', {
+            edge: edges,
+            captor: e.data
+          });
+        } else
+          self.dispatchEvent('rightClickStage', {captor: e.data});
+      }
+
+      function onOut(e) {
+        if (!self.settings('eventsEnabled'))
+          return;
+
+        var k,
+            i,
+            l,
+            le,
+            outNodes = [],
+            outEdges = [];
+
+        for (k in overNodes)
+          outNodes.push(overNodes[k]);
+
+        overNodes = {};
+        // Dispatch both single and multi events:
+        for (i = 0, l = outNodes.length; i < l; i++)
+          self.dispatchEvent('outNode', {
+            node: outNodes[i],
+            captor: e.data
+          });
+        if (outNodes.length)
+          self.dispatchEvent('outNodes', {
+            nodes: outNodes,
+            captor: e.data
+          });
+
+        overEdges = {};
+        // Dispatch both single and multi events:
+        for (i = 0, le = outEdges.length; i < le; i++)
+          self.dispatchEvent('outEdge', {
+            edge: outEdges[i],
+            captor: e.data
+          });
+        if (outEdges.length)
+          self.dispatchEvent('outEdges', {
+            edges: outEdges,
+            captor: e.data
+          });
+      }
+
+      function onMove(e) {
+        if (!self.settings('eventsEnabled'))
+          return;
+
+        nodes = getNodes(e);
+        edges = getEdges(e);
+
+        var i,
+            k,
+            node,
+            edge,
+            newOutNodes = [],
+            newOverNodes = [],
+            currentOverNodes = {},
+            l = nodes.length,
+            newOutEdges = [],
+            newOverEdges = [],
+            currentOverEdges = {},
+            le = edges.length;
+
+        // Check newly overred nodes:
+        for (i = 0; i < l; i++) {
+          node = nodes[i];
+          currentOverNodes[node.id] = node;
+          if (!overNodes[node.id]) {
+            newOverNodes.push(node);
+            overNodes[node.id] = node;
+          }
+        }
+
+        // Check no more overred nodes:
+        for (k in overNodes)
+          if (!currentOverNodes[k]) {
+            newOutNodes.push(overNodes[k]);
+            delete overNodes[k];
+          }
+
+        // Dispatch both single and multi events:
+        for (i = 0, l = newOverNodes.length; i < l; i++)
+          self.dispatchEvent('overNode', {
+            node: newOverNodes[i],
+            captor: e.data
+          });
+        for (i = 0, l = newOutNodes.length; i < l; i++)
+          self.dispatchEvent('outNode', {
+            node: newOutNodes[i],
+            captor: e.data
+          });
+        if (newOverNodes.length)
+          self.dispatchEvent('overNodes', {
+            nodes: newOverNodes,
+            captor: e.data
+          });
+        if (newOutNodes.length)
+          self.dispatchEvent('outNodes', {
+            nodes: newOutNodes,
+            captor: e.data
+          });
+
+        // Check newly overred edges:
+        for (i = 0; i < le; i++) {
+          edge = edges[i];
+          currentOverEdges[edge.id] = edge;
+          if (!overEdges[edge.id]) {
+            newOverEdges.push(edge);
+            overEdges[edge.id] = edge;
+          }
+        }
+
+        // Check no more overred edges:
+        for (k in overEdges)
+          if (!currentOverEdges[k]) {
+            newOutEdges.push(overEdges[k]);
+            delete overEdges[k];
+          }
+
+        // Dispatch both single and multi events:
+        for (i = 0, le = newOverEdges.length; i < le; i++)
+          self.dispatchEvent('overEdge', {
+            edge: newOverEdges[i],
+            captor: e.data
+          });
+        for (i = 0, le = newOutEdges.length; i < le; i++)
+          self.dispatchEvent('outEdge', {
+            edge: newOutEdges[i],
+            captor: e.data
+          });
+        if (newOverEdges.length)
+          self.dispatchEvent('overEdges', {
+            edges: newOverEdges,
+            captor: e.data
+          });
+        if (newOutEdges.length)
+          self.dispatchEvent('outEdges', {
+            edges: newOutEdges,
+            captor: e.data
+          });
+      }
+
+      // Bind events:
+      captor.bind('click', onClick);
+      captor.bind('mousedown', onMove);
+      captor.bind('mouseup', onMove);
+      captor.bind('mousemove', onMove);
+      captor.bind('mouseout', onOut);
+      captor.bind('doubleclick', onDoubleClick);
+      captor.bind('rightclick', onRightClick);
+      self.bind('render', onMove);
+    }
+
+    for (i = 0, l = this.captors.length; i < l; i++)
+      bindCaptor(this.captors[i]);
+  };
+}).call(this);
diff --git a/demo/static/sigmajs/misc/sigma.misc.drawHovers.js b/demo/static/sigmajs/misc/sigma.misc.drawHovers.js
new file mode 100644
index 000000000..fa95c4195
--- /dev/null
+++ b/demo/static/sigmajs/misc/sigma.misc.drawHovers.js
@@ -0,0 +1,220 @@
+;(function(undefined) {
+  'use strict';
+
+  if (typeof sigma === 'undefined')
+    throw 'sigma is not declared';
+
+  // Initialize packages:
+  sigma.utils.pkg('sigma.misc');
+
+  /**
+   * This method listens to "overNode", "outNode", "overEdge" and "outEdge"
+   * events from a renderer and renders the nodes differently on the top layer.
+   * The goal is to make any node label readable with the mouse, and to
+   * highlight hovered nodes and edges.
+   *
+   * It has to be called in the scope of the related renderer.
+   */
+  sigma.misc.drawHovers = function(prefix) {
+    var self = this,
+        hoveredNodes = {},
+        hoveredEdges = {};
+
+    this.bind('overNode', function(event) {
+      var node = event.data.node;
+      if (!node.hidden) {
+        hoveredNodes[node.id] = node;
+        draw();
+      }
+    });
+
+    this.bind('outNode', function(event) {
+      delete hoveredNodes[event.data.node.id];
+      draw();
+    });
+
+    this.bind('overEdge', function(event) {
+      var edge = event.data.edge;
+      if (!edge.hidden) {
+        hoveredEdges[edge.id] = edge;
+        draw();
+      }
+    });
+
+    this.bind('outEdge', function(event) {
+      delete hoveredEdges[event.data.edge.id];
+      draw();
+    });
+
+    this.bind('render', function(event) {
+      draw();
+    });
+
+    function draw() {
+      // Clear self.contexts.hover:
+      self.contexts.hover.canvas.width = self.contexts.hover.canvas.width;
+
+      var k,
+          source,
+          target,
+          hoveredNode,
+          hoveredEdge,
+          defaultNodeType = self.settings('defaultNodeType'),
+          defaultEdgeType = self.settings('defaultEdgeType'),
+          nodeRenderers = sigma.canvas.hovers,
+          edgeRenderers = sigma.canvas.edgehovers,
+          extremitiesRenderers = sigma.canvas.extremities,
+          embedSettings = self.settings.embedObjects({
+            prefix: prefix
+          });
+
+      // Node render: single hover
+      if (
+        embedSettings('enableHovering') &&
+        embedSettings('singleHover') &&
+        Object.keys(hoveredNodes).length
+      ) {
+        hoveredNode = hoveredNodes[Object.keys(hoveredNodes)[0]];
+        (
+          nodeRenderers[hoveredNode.type] ||
+          nodeRenderers[defaultNodeType] ||
+          nodeRenderers.def
+        )(
+          hoveredNode,
+          self.contexts.hover,
+          embedSettings
+        );
+      }
+
+      // Node render: multiple hover
+      if (
+        embedSettings('enableHovering') &&
+        !embedSettings('singleHover')
+      )
+        for (k in hoveredNodes)
+          (
+            nodeRenderers[hoveredNodes[k].type] ||
+            nodeRenderers[defaultNodeType] ||
+            nodeRenderers.def
+          )(
+            hoveredNodes[k],
+            self.contexts.hover,
+            embedSettings
+          );
+
+      // Edge render: single hover
+      if (
+        embedSettings('enableEdgeHovering') &&
+        embedSettings('singleHover') &&
+        Object.keys(hoveredEdges).length
+      ) {
+        hoveredEdge = hoveredEdges[Object.keys(hoveredEdges)[0]];
+        source = self.graph.nodes(hoveredEdge.source);
+        target = self.graph.nodes(hoveredEdge.target);
+
+        if (! hoveredEdge.hidden) {
+          (
+            edgeRenderers[hoveredEdge.type] ||
+            edgeRenderers[defaultEdgeType] ||
+            edgeRenderers.def
+          ) (
+            hoveredEdge,
+            source,
+            target,
+            self.contexts.hover,
+            embedSettings
+          );
+
+          if (embedSettings('edgeHoverExtremities')) {
+            (
+              extremitiesRenderers[hoveredEdge.type] ||
+              extremitiesRenderers.def
+            )(
+              hoveredEdge,
+              source,
+              target,
+              self.contexts.hover,
+              embedSettings
+            );
+
+          } else {
+            // Avoid edges rendered over nodes:
+            (
+              sigma.canvas.nodes[source.type] ||
+              sigma.canvas.nodes.def
+            ) (
+              source,
+              self.contexts.hover,
+              embedSettings
+            );
+            (
+              sigma.canvas.nodes[target.type] ||
+              sigma.canvas.nodes.def
+            ) (
+              target,
+              self.contexts.hover,
+              embedSettings
+            );
+          }
+        }
+      }
+
+      // Edge render: multiple hover
+      if (
+        embedSettings('enableEdgeHovering') &&
+        !embedSettings('singleHover')
+      ) {
+        for (k in hoveredEdges) {
+          hoveredEdge = hoveredEdges[k];
+          source = self.graph.nodes(hoveredEdge.source);
+          target = self.graph.nodes(hoveredEdge.target);
+
+          if (!hoveredEdge.hidden) {
+            (
+              edgeRenderers[hoveredEdge.type] ||
+              edgeRenderers[defaultEdgeType] ||
+              edgeRenderers.def
+            ) (
+              hoveredEdge,
+              source,
+              target,
+              self.contexts.hover,
+              embedSettings
+            );
+
+            if (embedSettings('edgeHoverExtremities')) {
+              (
+                extremitiesRenderers[hoveredEdge.type] ||
+                extremitiesRenderers.def
+              )(
+                hoveredEdge,
+                source,
+                target,
+                self.contexts.hover,
+                embedSettings
+              );
+            } else {
+              // Avoid edges rendered over nodes:
+              (
+                sigma.canvas.nodes[source.type] ||
+                sigma.canvas.nodes.def
+              ) (
+                source,
+                self.contexts.hover,
+                embedSettings
+              );
+              (
+                sigma.canvas.nodes[target.type] ||
+                sigma.canvas.nodes.def
+              ) (
+                target,
+                self.contexts.hover,
+                embedSettings
+              );
+            }
+          }
+        }
+      }
+    }
+  };
+}).call(this);
diff --git a/demo/static/sigmajs/renderers/canvas/sigma.canvas.edgehovers.arrow.js b/demo/static/sigmajs/renderers/canvas/sigma.canvas.edgehovers.arrow.js
new file mode 100644
index 000000000..1be0cc4ab
--- /dev/null
+++ b/demo/static/sigmajs/renderers/canvas/sigma.canvas.edgehovers.arrow.js
@@ -0,0 +1,76 @@
+;(function() {
+  'use strict';
+
+  sigma.utils.pkg('sigma.canvas.edgehovers');
+
+  /**
+   * This hover renderer will display the edge with a different color or size.
+   *
+   * @param  {object}                   edge         The edge object.
+   * @param  {object}                   source node  The edge source node.
+   * @param  {object}                   target node  The edge target node.
+   * @param  {CanvasRenderingContext2D} context      The canvas context.
+   * @param  {configurable}             settings     The settings function.
+   */
+  sigma.canvas.edgehovers.arrow =
+    function(edge, source, target, context, settings) {
+    var color = edge.color,
+        prefix = settings('prefix') || '',
+        edgeColor = settings('edgeColor'),
+        defaultNodeColor = settings('defaultNodeColor'),
+        defaultEdgeColor = settings('defaultEdgeColor'),
+        size = edge[prefix + 'size'] || 1,
+        tSize = target[prefix + 'size'],
+        sX = source[prefix + 'x'],
+        sY = source[prefix + 'y'],
+        tX = target[prefix + 'x'],
+        tY = target[prefix + 'y'];
+
+    size = (edge.hover) ?
+      settings('edgeHoverSizeRatio') * size : size;
+    var aSize = size * 2.5,
+        d = Math.sqrt(Math.pow(tX - sX, 2) + Math.pow(tY - sY, 2)),
+        aX = sX + (tX - sX) * (d - aSize - tSize) / d,
+        aY = sY + (tY - sY) * (d - aSize - tSize) / d,
+        vX = (tX - sX) * aSize / d,
+        vY = (tY - sY) * aSize / d;
+
+    if (!color)
+      switch (edgeColor) {
+        case 'source':
+          color = source.color || defaultNodeColor;
+          break;
+        case 'target':
+          color = target.color || defaultNodeColor;
+          break;
+        default:
+          color = defaultEdgeColor;
+          break;
+      }
+
+    if (settings('edgeHoverColor') === 'edge') {
+      color = edge.hover_color || color;
+    } else {
+      color = edge.hover_color || settings('defaultEdgeHoverColor') || color;
+    }
+
+    context.strokeStyle = color;
+    context.lineWidth = size;
+    context.beginPath();
+    context.moveTo(sX, sY);
+    context.lineTo(
+      aX,
+      aY
+    );
+    context.stroke();
+
+    context.fillStyle = color;
+    context.beginPath();
+    context.moveTo(aX + vX, aY + vY);
+    context.lineTo(aX + vY * 0.6, aY - vX * 0.6);
+    context.lineTo(aX - vY * 0.6, aY + vX * 0.6);
+    context.lineTo(aX + vX, aY + vY);
+    context.closePath();
+    context.fill();
+  };
+})();
diff --git a/demo/static/sigmajs/renderers/canvas/sigma.canvas.edgehovers.curve.js b/demo/static/sigmajs/renderers/canvas/sigma.canvas.edgehovers.curve.js
new file mode 100644
index 000000000..f79abf8ab
--- /dev/null
+++ b/demo/static/sigmajs/renderers/canvas/sigma.canvas.edgehovers.curve.js
@@ -0,0 +1,64 @@
+;(function() {
+  'use strict';
+
+  sigma.utils.pkg('sigma.canvas.edgehovers');
+
+  /**
+   * This hover renderer will display the edge with a different color or size.
+   *
+   * @param  {object}                   edge         The edge object.
+   * @param  {object}                   source node  The edge source node.
+   * @param  {object}                   target node  The edge target node.
+   * @param  {CanvasRenderingContext2D} context      The canvas context.
+   * @param  {configurable}             settings     The settings function.
+   */
+  sigma.canvas.edgehovers.curve =
+    function(edge, source, target, context, settings) {
+    var color = edge.color,
+        prefix = settings('prefix') || '',
+        size = settings('edgeHoverSizeRatio') * (edge[prefix + 'size'] || 1),
+        edgeColor = settings('edgeColor'),
+        defaultNodeColor = settings('defaultNodeColor'),
+        defaultEdgeColor = settings('defaultEdgeColor'),
+        cp = {},
+        sSize = source[prefix + 'size'],
+        sX = source[prefix + 'x'],
+        sY = source[prefix + 'y'],
+        tX = target[prefix + 'x'],
+        tY = target[prefix + 'y'];
+
+    cp = (source.id === target.id) ?
+      sigma.utils.getSelfLoopControlPoints(sX, sY, sSize) :
+      sigma.utils.getQuadraticControlPoint(sX, sY, tX, tY);
+
+    if (!color)
+      switch (edgeColor) {
+        case 'source':
+          color = source.color || defaultNodeColor;
+          break;
+        case 'target':
+          color = target.color || defaultNodeColor;
+          break;
+        default:
+          color = defaultEdgeColor;
+          break;
+      }
+
+    if (settings('edgeHoverColor') === 'edge') {
+      color = edge.hover_color || color;
+    } else {
+      color = edge.hover_color || settings('defaultEdgeHoverColor') || color;
+    }
+
+    context.strokeStyle = color;
+    context.lineWidth = size;
+    context.beginPath();
+    context.moveTo(sX, sY);
+    if (source.id === target.id) {
+      context.bezierCurveTo(cp.x1, cp.y1, cp.x2, cp.y2, tX, tY);
+    } else {
+      context.quadraticCurveTo(cp.x, cp.y, tX, tY);
+    }
+    context.stroke();
+  };
+})();
diff --git a/demo/static/sigmajs/renderers/canvas/sigma.canvas.edgehovers.curvedArrow.js b/demo/static/sigmajs/renderers/canvas/sigma.canvas.edgehovers.curvedArrow.js
new file mode 100644
index 000000000..6a34b7748
--- /dev/null
+++ b/demo/static/sigmajs/renderers/canvas/sigma.canvas.edgehovers.curvedArrow.js
@@ -0,0 +1,96 @@
+;(function() {
+  'use strict';
+
+  sigma.utils.pkg('sigma.canvas.edgehovers');
+
+  /**
+   * This hover renderer will display the edge with a different color or size.
+   *
+   * @param  {object}                   edge         The edge object.
+   * @param  {object}                   source node  The edge source node.
+   * @param  {object}                   target node  The edge target node.
+   * @param  {CanvasRenderingContext2D} context      The canvas context.
+   * @param  {configurable}             settings     The settings function.
+   */
+  sigma.canvas.edgehovers.curvedArrow =
+    function(edge, source, target, context, settings) {
+    var color = edge.color,
+        prefix = settings('prefix') || '',
+        edgeColor = settings('edgeColor'),
+        defaultNodeColor = settings('defaultNodeColor'),
+        defaultEdgeColor = settings('defaultEdgeColor'),
+        cp = {},
+        size = settings('edgeHoverSizeRatio') * (edge[prefix + 'size'] || 1),
+        tSize = target[prefix + 'size'],
+        sX = source[prefix + 'x'],
+        sY = source[prefix + 'y'],
+        tX = target[prefix + 'x'],
+        tY = target[prefix + 'y'],
+        d,
+        aSize,
+        aX,
+        aY,
+        vX,
+        vY;
+
+    cp = (source.id === target.id) ?
+      sigma.utils.getSelfLoopControlPoints(sX, sY, tSize) :
+      sigma.utils.getQuadraticControlPoint(sX, sY, tX, tY);
+
+    if (source.id === target.id) {
+      d = Math.sqrt(Math.pow(tX - cp.x1, 2) + Math.pow(tY - cp.y1, 2));
+      aSize = size * 2.5;
+      aX = cp.x1 + (tX - cp.x1) * (d - aSize - tSize) / d;
+      aY = cp.y1 + (tY - cp.y1) * (d - aSize - tSize) / d;
+      vX = (tX - cp.x1) * aSize / d;
+      vY = (tY - cp.y1) * aSize / d;
+    }
+    else {
+      d = Math.sqrt(Math.pow(tX - cp.x, 2) + Math.pow(tY - cp.y, 2));
+      aSize = size * 2.5;
+      aX = cp.x + (tX - cp.x) * (d - aSize - tSize) / d;
+      aY = cp.y + (tY - cp.y) * (d - aSize - tSize) / d;
+      vX = (tX - cp.x) * aSize / d;
+      vY = (tY - cp.y) * aSize / d;
+    }
+
+    if (!color)
+      switch (edgeColor) {
+        case 'source':
+          color = source.color || defaultNodeColor;
+          break;
+        case 'target':
+          color = target.color || defaultNodeColor;
+          break;
+        default:
+          color = defaultEdgeColor;
+          break;
+      }
+
+    if (settings('edgeHoverColor') === 'edge') {
+      color = edge.hover_color || color;
+    } else {
+      color = edge.hover_color || settings('defaultEdgeHoverColor') || color;
+    }
+
+    context.strokeStyle = color;
+    context.lineWidth = size;
+    context.beginPath();
+    context.moveTo(sX, sY);
+    if (source.id === target.id) {
+      context.bezierCurveTo(cp.x2, cp.y2, cp.x1, cp.y1, aX, aY);
+    } else {
+      context.quadraticCurveTo(cp.x, cp.y, aX, aY);
+    }
+    context.stroke();
+
+    context.fillStyle = color;
+    context.beginPath();
+    context.moveTo(aX + vX, aY + vY);
+    context.lineTo(aX + vY * 0.6, aY - vX * 0.6);
+    context.lineTo(aX - vY * 0.6, aY + vX * 0.6);
+    context.lineTo(aX + vX, aY + vY);
+    context.closePath();
+    context.fill();
+  };
+})();
diff --git a/demo/static/sigmajs/renderers/canvas/sigma.canvas.edgehovers.def.js b/demo/static/sigmajs/renderers/canvas/sigma.canvas.edgehovers.def.js
new file mode 100644
index 000000000..d88ad387f
--- /dev/null
+++ b/demo/static/sigmajs/renderers/canvas/sigma.canvas.edgehovers.def.js
@@ -0,0 +1,57 @@
+;(function() {
+  'use strict';
+
+  sigma.utils.pkg('sigma.canvas.edgehovers');
+
+  /**
+   * This hover renderer will display the edge with a different color or size.
+   *
+   * @param  {object}                   edge         The edge object.
+   * @param  {object}                   source node  The edge source node.
+   * @param  {object}                   target node  The edge target node.
+   * @param  {CanvasRenderingContext2D} context      The canvas context.
+   * @param  {configurable}             settings     The settings function.
+   */
+  sigma.canvas.edgehovers.def =
+    function(edge, source, target, context, settings) {
+      var color = edge.color,
+        prefix = settings('prefix') || '',
+        size = edge[prefix + 'size'] || 1,
+        edgeColor = settings('edgeColor'),
+        defaultNodeColor = settings('defaultNodeColor'),
+        defaultEdgeColor = settings('defaultEdgeColor');
+
+    if (!color)
+      switch (edgeColor) {
+        case 'source':
+          color = source.color || defaultNodeColor;
+          break;
+        case 'target':
+          color = target.color || defaultNodeColor;
+          break;
+        default:
+          color = defaultEdgeColor;
+          break;
+      }
+
+    if (settings('edgeHoverColor') === 'edge') {
+      color = edge.hover_color || color;
+    } else {
+      color = edge.hover_color || settings('defaultEdgeHoverColor') || color;
+    }
+    size *= settings('edgeHoverSizeRatio');
+
+    context.strokeStyle = color;
+    context.lineWidth = size;
+    context.beginPath();
+    context.moveTo(
+      source[prefix + 'x'],
+      source[prefix + 'y']
+    );
+    context.lineTo(
+      target[prefix + 'x'],
+      target[prefix + 'y']
+    );
+    context.stroke();
+  };
+})();
diff --git a/demo/static/sigmajs/renderers/canvas/sigma.canvas.edges.arrow.js b/demo/static/sigmajs/renderers/canvas/sigma.canvas.edges.arrow.js
new file mode 100644
index 000000000..4f12977a8
--- /dev/null
+++ b/demo/static/sigmajs/renderers/canvas/sigma.canvas.edges.arrow.js
@@ -0,0 +1,66 @@
+;(function() {
+  'use strict';
+
+  sigma.utils.pkg('sigma.canvas.edges');
+
+  /**
+   * This edge renderer will display edges as arrows going from the source node
+   *
+   * @param  {object}                   edge         The edge object.
+   * @param  {object}                   source node  The edge source node.
+   * @param  {object}                   target node  The edge target node.
+   * @param  {CanvasRenderingContext2D} context      The canvas context.
+   * @param  {configurable}             settings     The settings function.
+   */
+  sigma.canvas.edges.arrow = function(edge, source, target, context, settings) {
+    var color = edge.color,
+        prefix = settings('prefix') || '',
+        edgeColor = settings('edgeColor'),
+        defaultNodeColor = settings('defaultNodeColor'),
+        defaultEdgeColor = settings('defaultEdgeColor'),
+        size = edge[prefix + 'size'] || 1,
+        tSize = target[prefix + 'size'],
+        sX = source[prefix + 'x'],
+        sY = source[prefix + 'y'],
+        tX = target[prefix + 'x'],
+        tY = target[prefix + 'y'],
+        aSize = Math.max(size * 2.5, settings('minArrowSize')),
+        d = Math.sqrt(Math.pow(tX - sX, 2) + Math.pow(tY - sY, 2)),
+        aX = sX + (tX - sX) * (d - aSize - tSize) / d,
+        aY = sY + (tY - sY) * (d - aSize - tSize) / d,
+        vX = (tX - sX) * aSize / d,
+        vY = (tY - sY) * aSize / d;
+
+    if (!color)
+      switch (edgeColor) {
+        case 'source':
+          color = source.color || defaultNodeColor;
+          break;
+        case 'target':
+          color = target.color || defaultNodeColor;
+          break;
+        default:
+          color = defaultEdgeColor;
+          break;
+      }
+
+    context.strokeStyle = color;
+    context.lineWidth = size;
+    context.beginPath();
+    context.moveTo(sX, sY);
+    context.lineTo(
+      aX,
+      aY
+    );
+    context.stroke();
+
+    context.fillStyle = color;
+    context.beginPath();
+    context.moveTo(aX + vX, aY + vY);
+    context.lineTo(aX + vY * 0.6, aY - vX * 0.6);
+    context.lineTo(aX - vY * 0.6, aY + vX * 0.6);
+    context.lineTo(aX + vX, aY + vY);
+    context.closePath();
+    context.fill();
+  };
+})();
diff --git a/demo/static/sigmajs/renderers/canvas/sigma.canvas.edges.curve.js b/demo/static/sigmajs/renderers/canvas/sigma.canvas.edges.curve.js
new file mode 100644
index 000000000..3e1502b4f
--- /dev/null
+++ b/demo/static/sigmajs/renderers/canvas/sigma.canvas.edges.curve.js
@@ -0,0 +1,57 @@
+;(function() {
+  'use strict';
+
+  sigma.utils.pkg('sigma.canvas.edges');
+
+  /**
+   * This edge renderer will display edges as curves.
+   *
+   * @param  {object}                   edge         The edge object.
+   * @param  {object}                   source node  The edge source node.
+   * @param  {object}                   target node  The edge target node.
+   * @param  {CanvasRenderingContext2D} context      The canvas context.
+   * @param  {configurable}             settings     The settings function.
+   */
+  sigma.canvas.edges.curve = function(edge, source, target, context, settings) {
+    var color = edge.color,
+        prefix = settings('prefix') || '',
+        size = edge[prefix + 'size'] || 1,
+        edgeColor = settings('edgeColor'),
+        defaultNodeColor = settings('defaultNodeColor'),
+        defaultEdgeColor = settings('defaultEdgeColor'),
+        cp = {},
+        sSize = source[prefix + 'size'],
+        sX = source[prefix + 'x'],
+        sY = source[prefix + 'y'],
+        tX = target[prefix + 'x'],
+        tY = target[prefix + 'y'];
+
+    cp = (source.id === target.id) ?
+      sigma.utils.getSelfLoopControlPoints(sX, sY, sSize) :
+      sigma.utils.getQuadraticControlPoint(sX, sY, tX, tY);
+
+    if (!color)
+      switch (edgeColor) {
+        case 'source':
+          color = source.color || defaultNodeColor;
+          break;
+        case 'target':
+          color = target.color || defaultNodeColor;
+          break;
+        default:
+          color = defaultEdgeColor;
+          break;
+      }
+
+    context.strokeStyle = color;
+    context.lineWidth = size;
+    context.beginPath();
+    context.moveTo(sX, sY);
+    if (source.id === target.id) {
+      context.bezierCurveTo(cp.x1, cp.y1, cp.x2, cp.y2, tX, tY);
+    } else {
+      context.quadraticCurveTo(cp.x, cp.y, tX, tY);
+    }
+    context.stroke();
+  };
+})();
diff --git a/demo/static/sigmajs/renderers/canvas/sigma.canvas.edges.curvedArrow.js b/demo/static/sigmajs/renderers/canvas/sigma.canvas.edges.curvedArrow.js
new file mode 100644
index 000000000..9c7b66383
--- /dev/null
+++ b/demo/static/sigmajs/renderers/canvas/sigma.canvas.edges.curvedArrow.js
@@ -0,0 +1,88 @@
+;(function() {
+  'use strict';
+
+  sigma.utils.pkg('sigma.canvas.edges');
+
+  /**
+   * This edge renderer will display edges as curves with arrow heading.
+   *
+   * @param  {object}                   edge         The edge object.
+   * @param  {object}                   source node  The edge source node.
+   * @param  {object}                   target node  The edge target node.
+   * @param  {CanvasRenderingContext2D} context      The canvas context.
+   * @param  {configurable}             settings     The settings function.
+   */
+  sigma.canvas.edges.curvedArrow =
+    function(edge, source, target, context, settings) {
+    var color = edge.color,
+        prefix = settings('prefix') || '',
+        edgeColor = settings('edgeColor'),
+        defaultNodeColor = settings('defaultNodeColor'),
+        defaultEdgeColor = settings('defaultEdgeColor'),
+        cp = {},
+        size = edge[prefix + 'size'] || 1,
+        tSize = target[prefix + 'size'],
+        sX = source[prefix + 'x'],
+        sY = source[prefix + 'y'],
+        tX = target[prefix + 'x'],
+        tY = target[prefix + 'y'],
+        aSize = Math.max(size * 2.5, settings('minArrowSize')),
+        d,
+        aX,
+        aY,
+        vX,
+        vY;
+
+    cp = (source.id === target.id) ?
+      sigma.utils.getSelfLoopControlPoints(sX, sY, tSize) :
+      sigma.utils.getQuadraticControlPoint(sX, sY, tX, tY);
+
+    if (source.id === target.id) {
+      d = Math.sqrt(Math.pow(tX - cp.x1, 2) + Math.pow(tY - cp.y1, 2));
+      aX = cp.x1 + (tX - cp.x1) * (d - aSize - tSize) / d;
+      aY = cp.y1 + (tY - cp.y1) * (d - aSize - tSize) / d;
+      vX = (tX - cp.x1) * aSize / d;
+      vY = (tY - cp.y1) * aSize / d;
+    }
+    else {
+      d = Math.sqrt(Math.pow(tX - cp.x, 2) + Math.pow(tY - cp.y, 2));
+      aX = cp.x + (tX - cp.x) * (d - aSize - tSize) / d;
+      aY = cp.y + (tY - cp.y) * (d - aSize - tSize) / d;
+      vX = (tX - cp.x) * aSize / d;
+      vY = (tY - cp.y) * aSize / d;
+    }
+
+    if (!color)
+      switch (edgeColor) {
+        case 'source':
+          color = source.color || defaultNodeColor;
+          break;
+        case 'target':
+          color = target.color || defaultNodeColor;
+          break;
+        default:
+          color = defaultEdgeColor;
+          break;
+      }
+
+    context.strokeStyle = color;
+    context.lineWidth = size;
+    context.beginPath();
+    context.moveTo(sX, sY);
+    if (source.id === target.id) {
+      context.bezierCurveTo(cp.x2, cp.y2, cp.x1, cp.y1, aX, aY);
+    } else {
+      context.quadraticCurveTo(cp.x, cp.y, aX, aY);
+    }
+    context.stroke();
+
+    context.fillStyle = color;
+    context.beginPath();
+    context.moveTo(aX + vX, aY + vY);
+    context.lineTo(aX + vY * 0.6, aY - vX * 0.6);
+    context.lineTo(aX - vY * 0.6, aY + vX * 0.6);
+    context.lineTo(aX + vX, aY + vY);
+    context.closePath();
+    context.fill();
+  };
+})();
diff --git a/demo/static/sigmajs/renderers/canvas/sigma.canvas.edges.def.js b/demo/static/sigmajs/renderers/canvas/sigma.canvas.edges.def.js
new file mode 100644
index 000000000..dd97b9037
--- /dev/null
+++ b/demo/static/sigmajs/renderers/canvas/sigma.canvas.edges.def.js
@@ -0,0 +1,49 @@
+;(function() {
+  'use strict';
+
+  sigma.utils.pkg('sigma.canvas.edges');
+
+  /**
+   * The default edge renderer. It renders the edge as a simple line.
+   *
+   * @param  {object}                   edge         The edge object.
+   * @param  {object}                   source node  The edge source node.
+   * @param  {object}                   target node  The edge target node.
+   * @param  {CanvasRenderingContext2D} context      The canvas context.
+   * @param  {configurable}             settings     The settings function.
+   */
+  sigma.canvas.edges.def = function(edge, source, target, context, settings) {
+    var color = edge.color,
+        prefix = settings('prefix') || '',
+        size = edge[prefix + 'size'] || 1,
+        edgeColor = settings('edgeColor'),
+        defaultNodeColor = settings('defaultNodeColor'),
+        defaultEdgeColor = settings('defaultEdgeColor');
+
+    if (!color)
+      switch (edgeColor) {
+        case 'source':
+          color = source.color || defaultNodeColor;
+          break;
+        case 'target':
+          color = target.color || defaultNodeColor;
+          break;
+        default:
+          color = defaultEdgeColor;
+          break;
+      }
+
+    context.strokeStyle = color;
+    context.lineWidth = size;
+    context.beginPath();
+    context.moveTo(
+      source[prefix + 'x'],
+      source[prefix + 'y']
+    );
+    context.lineTo(
+      target[prefix + 'x'],
+      target[prefix + 'y']
+    );
+    context.stroke();
+  };
+})();
diff --git a/demo/static/sigmajs/renderers/canvas/sigma.canvas.extremities.def.js b/demo/static/sigmajs/renderers/canvas/sigma.canvas.extremities.def.js
new file mode 100644
index 000000000..7877dc2ca
--- /dev/null
+++ b/demo/static/sigmajs/renderers/canvas/sigma.canvas.extremities.def.js
@@ -0,0 +1,38 @@
+;(function(undefined) {
+  'use strict';
+
+  if (typeof sigma === 'undefined')
+    throw 'sigma is not declared';
+
+  // Initialize packages:
+  sigma.utils.pkg('sigma.canvas.extremities');
+
+  /**
+   * The default renderer for hovered edge extremities. It renders the edge
+   * extremities as hovered.
+   *
+   * @param  {object}                   edge         The edge object.
+   * @param  {object}                   source node  The edge source node.
+   * @param  {object}                   target node  The edge target node.
+   * @param  {CanvasRenderingContext2D} context      The canvas context.
+   * @param  {configurable}             settings     The settings function.
+   */
+  sigma.canvas.extremities.def =
+    function(edge, source, target, context, settings) {
+    // Source Node:
+    (
+      sigma.canvas.hovers[source.type] ||
+      sigma.canvas.hovers.def
+    ) (
+      source, context, settings
+    );
+
+    // Target Node:
+    (
+      sigma.canvas.hovers[target.type] ||
+      sigma.canvas.hovers.def
+    ) (
+      target, context, settings
+    );
+  };
+}).call(this);
diff --git a/demo/static/sigmajs/renderers/canvas/sigma.canvas.hovers.def.js b/demo/static/sigmajs/renderers/canvas/sigma.canvas.hovers.def.js
new file mode 100644
index 000000000..00185c251
--- /dev/null
+++ b/demo/static/sigmajs/renderers/canvas/sigma.canvas.hovers.def.js
@@ -0,0 +1,106 @@
+;(function(undefined) {
+  'use strict';
+
+  if (typeof sigma === 'undefined')
+    throw 'sigma is not declared';
+
+  // Initialize packages:
+  sigma.utils.pkg('sigma.canvas.hovers');
+
+  /**
+   * This hover renderer will basically display the label with a background.
+   *
+   * @param  {object}                   node     The node object.
+   * @param  {CanvasRenderingContext2D} context  The canvas context.
+   * @param  {configurable}             settings The settings function.
+   */
+  sigma.canvas.hovers.def = function(node, context, settings) {
+    var x,
+        y,
+        w,
+        h,
+        e,
+        fontStyle = settings('hoverFontStyle') || settings('fontStyle'),
+        prefix = settings('prefix') || '',
+        size = node[prefix + 'size'],
+        fontSize = (settings('labelSize') === 'fixed') ?
+          settings('defaultLabelSize') :
+          settings('labelSizeRatio') * size;
+
+    // Label background:
+    context.font = (fontStyle ? fontStyle + ' ' : '') +
+      fontSize + 'px ' + (settings('hoverFont') || settings('font'));
+
+    context.beginPath();
+    context.fillStyle = settings('labelHoverBGColor') === 'node' ?
+      (node.color || settings('defaultNodeColor')) :
+      settings('defaultHoverLabelBGColor');
+
+    if (node.label && settings('labelHoverShadow')) {
+      context.shadowOffsetX = 0;
+      context.shadowOffsetY = 0;
+      context.shadowBlur = 8;
+      context.shadowColor = settings('labelHoverShadowColor');
+    }
+
+    if (node.label && typeof node.label === 'string') {
+      x = Math.round(node[prefix + 'x'] - fontSize / 2 - 2);
+      y = Math.round(node[prefix + 'y'] - fontSize / 2 - 2);
+      w = Math.round(
+        context.measureText(node.label).width + fontSize / 2 + size + 7
+      );
+      h = Math.round(fontSize + 4);
+      e = Math.round(fontSize / 2 + 2);
+
+      context.moveTo(x, y + e);
+      context.arcTo(x, y, x + e, y, e);
+      context.lineTo(x + w, y);
+      context.lineTo(x + w, y + h);
+      context.lineTo(x + e, y + h);
+      context.arcTo(x, y + h, x, y + h - e, e);
+      context.lineTo(x, y + e);
+
+      context.closePath();
+      context.fill();
+
+      context.shadowOffsetX = 0;
+      context.shadowOffsetY = 0;
+      context.shadowBlur = 0;
+    }
+
+    // Node border:
+    if (settings('borderSize') > 0) {
+      context.beginPath();
+      context.fillStyle = settings('nodeBorderColor') === 'node' ?
+        (node.color || settings('defaultNodeColor')) :
+        settings('defaultNodeBorderColor');
+      context.arc(
+        node[prefix + 'x'],
+        node[prefix + 'y'],
+        size + settings('borderSize'),
+        0,
+        Math.PI * 2,
+        true
+      );
+      context.closePath();
+      context.fill();
+    }
+
+    // Node:
+    var nodeRenderer = sigma.canvas.nodes[node.type] || sigma.canvas.nodes.def;
+    nodeRenderer(node, context, settings);
+
+    // Display the label:
+    if (node.label && typeof node.label === 'string') {
+      context.fillStyle = (settings('labelHoverColor') === 'node') ?
+        (node.color || settings('defaultNodeColor')) :
+        settings('defaultLabelHoverColor');
+
+      context.fillText(
+        node.label,
+        Math.round(node[prefix + 'x'] + size + 3),
+        Math.round(node[prefix + 'y'] + fontSize / 3)
+      );
+    }
+  };
+}).call(this);
diff --git a/demo/static/sigmajs/renderers/canvas/sigma.canvas.labels.def.js b/demo/static/sigmajs/renderers/canvas/sigma.canvas.labels.def.js
new file mode 100644
index 000000000..8a70d7390
--- /dev/null
+++ b/demo/static/sigmajs/renderers/canvas/sigma.canvas.labels.def.js
@@ -0,0 +1,44 @@
+;(function(undefined) {
+  'use strict';
+
+  if (typeof sigma === 'undefined')
+    throw 'sigma is not declared';
+
+  // Initialize packages:
+  sigma.utils.pkg('sigma.canvas.labels');
+
+  /**
+   * This label renderer will just display the label on the right of the node.
+   *
+   * @param  {object}                   node     The node object.
+   * @param  {CanvasRenderingContext2D} context  The canvas context.
+   * @param  {configurable}             settings The settings function.
+   */
+  sigma.canvas.labels.def = function(node, context, settings) {
+    var fontSize,
+        prefix = settings('prefix') || '',
+        size = node[prefix + 'size'];
+
+    if (size < settings('labelThreshold'))
+      return;
+
+    if (!node.label || typeof node.label !== 'string')
+      return;
+
+    fontSize = (settings('labelSize') === 'fixed') ?
+      settings('defaultLabelSize') :
+      settings('labelSizeRatio') * size;
+
+    context.font = (settings('fontStyle') ? settings('fontStyle') + ' ' : '') +
+      fontSize + 'px ' + settings('font');
+    context.fillStyle = (settings('labelColor') === 'node') ?
+      (node.color || settings('defaultNodeColor')) :
+      settings('defaultLabelColor');
+
+    context.fillText(
+      node.label,
+      Math.round(node[prefix + 'x'] + size + 3),
+      Math.round(node[prefix + 'y'] + fontSize / 3)
+    );
+  };
+}).call(this);
diff --git a/demo/static/sigmajs/renderers/canvas/sigma.canvas.nodes.def.js b/demo/static/sigmajs/renderers/canvas/sigma.canvas.nodes.def.js
new file mode 100644
index 000000000..ee499b005
--- /dev/null
+++ b/demo/static/sigmajs/renderers/canvas/sigma.canvas.nodes.def.js
@@ -0,0 +1,30 @@
+;(function() {
+  'use strict';
+
+  sigma.utils.pkg('sigma.canvas.nodes');
+
+  /**
+   * The default node renderer. It renders the node as a simple disc.
+   *
+   * @param  {object}                   node     The node object.
+   * @param  {CanvasRenderingContext2D} context  The canvas context.
+   * @param  {configurable}             settings The settings function.
+   */
+  sigma.canvas.nodes.def = function(node, context, settings) {
+    var prefix = settings('prefix') || '';
+
+    context.fillStyle = node.color || settings('defaultNodeColor');
+    context.beginPath();
+    context.arc(
+      node[prefix + 'x'],
+      node[prefix + 'y'],
+      node[prefix + 'size'],
+      0,
+      Math.PI * 2,
+      true
+    );
+
+    context.closePath();
+    context.fill();
+  };
+})();
diff --git a/demo/static/sigmajs/renderers/sigma.renderers.canvas.js b/demo/static/sigmajs/renderers/sigma.renderers.canvas.js
new file mode 100644
index 000000000..4eda14550
--- /dev/null
+++ b/demo/static/sigmajs/renderers/sigma.renderers.canvas.js
@@ -0,0 +1,450 @@
+;(function(undefined) {
+  'use strict';
+
+  if (typeof sigma === 'undefined')
+    throw 'sigma is not declared';
+
+  if (typeof conrad === 'undefined')
+    throw 'conrad is not declared';
+
+  // Initialize packages:
+  sigma.utils.pkg('sigma.renderers');
+
+  /**
+   * This function is the constructor of the canvas sigma's renderer.
+   *
+   * @param  {sigma.classes.graph}            graph    The graph to render.
+   * @param  {sigma.classes.camera}           camera   The camera.
+   * @param  {configurable}           settings The sigma instance settings
+   *                                           function.
+   * @param  {object}                 object   The options object.
+   * @return {sigma.renderers.canvas}          The renderer instance.
+   */
+  sigma.renderers.canvas = function(graph, camera, settings, options) {
+    if (typeof options !== 'object')
+      throw 'sigma.renderers.canvas: Wrong arguments.';
+
+    if (!(options.container instanceof HTMLElement))
+      throw 'Container not found.';
+
+    var k,
+        i,
+        l,
+        a,
+        fn,
+        self = this;
+
+    sigma.classes.dispatcher.extend(this);
+
+    // Initialize main attributes:
+    Object.defineProperty(this, 'conradId', {
+      value: sigma.utils.id()
+    });
+    this.graph = graph;
+    this.camera = camera;
+    this.contexts = {};
+    this.domElements = {};
+    this.options = options;
+    this.container = this.options.container;
+    this.settings = (
+        typeof options.settings === 'object' &&
+        options.settings
+      ) ?
+        settings.embedObjects(options.settings) :
+        settings;
+
+    // Node indexes:
+    this.nodesOnScreen = [];
+    this.edgesOnScreen = [];
+
+    // Conrad related attributes:
+    this.jobs = {};
+
+    // Find the prefix:
+    this.options.prefix = 'renderer' + this.conradId + ':';
+
+    // Initialize the DOM elements:
+    if (
+      !this.settings('batchEdgesDrawing')
+    ) {
+      this.initDOM('canvas', 'scene');
+      this.contexts.edges = this.contexts.scene;
+      this.contexts.nodes = this.contexts.scene;
+      this.contexts.labels = this.contexts.scene;
+    } else {
+      this.initDOM('canvas', 'edges');
+      this.initDOM('canvas', 'scene');
+      this.contexts.nodes = this.contexts.scene;
+      this.contexts.labels = this.contexts.scene;
+    }
+
+    this.initDOM('canvas', 'mouse');
+    this.contexts.hover = this.contexts.mouse;
+
+    // Initialize captors:
+    this.captors = [];
+    a = this.options.captors || [sigma.captors.mouse, sigma.captors.touch];
+    for (i = 0, l = a.length; i < l; i++) {
+      fn = typeof a[i] === 'function' ? a[i] : sigma.captors[a[i]];
+      this.captors.push(
+        new fn(
+          this.domElements.mouse,
+          this.camera,
+          this.settings
+        )
+      );
+    }
+
+    // Deal with sigma events:
+    sigma.misc.bindEvents.call(this, this.options.prefix);
+    sigma.misc.drawHovers.call(this, this.options.prefix);
+
+    this.resize(false);
+  };
+
+
+
+
+  /**
+   * This method renders the graph on the canvases.
+   *
+   * @param  {?object}                options Eventually an object of options.
+   * @return {sigma.renderers.canvas}         Returns the instance itself.
+   */
+  sigma.renderers.canvas.prototype.render = function(options) {
+    options = options || {};
+
+    var a,
+        i,
+        k,
+        l,
+        o,
+        id,
+        end,
+        job,
+        start,
+        edges,
+        renderers,
+        rendererType,
+        batchSize,
+        tempGCO,
+        index = {},
+        graph = this.graph,
+        nodes = this.graph.nodes,
+        prefix = this.options.prefix || '',
+        drawEdges = this.settings(options, 'drawEdges'),
+        drawNodes = this.settings(options, 'drawNodes'),
+        drawLabels = this.settings(options, 'drawLabels'),
+        drawEdgeLabels = this.settings(options, 'drawEdgeLabels'),
+        embedSettings = this.settings.embedObjects(options, {
+          prefix: this.options.prefix
+        });
+
+    // Call the resize function:
+    this.resize(false);
+
+    // Check the 'hideEdgesOnMove' setting:
+    if (this.settings(options, 'hideEdgesOnMove'))
+      if (this.camera.isAnimated || this.camera.isMoving)
+        drawEdges = false;
+
+    // Apply the camera's view:
+    this.camera.applyView(
+      undefined,
+      this.options.prefix,
+      {
+        width: this.width,
+        height: this.height
+      }
+    );
+
+    // Clear canvases:
+    this.clear();
+
+    // Kill running jobs:
+    for (k in this.jobs)
+      if (conrad.hasJob(k))
+        conrad.killJob(k);
+
+    // Find which nodes are on screen:
+    this.edgesOnScreen = [];
+    this.nodesOnScreen = this.camera.quadtree.area(
+      this.camera.getRectangle(this.width, this.height)
+    );
+
+    for (a = this.nodesOnScreen, i = 0, l = a.length; i < l; i++)
+      index[a[i].id] = a[i];
+
+    // Draw edges:
+    // - If settings('batchEdgesDrawing') is true, the edges are displayed per
+    //   batches. If not, they are drawn in one frame.
+    if (drawEdges) {
+      // First, let's identify which edges to draw. To do this, we just keep
+      // every edges that have at least one extremity displayed according to
+      // the quadtree and the "hidden" attribute. We also do not keep hidden
+      // edges.
+      for (a = graph.edges(), i = 0, l = a.length; i < l; i++) {
+        o = a[i];
+        if (
+          (index[o.source] || index[o.target]) &&
+          (!o.hidden && !nodes(o.source).hidden && !nodes(o.target).hidden)
+        )
+          this.edgesOnScreen.push(o);
+      }
+
+      // If the "batchEdgesDrawing" settings is true, edges are batched:
+      if (this.settings(options, 'batchEdgesDrawing')) {
+        id = 'edges_' + this.conradId;
+        batchSize = embedSettings('canvasEdgesBatchSize');
+
+        edges = this.edgesOnScreen;
+        l = edges.length;
+
+        start = 0;
+        end = Math.min(edges.length, start + batchSize);
+
+        job = function() {
+          tempGCO = this.contexts.edges.globalCompositeOperation;
+          this.contexts.edges.globalCompositeOperation = 'destination-over';
+
+          renderers = sigma.canvas.edges;
+          for (i = start; i < end; i++) {
+            o = edges[i];
+            (renderers[
+              o.type || this.settings(options, 'defaultEdgeType')
+            ] || renderers.def)(
+              o,
+              graph.nodes(o.source),
+              graph.nodes(o.target),
+              this.contexts.edges,
+              embedSettings
+            );
+          }
+
+          // Draw edge labels:
+          if (drawEdgeLabels) {
+            renderers = sigma.canvas.edges.labels;
+            for (i = start; i < end; i++) {
+              o = edges[i];
+              if (!o.hidden)
+                (renderers[
+                  o.type || this.settings(options, 'defaultEdgeType')
+                ] || renderers.def)(
+                  o,
+                  graph.nodes(o.source),
+                  graph.nodes(o.target),
+                  this.contexts.labels,
+                  embedSettings
+                );
+            }
+          }
+
+          // Restore original globalCompositeOperation:
+          this.contexts.edges.globalCompositeOperation = tempGCO;
+
+          // Catch job's end:
+          if (end === edges.length) {
+            delete this.jobs[id];
+            return false;
+          }
+
+          start = end + 1;
+          end = Math.min(edges.length, start + batchSize);
+          return true;
+        };
+
+        this.jobs[id] = job;
+        conrad.addJob(id, job.bind(this));
+
+      // If not, they are drawn in one frame:
+      } else {
+        renderers = sigma.canvas.edges;
+        for (a = this.edgesOnScreen, i = 0, l = a.length; i < l; i++) {
+          o = a[i];
+          (renderers[
+            o.type || this.settings(options, 'defaultEdgeType')
+          ] || renderers.def)(
+            o,
+            graph.nodes(o.source),
+            graph.nodes(o.target),
+            this.contexts.edges,
+            embedSettings
+          );
+        }
+
+        // Draw edge labels:
+        // - No batching
+        if (drawEdgeLabels) {
+          renderers = sigma.canvas.edges.labels;
+          for (a = this.edgesOnScreen, i = 0, l = a.length; i < l; i++)
+            if (!a[i].hidden)
+              (renderers[
+                a[i].type || this.settings(options, 'defaultEdgeType')
+              ] || renderers.def)(
+                a[i],
+                graph.nodes(a[i].source),
+                graph.nodes(a[i].target),
+                this.contexts.labels,
+                embedSettings
+              );
+        }
+      }
+    }
+
+    // Draw nodes:
+    // - No batching
+    if (drawNodes) {
+      renderers = sigma.canvas.nodes;
+      for (a = this.nodesOnScreen, i = 0, l = a.length; i < l; i++)
+        if (!a[i].hidden)
+          (renderers[
+            a[i].type || this.settings(options, 'defaultNodeType')
+          ] || renderers.def)(
+            a[i],
+            this.contexts.nodes,
+            embedSettings
+          );
+    }
+
+    // Draw labels:
+    // - No batching
+    if (drawLabels) {
+      renderers = sigma.canvas.labels;
+      for (a = this.nodesOnScreen, i = 0, l = a.length; i < l; i++)
+        if (!a[i].hidden)
+          (renderers[
+            a[i].type || this.settings(options, 'defaultNodeType')
+          ] || renderers.def)(
+            a[i],
+            this.contexts.labels,
+            embedSettings
+          );
+    }
+
+    this.dispatchEvent('render');
+
+    return this;
+  };
+
+  /**
+   * This method creates a DOM element of the specified type, switches its
+   * position to "absolute", references it to the domElements attribute, and
+   * finally appends it to the container.
+   *
+   * @param  {string} tag The label tag.
+   * @param  {string} id  The id of the element (to store it in "domElements").
+   */
+  sigma.renderers.canvas.prototype.initDOM = function(tag, id) {
+    var dom = document.createElement(tag);
+
+    dom.style.position = 'absolute';
+    dom.setAttribute('class', 'sigma-' + id);
+
+    this.domElements[id] = dom;
+    this.container.appendChild(dom);
+
+    if (tag.toLowerCase() === 'canvas')
+      this.contexts[id] = dom.getContext('2d');
+  };
+
+  /**
+   * This method resizes each DOM elements in the container and stores the new
+   * dimensions. Then, it renders the graph.
+   *
+   * @param  {?number}                width  The new width of the container.
+   * @param  {?number}                height The new height of the container.
+   * @return {sigma.renderers.canvas}        Returns the instance itself.
+   */
+  sigma.renderers.canvas.prototype.resize = function(w, h) {
+    var k,
+        oldWidth = this.width,
+        oldHeight = this.height,
+        pixelRatio = 1;
+        // TODO:
+        // *****
+        // This pixelRatio is the solution to display with the good definition
+        // on canvases on Retina displays (ie oversampling). Unfortunately, it
+        // has a huge performance cost...
+        //  > pixelRatio = window.devicePixelRatio || 1;
+
+    if (w !== undefined && h !== undefined) {
+      this.width = w;
+      this.height = h;
+    } else {
+      this.width = this.container.offsetWidth;
+      this.height = this.container.offsetHeight;
+
+      w = this.width;
+      h = this.height;
+    }
+
+    if (oldWidth !== this.width || oldHeight !== this.height) {
+      for (k in this.domElements) {
+        this.domElements[k].style.width = w + 'px';
+        this.domElements[k].style.height = h + 'px';
+
+        if (this.domElements[k].tagName.toLowerCase() === 'canvas') {
+          this.domElements[k].setAttribute('width', (w * pixelRatio) + 'px');
+          this.domElements[k].setAttribute('height', (h * pixelRatio) + 'px');
+
+          if (pixelRatio !== 1)
+            this.contexts[k].scale(pixelRatio, pixelRatio);
+        }
+      }
+    }
+
+    return this;
+  };
+
+  /**
+   * This method clears each canvas.
+   *
+   * @return {sigma.renderers.canvas} Returns the instance itself.
+   */
+  sigma.renderers.canvas.prototype.clear = function() {
+    var k;
+
+    for (k in this.domElements)
+      if (this.domElements[k].tagName === 'CANVAS')
+        this.domElements[k].width = this.domElements[k].width;
+
+    return this;
+  };
+
+  /**
+   * This method kills contexts and other attributes.
+   */
+  sigma.renderers.canvas.prototype.kill = function() {
+    var k,
+        captor;
+
+    // Kill captors:
+    while ((captor = this.captors.pop()))
+      captor.kill();
+    delete this.captors;
+
+    // Kill contexts:
+    for (k in this.domElements) {
+      this.domElements[k].parentNode.removeChild(this.domElements[k]);
+      delete this.domElements[k];
+      delete this.contexts[k];
+    }
+    delete this.domElements;
+    delete this.contexts;
+  };
+
+
+
+
+  /**
+   * The labels, nodes and edges renderers are stored in the three following
+   * objects. When an element is drawn, its type will be checked and if a
+   * renderer with the same name exists, it will be used. If not found, the
+   * default renderer will be used instead.
+   *
+   * They are stored in different files, in the "./canvas" folder.
+   */
+  sigma.utils.pkg('sigma.canvas.nodes');
+  sigma.utils.pkg('sigma.canvas.edges');
+  sigma.utils.pkg('sigma.canvas.labels');
+}).call(this);
diff --git a/demo/static/sigmajs/renderers/sigma.renderers.def.js b/demo/static/sigmajs/renderers/sigma.renderers.def.js
new file mode 100644
index 000000000..b091d39da
--- /dev/null
+++ b/demo/static/sigmajs/renderers/sigma.renderers.def.js
@@ -0,0 +1,29 @@
+;(function(global) {
+  'use strict';
+
+  if (typeof sigma === 'undefined')
+    throw 'sigma is not declared';
+
+  // Initialize packages:
+  sigma.utils.pkg('sigma.renderers');
+
+  // Check if WebGL is enabled:
+  var canvas,
+      webgl = !!global.WebGLRenderingContext;
+  if (webgl) {
+    canvas = document.createElement('canvas');
+    try {
+      webgl = !!(
+        canvas.getContext('webgl') ||
+        canvas.getContext('experimental-webgl')
+      );
+    } catch (e) {
+      webgl = false;
+    }
+  }
+
+  // Copy the good renderer:
+  sigma.renderers.def = webgl ?
+    sigma.renderers.webgl :
+    sigma.renderers.canvas;
+})(this);
diff --git a/demo/static/sigmajs/renderers/sigma.renderers.svg.js b/demo/static/sigmajs/renderers/sigma.renderers.svg.js
new file mode 100644
index 000000000..ffec79e00
--- /dev/null
+++ b/demo/static/sigmajs/renderers/sigma.renderers.svg.js
@@ -0,0 +1,479 @@
+;(function(undefined) {
+  'use strict';
+
+  if (typeof sigma === 'undefined')
+    throw 'sigma is not declared';
+
+  if (typeof conrad === 'undefined')
+    throw 'conrad is not declared';
+
+  // Initialize packages:
+  sigma.utils.pkg('sigma.renderers');
+
+  /**
+   * This function is the constructor of the svg sigma's renderer.
+   *
+   * @param  {sigma.classes.graph}            graph    The graph to render.
+   * @param  {sigma.classes.camera}           camera   The camera.
+   * @param  {configurable}           settings The sigma instance settings
+   *                                           function.
+   * @param  {object}                 object   The options object.
+   * @return {sigma.renderers.svg}             The renderer instance.
+   */
+  sigma.renderers.svg = function(graph, camera, settings, options) {
+    if (typeof options !== 'object')
+      throw 'sigma.renderers.svg: Wrong arguments.';
+
+    if (!(options.container instanceof HTMLElement))
+      throw 'Container not found.';
+
+    var i,
+        l,
+        a,
+        fn,
+        self = this;
+
+    sigma.classes.dispatcher.extend(this);
+
+    // Initialize main attributes:
+    this.graph = graph;
+    this.camera = camera;
+    this.domElements = {
+      graph: null,
+      groups: {},
+      nodes: {},
+      edges: {},
+      labels: {},
+      hovers: {}
+    };
+    this.measurementCanvas = null;
+    this.options = options;
+    this.container = this.options.container;
+    this.settings = (
+        typeof options.settings === 'object' &&
+        options.settings
+      ) ?
+        settings.embedObjects(options.settings) :
+        settings;
+
+    // Is the renderer meant to be freestyle?
+    this.settings('freeStyle', !!this.options.freeStyle);
+
+    // SVG xmlns
+    this.settings('xmlns', 'http://www.w3.org/2000/svg');
+
+    // Indexes:
+    this.nodesOnScreen = [];
+    this.edgesOnScreen = [];
+
+    // Find the prefix:
+    this.options.prefix = 'renderer' + sigma.utils.id() + ':';
+
+    // Initialize the DOM elements
+    this.initDOM('svg');
+
+    // Initialize captors:
+    this.captors = [];
+    a = this.options.captors || [sigma.captors.mouse, sigma.captors.touch];
+    for (i = 0, l = a.length; i < l; i++) {
+      fn = typeof a[i] === 'function' ? a[i] : sigma.captors[a[i]];
+      this.captors.push(
+        new fn(
+          this.domElements.graph,
+          this.camera,
+          this.settings
+        )
+      );
+    }
+
+    // Bind resize:
+    window.addEventListener('resize', function() {
+      self.resize();
+    });
+
+    // Deal with sigma events:
+    // TODO: keep an option to override the DOM events?
+    sigma.misc.bindDOMEvents.call(this, this.domElements.graph);
+    this.bindHovers(this.options.prefix);
+
+    // Resize
+    this.resize(false);
+  };
+
+  /**
+   * This method renders the graph on the svg scene.
+   *
+   * @param  {?object}                options Eventually an object of options.
+   * @return {sigma.renderers.svg}            Returns the instance itself.
+   */
+  sigma.renderers.svg.prototype.render = function(options) {
+    options = options || {};
+
+    var a,
+        i,
+        k,
+        e,
+        l,
+        o,
+        source,
+        target,
+        start,
+        edges,
+        renderers,
+        subrenderers,
+        index = {},
+        graph = this.graph,
+        nodes = this.graph.nodes,
+        prefix = this.options.prefix || '',
+        drawEdges = this.settings(options, 'drawEdges'),
+        drawNodes = this.settings(options, 'drawNodes'),
+        drawLabels = this.settings(options, 'drawLabels'),
+        embedSettings = this.settings.embedObjects(options, {
+          prefix: this.options.prefix,
+          forceLabels: this.options.forceLabels
+        });
+
+    // Check the 'hideEdgesOnMove' setting:
+    if (this.settings(options, 'hideEdgesOnMove'))
+      if (this.camera.isAnimated || this.camera.isMoving)
+        drawEdges = false;
+
+    // Apply the camera's view:
+    this.camera.applyView(
+      undefined,
+      this.options.prefix,
+      {
+        width: this.width,
+        height: this.height
+      }
+    );
+
+    // Hiding everything
+    // TODO: find a more sensible way to perform this operation
+    this.hideDOMElements(this.domElements.nodes);
+    this.hideDOMElements(this.domElements.edges);
+    this.hideDOMElements(this.domElements.labels);
+
+    // Find which nodes are on screen
+    this.edgesOnScreen = [];
+    this.nodesOnScreen = this.camera.quadtree.area(
+      this.camera.getRectangle(this.width, this.height)
+    );
+
+    // Node index
+    for (a = this.nodesOnScreen, i = 0, l = a.length; i < l; i++)
+      index[a[i].id] = a[i];
+
+    // Find which edges are on screen
+    for (a = graph.edges(), i = 0, l = a.length; i < l; i++) {
+      o = a[i];
+      if (
+        (index[o.source] || index[o.target]) &&
+        (!o.hidden && !nodes(o.source).hidden && !nodes(o.target).hidden)
+      )
+        this.edgesOnScreen.push(o);
+    }
+
+    // Display nodes
+    //---------------
+    renderers = sigma.svg.nodes;
+    subrenderers = sigma.svg.labels;
+
+    //-- First we create the nodes which are not already created
+    if (drawNodes)
+      for (a = this.nodesOnScreen, i = 0, l = a.length; i < l; i++) {
+        if (!a[i].hidden && !this.domElements.nodes[a[i].id]) {
+
+          // Node
+          e = (renderers[a[i].type] || renderers.def).create(
+            a[i],
+            embedSettings
+          );
+
+          this.domElements.nodes[a[i].id] = e;
+          this.domElements.groups.nodes.appendChild(e);
+
+          // Label
+          e = (subrenderers[a[i].type] || subrenderers.def).create(
+            a[i],
+            embedSettings
+          );
+
+          this.domElements.labels[a[i].id] = e;
+          this.domElements.groups.labels.appendChild(e);
+        }
+      }
+
+    //-- Second we update the nodes
+    if (drawNodes)
+      for (a = this.nodesOnScreen, i = 0, l = a.length; i < l; i++) {
+
+        if (a[i].hidden)
+          continue;
+
+        // Node
+        (renderers[a[i].type] || renderers.def).update(
+          a[i],
+          this.domElements.nodes[a[i].id],
+          embedSettings
+        );
+
+        // Label
+        (subrenderers[a[i].type] || subrenderers.def).update(
+          a[i],
+          this.domElements.labels[a[i].id],
+          embedSettings
+        );
+      }
+
+    // Display edges
+    //---------------
+    renderers = sigma.svg.edges;
+
+    //-- First we create the edges which are not already created
+    if (drawEdges)
+      for (a = this.edgesOnScreen, i = 0, l = a.length; i < l; i++) {
+        if (!this.domElements.edges[a[i].id]) {
+          source = nodes(a[i].source);
+          target = nodes(a[i].target);
+
+          e = (renderers[a[i].type] || renderers.def).create(
+            a[i],
+            source,
+            target,
+            embedSettings
+          );
+
+          this.domElements.edges[a[i].id] = e;
+          this.domElements.groups.edges.appendChild(e);
+        }
+       }
+
+    //-- Second we update the edges
+    if (drawEdges)
+      for (a = this.edgesOnScreen, i = 0, l = a.length; i < l; i++) {
+        source = nodes(a[i].source);
+        target = nodes(a[i].target);
+
+        (renderers[a[i].type] || renderers.def).update(
+          a[i],
+          this.domElements.edges[a[i].id],
+          source,
+          target,
+          embedSettings
+        );
+       }
+
+    this.dispatchEvent('render');
+
+    return this;
+  };
+
+  /**
+   * This method creates a DOM element of the specified type, switches its
+   * position to "absolute", references it to the domElements attribute, and
+   * finally appends it to the container.
+   *
+   * @param  {string} tag The label tag.
+   * @param  {string} id  The id of the element (to store it in "domElements").
+   */
+  sigma.renderers.svg.prototype.initDOM = function(tag) {
+    var dom = document.createElementNS(this.settings('xmlns'), tag),
+        c = this.settings('classPrefix'),
+        g,
+        l,
+        i;
+
+    dom.style.position = 'absolute';
+    dom.setAttribute('class', c + '-svg');
+
+    // Setting SVG namespace
+    dom.setAttribute('xmlns', this.settings('xmlns'));
+    dom.setAttribute('xmlns:xlink', 'http://www.w3.org/1999/xlink');
+    dom.setAttribute('version', '1.1');
+
+    // Creating the measurement canvas
+    var canvas = document.createElement('canvas');
+    canvas.setAttribute('class', c + '-measurement-canvas');
+
+    // Appending elements
+    this.domElements.graph = this.container.appendChild(dom);
+
+    // Creating groups
+    var groups = ['edges', 'nodes', 'labels', 'hovers'];
+    for (i = 0, l = groups.length; i < l; i++) {
+      g = document.createElementNS(this.settings('xmlns'), 'g');
+
+      g.setAttributeNS(null, 'id', c + '-group-' + groups[i]);
+      g.setAttributeNS(null, 'class', c + '-group');
+
+      this.domElements.groups[groups[i]] =
+        this.domElements.graph.appendChild(g);
+    }
+
+    // Appending measurement canvas
+    this.container.appendChild(canvas);
+    this.measurementCanvas = canvas.getContext('2d');
+  };
+
+  /**
+   * This method hides a batch of SVG DOM elements.
+   *
+   * @param  {array}                  elements  An array of elements to hide.
+   * @param  {object}                 renderer  The renderer to use.
+   * @return {sigma.renderers.svg}              Returns the instance itself.
+   */
+  sigma.renderers.svg.prototype.hideDOMElements = function(elements) {
+    var o,
+        i;
+
+    for (i in elements) {
+      o = elements[i];
+      sigma.svg.utils.hide(o);
+    }
+
+    return this;
+  };
+
+  /**
+   * This method binds the hover events to the renderer.
+   *
+   * @param  {string} prefix The renderer prefix.
+   */
+  // TODO: add option about whether to display hovers or not
+  sigma.renderers.svg.prototype.bindHovers = function(prefix) {
+    var renderers = sigma.svg.hovers,
+        self = this,
+        hoveredNode;
+
+    function overNode(e) {
+      var node = e.data.node,
+          embedSettings = self.settings.embedObjects({
+            prefix: prefix
+          });
+
+      if (!embedSettings('enableHovering'))
+        return;
+
+      var hover = (renderers[node.type] || renderers.def).create(
+        node,
+        self.domElements.nodes[node.id],
+        self.measurementCanvas,
+        embedSettings
+      );
+
+      self.domElements.hovers[node.id] = hover;
+
+      // Inserting the hover in the dom
+      self.domElements.groups.hovers.appendChild(hover);
+      hoveredNode = node;
+    }
+
+    function outNode(e) {
+      var node = e.data.node,
+          embedSettings = self.settings.embedObjects({
+            prefix: prefix
+          });
+
+      if (!embedSettings('enableHovering'))
+        return;
+
+      // Deleting element
+      self.domElements.groups.hovers.removeChild(
+        self.domElements.hovers[node.id]
+      );
+      hoveredNode = null;
+      delete self.domElements.hovers[node.id];
+
+      // Reinstate
+      self.domElements.groups.nodes.appendChild(
+        self.domElements.nodes[node.id]
+      );
+    }
+
+    // OPTIMIZE: perform a real update rather than a deletion
+    function update() {
+      if (!hoveredNode)
+        return;
+
+      var embedSettings = self.settings.embedObjects({
+            prefix: prefix
+          });
+
+      // Deleting element before update
+      self.domElements.groups.hovers.removeChild(
+        self.domElements.hovers[hoveredNode.id]
+      );
+      delete self.domElements.hovers[hoveredNode.id];
+
+      var hover = (renderers[hoveredNode.type] || renderers.def).create(
+        hoveredNode,
+        self.domElements.nodes[hoveredNode.id],
+        self.measurementCanvas,
+        embedSettings
+      );
+
+      self.domElements.hovers[hoveredNode.id] = hover;
+
+      // Inserting the hover in the dom
+      self.domElements.groups.hovers.appendChild(hover);
+    }
+
+    // Binding events
+    this.bind('overNode', overNode);
+    this.bind('outNode', outNode);
+
+    // Update on render
+    this.bind('render', update);
+  };
+
+  /**
+   * This method resizes each DOM elements in the container and stores the new
+   * dimensions. Then, it renders the graph.
+   *
+   * @param  {?number}                width  The new width of the container.
+   * @param  {?number}                height The new height of the container.
+   * @return {sigma.renderers.svg}           Returns the instance itself.
+   */
+  sigma.renderers.svg.prototype.resize = function(w, h) {
+    var oldWidth = this.width,
+        oldHeight = this.height,
+        pixelRatio = 1;
+
+    if (w !== undefined && h !== undefined) {
+      this.width = w;
+      this.height = h;
+    } else {
+      this.width = this.container.offsetWidth;
+      this.height = this.container.offsetHeight;
+
+      w = this.width;
+      h = this.height;
+    }
+
+    if (oldWidth !== this.width || oldHeight !== this.height) {
+      this.domElements.graph.style.width = w + 'px';
+      this.domElements.graph.style.height = h + 'px';
+
+      if (this.domElements.graph.tagName.toLowerCase() === 'svg') {
+        this.domElements.graph.setAttribute('width', (w * pixelRatio));
+        this.domElements.graph.setAttribute('height', (h * pixelRatio));
+      }
+    }
+
+    return this;
+  };
+
+
+  /**
+   * The labels, nodes and edges renderers are stored in the three following
+   * objects. When an element is drawn, its type will be checked and if a
+   * renderer with the same name exists, it will be used. If not found, the
+   * default renderer will be used instead.
+   *
+   * They are stored in different files, in the "./svg" folder.
+   */
+  sigma.utils.pkg('sigma.svg.nodes');
+  sigma.utils.pkg('sigma.svg.edges');
+  sigma.utils.pkg('sigma.svg.labels');
+}).call(this);
diff --git a/demo/static/sigmajs/renderers/sigma.renderers.webgl.js b/demo/static/sigmajs/renderers/sigma.renderers.webgl.js
new file mode 100644
index 000000000..b2a7baa48
--- /dev/null
+++ b/demo/static/sigmajs/renderers/sigma.renderers.webgl.js
@@ -0,0 +1,693 @@
+;(function(undefined) {
+  'use strict';
+
+  if (typeof sigma === 'undefined')
+    throw 'sigma is not declared';
+
+  // Initialize packages:
+  sigma.utils.pkg('sigma.renderers');
+
+  /**
+   * This function is the constructor of the canvas sigma's renderer.
+   *
+   * @param  {sigma.classes.graph}            graph    The graph to render.
+   * @param  {sigma.classes.camera}           camera   The camera.
+   * @param  {configurable}           settings The sigma instance settings
+   *                                           function.
+   * @param  {object}                 object   The options object.
+   * @return {sigma.renderers.canvas}          The renderer instance.
+   */
+  sigma.renderers.webgl = function(graph, camera, settings, options) {
+    if (typeof options !== 'object')
+      throw 'sigma.renderers.webgl: Wrong arguments.';
+
+    if (!(options.container instanceof HTMLElement))
+      throw 'Container not found.';
+
+    var k,
+        i,
+        l,
+        a,
+        fn,
+        _self = this;
+
+    sigma.classes.dispatcher.extend(this);
+
+    // Conrad related attributes:
+    this.jobs = {};
+
+    Object.defineProperty(this, 'conradId', {
+      value: sigma.utils.id()
+    });
+
+    // Initialize main attributes:
+    this.graph = graph;
+    this.camera = camera;
+    this.contexts = {};
+    this.domElements = {};
+    this.options = options;
+    this.container = this.options.container;
+    this.settings = (
+        typeof options.settings === 'object' &&
+        options.settings
+      ) ?
+        settings.embedObjects(options.settings) :
+        settings;
+
+    // Find the prefix:
+    this.options.prefix = this.camera.readPrefix;
+
+    // Initialize programs hash
+    Object.defineProperty(this, 'nodePrograms', {
+      value: {}
+    });
+    Object.defineProperty(this, 'edgePrograms', {
+      value: {}
+    });
+    Object.defineProperty(this, 'nodeFloatArrays', {
+      value: {}
+    });
+    Object.defineProperty(this, 'edgeFloatArrays', {
+      value: {}
+    });
+
+    // Initialize the DOM elements:
+    if (this.settings(options, 'batchEdgesDrawing')) {
+      this.initDOM('canvas', 'edges', true);
+      this.initDOM('canvas', 'nodes', true);
+    } else {
+      this.initDOM('canvas', 'scene', true);
+      this.contexts.nodes = this.contexts.scene;
+      this.contexts.edges = this.contexts.scene;
+    }
+
+    this.initDOM('canvas', 'labels');
+    this.initDOM('canvas', 'mouse');
+    this.contexts.hover = this.contexts.mouse;
+
+    // Initialize captors:
+    this.captors = [];
+    a = this.options.captors || [sigma.captors.mouse, sigma.captors.touch];
+    for (i = 0, l = a.length; i < l; i++) {
+      fn = typeof a[i] === 'function' ? a[i] : sigma.captors[a[i]];
+      this.captors.push(
+        new fn(
+          this.domElements.mouse,
+          this.camera,
+          this.settings
+        )
+      );
+    }
+
+    // Deal with sigma events:
+    sigma.misc.bindEvents.call(this, this.camera.prefix);
+    sigma.misc.drawHovers.call(this, this.camera.prefix);
+
+    this.resize();
+  };
+
+
+
+
+  /**
+   * This method will generate the nodes and edges float arrays. This step is
+   * separated from the "render" method, because to keep WebGL efficient, since
+   * all the camera and middlewares are modelised as matrices and they do not
+   * require the float arrays to be regenerated.
+   *
+   * Basically, when the user moves the camera or applies some specific linear
+   * transformations, this process step will be skipped, and the "render"
+   * method will efficiently refresh the rendering.
+   *
+   * And when the user modifies the graph colors or positions (applying a new
+   * layout or filtering the colors, for instance), this "process" step will be
+   * required to regenerate the float arrays.
+   *
+   * @return {sigma.renderers.webgl} Returns the instance itself.
+   */
+  sigma.renderers.webgl.prototype.process = function() {
+    var a,
+        i,
+        l,
+        k,
+        type,
+        renderer,
+        graph = this.graph,
+        options = sigma.utils.extend(options, this.options);
+
+    // Empty float arrays:
+    for (k in this.nodeFloatArrays)
+      delete this.nodeFloatArrays[k];
+
+    for (k in this.edgeFloatArrays)
+      delete this.edgeFloatArrays[k];
+
+    // Sort edges and nodes per types:
+    for (a = graph.edges(), i = 0, l = a.length; i < l; i++) {
+      type = a[i].type || this.settings(options, 'defaultEdgeType');
+      k = (type && sigma.webgl.edges[type]) ? type : 'def';
+
+      if (!this.edgeFloatArrays[k])
+        this.edgeFloatArrays[k] = {
+          edges: []
+        };
+
+      this.edgeFloatArrays[k].edges.push(a[i]);
+    }
+
+    for (a = graph.nodes(), i = 0, l = a.length; i < l; i++) {
+      type = a[i].type || this.settings(options, 'defaultNodeType');
+      k = (type && sigma.webgl.nodes[type]) ? type : 'def';
+
+      if (!this.nodeFloatArrays[k])
+        this.nodeFloatArrays[k] = {
+          nodes: []
+        };
+
+      this.nodeFloatArrays[k].nodes.push(a[i]);
+    }
+
+    // Push edges:
+    for (k in this.edgeFloatArrays) {
+      renderer = sigma.webgl.edges[k];
+
+      for (a = this.edgeFloatArrays[k].edges, i = 0, l = a.length; i < l; i++) {
+        if (!this.edgeFloatArrays[k].array)
+          this.edgeFloatArrays[k].array = new Float32Array(
+            a.length * renderer.POINTS * renderer.ATTRIBUTES
+          );
+
+        // Just check that the edge and both its extremities are visible:
+        if (
+          !a[i].hidden &&
+          !graph.nodes(a[i].source).hidden &&
+          !graph.nodes(a[i].target).hidden
+        )
+          renderer.addEdge(
+            a[i],
+            graph.nodes(a[i].source),
+            graph.nodes(a[i].target),
+            this.edgeFloatArrays[k].array,
+            i * renderer.POINTS * renderer.ATTRIBUTES,
+            options.prefix,
+            this.settings
+          );
+      }
+    }
+
+    // Push nodes:
+    for (k in this.nodeFloatArrays) {
+      renderer = sigma.webgl.nodes[k];
+
+      for (a = this.nodeFloatArrays[k].nodes, i = 0, l = a.length; i < l; i++) {
+        if (!this.nodeFloatArrays[k].array)
+          this.nodeFloatArrays[k].array = new Float32Array(
+            a.length * renderer.POINTS * renderer.ATTRIBUTES
+          );
+
+        // Just check that the edge and both its extremities are visible:
+        if (
+          !a[i].hidden
+        )
+          renderer.addNode(
+            a[i],
+            this.nodeFloatArrays[k].array,
+            i * renderer.POINTS * renderer.ATTRIBUTES,
+            options.prefix,
+            this.settings
+          );
+      }
+    }
+
+    return this;
+  };
+
+
+
+
+  /**
+   * This method renders the graph. It basically calls each program (and
+   * generate them if they do not exist yet) to render nodes and edges, batched
+   * per renderer.
+   *
+   * As in the canvas renderer, it is possible to display edges, nodes and / or
+   * labels in batches, to make the whole thing way more scalable.
+   *
+   * @param  {?object}               params Eventually an object of options.
+   * @return {sigma.renderers.webgl}        Returns the instance itself.
+   */
+  sigma.renderers.webgl.prototype.render = function(params) {
+    var a,
+        i,
+        l,
+        k,
+        o,
+        program,
+        renderer,
+        self = this,
+        graph = this.graph,
+        nodesGl = this.contexts.nodes,
+        edgesGl = this.contexts.edges,
+        matrix = this.camera.getMatrix(),
+        options = sigma.utils.extend(params, this.options),
+        drawLabels = this.settings(options, 'drawLabels'),
+        drawEdges = this.settings(options, 'drawEdges'),
+        drawNodes = this.settings(options, 'drawNodes');
+
+    // Call the resize function:
+    this.resize(false);
+
+    // Check the 'hideEdgesOnMove' setting:
+    if (this.settings(options, 'hideEdgesOnMove'))
+      if (this.camera.isAnimated || this.camera.isMoving)
+        drawEdges = false;
+
+    // Clear canvases:
+    this.clear();
+
+    // Translate matrix to [width/2, height/2]:
+    matrix = sigma.utils.matrices.multiply(
+      matrix,
+      sigma.utils.matrices.translation(this.width / 2, this.height / 2)
+    );
+
+    // Kill running jobs:
+    for (k in this.jobs)
+      if (conrad.hasJob(k))
+        conrad.killJob(k);
+
+    if (drawEdges) {
+      if (this.settings(options, 'batchEdgesDrawing'))
+        (function() {
+          var a,
+              k,
+              i,
+              id,
+              job,
+              arr,
+              end,
+              start,
+              renderer,
+              batchSize,
+              currentProgram;
+
+          id = 'edges_' + this.conradId;
+          batchSize = this.settings(options, 'webglEdgesBatchSize');
+          a = Object.keys(this.edgeFloatArrays);
+
+          if (!a.length)
+            return;
+          i = 0;
+          renderer = sigma.webgl.edges[a[i]];
+          arr = this.edgeFloatArrays[a[i]].array;
+          start = 0;
+          end = Math.min(
+            start + batchSize * renderer.POINTS,
+            arr.length / renderer.ATTRIBUTES
+          );
+
+          job = function() {
+            // Check program:
+            if (!this.edgePrograms[a[i]])
+              this.edgePrograms[a[i]] = renderer.initProgram(edgesGl);
+
+            if (start < end) {
+              edgesGl.useProgram(this.edgePrograms[a[i]]);
+              renderer.render(
+                edgesGl,
+                this.edgePrograms[a[i]],
+                arr,
+                {
+                  settings: this.settings,
+                  matrix: matrix,
+                  width: this.width,
+                  height: this.height,
+                  ratio: this.camera.ratio,
+                  scalingRatio: this.settings(
+                    options,
+                    'webglOversamplingRatio'
+                  ),
+                  start: start,
+                  count: end - start
+                }
+              );
+            }
+
+            // Catch job's end:
+            if (
+              end >= arr.length / renderer.ATTRIBUTES &&
+              i === a.length - 1
+            ) {
+              delete this.jobs[id];
+              return false;
+            }
+
+            if (end >= arr.length / renderer.ATTRIBUTES) {
+              i++;
+              arr = this.edgeFloatArrays[a[i]].array;
+              renderer = sigma.webgl.edges[a[i]];
+              start = 0;
+              end = Math.min(
+                start + batchSize * renderer.POINTS,
+                arr.length / renderer.ATTRIBUTES
+              );
+            } else {
+              start = end;
+              end = Math.min(
+                start + batchSize * renderer.POINTS,
+                arr.length / renderer.ATTRIBUTES
+              );
+            }
+
+            return true;
+          };
+
+          this.jobs[id] = job;
+          conrad.addJob(id, job.bind(this));
+        }).call(this);
+      else {
+        for (k in this.edgeFloatArrays) {
+          renderer = sigma.webgl.edges[k];
+
+          // Check program:
+          if (!this.edgePrograms[k])
+            this.edgePrograms[k] = renderer.initProgram(edgesGl);
+
+          // Render
+          if (this.edgeFloatArrays[k]) {
+            edgesGl.useProgram(this.edgePrograms[k]);
+            renderer.render(
+              edgesGl,
+              this.edgePrograms[k],
+              this.edgeFloatArrays[k].array,
+              {
+                settings: this.settings,
+                matrix: matrix,
+                width: this.width,
+                height: this.height,
+                ratio: this.camera.ratio,
+                scalingRatio: this.settings(options, 'webglOversamplingRatio')
+              }
+            );
+          }
+        }
+      }
+    }
+
+    if (drawNodes) {
+      // Enable blending:
+      nodesGl.blendFunc(nodesGl.SRC_ALPHA, nodesGl.ONE_MINUS_SRC_ALPHA);
+      nodesGl.enable(nodesGl.BLEND);
+
+      for (k in this.nodeFloatArrays) {
+        renderer = sigma.webgl.nodes[k];
+
+        // Check program:
+        if (!this.nodePrograms[k])
+          this.nodePrograms[k] = renderer.initProgram(nodesGl);
+
+        // Render
+        if (this.nodeFloatArrays[k]) {
+          nodesGl.useProgram(this.nodePrograms[k]);
+          renderer.render(
+            nodesGl,
+            this.nodePrograms[k],
+            this.nodeFloatArrays[k].array,
+            {
+              settings: this.settings,
+              matrix: matrix,
+              width: this.width,
+              height: this.height,
+              ratio: this.camera.ratio,
+              scalingRatio: this.settings(options, 'webglOversamplingRatio')
+            }
+          );
+        }
+      }
+    }
+
+    if (drawLabels) {
+      a = this.camera.quadtree.area(
+        this.camera.getRectangle(this.width, this.height)
+      );
+
+      // Apply camera view to these nodes:
+      this.camera.applyView(
+        undefined,
+        undefined,
+        {
+          nodes: a,
+          edges: [],
+          width: this.width,
+          height: this.height
+        }
+      );
+
+      o = function(key) {
+        return self.settings({
+          prefix: self.camera.prefix
+        }, key);
+      };
+
+      for (i = 0, l = a.length; i < l; i++)
+        if (!a[i].hidden)
+          (
+            sigma.canvas.labels[
+              a[i].type ||
+              this.settings(options, 'defaultNodeType')
+            ] || sigma.canvas.labels.def
+          )(a[i], this.contexts.labels, o);
+    }
+
+    this.dispatchEvent('render');
+
+    return this;
+  };
+
+
+
+
+  /**
+   * This method creates a DOM element of the specified type, switches its
+   * position to "absolute", references it to the domElements attribute, and
+   * finally appends it to the container.
+   *
+   * @param  {string}   tag   The label tag.
+   * @param  {string}   id    The id of the element (to store it in
+   *                          "domElements").
+   * @param  {?boolean} webgl Will init the WebGL context if true.
+   */
+  sigma.renderers.webgl.prototype.initDOM = function(tag, id, webgl) {
+    var gl,
+        dom = document.createElement(tag),
+        self = this;
+
+    dom.style.position = 'absolute';
+    dom.setAttribute('class', 'sigma-' + id);
+
+    this.domElements[id] = dom;
+    this.container.appendChild(dom);
+
+    if (tag.toLowerCase() === 'canvas') {
+      this.contexts[id] = dom.getContext(webgl ? 'experimental-webgl' : '2d', {
+        preserveDrawingBuffer: true
+      });
+
+      // Adding webgl context loss listeners
+      if (webgl) {
+        dom.addEventListener('webglcontextlost', function(e) {
+          e.preventDefault();
+        }, false);
+
+        dom.addEventListener('webglcontextrestored', function(e) {
+          self.render();
+        }, false);
+      }
+    }
+  };
+
+  /**
+   * This method resizes each DOM elements in the container and stores the new
+   * dimensions. Then, it renders the graph.
+   *
+   * @param  {?number}               width  The new width of the container.
+   * @param  {?number}               height The new height of the container.
+   * @return {sigma.renderers.webgl}        Returns the instance itself.
+   */
+  sigma.renderers.webgl.prototype.resize = function(w, h) {
+    var k,
+        oldWidth = this.width,
+        oldHeight = this.height;
+
+    if (w !== undefined && h !== undefined) {
+      this.width = w;
+      this.height = h;
+    } else {
+      this.width = this.container.offsetWidth;
+      this.height = this.container.offsetHeight;
+
+      w = this.width;
+      h = this.height;
+    }
+
+    if (oldWidth !== this.width || oldHeight !== this.height) {
+      for (k in this.domElements) {
+        this.domElements[k].style.width = w + 'px';
+        this.domElements[k].style.height = h + 'px';
+
+        if (this.domElements[k].tagName.toLowerCase() === 'canvas') {
+          // If simple 2D canvas:
+          if (this.contexts[k] && this.contexts[k].scale) {
+            this.domElements[k].setAttribute('width', w + 'px');
+            this.domElements[k].setAttribute('height', h + 'px');
+          } else {
+            this.domElements[k].setAttribute(
+              'width',
+              (w * this.settings('webglOversamplingRatio')) + 'px'
+            );
+            this.domElements[k].setAttribute(
+              'height',
+              (h * this.settings('webglOversamplingRatio')) + 'px'
+            );
+          }
+        }
+      }
+    }
+
+    // Scale:
+    for (k in this.contexts)
+      if (this.contexts[k] && this.contexts[k].viewport)
+        this.contexts[k].viewport(
+          0,
+          0,
+          this.width * this.settings('webglOversamplingRatio'),
+          this.height * this.settings('webglOversamplingRatio')
+        );
+
+    return this;
+  };
+
+  /**
+   * This method clears each canvas.
+   *
+   * @return {sigma.renderers.webgl} Returns the instance itself.
+   */
+  sigma.renderers.webgl.prototype.clear = function() {
+    var k;
+
+    for (k in this.domElements)
+      if (this.domElements[k].tagName === 'CANVAS')
+        this.domElements[k].width = this.domElements[k].width;
+
+    this.contexts.nodes.clear(this.contexts.nodes.COLOR_BUFFER_BIT);
+    this.contexts.edges.clear(this.contexts.edges.COLOR_BUFFER_BIT);
+
+    return this;
+  };
+
+  /**
+   * This method kills contexts and other attributes.
+   */
+  sigma.renderers.webgl.prototype.kill = function() {
+    var k,
+        captor;
+
+    // Kill captors:
+    while ((captor = this.captors.pop()))
+      captor.kill();
+    delete this.captors;
+
+    // Kill contexts:
+    for (k in this.domElements) {
+      this.domElements[k].parentNode.removeChild(this.domElements[k]);
+      delete this.domElements[k];
+      delete this.contexts[k];
+    }
+    delete this.domElements;
+    delete this.contexts;
+  };
+
+
+
+
+  /**
+   * The object "sigma.webgl.nodes" contains the different WebGL node
+   * renderers. The default one draw nodes as discs. Here are the attributes
+   * any node renderer must have:
+   *
+   * {number}   POINTS      The number of points required to draw a node.
+   * {number}   ATTRIBUTES  The number of attributes needed to draw one point.
+   * {function} addNode     A function that adds a node to the data stack that
+   *                        will be given to the buffer. Here is the arguments:
+   *                        > {object}       node
+   *                        > {number}       index   The node index in the
+   *                                                 nodes array.
+   *                        > {Float32Array} data    The stack.
+   *                        > {object}       options Some options.
+   * {function} render      The function that will effectively render the nodes
+   *                        into the buffer.
+   *                        > {WebGLRenderingContext} gl
+   *                        > {WebGLProgram}          program
+   *                        > {Float32Array} data    The stack to give to the
+   *                                                 buffer.
+   *                        > {object}       params  An object containing some
+   *                                                 options, like width,
+   *                                                 height, the camera ratio.
+   * {function} initProgram The function that will initiate the program, with
+   *                        the relevant shaders and parameters. It must return
+   *                        the newly created program.
+   *
+   * Check sigma.webgl.nodes.def or sigma.webgl.nodes.fast to see how it
+   * works more precisely.
+   */
+  sigma.utils.pkg('sigma.webgl.nodes');
+
+
+
+
+  /**
+   * The object "sigma.webgl.edges" contains the different WebGL edge
+   * renderers. The default one draw edges as direct lines. Here are the
+   * attributes any edge renderer must have:
+   *
+   * {number}   POINTS      The number of points required to draw an edge.
+   * {number}   ATTRIBUTES  The number of attributes needed to draw one point.
+   * {function} addEdge     A function that adds an edge to the data stack that
+   *                        will be given to the buffer. Here is the arguments:
+   *                        > {object}       edge
+   *                        > {object}       source
+   *                        > {object}       target
+   *                        > {Float32Array} data    The stack.
+   *                        > {object}       options Some options.
+   * {function} render      The function that will effectively render the edges
+   *                        into the buffer.
+   *                        > {WebGLRenderingContext} gl
+   *                        > {WebGLProgram}          program
+   *                        > {Float32Array} data    The stack to give to the
+   *                                                 buffer.
+   *                        > {object}       params  An object containing some
+   *                                                 options, like width,
+   *                                                 height, the camera ratio.
+   * {function} initProgram The function that will initiate the program, with
+   *                        the relevant shaders and parameters. It must return
+   *                        the newly created program.
+   *
+   * Check sigma.webgl.edges.def or sigma.webgl.edges.fast to see how it
+   * works more precisely.
+   */
+  sigma.utils.pkg('sigma.webgl.edges');
+
+
+
+
+  /**
+   * The object "sigma.canvas.labels" contains the different
+   * label renderers for the WebGL renderer. Since displaying texts in WebGL is
+   * definitely painful and since there a way less labels to display than nodes
+   * or edges, the default renderer simply renders them in a canvas.
+   *
+   * A labels renderer is a simple function, taking as arguments the related
+   * node, the renderer and a settings function.
+   */
+  sigma.utils.pkg('sigma.canvas.labels');
+}).call(this);
diff --git a/demo/static/sigmajs/renderers/svg/sigma.svg.edges.curve.js b/demo/static/sigmajs/renderers/svg/sigma.svg.edges.curve.js
new file mode 100644
index 000000000..37f82e691
--- /dev/null
+++ b/demo/static/sigmajs/renderers/svg/sigma.svg.edges.curve.js
@@ -0,0 +1,84 @@
+;(function() {
+  'use strict';
+
+  sigma.utils.pkg('sigma.svg.edges');
+
+  /**
+   * The curve edge renderer. It renders the node as a bezier curve.
+   */
+  sigma.svg.edges.curve = {
+
+    /**
+     * SVG Element creation.
+     *
+     * @param  {object}                   edge       The edge object.
+     * @param  {object}                   source     The source node object.
+     * @param  {object}                   target     The target node object.
+     * @param  {configurable}             settings   The settings function.
+     */
+    create: function(edge, source, target, settings) {
+      var color = edge.color,
+          prefix = settings('prefix') || '',
+          edgeColor = settings('edgeColor'),
+          defaultNodeColor = settings('defaultNodeColor'),
+          defaultEdgeColor = settings('defaultEdgeColor');
+
+      if (!color)
+        switch (edgeColor) {
+          case 'source':
+            color = source.color || defaultNodeColor;
+            break;
+          case 'target':
+            color = target.color || defaultNodeColor;
+            break;
+          default:
+            color = defaultEdgeColor;
+            break;
+        }
+
+      var path = document.createElementNS(settings('xmlns'), 'path');
+
+      // Attributes
+      path.setAttributeNS(null, 'data-edge-id', edge.id);
+      path.setAttributeNS(null, 'class', settings('classPrefix') + '-edge');
+      path.setAttributeNS(null, 'stroke', color);
+
+      return path;
+    },
+
+    /**
+     * SVG Element update.
+     *
+     * @param  {object}                   edge       The edge object.
+     * @param  {DOMElement}               line       The line DOM Element.
+     * @param  {object}                   source     The source node object.
+     * @param  {object}                   target     The target node object.
+     * @param  {configurable}             settings   The settings function.
+     */
+    update: function(edge, path, source, target, settings) {
+      var prefix = settings('prefix') || '';
+
+      path.setAttributeNS(null, 'stroke-width', edge[prefix + 'size'] || 1);
+
+      // Control point
+      var cx = (source[prefix + 'x'] + target[prefix + 'x']) / 2 +
+        (target[prefix + 'y'] - source[prefix + 'y']) / 4,
+          cy = (source[prefix + 'y'] + target[prefix + 'y']) / 2 +
+        (source[prefix + 'x'] - target[prefix + 'x']) / 4;
+
+      // Path
+      var p = 'M' + source[prefix + 'x'] + ',' + source[prefix + 'y'] + ' ' +
+              'Q' + cx + ',' + cy + ' ' +
+              target[prefix + 'x'] + ',' + target[prefix + 'y'];
+
+      // Updating attributes
+      path.setAttributeNS(null, 'd', p);
+      path.setAttributeNS(null, 'fill', 'none');
+
+      // Showing
+      path.style.display = '';
+
+      return this;
+    }
+  };
+})();
diff --git a/demo/static/sigmajs/renderers/svg/sigma.svg.edges.def.js b/demo/static/sigmajs/renderers/svg/sigma.svg.edges.def.js
new file mode 100644
index 000000000..e48d57b1f
--- /dev/null
+++ b/demo/static/sigmajs/renderers/svg/sigma.svg.edges.def.js
@@ -0,0 +1,73 @@
+;(function() {
+  'use strict';
+
+  sigma.utils.pkg('sigma.svg.edges');
+
+  /**
+   * The default edge renderer. It renders the node as a simple line.
+   */
+  sigma.svg.edges.def = {
+
+    /**
+     * SVG Element creation.
+     *
+     * @param  {object}                   edge       The edge object.
+     * @param  {object}                   source     The source node object.
+     * @param  {object}                   target     The target node object.
+     * @param  {configurable}             settings   The settings function.
+     */
+    create: function(edge, source, target, settings) {
+      var color = edge.color,
+          prefix = settings('prefix') || '',
+          edgeColor = settings('edgeColor'),
+          defaultNodeColor = settings('defaultNodeColor'),
+          defaultEdgeColor = settings('defaultEdgeColor');
+
+      if (!color)
+        switch (edgeColor) {
+          case 'source':
+            color = source.color || defaultNodeColor;
+            break;
+          case 'target':
+            color = target.color || defaultNodeColor;
+            break;
+          default:
+            color = defaultEdgeColor;
+            break;
+        }
+
+      var line = document.createElementNS(settings('xmlns'), 'line');
+
+      // Attributes
+      line.setAttributeNS(null, 'data-edge-id', edge.id);
+      line.setAttributeNS(null, 'class', settings('classPrefix') + '-edge');
+      line.setAttributeNS(null, 'stroke', color);
+
+      return line;
+    },
+
+    /**
+     * SVG Element update.
+     *
+     * @param  {object}                   edge       The edge object.
+     * @param  {DOMElement}               line       The line DOM Element.
+     * @param  {object}                   source     The source node object.
+     * @param  {object}                   target     The target node object.
+     * @param  {configurable}             settings   The settings function.
+     */
+    update: function(edge, line, source, target, settings) {
+      var prefix = settings('prefix') || '';
+
+      line.setAttributeNS(null, 'stroke-width', edge[prefix + 'size'] || 1);
+      line.setAttributeNS(null, 'x1', source[prefix + 'x']);
+      line.setAttributeNS(null, 'y1', source[prefix + 'y']);
+      line.setAttributeNS(null, 'x2', target[prefix + 'x']);
+      line.setAttributeNS(null, 'y2', target[prefix + 'y']);
+
+      // Showing
+      line.style.display = '';
+
+      return this;
+    }
+  };
+})();
diff --git a/demo/static/sigmajs/renderers/svg/sigma.svg.hovers.def.js b/demo/static/sigmajs/renderers/svg/sigma.svg.hovers.def.js
new file mode 100644
index 000000000..6525ab9ad
--- /dev/null
+++ b/demo/static/sigmajs/renderers/svg/sigma.svg.hovers.def.js
@@ -0,0 +1,113 @@
+;(function(undefined) {
+  'use strict';
+
+  if (typeof sigma === 'undefined')
+    throw 'sigma is not declared';
+
+  // Initialize packages:
+  sigma.utils.pkg('sigma.svg.hovers');
+
+  /**
+   * The default hover renderer.
+   */
+  sigma.svg.hovers.def = {
+
+    /**
+     * SVG Element creation.
+     *
+     * @param  {object}           node               The node object.
+     * @param  {CanvasElement}    measurementCanvas  A fake canvas handled by
+     *                            the svg to perform some measurements and
+     *                            passed by the renderer.
+     * @param  {DOMElement}       nodeCircle         The node DOM Element.
+     * @param  {configurable}     settings           The settings function.
+     */
+    create: function(node, nodeCircle, measurementCanvas, settings) {
+
+      // Defining visual properties
+      var x,
+          y,
+          w,
+          h,
+          e,
+          d,
+          fontStyle = settings('hoverFontStyle') || settings('fontStyle'),
+          prefix = settings('prefix') || '',
+          size = node[prefix + 'size'],
+          fontSize = (settings('labelSize') === 'fixed') ?
+            settings('defaultLabelSize') :
+            settings('labelSizeRatio') * size,
+          fontColor = (settings('labelHoverColor') === 'node') ?
+                        (node.color || settings('defaultNodeColor')) :
+                        settings('defaultLabelHoverColor');
+
+      // Creating elements
+      var group = document.createElementNS(settings('xmlns'), 'g'),
+          rectangle = document.createElementNS(settings('xmlns'), 'rect'),
+          circle = document.createElementNS(settings('xmlns'), 'circle'),
+          text = document.createElementNS(settings('xmlns'), 'text');
+
+      // Defining properties
+      group.setAttributeNS(null, 'class', settings('classPrefix') + '-hover');
+      group.setAttributeNS(null, 'data-node-id', node.id);
+
+      if (typeof node.label === 'string') {
+
+        // Text
+        text.innerHTML = node.label;
+        text.textContent = node.label;
+        text.setAttributeNS(
+            null,
+            'class',
+            settings('classPrefix') + '-hover-label');
+        text.setAttributeNS(null, 'font-size', fontSize);
+        text.setAttributeNS(null, 'font-family', settings('font'));
+        text.setAttributeNS(null, 'fill', fontColor);
+        text.setAttributeNS(null, 'x',
+          Math.round(node[prefix + 'x'] + size + 3));
+        text.setAttributeNS(null, 'y',
+          Math.round(node[prefix + 'y'] + fontSize / 3));
+
+        // Measures
+        // OPTIMIZE: Find a better way than a measurement canvas
+        x = Math.round(node[prefix + 'x'] - fontSize / 2 - 2);
+        y = Math.round(node[prefix + 'y'] - fontSize / 2 - 2);
+        w = Math.round(
+          measurementCanvas.measureText(node.label).width +
+            fontSize / 2 + size + 9
+        );
+        h = Math.round(fontSize + 4);
+        e = Math.round(fontSize / 2 + 2);
+
+        // Circle
+        circle.setAttributeNS(
+            null,
+            'class',
+            settings('classPrefix') + '-hover-area');
+        circle.setAttributeNS(null, 'fill', '#fff');
+        circle.setAttributeNS(null, 'cx', node[prefix + 'x']);
+        circle.setAttributeNS(null, 'cy', node[prefix + 'y']);
+        circle.setAttributeNS(null, 'r', e);
+
+        // Rectangle
+        rectangle.setAttributeNS(
+            null,
+            'class',
+            settings('classPrefix') + '-hover-area');
+        rectangle.setAttributeNS(null, 'fill', '#fff');
+        rectangle.setAttributeNS(null, 'x', node[prefix + 'x'] + e / 4);
+        rectangle.setAttributeNS(null, 'y', node[prefix + 'y'] - e);
+        rectangle.setAttributeNS(null, 'width', w);
+        rectangle.setAttributeNS(null, 'height', h);
+      }
+
+      // Appending childs
+      group.appendChild(circle);
+      group.appendChild(rectangle);
+      group.appendChild(text);
+      group.appendChild(nodeCircle);
+
+      return group;
+    }
+  };
+}).call(this);
diff --git a/demo/static/sigmajs/renderers/svg/sigma.svg.labels.def.js b/demo/static/sigmajs/renderers/svg/sigma.svg.labels.def.js
new file mode 100644
index 000000000..4027c83b6
--- /dev/null
+++ b/demo/static/sigmajs/renderers/svg/sigma.svg.labels.def.js
@@ -0,0 +1,80 @@
+;(function(undefined) {
+  'use strict';
+
+  if (typeof sigma === 'undefined')
+    throw 'sigma is not declared';
+
+  // Initialize packages:
+  sigma.utils.pkg('sigma.svg.labels');
+
+  /**
+   * The default label renderer. It renders the label as a simple text.
+   */
+  sigma.svg.labels.def = {
+
+    /**
+     * SVG Element creation.
+     *
+     * @param  {object}                   node       The node object.
+     * @param  {configurable}             settings   The settings function.
+     */
+    create: function(node, settings) {
+      var prefix = settings('prefix') || '',
+          size = node[prefix + 'size'],
+          text = document.createElementNS(settings('xmlns'), 'text');
+
+      var fontSize = (settings('labelSize') === 'fixed') ?
+        settings('defaultLabelSize') :
+        settings('labelSizeRatio') * size;
+
+      var fontColor = (settings('labelColor') === 'node') ?
+        (node.color || settings('defaultNodeColor')) :
+        settings('defaultLabelColor');
+
+      text.setAttributeNS(null, 'data-label-target', node.id);
+      text.setAttributeNS(null, 'class', settings('classPrefix') + '-label');
+      text.setAttributeNS(null, 'font-size', fontSize);
+      text.setAttributeNS(null, 'font-family', settings('font'));
+      text.setAttributeNS(null, 'fill', fontColor);
+
+      text.innerHTML = node.label;
+      text.textContent = node.label;
+
+      return text;
+    },
+
+    /**
+     * SVG Element update.
+     *
+     * @param  {object}                   node     The node object.
+     * @param  {DOMElement}               text     The label DOM element.
+     * @param  {configurable}             settings The settings function.
+     */
+    update: function(node, text, settings) {
+      var prefix = settings('prefix') || '',
+          size = node[prefix + 'size'];
+
+      var fontSize = (settings('labelSize') === 'fixed') ?
+        settings('defaultLabelSize') :
+        settings('labelSizeRatio') * size;
+
+      // Case when we don't want to display the label
+      if (!settings('forceLabels') && size < settings('labelThreshold'))
+        return;
+
+      if (typeof node.label !== 'string')
+        return;
+
+      // Updating
+      text.setAttributeNS(null, 'x',
+        Math.round(node[prefix + 'x'] + size + 3));
+      text.setAttributeNS(null, 'y',
+        Math.round(node[prefix + 'y'] + fontSize / 3));
+
+      // Showing
+      text.style.display = '';
+
+      return this;
+    }
+  };
+}).call(this);
diff --git a/demo/static/sigmajs/renderers/svg/sigma.svg.nodes.def.js b/demo/static/sigmajs/renderers/svg/sigma.svg.nodes.def.js
new file mode 100644
index 000000000..4c01b7a44
--- /dev/null
+++ b/demo/static/sigmajs/renderers/svg/sigma.svg.nodes.def.js
@@ -0,0 +1,58 @@
+;(function() {
+  'use strict';
+
+  sigma.utils.pkg('sigma.svg.nodes');
+
+  /**
+   * The default node renderer. It renders the node as a simple disc.
+   */
+  sigma.svg.nodes.def = {
+
+    /**
+     * SVG Element creation.
+     *
+     * @param  {object}                   node     The node object.
+     * @param  {configurable}             settings The settings function.
+     */
+    create: function(node, settings) {
+      var prefix = settings('prefix') || '',
+          circle = document.createElementNS(settings('xmlns'), 'circle');
+
+      // Defining the node's circle
+      circle.setAttributeNS(null, 'data-node-id', node.id);
+      circle.setAttributeNS(null, 'class', settings('classPrefix') + '-node');
+      circle.setAttributeNS(
+        null, 'fill', node.color || settings('defaultNodeColor'));
+
+      // Returning the DOM Element
+      return circle;
+    },
+
+    /**
+     * SVG Element update.
+     *
+     * @param  {object}                   node     The node object.
+     * @param  {DOMElement}               circle   The node DOM element.
+     * @param  {configurable}             settings The settings function.
+     */
+    update: function(node, circle, settings) {
+      var prefix = settings('prefix') || '';
+
+      // Applying changes
+      // TODO: optimize - check if necessary
+      circle.setAttributeNS(null, 'cx', node[prefix + 'x']);
+      circle.setAttributeNS(null, 'cy', node[prefix + 'y']);
+      circle.setAttributeNS(null, 'r', node[prefix + 'size']);
+
+      // Updating only if not freestyle
+      if (!settings('freeStyle'))
+        circle.setAttributeNS(
+          null, 'fill', node.color || settings('defaultNodeColor'));
+
+      // Showing
+      circle.style.display = '';
+
+      return this;
+    }
+  };
+})();
diff --git a/demo/static/sigmajs/renderers/svg/sigma.svg.utils.js b/demo/static/sigmajs/renderers/svg/sigma.svg.utils.js
new file mode 100644
index 000000000..f00e2e5ec
--- /dev/null
+++ b/demo/static/sigmajs/renderers/svg/sigma.svg.utils.js
@@ -0,0 +1,31 @@
+;(function() {
+  'use strict';
+
+  sigma.utils.pkg('sigma.svg.utils');
+
+  /**
+   * Some useful functions used by sigma's SVG renderer.
+   */
+  sigma.svg.utils = {
+
+    /**
+     * SVG Element show.
+     *
+     * @param  {DOMElement}               element   The DOM element to show.
+     */
+    show: function(element) {
+      element.style.display = '';
+      return this;
+    },
+
+    /**
+     * SVG Element hide.
+     *
+     * @param  {DOMElement}               element   The DOM element to hide.
+     */
+    hide: function(element) {
+      element.style.display = 'none';
+      return this;
+    }
+  };
+})();
diff --git a/demo/static/sigmajs/renderers/webgl/sigma.webgl.edges.arrow.js b/demo/static/sigmajs/renderers/webgl/sigma.webgl.edges.arrow.js
new file mode 100644
index 000000000..4b548acd2
--- /dev/null
+++ b/demo/static/sigmajs/renderers/webgl/sigma.webgl.edges.arrow.js
@@ -0,0 +1,391 @@
+;(function() {
+  'use strict';
+
+  sigma.utils.pkg('sigma.webgl.edges');
+
+  /**
+   * This edge renderer will display edges as arrows going from the source node
+   * to the target node. To deal with edge thicknesses, the lines are made of
+   * three triangles: two forming rectangles, with the gl.TRIANGLES drawing
+   * mode.
+   *
+   * It is expensive, since drawing a single edge requires 9 points, each
+   * having a lot of attributes.
+   */
+  sigma.webgl.edges.arrow = {
+    POINTS: 9,
+    ATTRIBUTES: 11,
+    addEdge: function(edge, source, target, data, i, prefix, settings) {
+      var w = (edge[prefix + 'size'] || 1) / 2,
+          x1 = source[prefix + 'x'],
+          y1 = source[prefix + 'y'],
+          x2 = target[prefix + 'x'],
+          y2 = target[prefix + 'y'],
+          targetSize = target[prefix + 'size'],
+          color = edge.color;
+
+      if (!color)
+        switch (settings('edgeColor')) {
+          case 'source':
+            color = source.color || settings('defaultNodeColor');
+            break;
+          case 'target':
+            color = target.color || settings('defaultNodeColor');
+            break;
+          default:
+            color = settings('defaultEdgeColor');
+            break;
+        }
+
+      // Normalize color:
+      color = sigma.utils.floatColor(color);
+
+      data[i++] = x1;
+      data[i++] = y1;
+      data[i++] = x2;
+      data[i++] = y2;
+      data[i++] = w;
+      data[i++] = targetSize;
+      data[i++] = 0.0;
+      data[i++] = 0.0;
+      data[i++] = 0.0;
+      data[i++] = 0.0;
+      data[i++] = color;
+
+      data[i++] = x2;
+      data[i++] = y2;
+      data[i++] = x1;
+      data[i++] = y1;
+      data[i++] = w;
+      data[i++] = targetSize;
+      data[i++] = 1.0;
+      data[i++] = 1.0;
+      data[i++] = 0.0;
+      data[i++] = 0.0;
+      data[i++] = color;
+
+      data[i++] = x2;
+      data[i++] = y2;
+      data[i++] = x1;
+      data[i++] = y1;
+      data[i++] = w;
+      data[i++] = targetSize;
+      data[i++] = 1.0;
+      data[i++] = 0.0;
+      data[i++] = 0.0;
+      data[i++] = 0.0;
+      data[i++] = color;
+
+      data[i++] = x2;
+      data[i++] = y2;
+      data[i++] = x1;
+      data[i++] = y1;
+      data[i++] = w;
+      data[i++] = targetSize;
+      data[i++] = 1.0;
+      data[i++] = 0.0;
+      data[i++] = 0.0;
+      data[i++] = 0.0;
+      data[i++] = color;
+
+      data[i++] = x1;
+      data[i++] = y1;
+      data[i++] = x2;
+      data[i++] = y2;
+      data[i++] = w;
+      data[i++] = targetSize;
+      data[i++] = 0.0;
+      data[i++] = 1.0;
+      data[i++] = 0.0;
+      data[i++] = 0.0;
+      data[i++] = color;
+
+      data[i++] = x1;
+      data[i++] = y1;
+      data[i++] = x2;
+      data[i++] = y2;
+      data[i++] = w;
+      data[i++] = targetSize;
+      data[i++] = 0.0;
+      data[i++] = 0.0;
+      data[i++] = 0.0;
+      data[i++] = 0.0;
+      data[i++] = color;
+
+      // Arrow head:
+      data[i++] = x2;
+      data[i++] = y2;
+      data[i++] = x1;
+      data[i++] = y1;
+      data[i++] = w;
+      data[i++] = targetSize;
+      data[i++] = 1.0;
+      data[i++] = 0.0;
+      data[i++] = 1.0;
+      data[i++] = -1.0;
+      data[i++] = color;
+
+      data[i++] = x2;
+      data[i++] = y2;
+      data[i++] = x1;
+      data[i++] = y1;
+      data[i++] = w;
+      data[i++] = targetSize;
+      data[i++] = 1.0;
+      data[i++] = 0.0;
+      data[i++] = 1.0;
+      data[i++] = 0.0;
+      data[i++] = color;
+
+      data[i++] = x2;
+      data[i++] = y2;
+      data[i++] = x1;
+      data[i++] = y1;
+      data[i++] = w;
+      data[i++] = targetSize;
+      data[i++] = 1.0;
+      data[i++] = 0.0;
+      data[i++] = 1.0;
+      data[i++] = 1.0;
+      data[i++] = color;
+    },
+    render: function(gl, program, data, params) {
+      var buffer;
+
+      // Define attributes:
+      var positionLocation1 =
+            gl.getAttribLocation(program, 'a_pos1'),
+          positionLocation2 =
+            gl.getAttribLocation(program, 'a_pos2'),
+          thicknessLocation =
+            gl.getAttribLocation(program, 'a_thickness'),
+          targetSizeLocation =
+            gl.getAttribLocation(program, 'a_tSize'),
+          delayLocation =
+            gl.getAttribLocation(program, 'a_delay'),
+          minusLocation =
+            gl.getAttribLocation(program, 'a_minus'),
+          headLocation =
+            gl.getAttribLocation(program, 'a_head'),
+          headPositionLocation =
+            gl.getAttribLocation(program, 'a_headPosition'),
+          colorLocation =
+            gl.getAttribLocation(program, 'a_color'),
+          resolutionLocation =
+            gl.getUniformLocation(program, 'u_resolution'),
+          matrixLocation =
+            gl.getUniformLocation(program, 'u_matrix'),
+          matrixHalfPiLocation =
+            gl.getUniformLocation(program, 'u_matrixHalfPi'),
+          matrixHalfPiMinusLocation =
+            gl.getUniformLocation(program, 'u_matrixHalfPiMinus'),
+          ratioLocation =
+            gl.getUniformLocation(program, 'u_ratio'),
+          nodeRatioLocation =
+            gl.getUniformLocation(program, 'u_nodeRatio'),
+          arrowHeadLocation =
+            gl.getUniformLocation(program, 'u_arrowHead'),
+          scaleLocation =
+            gl.getUniformLocation(program, 'u_scale');
+
+      buffer = gl.createBuffer();
+      gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
+      gl.bufferData(gl.ARRAY_BUFFER, data, gl.STATIC_DRAW);
+
+      gl.uniform2f(resolutionLocation, params.width, params.height);
+      gl.uniform1f(
+        ratioLocation,
+        params.ratio / Math.pow(params.ratio, params.settings('edgesPowRatio'))
+      );
+      gl.uniform1f(
+        nodeRatioLocation,
+        Math.pow(params.ratio, params.settings('nodesPowRatio')) /
+        params.ratio
+      );
+      gl.uniform1f(arrowHeadLocation, 5.0);
+      gl.uniform1f(scaleLocation, params.scalingRatio);
+      gl.uniformMatrix3fv(matrixLocation, false, params.matrix);
+      gl.uniformMatrix2fv(
+        matrixHalfPiLocation,
+        false,
+        sigma.utils.matrices.rotation(Math.PI / 2, true)
+      );
+      gl.uniformMatrix2fv(
+        matrixHalfPiMinusLocation,
+        false,
+        sigma.utils.matrices.rotation(-Math.PI / 2, true)
+      );
+
+      gl.enableVertexAttribArray(positionLocation1);
+      gl.enableVertexAttribArray(positionLocation2);
+      gl.enableVertexAttribArray(thicknessLocation);
+      gl.enableVertexAttribArray(targetSizeLocation);
+      gl.enableVertexAttribArray(delayLocation);
+      gl.enableVertexAttribArray(minusLocation);
+      gl.enableVertexAttribArray(headLocation);
+      gl.enableVertexAttribArray(headPositionLocation);
+      gl.enableVertexAttribArray(colorLocation);
+
+      gl.vertexAttribPointer(positionLocation1,
+        2,
+        gl.FLOAT,
+        false,
+        this.ATTRIBUTES * Float32Array.BYTES_PER_ELEMENT,
+        0
+      );
+      gl.vertexAttribPointer(positionLocation2,
+        2,
+        gl.FLOAT,
+        false,
+        this.ATTRIBUTES * Float32Array.BYTES_PER_ELEMENT,
+        8
+      );
+      gl.vertexAttribPointer(thicknessLocation,
+        1,
+        gl.FLOAT,
+        false,
+        this.ATTRIBUTES * Float32Array.BYTES_PER_ELEMENT,
+        16
+      );
+      gl.vertexAttribPointer(targetSizeLocation,
+        1,
+        gl.FLOAT,
+        false,
+        this.ATTRIBUTES * Float32Array.BYTES_PER_ELEMENT,
+        20
+      );
+      gl.vertexAttribPointer(delayLocation,
+        1,
+        gl.FLOAT,
+        false,
+        this.ATTRIBUTES * Float32Array.BYTES_PER_ELEMENT,
+        24
+      );
+      gl.vertexAttribPointer(minusLocation,
+        1,
+        gl.FLOAT,
+        false,
+        this.ATTRIBUTES * Float32Array.BYTES_PER_ELEMENT,
+        28
+      );
+      gl.vertexAttribPointer(headLocation,
+        1,
+        gl.FLOAT,
+        false,
+        this.ATTRIBUTES * Float32Array.BYTES_PER_ELEMENT,
+        32
+      );
+      gl.vertexAttribPointer(headPositionLocation,
+        1,
+        gl.FLOAT,
+        false,
+        this.ATTRIBUTES * Float32Array.BYTES_PER_ELEMENT,
+        36
+      );
+      gl.vertexAttribPointer(colorLocation,
+        1,
+        gl.FLOAT,
+        false,
+        this.ATTRIBUTES * Float32Array.BYTES_PER_ELEMENT,
+        40
+      );
+
+      gl.drawArrays(
+        gl.TRIANGLES,
+        params.start || 0,
+        params.count || (data.length / this.ATTRIBUTES)
+      );
+    },
+    initProgram: function(gl) {
+      var vertexShader,
+          fragmentShader,
+          program;
+
+      vertexShader = sigma.utils.loadShader(
+        gl,
+        [
+          'attribute vec2 a_pos1;',
+          'attribute vec2 a_pos2;',
+          'attribute float a_thickness;',
+          'attribute float a_tSize;',
+          'attribute float a_delay;',
+          'attribute float a_minus;',
+          'attribute float a_head;',
+          'attribute float a_headPosition;',
+          'attribute float a_color;',
+
+          'uniform vec2 u_resolution;',
+          'uniform float u_ratio;',
+          'uniform float u_nodeRatio;',
+          'uniform float u_arrowHead;',
+          'uniform float u_scale;',
+          'uniform mat3 u_matrix;',
+          'uniform mat2 u_matrixHalfPi;',
+          'uniform mat2 u_matrixHalfPiMinus;',
+
+          'varying vec4 color;',
+
+          'void main() {',
+            // Find the good point:
+            'vec2 pos = normalize(a_pos2 - a_pos1);',
+
+            'mat2 matrix = (1.0 - a_head) *',
+              '(',
+                'a_minus * u_matrixHalfPiMinus +',
+                '(1.0 - a_minus) * u_matrixHalfPi',
+              ') + a_head * (',
+                'a_headPosition * u_matrixHalfPiMinus * 0.6 +',
+                '(a_headPosition * a_headPosition - 1.0) * mat2(1.0)',
+              ');',
+
+            'pos = a_pos1 + (',
+              // Deal with body:
+              '(1.0 - a_head) * a_thickness * u_ratio * matrix * pos +',
+              // Deal with head:
+              'a_head * u_arrowHead * a_thickness * u_ratio * matrix * pos +',
+              // Deal with delay:
+              'a_delay * pos * (',
+                'a_tSize / u_nodeRatio +',
+                'u_arrowHead * a_thickness * u_ratio',
+              ')',
+            ');',
+
+            // Scale from [[-1 1] [-1 1]] to the container:
+            'gl_Position = vec4(',
+              '((u_matrix * vec3(pos, 1)).xy /',
+                'u_resolution * 2.0 - 1.0) * vec2(1, -1),',
+              '0,',
+              '1',
+            ');',
+
+            // Extract the color:
+            'float c = a_color;',
+            'color.b = mod(c, 256.0); c = floor(c / 256.0);',
+            'color.g = mod(c, 256.0); c = floor(c / 256.0);',
+            'color.r = mod(c, 256.0); c = floor(c / 256.0); color /= 255.0;',
+            'color.a = 1.0;',
+          '}'
+        ].join('\n'),
+        gl.VERTEX_SHADER
+      );
+
+      fragmentShader = sigma.utils.loadShader(
+        gl,
+        [
+          'precision mediump float;',
+
+          'varying vec4 color;',
+
+          'void main(void) {',
+            'gl_FragColor = color;',
+          '}'
+        ].join('\n'),
+        gl.FRAGMENT_SHADER
+      );
+
+      program = sigma.utils.loadProgram(gl, [vertexShader, fragmentShader]);
+
+      return program;
+    }
+  };
+})();
diff --git a/demo/static/sigmajs/renderers/webgl/sigma.webgl.edges.def.js b/demo/static/sigmajs/renderers/webgl/sigma.webgl.edges.def.js
new file mode 100644
index 000000000..6931bb586
--- /dev/null
+++ b/demo/static/sigmajs/renderers/webgl/sigma.webgl.edges.def.js
@@ -0,0 +1,258 @@
+;(function() {
+  'use strict';
+
+  sigma.utils.pkg('sigma.webgl.edges');
+
+  /**
+   * This edge renderer will display edges as lines going from the source node
+   * to the target node. To deal with edge thicknesses, the lines are made of
+   * two triangles forming rectangles, with the gl.TRIANGLES drawing mode.
+   *
+   * It is expensive, since drawing a single edge requires 6 points, each
+   * having 7 attributes (source position, target position, thickness, color
+   * and a flag indicating which vertice of the rectangle it is).
+   */
+  sigma.webgl.edges.def = {
+    POINTS: 6,
+    ATTRIBUTES: 7,
+    addEdge: function(edge, source, target, data, i, prefix, settings) {
+      var w = (edge[prefix + 'size'] || 1) / 2,
+          x1 = source[prefix + 'x'],
+          y1 = source[prefix + 'y'],
+          x2 = target[prefix + 'x'],
+          y2 = target[prefix + 'y'],
+          color = edge.color;
+
+      if (!color)
+        switch (settings('edgeColor')) {
+          case 'source':
+            color = source.color || settings('defaultNodeColor');
+            break;
+          case 'target':
+            color = target.color || settings('defaultNodeColor');
+            break;
+          default:
+            color = settings('defaultEdgeColor');
+            break;
+        }
+
+      // Normalize color:
+      color = sigma.utils.floatColor(color);
+
+      data[i++] = x1;
+      data[i++] = y1;
+      data[i++] = x2;
+      data[i++] = y2;
+      data[i++] = w;
+      data[i++] = 0.0;
+      data[i++] = color;
+
+      data[i++] = x2;
+      data[i++] = y2;
+      data[i++] = x1;
+      data[i++] = y1;
+      data[i++] = w;
+      data[i++] = 1.0;
+      data[i++] = color;
+
+      data[i++] = x2;
+      data[i++] = y2;
+      data[i++] = x1;
+      data[i++] = y1;
+      data[i++] = w;
+      data[i++] = 0.0;
+      data[i++] = color;
+
+      data[i++] = x2;
+      data[i++] = y2;
+      data[i++] = x1;
+      data[i++] = y1;
+      data[i++] = w;
+      data[i++] = 0.0;
+      data[i++] = color;
+
+      data[i++] = x1;
+      data[i++] = y1;
+      data[i++] = x2;
+      data[i++] = y2;
+      data[i++] = w;
+      data[i++] = 1.0;
+      data[i++] = color;
+
+      data[i++] = x1;
+      data[i++] = y1;
+      data[i++] = x2;
+      data[i++] = y2;
+      data[i++] = w;
+      data[i++] = 0.0;
+      data[i++] = color;
+    },
+    render: function(gl, program, data, params) {
+      var buffer;
+
+      // Define attributes:
+      var colorLocation =
+            gl.getAttribLocation(program, 'a_color'),
+          positionLocation1 =
+            gl.getAttribLocation(program, 'a_position1'),
+          positionLocation2 =
+            gl.getAttribLocation(program, 'a_position2'),
+          thicknessLocation =
+            gl.getAttribLocation(program, 'a_thickness'),
+          minusLocation =
+            gl.getAttribLocation(program, 'a_minus'),
+          resolutionLocation =
+            gl.getUniformLocation(program, 'u_resolution'),
+          matrixLocation =
+            gl.getUniformLocation(program, 'u_matrix'),
+          matrixHalfPiLocation =
+            gl.getUniformLocation(program, 'u_matrixHalfPi'),
+          matrixHalfPiMinusLocation =
+            gl.getUniformLocation(program, 'u_matrixHalfPiMinus'),
+          ratioLocation =
+            gl.getUniformLocation(program, 'u_ratio'),
+          scaleLocation =
+            gl.getUniformLocation(program, 'u_scale');
+
+      buffer = gl.createBuffer();
+      gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
+      gl.bufferData(gl.ARRAY_BUFFER, data, gl.STATIC_DRAW);
+
+      gl.uniform2f(resolutionLocation, params.width, params.height);
+      gl.uniform1f(
+        ratioLocation,
+        params.ratio / Math.pow(params.ratio, params.settings('edgesPowRatio'))
+      );
+      gl.uniform1f(scaleLocation, params.scalingRatio);
+      gl.uniformMatrix3fv(matrixLocation, false, params.matrix);
+      gl.uniformMatrix2fv(
+        matrixHalfPiLocation,
+        false,
+        sigma.utils.matrices.rotation(Math.PI / 2, true)
+      );
+      gl.uniformMatrix2fv(
+        matrixHalfPiMinusLocation,
+        false,
+        sigma.utils.matrices.rotation(-Math.PI / 2, true)
+      );
+
+      gl.enableVertexAttribArray(colorLocation);
+      gl.enableVertexAttribArray(positionLocation1);
+      gl.enableVertexAttribArray(positionLocation2);
+      gl.enableVertexAttribArray(thicknessLocation);
+      gl.enableVertexAttribArray(minusLocation);
+
+      gl.vertexAttribPointer(positionLocation1,
+        2,
+        gl.FLOAT,
+        false,
+        this.ATTRIBUTES * Float32Array.BYTES_PER_ELEMENT,
+        0
+      );
+      gl.vertexAttribPointer(positionLocation2,
+        2,
+        gl.FLOAT,
+        false,
+        this.ATTRIBUTES * Float32Array.BYTES_PER_ELEMENT,
+        8
+      );
+      gl.vertexAttribPointer(thicknessLocation,
+        1,
+        gl.FLOAT,
+        false,
+        this.ATTRIBUTES * Float32Array.BYTES_PER_ELEMENT,
+        16
+      );
+      gl.vertexAttribPointer(minusLocation,
+        1,
+        gl.FLOAT,
+        false,
+        this.ATTRIBUTES * Float32Array.BYTES_PER_ELEMENT,
+        20
+      );
+      gl.vertexAttribPointer(colorLocation,
+        1,
+        gl.FLOAT,
+        false,
+        this.ATTRIBUTES * Float32Array.BYTES_PER_ELEMENT,
+        24
+      );
+
+      gl.drawArrays(
+        gl.TRIANGLES,
+        params.start || 0,
+        params.count || (data.length / this.ATTRIBUTES)
+      );
+    },
+    initProgram: function(gl) {
+      var vertexShader,
+          fragmentShader,
+          program;
+
+      vertexShader = sigma.utils.loadShader(
+        gl,
+        [
+          'attribute vec2 a_position1;',
+          'attribute vec2 a_position2;',
+          'attribute float a_thickness;',
+          'attribute float a_minus;',
+          'attribute float a_color;',
+
+          'uniform vec2 u_resolution;',
+          'uniform float u_ratio;',
+          'uniform float u_scale;',
+          'uniform mat3 u_matrix;',
+          'uniform mat2 u_matrixHalfPi;',
+          'uniform mat2 u_matrixHalfPiMinus;',
+
+          'varying vec4 color;',
+
+          'void main() {',
+            // Find the good point:
+            'vec2 position = a_thickness * u_ratio *',
+              'normalize(a_position2 - a_position1);',
+
+            'mat2 matrix = a_minus * u_matrixHalfPiMinus +',
+              '(1.0 - a_minus) * u_matrixHalfPi;',
+
+            'position = matrix * position + a_position1;',
+
+            // Scale from [[-1 1] [-1 1]] to the container:
+            'gl_Position = vec4(',
+              '((u_matrix * vec3(position, 1)).xy /',
+                'u_resolution * 2.0 - 1.0) * vec2(1, -1),',
+              '0,',
+              '1',
+            ');',
+
+            // Extract the color:
+            'float c = a_color;',
+            'color.b = mod(c, 256.0); c = floor(c / 256.0);',
+            'color.g = mod(c, 256.0); c = floor(c / 256.0);',
+            'color.r = mod(c, 256.0); c = floor(c / 256.0); color /= 255.0;',
+            'color.a = 1.0;',
+          '}'
+        ].join('\n'),
+        gl.VERTEX_SHADER
+      );
+
+      fragmentShader = sigma.utils.loadShader(
+        gl,
+        [
+          'precision mediump float;',
+
+          'varying vec4 color;',
+
+          'void main(void) {',
+            'gl_FragColor = color;',
+          '}'
+        ].join('\n'),
+        gl.FRAGMENT_SHADER
+      );
+
+      program = sigma.utils.loadProgram(gl, [vertexShader, fragmentShader]);
+
+      return program;
+    }
+  };
+})();
diff --git a/demo/static/sigmajs/renderers/webgl/sigma.webgl.edges.fast.js b/demo/static/sigmajs/renderers/webgl/sigma.webgl.edges.fast.js
new file mode 100644
index 000000000..48f56d76a
--- /dev/null
+++ b/demo/static/sigmajs/renderers/webgl/sigma.webgl.edges.fast.js
@@ -0,0 +1,147 @@
+;(function() {
+  'use strict';
+
+  sigma.utils.pkg('sigma.webgl.edges');
+
+  /**
+   * This edge renderer will display edges as lines with the gl.LINES display
+   * mode. Since this mode does not support well thickness, edges are all drawn
+   * with the same thickness (3px), independantly of the edge attributes or the
+   * zooming ratio.
+   */
+  sigma.webgl.edges.fast = {
+    POINTS: 2,
+    ATTRIBUTES: 3,
+    addEdge: function(edge, source, target, data, i, prefix, settings) {
+      var w = (edge[prefix + 'size'] || 1) / 2,
+          x1 = source[prefix + 'x'],
+          y1 = source[prefix + 'y'],
+          x2 = target[prefix + 'x'],
+          y2 = target[prefix + 'y'],
+          color = edge.color;
+
+      if (!color)
+        switch (settings('edgeColor')) {
+          case 'source':
+            color = source.color || settings('defaultNodeColor');
+            break;
+          case 'target':
+            color = target.color || settings('defaultNodeColor');
+            break;
+          default:
+            color = settings('defaultEdgeColor');
+            break;
+        }
+
+      // Normalize color:
+      color = sigma.utils.floatColor(color);
+
+      data[i++] = x1;
+      data[i++] = y1;
+      data[i++] = color;
+
+      data[i++] = x2;
+      data[i++] = y2;
+      data[i++] = color;
+    },
+    render: function(gl, program, data, params) {
+      var buffer;
+
+      // Define attributes:
+      var colorLocation =
+            gl.getAttribLocation(program, 'a_color'),
+          positionLocation =
+            gl.getAttribLocation(program, 'a_position'),
+          resolutionLocation =
+            gl.getUniformLocation(program, 'u_resolution'),
+          matrixLocation =
+            gl.getUniformLocation(program, 'u_matrix');
+
+      buffer = gl.createBuffer();
+      gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
+      gl.bufferData(gl.ARRAY_BUFFER, data, gl.DYNAMIC_DRAW);
+
+      gl.uniform2f(resolutionLocation, params.width, params.height);
+      gl.uniformMatrix3fv(matrixLocation, false, params.matrix);
+
+      gl.enableVertexAttribArray(positionLocation);
+      gl.enableVertexAttribArray(colorLocation);
+
+      gl.vertexAttribPointer(positionLocation,
+        2,
+        gl.FLOAT,
+        false,
+        this.ATTRIBUTES * Float32Array.BYTES_PER_ELEMENT,
+        0
+      );
+      gl.vertexAttribPointer(colorLocation,
+        1,
+        gl.FLOAT,
+        false,
+        this.ATTRIBUTES * Float32Array.BYTES_PER_ELEMENT,
+        8
+      );
+
+      gl.lineWidth(3);
+      gl.drawArrays(
+        gl.LINES,
+        params.start || 0,
+        params.count || (data.length / this.ATTRIBUTES)
+      );
+    },
+    initProgram: function(gl) {
+      var vertexShader,
+          fragmentShader,
+          program;
+
+      vertexShader = sigma.utils.loadShader(
+        gl,
+        [
+          'attribute vec2 a_position;',
+          'attribute float a_color;',
+
+          'uniform vec2 u_resolution;',
+          'uniform mat3 u_matrix;',
+
+          'varying vec4 color;',
+
+          'void main() {',
+            // Scale from [[-1 1] [-1 1]] to the container:
+            'gl_Position = vec4(',
+              '((u_matrix * vec3(a_position, 1)).xy /',
+                'u_resolution * 2.0 - 1.0) * vec2(1, -1),',
+              '0,',
+              '1',
+            ');',
+
+            // Extract the color:
+            'float c = a_color;',
+            'color.b = mod(c, 256.0); c = floor(c / 256.0);',
+            'color.g = mod(c, 256.0); c = floor(c / 256.0);',
+            'color.r = mod(c, 256.0); c = floor(c / 256.0); color /= 255.0;',
+            'color.a = 1.0;',
+          '}'
+        ].join('\n'),
+        gl.VERTEX_SHADER
+      );
+
+      fragmentShader = sigma.utils.loadShader(
+        gl,
+        [
+          'precision mediump float;',
+
+          'varying vec4 color;',
+
+          'void main(void) {',
+            'gl_FragColor = color;',
+          '}'
+        ].join('\n'),
+        gl.FRAGMENT_SHADER
+      );
+
+      program = sigma.utils.loadProgram(gl, [vertexShader, fragmentShader]);
+
+      return program;
+    }
+  };
+})();
diff --git a/demo/static/sigmajs/renderers/webgl/sigma.webgl.nodes.def.js b/demo/static/sigmajs/renderers/webgl/sigma.webgl.nodes.def.js
new file mode 100644
index 000000000..d16971353
--- /dev/null
+++ b/demo/static/sigmajs/renderers/webgl/sigma.webgl.nodes.def.js
@@ -0,0 +1,201 @@
+;(function() {
+  'use strict';
+
+  sigma.utils.pkg('sigma.webgl.nodes');
+
+  /**
+   * This node renderer will display nodes as discs, shaped in triangles with
+   * the gl.TRIANGLES display mode. So, to be more precise, to draw one node,
+   * it will store three times the center of node, with the color and the size,
+   * and an angle indicating which "corner" of the triangle to draw.
+   *
+   * The fragment shader does not deal with anti-aliasing, so make sure that
+   * you deal with it somewhere else in the code (by default, the WebGL
+   * renderer will oversample the rendering through the webglOversamplingRatio
+   * value).
+   */
+  sigma.webgl.nodes.def = {
+    POINTS: 3,
+    ATTRIBUTES: 5,
+    addNode: function(node, data, i, prefix, settings) {
+      var color = sigma.utils.floatColor(
+        node.color || settings('defaultNodeColor')
+      );
+
+      data[i++] = node[prefix + 'x'];
+      data[i++] = node[prefix + 'y'];
+      data[i++] = node[prefix + 'size'];
+      data[i++] = color;
+      data[i++] = 0;
+
+      data[i++] = node[prefix + 'x'];
+      data[i++] = node[prefix + 'y'];
+      data[i++] = node[prefix + 'size'];
+      data[i++] = color;
+      data[i++] = 2 * Math.PI / 3;
+
+      data[i++] = node[prefix + 'x'];
+      data[i++] = node[prefix + 'y'];
+      data[i++] = node[prefix + 'size'];
+      data[i++] = color;
+      data[i++] = 4 * Math.PI / 3;
+    },
+    render: function(gl, program, data, params) {
+      var buffer;
+
+      // Define attributes:
+      var positionLocation =
+            gl.getAttribLocation(program, 'a_position'),
+          sizeLocation =
+            gl.getAttribLocation(program, 'a_size'),
+          colorLocation =
+            gl.getAttribLocation(program, 'a_color'),
+          angleLocation =
+            gl.getAttribLocation(program, 'a_angle'),
+          resolutionLocation =
+            gl.getUniformLocation(program, 'u_resolution'),
+          matrixLocation =
+            gl.getUniformLocation(program, 'u_matrix'),
+          ratioLocation =
+            gl.getUniformLocation(program, 'u_ratio'),
+          scaleLocation =
+            gl.getUniformLocation(program, 'u_scale');
+
+      buffer = gl.createBuffer();
+      gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
+      gl.bufferData(gl.ARRAY_BUFFER, data, gl.DYNAMIC_DRAW);
+
+      gl.uniform2f(resolutionLocation, params.width, params.height);
+      gl.uniform1f(
+        ratioLocation,
+        1 / Math.pow(params.ratio, params.settings('nodesPowRatio'))
+      );
+      gl.uniform1f(scaleLocation, params.scalingRatio);
+      gl.uniformMatrix3fv(matrixLocation, false, params.matrix);
+
+      gl.enableVertexAttribArray(positionLocation);
+      gl.enableVertexAttribArray(sizeLocation);
+      gl.enableVertexAttribArray(colorLocation);
+      gl.enableVertexAttribArray(angleLocation);
+
+      gl.vertexAttribPointer(
+        positionLocation,
+        2,
+        gl.FLOAT,
+        false,
+        this.ATTRIBUTES * Float32Array.BYTES_PER_ELEMENT,
+        0
+      );
+      gl.vertexAttribPointer(
+        sizeLocation,
+        1,
+        gl.FLOAT,
+        false,
+        this.ATTRIBUTES * Float32Array.BYTES_PER_ELEMENT,
+        8
+      );
+      gl.vertexAttribPointer(
+        colorLocation,
+        1,
+        gl.FLOAT,
+        false,
+        this.ATTRIBUTES * Float32Array.BYTES_PER_ELEMENT,
+        12
+      );
+      gl.vertexAttribPointer(
+        angleLocation,
+        1,
+        gl.FLOAT,
+        false,
+        this.ATTRIBUTES * Float32Array.BYTES_PER_ELEMENT,
+        16
+      );
+
+      gl.drawArrays(
+        gl.TRIANGLES,
+        params.start || 0,
+        params.count || (data.length / this.ATTRIBUTES)
+      );
+    },
+    initProgram: function(gl) {
+      var vertexShader,
+          fragmentShader,
+          program;
+
+      vertexShader = sigma.utils.loadShader(
+        gl,
+        [
+          'attribute vec2 a_position;',
+          'attribute float a_size;',
+          'attribute float a_color;',
+          'attribute float a_angle;',
+
+          'uniform vec2 u_resolution;',
+          'uniform float u_ratio;',
+          'uniform float u_scale;',
+          'uniform mat3 u_matrix;',
+
+          'varying vec4 color;',
+          'varying vec2 center;',
+          'varying float radius;',
+
+          'void main() {',
+            // Multiply the point size twice:
+            'radius = a_size * u_ratio;',
+
+            // Scale from [[-1 1] [-1 1]] to the container:
+            'vec2 position = (u_matrix * vec3(a_position, 1)).xy;',
+            // 'center = (position / u_resolution * 2.0 - 1.0) * vec2(1, -1);',
+            'center = position * u_scale;',
+            'center = vec2(center.x, u_scale * u_resolution.y - center.y);',
+
+            'position = position +',
+              '2.0 * radius * vec2(cos(a_angle), sin(a_angle));',
+            'position = (position / u_resolution * 2.0 - 1.0) * vec2(1, -1);',
+
+            'radius = radius * u_scale;',
+
+            'gl_Position = vec4(position, 0, 1);',
+
+            // Extract the color:
+            'float c = a_color;',
+            'color.b = mod(c, 256.0); c = floor(c / 256.0);',
+            'color.g = mod(c, 256.0); c = floor(c / 256.0);',
+            'color.r = mod(c, 256.0); c = floor(c / 256.0); color /= 255.0;',
+            'color.a = 1.0;',
+          '}'
+        ].join('\n'),
+        gl.VERTEX_SHADER
+      );
+
+      fragmentShader = sigma.utils.loadShader(
+        gl,
+        [
+          'precision mediump float;',
+
+          'varying vec4 color;',
+          'varying vec2 center;',
+          'varying float radius;',
+
+          'void main(void) {',
+            'vec4 color0 = vec4(0.0, 0.0, 0.0, 0.0);',
+
+            'vec2 m = gl_FragCoord.xy - center;',
+            'float diff = radius - sqrt(m.x * m.x + m.y * m.y);',
+
+            // Here is how we draw a disc instead of a square:
+            'if (diff > 0.0)',
+              'gl_FragColor = color;',
+            'else',
+              'gl_FragColor = color0;',
+          '}'
+        ].join('\n'),
+        gl.FRAGMENT_SHADER
+      );
+
+      program = sigma.utils.loadProgram(gl, [vertexShader, fragmentShader]);
+
+      return program;
+    }
+  };
+})();
diff --git a/demo/static/sigmajs/renderers/webgl/sigma.webgl.nodes.fast.js b/demo/static/sigmajs/renderers/webgl/sigma.webgl.nodes.fast.js
new file mode 100644
index 000000000..3769fb698
--- /dev/null
+++ b/demo/static/sigmajs/renderers/webgl/sigma.webgl.nodes.fast.js
@@ -0,0 +1,163 @@
+;(function() {
+  'use strict';
+
+  sigma.utils.pkg('sigma.webgl.nodes');
+
+  /**
+   * This node renderer will display nodes in the fastest way: Nodes are basic
+   * squares, drawn through the gl.POINTS drawing method. The size of the nodes
+   * are represented with the "gl_PointSize" value in the vertex shader.
+   *
+   * It is the fastest node renderer here since the buffer just takes one line
+   * to draw each node (with attributes "x", "y", "size" and "color").
+   *
+   * Nevertheless, this method has some problems, especially due to some issues
+   * with the gl.POINTS:
+   *  - First, if the center of a node is outside the scene, the point will not
+   *    be drawn, even if it should be partly on screen.
+   *  - I tried applying a fragment shader similar to the one in the default
+   *    node renderer to display them as discs, but it did not work fine on
+   *    some computers settings, filling the discs with weird gradients not
+   *    depending on the actual color.
+   */
+  sigma.webgl.nodes.fast = {
+    POINTS: 1,
+    ATTRIBUTES: 4,
+    addNode: function(node, data, i, prefix, settings) {
+      data[i++] = node[prefix + 'x'];
+      data[i++] = node[prefix + 'y'];
+      data[i++] = node[prefix + 'size'];
+      data[i++] = sigma.utils.floatColor(
+        node.color || settings('defaultNodeColor')
+      );
+    },
+    render: function(gl, program, data, params) {
+      var buffer;
+
+      // Define attributes:
+      var positionLocation =
+            gl.getAttribLocation(program, 'a_position'),
+          sizeLocation =
+            gl.getAttribLocation(program, 'a_size'),
+          colorLocation =
+            gl.getAttribLocation(program, 'a_color'),
+          resolutionLocation =
+            gl.getUniformLocation(program, 'u_resolution'),
+          matrixLocation =
+            gl.getUniformLocation(program, 'u_matrix'),
+          ratioLocation =
+            gl.getUniformLocation(program, 'u_ratio'),
+          scaleLocation =
+            gl.getUniformLocation(program, 'u_scale');
+
+      buffer = gl.createBuffer();
+      gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
+      gl.bufferData(gl.ARRAY_BUFFER, data, gl.DYNAMIC_DRAW);
+
+      gl.uniform2f(resolutionLocation, params.width, params.height);
+      gl.uniform1f(
+        ratioLocation,
+        1 / Math.pow(params.ratio, params.settings('nodesPowRatio'))
+      );
+      gl.uniform1f(scaleLocation, params.scalingRatio);
+      gl.uniformMatrix3fv(matrixLocation, false, params.matrix);
+
+      gl.enableVertexAttribArray(positionLocation);
+      gl.enableVertexAttribArray(sizeLocation);
+      gl.enableVertexAttribArray(colorLocation);
+
+      gl.vertexAttribPointer(
+        positionLocation,
+        2,
+        gl.FLOAT,
+        false,
+        this.ATTRIBUTES * Float32Array.BYTES_PER_ELEMENT,
+        0
+      );
+      gl.vertexAttribPointer(
+        sizeLocation,
+        1,
+        gl.FLOAT,
+        false,
+        this.ATTRIBUTES * Float32Array.BYTES_PER_ELEMENT,
+        8
+      );
+      gl.vertexAttribPointer(
+        colorLocation,
+        1,
+        gl.FLOAT,
+        false,
+        this.ATTRIBUTES * Float32Array.BYTES_PER_ELEMENT,
+        12
+      );
+
+      gl.drawArrays(
+        gl.POINTS,
+        params.start || 0,
+        params.count || (data.length / this.ATTRIBUTES)
+      );
+    },
+    initProgram: function(gl) {
+      var vertexShader,
+          fragmentShader,
+          program;
+
+      vertexShader = sigma.utils.loadShader(
+        gl,
+        [
+          'attribute vec2 a_position;',
+          'attribute float a_size;',
+          'attribute float a_color;',
+
+          'uniform vec2 u_resolution;',
+          'uniform float u_ratio;',
+          'uniform float u_scale;',
+          'uniform mat3 u_matrix;',
+
+          'varying vec4 color;',
+
+          'void main() {',
+            // Scale from [[-1 1] [-1 1]] to the container:
+            'gl_Position = vec4(',
+              '((u_matrix * vec3(a_position, 1)).xy /',
+                'u_resolution * 2.0 - 1.0) * vec2(1, -1),',
+              '0,',
+              '1',
+            ');',
+
+            // Multiply the point size twice:
+            //  - x SCALING_RATIO to correct the canvas scaling
+            //  - x 2 to correct the formulae
+            'gl_PointSize = a_size * u_ratio * u_scale * 2.0;',
+
+            // Extract the color:
+            'float c = a_color;',
+            'color.b = mod(c, 256.0); c = floor(c / 256.0);',
+            'color.g = mod(c, 256.0); c = floor(c / 256.0);',
+            'color.r = mod(c, 256.0); c = floor(c / 256.0); color /= 255.0;',
+            'color.a = 1.0;',
+          '}'
+        ].join('\n'),
+        gl.VERTEX_SHADER
+      );
+
+      fragmentShader = sigma.utils.loadShader(
+        gl,
+        [
+          'precision mediump float;',
+
+          'varying vec4 color;',
+
+          'void main(void) {',
+            'gl_FragColor = color;',
+          '}'
+        ].join('\n'),
+        gl.FRAGMENT_SHADER
+      );
+
+      program = sigma.utils.loadProgram(gl, [vertexShader, fragmentShader]);
+
+      return program;
+    }
+  };
+})();
diff --git a/demo/static/sigmajs/sigma.core.js b/demo/static/sigmajs/sigma.core.js
new file mode 100644
index 000000000..4b77def21
--- /dev/null
+++ b/demo/static/sigmajs/sigma.core.js
@@ -0,0 +1,739 @@
+;(function(undefined) {
+  'use strict';
+
+  var __instances = {};
+
+  /**
+   * This is the sigma instances constructor. One instance of sigma represent
+   * one graph. It is possible to represent this grapĥ with several renderers
+   * at the same time. By default, the default renderer (WebGL + Canvas
+   * polyfill) will be used as the only renderer, with the container specified
+   * in the configuration.
+   *
+   * @param  {?*}    conf The configuration of the instance. There are a lot of
+   *                      different recognized forms to instantiate sigma, check
+   *                      example files, documentation in this file and unit
+   *                      tests to know more.
+   * @return {sigma}      The fresh new sigma instance.
+   *
+   * Instanciating sigma:
+   * ********************
+   * If no parameter is given to the constructor, the instance will be created
+   * without any renderer or camera. It will just instantiate the graph, and
+   * other modules will have to be instantiated through the public methods,
+   * like "addRenderer" etc:
+   *
+   *  > s0 = new sigma();
+   *  > s0.addRenderer({
+   *  >   type: 'canvas',
+   *  >   container: 'my-container-id'
+   *  > });
+   *
+   * In most of the cases, sigma will simply be used with the default renderer.
+   * Then, since the only required parameter is the DOM container, there are
+   * some simpler way to call the constructor. The four following calls do the
+   * exact same things:
+   *
+   *  > s1 = new sigma('my-container-id');
+   *  > s2 = new sigma(document.getElementById('my-container-id'));
+   *  > s3 = new sigma({
+   *  >   container: document.getElementById('my-container-id')
+   *  > });
+   *  > s4 = new sigma({
+   *  >   renderers: [{
+   *  >     container: document.getElementById('my-container-id')
+   *  >   }]
+   *  > });
+   *
+   * Recognized parameters:
+   * **********************
+   * Here is the exhaustive list of every accepted parameters, when calling the
+   * constructor with to top level configuration object (fourth case in the
+   * previous examples):
+   *
+   *   {?string} id        The id of the instance. It will be generated
+   *                       automatically if not specified.
+   *   {?array}  renderers An array containing objects describing renderers.
+   *   {?object} graph     An object containing an array of nodes and an array
+   *                       of edges, to avoid having to add them by hand later.
+   *   {?object} settings  An object containing instance specific settings that
+   *                       will override the default ones defined in the object
+   *                       sigma.settings.
+   */
+  var sigma = function(conf) {
+    // Local variables:
+    // ****************
+    var i,
+        l,
+        a,
+        c,
+        o,
+        id;
+
+    sigma.classes.dispatcher.extend(this);
+
+    // Private attributes:
+    // *******************
+    var _self = this,
+        _conf = conf || {};
+
+    // Little shortcut:
+    // ****************
+    // The configuration is supposed to have a list of the configuration
+    // objects for each renderer.
+    //  - If there are no configuration at all, then nothing is done.
+    //  - If there are no renderer list, the given configuration object will be
+    //    considered as describing the first and only renderer.
+    //  - If there are no renderer list nor "container" object, it will be
+    //    considered as the container itself (a DOM element).
+    //  - If the argument passed to sigma() is a string, it will be considered
+    //    as the ID of the DOM container.
+    if (
+      typeof _conf === 'string' ||
+      _conf instanceof HTMLElement
+    )
+      _conf = {
+        renderers: [_conf]
+      };
+    else if (Object.prototype.toString.call(_conf) === '[object Array]')
+      _conf = {
+        renderers: _conf
+      };
+
+    // Also check "renderer" and "container" keys:
+    o = _conf.renderers || _conf.renderer || _conf.container;
+    if (!_conf.renderers || _conf.renderers.length === 0)
+      if (
+        typeof o === 'string' ||
+        o instanceof HTMLElement ||
+        (typeof o === 'object' && 'container' in o)
+      )
+        _conf.renderers = [o];
+
+    // Recense the instance:
+    if (_conf.id) {
+      if (__instances[_conf.id])
+        throw 'sigma: Instance "' + _conf.id + '" already exists.';
+      Object.defineProperty(this, 'id', {
+        value: _conf.id
+      });
+    } else {
+      id = 0;
+      while (__instances[id])
+        id++;
+      Object.defineProperty(this, 'id', {
+        value: '' + id
+      });
+    }
+    __instances[this.id] = this;
+
+    // Initialize settings function:
+    this.settings = new sigma.classes.configurable(
+      sigma.settings,
+      _conf.settings || {}
+    );
+
+    // Initialize locked attributes:
+    Object.defineProperty(this, 'graph', {
+      value: new sigma.classes.graph(this.settings),
+      configurable: true
+    });
+    Object.defineProperty(this, 'middlewares', {
+      value: [],
+      configurable: true
+    });
+    Object.defineProperty(this, 'cameras', {
+      value: {},
+      configurable: true
+    });
+    Object.defineProperty(this, 'renderers', {
+      value: {},
+      configurable: true
+    });
+    Object.defineProperty(this, 'renderersPerCamera', {
+      value: {},
+      configurable: true
+    });
+    Object.defineProperty(this, 'cameraFrames', {
+      value: {},
+      configurable: true
+    });
+    Object.defineProperty(this, 'camera', {
+      get: function() {
+        return this.cameras[0];
+      }
+    });
+    Object.defineProperty(this, 'events', {
+      value: [
+        'click',
+        'rightClick',
+        'clickStage',
+        'doubleClickStage',
+        'rightClickStage',
+        'clickNode',
+        'clickNodes',
+        'doubleClickNode',
+        'doubleClickNodes',
+        'rightClickNode',
+        'rightClickNodes',
+        'overNode',
+        'overNodes',
+        'outNode',
+        'outNodes',
+        'downNode',
+        'downNodes',
+        'upNode',
+        'upNodes'
+      ],
+      configurable: true
+    });
+
+    // Add a custom handler, to redispatch events from renderers:
+    this._handler = (function(e) {
+      var k,
+          data = {};
+
+      for (k in e.data)
+        data[k] = e.data[k];
+
+      data.renderer = e.target;
+      this.dispatchEvent(e.type, data);
+    }).bind(this);
+
+    // Initialize renderers:
+    a = _conf.renderers || [];
+    for (i = 0, l = a.length; i < l; i++)
+      this.addRenderer(a[i]);
+
+    // Initialize middlewares:
+    a = _conf.middlewares || [];
+    for (i = 0, l = a.length; i < l; i++)
+      this.middlewares.push(
+        typeof a[i] === 'string' ?
+          sigma.middlewares[a[i]] :
+          a[i]
+      );
+
+    // Check if there is already a graph to fill in:
+    if (typeof _conf.graph === 'object' && _conf.graph) {
+      this.graph.read(_conf.graph);
+
+      // If a graph is given to the to the instance, the "refresh" method is
+      // directly called:
+      this.refresh();
+    }
+
+    // Deal with resize:
+    window.addEventListener('resize', function() {
+      if (_self.settings)
+        _self.refresh();
+    });
+  };
+
+
+
+
+  /**
+   * This methods will instantiate and reference a new camera. If no id is
+   * specified, then an automatic id will be generated.
+   *
+   * @param  {?string}              id Eventually the camera id.
+   * @return {sigma.classes.camera}    The fresh new camera instance.
+   */
+  sigma.prototype.addCamera = function(id) {
+    var self = this,
+        camera;
+
+    if (!arguments.length) {
+      id = 0;
+      while (this.cameras['' + id])
+        id++;
+      id = '' + id;
+    }
+
+    if (this.cameras[id])
+      throw 'sigma.addCamera: The camera "' + id + '" already exists.';
+
+    camera = new sigma.classes.camera(id, this.graph, this.settings);
+    this.cameras[id] = camera;
+
+    // Add a quadtree to the camera:
+    camera.quadtree = new sigma.classes.quad();
+
+    // Add an edgequadtree to the camera:
+    if (sigma.classes.edgequad !== undefined) {
+      camera.edgequadtree = new sigma.classes.edgequad();
+    }
+
+    camera.bind('coordinatesUpdated', function(e) {
+      self.renderCamera(camera, camera.isAnimated);
+    });
+
+    this.renderersPerCamera[id] = [];
+
+    return camera;
+  };
+
+  /**
+   * This method kills a camera, and every renderer attached to it.
+   *
+   * @param  {string|camera} v The camera to kill or its ID.
+   * @return {sigma}           Returns the instance.
+   */
+  sigma.prototype.killCamera = function(v) {
+    v = typeof v === 'string' ? this.cameras[v] : v;
+
+    if (!v)
+      throw 'sigma.killCamera: The camera is undefined.';
+
+    var i,
+        l,
+        a = this.renderersPerCamera[v.id];
+
+    for (l = a.length, i = l - 1; i >= 0; i--)
+      this.killRenderer(a[i]);
+
+    delete this.renderersPerCamera[v.id];
+    delete this.cameraFrames[v.id];
+    delete this.cameras[v.id];
+
+    if (v.kill)
+      v.kill();
+
+    return this;
+  };
+
+  /**
+   * This methods will instantiate and reference a new renderer. The "type"
+   * argument can be the constructor or its name in the "sigma.renderers"
+   * package. If no type is specified, then "sigma.renderers.def" will be used.
+   * If no id is specified, then an automatic id will be generated.
+   *
+   * @param  {?object}  options Eventually some options to give to the renderer
+   *                            constructor.
+   * @return {renderer}         The fresh new renderer instance.
+   *
+   * Recognized parameters:
+   * **********************
+   * Here is the exhaustive list of every accepted parameters in the "options"
+   * object:
+   *
+   *   {?string}            id     Eventually the renderer id.
+   *   {?(function|string)} type   Eventually the renderer constructor or its
+   *                               name in the "sigma.renderers" package.
+   *   {?(camera|string)}   camera Eventually the renderer camera or its
+   *                               id.
+   */
+  sigma.prototype.addRenderer = function(options) {
+    var id,
+        fn,
+        camera,
+        renderer,
+        o = options || {};
+
+    // Polymorphism:
+    if (typeof o === 'string')
+      o = {
+        container: document.getElementById(o)
+      };
+    else if (o instanceof HTMLElement)
+      o = {
+        container: o
+      };
+
+    // If the container still is a string, we get it by id
+    if (typeof o.container === 'string')
+      o.container = document.getElementById(o.container);
+
+    // Reference the new renderer:
+    if (!('id' in o)) {
+      id = 0;
+      while (this.renderers['' + id])
+        id++;
+      id = '' + id;
+    } else
+      id = o.id;
+
+    if (this.renderers[id])
+      throw 'sigma.addRenderer: The renderer "' + id + '" already exists.';
+
+    // Find the good constructor:
+    fn = typeof o.type === 'function' ? o.type : sigma.renderers[o.type];
+    fn = fn || sigma.renderers.def;
+
+    // Find the good camera:
+    camera = 'camera' in o ?
+      (
+        o.camera instanceof sigma.classes.camera ?
+          o.camera :
+          this.cameras[o.camera] || this.addCamera(o.camera)
+      ) :
+      this.addCamera();
+
+    if (this.cameras[camera.id] !== camera)
+      throw 'sigma.addRenderer: The camera is not properly referenced.';
+
+    // Instantiate:
+    renderer = new fn(this.graph, camera, this.settings, o);
+    this.renderers[id] = renderer;
+    Object.defineProperty(renderer, 'id', {
+      value: id
+    });
+
+    // Bind events:
+    if (renderer.bind)
+      renderer.bind(
+        [
+          'click',
+          'rightClick',
+          'clickStage',
+          'doubleClickStage',
+          'rightClickStage',
+          'clickNode',
+          'clickNodes',
+          'clickEdge',
+          'clickEdges',
+          'doubleClickNode',
+          'doubleClickNodes',
+          'doubleClickEdge',
+          'doubleClickEdges',
+          'rightClickNode',
+          'rightClickNodes',
+          'rightClickEdge',
+          'rightClickEdges',
+          'overNode',
+          'overNodes',
+          'overEdge',
+          'overEdges',
+          'outNode',
+          'outNodes',
+          'outEdge',
+          'outEdges',
+          'downNode',
+          'downNodes',
+          'downEdge',
+          'downEdges',
+          'upNode',
+          'upNodes',
+          'upEdge',
+          'upEdges'
+        ],
+        this._handler
+      );
+
+    // Reference the renderer by its camera:
+    this.renderersPerCamera[camera.id].push(renderer);
+
+    return renderer;
+  };
+
+  /**
+   * This method kills a renderer.
+   *
+   * @param  {string|renderer} v The renderer to kill or its ID.
+   * @return {sigma}             Returns the instance.
+   */
+  sigma.prototype.killRenderer = function(v) {
+    v = typeof v === 'string' ? this.renderers[v] : v;
+
+    if (!v)
+      throw 'sigma.killRenderer: The renderer is undefined.';
+
+    var a = this.renderersPerCamera[v.camera.id],
+        i = a.indexOf(v);
+
+    if (i >= 0)
+      a.splice(i, 1);
+
+    if (v.kill)
+      v.kill();
+
+    delete this.renderers[v.id];
+
+    return this;
+  };
+
+
+
+
+  /**
+   * This method calls the "render" method of each renderer, with the same
+   * arguments than the "render" method, but will also check if the renderer
+   * has a "process" method, and call it if it exists.
+   *
+   * It is useful for quadtrees or WebGL processing, for instance.
+   *
+   * @param  {?object}  options Eventually some options to give to the refresh
+   *                            method.
+   * @return {sigma}            Returns the instance itself.
+   *
+   * Recognized parameters:
+   * **********************
+   * Here is the exhaustive list of every accepted parameters in the "options"
+   * object:
+   *
+   *   {?boolean} skipIndexation A flag specifying wether or not the refresh
+   *                             function should reindex the graph in the
+   *                             quadtrees or not (default: false).
+   */
+  sigma.prototype.refresh = function(options) {
+    var i,
+        l,
+        k,
+        a,
+        c,
+        bounds,
+        prefix = 0;
+
+    options = options || {};
+
+    // Call each middleware:
+    a = this.middlewares || [];
+    for (i = 0, l = a.length; i < l; i++)
+      a[i].call(
+        this,
+        (i === 0) ? '' : 'tmp' + prefix + ':',
+        (i === l - 1) ? 'ready:' : ('tmp' + (++prefix) + ':')
+      );
+
+    // Then, for each camera, call the "rescale" middleware, unless the
+    // settings specify not to:
+    for (k in this.cameras) {
+      c = this.cameras[k];
+      if (
+        c.settings('autoRescale') &&
+        this.renderersPerCamera[c.id] &&
+        this.renderersPerCamera[c.id].length
+      )
+        sigma.middlewares.rescale.call(
+          this,
+          a.length ? 'ready:' : '',
+          c.readPrefix,
+          {
+            width: this.renderersPerCamera[c.id][0].width,
+            height: this.renderersPerCamera[c.id][0].height
+          }
+        );
+      else
+        sigma.middlewares.copy.call(
+          this,
+          a.length ? 'ready:' : '',
+          c.readPrefix
+        );
+
+      if (!options.skipIndexation) {
+        // Find graph boundaries:
+        bounds = sigma.utils.getBoundaries(
+          this.graph,
+          c.readPrefix
+        );
+
+        // Refresh quadtree:
+        c.quadtree.index(this.graph.nodes(), {
+          prefix: c.readPrefix,
+          bounds: {
+            x: bounds.minX,
+            y: bounds.minY,
+            width: bounds.maxX - bounds.minX,
+            height: bounds.maxY - bounds.minY
+          }
+        });
+
+        // Refresh edgequadtree:
+        if (
+          c.edgequadtree !== undefined &&
+          c.settings('drawEdges') &&
+          c.settings('enableEdgeHovering')
+        ) {
+          c.edgequadtree.index(this.graph, {
+            prefix: c.readPrefix,
+            bounds: {
+              x: bounds.minX,
+              y: bounds.minY,
+              width: bounds.maxX - bounds.minX,
+              height: bounds.maxY - bounds.minY
+            }
+          });
+        }
+      }
+    }
+
+    // Call each renderer:
+    a = Object.keys(this.renderers);
+    for (i = 0, l = a.length; i < l; i++)
+      if (this.renderers[a[i]].process) {
+        if (this.settings('skipErrors'))
+          try {
+            this.renderers[a[i]].process();
+          } catch (e) {
+            console.log(
+              'Warning: The renderer "' + a[i] + '" crashed on ".process()"'
+            );
+          }
+        else
+          this.renderers[a[i]].process();
+      }
+
+    this.render();
+
+    return this;
+  };
+
+  /**
+   * This method calls the "render" method of each renderer.
+   *
+   * @return {sigma} Returns the instance itself.
+   */
+  sigma.prototype.render = function() {
+    var i,
+        l,
+        a,
+        prefix = 0;
+
+    // Call each renderer:
+    a = Object.keys(this.renderers);
+    for (i = 0, l = a.length; i < l; i++)
+      if (this.settings('skipErrors'))
+        try {
+          this.renderers[a[i]].render();
+        } catch (e) {
+          if (this.settings('verbose'))
+            console.log(
+              'Warning: The renderer "' + a[i] + '" crashed on ".render()"'
+            );
+        }
+      else
+        this.renderers[a[i]].render();
+
+    return this;
+  };
+
+  /**
+   * This method calls the "render" method of each renderer that is bound to
+   * the specified camera. To improve the performances, if this method is
+   * called too often, the number of effective renderings is limitated to one
+   * per frame, unless you are using the "force" flag.
+   *
+   * @param  {sigma.classes.camera} camera The camera to render.
+   * @param  {?boolean}             force  If true, will render the camera
+   *                                       directly.
+   * @return {sigma}                       Returns the instance itself.
+   */
+  sigma.prototype.renderCamera = function(camera, force) {
+    var i,
+        l,
+        a,
+        self = this;
+
+    if (force) {
+      a = this.renderersPerCamera[camera.id];
+      for (i = 0, l = a.length; i < l; i++)
+        if (this.settings('skipErrors'))
+          try {
+            a[i].render();
+          } catch (e) {
+            if (this.settings('verbose'))
+              console.log(
+                'Warning: The renderer "' + a[i].id + '" crashed on ".render()"'
+              );
+          }
+        else
+          a[i].render();
+    } else {
+      if (!this.cameraFrames[camera.id]) {
+        a = this.renderersPerCamera[camera.id];
+        for (i = 0, l = a.length; i < l; i++)
+          if (this.settings('skipErrors'))
+            try {
+              a[i].render();
+            } catch (e) {
+              if (this.settings('verbose'))
+                console.log(
+                  'Warning: The renderer "' +
+                    a[i].id +
+                    '" crashed on ".render()"'
+                );
+            }
+          else
+            a[i].render();
+
+        this.cameraFrames[camera.id] = requestAnimationFrame(function() {
+          delete self.cameraFrames[camera.id];
+        });
+      }
+    }
+
+    return this;
+  };
+
+  /**
+   * This method calls the "kill" method of each module and destroys any
+   * reference from the instance.
+   */
+  sigma.prototype.kill = function() {
+    var k;
+
+    // Dispatching event
+    this.dispatchEvent('kill');
+
+    // Kill graph:
+    this.graph.kill();
+
+    // Kill middlewares:
+    delete this.middlewares;
+
+    // Kill each renderer:
+    for (k in this.renderers)
+      this.killRenderer(this.renderers[k]);
+
+    // Kill each camera:
+    for (k in this.cameras)
+      this.killCamera(this.cameras[k]);
+
+    delete this.renderers;
+    delete this.cameras;
+
+    // Kill everything else:
+    for (k in this)
+      if (this.hasOwnProperty(k))
+        delete this[k];
+
+    delete __instances[this.id];
+  };
+
+
+
+
+  /**
+   * Returns a clone of the instances object or a specific running instance.
+   *
+   * @param  {?string} id Eventually an instance ID.
+   * @return {object}     The related instance or a clone of the instances
+   *                      object.
+   */
+  sigma.instances = function(id) {
+    return arguments.length ?
+      __instances[id] :
+      sigma.utils.extend({}, __instances);
+  };
+
+
+
+  /**
+   * The current version of sigma:
+   */
+  sigma.version = '1.0.3';
+
+
+
+
+  /**
+   * EXPORT:
+   * *******
+   */
+  if (typeof this.sigma !== 'undefined')
+    throw 'An object called sigma is already in the global scope.';
+
+  this.sigma = sigma;
+
+}).call(this);
diff --git a/demo/static/sigmajs/sigma.export.js b/demo/static/sigmajs/sigma.export.js
new file mode 100644
index 000000000..ff9e7856a
--- /dev/null
+++ b/demo/static/sigmajs/sigma.export.js
@@ -0,0 +1,20 @@
+// Hardcoded export for the node.js version:
+var sigma = this.sigma,
+    conrad = this.conrad;
+
+sigma.conrad = conrad;
+
+// Dirty polyfills to permit sigma usage in node
+if (HTMLElement === undefined)
+  var HTMLElement = function() {};
+
+if (window === undefined)
+  var window = {
+    addEventListener: function() {}
+  };
+
+if (typeof exports !== 'undefined') {
+  if (typeof module !== 'undefined' && module.exports)
+    exports = module.exports = sigma;
+  exports.sigma = sigma;
+}
diff --git a/demo/static/sigmajs/sigma.settings.js b/demo/static/sigmajs/sigma.settings.js
new file mode 100644
index 000000000..cee34b885
--- /dev/null
+++ b/demo/static/sigmajs/sigma.settings.js
@@ -0,0 +1,251 @@
+;(function(undefined) {
+  'use strict';
+
+  if (typeof sigma === 'undefined')
+    throw 'sigma is not declared';
+
+  // Packages initialization:
+  sigma.utils.pkg('sigma.settings');
+
+  var settings = {
+    /**
+     * GRAPH SETTINGS:
+     * ***************
+     */
+    // {boolean} Indicates if the data have to be cloned in methods to add
+    //           nodes or edges.
+    clone: true,
+    // {boolean} Indicates if nodes "id" values and edges "id", "source" and
+    //           "target" values must be set as immutable.
+    immutable: true,
+    // {boolean} Indicates if sigma can log its errors and warnings.
+    verbose: false,
+
+
+    /**
+     * RENDERERS SETTINGS:
+     * *******************
+     */
+    // {string}
+    classPrefix: 'sigma',
+    // {string}
+    defaultNodeType: 'def',
+    // {string}
+    defaultEdgeType: 'def',
+    // {string}
+    defaultLabelColor: '#000',
+    // {string}
+    defaultEdgeColor: '#000',
+    // {string}
+    defaultNodeColor: '#000',
+    // {string}
+    defaultLabelSize: 14,
+    // {string} Indicates how to choose the edges color. Available values:
+    //          "source", "target", "default"
+    edgeColor: 'source',
+    // {number} Defines the minimal edge's arrow display size.
+    minArrowSize: 0,
+    // {string}
+    font: 'arial',
+    // {string} Example: 'bold'
+    fontStyle: '',
+    // {string} Indicates how to choose the labels color. Available values:
+    //          "node", "default"
+    labelColor: 'default',
+    // {string} Indicates how to choose the labels size. Available values:
+    //          "fixed", "proportional"
+    labelSize: 'fixed',
+    // {string} The ratio between the font size of the label and the node size.
+    labelSizeRatio: 1,
+    // {number} The minimum size a node must have to see its label displayed.
+    labelThreshold: 8,
+    // {number} The oversampling factor used in WebGL renderer.
+    webglOversamplingRatio: 2,
+    // {number} The size of the border of hovered nodes.
+    borderSize: 0,
+    // {number} The default hovered node border's color.
+    defaultNodeBorderColor: '#000',
+    // {number} The hovered node's label font. If not specified, will heritate
+    //          the "font" value.
+    hoverFont: '',
+    // {boolean} If true, then only one node can be hovered at a time.
+    singleHover: true,
+    // {string} Example: 'bold'
+    hoverFontStyle: '',
+    // {string} Indicates how to choose the hovered nodes shadow color.
+    //          Available values: "node", "default"
+    labelHoverShadow: 'default',
+    // {string}
+    labelHoverShadowColor: '#000',
+    // {string} Indicates how to choose the hovered nodes color.
+    //          Available values: "node", "default"
+    nodeHoverColor: 'node',
+    // {string}
+    defaultNodeHoverColor: '#000',
+    // {string} Indicates how to choose the hovered nodes background color.
+    //          Available values: "node", "default"
+    labelHoverBGColor: 'default',
+    // {string}
+    defaultHoverLabelBGColor: '#fff',
+    // {string} Indicates how to choose the hovered labels color.
+    //          Available values: "node", "default"
+    labelHoverColor: 'default',
+    // {string}
+    defaultLabelHoverColor: '#000',
+    // {string} Indicates how to choose the edges hover color. Available values:
+    //          "edge", "default"
+    edgeHoverColor: 'edge',
+    // {number} The size multiplicator of hovered edges.
+    edgeHoverSizeRatio: 1,
+    // {string}
+    defaultEdgeHoverColor: '#000',
+    // {boolean} Indicates if the edge extremities must be hovered when the
+    //           edge is hovered.
+    edgeHoverExtremities: false,
+    // {booleans} The different drawing modes:
+    //           false: Layered not displayed.
+    //           true: Layered displayed.
+    drawEdges: true,
+    drawNodes: true,
+    drawLabels: true,
+    drawEdgeLabels: false,
+    // {boolean} Indicates if the edges must be drawn in several frames or in
+    //           one frame, as the nodes and labels are drawn.
+    batchEdgesDrawing: false,
+    // {boolean} Indicates if the edges must be hidden during dragging and
+    //           animations.
+    hideEdgesOnMove: false,
+    // {numbers} The different batch sizes, when elements are displayed in
+    //           several frames.
+    canvasEdgesBatchSize: 500,
+    webglEdgesBatchSize: 1000,
+
+
+
+
+    /**
+     * RESCALE SETTINGS:
+     * *****************
+     */
+    // {string} Indicates of to scale the graph relatively to its container.
+    //          Available values: "inside", "outside"
+    scalingMode: 'inside',
+    // {number} The margin to keep around the graph.
+    sideMargin: 0,
+    // {number} Determine the size of the smallest and the biggest node / edges
+    //          on the screen. This mapping makes easier to display the graph,
+    //          avoiding too big nodes that take half of the screen, or too
+    //          small ones that are not readable. If the two parameters are
+    //          equals, then the minimal display size will be 0. And if they
+    //          are both equal to 0, then there is no mapping, and the radius
+    //          of the nodes will be their size.
+    minEdgeSize: 0.5,
+    maxEdgeSize: 1,
+    minNodeSize: 1,
+    maxNodeSize: 8,
+
+
+
+
+    /**
+     * CAPTORS SETTINGS:
+     * *****************
+     */
+    // {boolean}
+    touchEnabled: true,
+    // {boolean}
+    mouseEnabled: true,
+    // {boolean}
+    mouseWheelEnabled: true,
+    // {boolean}
+    doubleClickEnabled: true,
+    // {boolean} Defines whether the custom events such as "clickNode" can be
+    //           used.
+    eventsEnabled: true,
+    // {number} Defines by how much multiplicating the zooming level when the
+    //          user zooms with the mouse-wheel.
+    zoomingRatio: 1.7,
+    // {number} Defines by how much multiplicating the zooming level when the
+    //          user zooms by double clicking.
+    doubleClickZoomingRatio: 2.2,
+    // {number} The minimum zooming level.
+    zoomMin: 0.0625,
+    // {number} The maximum zooming level.
+    zoomMax: 2,
+    // {number} The duration of animations following a mouse scrolling.
+    mouseZoomDuration: 200,
+    // {number} The duration of animations following a mouse double click.
+    doubleClickZoomDuration: 200,
+    // {number} The duration of animations following a mouse dropping.
+    mouseInertiaDuration: 200,
+    // {number} The inertia power (mouse captor).
+    mouseInertiaRatio: 3,
+    // {number} The duration of animations following a touch dropping.
+    touchInertiaDuration: 200,
+    // {number} The inertia power (touch captor).
+    touchInertiaRatio: 3,
+    // {number} The maximum time between two clicks to make it a double click.
+    doubleClickTimeout: 300,
+    // {number} The maximum time between two taps to make it a double tap.
+    doubleTapTimeout: 300,
+    // {number} The maximum time of dragging to trigger intertia.
+    dragTimeout: 200,
+
+
+
+
+    /**
+     * GLOBAL SETTINGS:
+     * ****************
+     */
+    // {boolean} Determines whether the instance has to refresh itself
+    //           automatically when a "resize" event is dispatched from the
+    //           window object.
+    autoResize: true,
+    // {boolean} Determines whether the "rescale" middleware has to be called
+    //           automatically for each camera on refresh.
+    autoRescale: true,
+    // {boolean} If set to false, the camera method "goTo" will basically do
+    //           nothing.
+    enableCamera: true,
+    // {boolean} If set to false, the nodes cannot be hovered.
+    enableHovering: true,
+    // {boolean} If set to true, the edges can be hovered.
+    enableEdgeHovering: false,
+    // {number} The size of the area around the edges to activate hovering.
+    edgeHoverPrecision: 5,
+    // {boolean} If set to true, the rescale middleware will ignore node sizes
+    //           to determine the graphs boundings.
+    rescaleIgnoreSize: false,
+    // {boolean} Determines if the core has to try to catch errors on
+    //           rendering.
+    skipErrors: false,
+
+
+
+
+    /**
+     * CAMERA SETTINGS:
+     * ****************
+     */
+    // {number} The power degrees applied to the nodes/edges size relatively to
+    //          the zooming level. Basically:
+    //           > onScreenR = Math.pow(zoom, nodesPowRatio) * R
+    //           > onScreenT = Math.pow(zoom, edgesPowRatio) * T
+    nodesPowRatio: 0.5,
+    edgesPowRatio: 0.5,
+
+
+
+
+    /**
+     * ANIMATIONS SETTINGS:
+     * ********************
+     */
+    // {number} The default animation time.
+    animationsTime: 200
+  };
+
+  // Export the previously designed settings:
+  sigma.settings = sigma.utils.extend(sigma.settings || {}, settings);
+}).call(this);
diff --git a/demo/static/sigmajs/utils/sigma.polyfills.js b/demo/static/sigmajs/utils/sigma.polyfills.js
new file mode 100644
index 000000000..459b94382
--- /dev/null
+++ b/demo/static/sigmajs/utils/sigma.polyfills.js
@@ -0,0 +1,77 @@
+;(function(global) {
+  'use strict';
+
+  /**
+   * http://paulirish.com/2011/requestanimationframe-for-smart-animating/
+   * http://my.opera.com/emoller/blog/2011/12/20/requestanimationframe-for-smart-er-animating
+   * requestAnimationFrame polyfill by Erik Möller.
+   * fixes from Paul Irish and Tino Zijdel
+   * MIT license
+   */
+  var x,
+      lastTime = 0,
+      vendors = ['ms', 'moz', 'webkit', 'o'];
+
+  for (x = 0; x < vendors.length && !global.requestAnimationFrame; x++) {
+    global.requestAnimationFrame =
+      global[vendors[x] + 'RequestAnimationFrame'];
+    global.cancelAnimationFrame =
+      global[vendors[x] + 'CancelAnimationFrame'] ||
+      global[vendors[x] + 'CancelRequestAnimationFrame'];
+  }
+
+  if (!global.requestAnimationFrame)
+    global.requestAnimationFrame = function(callback, element) {
+      var currTime = new Date().getTime(),
+          timeToCall = Math.max(0, 16 - (currTime - lastTime)),
+          id = global.setTimeout(
+            function() {
+              callback(currTime + timeToCall);
+            },
+            timeToCall
+          );
+
+      lastTime = currTime + timeToCall;
+      return id;
+    };
+
+  if (!global.cancelAnimationFrame)
+    global.cancelAnimationFrame = function(id) {
+      clearTimeout(id);
+    };
+
+  /**
+   * Function.prototype.bind polyfill found on MDN.
+   * https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function/bind#Compatibility
+   * Public domain
+   */
+  if (!Function.prototype.bind)
+    Function.prototype.bind = function(oThis) {
+      if (typeof this !== 'function')
+        // Closest thing possible to the ECMAScript 5 internal IsCallable
+        // function:
+        throw new TypeError(
+          'Function.prototype.bind - what is trying to be bound is not callable'
+        );
+
+      var aArgs = Array.prototype.slice.call(arguments, 1),
+          fToBind = this,
+          fNOP,
+          fBound;
+
+      fNOP = function() {};
+      fBound = function() {
+        return fToBind.apply(
+          this instanceof fNOP && oThis ?
+            this :
+            oThis,
+          aArgs.concat(Array.prototype.slice.call(arguments))
+        );
+      };
+
+      fNOP.prototype = this.prototype;
+      fBound.prototype = new fNOP();
+
+      return fBound;
+    };
+})(this);
diff --git a/demo/static/sigmajs/utils/sigma.utils.js b/demo/static/sigmajs/utils/sigma.utils.js
new file mode 100644
index 000000000..fd0f59b5d
--- /dev/null
+++ b/demo/static/sigmajs/utils/sigma.utils.js
@@ -0,0 +1,950 @@
+;(function(undefined) {
+  'use strict';
+
+  if (typeof sigma === 'undefined')
+    throw 'sigma is not declared';
+
+  var _root = this;
+
+  // Initialize packages:
+  sigma.utils = sigma.utils || {};
+
+  /**
+   * MISC UTILS:
+   */
+  /**
+   * This function takes any number of objects as arguments, copies from each
+   * of these objects each pair key/value into a new object, and finally
+   * returns this object.
+   *
+   * The arguments are parsed from the last one to the first one, such that
+   * when several objects have keys in common, the "earliest" object wins.
+   *
+   * Example:
+   * ********
+   *  > var o1 = {
+   *  >       a: 1,
+   *  >       b: 2,
+   *  >       c: '3'
+   *  >     },
+   *  >     o2 = {
+   *  >       c: '4',
+   *  >       d: [ 5 ]
+   *  >     };
+   *  > sigma.utils.extend(o1, o2);
+   *  > // Returns: {
+   *  > //   a: 1,
+   *  > //   b: 2,
+   *  > //   c: '3',
+   *  > //   d: [ 5 ]
+   *  > // };
+   *
+   * @param  {object+} Any number of objects.
+   * @return {object}  The merged object.
+   */
+  sigma.utils.extend = function() {
+    var i,
+        k,
+        res = {},
+        l = arguments.length;
+
+    for (i = l - 1; i >= 0; i--)
+      for (k in arguments[i])
+        res[k] = arguments[i][k];
+
+    return res;
+  };
+
+  /**
+   * A short "Date.now()" polyfill.
+   *
+   * @return {Number} The current time (in ms).
+   */
+  sigma.utils.dateNow = function() {
+    return Date.now ? Date.now() : new Date().getTime();
+  };
+
+  /**
+   * Takes a package name as parameter and checks at each lebel if it exists,
+   * and if it does not, creates it.
+   *
+   * Example:
+   * ********
+   *  > sigma.utils.pkg('a.b.c');
+   *  > a.b.c;
+   *  > // Object {};
+   *  >
+   *  > sigma.utils.pkg('a.b.d');
+   *  > a.b;
+   *  > // Object { c: {}, d: {} };
+   *
+   * @param  {string} pkgName The name of the package to create/find.
+   * @return {object}         The related package.
+   */
+  sigma.utils.pkg = function(pkgName) {
+    return (pkgName || '').split('.').reduce(function(context, objName) {
+      return (objName in context) ?
+        context[objName] :
+        (context[objName] = {});
+    }, _root);
+  };
+
+  /**
+   * Returns a unique incremental number ID.
+   *
+   * Example:
+   * ********
+   *  > sigma.utils.id();
+   *  > // 1;
+   *  >
+   *  > sigma.utils.id();
+   *  > // 2;
+   *  >
+   *  > sigma.utils.id();
+   *  > // 3;
+   *
+   * @param  {string} pkgName The name of the package to create/find.
+   * @return {object}         The related package.
+   */
+  sigma.utils.id = (function() {
+    var i = 0;
+    return function() {
+      return ++i;
+    };
+  })();
+
+  /**
+   * This function takes an hexa color (for instance "#ffcc00" or "#fc0") or a
+   * rgb / rgba color (like "rgb(255,255,12)" or "rgba(255,255,12,1)") and
+   * returns an integer equal to "r * 255 * 255 + g * 255 + b", to gain some
+   * memory in the data given to WebGL shaders.
+   *
+   * @param  {string} val The hexa or rgba color.
+   * @return {number}     The number value.
+   */
+  sigma.utils.floatColor = function(val) {
+    var result = [0, 0, 0];
+
+    if (val.match(/^#/)) {
+      val = (val || '').replace(/^#/, '');
+      result = (val.length === 3) ?
+        [
+          parseInt(val.charAt(0) + val.charAt(0), 16),
+          parseInt(val.charAt(1) + val.charAt(1), 16),
+          parseInt(val.charAt(2) + val.charAt(2), 16)
+        ] :
+        [
+          parseInt(val.charAt(0) + val.charAt(1), 16),
+          parseInt(val.charAt(2) + val.charAt(3), 16),
+          parseInt(val.charAt(4) + val.charAt(5), 16)
+        ];
+    } else if (val.match(/^ *rgba? *\(/)) {
+      val = val.match(
+        /^ *rgba? *\( *([0-9]*) *, *([0-9]*) *, *([0-9]*) *(,.*)?\) *$/
+      );
+      result = [
+        +val[1],
+        +val[2],
+        +val[3]
+      ];
+    }
+
+    return (
+      result[0] * 256 * 256 +
+      result[1] * 256 +
+      result[2]
+    );
+  };
+
+    /**
+   * Perform a zoom into a camera, with or without animation, to the
+   * coordinates indicated using a specified ratio.
+   *
+   * Recognized parameters:
+   * **********************
+   * Here is the exhaustive list of every accepted parameters in the animation
+   * object:
+   *
+   *   {?number} duration     An amount of time that means the duration of the
+   *                          animation. If this parameter doesn't exist the
+   *                          zoom will be performed without animation.
+   *   {?function} onComplete A function to perform it after the animation. It
+   *                          will be performed even if there is no duration.
+   *
+   * @param {camera}     The camera where perform the zoom.
+   * @param {x}          The X coordiantion where the zoom goes.
+   * @param {y}          The Y coordiantion where the zoom goes.
+   * @param {ratio}      The ratio to apply it to the current camera ratio.
+   * @param {?animation} A dictionary with options for a possible animation.
+   */
+  sigma.utils.zoomTo = function(camera, x, y, ratio, animation) {
+    var settings = camera.settings,
+        count,
+        newRatio,
+        animationSettings,
+        coordinates;
+
+    // Create the newRatio dealing with min / max:
+    newRatio = Math.max(
+      settings('zoomMin'),
+      Math.min(
+        settings('zoomMax'),
+        camera.ratio * ratio
+      )
+    );
+
+    // Check that the new ratio is different from the initial one:
+    if (newRatio !== camera.ratio) {
+      // Create the coordinates variable:
+      ratio = newRatio / camera.ratio;
+      coordinates = {
+        x: x * (1 - ratio) + camera.x,
+        y: y * (1 - ratio) + camera.y,
+        ratio: newRatio
+      };
+
+      if (animation && animation.duration) {
+        // Complete the animation setings:
+        count = sigma.misc.animation.killAll(camera);
+        animation = sigma.utils.extend(
+          animation,
+          {
+            easing: count ? 'quadraticOut' : 'quadraticInOut'
+          }
+        );
+
+        sigma.misc.animation.camera(camera, coordinates, animation);
+      } else {
+        camera.goTo(coordinates);
+        if (animation && animation.onComplete)
+          animation.onComplete();
+      }
+    }
+  };
+
+  /**
+   * Return the control point coordinates for a quadratic bezier curve.
+   *
+   * @param  {number} x1  The X coordinate of the start point.
+   * @param  {number} y1  The Y coordinate of the start point.
+   * @param  {number} x2  The X coordinate of the end point.
+   * @param  {number} y2  The Y coordinate of the end point.
+   * @return {x,y}        The control point coordinates.
+   */
+  sigma.utils.getQuadraticControlPoint = function(x1, y1, x2, y2) {
+    return {
+      x: (x1 + x2) / 2 + (y2 - y1) / 4,
+      y: (y1 + y2) / 2 + (x1 - x2) / 4
+    };
+  };
+
+  /**
+    * Compute the coordinates of the point positioned
+    * at length t in the quadratic bezier curve.
+    *
+    * @param  {number} t  In [0,1] the step percentage to reach
+    *                     the point in the curve from the context point.
+    * @param  {number} x1 The X coordinate of the context point.
+    * @param  {number} y1 The Y coordinate of the context point.
+    * @param  {number} x2 The X coordinate of the ending point.
+    * @param  {number} y2 The Y coordinate of the ending point.
+    * @param  {number} xi The X coordinate of the control point.
+    * @param  {number} yi The Y coordinate of the control point.
+    * @return {object}    {x,y}.
+  */
+  sigma.utils.getPointOnQuadraticCurve = function(t, x1, y1, x2, y2, xi, yi) {
+    // http://stackoverflow.com/a/5634528
+    return {
+      x: Math.pow(1 - t, 2) * x1 + 2 * (1 - t) * t * xi + Math.pow(t, 2) * x2,
+      y: Math.pow(1 - t, 2) * y1 + 2 * (1 - t) * t * yi + Math.pow(t, 2) * y2
+    };
+  };
+
+  /**
+    * Compute the coordinates of the point positioned
+    * at length t in the cubic bezier curve.
+    *
+    * @param  {number} t  In [0,1] the step percentage to reach
+    *                     the point in the curve from the context point.
+    * @param  {number} x1 The X coordinate of the context point.
+    * @param  {number} y1 The Y coordinate of the context point.
+    * @param  {number} x2 The X coordinate of the end point.
+    * @param  {number} y2 The Y coordinate of the end point.
+    * @param  {number} cx The X coordinate of the first control point.
+    * @param  {number} cy The Y coordinate of the first control point.
+    * @param  {number} dx The X coordinate of the second control point.
+    * @param  {number} dy The Y coordinate of the second control point.
+    * @return {object}    {x,y} The point at t.
+  */
+  sigma.utils.getPointOnBezierCurve =
+    function(t, x1, y1, x2, y2, cx, cy, dx, dy) {
+    // http://stackoverflow.com/a/15397596
+    // Blending functions:
+    var B0_t = Math.pow(1 - t, 3),
+        B1_t = 3 * t * Math.pow(1 - t, 2),
+        B2_t = 3 * Math.pow(t, 2) * (1 - t),
+        B3_t = Math.pow(t, 3);
+
+    return {
+      x: (B0_t * x1) + (B1_t * cx) + (B2_t * dx) + (B3_t * x2),
+      y: (B0_t * y1) + (B1_t * cy) + (B2_t * dy) + (B3_t * y2)
+    };
+  };
+
+  /**
+   * Return the coordinates of the two control points for a self loop (i.e.
+   * where the start point is also the end point) computed as a cubic bezier
+   * curve.
+   *
+   * @param  {number} x    The X coordinate of the node.
+   * @param  {number} y    The Y coordinate of the node.
+   * @param  {number} size The node size.
+   * @return {x1,y1,x2,y2} The coordinates of the two control points.
+   */
+  sigma.utils.getSelfLoopControlPoints = function(x , y, size) {
+    return {
+      x1: x - size * 7,
+      y1: y,
+      x2: x,
+      y2: y + size * 7
+    };
+  };
+
+  /**
+   * Return the euclidian distance between two points of a plane
+   * with an orthonormal basis.
+   *
+   * @param  {number} x1  The X coordinate of the first point.
+   * @param  {number} y1  The Y coordinate of the first point.
+   * @param  {number} x2  The X coordinate of the second point.
+   * @param  {number} y2  The Y coordinate of the second point.
+   * @return {number}     The euclidian distance.
+   */
+  sigma.utils.getDistance = function(x0, y0, x1, y1) {
+    return Math.sqrt(Math.pow(x1 - x0, 2) + Math.pow(y1 - y0, 2));
+  };
+
+  /**
+   * Return the coordinates of the intersection points of two circles.
+   *
+   * @param  {number} x0  The X coordinate of center location of the first
+   *                      circle.
+   * @param  {number} y0  The Y coordinate of center location of the first
+   *                      circle.
+   * @param  {number} r0  The radius of the first circle.
+   * @param  {number} x1  The X coordinate of center location of the second
+   *                      circle.
+   * @param  {number} y1  The Y coordinate of center location of the second
+   *                      circle.
+   * @param  {number} r1  The radius of the second circle.
+   * @return {xi,yi}      The coordinates of the intersection points.
+   */
+  sigma.utils.getCircleIntersection = function(x0, y0, r0, x1, y1, r1) {
+    // http://stackoverflow.com/a/12219802
+    var a, dx, dy, d, h, rx, ry, x2, y2;
+
+    // dx and dy are the vertical and horizontal distances between the circle
+    // centers:
+    dx = x1 - x0;
+    dy = y1 - y0;
+
+    // Determine the straight-line distance between the centers:
+    d = Math.sqrt((dy * dy) + (dx * dx));
+
+    // Check for solvability:
+    if (d > (r0 + r1)) {
+        // No solution. circles do not intersect.
+        return false;
+    }
+    if (d < Math.abs(r0 - r1)) {
+        // No solution. one circle is contained in the other.
+        return false;
+    }
+
+    //'point 2' is the point where the line through the circle intersection
+    // points crosses the line between the circle centers.
+
+    // Determine the distance from point 0 to point 2:
+    a = ((r0 * r0) - (r1 * r1) + (d * d)) / (2.0 * d);
+
+    // Determine the coordinates of point 2:
+    x2 = x0 + (dx * a / d);
+    y2 = y0 + (dy * a / d);
+
+    // Determine the distance from point 2 to either of the intersection
+    // points:
+    h = Math.sqrt((r0 * r0) - (a * a));
+
+    // Determine the offsets of the intersection points from point 2:
+    rx = -dy * (h / d);
+    ry = dx * (h / d);
+
+    // Determine the absolute intersection points:
+    var xi = x2 + rx;
+    var xi_prime = x2 - rx;
+    var yi = y2 + ry;
+    var yi_prime = y2 - ry;
+
+    return {xi: xi, xi_prime: xi_prime, yi: yi, yi_prime: yi_prime};
+  };
+
+  /**
+    * Check if a point is on a line segment.
+    *
+    * @param  {number} x       The X coordinate of the point to check.
+    * @param  {number} y       The Y coordinate of the point to check.
+    * @param  {number} x1      The X coordinate of the line start point.
+    * @param  {number} y1      The Y coordinate of the line start point.
+    * @param  {number} x2      The X coordinate of the line end point.
+    * @param  {number} y2      The Y coordinate of the line end point.
+    * @param  {number} epsilon The precision (consider the line thickness).
+    * @return {boolean}        True if point is "close to" the line
+    *                          segment, false otherwise.
+  */
+  sigma.utils.isPointOnSegment = function(x, y, x1, y1, x2, y2, epsilon) {
+    // http://stackoverflow.com/a/328122
+    var crossProduct = Math.abs((y - y1) * (x2 - x1) - (x - x1) * (y2 - y1)),
+        d = sigma.utils.getDistance(x1, y1, x2, y2),
+        nCrossProduct = crossProduct / d; // normalized cross product
+
+    return (nCrossProduct < epsilon &&
+     Math.min(x1, x2) <= x && x <= Math.max(x1, x2) &&
+     Math.min(y1, y2) <= y && y <= Math.max(y1, y2));
+  };
+
+  /**
+    * Check if a point is on a quadratic bezier curve segment with a thickness.
+    *
+    * @param  {number} x       The X coordinate of the point to check.
+    * @param  {number} y       The Y coordinate of the point to check.
+    * @param  {number} x1      The X coordinate of the curve start point.
+    * @param  {number} y1      The Y coordinate of the curve start point.
+    * @param  {number} x2      The X coordinate of the curve end point.
+    * @param  {number} y2      The Y coordinate of the curve end point.
+    * @param  {number} cpx     The X coordinate of the curve control point.
+    * @param  {number} cpy     The Y coordinate of the curve control point.
+    * @param  {number} epsilon The precision (consider the line thickness).
+    * @return {boolean}        True if (x,y) is on the curve segment,
+    *                          false otherwise.
+  */
+  sigma.utils.isPointOnQuadraticCurve =
+    function(x, y, x1, y1, x2, y2, cpx, cpy, epsilon) {
+    // Fails if the point is too far from the extremities of the segment,
+    // preventing for more costly computation:
+    var dP1P2 = sigma.utils.getDistance(x1, y1, x2, y2);
+    if (Math.abs(x - x1) > dP1P2 || Math.abs(y - y1) > dP1P2) {
+      return false;
+    }
+
+    var dP1 = sigma.utils.getDistance(x, y, x1, y1),
+        dP2 = sigma.utils.getDistance(x, y, x2, y2),
+        t = 0.5,
+        r = (dP1 < dP2) ? -0.01 : 0.01,
+        rThreshold = 0.001,
+        i = 100,
+        pt = sigma.utils.getPointOnQuadraticCurve(t, x1, y1, x2, y2, cpx, cpy),
+        dt = sigma.utils.getDistance(x, y, pt.x, pt.y),
+        old_dt;
+
+    // This algorithm minimizes the distance from the point to the curve. It
+    // find the optimal t value where t=0 is the start point and t=1 is the end
+    // point of the curve, starting from t=0.5.
+    // It terminates because it runs a maximum of i interations.
+    while (i-- > 0 &&
+      t >= 0 && t <= 1 &&
+      (dt > epsilon) &&
+      (r > rThreshold || r < -rThreshold)) {
+      old_dt = dt;
+      pt = sigma.utils.getPointOnQuadraticCurve(t, x1, y1, x2, y2, cpx, cpy);
+      dt = sigma.utils.getDistance(x, y, pt.x, pt.y);
+
+      if (dt > old_dt) {
+        // not the right direction:
+        // halfstep in the opposite direction
+        r = -r / 2;
+        t += r;
+      }
+      else if (t + r < 0 || t + r > 1) {
+        // oops, we've gone too far:
+        // revert with a halfstep
+        r = r / 2;
+        dt = old_dt;
+      }
+      else {
+        // progress:
+        t += r;
+      }
+    }
+
+    return dt < epsilon;
+  };
+
+
+  /**
+    * Check if a point is on a cubic bezier curve segment with a thickness.
+    *
+    * @param  {number} x       The X coordinate of the point to check.
+    * @param  {number} y       The Y coordinate of the point to check.
+    * @param  {number} x1      The X coordinate of the curve start point.
+    * @param  {number} y1      The Y coordinate of the curve start point.
+    * @param  {number} x2      The X coordinate of the curve end point.
+    * @param  {number} y2      The Y coordinate of the curve end point.
+    * @param  {number} cpx1    The X coordinate of the 1st curve control point.
+    * @param  {number} cpy1    The Y coordinate of the 1st curve control point.
+    * @param  {number} cpx2    The X coordinate of the 2nd curve control point.
+    * @param  {number} cpy2    The Y coordinate of the 2nd curve control point.
+    * @param  {number} epsilon The precision (consider the line thickness).
+    * @return {boolean}        True if (x,y) is on the curve segment,
+    *                          false otherwise.
+  */
+  sigma.utils.isPointOnBezierCurve =
+    function(x, y, x1, y1, x2, y2, cpx1, cpy1, cpx2, cpy2, epsilon) {
+    // Fails if the point is too far from the extremities of the segment,
+    // preventing for more costly computation:
+    var dP1CP1 = sigma.utils.getDistance(x1, y1, cpx1, cpy1);
+    if (Math.abs(x - x1) > dP1CP1 || Math.abs(y - y1) > dP1CP1) {
+      return false;
+    }
+
+    var dP1 = sigma.utils.getDistance(x, y, x1, y1),
+        dP2 = sigma.utils.getDistance(x, y, x2, y2),
+        t = 0.5,
+        r = (dP1 < dP2) ? -0.01 : 0.01,
+        rThreshold = 0.001,
+        i = 100,
+        pt = sigma.utils.getPointOnBezierCurve(
+          t, x1, y1, x2, y2, cpx1, cpy1, cpx2, cpy2),
+        dt = sigma.utils.getDistance(x, y, pt.x, pt.y),
+        old_dt;
+
+    // This algorithm minimizes the distance from the point to the curve. It
+    // find the optimal t value where t=0 is the start point and t=1 is the end
+    // point of the curve, starting from t=0.5.
+    // It terminates because it runs a maximum of i interations.
+    while (i-- > 0 &&
+      t >= 0 && t <= 1 &&
+      (dt > epsilon) &&
+      (r > rThreshold || r < -rThreshold)) {
+      old_dt = dt;
+      pt = sigma.utils.getPointOnBezierCurve(
+        t, x1, y1, x2, y2, cpx1, cpy1, cpx2, cpy2);
+      dt = sigma.utils.getDistance(x, y, pt.x, pt.y);
+
+      if (dt > old_dt) {
+        // not the right direction:
+        // halfstep in the opposite direction
+        r = -r / 2;
+        t += r;
+      }
+      else if (t + r < 0 || t + r > 1) {
+        // oops, we've gone too far:
+        // revert with a halfstep
+        r = r / 2;
+        dt = old_dt;
+      }
+      else {
+        // progress:
+        t += r;
+      }
+    }
+
+    return dt < epsilon;
+  };
+
+
+  /**
+   * ************
+   * EVENTS UTILS:
+   * ************
+   */
+  /**
+   * Here are some useful functions to unify extraction of the information we
+   * need with mouse events and touch events, from different browsers:
+   */
+
+  /**
+   * Extract the local X position from a mouse or touch event.
+   *
+   * @param  {event}  e A mouse or touch event.
+   * @return {number}   The local X value of the mouse.
+   */
+  sigma.utils.getX = function(e) {
+    return (
+      (e.offsetX !== undefined && e.offsetX) ||
+      (e.layerX !== undefined && e.layerX) ||
+      (e.clientX !== undefined && e.clientX)
+    );
+  };
+
+  /**
+   * Extract the local Y position from a mouse or touch event.
+   *
+   * @param  {event}  e A mouse or touch event.
+   * @return {number}   The local Y value of the mouse.
+   */
+  sigma.utils.getY = function(e) {
+    return (
+      (e.offsetY !== undefined && e.offsetY) ||
+      (e.layerY !== undefined && e.layerY) ||
+      (e.clientY !== undefined && e.clientY)
+    );
+  };
+
+  /**
+   * Extract the width from a mouse or touch event.
+   *
+   * @param  {event}  e A mouse or touch event.
+   * @return {number}   The width of the event's target.
+   */
+  sigma.utils.getWidth = function(e) {
+    var w = (!e.target.ownerSVGElement) ?
+              e.target.width :
+              e.target.ownerSVGElement.width;
+
+    return (
+      (typeof w === 'number' && w) ||
+      (w !== undefined && w.baseVal !== undefined && w.baseVal.value)
+    );
+  };
+
+  /**
+   * Extract the height from a mouse or touch event.
+   *
+   * @param  {event}  e A mouse or touch event.
+   * @return {number}   The height of the event's target.
+   */
+  sigma.utils.getHeight = function(e) {
+    var h = (!e.target.ownerSVGElement) ?
+              e.target.height :
+              e.target.ownerSVGElement.height;
+
+    return (
+      (typeof h === 'number' && h) ||
+      (h !== undefined && h.baseVal !== undefined && h.baseVal.value)
+    );
+  };
+
+  /**
+   * Extract the wheel delta from a mouse or touch event.
+   *
+   * @param  {event}  e A mouse or touch event.
+   * @return {number}   The wheel delta of the mouse.
+   */
+  sigma.utils.getDelta = function(e) {
+    return (
+      (e.wheelDelta !== undefined && e.wheelDelta) ||
+      (e.detail !== undefined && -e.detail)
+    );
+  };
+
+  /**
+   * Returns the offset of a DOM element.
+   *
+   * @param  {DOMElement} dom The element to retrieve the position.
+   * @return {object}         The offset of the DOM element (top, left).
+   */
+  sigma.utils.getOffset = function(dom) {
+    var left = 0,
+        top = 0;
+
+    while (dom) {
+      top = top + parseInt(dom.offsetTop);
+      left = left + parseInt(dom.offsetLeft);
+      dom = dom.offsetParent;
+    }
+
+    return {
+      top: top,
+      left: left
+    };
+  };
+
+  /**
+   * Simulates a "double click" event.
+   *
+   * @param  {HTMLElement} target   The event target.
+   * @param  {string}      type     The event type.
+   * @param  {function}    callback The callback to execute.
+   */
+  sigma.utils.doubleClick = function(target, type, callback) {
+    var clicks = 0,
+        self = this,
+        handlers;
+
+    target._doubleClickHandler = target._doubleClickHandler || {};
+    target._doubleClickHandler[type] = target._doubleClickHandler[type] || [];
+    handlers = target._doubleClickHandler[type];
+
+    handlers.push(function(e) {
+      clicks++;
+
+      if (clicks === 2) {
+        clicks = 0;
+        return callback(e);
+      } else if (clicks === 1) {
+        setTimeout(function() {
+          clicks = 0;
+        }, sigma.settings.doubleClickTimeout);
+      }
+    });
+
+    target.addEventListener(type, handlers[handlers.length - 1], false);
+  };
+
+  /**
+   * Unbind simulated "double click" events.
+   *
+   * @param  {HTMLElement} target   The event target.
+   * @param  {string}      type     The event type.
+   */
+  sigma.utils.unbindDoubleClick = function(target, type) {
+    var handler,
+        handlers = (target._doubleClickHandler || {})[type] || [];
+
+    while ((handler = handlers.pop())) {
+      target.removeEventListener(type, handler);
+    }
+
+    delete (target._doubleClickHandler || {})[type];
+  };
+
+
+
+
+  /**
+   * Here are just some of the most basic easing functions, used for the
+   * animated camera "goTo" calls.
+   *
+   * If you need some more easings functions, don't hesitate to add them to
+   * sigma.utils.easings. But I will not add some more here or merge PRs
+   * containing, because I do not want sigma sources full of overkill and never
+   * used stuff...
+   */
+  sigma.utils.easings = sigma.utils.easings || {};
+  sigma.utils.easings.linearNone = function(k) {
+    return k;
+  };
+  sigma.utils.easings.quadraticIn = function(k) {
+    return k * k;
+  };
+  sigma.utils.easings.quadraticOut = function(k) {
+    return k * (2 - k);
+  };
+  sigma.utils.easings.quadraticInOut = function(k) {
+    if ((k *= 2) < 1)
+      return 0.5 * k * k;
+    return - 0.5 * (--k * (k - 2) - 1);
+  };
+  sigma.utils.easings.cubicIn = function(k) {
+    return k * k * k;
+  };
+  sigma.utils.easings.cubicOut = function(k) {
+    return --k * k * k + 1;
+  };
+  sigma.utils.easings.cubicInOut = function(k) {
+    if ((k *= 2) < 1)
+      return 0.5 * k * k * k;
+    return 0.5 * ((k -= 2) * k * k + 2);
+  };
+
+
+
+
+  /**
+   * ************
+   * WEBGL UTILS:
+   * ************
+   */
+  /**
+   * Loads a WebGL shader and returns it.
+   *
+   * @param  {WebGLContext}           gl           The WebGLContext to use.
+   * @param  {string}                 shaderSource The shader source.
+   * @param  {number}                 shaderType   The type of shader.
+   * @param  {function(string): void} error        Callback for errors.
+   * @return {WebGLShader}                         The created shader.
+   */
+  sigma.utils.loadShader = function(gl, shaderSource, shaderType, error) {
+    var compiled,
+        shader = gl.createShader(shaderType);
+
+    // Load the shader source
+    gl.shaderSource(shader, shaderSource);
+
+    // Compile the shader
+    gl.compileShader(shader);
+
+    // Check the compile status
+    compiled = gl.getShaderParameter(shader, gl.COMPILE_STATUS);
+
+    // If something went wrong:
+    if (!compiled) {
+      if (error) {
+        error(
+          'Error compiling shader "' + shader + '":' +
+          gl.getShaderInfoLog(shader)
+        );
+      }
+
+      gl.deleteShader(shader);
+      return null;
+    }
+
+    return shader;
+  };
+
+  /**
+   * Creates a program, attaches shaders, binds attrib locations, links the
+   * program and calls useProgram.
+   *
+   * @param  {Array.<WebGLShader>}    shaders   The shaders to attach.
+   * @param  {Array.<string>}         attribs   The attribs names.
+   * @param  {Array.<number>}         locations The locations for the attribs.
+   * @param  {function(string): void} error     Callback for errors.
+   * @return {WebGLProgram}                     The created program.
+   */
+  sigma.utils.loadProgram = function(gl, shaders, attribs, loc, error) {
+    var i,
+        linked,
+        program = gl.createProgram();
+
+    for (i = 0; i < shaders.length; ++i)
+      gl.attachShader(program, shaders[i]);
+
+    if (attribs)
+      for (i = 0; i < attribs.length; ++i)
+        gl.bindAttribLocation(
+          program,
+          locations ? locations[i] : i,
+          opt_attribs[i]
+        );
+
+    gl.linkProgram(program);
+
+    // Check the link status
+    linked = gl.getProgramParameter(program, gl.LINK_STATUS);
+    if (!linked) {
+      if (error)
+        error('Error in program linking: ' + gl.getProgramInfoLog(program));
+
+      gl.deleteProgram(program);
+      return null;
+    }
+
+    return program;
+  };
+
+
+
+
+  /**
+   * *********
+   * MATRICES:
+   * *********
+   * The following utils are just here to help generating the transformation
+   * matrices for the WebGL renderers.
+   */
+  sigma.utils.pkg('sigma.utils.matrices');
+
+  /**
+   * The returns a 3x3 translation matrix.
+   *
+   * @param  {number} dx The X translation.
+   * @param  {number} dy The Y translation.
+   * @return {array}     Returns the matrix.
+   */
+  sigma.utils.matrices.translation = function(dx, dy) {
+    return [
+      1, 0, 0,
+      0, 1, 0,
+      dx, dy, 1
+    ];
+  };
+
+  /**
+   * The returns a 3x3 or 2x2 rotation matrix.
+   *
+   * @param  {number}  angle The rotation angle.
+   * @param  {boolean} m2    If true, the function will return a 2x2 matrix.
+   * @return {array}         Returns the matrix.
+   */
+  sigma.utils.matrices.rotation = function(angle, m2) {
+    var cos = Math.cos(angle),
+        sin = Math.sin(angle);
+
+    return m2 ? [
+      cos, -sin,
+      sin, cos
+    ] : [
+      cos, -sin, 0,
+      sin, cos, 0,
+      0, 0, 1
+    ];
+  };
+
+  /**
+   * The returns a 3x3 or 2x2 homothetic transformation matrix.
+   *
+   * @param  {number}  ratio The scaling ratio.
+   * @param  {boolean} m2    If true, the function will return a 2x2 matrix.
+   * @return {array}         Returns the matrix.
+   */
+  sigma.utils.matrices.scale = function(ratio, m2) {
+    return m2 ? [
+      ratio, 0,
+      0, ratio
+    ] : [
+      ratio, 0, 0,
+      0, ratio, 0,
+      0, 0, 1
+    ];
+  };
+
+  /**
+   * The returns a 3x3 or 2x2 homothetic transformation matrix.
+   *
+   * @param  {array}   a  The first matrix.
+   * @param  {array}   b  The second matrix.
+   * @param  {boolean} m2 If true, the function will assume both matrices are
+   *                      2x2.
+   * @return {array}      Returns the matrix.
+   */
+  sigma.utils.matrices.multiply = function(a, b, m2) {
+    var l = m2 ? 2 : 3,
+        a00 = a[0 * l + 0],
+        a01 = a[0 * l + 1],
+        a02 = a[0 * l + 2],
+        a10 = a[1 * l + 0],
+        a11 = a[1 * l + 1],
+        a12 = a[1 * l + 2],
+        a20 = a[2 * l + 0],
+        a21 = a[2 * l + 1],
+        a22 = a[2 * l + 2],
+        b00 = b[0 * l + 0],
+        b01 = b[0 * l + 1],
+        b02 = b[0 * l + 2],
+        b10 = b[1 * l + 0],
+        b11 = b[1 * l + 1],
+        b12 = b[1 * l + 2],
+        b20 = b[2 * l + 0],
+        b21 = b[2 * l + 1],
+        b22 = b[2 * l + 2];
+
+    return m2 ? [
+      a00 * b00 + a01 * b10,
+      a00 * b01 + a01 * b11,
+      a10 * b00 + a11 * b10,
+      a10 * b01 + a11 * b11
+    ] : [
+      a00 * b00 + a01 * b10 + a02 * b20,
+      a00 * b01 + a01 * b11 + a02 * b21,
+      a00 * b02 + a01 * b12 + a02 * b22,
+      a10 * b00 + a11 * b10 + a12 * b20,
+      a10 * b01 + a11 * b11 + a12 * b21,
+      a10 * b02 + a11 * b12 + a12 * b22,
+      a20 * b00 + a21 * b10 + a22 * b20,
+      a20 * b01 + a21 * b11 + a22 * b21,
+      a20 * b02 + a21 * b12 + a22 * b22
+    ];
+  };
+}).call(this);
diff --git a/demo/worker/benchmark.hpp b/demo/worker/benchmark.hpp
new file mode 100644
index 000000000..43c52bd85
--- /dev/null
+++ b/demo/worker/benchmark.hpp
@@ -0,0 +1,77 @@
+#pragma once
+
+#include <vector>
+#include <memory>
+#include <chrono>
+#include <future>
+
+#include "worker.hpp"
+
+template <class W>
+class WorkerRunner
+{
+public:
+    WorkerRunner(const std::vector<std::string>& queries)
+        : worker(std::make_unique<W>(queries)) {}
+
+    W* operator->() { return worker.get(); }
+    const W* operator->() const { return worker.get(); }
+
+    void operator()(std::chrono::duration<double> duration)
+    {
+        std::packaged_task<WorkerResult()> task([this, duration]() {
+            return this->worker->benchmark(duration);
+        });
+
+        result = std::move(task.get_future());
+        std::thread(std::move(task)).detach();
+    }
+
+    std::unique_ptr<W> worker;
+    std::future<WorkerResult> result;
+};
+
+struct Result
+{
+    std::chrono::duration<double> elapsed;
+    std::vector<uint64_t> requests;
+};
+
+Result benchmark(const std::string& host, const std::string& port,
+                 int threads, int connections,
+                 std::chrono::duration<double> duration,
+                 const std::vector<std::string>& queries)
+{
+    std::vector<WorkerRunner<CypherWorker>> workers;
+
+    for(int i = 0; i < threads; ++i)
+        workers.emplace_back(queries);
+
+    for(int i = 0; i < connections; ++i)
+        workers[i % threads]->connect(host, port);
+
+    for(auto& worker : workers)
+        worker(duration);
+
+    std::vector<WorkerResult> results;
+
+    for(auto& worker : workers)
+    {
+        worker.result.wait();
+        results.push_back(worker.result.get());
+    }
+
+    auto start = std::min_element(results.begin(), results.end(),
+        [](auto a, auto b) { return a.start < b.start; })->start;
+
+    auto end = std::max_element(results.begin(), results.end(),
+        [](auto a, auto b) { return a.end < b.end; })->end;
+
+    std::vector<uint64_t> qps(queries.size(), 0);
+
+    for(auto& result : results)
+        for(size_t i = 0; i < result.requests.size(); ++i)
+            qps[i] += result.requests[i];
+
+    return {end - start, qps};
+}
diff --git a/demo/worker/client.cpp b/demo/worker/client.cpp
new file mode 100644
index 000000000..0ab4824ef
--- /dev/null
+++ b/demo/worker/client.cpp
@@ -0,0 +1,59 @@
+#include <iostream>
+#include <cstdlib>
+#include <vector>
+
+#include "debug/log.hpp"
+#include "benchmark.hpp"
+
+void help()
+{
+    std::cout << "error: too few arguments." << std::endl
+              << "usage: host port threads connections duration[s]"
+              << std::endl;
+
+    std::exit(0);
+}
+
+int main(int argc, char* argv[])
+{
+    if(argc < 6)
+        help();
+
+    auto host        = std::string(argv[1]);
+    auto port        = std::string(argv[2]);
+    auto threads     = std::stoi(argv[3]);
+    auto connections = std::stoi(argv[4]);
+    auto duration    = std::stod(argv[5]);
+
+    std::vector<std::string> queries {
+        "CREATE (n{id:@}) RETURN n",
+        "MATCH (n{id:#}),(m{id:#}) CREATE (n)-[r:test]->(m) RETURN r",
+        "MATCH (n{id:#}) SET n.prop = ^ RETURN n",
+        "MATCH (n{id:#})-[r]->(m) RETURN count(r)"
+    };
+
+    std::cout << "Running queries on " << connections << " connections "
+              << "using " << threads << " threads "
+              << "for " << duration << " seconds." << std::endl
+              << "..." << std::endl;
+
+    auto result = benchmark(host, port, threads, connections,
+                            std::chrono::duration<double>(duration), queries);
+
+    auto& reqs = result.requests;
+    auto elapsed = result.elapsed.count();
+
+    auto total = std::accumulate(reqs.begin(), reqs.end(), 0.0,
+        [](auto acc, auto x) { return acc + x; }
+    );
+
+    std::cout << "Total of " << total << " requests in "
+              << elapsed  << "s (" << int(total / elapsed) << " req/s)."
+              << std::endl;
+
+    for(size_t i = 0; i < queries.size(); ++i)
+        std::cout << queries[i] << " => "
+                  << int(reqs[i] / elapsed) << " req/s." << std::endl;
+
+    return 0;
+}
diff --git a/demo/worker/cypher.hpp b/demo/worker/cypher.hpp
new file mode 100644
index 000000000..6e883d479
--- /dev/null
+++ b/demo/worker/cypher.hpp
@@ -0,0 +1,55 @@
+#pragma once
+
+#include <string>
+
+#include "memory/literals.hpp"
+#include "cypher_replacer.hpp"
+
+using namespace memory::literals;
+
+class Cypher
+{
+    static std::string body_start;
+    static std::string body_end;
+
+public:
+    Cypher()
+    {
+        request.reserve(64_kB);
+    }
+
+    std::string& operator()(std::string query)
+    {
+        request.clear();
+        replacer(query);
+
+        // request begin and headers
+        request += "POST /db/data/transaction/commit HTTP/1.1\r\n" \
+                   "Host: localhost:7474\r\n" \
+                   "Authorization: Basic bmVvNGo6cGFzcw==\r\n" \
+                   "Accept: application/json; charset=UTF-8\r\n" \
+                   "Content-Type: application/json\r\n" \
+                   "Content-Length: ";
+
+        // content length
+        auto size = body_start.size() + query.size() + body_end.size();
+        request += std::to_string(size);
+
+        // headers end
+        request += "\r\n\r\n";
+
+        // write body
+        request += body_start;
+        request += query;
+        request += body_end;
+
+        return request;
+    }
+
+private:
+    std::string request;
+    CypherReplacer replacer;
+};
+
+std::string Cypher::body_start = "{\"statements\":[{\"statement\":\"";
+std::string Cypher::body_end = "\"}]}";
diff --git a/demo/worker/cypher_replacer.hpp b/demo/worker/cypher_replacer.hpp
new file mode 100644
index 000000000..210c1898b
--- /dev/null
+++ b/demo/worker/cypher_replacer.hpp
@@ -0,0 +1,39 @@
+#pragma once
+
+#include <random>
+#include <atomic>
+
+#include "replacer.hpp"
+#include "random.hpp"
+
+static std::atomic<uint64_t> counter {0};
+static thread_local std::mt19937 mt;
+
+class CypherReplacer
+{
+    std::uniform_int_distribution<uint64_t> random_int;
+    RandomString random_string;
+
+public:
+    CypherReplacer()
+    {
+        replacer
+            .replace("#", [&]() {
+                return std::to_string(random_int(mt) % (counter.load() + 1));
+            })
+            .replace("^", [&]() {
+                return random_string(mt, 15);
+            })
+            .replace("@", [&]() {
+                return std::to_string(counter.fetch_add(1));
+            });
+    }
+
+    std::string& operator()(std::string& query)
+    {
+        return replacer(query);
+    }
+
+private:
+    Replacer replacer;
+};
diff --git a/demo/worker/random.hpp b/demo/worker/random.hpp
new file mode 100644
index 000000000..45d5d1585
--- /dev/null
+++ b/demo/worker/random.hpp
@@ -0,0 +1,32 @@
+#pragma once
+
+#include <string>
+#include <random>
+
+class RandomString
+{
+    static constexpr char charset[] =
+        "0123456789"
+        "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
+        "abcdefghijklmnopqrstuvwxyz";
+
+public:
+    template <class Rg>
+    std::string operator()(Rg&& gen, size_t len)
+    {
+        auto str = std::string();
+        str.reserve(len + 2);
+        str.push_back('\'');
+
+        while(str.size() < len)
+            str.push_back(charset[rnd(std::forward<Rg>(gen))]);
+
+        str.push_back('\'');
+        return str;
+    }
+
+private:
+    std::uniform_int_distribution<> rnd {0, sizeof(charset) - 1};
+};
+
+constexpr char RandomString::charset[];
diff --git a/demo/worker/replacer.hpp b/demo/worker/replacer.hpp
new file mode 100644
index 000000000..cdbea2842
--- /dev/null
+++ b/demo/worker/replacer.hpp
@@ -0,0 +1,40 @@
+#pragma once
+
+#include <vector>
+#include <string>
+#include <functional>
+
+class Replacer
+{
+    struct Rule
+    {
+        std::string match;
+        std::function<std::string()> generator;
+    };
+
+public:
+    Replacer() {}
+
+    template <class F>
+    Replacer& replace(const std::string& match, F&& generator)
+    {
+        rules.push_back({match, std::forward<F>(generator)});
+        return *this;
+    }
+
+    std::string& operator()(std::string& str)
+    {
+        size_t n;
+
+        for(auto& rule : rules)
+        {
+            while((n = str.find_first_of(rule.match)) != std::string::npos)
+                str.replace(n, rule.match.size(), rule.generator());
+        }
+
+        return str;
+    }
+
+private:
+    std::vector<Rule> rules;
+};
diff --git a/demo/worker/simple_client.hpp b/demo/worker/simple_client.hpp
new file mode 100644
index 000000000..7459124d4
--- /dev/null
+++ b/demo/worker/simple_client.hpp
@@ -0,0 +1,26 @@
+#pragma once
+
+#include "io/network/client.hpp"
+
+template <class Derived, class Stream>
+class SimpleClient : public io::Client<Derived, Stream>
+{
+    char buf[65535];
+
+public:
+    using Buffer = typename io::StreamReader<Derived, Stream>::Buffer;
+
+    void on_wait_timeout() {}
+
+    void on_error(Stream&)
+    {
+        std::abort();
+    }
+
+    Buffer on_alloc(Stream&)
+    {
+        return Buffer { buf, sizeof buf };
+    }
+
+    void on_close(Stream&) {}
+};
diff --git a/demo/worker/worker.hpp b/demo/worker/worker.hpp
new file mode 100644
index 000000000..385dc2b3d
--- /dev/null
+++ b/demo/worker/worker.hpp
@@ -0,0 +1,92 @@
+#pragma once
+
+#include <functional>
+#include <algorithm>
+#include <iostream>
+#include <random>
+#include <vector>
+#include <thread>
+#include <chrono>
+#include <atomic>
+#include <future>
+
+#include "simple_client.hpp"
+
+#include "io/network/tcp/stream.hpp"
+#include "cypher.hpp"
+
+struct WorkerResult
+{
+    std::chrono::high_resolution_clock::time_point start, end;
+    std::vector<uint64_t> requests;
+};
+
+class CypherWorker : public SimpleClient<CypherWorker, io::tcp::Stream>
+{
+public:
+    CypherWorker(const std::vector<std::string>& queries)
+        : queries(queries), requests(queries.size(), 0) {}
+
+    io::tcp::Stream& on_connect(io::Socket&& socket)
+    {
+        streams.emplace_back(std::make_unique<io::tcp::Stream>(
+            std::forward<io::Socket>(socket)
+        ));
+
+        return *streams.back();
+    }
+
+    void on_read(io::tcp::Stream& stream, Buffer& buf)
+    {
+        /* std::cout << "------------------- RESPONSE ------------------" << std::endl; */
+        /* std::cout << std::string(buf.ptr, buf.len) << std::endl; */
+        /* std::cout << "-----------------------------------------------" << std::endl; */
+        /* std::cout << std::endl; */
+
+        send(stream.socket);
+    }
+
+    void send(io::Socket& socket)
+    {
+        auto idx = random_int(mt) % queries.size();
+
+        // increase the number of requests
+        requests[idx]++;
+
+        // cypherize and send the request
+        //socket.write(cypher(queries[idx]));
+        auto req = cypher(queries[idx]);
+
+        /* std::cout << "-------------------- REQUEST ------------------" << std::endl; */
+        /* std::cout << req << std::endl; */
+
+        socket.write(req);
+    }
+
+    WorkerResult benchmark(std::chrono::duration<double> duration)
+    {
+        using clock = std::chrono::high_resolution_clock;
+        clock::time_point end, start = clock::now();
+
+        for(auto& stream : streams)
+            send(stream->socket);
+
+        while(true)
+        {
+            this->wait_and_process_events();
+
+            if((end = clock::now()) - start > duration)
+                break;
+        }
+
+        return {start, end, requests};
+    }
+
+private:
+    std::uniform_int_distribution<> random_int;
+    Cypher cypher;
+
+    std::vector<std::unique_ptr<io::tcp::Stream>> streams;
+    std::vector<std::string> queries;
+    std::vector<uint64_t> requests;
+};
diff --git a/examples/client.cpp b/examples/client.cpp
new file mode 100644
index 000000000..10972d07c
--- /dev/null
+++ b/examples/client.cpp
@@ -0,0 +1,293 @@
+#include <functional>
+#include <algorithm>
+#include <iostream>
+#include <random>
+#include <vector>
+#include <thread>
+#include <chrono>
+#include <atomic>
+#include <future>
+
+#include <unistd.h>
+#include <sys/types.h>
+#include <sys/socket.h>
+#include <netinet/in.h>
+
+#include "debug/log.hpp"
+
+#include "io/network/epoll.hpp"
+#include "io/network/socket.hpp"
+#include "io/network/tcp/stream.hpp"
+#include "io/network/stream_reader.hpp"
+
+#include "memory/literals.hpp"
+
+using namespace memory::literals;
+
+
+class RandomString
+{
+    static constexpr char charset[] =
+        "0123456789"
+        "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
+        "abcdefghijklmnopqrstuvwxyz";
+
+public:
+    template <class Rg>
+    std::string operator()(Rg&& gen, size_t len)
+    {
+        auto str = std::string();
+        str.reserve(len + 2);
+        str.push_back('\'');
+
+        while(str.size() < len)
+            str.push_back(charset[rnd(std::forward<Rg>(gen))]);
+
+        str.push_back('\'');
+        return str;
+    }
+
+private:
+    std::uniform_int_distribution<> rnd {0, sizeof(charset) - 1};
+};
+
+constexpr char RandomString::charset[];
+
+static std::mt19937 mt {std::random_device{}()};
+
+class CypherPost
+{
+    static std::string templ;
+
+public:
+    CypherPost()
+    {
+        request.reserve(64_kB);
+    }
+
+    void set(const std::string& query)
+    {
+        request.clear();
+
+        request += "POST /db/data/transaction/commit HTTP/1.1\r\n" \
+                   "Host: localhost:7474\r\n" \
+                   "Authorization: Basic bmVvNGo6cGFzcw==\r\n" \
+                   "Accept: application/json; charset=UTF-8\r\n" \
+                   "Content-Type: application/json\r\n" \
+                   "Content-Length: ";
+        request += std::to_string(query.size() + templ.size() + 4);
+        request += "\r\n\r\n";
+        request += templ;
+        request += query;
+        request += "\"}]}";
+    }
+
+    operator const std::string&() const { return request; }
+
+private:
+    std::string request;
+};
+
+std::string CypherPost::templ = "{\"statements\":[{\"statement\":\"";
+
+struct Result
+{
+    std::chrono::high_resolution_clock::time_point start, end;
+    uint64_t requests;
+};
+
+class Worker : public io::StreamReader<Worker, io::tcp::Stream>
+{
+    char buf[65535];
+    CypherPost post;
+
+    std::uniform_int_distribution<> random_int;
+    RandomString random_string;
+    Replacer replacer;
+
+public:
+    Worker()
+    {
+        replacer.replace("#", [&]() { return std::to_string(random_int(mt)); })
+                .replace("^", [&]() { return random_string(mt, 15); });
+    }
+
+    io::tcp::Stream& on_connect(io::Socket&&)
+    {
+        // DUMMY, refactor StreamReader to be more generic
+        return *streams.back();
+    }
+
+    bool connect(const char* name, const char* port)
+    {
+        auto socket = io::Socket::connect(name, port);
+
+        if(!socket.is_open())
+            return false;
+
+        socket.set_non_blocking();
+
+        streams.push_back(std::make_unique<io::tcp::Stream>(std::move(socket)));
+        auto& stream = streams.back();
+
+        stream->event.events = EPOLLIN | EPOLLET | EPOLLRDHUP;
+        this->add(*stream);
+
+        return true;
+    }
+
+    void on_error(io::tcp::Stream& conn)
+    {
+        LOG_DEBUG("error on socket " << conn.id());
+        (void)conn;
+
+        LOG_DEBUG((errno == EBADF));
+
+        std::this_thread::sleep_for(std::chrono::milliseconds(10));
+        std::abort();
+    }
+
+    void on_wait_timeout() {}
+
+    Buffer on_alloc(io::tcp::Stream&)
+    {
+        return Buffer { buf, sizeof buf };
+    }
+
+    void on_read(io::tcp::Stream& stream, Buffer& buf)
+    {
+        /* std::cout << "RESPONSE" << std::endl; */
+        /* std::cout << std::string(buf.ptr, buf.len) << std::endl; */
+
+        requests++;
+
+        LOG_DEBUG("on_read");
+        sendreq(stream.socket);
+    }
+
+    void on_close(io::tcp::Stream&) {}
+
+    void sendreq(io::Socket& socket)
+    {
+        /* auto query = std::string("CREATE (n:Person {id: #, name: ^}) RETURN n"); */
+        auto query = std::string("MATCH (n:Person {id: #}) RETURN n");
+
+        post.set(replacer(query));
+
+        /* std::cout << "REQUEST" << std::endl; */
+        /* std::cout << static_cast<const std::string&>(post) << std::endl; */
+        /* std::cout << "SIZE = " << static_cast<const std::string&>(post).size() << std::endl; */
+
+        auto n = socket.write(static_cast<const std::string&>(post));
+
+        /* std::cout << "Written N = " << n << " bytes." << std::endl; */
+
+        LOG_DEBUG("sent.");
+    }
+
+    Result run_benchmark(std::chrono::duration<double> duration)
+    {
+        LOG_DEBUG("run_benchmark");
+        using clock = std::chrono::high_resolution_clock;
+        clock::time_point end, start = clock::now();
+
+        for(auto& stream : streams)
+            sendreq(stream->socket);
+
+        LOG_DEBUG("sent req to all streams");
+
+        while(true)
+        {
+            LOG_DEBUG("WAIT AND PROCESS");
+            this->wait_and_process_events();
+
+            if((end = clock::now()) - start > duration)
+                break;
+        }
+
+        return {start, end, requests};
+    }
+
+private:
+    uint64_t requests {0};
+    std::vector<std::unique_ptr<io::tcp::Stream>> streams;
+};
+
+class WorkerRunner
+{
+public:
+    WorkerRunner() : worker(std::make_unique<Worker>()) {}
+
+    Worker* operator->() { return worker.get(); }
+    const Worker* operator->() const { return worker.get(); }
+
+    void operator()(std::chrono::duration<double> duration)
+    {
+        std::packaged_task<Result()> task([this, duration]() {
+            return this->worker->run_benchmark(duration);
+        });
+
+        result = std::move(task.get_future());
+        std::thread(std::move(task)).detach();
+    }
+
+    std::unique_ptr<Worker> worker;
+    std::future<Result> result;
+};
+
+std::atomic<bool> alive {true};
+
+int main(int argc, const char* argv[])
+{
+    using clock = std::chrono::high_resolution_clock;
+    using namespace std::chrono;
+
+    if(argc < 4)
+        std::abort();
+
+    auto threads = std::stoi(argv[1]);
+    auto connections = std::stoi(argv[2]);
+    auto duration = std::stoi(argv[3]);
+
+    std::vector<WorkerRunner> workers;
+
+    for(int i = 0; i < threads; ++i)
+        workers.emplace_back();
+
+    for(int i = 0; i < connections; ++i)
+        workers[i % threads]->connect("localhost", "7474");
+
+    std::vector<Result> results;
+
+    std::cout << "Running queries on " << connections << " connections "
+              << "using " << threads << " threads "
+              << "for " << duration << " seconds." << std::endl
+              << "..." << std::endl;
+
+    for(auto& worker : workers)
+        worker(std::chrono::seconds(duration));
+
+    for(auto& worker : workers)
+    {
+        worker.result.wait();
+        results.push_back(worker.result.get());
+    }
+
+    auto start = std::min_element(results.begin(), results.end(),
+        [](auto a, auto b) { return a.start < b.start; })->start;
+
+    auto end = std::max_element(results.begin(), results.end(),
+        [](auto a, auto b) { return a.end < b.end; })->end;
+
+    auto requests = std::accumulate(results.begin() + 1, results.end(),
+        results[0].requests, [](auto acc, auto r) { return acc + r.requests; });
+
+    auto elapsed = (end - start).count() / 1.0e9;
+
+    std::cout << "Total of " << requests << " requests in "
+              << elapsed  << "s." << std::endl
+              << "Requests/sec: " << int(requests / elapsed)
+              << "." << std::endl;
+
+    return 0;
+}
diff --git a/examples/index.cpp b/examples/index.cpp
new file mode 100644
index 000000000..655d533ac
--- /dev/null
+++ b/examples/index.cpp
@@ -0,0 +1,59 @@
+#include <iostream>
+
+#include "transactions/engine.hpp"
+#include "mvcc/version_list.hpp"
+#include "storage/vertex.hpp"
+
+#include "storage/indexes/property_index.hpp"
+
+using std::cout;
+using std::endl;
+
+using Record = mvcc::VersionList<Vertex>;
+
+tx::Engine engine;
+Index<Vertex>* index = new PropertyIndex<Vertex>();
+
+Record* create(int id, tx::Transaction& t)
+{
+    auto v = new record_t(id);
+    auto a = v->access(t);
+    a.insert();
+
+    return v;
+}
+
+auto insert(int id, tx::Transaction& t)
+{
+    auto r = create(id, t);
+    return index.insert(&r->id, r, t);
+}
+
+int main(void)
+{
+    /* v1->data.props.set<String>("name", "buda"); */
+    /* v1->data.props.set<Int32>("age", 23); */
+
+    auto& t1 = engine.begin();
+    insert(0, t1);
+    insert(1, t1);
+    insert(2, t1);
+    insert(3, t1);
+    insert(6, t1);
+    insert(7, t1);
+    t1.commit();
+
+    auto& t2 = engine.begin();
+    insert(4, t2);
+    insert(5, t2);
+    insert(8, t2);
+    t2.commit();
+
+    auto& t3 = engine.begin();
+    auto cursor = index.scan(8, t3);
+
+    for(; not cursor.end(); cursor++)
+        cout << cursor->id() << endl;
+
+    return 0;
+}
diff --git a/examples/proptest.cpp b/examples/proptest.cpp
index a4b531677..a6aeb53a0 100644
--- a/examples/proptest.cpp
+++ b/examples/proptest.cpp
@@ -36,6 +36,12 @@ int main(void)
 
     props.set<Float>("pi", z);
 
+    cout << props.at("awesome") << endl;
+    cout << props.at("lame") << endl;
+    cout << props.at("age") << endl;
+    cout << props.at("pi") << endl;
+    cout << props.at("lol") << endl;
+
     StringBuffer buffer;
     JsonWriter<StringBuffer> writer(buffer);
 
diff --git a/examples/strcmp.cpp b/examples/strcmp.cpp
new file mode 100644
index 000000000..02caf959c
--- /dev/null
+++ b/examples/strcmp.cpp
@@ -0,0 +1,27 @@
+#include <iostream>
+#include <fstream>
+#include <cstring>
+std::string load_file(const std::string& fname)
+{
+    std::ifstream in(fname);
+    return std::string((std::istreambuf_iterator<char>(in)),
+                        std::istreambuf_iterator<char>());
+}
+
+
+int main(int argc, const char* argv[])
+{
+    if(argc < 3)
+        return -1;
+
+    auto a = load_file(argv[1]);
+    auto b = load_file(argv[2]);
+
+    bool result = true;
+
+    for(size_t i = 0; i < a.size(); ++i)
+        result &= strcmp(a.c_str() + i, b.c_str() + i) == 0;
+
+    std::cout << result << std::endl;
+    return 0;
+}
diff --git a/examples/test.cpp b/examples/test.cpp
new file mode 100644
index 000000000..1bcf3c612
--- /dev/null
+++ b/examples/test.cpp
@@ -0,0 +1,24 @@
+#include <iostream>
+
+class A
+{
+public:
+    class B;
+
+};
+
+class A::B
+{
+public:
+
+};
+
+int main(void)
+{
+    A a;
+    A::B b;
+
+
+
+    return 0;
+}
diff --git a/http/connection.hpp b/http/connection.hpp
index ea72e1044..e385e440f 100644
--- a/http/connection.hpp
+++ b/http/connection.hpp
@@ -1,23 +1,16 @@
 #pragma once
 
-#include "io/network/tcp_stream.hpp"
+#include "io/network/tcp/stream.hpp"
+#include "memory/literals.hpp"
 
-namespace htpp
+namespace http
 {
-using memory::literals::operator "" _kB;
+using namespace memory::literals;
 
-class Connection
+template <class Req, class Res>
+class Connection : public io::tcp::Stream
 {
-    Connection(io::Socket&& socket) : stream(std::move(socket))
-    {
-        stream.data = this;
-    }
-
-    void close()
-    {
-        delete reinterpret_cast<Connection*>(stream.data);
-    }
-
+public:
     struct Buffers
     {
         char headers[8_kB];
@@ -26,11 +19,14 @@ class Connection
         static constexpr size_t size = sizeof headers + sizeof body;
     };
 
+    Connection(io::Socket&& socket) : io::tcp::Stream(std::move(socket)),
+        response(this->socket) {}
+
     // tcp stream reads into these buffers
     Buffers buffers;
 
-    // tcp stream this connection reads from
-    io::TcpStream stream;
+    Req request;
+    Res response;
 };
 
 }
diff --git a/http/headers.hpp b/http/headers.hpp
new file mode 100644
index 000000000..302d364ad
--- /dev/null
+++ b/http/headers.hpp
@@ -0,0 +1,37 @@
+#pragma once
+
+#include <array>
+
+#include "utils/string/weak_string.hpp"
+
+namespace http
+{
+
+class Headers
+{
+    static constexpr WeakString empty = WeakString();
+
+public:
+    struct Header
+    {
+        WeakString key;
+        WeakString value;
+    };
+
+    std::pair<bool, WeakString&> operator[](const std::string& key)
+    {
+        auto ww = WeakString();
+
+        for(auto& header : headers)
+            if(key == header.key)
+                return {true, header.value};
+
+        return {false, ww};
+    }
+
+private:
+    std::array<Header, 64> headers;
+};
+
+
+}
diff --git a/http/parser_test.cpp b/http/parser_test.cpp
index 41b951fce..8e3841181 100644
--- a/http/parser_test.cpp
+++ b/http/parser_test.cpp
@@ -17,7 +17,7 @@ int main(void)
     io::TcpServer<http::Worker> server;
 
     server.bind("0.0.0.0", "7474").listen(8, 128, []() {
-            std::cout << "response!" << std::endl;
+        std::cout << "response!" << std::endl;
     });
 
     return 0;
diff --git a/http/request.hpp b/http/request.hpp
new file mode 100644
index 000000000..482408658
--- /dev/null
+++ b/http/request.hpp
@@ -0,0 +1,13 @@
+#pragma once
+
+namespace http
+{
+
+class Request
+{
+public:
+    Request() = default;
+
+};
+
+}
diff --git a/http/response.hpp b/http/response.hpp
new file mode 100644
index 000000000..aa10db30f
--- /dev/null
+++ b/http/response.hpp
@@ -0,0 +1,61 @@
+#pragma once
+
+#include <array>
+
+#include "io/network/socket.hpp"
+#include "memory/literals.hpp"
+
+namespace http
+{
+using namespace memory::literals;
+
+class Response
+{
+    template <size_t capacity = 64_kB>
+    struct Buffer
+    {
+    public:
+        Buffer() = default;
+
+        size_t write(const char* data, size_t n)
+        {
+            ::memcpy(buf + len, data, std::max(n, capacity - len));
+        }
+
+        char buf[capacity];
+        size_t len {0};
+    };
+
+public:
+    Response(io::Socket& socket) : socket(socket) {}
+
+    Response& write(const char* data, size_t len)
+    {
+        /* while(true) */
+        /* { */
+        /*     auto bytes_written = buffer.write(data, len); */
+
+        /*     if(len - bytes_written == 0) */
+        /*         break; */
+
+        /*     len -= bytes_written, data += bytes_written; */
+
+        /*     char* ptr = buffer.buf; */
+
+        /*     socket.write(p */
+
+        /* } */
+
+
+
+        return *this;
+    }
+
+private:
+    io::Socket& socket;
+    Buffer<64_kB> buffer;
+
+    /* void flush_socket() */
+};
+
+}
diff --git a/http/worker.hpp b/http/worker.hpp
index 029375105..86640e33f 100644
--- a/http/worker.hpp
+++ b/http/worker.hpp
@@ -1,64 +1,61 @@
 #pragma once
 
-#include "io/network/tcp_reader.hpp"
+#include "io/network/server.hpp"
 #include "debug/log.hpp"
+#include "connection.hpp"
 
 namespace http
 {
 
-const char* body = "Now that the internet can be accessed on any mobile device, whether a laptop, desktop, tablets, smartphone, websites are designed in responsive web version. It is the ability to change the page and font size according to the screen size of the user. desktop, tablets, smartphone, websites are designed in responsive web version. It is the ability to change the page and font size according to the screen size of the user. Thus a website is accessible anytime on any instrument. CSS3 frameworks were widely accepted in 2014 and is growing in 2015. It reduces time and money by helping not creating different sites for different users";
+/* const char* body = "Now that the internet can be accessed on any mobile device, whether a laptop, desktop, tablets, smartphone, websites are designed in responsive web version. It is the ability to change the page and font size according to the screen size of the user. desktop, tablets, smartphone, websites are designed in responsive web version. It is the ability to change the page and font size according to the screen size of the user. Thus a website is accessible anytime on any instrument. CSS3 frameworks were widely accepted in 2014 and is growing in 2015. It reduces time and money by helping not creating different sites for different users"; */
 
-std::string response = "HTTP/1.1 200 OK\r\nContent-Length:"
-    + std::to_string(strlen(body)) + "\r\n\r\n" + body;
+/* const char* body = ""; */
 
-class Worker : public io::TcpReader<Worker>
+std::string response = "HTTP/1.1 200 OK\r\nContent-Length:0\r\n\r\n";
+
+template <class Req, class Res>
+class Parser : public io::Server<Parser<Req, Res>, Connection<Req, Res>>
 {
+    using Connection = Connection<Req, Res>;
+    using Buffer = typename io::StreamReader<Parser<Req, Res>, Connection>::Buffer;
+
 public:
     char buf[65536];
 
-    Worker() = default;
+    Parser() = default;
 
-    io::TcpStream& on_connect(io::Socket&& socket)
+    Connection& on_connect(io::Socket&& socket)
     {
-        auto stream = new io::TcpStream(std::move(socket));
+        auto stream = new Connection(std::move(socket));
         LOG_DEBUG("on_connect socket " << stream->id());
 
         return *stream;
     }
 
-    void on_error(io::TcpStream& stream)
+    void on_error(Connection& conn)
     {
-        LOG_DEBUG("on_error: " << stream.id());
+        LOG_DEBUG("on_error: " << conn.id());
     }
 
-    void on_wait_timeout()
-    {
-        LOG_DEBUG("Worker on_wait_timeout");
-    }
+    void on_wait_timeout() {}
 
-    Buffer on_alloc(size_t suggested_size)
+    Buffer on_alloc(Connection& conn)
     {
-        LOG_DEBUG("Allocating buffer");
-
+        LOG_DEBUG("on_alloc socket " << conn.id());
         return Buffer { buf, sizeof buf };
     }
 
-    void on_read(io::TcpStream& stream, Buffer& buf)
+    void on_read(Connection& conn, Buffer& buf)
     {
-        LOG_DEBUG("on_read (socket: " << stream.id() <<  "): '"
-            << std::string(buf.ptr, buf.len) << "'");
-
-        auto& socket = stream.socket;
-
-        auto n = write(socket, response.c_str(), response.size());
-
-        LOG_DEBUG("Responded with " << n << " characters");
+        LOG_DEBUG("on_read socket " << conn.id());
+        auto& socket = conn.socket;
+        socket.write(response.c_str(), response.size());
     }
 
-    void on_close(io::TcpStream& stream)
+    void on_close(Connection& conn)
     {
-        LOG_DEBUG("on_close: " << stream.id());
-        stream.close();
+        LOG_DEBUG("on_close socket " << conn.id());
+        conn.close();
     }
 };
 
diff --git a/io/network/client.hpp b/io/network/client.hpp
new file mode 100644
index 000000000..ec2ea102d
--- /dev/null
+++ b/io/network/client.hpp
@@ -0,0 +1,35 @@
+#pragma once
+
+#include "stream_reader.hpp"
+
+namespace io
+{
+
+template <class Derived, class Stream>
+class Client : public StreamReader<Derived, Stream>
+{
+public:
+    bool connect(const std::string& host, const std::string& port)
+    {
+        return connect(host.c_str(), port.c_str());
+    }
+
+    bool connect(const char* host, const char* port)
+    {
+        auto socket = io::Socket::connect(host, port);
+
+        if(!socket.is_open())
+            return false;
+
+        socket.set_non_blocking();
+
+        auto& stream = this->derived().on_connect(std::move(socket));
+
+        stream.event.events = EPOLLIN | EPOLLET | EPOLLRDHUP;
+        this->add(stream);
+
+        return true;
+    }
+};
+
+}
diff --git a/io/network/tcp_listener.hpp b/io/network/event_listener.hpp
similarity index 70%
rename from io/network/tcp_listener.hpp
rename to io/network/event_listener.hpp
index ea8b3c256..b536b437a 100644
--- a/io/network/tcp_listener.hpp
+++ b/io/network/event_listener.hpp
@@ -2,22 +2,21 @@
 
 #include "socket.hpp"
 #include "epoll.hpp"
-#include "tcp_stream.hpp"
-
 #include "utils/crtp.hpp"
 
 namespace io
 {
 
-template <class Derived, size_t max_events = 64, int wait_timeout = -1>
-class TcpListener : public Crtp<Derived>
+template <class Derived, class Stream,
+          size_t max_events = 64, int wait_timeout = -1>
+class EventListener : public Crtp<Derived>
 {
 public:
     using Crtp<Derived>::derived;
 
-    TcpListener(uint32_t flags = 0) : listener(flags) {}
+    EventListener(uint32_t flags = 0) : listener(flags) {}
 
-    void add(TcpStream& stream)
+    void add(Stream& stream)
     {
         // add the stream to the event listener
         listener.add(stream.socket, &stream.event);
@@ -30,15 +29,18 @@ public:
         // wait_timeout milliseconds. if wait_timeout is achieved, returns 0
         auto n = listener.wait(events, max_events, wait_timeout);
 
+        LOG_DEBUG("received " << n << " events");
+
         // go through all events and process them in order
         for(int i = 0; i < n; ++i)
         {
             auto& event = events[i];
-            auto& stream = *reinterpret_cast<TcpStream*>(event.data.ptr);
+            auto& stream = *reinterpret_cast<Stream*>(event.data.ptr);
 
-            // a tcp stream was closed
+            // a stream was closed
             if(UNLIKELY(event.events & EPOLLRDHUP))
             {
+                LOG_DEBUG("EPOLLRDHUP event recieved on socket " << stream.id());
                 this->derived().on_close(stream);
                 continue;
             }
@@ -47,10 +49,16 @@ public:
             if(UNLIKELY(!(event.events & EPOLLIN) ||
                           event.events & (EPOLLHUP | EPOLLERR)))
             {
+                LOG_DEBUG(">> EPOLL ERR");
+                LOG_DEBUG("EPOLLIN" << (event.events & EPOLLIN));
+                LOG_DEBUG("EPOLLHUP" << (event.events & EPOLLHUP));
+                LOG_DEBUG("EPOLLERR" << (event.events & EPOLLERR));
+
                 this->derived().on_error(stream);
                 continue;
             }
 
+            LOG_DEBUG("signalling that data exists on socket " << stream.id());
             // we have some data waiting to be read
             this->derived().on_data(stream);
         }
diff --git a/io/network/server.hpp b/io/network/server.hpp
new file mode 100644
index 000000000..8467086d0
--- /dev/null
+++ b/io/network/server.hpp
@@ -0,0 +1,37 @@
+#pragma once
+
+#include "stream_reader.hpp"
+
+namespace io
+{
+
+template <class Derived, class Stream>
+class Server : public StreamReader<Derived, Stream>
+{
+public:
+    bool accept(Socket& socket)
+    {
+        // accept a connection from a socket
+        auto s = socket.accept(nullptr, nullptr);
+        LOG_DEBUG("socket " << s.id() << " accepted");
+
+        if(!s.is_open())
+            return false;
+
+        // make the recieved socket non blocking
+        s.set_non_blocking();
+
+        auto& stream = this->derived().on_connect(std::move(s));
+
+        // we want to listen to an incoming event which is edge triggered and
+        // we also want to listen on the hangup event
+        stream.event.events = EPOLLIN | EPOLLET | EPOLLRDHUP;
+
+        // add the connection to the event listener
+        this->add(stream);
+
+        return true;
+    }
+};
+
+}
diff --git a/io/network/socket.hpp b/io/network/socket.hpp
index 47725d007..d8ab88601 100644
--- a/io/network/socket.hpp
+++ b/io/network/socket.hpp
@@ -56,6 +56,34 @@ public:
         return socket != -1;
     }
 
+    static Socket connect(const std::string& addr, const std::string& port)
+    {
+        return connect(addr.c_str(), port.c_str());
+    }
+
+    static Socket connect(const char* addr, const char* port)
+    {
+        auto info = AddrInfo::get(addr, port);
+
+        for(struct addrinfo* it = info; it != nullptr; it = it->ai_next)
+        {
+            auto s = Socket(it->ai_family, it->ai_socktype, it->ai_protocol);
+
+            if(!s.is_open())
+                continue;
+
+            if(::connect(s, it->ai_addr, it->ai_addrlen) == 0)
+                return std::move(s);
+        }
+
+        throw NetworkError("Unable to connect to socket");
+    }
+
+    static Socket bind(const std::string& addr, const std::string& port)
+    {
+        return bind(addr.c_str(), port.c_str());
+    }
+
     static Socket bind(const char* addr, const char* port)
     {
         auto info = AddrInfo::get(addr, port);
@@ -113,6 +141,21 @@ public:
         return socket;
     }
 
+    size_t write(const std::string& str)
+    {
+        return ::write(socket, str.c_str(), str.size());
+    }
+
+    size_t write(const char* data, size_t len)
+    {
+        return ::write(socket, data, len);
+    }
+
+    size_t read(char* buffer, size_t len)
+    {
+        return ::read(socket, buffer, len);
+    }
+
 private:
     int socket;
 };
diff --git a/io/network/stream_reader.hpp b/io/network/stream_reader.hpp
new file mode 100644
index 000000000..3fad770f8
--- /dev/null
+++ b/io/network/stream_reader.hpp
@@ -0,0 +1,60 @@
+#pragma once
+
+#include "event_listener.hpp"
+#include "memory/literals.hpp"
+
+namespace io
+{
+using namespace memory::literals;
+
+template <class Derived, class Stream>
+class StreamReader : public EventListener<Derived, Stream>
+{
+public:
+    struct Buffer
+    {
+        char* ptr;
+        size_t len;
+    };
+
+    StreamReader(uint32_t flags = 0) : EventListener<Derived, Stream>(flags) {}
+
+    void on_data(Stream& stream)
+    {
+        while(true)
+        {
+            // allocate the buffer to fill the data
+            auto buf = this->derived().on_alloc(stream);
+
+            // read from the buffer at most buf.len bytes
+            buf.len = stream.socket.read(buf.ptr, buf.len);
+
+            // check for read errors
+            if(buf.len == -1)
+            {
+                // this means we have read all available data
+                if(LIKELY(errno == EAGAIN))
+                {
+                    LOG_DEBUG("EAGAIN read all data on socket " << stream.id());
+                    break;
+                }
+
+                // some other error occurred, check errno
+                this->derived().on_error(stream);
+            }
+
+            // end of file, the client has closed the connection
+            if(UNLIKELY(buf.len == 0))
+            {
+                LOG_DEBUG("EOF stream closed on socket " << stream.id());
+                stream.close();
+                break;
+            }
+
+            LOG_DEBUG("data on socket " << stream.id());
+            this->derived().on_read(stream, buf);
+        }
+    }
+};
+
+}
diff --git a/io/network/tcp/stream.hpp b/io/network/tcp/stream.hpp
new file mode 100644
index 000000000..c4b1af76c
--- /dev/null
+++ b/io/network/tcp/stream.hpp
@@ -0,0 +1,39 @@
+#pragma once
+
+#include "io/network/epoll.hpp"
+#include "io/network/socket.hpp"
+
+namespace io
+{
+namespace tcp
+{
+
+class Stream
+{
+public:
+    Stream(Socket&& socket) : socket(std::move(socket))
+    {
+        // save this to epoll event data baton to access later
+        event.data.ptr = this;
+    }
+
+    Stream(Stream&& stream)
+    {
+        socket = std::move(stream.socket);
+        event = stream.event;
+        event.data.ptr = this;
+    }
+
+    void close()
+    {
+        delete reinterpret_cast<Stream*>(event.data.ptr);
+    }
+
+    int id() const { return socket.id(); }
+
+    Socket socket;
+    Epoll::Event event;
+};
+
+}
+}
diff --git a/io/network/tcp_reader.hpp b/io/network/tcp_reader.hpp
deleted file mode 100644
index fec242509..000000000
--- a/io/network/tcp_reader.hpp
+++ /dev/null
@@ -1,91 +0,0 @@
-#pragma once
-
-#include "tcp_listener.hpp"
-#include "tcp_stream.hpp"
-
-namespace io
-{
-
-template <class Derived>
-class TcpReader : public TcpListener<TcpReader<Derived>>, public Crtp<Derived>
-{
-    using listener_t = TcpListener<TcpReader<Derived>>;
-    using Crtp<Derived>::derived;
-
-public:
-    TcpReader() : listener_t(0) {}
-
-    struct Buffer
-    {
-        char* ptr;
-        size_t len;
-    };
-
-    bool accept(Socket& socket)
-    {
-        // accept a connection from a socket
-        auto s = socket.accept(nullptr, nullptr);
-
-        if(!s.is_open())
-            return false;
-
-        // make the recieved socket non blocking
-        s.set_non_blocking();
-
-        auto& stream = derived().on_connect(std::move(s));
-
-        // we want to listen to an incoming event whish is edge triggered and
-        // we also want to listen on the hangup event
-        stream.event.events = EPOLLIN | EPOLLET | EPOLLRDHUP;
-
-        // add the connection to the event listener
-        this->add(stream);
-
-        return true;
-    }
-
-    void on_close(TcpStream& stream)
-    {
-        derived().on_close(stream);
-    }
-
-    void on_error(TcpStream& stream)
-    {
-        derived().on_error(stream);
-    }
-
-    void on_wait_timeout()
-    {
-        derived().on_wait_timeout();
-    }
-
-    void on_data(TcpStream& stream)
-    {
-        constexpr size_t suggested_size = 64_kB;
-
-        while(true)
-        {
-            Buffer buf = derived().on_alloc(suggested_size);
-
-            buf.len = read(stream.socket, buf.ptr, buf.len);
-
-            if(buf.len == -1)
-            {
-                if(UNLIKELY(errno != EAGAIN))
-                    derived().on_error(stream);
-
-                break;
-            }
-
-            if(UNLIKELY(buf.len == 0))
-            {
-                stream.close();
-                break;
-            }
-
-            derived().on_read(stream, buf);
-        }
-    }
-};
-
-}
diff --git a/io/network/tcp_stream.hpp b/io/network/tcp_stream.hpp
deleted file mode 100644
index 3e7e5368c..000000000
--- a/io/network/tcp_stream.hpp
+++ /dev/null
@@ -1,33 +0,0 @@
-#pragma once
-
-#include "epoll.hpp"
-#include "socket.hpp"
-
-#include "memory/literals.hpp"
-
-namespace io
-{
-
-class TcpStream
-{
-public:
-    TcpStream(Socket&& socket) : socket(std::move(socket))
-    {
-        event.data.ptr = this;
-    }
-
-    void close()
-    {
-        delete reinterpret_cast<TcpStream*>(event.data.ptr);
-    }
-
-    int id() const { return socket.id(); }
-
-    Socket socket;
-    Epoll::Event event;
-
-    // custom data we can pass on
-    void* data;
-};
-
-}
diff --git a/io/network/test b/io/network/test
deleted file mode 100755
index 0e8abce12..000000000
Binary files a/io/network/test and /dev/null differ
diff --git a/io/network/test.cpp b/io/network/test.cpp
index 73a96090c..f79be771a 100644
--- a/io/network/test.cpp
+++ b/io/network/test.cpp
@@ -3,15 +3,20 @@
 #include <thread>
 #include <signal.h>
 
-std::hash<std::thread::id> hash;
-
 #include "debug/log.hpp"
 
+#include "http/request.hpp"
+#include "http/response.hpp"
+
 #include "socket.hpp"
 #include "http/worker.hpp"
 
-std::array<http::Worker, 16> workers;
-std::array<std::thread, 16> threads;
+std::hash<std::thread::id> hash;
+
+constexpr unsigned K = 128;
+
+std::array<http::Parser<http::Request, http::Response>, K> workers;
+std::array<std::thread, K> threads;
 
 std::atomic<bool> alive { true };
 
@@ -24,25 +29,24 @@ void sigint_handler(int)
 {
 
     exiting();
-    std::abort();
+    std::exit(0);
 }
 
 #define MAXEVENTS 64
 
 int main(void)
 {
-    std::atexit(exiting);
+    //std::atexit(exiting);
     signal(SIGINT, sigint_handler);
 
     for(size_t i = 0; i < workers.size(); ++i)
     {
         auto& w = workers[i];
 
-        threads[i] = std::thread([&w]() {
+        threads[i] = std::thread([i, &w]() {
             while(alive)
             {
-                LOG_DEBUG("Worker " << hash(std::this_thread::get_id())
-                          << " waiting... ");
+                LOG_DEBUG("waiting for events on thread " << i);
                 w.wait_and_process_events();
             }
         });
@@ -93,8 +97,11 @@ int main(void)
     {
       int n, i;
 
+      LOG_DEBUG("acceptor waiting for events");
       n = epoll_wait (efd, events, MAXEVENTS, -1);
 
+      LOG_DEBUG("acceptor recieved " << n << " connection requests");
+
       for (i = 0; i < n; i++)
 	{
 	  if ((events[i].events & EPOLLERR) ||
@@ -109,123 +116,20 @@ int main(void)
 	    }
 
 	  else if (socket == events[i].data.fd)
-	    {
+      {
               /* We have a notification on the listening socket, which
                  means one or more incoming connections. */
               while (true)
               {
-                  LOG_DEBUG("Trying to accept... ");
+                  LOG_DEBUG("trying to accept connection on thread " << idx);
                   if(!workers[idx].accept(socket))
-                  {
-                      LOG_DEBUG("Did not accept!");
                       break;
-                  }
 
+                  LOG_DEBUG("Accepted a new connection on thread " << idx);
                   idx = (idx + 1) % workers.size();
-                  LOG_DEBUG("Accepted a new connection!");
+                  break;
               }
-
-                  /* struct sockaddr in_addr; */
-                  /* socklen_t in_len; */
-                  /* int infd; */
-                  /* char hbuf[NI_MAXHOST], sbuf[NI_MAXSERV]; */
-
-                  /* in_len = sizeof in_addr; */
-                  /* infd = accept (socket, &in_addr, &in_len); */
-                  /* if (infd == -1) */
-                  /*   { */
-                  /*     if ((errno == EAGAIN) || */
-                  /*         (errno == EWOULDBLOCK)) */
-                  /*       { */
-                  /*         /1* We have processed all incoming */
-                  /*            connections. *1/ */
-                  /*         break; */
-                  /*       } */
-                  /*     else */
-                  /*       { */
-                  /*         perror ("accept"); */
-                  /*         break; */
-                  /*       } */
-                  /*   } */
-
-                  /* s = getnameinfo (&in_addr, in_len, */
-                  /*                  hbuf, sizeof hbuf, */
-                  /*                  sbuf, sizeof sbuf, */
-                  /*                  NI_NUMERICHOST | NI_NUMERICSERV); */
-                  /* if (s == 0) */
-                  /*   { */
-                  /*     printf("Accepted connection on descriptor %d " */
-                  /*            "(host=%s, port=%s)\n", infd, hbuf, sbuf); */
-                  /*   } */
-
-                  /* /1* Make the incoming socket non-blocking and add it to the */
-                  /*    list of fds to monitor. *1/ */
-                  /* s = make_socket_non_blocking (infd); */
-                  /* if (s == -1) */
-                  /*   abort (); */
-
-                  /* event.data.fd = infd; */
-                  /* event.events = EPOLLIN | EPOLLET; */
-                  /* s = epoll_ctl (efd, EPOLL_CTL_ADD, infd, &event); */
-                  /* if (s == -1) */
-                  /*   { */
-                  /*     perror ("epoll_ctl"); */
-                  /*     abort (); */
-                  /*   } */
             }
-          /* else */
-          /*   { */
-          /*     /1* We have data on the fd waiting to be read. Read and */
-          /*        display it. We must read whatever data is available */
-          /*        completely, as we are running in edge-triggered mode */
-          /*        and won't get a notification again for the same */
-          /*        data. *1/ */
-          /*     int done = 0; */
-
-          /*     while (1) */
-          /*       { */
-          /*         ssize_t count; */
-          /*         char buf[512]; */
-
-          /*         count = read (events[i].data.fd, buf, sizeof buf); */
-          /*         if (count == -1) */
-          /*           { */
-          /*             /1* If errno == EAGAIN, that means we have read all */
-          /*                data. So go back to the main loop. *1/ */
-          /*             if (errno != EAGAIN) */
-          /*               { */
-          /*                 perror ("read"); */
-          /*                 done = 1; */
-          /*               } */
-          /*             break; */
-          /*           } */
-          /*         else if (count == 0) */
-          /*           { */
-          /*             /1* End of file. The remote has closed the */
-          /*                connection. *1/ */
-          /*             done = 1; */
-          /*             break; */
-          /*           } */
-
-          /*         /1* Write the buffer to standard output *1/ */
-          /*         s = write (1, buf, count); */
-          /*         if (s == -1) */
-          /*           { */
-          /*             perror ("write"); */
-          /*             abort (); */
-          /*           } */
-          /*       } */
-
-          /*     if (done) */
-          /*       { */
-          /*         printf ("Closed connection on descriptor %d\n", */
-          /*                 events[i].data.fd); */
-
-          /*         /1* Closing the descriptor will make epoll remove it */
-          /*            from the set of descriptors which are monitored. *1/ */
-          /*         close (events[i].data.fd); */
-          /*       } */
-          /*   } */
         }
     }
 
diff --git a/io/network/worker.hpp b/io/network/worker.hpp
index 9e7692bb2..b50610525 100644
--- a/io/network/worker.hpp
+++ b/io/network/worker.hpp
@@ -11,7 +11,7 @@ namespace io
 
 class Worker : public Listener<Worker>
 {
-    char buf[512];
+    char buf[64_kB];
 
 public:
     using Listener::Listener;
diff --git a/speedy/http/httpconnection.hpp b/speedy/http/httpconnection.hpp
index dcc3afbcb..0da5949dc 100644
--- a/speedy/http/httpconnection.hpp
+++ b/speedy/http/httpconnection.hpp
@@ -31,7 +31,7 @@ public:
     uv::TcpStream client;
 
     parser_t parser;
-    
+
     Req request;
     Res response;
 
diff --git a/speedy/http/response.inl b/speedy/http/response.inl
index bc142449e..b4235f43b 100644
--- a/speedy/http/response.inl
+++ b/speedy/http/response.inl
@@ -42,7 +42,7 @@ void Response<Req, Res>::send(const std::string& body)
 
     buffer << "\r\n" << body;
 
-    uv_write(write_req, connection.client, buffer, buffer.count(), 
+    uv_write(write_req, connection.client, buffer, buffer.count(),
             [](uv_write_t* write_req, int) {
 
         connection_t& conn = *reinterpret_cast<connection_t*>(write_req->data);
diff --git a/utils/command_line/arguments.hpp b/utils/command_line/arguments.hpp
index 1b3094b5d..4b3979ebc 100644
--- a/utils/command_line/arguments.hpp
+++ b/utils/command_line/arguments.hpp
@@ -7,38 +7,30 @@
 namespace
 {
 
-using std::string;
-using std::vector;
-using vector_str = vector<string>;
-
 #pragma clang diagnostic push
 #pragma clang diagnostic ignored "-Wunused-function"
 
-decltype(auto) all_arguments(int argc, char *argv[])
+auto all_arguments(int argc, char *argv[])
 {
-    vector_str args(argv + 1, argv + argc);
-    return args;
+    return std::vector<std::string>(argv + 1, argv + argc);
 }
 
-bool contain_argument(const vector_str& all, const string& flag)
+bool contains_argument(const std::vector<std::string>& all,
+                       const std::string& flag)
 {
-    // TODO: optimize this implementation
-    auto it = std::find(all.begin(), all.end(), flag);
-    if (it == all.end())
-        return false;
-    return true;
+    return std::find(all.begin(), all.end(), flag) != all.end();
 }
 
-decltype(auto) get_argument(const vector_str& all,
-                            const std::string& flag,
-                            const std::string& default_value)
+auto get_argument(const std::vector<std::string>& all,
+                  const std::string& flag,
+                  const std::string& default_value)
 {
-    // TODO: optimize this implementation
     auto it = std::find(all.begin(), all.end(), flag);
-    if (it == all.end())
+
+    if(it == all.end())
         return default_value;
-    auto pos = std::distance(all.begin(), it);
-    return all[pos + 1];
+
+    return all[std::distance(all.begin(), it) + 1];
 }
 
 #pragma clang diagnostic pop
diff --git a/utils/string/streq.hpp b/utils/string/streq.hpp
new file mode 100644
index 000000000..071cfaa85
--- /dev/null
+++ b/utils/string/streq.hpp
@@ -0,0 +1,37 @@
+#pragma once
+
+#include <x86intrin.h>
+
+namespace sse42
+{
+constexpr int strcmp_mode = _SIDD_CMP_EQUAL_EACH
+                          | _SIDD_NEGATIVE_POLARITY;
+
+constexpr unsigned CF = 0x1;
+constexpr unsigned ZF = 0x40;
+
+bool streq(const char* lhs, const char* rhs)
+{
+    int idx, eflags;
+
+    while(true)
+    {
+        auto lhs_mm = _mm_loadu_si128((__m128i*)lhs);
+        auto rhs_mm = _mm_loadu_si128((__m128i*)rhs);
+
+        idx = _mm_cmpistri(lhs_mm, rhs_mm, strcmp_mode);
+        eflags = __readeflags();
+
+        if(idx != 0x10)
+            return false;
+
+        if((eflags & (ZF | CF)) != 0)
+            return true;
+
+        lhs += 16;
+        rhs += 16;
+    }
+}
+
+}
+
diff --git a/utils/string/weak_string.hpp b/utils/string/weak_string.hpp
new file mode 100644
index 000000000..17189eff1
--- /dev/null
+++ b/utils/string/weak_string.hpp
@@ -0,0 +1,86 @@
+#pragma once
+
+#include <cstring>
+#include <cassert>
+#include <string>
+
+#include "utils/total_ordering.hpp"
+#include "utils/total_ordering_with.hpp"
+
+class WeakString
+{
+public:
+    constexpr WeakString() : str(nullptr), len(0) {}
+
+    WeakString(const std::string& str) : str(str.c_str()), len(str.size()) {}
+
+    WeakString(const char* str) : str(str), len(strlen(str)) {}
+
+    constexpr WeakString(const char* str, size_t len) : str(str), len(len) {}
+
+    const char& operator[](size_t idx) const
+    {
+        assert(idx < len);
+        return str[idx];
+    }
+
+    const char& front() const
+    {
+        assert(len > 0);
+        return str[0];
+    }
+
+    const char& back() const
+    {
+        assert(len > 0);
+        return str[len - 1];
+    }
+
+    const char* data() const
+    {
+        return str;
+    }
+
+    bool empty() const
+    {
+        return len == 0;
+    }
+
+    size_t size() const
+    {
+        return len;
+    }
+
+    size_t length() const
+    {
+        return size();
+    }
+
+    std::string to_string() const
+    {
+        return std::string(str, len);
+    }
+
+    friend bool operator==(const WeakString& lhs, const WeakString rhs)
+    {
+        // oh dear god, make this better with custom iterators
+        if(lhs.size() != rhs.size())
+            return false;
+
+        for(size_t i = 0; i < lhs.size(); ++i)
+            if(lhs[i] != rhs[i])
+                return false;
+
+        return true;
+    }
+
+    friend bool operator!=(const WeakString& lhs, const WeakString& rhs)
+    {
+        return !(lhs == rhs);
+    }
+
+
+private:
+    const char* str;
+    size_t len;
+};