diff --git a/www/hope/hope.annotation.js b/www/hope/hope.annotation.js
new file mode 100644
index 0000000..bf0e99a
--- /dev/null
+++ b/www/hope/hope.annotation.js
@@ -0,0 +1,46 @@
+hope.register( 'hope.annotation', function() {
+
+ function hopeAnnotation(range, tag) {
+ this.range = hope.range.create(range);
+ this.tag = tag;
+ Object.freeze(this);
+ }
+
+ hopeAnnotation.prototype.delete = function( range ) {
+ return new hopeAnnotation( this.range.delete( range ), this.tag );
+ };
+
+ hopeAnnotation.prototype.copy = function( range ) {
+ return new hopeAnnotation( this.range.copy( range ), this.tag );
+ };
+
+ hopeAnnotation.prototype.compare = function( annotation ) {
+ return this.range.compare( annotation.range );
+ };
+
+ hopeAnnotation.prototype.has = function( tag ) {
+ //FIXME: should be able to specify attributes and attribute values as well
+ return this.stripTag() == hope.annotation.stripTag(tag);
+ };
+
+ hopeAnnotation.prototype.toString = function() {
+ return this.range + ':' + this.tag;
+ };
+
+ hopeAnnotation.prototype.stripTag = function() {
+ return hope.annotation.stripTag(this.tag);
+ };
+
+ hopeAnnotation.prototype.isBlock = function() {
+ return (hope.render.html.rules.nestingSets.block.indexOf(hope.annotation.stripTag(this.tag)) != -1 ); // FIXME: this should not know about hope.render.html;
+ };
+
+ this.create = function( range, tag ) {
+ return new hopeAnnotation( range, tag );
+ };
+
+ this.stripTag = function(tag) {
+ return tag.split(' ')[0];
+ };
+});
+
diff --git a/www/hope/hope.editor.events.js b/www/hope/hope.editor.events.js
new file mode 100644
index 0000000..c2ab6ff
--- /dev/null
+++ b/www/hope/hope.editor.events.js
@@ -0,0 +1,35 @@
+hope.register('hope.editor.events', function() {
+
+ if ( typeof hope.global.addEventListener != 'undefined' ) {
+ this.listen = function( el, event, callback, capture ) {
+ return el.addEventListener( event, callback, capture );
+ };
+ } else if ( typeof hope.global.attachEvent != 'undefined' ) {
+ this.listen = function( el, event, callback, capture ) {
+ return el.attachEvent( 'on' + event, function() {
+ var evt = hope.global.event;
+ var self = evt.srcElement;
+ if ( !self ) {
+ self = hope.global;
+ }
+ return callback.call( self, evt );
+ } );
+ };
+ } else {
+ throw new hope.Exception( 'Browser is not supported', 'hope.editor.events.1' );
+ }
+
+ this.cancel = function( evt ) {
+ if ( typeof evt.stopPropagation != 'undefined' ) {
+ evt.stopPropagation();
+ }
+ if ( typeof evt.preventDefault != 'undefined' ) {
+ evt.preventDefault();
+ }
+ if ( typeof evt.cancelBubble != 'undefined' ) {
+ evt.cancelBubble = true;
+ }
+ return false;
+ };
+
+} );
\ No newline at end of file
diff --git a/www/hope/hope.editor.js b/www/hope/hope.editor.js
new file mode 100644
index 0000000..97e1c0d
--- /dev/null
+++ b/www/hope/hope.editor.js
@@ -0,0 +1,483 @@
+hope.register( 'hope.editor', function() {
+ var hopeTokenCounter = 0;
+ var browserCountsWhitespace = (function() {
+ var div = document.createElement("DIV");
+ div.innerHTML = "
abc
";
+ document.body.appendChild(div);
+ var range = document.createRange();
+ range.setStart(div.querySelector("div"), 1);
+ var offset1 = range.startOffset;
+ var sel = window.getSelection();
+ sel.removeAllRanges();
+ sel.addRange(range);
+ var newRange = sel.getRangeAt(0);
+ var offset2 = newRange.startOffset;
+ var result = (offset1 == offset2);
+ document.body.removeChild(div);
+ return result;
+ }());
+
+ function unrender(target) {
+ var textValue = '';
+ var tags = [];
+
+ var node;
+
+ var tagStart, tagEnd;
+
+ for (var i in target.childNodes) {
+ if (target.childNodes[i].nodeType == document.ELEMENT_NODE) {
+ if (
+ !target.childNodes[i].hasChildNodes()
+ ) {
+ tagStart = hopeTokenCounter;
+ hopeTokenCounter += 1;
+
+ switch (target.childNodes[i].tagName.toLowerCase()) {
+ case 'br':
+ case 'hr':
+ textValue += "\n";
+ break;
+ case 'img':
+ textValue += "\u00AD"; //
+ break;
+ default:
+ textValue += "\u00AD"; //
+ }
+
+ tagEnd = hopeTokenCounter;
+
+ tags.push({
+ start : tagStart,
+ end : tagEnd,
+ tag : target.childNodes[i].tagName.toLowerCase(),
+ attrs : target.childNodes[i].attributes
+ });
+ } else {
+
+ tagStart = hopeTokenCounter;
+
+ node = this.unrender(target.childNodes[i]);
+ // hopeTokenCounter += node.length;
+ textValue += node.text;
+ tagEnd = hopeTokenCounter;
+
+ tags.push({
+ start : tagStart,
+ end : tagEnd,
+ tag : target.childNodes[i].tagName.toLowerCase(),
+ attrs : target.childNodes[i].attributes,
+ caret : this.getCaretOffset(target.childNodes[i])
+ });
+
+ for (var j=0; j= caret) {
+ try {
+ selection.setStart(node.childNodes[i], caret - nodeOffset);
+ } catch (e) {
+ console.log("Warning: could not set caret position");
+ }
+ node.removeAttribute("data-hope-caret");
+ return selection;
+ }
+ }
+
+ }
+
+ function tagsToText(tags) {
+ var result = '';
+ var i,j;
+
+ for (i=0; i -1) {
+ result += " data-hope-caret=\"" + tags[i].caret + "\"";
+ }
+ result += "\n";
+ }
+ return result;
+ }
+
+ function hopeEditor( textEl, annotationsEl, outputEl, renderEl ) {
+ this.refs = {
+ text: textEl,
+ annotations: annotationsEl,
+ output: outputEl,
+ render: renderEl
+ };
+ this.selection = hope.editor.selection.create(0,0,this);
+ this.commandsKeyUp = {};
+
+ if (this.refs.output.innerHTML !== '') {
+ this.refs.output.innerHTML = this.refs.output.innerHTML.replace(/\/p>/g, "/p>");
+ this.parseHTML();
+ }
+
+ this.browserCountsWhitespace = browserCountsWhitespace;
+
+ var text = this.refs.text.value;
+ var annotations = this.refs.annotations.value;
+ this.fragment = hope.fragment.create( text, annotations );
+ this.refs.output.contentEditable = true;
+ this.update();
+ this.initDone = true;
+// initEvents(this);
+ }
+
+ function initEvents(editor) {
+ hope.events.listen(editor.refs.output, 'keypress', function( evt ) {
+ if ( !evt.ctrlKey && !evt.altKey ) {
+ // check selection length
+ // remove text in selection
+ // add character
+ var range = editor.selection.getRange();
+ var charCode = evt.which;
+
+ if (evt.which) {
+ var charTyped = String.fromCharCode(charCode);
+ if ( charTyped ) { // ignore non printable characters
+ if ( range.length ) {
+ editor.fragment = editor.fragment.delete(range);
+ }
+ editor.fragment = editor.fragment.insert(range.start, charTyped );
+ editor.selection.collapse().move(1);
+ setTimeout( function() {
+ editor.update();
+ }, 0 );
+ }
+ return hope.events.cancel(evt);
+ }
+ }
+ });
+
+ hope.events.listen(editor.refs.output, 'keydown', function( evt ) {
+ var key = hope.keyboard.getKey( evt );
+ if ( editor.commands[key] ) {
+ var range = editor.selection.getRange();
+ editor.commands[key].call(editor, range);
+ setTimeout( function() {
+ editor.update();
+ }, 0);
+ return hope.events.cancel(evt);
+ } else if ( evt.ctrlKey || evt.altKey ) {
+ return hope.events.cancel(evt);
+ }
+ });
+
+ hope.events.listen(editor.refs.output, 'keyup', function( evt ) {
+ var key = hope.keyboard.getKey( evt );
+ if ( editor.selection.cursorCommands.indexOf(key)<0 ) {
+ if ( editor.commandsKeyUp[key] ) {
+ var range = editor.selection.getRange();
+ editor.commandsKeyUp[key].call(editor, range);
+ setTimeout( function() {
+ editor.update();
+ }, 0);
+ }
+ return hope.events.cancel(evt);
+ }
+ });
+
+ }
+
+ hopeEditor.prototype.setCaretOffset = setCaretOffset;
+ hopeEditor.prototype.getCaretOffset = getCaretOffset;
+ hopeEditor.prototype.unrender = unrender;
+
+ hopeEditor.prototype.parseHTML = function() {
+ hopeTokenCounter = 0;
+
+ var data = this.unrender(this.refs.output);
+ this.refs.annotations.value = tagsToText(data.tags);
+ this.refs.text.value = data.text;
+ this.fragment = hope.fragment.create( this.refs.text.value, this.refs.annotations.value );
+ };
+
+ hopeEditor.prototype.getEditorRange = function(start, end ) {
+ var treeWalker = document.createTreeWalker(
+ this.refs.output,
+ NodeFilter.SHOW_TEXT | NodeFilter.SHOW_ELEMENT,
+ function(node) {
+ if (
+ node.nodeType == document.TEXT_NODE ||
+ !node.hasChildNodes()
+ ) {
+ return NodeFilter.FILTER_ACCEPT;
+ } else {
+ return NodeFilter.FILTER_SKIP;
+ }
+ },
+ false
+ );
+ var offset = 0;
+ var node = null;
+ var range = document.createRange();
+ var lastNode = null;
+ do {
+ lastNode = node;
+ node = treeWalker.nextNode();
+ if ( node ) {
+ if (node.nodeType == document.ELEMENT_NODE) {
+ offset += 1;
+ } else {
+ offset += node.textContent.length;
+ }
+ }
+ } while ( offset < start && node );
+ if ( !node ) {
+ if (lastNode) {
+ if (lastNode.nodeType == document.ELEMENT_NODE) {
+ range.setStart(lastNode, 0);
+ range.setEndAfter(lastNode);
+ } else {
+ range.setStart(lastNode, lastNode.textContent ? lastNode.textContent.length : 1 );
+ range.setEnd(lastNode, lastNode.textContent ? lastNode.textContent.length : 1 );
+ }
+ return range;
+ }
+ return false;
+ }
+
+ var preOffset = offset - (node.nodeType == document.TEXT_NODE ? node.textContent.length : 1);
+ var nextNode;
+ if (node.nodeType == document.ELEMENT_NODE) {
+ nextNode = treeWalker.nextNode();
+ treeWalker.previousNode();
+ if (nextNode) {
+ range.setStartBefore(nextNode);
+ } else {
+ range.setStartAfter(node);
+ }
+ } else {
+ if (start-preOffset == node.textContent.length) {
+ nextNode = treeWalker.nextNode();
+ treeWalker.previousNode();
+ if (nextNode) {
+ range.setStartBefore(nextNode);
+ } else {
+ range.setStartAfter(node);
+ }
+ } else {
+ range.setStart(node, start - preOffset );
+ }
+ }
+ while ( offset < end && node ) {
+ node = treeWalker.nextNode();
+ if ( node ) {
+ if (node.nodeType == document.ELEMENT_NODE) {
+ offset += 1;
+ } else {
+ offset += node.textContent.length;
+ }
+ }
+ }
+ if ( !node ) {
+ if (lastNode) {
+ range.setEnd(lastNode, lastNode.textContent ? lastNode.textContent.length : 1 );
+ return range;
+ }
+ return false;
+ }
+
+ preOffset = offset - (node.nodeType == document.TEXT_NODE ? node.textContent.length : 1);
+
+ if (node.nodeType == document.ELEMENT_NODE) {
+ range.setEndAfter(node);
+ } else {
+ range.setEnd(node, end - preOffset );
+ }
+ return range;
+ };
+
+ hopeEditor.prototype.showCursor = function() {
+ var range = this.selection.getRange();
+ var selection = this.getEditorRange(range.start, range.end);
+ var caretElm = document.querySelector('[data-hope-caret]');
+ if (caretElm) {
+ selection = this.setCaretOffset(caretElm);
+ }
+ // remove all other caret attributes from the other elements;
+ var otherCarets = document.querySelectorAll('[data-hope-caret]');
+ for (var i=0; i', '>');
+ }
+ if ( this.refs.annotations ) {
+ this.refs.annotations.value = this.fragment.annotations+'';
+ }
+ };
+
+ hopeEditor.prototype.command = function( key, callback, keyup ) {
+ if ( keyup ) {
+ this.commandsKeyUp[key] = callback;
+ } else {
+ this.commands[key] = callback;
+ }
+ };
+
+ this.create = function( textEl, annotationsEl, outputEl, previewEl ) {
+ return new hopeEditor( textEl, annotationsEl, outputEl, previewEl);
+ };
+
+});
\ No newline at end of file
diff --git a/www/hope/hope.editor.selection.js b/www/hope/hope.editor.selection.js
new file mode 100644
index 0000000..4f650ab
--- /dev/null
+++ b/www/hope/hope.editor.selection.js
@@ -0,0 +1,347 @@
+hope.register( 'hope.editor.selection', function() {
+ function hopeEditorSelection(start, end, editor) {
+ this.start = start;
+ this.end = end;
+ this.editor = editor;
+
+ var self = this;
+
+ var updateRange = function() {
+ var sel = window.getSelection();
+ var rangeStart, rangeEnd;
+ var bestStart, bestEnd;
+
+ if (sel.rangeCount) {
+ for (var i=0; i not a text node
+ var nodeRect = null;
+ var range = null;
+ var rangeRect = null;
+ var yBias = null;
+ // find textnode to place cursor in
+ do {
+ node = this.getNextTextNode(node);
+ if ( node ) {
+ range = document.createRange();
+ range.setStart(node, 0);
+ range.setEnd(node, node.textContent.length);
+ nodeRect = range.getBoundingClientRect();
+ if ( !yBias ) {
+ if ( nodeRect.top > cursorRect.top ) {
+ yBias = nodeRect.top;
+ } else {
+ yBias = cursorRect.top;
+ }
+ }
+ }
+ } while ( node && nodeRect.height!==0 && nodeRect.top <= yBias ); //< cursorRect.bottom ); //left >= this.xBias );
+
+ if ( node && nodeRect.right >= this.xBias ) {
+ // find range in textnode to set cursor to
+ var nodeLength = node.textContent.length;
+ range.setEnd( node, 0 );
+ var offset = 0;
+ do {
+ offset++;
+ range.setStart( node, offset);
+ range.setEnd( node, offset);
+ rangeRect = range.getBoundingClientRect();
+ } while (
+ offset < nodeLength &&
+ (
+ (rangeRect.top <= yBias ) ||
+ ( rangeRect.right < this.xBias)
+ )
+ );
+
+ return range.endOffset + this.getTotalOffset(node); // should check distance for end-1 as well
+ } else if ( node && range ) {
+ range.setStart( range.endContainer, range.endOffset );
+ rangeRect = range.getBoundingClientRect();
+ if ( rangeRect.top > yBias ) {
+ // cannot set cursor to x pos > xBias, so get rightmost position in current node
+ range.setEnd(node, node.textContent.length);
+ return range.endOffset + this.getTotalOffset(node);
+ } else {
+ // cursor cannot advance further
+ return this.getCursor();
+ }
+ } else {
+ return this.getCursor();
+ }
+ };
+
+
+ this.create = function(start, end, editor) {
+ return new hopeEditorSelection(start, end, editor);
+ };
+
+});
\ No newline at end of file
diff --git a/www/hope/hope.events.js b/www/hope/hope.events.js
new file mode 100644
index 0000000..da14d3f
--- /dev/null
+++ b/www/hope/hope.events.js
@@ -0,0 +1,35 @@
+hope.register('hope.events', function() {
+
+ if ( typeof hope.global.addEventListener != 'undefined' ) {
+ this.listen = function( el, event, callback, capture ) {
+ return el.addEventListener( event, callback, capture );
+ };
+ } else if ( typeof hope.global.attachEvent != 'undefined' ) {
+ this.listen = function( el, event, callback, capture ) {
+ return el.attachEvent( 'on' + event, function() {
+ var evt = hope.global.event;
+ var self = evt.srcElement;
+ if ( !self ) {
+ self = hope.global;
+ }
+ return callback.call( self, evt );
+ } );
+ };
+ } else {
+ throw new hope.Exception( 'Browser is not supported', 'hope.editor.events.1' );
+ }
+
+ this.cancel = function( evt ) {
+ if ( typeof evt.stopPropagation != 'undefined' ) {
+ evt.stopPropagation();
+ }
+ if ( typeof evt.preventDefault != 'undefined' ) {
+ evt.preventDefault();
+ }
+ if ( typeof evt.cancelBubble != 'undefined' ) {
+ evt.cancelBubble = true;
+ }
+ return false;
+ };
+
+} );
\ No newline at end of file
diff --git a/www/hope/hope.fragment.annotations.js b/www/hope/hope.fragment.annotations.js
new file mode 100644
index 0000000..3009768
--- /dev/null
+++ b/www/hope/hope.fragment.annotations.js
@@ -0,0 +1,380 @@
+hope.register( 'hope.fragment.annotations', function() {
+
+ function parseMarkup( annotations ) {
+ var reMarkupLine = /^(?:([0-9]+)(?:-([0-9]+))?:)?(.*)$/m;
+ var matches = [];
+ var list = [];
+ var annotation = null;
+ while ( annotations && ( matches = annotations.match(reMarkupLine) ) ) {
+ if ( matches[2] ) {
+ annotation = hope.annotation.create(
+ [ parseInt(matches[1]), parseInt(matches[2]) ],
+ matches[3]
+ );
+ } else {
+ annotation = hope.annotation.create(
+ matches[1], matches[3]
+ );
+ }
+ list.push(annotation);
+ annotations = annotations.substr( matches[0].length + 1 );
+ }
+ return list;
+ }
+
+ function hopeAnnotationList( annotations ) {
+ this.list = [];
+ if ( annotations instanceof hopeAnnotationList ) {
+ this.list = annotations.list;
+ } else if ( Array.isArray( annotations) ) {
+ this.list = annotations;
+ } else {
+ this.list = parseMarkup( annotations + '' );
+ }
+ this.list.sort( function( a, b ) {
+ return a.compare( b );
+ });
+ }
+
+ hopeAnnotationList.prototype.toString = function() {
+ var result = '';
+ for ( var i=0, l=this.list.length; i0);
+ //});
+ list.sort( function( a, b ) {
+ return a.compare( b );
+ });
+ return new hopeAnnotationList(list);
+ };
+
+ hopeAnnotationList.prototype.apply = function( range, tag ) {
+ var list = this.list.slice();
+ list.push( hope.annotation.create( range, tag ) );
+ return new hopeAnnotationList(list).clean();
+ };
+
+ hopeAnnotationList.prototype.grow = function( position, size ) {
+ var i;
+
+ function getBlockIndexes(list, index, position) {
+ var blockIndexes = [];
+ for ( var i=index-1; i>=0; i-- ) {
+ if ( list[i].range.contains([position-1, position]) && list[i].isBlock() ) {
+ blockIndexes.push(i);
+ }
+ }
+ return blockIndexes;
+ }
+
+ var list = this.list.slice();
+ var removeRange = false;
+ var growRange = false;
+ var removeList = [];
+ var foundCaret = false;
+ if ( size < 0 ) {
+ removeRange = hope.range.create( position + size, position );
+ } else {
+ growRange = hope.range.create( position, position + size );
+ }
+ for ( i=0, l=list.length; i= list[i].range.start && removeRange.start <= list[i].range.start ) {
+ // range to remove overlaps start of this range, but is not equal
+ if ( list[i].isBlock() ) {
+ // block annotation must be merged with previous annotation, if available
+ // get block annotation at start of removeRange
+ var prevBlockIndexes = getBlockIndexes(list, i, removeRange.start);
+ if ( prevBlockIndexes.length === 0 ) {
+ // no block element in removeRange.start, so just move this block element
+ list[i] = hope.annotation.create( list[i].range.delete( removeRange ), list[i].tag );
+ } else {
+ // prevBlocks must now contain this block
+ for ( var ii=0, ll=prevBlockIndexes.length; ii= list[i].range.end ) {
+ // range to remove overlaps end of this range, but is not equal
+ // if this range needs to be extended, that will done when we find the next block range
+ // so just shrink this range
+ list[i] = hope.annotation.create( list[i].range.delete( removeRange ), list[i].tag );
+ }
+ } else if (growRange) {
+ var range;
+ if ( list[i].range.start == position ) {
+ if (list[i].tag.indexOf("data-hope-caret") > -1) {
+ foundCaret = true;
+ range = list[i].range.grow( size );
+ list[i] = hope.annotation.create( range, list[i].tag );
+ } else {
+ if (foundCaret) {
+ range = list[i].range.move( size, position );
+ list[i] = hope.annotation.create( range, list[i].tag );
+ }
+ }
+ } else if (list[i].range.end == position ) {
+ if (list[i].tag.indexOf("data-hope-caret") > -1) {
+ foundCaret = true;
+ range = list[i].range.grow( size );
+ list[i] = hope.annotation.create( range, list[i].tag );
+ } else {
+ if (foundCaret) {
+ range = list[i].range.grow( size );
+ list[i] = hope.annotation.create( range, list[i].tag );
+ }
+ }
+ } else if ( list[i].range.start > position ) {
+ range = list[i].range.move( size, position );
+ list[i] = hope.annotation.create( range, list[i].tag );
+ } else if ( list[i].range.end > position ) {
+ range = list[i].range.grow( size );
+ list[i] = hope.annotation.create( range, list[i].tag );
+ }
+ }
+ }
+ // now remove indexes in removeList from list
+ for ( i=removeList.length-1; i>=0; i--) {
+ list.splice( removeList[i], 1);
+ }
+ return new hopeAnnotationList(list).clean();
+ };
+
+ hopeAnnotationList.prototype.clear = function( range ) {
+ var i;
+ range = hope.range.create(range);
+ var list = this.list.slice();
+ var remove = [];
+ for ( i=0, l=list.length; i range.start ) {
+ list[i] = hope.annotation.create( [range.end, listRange.end], list[i].tag );
+ } else if ( listRange.end <= range.end ) {
+ list[i] = hope.annotation.create( [listRange.start, range.start], list[i].tag );
+ }
+ }
+ }
+ for ( i=remove.length-1; i>=0; i-- ) {
+ list.splice( remove[i], 1);
+ }
+ return new hopeAnnotationList(list).clean();
+ };
+
+ hopeAnnotationList.prototype.remove = function( range, tag ) {
+ var i;
+ range = hope.range.create(range);
+ var list = this.list.slice();
+ var remove = [];
+ var add = [];
+ for ( i=0, l=list.length; irange.end) {
+ // range is enclosed entirely in annotation range
+ list[i] = hope.annotation.create(
+ [ listRange.start, range.start ],
+ list[i].tag
+ );
+ add.push( hope.annotation.create(
+ [ range.end, listRange.end ],
+ list[i].tag
+ ));
+ } else if ( listRange.start < range.start ) {
+ // range overlaps annotation to the right
+ list[i] = hope.annotation.create(
+ [ listRange.start, range.start ],
+ list[i].tag
+ );
+ } else if ( listRange.end > range.end ) {
+ // range overlaps annotation to the left
+ list[i] = hope.annotation.create(
+ [ range.end, listRange.end ],
+ list[i].tag
+ );
+ }
+
+ }
+ for ( i=remove.length-1;i>=0; i-- ) {
+ list.splice( remove[i], 1);
+ }
+ list = list.concat(add);
+ return new hopeAnnotationList(list).clean();
+ };
+
+ hopeAnnotationList.prototype.delete = function( range ) {
+ range = hope.range.create(range);
+ return this.grow( range.end, -range.length );
+ };
+
+ hopeAnnotationList.prototype.copy = function( range ) {
+ range = hope.range.create(range);
+ var copy = [];
+ for ( var i=0, l=this.list.length; i 0 ) {
+ current++;
+ }
+ if ( current < 0 ) {
+ current = 0;
+ }
+ if ( !groupedList[current] ) {
+ groupedList[current] = { offset: eventList[i].offset, markup: [] };
+ }
+ groupedList[current].markup.push( { type: eventList[i].type, index: eventList[i].index } );
+ }
+ return groupedList;
+ }
+
+ var relativeList = getUnsortedEventList.call(this);
+ relativeList.sort(function(a,b) {
+ if ( a.offset < b.offset ) {
+ return -1;
+ } else if ( a.offset > b.offset ) {
+ return 1;
+ }
+ return 0;
+ });
+ relativeList = calculateRelativeOffsets.call( this, relativeList );
+ relativeList = groupByOffset.call( this, relativeList );
+ return relativeList;
+ };
+
+ this.create = function( annotations ) {
+ return new hopeAnnotationList( annotations );
+ };
+
+});
\ No newline at end of file
diff --git a/www/hope/hope.fragment.js b/www/hope/hope.fragment.js
new file mode 100644
index 0000000..140c521
--- /dev/null
+++ b/www/hope/hope.fragment.js
@@ -0,0 +1,105 @@
+hope.register( 'hope.fragment', function() {
+
+ var self = this;
+
+ function hopeFragment( text, annotations ) {
+ this.text = hope.fragment.text.create( text );
+ this.annotations = hope.fragment.annotations.create( annotations );
+ Object.freeze(this);
+ }
+
+ hopeFragment.prototype.delete = function( range ) {
+ return new hopeFragment(
+ this.text.delete( range ),
+ this.annotations.delete( range )
+ );
+ };
+
+ hopeFragment.prototype.copy = function( range ) {
+ // return copy fragment at range with the content and annotations at that range
+ return new hopeFragment(
+ this.text.copy( range ),
+ this.annotations.copy( range ).delete( hope.range.create( 0, range.start ) )
+ );
+ };
+
+ hopeFragment.prototype.insert = function( position, fragment ) {
+ if ( ! ( fragment instanceof hopeFragment ) ) {
+ fragment = new hopeFragment( fragment );
+ }
+ var result = new hopeFragment(
+ this.text.insert(position, fragment.text),
+ this.annotations.grow(position, fragment.text.length )
+ );
+ for ( var i=0, l=fragment.annotations.length; i= range.end ) {
+ return this;
+ } else {
+ return new hopeTextFragment( this.content.slice( 0, range.start ) + this.content.slice( range.end ) );
+ }
+ };
+
+ hopeTextFragment.prototype.copy = function( range ) {
+ range = hope.range.create(range);
+ // return copy of content at range
+ return new hopeTextFragment( this.content.slice( range.start, range.end ) );
+ };
+
+ hopeTextFragment.prototype.insert = function( position, content ) {
+ // insert fragment at range, return cut fragment
+ return new hopeTextFragment( this.content.slice( 0, position ) + content + this.content.slice( position ) );
+ };
+
+ hopeTextFragment.prototype.toString = function() {
+ return this.content;
+ };
+
+ hopeTextFragment.prototype.search = function( re, matchIndex ) {
+ function escapeRegExp(s) {
+ return s.replace(/[-/\\^$*+?.()|[\]{}]/g, '\\$&');
+ }
+ if ( ! ( re instanceof RegExp ) ) {
+ re = new RegExp( escapeRegExp( re ) , 'g' );
+ }
+ var result = [];
+ var match = null;
+ if ( !matchIndex ) {
+ matchIndex = 0;
+ }
+ while ( ( match = re.exec( this.content ) ) !== null ) {
+ result.push( hope.range.create( match.index, match.index + match[matchIndex].length ) );
+ if ( !re.global ) {
+ break;
+ }
+ }
+ return result;
+ };
+
+ this.create = function( content ) {
+ return new hopeTextFragment( content );
+ };
+
+});
\ No newline at end of file
diff --git a/www/hope/hope.js b/www/hope/hope.js
new file mode 100644
index 0000000..be66cb7
--- /dev/null
+++ b/www/hope/hope.js
@@ -0,0 +1,58 @@
+var hope = this.hope = ( function( global ) {
+
+ var registered = {};
+ var hope = {};
+
+ function _namespaceWalk( module, handler ) {
+ var rest = module.replace(/^\s+|\s+$/g, ''); //trim
+ var name = '';
+ var temp = hope.global;
+ var i = rest.indexOf( '.' );
+ while ( i != -1 ) {
+ name = rest.substring( 0, i );
+ if ( !temp[name]) {
+ temp = handler(temp, name);
+ if (!temp) {
+ return temp;
+ }
+ }
+ temp = temp[name];
+ rest = rest.substring( i + 1 );
+ i = rest.indexOf( '.' );
+ }
+ if ( rest ) {
+ if ( !temp[rest] ) {
+ temp = handler(temp, rest);
+ if (!temp) {
+ return temp;
+ }
+ }
+ temp = temp[rest];
+ }
+ return temp;
+ }
+
+ hope.global = global;
+
+ hope.register = function( module, implementation ) {
+ var moduleInstance = _namespaceWalk( module, function(ob, name) {
+ ob[name] = {};
+ return ob;
+ });
+ registered[module]=true;
+ if (typeof implementation == 'function') {
+ implementation.call(moduleInstance);
+ }
+ return moduleInstance;
+ };
+
+ hope.Exception = function(message, code) {
+ this.message = message;
+ this.code = code;
+ this.name = 'hope.Exception';
+ };
+
+ return hope;
+
+} )(this);
+
diff --git a/www/hope/hope.keyboard.js b/www/hope/hope.keyboard.js
new file mode 100644
index 0000000..1a96f44
--- /dev/null
+++ b/www/hope/hope.keyboard.js
@@ -0,0 +1,214 @@
+hope.register( 'hope.keyboard', function() {
+ var i;
+
+ var self = this;
+
+ var keyCodes = [];
+ keyCodes[3] = 'Cancel';
+ keyCodes[6] = 'Help';
+ keyCodes[8] = 'Backspace';
+ keyCodes[9] = 'Tab';
+ keyCodes[12] = 'Numlock-5';
+ keyCodes[13] = 'Enter';
+
+ keyCodes[16] = 'Shift';
+ keyCodes[17] = 'Control';
+ keyCodes[18] = 'Alt';
+ keyCodes[19] = 'Pause';
+ keyCodes[20] = 'CapsLock';
+ keyCodes[21] = 'KanaMode'; //HANGUL
+
+ keyCodes[23] = 'JunjaMode';
+ keyCodes[24] = 'FinalMode';
+ keyCodes[25] = 'HanjaMode'; //KANJI
+
+ keyCodes[27] = 'Escape';
+ keyCodes[28] = 'Convert';
+ keyCodes[29] = 'NonConvert';
+ keyCodes[30] = 'Accept';
+ keyCodes[31] = 'ModeChange';
+ keyCodes[32] = 'Spacebar';
+ keyCodes[33] = 'PageUp';
+ keyCodes[34] = 'PageDown';
+ keyCodes[35] = 'End';
+ keyCodes[36] = 'Home';
+ keyCodes[37] = 'ArrowLeft';
+ keyCodes[38] = 'ArrowUp';
+ keyCodes[39] = 'ArrowRight'; // opera has this as a "'" as well...
+ keyCodes[40] = 'ArrowDown';
+ keyCodes[41] = 'Select';
+ keyCodes[42] = 'Print';
+ keyCodes[43] = 'Execute';
+ keyCodes[44] = 'PrintScreen'; // opera ';';
+ keyCodes[45] = 'Insert'; // opera has this as a '-' as well...
+ keyCodes[46] = 'Delete'; // opera - ',';
+ keyCodes[47] = '/'; // opera
+
+ keyCodes[59] = ';';
+ keyCodes[60] = '<';
+ keyCodes[61] = '=';
+ keyCodes[62] = '>';
+ keyCodes[63] = '?';
+ keyCodes[64] = '@';
+
+ keyCodes[91] = 'OS'; // opera '[';
+ keyCodes[92] = 'OS'; // opera '\\';
+ keyCodes[93] = 'ContextMenu'; // opera ']';
+ keyCodes[95] = 'Sleep';
+ keyCodes[96] = '`';
+
+ keyCodes[106] = '*'; // keypad
+ keyCodes[107] = '+'; // keypad
+ keyCodes[109] = '-'; // keypad
+ keyCodes[110] = 'Separator';
+ keyCodes[111] = '/'; // keypad
+
+ keyCodes[144] = 'NumLock';
+ keyCodes[145] = 'ScrollLock';
+
+ keyCodes[160] = '^';
+ keyCodes[161] = '!';
+ keyCodes[162] = '"';
+ keyCodes[163] = '#';
+ keyCodes[164] = '$';
+ keyCodes[165] = '%';
+ keyCodes[166] = '&';
+ keyCodes[167] = '_';
+ keyCodes[168] = '(';
+ keyCodes[169] = ')';
+ keyCodes[170] = '*';
+ keyCodes[171] = '+';
+ keyCodes[172] = '|';
+ keyCodes[173] = '-';
+ keyCodes[174] = '{';
+ keyCodes[175] = '}';
+ keyCodes[176] = '~';
+
+ keyCodes[181] = 'VolumeMute';
+ keyCodes[182] = 'VolumeDown';
+ keyCodes[183] = 'VolumeUp';
+
+ keyCodes[186] = ';';
+ keyCodes[187] = '=';
+ keyCodes[188] = ',';
+ keyCodes[189] = '-';
+ keyCodes[190] = '.';
+ keyCodes[191] = '/';
+ keyCodes[192] = '`';
+
+ keyCodes[219] = '[';
+ keyCodes[220] = '\\';
+ keyCodes[221] = ']';
+ keyCodes[222] = "'";
+ keyCodes[224] = 'Meta';
+ keyCodes[225] = 'AltGraph';
+
+ keyCodes[246] = 'Attn';
+ keyCodes[247] = 'CrSel';
+ keyCodes[248] = 'ExSel';
+ keyCodes[249] = 'EREOF';
+ keyCodes[250] = 'Play';
+ keyCodes[251] = 'Zoom';
+ keyCodes[254] = 'Clear';
+
+ // a-z
+ for ( i=65; i<=90; i++ ) {
+ keyCodes[i] = String.fromCharCode( i ).toLowerCase();
+ }
+
+ // 0-9
+ for ( i=48; i<=57; i++ ) {
+ keyCodes[i] = String.fromCharCode( i );
+ }
+ // 0-9 keypad
+ for ( i=96; i<=105; i++ ) {
+ keyCodes[i] = ''+(i-95);
+ }
+
+ // F1 - F24
+ for ( i=112; i<=135; i++ ) {
+ keyCodes[i] = 'F'+(i-111);
+ }
+
+ function convertKeyNames( key ) {
+ switch ( key ) {
+ case ' ':
+ return 'Spacebar';
+ case 'Esc' :
+ return 'Escape';
+ case 'Left' :
+ case 'Up' :
+ case 'Right' :
+ case 'Down' :
+ return 'Arrow'+key;
+ case 'Del' :
+ return 'Delete';
+ case 'Scroll' :
+ return 'ScrollLock';
+ case 'MediaNextTrack' :
+ return 'MediaTrackNext';
+ case 'MediaPreviousTrack' :
+ return 'MediaTrackPrevious';
+ case 'Crsel' :
+ return 'CrSel';
+ case 'Exsel' :
+ return 'ExSel';
+ case 'Zoom' :
+ return 'ZoomToggle';
+ case 'Multiply' :
+ return '*';
+ case 'Add' :
+ return '+';
+ case 'Subtract' :
+ return '-';
+ case 'Decimal' :
+ return '.';
+ case 'Divide' :
+ return '/';
+ case 'Apps' :
+ return 'Menu';
+ default:
+ return key;
+ }
+ }
+
+ this.getKey = function( evt ) {
+ var keyInfo = '';
+ if ( evt.ctrlKey && evt.keyCode != 17 ) {
+ keyInfo += 'Control+';
+ }
+ if ( evt.metaKey && evt.keyCode != 224 ) {
+ keyInfo += 'Meta+';
+ }
+ if ( evt.altKey && evt.keyCode != 18 ) {
+ keyInfo += 'Alt+';
+ }
+ if ( evt.shiftKey && evt.keyCode != 16 ) {
+ keyInfo += 'Shift+';
+ }
+ // evt.key turns shift+a into A, while keeping shiftKey, so it becomes Shift+A, instead of Shift+a.
+ // so while it may be the future, i'm not using it here.
+ if ( evt.charCode ) {
+ keyInfo += String.fromCharCode( evt.charCode ).toLowerCase();
+ } else if ( evt.keyCode ) {
+ if ( typeof keyCodes[evt.keyCode] == 'undefined' ) {
+ keyInfo += '('+evt.keyCode+')';
+ } else {
+ keyInfo += keyCodes[evt.keyCode];
+ }
+ } else {
+ keyInfo += 'Unknown';
+ }
+ return keyInfo;
+ };
+
+ this.listen = function( el, key, callback, capture ) {
+ return hope.editor.events.listen( el, 'keydown', function(evt) {
+ var pressedKey = self.getKey( evt );
+ if ( key == pressedKey ) {
+ callback.call( this, evt );
+ }
+ }, capture);
+ };
+
+} );
\ No newline at end of file
diff --git a/www/hope/hope.mime.js b/www/hope/hope.mime.js
new file mode 100644
index 0000000..4d41993
--- /dev/null
+++ b/www/hope/hope.mime.js
@@ -0,0 +1,85 @@
+hope.register( 'hope.mime', function() {
+ // minimal mime encoding/decoding, stolen from https://github.com/andris9/mimelib/blob/master/lib/mimelib.js
+ var self = this;
+
+ self.getHeaders = function( message ) {
+ var parseHeader = function( line ) {
+ if (!line) {
+ return {};
+ }
+
+ var result = {}, parts = line.split(";"),
+ pos;
+
+ for (var i = 0, len = parts.length; i < len; i++) {
+ pos = parts[i].indexOf("=");
+ if (pos < 0) {
+ pos = parts[i].indexOf(':');
+ }
+ if ( pos < 0 ) {
+ result[!i ? "defaultValue" : "i-" + i] = parts[i].trim();
+ } else {
+ result[parts[i].substr(0, pos).trim().toLowerCase()] = parts[i].substr(pos + 1).trim();
+ }
+ }
+ return result;
+ };
+ var line = null;
+ var headers = {};
+ var temp = {};
+ line = message.match(/^.*$/m)[0];
+ while ( line ) {
+ message = message.substring( line.length );
+ temp = parseHeader( line );
+ for ( var i in temp ) {
+ headers[i] = temp[i];
+ }
+ var returns = message.match(/^\r?\n|\r/);
+ if ( returns[0] ) {
+ message = message.substring( returns[0].length );
+ }
+ line = message.match(/^.*$/m)[0];
+ }
+ return {
+ headers: headers,
+ message: message.substring(1)
+ };
+ };
+
+ self.encode = function( parts, message, headers ) {
+ var boundary = 'hopeBoundary'+Date.now();
+ var result = 'MIME-Version: 1.0\n';
+ if ( headers ) {
+ result += headers.join("\n");
+ }
+ result += 'Content-Type: multipart/related; boundary='+boundary+'\n\n';
+ if ( message ) {
+ result += message;
+ }
+ for ( var i=0, l=parts.length; i= 0) {
+ k = n;
+ } else {
+ k = len + n;
+ if (k < 0) {k = 0;}
+ }
+ var currentElement;
+ while (k < len) {
+ currentElement = O[k];
+ if (searchElement === currentElement ||
+ (searchElement !== searchElement && currentElement !== currentElement)) {
+ return true;
+ }
+ k++;
+ }
+ return false;
+ };
+}/**
+ * hope.range
+ *
+ * This implements an immutable range object.
+ * Once created using hope.range.create() a range cannot change its start and end properties.
+ * Any method that needs to change start or end, will instead create a new range with the new start/end
+ * values and return that.
+ * If you need to change a range value in place, you can assign the return value back to the original variable, e.g.:
+ * range = range.collapse();
+ */
+
+hope.register( 'hope.range', function() {
+
+
+ function hopeRange( start, end ) {
+ if ( typeof end == 'undefined' || end < start ) {
+ end = start;
+ }
+ this.start = start;
+ this.end = end;
+ Object.freeze(this);
+ }
+
+ hopeRange.prototype = {
+ constructor: hopeRange,
+ get length () {
+ return this.end - this.start;
+ }
+ };
+
+ hopeRange.prototype.collapse = function( toEnd ) {
+ var start = this.start;
+ var end = this.end;
+ if ( toEnd ) {
+ start = end;
+ } else {
+ end = start;
+ }
+ return new hopeRange(start, end );
+ };
+
+ hopeRange.prototype.compare = function( range ) {
+ range = hope.range.create(range);
+ if ( range.start < this.start ) {
+ return 1;
+ } else if ( range.start > this.start ) {
+ return -1;
+ } else if ( range.end < this.end ) {
+ return 1;
+ } else if ( range.end > this.end ) {
+ return -1;
+ }
+ return 0;
+ };
+
+ hopeRange.prototype.equals = function( range ) {
+ return this.compare(range)===0;
+ };
+
+ hopeRange.prototype.smallerThan = function( range ) {
+ return ( this.compare( range ) == -1 );
+ };
+
+ hopeRange.prototype.largerThan = function( range ) {
+ return ( this.compare( range ) == 1 );
+ };
+
+ hopeRange.prototype.contains = function( range ) {
+ range = hope.range.create(range);
+ return this.start <= range.start && this.end >= range.end;
+ };
+
+ hopeRange.prototype.overlaps = function( range ) {
+ range = hope.range.create(range);
+ if (range.equals(this)) {
+ return true;
+ }
+
+ // not overlapping if only the edges touch...
+ if ((range.start == this.end) && (range.start < range.end)) {
+ return false;
+ }
+ if ((range.end == this.start) && (range.start < range.end)) {
+ return false;
+ }
+
+ // but overlapping if the range to check is collapsed
+ if ((range.start == this.end) && (range.start == range.end)) {
+ return true;
+ }
+ if ((range.end == this.start) && (range.start == range.end)) {
+ return true;
+ }
+
+ return ( range.start <= this.end && range.end >= this.start );
+ };
+
+ hopeRange.prototype.isEmpty = function() {
+ return this.start >= this.end;
+ };
+
+ hopeRange.prototype.overlap = function( range ) {
+ range = hope.range.create(range);
+ var start = 0;
+ var end = 0;
+ if ( this.overlaps( range ) ) {
+ if ( range.start < this.start ) {
+ start = this.start;
+ } else {
+ start = range.start;
+ }
+ if ( range.end < this.end ) {
+ end = range.end;
+ } else {
+ end = this.end;
+ }
+ }
+ return new hopeRange(start, end); // FIXME: is this range( 0, 0 ) a useful return value when there is no overlap?
+ };
+
+ hopeRange.prototype.exclude = function( range ) {
+ // return parts of this that do not overlap with range
+ var left = null;
+ var right = null;
+ if ( this.equals(range) ) {
+ // nop
+ } else if ( this.overlaps( range ) ) {
+ left = new hopeRange( this.start, range.start );
+ right = new hopeRange( range.end, this.end );
+ if ( left.isEmpty() ) {
+ left = null;
+ }
+ if ( right.isEmpty() ) {
+ right = null;
+ }
+ } else if ( this.largerThan(range) ) {
+ left = null;
+ right = right;
+ } else {
+ left = this;
+ right = left;
+ }
+ return [ left, right ];
+ };
+
+ hopeRange.prototype.excludeLeft = function( range ) {
+ return this.exclude(range)[0];
+ };
+
+ hopeRange.prototype.excludeRight = function( range ) {
+ return this.exclude(range)[1];
+ };
+
+ /**
+ * remove overlapping part of range from this range
+ * [ 5 .. 20 ].delete( 10, 25 ) => [ 5 .. 10 ]
+ * [ 5 .. 20 ].delete( 10, 15) => [ 5 .. 15 ]
+ * [ 5 .. 20 ].delete( 5, 20 ) => [ 5 .. 5 ]
+ * [ 5 .. 20 ].delete( 0, 10 ) => [ 0 .. 10 ] ?
+ */
+ hopeRange.prototype.delete = function( range ) {
+ range = hope.range.create(range);
+ var moveLeft = 0;
+ var end = this.end;
+ if ( this.overlaps(range) ) {
+ var cutRange = this.overlap( range );
+ var cutLength = cutRange.length;
+ end -= cutLength;
+ }
+ var result = new hopeRange( this.start, end );
+ var exclude = range.excludeLeft( this );
+ if ( exclude ) {
+ result = result.move( -exclude.length );
+ }
+ return result;
+ };
+
+ hopeRange.prototype.copy = function( range ) {
+ range = hope.range.create(range);
+ return new hopeRange( 0, this.overlap( range ).length );
+ };
+
+ hopeRange.prototype.extend = function( length, direction ) {
+ var start = this.start;
+ var end = this.end;
+ if ( !direction ) {
+ direction = 1;
+ }
+ if ( direction == 1 ) {
+ end += length;
+ } else {
+ start = Math.max( 0, start - length );
+ }
+ return new hopeRange(start, end);
+ };
+
+ hopeRange.prototype.toString = function() {
+ if ( this.start != this.end ) {
+ return this.start + '-' + this.end;
+ } else {
+ return this.start + '';
+ }
+ };
+
+ hopeRange.prototype.grow = function( size ) {
+ var end = this.end + size;
+ if ( end < this.start ) {
+ end = this.start;
+ }
+ return new hopeRange(this.start, end);
+ };
+
+ hopeRange.prototype.shrink = function( size ) {
+ return this.grow( -size );
+ };
+
+ hopeRange.prototype.move = function( length, min, max ) {
+ var start = this.start;
+ var end = this.end;
+ start += length;
+ end += length;
+ if ( !min ) {
+ min = 0;
+ }
+ start = Math.max( min, start );
+ end = Math.max( start, end );
+ if ( max ) {
+ start = Math.min( max, start );
+ end = Math.min( max, start );
+ }
+ return new hopeRange(start, end);
+ };
+
+ this.create = function( start, end ) {
+ if ( start instanceof hopeRange ) {
+ return start;
+ }
+ if ( typeof end =='undefined' && parseInt(start,10)==start ) {
+ end = start;
+ } else if ( Array.isArray(start) && typeof start[1] != 'undefined' ) {
+ end = start[1];
+ start = start[0];
+ }
+ return new hopeRange( parseInt(start), parseInt(end) );
+ };
+
+});hope.register( 'hope.annotation', function() {
+
+ function hopeAnnotation(range, tag) {
+ this.range = hope.range.create(range);
+ this.tag = tag;
+ Object.freeze(this);
+ }
+
+ hopeAnnotation.prototype.delete = function( range ) {
+ return new hopeAnnotation( this.range.delete( range ), this.tag );
+ };
+
+ hopeAnnotation.prototype.copy = function( range ) {
+ return new hopeAnnotation( this.range.copy( range ), this.tag );
+ };
+
+ hopeAnnotation.prototype.compare = function( annotation ) {
+ return this.range.compare( annotation.range );
+ };
+
+ hopeAnnotation.prototype.has = function( tag ) {
+ //FIXME: should be able to specify attributes and attribute values as well
+ return this.stripTag() == hope.annotation.stripTag(tag);
+ };
+
+ hopeAnnotation.prototype.toString = function() {
+ return this.range + ':' + this.tag;
+ };
+
+ hopeAnnotation.prototype.stripTag = function() {
+ return hope.annotation.stripTag(this.tag);
+ };
+
+ hopeAnnotation.prototype.isBlock = function() {
+ return (hope.render.html.rules.nestingSets.block.indexOf(hope.annotation.stripTag(this.tag)) != -1 ); // FIXME: this should not know about hope.render.html;
+ };
+
+ this.create = function( range, tag ) {
+ return new hopeAnnotation( range, tag );
+ };
+
+ this.stripTag = function(tag) {
+ return tag.split(' ')[0];
+ };
+});
+
+hope.register( 'hope.fragment', function() {
+
+ var self = this;
+
+ function hopeFragment( text, annotations ) {
+ this.text = hope.fragment.text.create( text );
+ this.annotations = hope.fragment.annotations.create( annotations );
+ Object.freeze(this);
+ }
+
+ hopeFragment.prototype.delete = function( range ) {
+ return new hopeFragment(
+ this.text.delete( range ),
+ this.annotations.delete( range )
+ );
+ };
+
+ hopeFragment.prototype.copy = function( range ) {
+ // return copy fragment at range with the content and annotations at that range
+ return new hopeFragment(
+ this.text.copy( range ),
+ this.annotations.copy( range ).delete( hope.range.create( 0, range.start ) )
+ );
+ };
+
+ hopeFragment.prototype.insert = function( position, fragment ) {
+ if ( ! ( fragment instanceof hopeFragment ) ) {
+ fragment = new hopeFragment( fragment );
+ }
+ var result = new hopeFragment(
+ this.text.insert(position, fragment.text),
+ this.annotations.grow(position, fragment.text.length )
+ );
+ for ( var i=0, l=fragment.annotations.length; i0);
+ //});
+ list.sort( function( a, b ) {
+ return a.compare( b );
+ });
+ return new hopeAnnotationList(list);
+ };
+
+ hopeAnnotationList.prototype.apply = function( range, tag ) {
+ var list = this.list.slice();
+ list.push( hope.annotation.create( range, tag ) );
+ return new hopeAnnotationList(list).clean();
+ };
+
+ hopeAnnotationList.prototype.grow = function( position, size ) {
+ var i;
+
+ function getBlockIndexes(list, index, position) {
+ var blockIndexes = [];
+ for ( var i=index-1; i>=0; i-- ) {
+ if ( list[i].range.contains([position-1, position]) && list[i].isBlock() ) {
+ blockIndexes.push(i);
+ }
+ }
+ return blockIndexes;
+ }
+
+ var list = this.list.slice();
+ var removeRange = false;
+ var growRange = false;
+ var removeList = [];
+ var foundCaret = false;
+ if ( size < 0 ) {
+ removeRange = hope.range.create( position + size, position );
+ } else {
+ growRange = hope.range.create( position, position + size );
+ }
+ for ( i=0, l=list.length; i= list[i].range.start && removeRange.start <= list[i].range.start ) {
+ // range to remove overlaps start of this range, but is not equal
+ if ( list[i].isBlock() ) {
+ // block annotation must be merged with previous annotation, if available
+ // get block annotation at start of removeRange
+ var prevBlockIndexes = getBlockIndexes(list, i, removeRange.start);
+ if ( prevBlockIndexes.length === 0 ) {
+ // no block element in removeRange.start, so just move this block element
+ list[i] = hope.annotation.create( list[i].range.delete( removeRange ), list[i].tag );
+ } else {
+ // prevBlocks must now contain this block
+ for ( var ii=0, ll=prevBlockIndexes.length; ii= list[i].range.end ) {
+ // range to remove overlaps end of this range, but is not equal
+ // if this range needs to be extended, that will done when we find the next block range
+ // so just shrink this range
+ list[i] = hope.annotation.create( list[i].range.delete( removeRange ), list[i].tag );
+ }
+ } else if (growRange) {
+ var range;
+ if ( list[i].range.start == position ) {
+ if (list[i].tag.indexOf("data-hope-caret") > -1) {
+ foundCaret = true;
+ range = list[i].range.grow( size );
+ list[i] = hope.annotation.create( range, list[i].tag );
+ } else {
+ if (foundCaret) {
+ range = list[i].range.move( size, position );
+ list[i] = hope.annotation.create( range, list[i].tag );
+ }
+ }
+ } else if (list[i].range.end == position ) {
+ if (list[i].tag.indexOf("data-hope-caret") > -1) {
+ foundCaret = true;
+ range = list[i].range.grow( size );
+ list[i] = hope.annotation.create( range, list[i].tag );
+ } else {
+ if (foundCaret) {
+ range = list[i].range.grow( size );
+ list[i] = hope.annotation.create( range, list[i].tag );
+ }
+ }
+ } else if ( list[i].range.start > position ) {
+ range = list[i].range.move( size, position );
+ list[i] = hope.annotation.create( range, list[i].tag );
+ } else if ( list[i].range.end > position ) {
+ range = list[i].range.grow( size );
+ list[i] = hope.annotation.create( range, list[i].tag );
+ }
+ }
+ }
+ // now remove indexes in removeList from list
+ for ( i=removeList.length-1; i>=0; i--) {
+ list.splice( removeList[i], 1);
+ }
+ return new hopeAnnotationList(list).clean();
+ };
+
+ hopeAnnotationList.prototype.clear = function( range ) {
+ var i;
+ range = hope.range.create(range);
+ var list = this.list.slice();
+ var remove = [];
+ for ( i=0, l=list.length; i range.start ) {
+ list[i] = hope.annotation.create( [range.end, listRange.end], list[i].tag );
+ } else if ( listRange.end <= range.end ) {
+ list[i] = hope.annotation.create( [listRange.start, range.start], list[i].tag );
+ }
+ }
+ }
+ for ( i=remove.length-1; i>=0; i-- ) {
+ list.splice( remove[i], 1);
+ }
+ return new hopeAnnotationList(list).clean();
+ };
+
+ hopeAnnotationList.prototype.remove = function( range, tag ) {
+ var i;
+ range = hope.range.create(range);
+ var list = this.list.slice();
+ var remove = [];
+ var add = [];
+ for ( i=0, l=list.length; irange.end) {
+ // range is enclosed entirely in annotation range
+ list[i] = hope.annotation.create(
+ [ listRange.start, range.start ],
+ list[i].tag
+ );
+ add.push( hope.annotation.create(
+ [ range.end, listRange.end ],
+ list[i].tag
+ ));
+ } else if ( listRange.start < range.start ) {
+ // range overlaps annotation to the right
+ list[i] = hope.annotation.create(
+ [ listRange.start, range.start ],
+ list[i].tag
+ );
+ } else if ( listRange.end > range.end ) {
+ // range overlaps annotation to the left
+ list[i] = hope.annotation.create(
+ [ range.end, listRange.end ],
+ list[i].tag
+ );
+ }
+
+ }
+ for ( i=remove.length-1;i>=0; i-- ) {
+ list.splice( remove[i], 1);
+ }
+ list = list.concat(add);
+ return new hopeAnnotationList(list).clean();
+ };
+
+ hopeAnnotationList.prototype.delete = function( range ) {
+ range = hope.range.create(range);
+ return this.grow( range.end, -range.length );
+ };
+
+ hopeAnnotationList.prototype.copy = function( range ) {
+ range = hope.range.create(range);
+ var copy = [];
+ for ( var i=0, l=this.list.length; i 0 ) {
+ current++;
+ }
+ if ( current < 0 ) {
+ current = 0;
+ }
+ if ( !groupedList[current] ) {
+ groupedList[current] = { offset: eventList[i].offset, markup: [] };
+ }
+ groupedList[current].markup.push( { type: eventList[i].type, index: eventList[i].index } );
+ }
+ return groupedList;
+ }
+
+ var relativeList = getUnsortedEventList.call(this);
+ relativeList.sort(function(a,b) {
+ if ( a.offset < b.offset ) {
+ return -1;
+ } else if ( a.offset > b.offset ) {
+ return 1;
+ }
+ return 0;
+ });
+ relativeList = calculateRelativeOffsets.call( this, relativeList );
+ relativeList = groupByOffset.call( this, relativeList );
+ return relativeList;
+ };
+
+ this.create = function( annotations ) {
+ return new hopeAnnotationList( annotations );
+ };
+
+});hope.register( 'hope.fragment.text', function() {
+
+ function hopeTextFragment( text ) {
+ this.content = text+'';
+ }
+
+ hopeTextFragment.prototype = {
+ constructor: hopeTextFragment,
+ get length () {
+ return this.content.length;
+ }
+ };
+
+ hopeTextFragment.prototype.delete = function( range ) {
+ range = hope.range.create(range);
+ // cut range from content, return the cut content
+ if ( range.start >= range.end ) {
+ return this;
+ } else {
+ return new hopeTextFragment( this.content.slice( 0, range.start ) + this.content.slice( range.end ) );
+ }
+ };
+
+ hopeTextFragment.prototype.copy = function( range ) {
+ range = hope.range.create(range);
+ // return copy of content at range
+ return new hopeTextFragment( this.content.slice( range.start, range.end ) );
+ };
+
+ hopeTextFragment.prototype.insert = function( position, content ) {
+ // insert fragment at range, return cut fragment
+ return new hopeTextFragment( this.content.slice( 0, position ) + content + this.content.slice( position ) );
+ };
+
+ hopeTextFragment.prototype.toString = function() {
+ return this.content;
+ };
+
+ hopeTextFragment.prototype.search = function( re, matchIndex ) {
+ function escapeRegExp(s) {
+ return s.replace(/[-/\\^$*+?.()|[\]{}]/g, '\\$&');
+ }
+ if ( ! ( re instanceof RegExp ) ) {
+ re = new RegExp( escapeRegExp( re ) , 'g' );
+ }
+ var result = [];
+ var match = null;
+ if ( !matchIndex ) {
+ matchIndex = 0;
+ }
+ while ( ( match = re.exec( this.content ) ) !== null ) {
+ result.push( hope.range.create( match.index, match.index + match[matchIndex].length ) );
+ if ( !re.global ) {
+ break;
+ }
+ }
+ return result;
+ };
+
+ this.create = function( content ) {
+ return new hopeTextFragment( content );
+ };
+
+});hope.register( 'hope.render.html', function() {
+
+ var nestingSets = {
+ 'inline' : [ 'tt', 'u', 'strike', 'em', 'strong', 'dfn', 'code', 'samp', 'kbd', 'var', 'cite', 'abbr', 'acronym', 'sub', 'sup', 'q', 'span', 'bdo', 'a', 'object', 'img', 'bd', 'br', 'i' ],
+ 'block' : [ 'address', 'dir', 'menu', 'hr', 'li', 'table', 'p', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'pre', 'ul', 'ol', 'dl', 'div', 'blockquote', 'iframe' ]
+ };
+
+ nestingSets.all = nestingSets.block.concat( nestingSets.inline );
+
+ this.rules = {
+ nesting: {
+ 'a' : nestingSets.inline.filter( function(element) { return element != 'a'; } ),
+ 'abbr' : nestingSets.inline,
+ 'acronym' : nestingSets.inline,
+ 'address' : [ 'p' ].concat( nestingSets.inline ),
+ 'bdo' : nestingSets.inline,
+ 'blockquote': nestingSets.all,
+ 'br' : [],
+ 'caption' : nestingSets.inline,
+ 'cite' : nestingSets.inline,
+ 'code' : nestingSets.inline,
+ 'col' : [],
+ 'colgroup' : [ 'col' ],
+ 'dd' : nestingSets.all,
+ 'dfn' : nestingSets.inline,
+ 'dir' : [ 'li' ],
+ 'div' : nestingSets.all,
+ 'dl' : [ 'dt', 'dd' ],
+ 'dt' : nestingSets.inline,
+ 'em' : nestingSets.inline,
+ 'h1' : nestingSets.inline,
+ 'h2' : nestingSets.inline,
+ 'h3' : nestingSets.inline,
+ 'h4' : nestingSets.inline,
+ 'h5' : nestingSets.inline,
+ 'h6' : nestingSets.inline,
+ 'hr' : [],
+ 'img' : [],
+ 'kbd' : nestingSets.inline,
+ 'li' : nestingSets.all,
+ 'menu' : [ 'li' ],
+ 'object' : [ 'param' ].concat( nestingSets.all ),
+ 'ol' : [ 'li' ],
+ 'p' : nestingSets.inline,
+ 'pre' : nestingSets.inline,
+ 'q' : nestingSets.inline,
+ 'samp' : nestingSets.inline,
+ 'span' : nestingSets.inline,
+ 'strike' : nestingSets.inline,
+ 'strong' : nestingSets.inline,
+ 'sub' : nestingSets.inline,
+ 'sup' : nestingSets.inline,
+ 'table' : [ 'caption', 'colgroup', 'col', 'thead', 'tbody' ],
+ 'tbody' : [ 'tr' ],
+ 'td' : nestingSets.all,
+ 'th' : nestingSets.all,
+ 'thead' : [ 'tr' ],
+ 'tr' : [ 'td', 'th' ],
+ 'tt' : nestingSets.inline,
+ 'u' : nestingSets.inline,
+ 'ul' : [ 'li' ],
+ 'var' : nestingSets.inline
+ },
+ // which html elements can not have child elements at all and shouldn't be closed
+ 'noChildren' : [ 'hr', 'br', 'col', 'img' ],
+ // which html elements must have a specific child element
+ 'obligChild' : {
+ 'ol' : [ 'li' ],
+ 'ul' : [ 'li' ],
+ 'dl' : [ 'dt', 'dd' ]
+ },
+ // which html elements must have a specific parent element
+ 'obligParent' : {
+ 'li' : [ 'ul', 'ol', 'dir', 'menu' ],
+ 'dt' : [ 'dl' ],
+ 'dd' : [ 'dl' ]
+ },
+ // which html elements to allow as the top level, default is only block elements
+ 'toplevel' : nestingSets.block.concat(nestingSets.inline), // [ 'li', 'img', 'span', 'strong', 'em', 'code' ],
+ 'nestingSets' : nestingSets
+ };
+
+ this.getTag = function( markup ) {
+ return markup.split(' ')[0].toLowerCase(); // FIXME: more robust parsing needed
+ };
+
+ this.getAnnotationStack = function( annotationSet ) {
+ // { index:nextAnnotationEntry.index, entry:nextAnnotation }
+ // { start:, end:, annotation: }
+ // assert: annotationSet must only contain annotation that has overlapping ranges
+ // if not results will be unpredictable
+ var annotationStack = [];
+ if ( !annotationSet.length ) {
+ return [];
+ }
+
+ var rules = this.rules;
+
+ annotationSet.sort( function( a, b ) {
+ if ( a.range.start < b.range.start ) {
+ return -1;
+ } else if ( a.range.start > b.range.start ) {
+ return 1;
+ } else if ( a.range.end > b.range.end ) {
+ return -1;
+ } else if ( a.range.end < b.range.end ) {
+ return 1;
+ }
+
+ // if comparing ul/ol and li on the same range, ul/ol goes first;
+ if (rules.obligParent[a.tag.split(/ /)[0]]) {
+ if (rules.obligParent[a.tag.split(/ /)[0]].indexOf(b.tag.split(/ /)[0]) != -1) {
+ return 1;
+ }
+ }
+ if (rules.obligParent[b.tag.split(/ /)[0]]) {
+ if (rules.obligParent[b.tag.split(/ /)[0]].indexOf(a.tag.split(/ /)[0]) != -1) {
+ return -1;
+ }
+ }
+
+ // block elementen komen voor andere elementen
+ if (nestingSets.block.indexOf(a.tag.split(/ /)[0]) != '-1') {
+ return -1;
+ }
+ if (nestingSets.block.indexOf(b.tag.split(/ /)[0]) != '-1') {
+ return 1;
+ }
+
+ // hack om hyperlinks met images er in te laten werken.
+ if (a.tag.split(/ /)[0] == 'a') {
+ return -1;
+ }
+ if (b.tag.split(/ /)[0] == 'a') {
+ return 1;
+ }
+
+ // daarna komen inline elementen
+ if (nestingSets.inline.indexOf(a.tag.split(/ /)[0]) != '-1') {
+ return -1;
+ }
+ if (nestingSets.inline.indexOf(b.tag.split(/ /)[0]) != '-1') {
+ return 1;
+ }
+
+ return 0;
+ });
+ var unfilteredStack = [];
+ for ( var i=0, l=annotationSet.length; icommonIndex; i-- ) {
+ annotationDiff.push( { type : 'close', annotation : annotationStackFrom[i] } );
+ }
+ for ( i=commonIndex+1, l=annotationStackTo.length; i';
+ }
+ } else if ( annotationDiff[i].type == 'insert' ) {
+ renderedDiff += '<' + annotationDiff[i].annotation.tag + '>';
+ annotationTag = this.getTag( annotationDiff[i].annotation.tag );
+ if ( this.rules.noChildren.indexOf( annotationTag ) == -1 ) {
+ renderedDiff += '' + annotationTag + '>';
+ }
+ } else {
+ renderedDiff += '<' + annotationDiff[i].annotation.tag + '>';
+ }
+ }
+ return renderedDiff;
+ };
+
+ this.escape = function( content ) {
+ return content
+ .replace(/&/g, "&")
+ .replace(//g, ">")
+ .replace(/"/g, """)
+ .replace(/'/g, "'");
+ };
+
+ this.render = function( fragment ) {
+ // FIXME: annotation should be the relative annotation list to speed things up
+ var annotationSet = []; // set of applicable annotation at current position
+ var annotationStack = []; // stack of applied (valid) annotation at current position
+
+ var relativeAnnotation = fragment.annotations.getEventList();
+ var content = fragment.text.toString();
+
+ var renderedHTML = '';
+ var cursor = 0;
+
+ while ( relativeAnnotation.length ) {
+
+ var annotationChangeSet = relativeAnnotation.shift();
+ var annotationAdded = []; // list of annotation added in this change set
+ var annotationSetOnce = []; // list of annotation that can not have children, needs no close
+ for ( i=0, l=annotationChangeSet.markup.length; i 0 ) {
+ if (diffHTML && (
+ diffHTML.indexOf("
") !== -1 ||
+ diffHTML.indexOf("
") !== -1 ||
+ diffHTML.indexOf("
';
+ keyCodes[63] = '?';
+ keyCodes[64] = '@';
+
+ keyCodes[91] = 'OS'; // opera '[';
+ keyCodes[92] = 'OS'; // opera '\\';
+ keyCodes[93] = 'ContextMenu'; // opera ']';
+ keyCodes[95] = 'Sleep';
+ keyCodes[96] = '`';
+
+ keyCodes[106] = '*'; // keypad
+ keyCodes[107] = '+'; // keypad
+ keyCodes[109] = '-'; // keypad
+ keyCodes[110] = 'Separator';
+ keyCodes[111] = '/'; // keypad
+
+ keyCodes[144] = 'NumLock';
+ keyCodes[145] = 'ScrollLock';
+
+ keyCodes[160] = '^';
+ keyCodes[161] = '!';
+ keyCodes[162] = '"';
+ keyCodes[163] = '#';
+ keyCodes[164] = '$';
+ keyCodes[165] = '%';
+ keyCodes[166] = '&';
+ keyCodes[167] = '_';
+ keyCodes[168] = '(';
+ keyCodes[169] = ')';
+ keyCodes[170] = '*';
+ keyCodes[171] = '+';
+ keyCodes[172] = '|';
+ keyCodes[173] = '-';
+ keyCodes[174] = '{';
+ keyCodes[175] = '}';
+ keyCodes[176] = '~';
+
+ keyCodes[181] = 'VolumeMute';
+ keyCodes[182] = 'VolumeDown';
+ keyCodes[183] = 'VolumeUp';
+
+ keyCodes[186] = ';';
+ keyCodes[187] = '=';
+ keyCodes[188] = ',';
+ keyCodes[189] = '-';
+ keyCodes[190] = '.';
+ keyCodes[191] = '/';
+ keyCodes[192] = '`';
+
+ keyCodes[219] = '[';
+ keyCodes[220] = '\\';
+ keyCodes[221] = ']';
+ keyCodes[222] = "'";
+ keyCodes[224] = 'Meta';
+ keyCodes[225] = 'AltGraph';
+
+ keyCodes[246] = 'Attn';
+ keyCodes[247] = 'CrSel';
+ keyCodes[248] = 'ExSel';
+ keyCodes[249] = 'EREOF';
+ keyCodes[250] = 'Play';
+ keyCodes[251] = 'Zoom';
+ keyCodes[254] = 'Clear';
+
+ // a-z
+ for ( i=65; i<=90; i++ ) {
+ keyCodes[i] = String.fromCharCode( i ).toLowerCase();
+ }
+
+ // 0-9
+ for ( i=48; i<=57; i++ ) {
+ keyCodes[i] = String.fromCharCode( i );
+ }
+ // 0-9 keypad
+ for ( i=96; i<=105; i++ ) {
+ keyCodes[i] = ''+(i-95);
+ }
+
+ // F1 - F24
+ for ( i=112; i<=135; i++ ) {
+ keyCodes[i] = 'F'+(i-111);
+ }
+
+ function convertKeyNames( key ) {
+ switch ( key ) {
+ case ' ':
+ return 'Spacebar';
+ case 'Esc' :
+ return 'Escape';
+ case 'Left' :
+ case 'Up' :
+ case 'Right' :
+ case 'Down' :
+ return 'Arrow'+key;
+ case 'Del' :
+ return 'Delete';
+ case 'Scroll' :
+ return 'ScrollLock';
+ case 'MediaNextTrack' :
+ return 'MediaTrackNext';
+ case 'MediaPreviousTrack' :
+ return 'MediaTrackPrevious';
+ case 'Crsel' :
+ return 'CrSel';
+ case 'Exsel' :
+ return 'ExSel';
+ case 'Zoom' :
+ return 'ZoomToggle';
+ case 'Multiply' :
+ return '*';
+ case 'Add' :
+ return '+';
+ case 'Subtract' :
+ return '-';
+ case 'Decimal' :
+ return '.';
+ case 'Divide' :
+ return '/';
+ case 'Apps' :
+ return 'Menu';
+ default:
+ return key;
+ }
+ }
+
+ this.getKey = function( evt ) {
+ var keyInfo = '';
+ if ( evt.ctrlKey && evt.keyCode != 17 ) {
+ keyInfo += 'Control+';
+ }
+ if ( evt.metaKey && evt.keyCode != 224 ) {
+ keyInfo += 'Meta+';
+ }
+ if ( evt.altKey && evt.keyCode != 18 ) {
+ keyInfo += 'Alt+';
+ }
+ if ( evt.shiftKey && evt.keyCode != 16 ) {
+ keyInfo += 'Shift+';
+ }
+ // evt.key turns shift+a into A, while keeping shiftKey, so it becomes Shift+A, instead of Shift+a.
+ // so while it may be the future, i'm not using it here.
+ if ( evt.charCode ) {
+ keyInfo += String.fromCharCode( evt.charCode ).toLowerCase();
+ } else if ( evt.keyCode ) {
+ if ( typeof keyCodes[evt.keyCode] == 'undefined' ) {
+ keyInfo += '('+evt.keyCode+')';
+ } else {
+ keyInfo += keyCodes[evt.keyCode];
+ }
+ } else {
+ keyInfo += 'Unknown';
+ }
+ return keyInfo;
+ };
+
+ this.listen = function( el, key, callback, capture ) {
+ return hope.editor.events.listen( el, 'keydown', function(evt) {
+ var pressedKey = self.getKey( evt );
+ if ( key == pressedKey ) {
+ callback.call( this, evt );
+ }
+ }, capture);
+ };
+
+} );hope.register( 'hope.editor', function() {
+ var hopeTokenCounter = 0;
+ var browserCountsWhitespace = (function() {
+ var div = document.createElement("DIV");
+ div.innerHTML = " abc
";
+ document.body.appendChild(div);
+ var range = document.createRange();
+ range.setStart(div.querySelector("div"), 1);
+ var offset1 = range.startOffset;
+ var sel = window.getSelection();
+ sel.removeAllRanges();
+ sel.addRange(range);
+ var newRange = sel.getRangeAt(0);
+ var offset2 = newRange.startOffset;
+ var result = (offset1 == offset2);
+ document.body.removeChild(div);
+ return result;
+ }());
+
+ function unrender(target) {
+ var textValue = '';
+ var tags = [];
+
+ var node;
+
+ var tagStart, tagEnd;
+
+ for (var i in target.childNodes) {
+ if (target.childNodes[i].nodeType == document.ELEMENT_NODE) {
+ if (
+ !target.childNodes[i].hasChildNodes()
+ ) {
+ tagStart = hopeTokenCounter;
+ hopeTokenCounter += 1;
+
+ switch (target.childNodes[i].tagName.toLowerCase()) {
+ case 'br':
+ case 'hr':
+ textValue += "\n";
+ break;
+ case 'img':
+ textValue += "\u00AD"; //
+ break;
+ default:
+ textValue += "\u00AD"; //
+ }
+
+ tagEnd = hopeTokenCounter;
+
+ tags.push({
+ start : tagStart,
+ end : tagEnd,
+ tag : target.childNodes[i].tagName.toLowerCase(),
+ attrs : target.childNodes[i].attributes
+ });
+ } else {
+
+ tagStart = hopeTokenCounter;
+
+ node = this.unrender(target.childNodes[i]);
+ // hopeTokenCounter += node.length;
+ textValue += node.text;
+ tagEnd = hopeTokenCounter;
+
+ tags.push({
+ start : tagStart,
+ end : tagEnd,
+ tag : target.childNodes[i].tagName.toLowerCase(),
+ attrs : target.childNodes[i].attributes,
+ caret : this.getCaretOffset(target.childNodes[i])
+ });
+
+ for (var j=0; j= caret) {
+ try {
+ selection.setStart(node.childNodes[i], caret - nodeOffset);
+ } catch (e) {
+ console.log("Warning: could not set caret position");
+ }
+ node.removeAttribute("data-hope-caret");
+ return selection;
+ }
+ }
+
+ }
+
+ function tagsToText(tags) {
+ var result = '';
+ var i,j;
+
+ for (i=0; i -1) {
+ result += " data-hope-caret=\"" + tags[i].caret + "\"";
+ }
+ result += "\n";
+ }
+ return result;
+ }
+
+ function hopeEditor( textEl, annotationsEl, outputEl, renderEl ) {
+ this.refs = {
+ text: textEl,
+ annotations: annotationsEl,
+ output: outputEl,
+ render: renderEl
+ };
+ this.selection = hope.editor.selection.create(0,0,this);
+ this.commandsKeyUp = {};
+
+ if (this.refs.output.innerHTML !== '') {
+ this.refs.output.innerHTML = this.refs.output.innerHTML.replace(/\/p>/g, "/p>");
+ this.parseHTML();
+ }
+
+ this.browserCountsWhitespace = browserCountsWhitespace;
+
+ var text = this.refs.text.value;
+ var annotations = this.refs.annotations.value;
+ this.fragment = hope.fragment.create( text, annotations );
+ this.refs.output.contentEditable = true;
+ this.update();
+ this.initDone = true;
+// initEvents(this);
+ }
+
+ function initEvents(editor) {
+ hope.events.listen(editor.refs.output, 'keypress', function( evt ) {
+ if ( !evt.ctrlKey && !evt.altKey ) {
+ // check selection length
+ // remove text in selection
+ // add character
+ var range = editor.selection.getRange();
+ var charCode = evt.which;
+
+ if (evt.which) {
+ var charTyped = String.fromCharCode(charCode);
+ if ( charTyped ) { // ignore non printable characters
+ if ( range.length ) {
+ editor.fragment = editor.fragment.delete(range);
+ }
+ editor.fragment = editor.fragment.insert(range.start, charTyped );
+ editor.selection.collapse().move(1);
+ setTimeout( function() {
+ editor.update();
+ }, 0 );
+ }
+ return hope.events.cancel(evt);
+ }
+ }
+ });
+
+ hope.events.listen(editor.refs.output, 'keydown', function( evt ) {
+ var key = hope.keyboard.getKey( evt );
+ if ( editor.commands[key] ) {
+ var range = editor.selection.getRange();
+ editor.commands[key].call(editor, range);
+ setTimeout( function() {
+ editor.update();
+ }, 0);
+ return hope.events.cancel(evt);
+ } else if ( evt.ctrlKey || evt.altKey ) {
+ return hope.events.cancel(evt);
+ }
+ });
+
+ hope.events.listen(editor.refs.output, 'keyup', function( evt ) {
+ var key = hope.keyboard.getKey( evt );
+ if ( editor.selection.cursorCommands.indexOf(key)<0 ) {
+ if ( editor.commandsKeyUp[key] ) {
+ var range = editor.selection.getRange();
+ editor.commandsKeyUp[key].call(editor, range);
+ setTimeout( function() {
+ editor.update();
+ }, 0);
+ }
+ return hope.events.cancel(evt);
+ }
+ });
+
+ }
+
+ hopeEditor.prototype.setCaretOffset = setCaretOffset;
+ hopeEditor.prototype.getCaretOffset = getCaretOffset;
+ hopeEditor.prototype.unrender = unrender;
+
+ hopeEditor.prototype.parseHTML = function() {
+ hopeTokenCounter = 0;
+
+ var data = this.unrender(this.refs.output);
+ this.refs.annotations.value = tagsToText(data.tags);
+ this.refs.text.value = data.text;
+ this.fragment = hope.fragment.create( this.refs.text.value, this.refs.annotations.value );
+ };
+
+ hopeEditor.prototype.getEditorRange = function(start, end ) {
+ var treeWalker = document.createTreeWalker(
+ this.refs.output,
+ NodeFilter.SHOW_TEXT | NodeFilter.SHOW_ELEMENT,
+ function(node) {
+ if (
+ node.nodeType == document.TEXT_NODE ||
+ !node.hasChildNodes()
+ ) {
+ return NodeFilter.FILTER_ACCEPT;
+ } else {
+ return NodeFilter.FILTER_SKIP;
+ }
+ },
+ false
+ );
+ var offset = 0;
+ var node = null;
+ var range = document.createRange();
+ var lastNode = null;
+ do {
+ lastNode = node;
+ node = treeWalker.nextNode();
+ if ( node ) {
+ if (node.nodeType == document.ELEMENT_NODE) {
+ offset += 1;
+ } else {
+ offset += node.textContent.length;
+ }
+ }
+ } while ( offset < start && node );
+ if ( !node ) {
+ if (lastNode) {
+ if (lastNode.nodeType == document.ELEMENT_NODE) {
+ range.setStart(lastNode, 0);
+ range.setEndAfter(lastNode);
+ } else {
+ range.setStart(lastNode, lastNode.textContent ? lastNode.textContent.length : 1 );
+ range.setEnd(lastNode, lastNode.textContent ? lastNode.textContent.length : 1 );
+ }
+ return range;
+ }
+ return false;
+ }
+
+ var preOffset = offset - (node.nodeType == document.TEXT_NODE ? node.textContent.length : 1);
+ var nextNode;
+ if (node.nodeType == document.ELEMENT_NODE) {
+ nextNode = treeWalker.nextNode();
+ treeWalker.previousNode();
+ if (nextNode) {
+ range.setStartBefore(nextNode);
+ } else {
+ range.setStartAfter(node);
+ }
+ } else {
+ if (start-preOffset == node.textContent.length) {
+ nextNode = treeWalker.nextNode();
+ treeWalker.previousNode();
+ if (nextNode) {
+ range.setStartBefore(nextNode);
+ } else {
+ range.setStartAfter(node);
+ }
+ } else {
+ range.setStart(node, start - preOffset );
+ }
+ }
+ while ( offset < end && node ) {
+ node = treeWalker.nextNode();
+ if ( node ) {
+ if (node.nodeType == document.ELEMENT_NODE) {
+ offset += 1;
+ } else {
+ offset += node.textContent.length;
+ }
+ }
+ }
+ if ( !node ) {
+ if (lastNode) {
+ range.setEnd(lastNode, lastNode.textContent ? lastNode.textContent.length : 1 );
+ return range;
+ }
+ return false;
+ }
+
+ preOffset = offset - (node.nodeType == document.TEXT_NODE ? node.textContent.length : 1);
+
+ if (node.nodeType == document.ELEMENT_NODE) {
+ range.setEndAfter(node);
+ } else {
+ range.setEnd(node, end - preOffset );
+ }
+ return range;
+ };
+
+ hopeEditor.prototype.showCursor = function() {
+ var range = this.selection.getRange();
+ var selection = this.getEditorRange(range.start, range.end);
+ var caretElm = document.querySelector('[data-hope-caret]');
+ if (caretElm) {
+ selection = this.setCaretOffset(caretElm);
+ }
+ // remove all other caret attributes from the other elements;
+ var otherCarets = document.querySelectorAll('[data-hope-caret]');
+ for (var i=0; i', '>');
+ }
+ if ( this.refs.annotations ) {
+ this.refs.annotations.value = this.fragment.annotations+'';
+ }
+ };
+
+ hopeEditor.prototype.command = function( key, callback, keyup ) {
+ if ( keyup ) {
+ this.commandsKeyUp[key] = callback;
+ } else {
+ this.commands[key] = callback;
+ }
+ };
+
+ this.create = function( textEl, annotationsEl, outputEl, previewEl ) {
+ return new hopeEditor( textEl, annotationsEl, outputEl, previewEl);
+ };
+
+});hope.register( 'hope.editor.selection', function() {
+ function hopeEditorSelection(start, end, editor) {
+ this.start = start;
+ this.end = end;
+ this.editor = editor;
+
+ var self = this;
+
+ var updateRange = function() {
+ var sel = window.getSelection();
+ var rangeStart, rangeEnd;
+ var bestStart, bestEnd;
+
+ if (sel.rangeCount) {
+ for (var i=0; i not a text node
+ var nodeRect = null;
+ var range = null;
+ var rangeRect = null;
+ var yBias = null;
+ // find textnode to place cursor in
+ do {
+ node = this.getNextTextNode(node);
+ if ( node ) {
+ range = document.createRange();
+ range.setStart(node, 0);
+ range.setEnd(node, node.textContent.length);
+ nodeRect = range.getBoundingClientRect();
+ if ( !yBias ) {
+ if ( nodeRect.top > cursorRect.top ) {
+ yBias = nodeRect.top;
+ } else {
+ yBias = cursorRect.top;
+ }
+ }
+ }
+ } while ( node && nodeRect.height!==0 && nodeRect.top <= yBias ); //< cursorRect.bottom ); //left >= this.xBias );
+
+ if ( node && nodeRect.right >= this.xBias ) {
+ // find range in textnode to set cursor to
+ var nodeLength = node.textContent.length;
+ range.setEnd( node, 0 );
+ var offset = 0;
+ do {
+ offset++;
+ range.setStart( node, offset);
+ range.setEnd( node, offset);
+ rangeRect = range.getBoundingClientRect();
+ } while (
+ offset < nodeLength &&
+ (
+ (rangeRect.top <= yBias ) ||
+ ( rangeRect.right < this.xBias)
+ )
+ );
+
+ return range.endOffset + this.getTotalOffset(node); // should check distance for end-1 as well
+ } else if ( node && range ) {
+ range.setStart( range.endContainer, range.endOffset );
+ rangeRect = range.getBoundingClientRect();
+ if ( rangeRect.top > yBias ) {
+ // cannot set cursor to x pos > xBias, so get rightmost position in current node
+ range.setEnd(node, node.textContent.length);
+ return range.endOffset + this.getTotalOffset(node);
+ } else {
+ // cursor cannot advance further
+ return this.getCursor();
+ }
+ } else {
+ return this.getCursor();
+ }
+ };
+
+
+ this.create = function(start, end, editor) {
+ return new hopeEditorSelection(start, end, editor);
+ };
+
+});
\ No newline at end of file
diff --git a/www/hope/hope.polyfills.js b/www/hope/hope.polyfills.js
new file mode 100644
index 0000000..67345af
--- /dev/null
+++ b/www/hope/hope.polyfills.js
@@ -0,0 +1,30 @@
+if (![].includes) {
+ Array.prototype.includes = function(searchElement /*, fromIndex*/ ) {'use strict';
+ if (this === undefined || this === null) {
+ throw new TypeError('Cannot convert this value to object');
+ }
+ var O = Object(this);
+ var len = parseInt(O.length) || 0;
+ if (len === 0) {
+ return false;
+ }
+ var n = parseInt(arguments[1]) || 0;
+ var k;
+ if (n >= 0) {
+ k = n;
+ } else {
+ k = len + n;
+ if (k < 0) {k = 0;}
+ }
+ var currentElement;
+ while (k < len) {
+ currentElement = O[k];
+ if (searchElement === currentElement ||
+ (searchElement !== searchElement && currentElement !== currentElement)) {
+ return true;
+ }
+ k++;
+ }
+ return false;
+ };
+}
\ No newline at end of file
diff --git a/www/hope/hope.range.js b/www/hope/hope.range.js
new file mode 100644
index 0000000..1d09c39
--- /dev/null
+++ b/www/hope/hope.range.js
@@ -0,0 +1,247 @@
+/**
+ * hope.range
+ *
+ * This implements an immutable range object.
+ * Once created using hope.range.create() a range cannot change its start and end properties.
+ * Any method that needs to change start or end, will instead create a new range with the new start/end
+ * values and return that.
+ * If you need to change a range value in place, you can assign the return value back to the original variable, e.g.:
+ * range = range.collapse();
+ */
+
+hope.register( 'hope.range', function() {
+
+
+ function hopeRange( start, end ) {
+ if ( typeof end == 'undefined' || end < start ) {
+ end = start;
+ }
+ this.start = start;
+ this.end = end;
+ Object.freeze(this);
+ }
+
+ hopeRange.prototype = {
+ constructor: hopeRange,
+ get length () {
+ return this.end - this.start;
+ }
+ };
+
+ hopeRange.prototype.collapse = function( toEnd ) {
+ var start = this.start;
+ var end = this.end;
+ if ( toEnd ) {
+ start = end;
+ } else {
+ end = start;
+ }
+ return new hopeRange(start, end );
+ };
+
+ hopeRange.prototype.compare = function( range ) {
+ range = hope.range.create(range);
+ if ( range.start < this.start ) {
+ return 1;
+ } else if ( range.start > this.start ) {
+ return -1;
+ } else if ( range.end < this.end ) {
+ return 1;
+ } else if ( range.end > this.end ) {
+ return -1;
+ }
+ return 0;
+ };
+
+ hopeRange.prototype.equals = function( range ) {
+ return this.compare(range)===0;
+ };
+
+ hopeRange.prototype.smallerThan = function( range ) {
+ return ( this.compare( range ) == -1 );
+ };
+
+ hopeRange.prototype.largerThan = function( range ) {
+ return ( this.compare( range ) == 1 );
+ };
+
+ hopeRange.prototype.contains = function( range ) {
+ range = hope.range.create(range);
+ return this.start <= range.start && this.end >= range.end;
+ };
+
+ hopeRange.prototype.overlaps = function( range ) {
+ range = hope.range.create(range);
+ if (range.equals(this)) {
+ return true;
+ }
+
+ // not overlapping if only the edges touch...
+ if ((range.start == this.end) && (range.start < range.end)) {
+ return false;
+ }
+ if ((range.end == this.start) && (range.start < range.end)) {
+ return false;
+ }
+
+ // but overlapping if the range to check is collapsed
+ if ((range.start == this.end) && (range.start == range.end)) {
+ return true;
+ }
+ if ((range.end == this.start) && (range.start == range.end)) {
+ return true;
+ }
+
+ return ( range.start <= this.end && range.end >= this.start );
+ };
+
+ hopeRange.prototype.isEmpty = function() {
+ return this.start >= this.end;
+ };
+
+ hopeRange.prototype.overlap = function( range ) {
+ range = hope.range.create(range);
+ var start = 0;
+ var end = 0;
+ if ( this.overlaps( range ) ) {
+ if ( range.start < this.start ) {
+ start = this.start;
+ } else {
+ start = range.start;
+ }
+ if ( range.end < this.end ) {
+ end = range.end;
+ } else {
+ end = this.end;
+ }
+ }
+ return new hopeRange(start, end); // FIXME: is this range( 0, 0 ) a useful return value when there is no overlap?
+ };
+
+ hopeRange.prototype.exclude = function( range ) {
+ // return parts of this that do not overlap with range
+ var left = null;
+ var right = null;
+ if ( this.equals(range) ) {
+ // nop
+ } else if ( this.overlaps( range ) ) {
+ left = new hopeRange( this.start, range.start );
+ right = new hopeRange( range.end, this.end );
+ if ( left.isEmpty() ) {
+ left = null;
+ }
+ if ( right.isEmpty() ) {
+ right = null;
+ }
+ } else if ( this.largerThan(range) ) {
+ left = null;
+ right = right;
+ } else {
+ left = this;
+ right = left;
+ }
+ return [ left, right ];
+ };
+
+ hopeRange.prototype.excludeLeft = function( range ) {
+ return this.exclude(range)[0];
+ };
+
+ hopeRange.prototype.excludeRight = function( range ) {
+ return this.exclude(range)[1];
+ };
+
+ /**
+ * remove overlapping part of range from this range
+ * [ 5 .. 20 ].delete( 10, 25 ) => [ 5 .. 10 ]
+ * [ 5 .. 20 ].delete( 10, 15) => [ 5 .. 15 ]
+ * [ 5 .. 20 ].delete( 5, 20 ) => [ 5 .. 5 ]
+ * [ 5 .. 20 ].delete( 0, 10 ) => [ 0 .. 10 ] ?
+ */
+ hopeRange.prototype.delete = function( range ) {
+ range = hope.range.create(range);
+ var moveLeft = 0;
+ var end = this.end;
+ if ( this.overlaps(range) ) {
+ var cutRange = this.overlap( range );
+ var cutLength = cutRange.length;
+ end -= cutLength;
+ }
+ var result = new hopeRange( this.start, end );
+ var exclude = range.excludeLeft( this );
+ if ( exclude ) {
+ result = result.move( -exclude.length );
+ }
+ return result;
+ };
+
+ hopeRange.prototype.copy = function( range ) {
+ range = hope.range.create(range);
+ return new hopeRange( 0, this.overlap( range ).length );
+ };
+
+ hopeRange.prototype.extend = function( length, direction ) {
+ var start = this.start;
+ var end = this.end;
+ if ( !direction ) {
+ direction = 1;
+ }
+ if ( direction == 1 ) {
+ end += length;
+ } else {
+ start = Math.max( 0, start - length );
+ }
+ return new hopeRange(start, end);
+ };
+
+ hopeRange.prototype.toString = function() {
+ if ( this.start != this.end ) {
+ return this.start + '-' + this.end;
+ } else {
+ return this.start + '';
+ }
+ };
+
+ hopeRange.prototype.grow = function( size ) {
+ var end = this.end + size;
+ if ( end < this.start ) {
+ end = this.start;
+ }
+ return new hopeRange(this.start, end);
+ };
+
+ hopeRange.prototype.shrink = function( size ) {
+ return this.grow( -size );
+ };
+
+ hopeRange.prototype.move = function( length, min, max ) {
+ var start = this.start;
+ var end = this.end;
+ start += length;
+ end += length;
+ if ( !min ) {
+ min = 0;
+ }
+ start = Math.max( min, start );
+ end = Math.max( start, end );
+ if ( max ) {
+ start = Math.min( max, start );
+ end = Math.min( max, start );
+ }
+ return new hopeRange(start, end);
+ };
+
+ this.create = function( start, end ) {
+ if ( start instanceof hopeRange ) {
+ return start;
+ }
+ if ( typeof end =='undefined' && parseInt(start,10)==start ) {
+ end = start;
+ } else if ( Array.isArray(start) && typeof start[1] != 'undefined' ) {
+ end = start[1];
+ start = start[0];
+ }
+ return new hopeRange( parseInt(start), parseInt(end) );
+ };
+
+});
\ No newline at end of file
diff --git a/www/hope/hope.render.html.js b/www/hope/hope.render.html.js
new file mode 100644
index 0000000..212211f
--- /dev/null
+++ b/www/hope/hope.render.html.js
@@ -0,0 +1,353 @@
+hope.register( 'hope.render.html', function() {
+
+ var nestingSets = {
+ 'inline' : [ 'tt', 'u', 'strike', 'em', 'strong', 'dfn', 'code', 'samp', 'kbd', 'var', 'cite', 'abbr', 'acronym', 'sub', 'sup', 'q', 'span', 'bdo', 'a', 'object', 'img', 'bd', 'br', 'i' ],
+ 'block' : [ 'address', 'dir', 'menu', 'hr', 'li', 'table', 'p', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'pre', 'ul', 'ol', 'dl', 'div', 'blockquote', 'iframe' ]
+ };
+
+ nestingSets.all = nestingSets.block.concat( nestingSets.inline );
+
+ this.rules = {
+ nesting: {
+ 'a' : nestingSets.inline.filter( function(element) { return element != 'a'; } ),
+ 'abbr' : nestingSets.inline,
+ 'acronym' : nestingSets.inline,
+ 'address' : [ 'p' ].concat( nestingSets.inline ),
+ 'bdo' : nestingSets.inline,
+ 'blockquote': nestingSets.all,
+ 'br' : [],
+ 'caption' : nestingSets.inline,
+ 'cite' : nestingSets.inline,
+ 'code' : nestingSets.inline,
+ 'col' : [],
+ 'colgroup' : [ 'col' ],
+ 'dd' : nestingSets.all,
+ 'dfn' : nestingSets.inline,
+ 'dir' : [ 'li' ],
+ 'div' : nestingSets.all,
+ 'dl' : [ 'dt', 'dd' ],
+ 'dt' : nestingSets.inline,
+ 'em' : nestingSets.inline,
+ 'h1' : nestingSets.inline,
+ 'h2' : nestingSets.inline,
+ 'h3' : nestingSets.inline,
+ 'h4' : nestingSets.inline,
+ 'h5' : nestingSets.inline,
+ 'h6' : nestingSets.inline,
+ 'hr' : [],
+ 'img' : [],
+ 'kbd' : nestingSets.inline,
+ 'li' : nestingSets.all,
+ 'menu' : [ 'li' ],
+ 'object' : [ 'param' ].concat( nestingSets.all ),
+ 'ol' : [ 'li' ],
+ 'p' : nestingSets.inline,
+ 'pre' : nestingSets.inline,
+ 'q' : nestingSets.inline,
+ 'samp' : nestingSets.inline,
+ 'span' : nestingSets.inline,
+ 'strike' : nestingSets.inline,
+ 'strong' : nestingSets.inline,
+ 'sub' : nestingSets.inline,
+ 'sup' : nestingSets.inline,
+ 'table' : [ 'caption', 'colgroup', 'col', 'thead', 'tbody' ],
+ 'tbody' : [ 'tr' ],
+ 'td' : nestingSets.all,
+ 'th' : nestingSets.all,
+ 'thead' : [ 'tr' ],
+ 'tr' : [ 'td', 'th' ],
+ 'tt' : nestingSets.inline,
+ 'u' : nestingSets.inline,
+ 'ul' : [ 'li' ],
+ 'var' : nestingSets.inline
+ },
+ // which html elements can not have child elements at all and shouldn't be closed
+ 'noChildren' : [ 'hr', 'br', 'col', 'img' ],
+ // which html elements must have a specific child element
+ 'obligChild' : {
+ 'ol' : [ 'li' ],
+ 'ul' : [ 'li' ],
+ 'dl' : [ 'dt', 'dd' ]
+ },
+ // which html elements must have a specific parent element
+ 'obligParent' : {
+ 'li' : [ 'ul', 'ol', 'dir', 'menu' ],
+ 'dt' : [ 'dl' ],
+ 'dd' : [ 'dl' ]
+ },
+ // which html elements to allow as the top level, default is only block elements
+ 'toplevel' : nestingSets.block.concat(nestingSets.inline), // [ 'li', 'img', 'span', 'strong', 'em', 'code' ],
+ 'nestingSets' : nestingSets
+ };
+
+ this.getTag = function( markup ) {
+ return markup.split(' ')[0].toLowerCase(); // FIXME: more robust parsing needed
+ };
+
+ this.getAnnotationStack = function( annotationSet ) {
+ // { index:nextAnnotationEntry.index, entry:nextAnnotation }
+ // { start:, end:, annotation: }
+ // assert: annotationSet must only contain annotation that has overlapping ranges
+ // if not results will be unpredictable
+ var annotationStack = [];
+ if ( !annotationSet.length ) {
+ return [];
+ }
+
+ var rules = this.rules;
+
+ annotationSet.sort( function( a, b ) {
+ if ( a.range.start < b.range.start ) {
+ return -1;
+ } else if ( a.range.start > b.range.start ) {
+ return 1;
+ } else if ( a.range.end > b.range.end ) {
+ return -1;
+ } else if ( a.range.end < b.range.end ) {
+ return 1;
+ }
+
+ // if comparing ul/ol and li on the same range, ul/ol goes first;
+ if (rules.obligParent[a.tag.split(/ /)[0]]) {
+ if (rules.obligParent[a.tag.split(/ /)[0]].indexOf(b.tag.split(/ /)[0]) != -1) {
+ return 1;
+ }
+ }
+ if (rules.obligParent[b.tag.split(/ /)[0]]) {
+ if (rules.obligParent[b.tag.split(/ /)[0]].indexOf(a.tag.split(/ /)[0]) != -1) {
+ return -1;
+ }
+ }
+
+ // block elementen komen voor andere elementen
+ if (nestingSets.block.indexOf(a.tag.split(/ /)[0]) != '-1') {
+ return -1;
+ }
+ if (nestingSets.block.indexOf(b.tag.split(/ /)[0]) != '-1') {
+ return 1;
+ }
+
+ // hack om hyperlinks met images er in te laten werken.
+ if (a.tag.split(/ /)[0] == 'a') {
+ return -1;
+ }
+ if (b.tag.split(/ /)[0] == 'a') {
+ return 1;
+ }
+
+ // daarna komen inline elementen
+ if (nestingSets.inline.indexOf(a.tag.split(/ /)[0]) != '-1') {
+ return -1;
+ }
+ if (nestingSets.inline.indexOf(b.tag.split(/ /)[0]) != '-1') {
+ return 1;
+ }
+
+ return 0;
+ });
+ var unfilteredStack = [];
+ for ( var i=0, l=annotationSet.length; icommonIndex; i-- ) {
+ annotationDiff.push( { type : 'close', annotation : annotationStackFrom[i] } );
+ }
+ for ( i=commonIndex+1, l=annotationStackTo.length; i';
+ }
+ } else if ( annotationDiff[i].type == 'insert' ) {
+ renderedDiff += '<' + annotationDiff[i].annotation.tag + '>';
+ annotationTag = this.getTag( annotationDiff[i].annotation.tag );
+ if ( this.rules.noChildren.indexOf( annotationTag ) == -1 ) {
+ renderedDiff += '' + annotationTag + '>';
+ }
+ } else {
+ renderedDiff += '<' + annotationDiff[i].annotation.tag + '>';
+ }
+ }
+ return renderedDiff;
+ };
+
+ this.escape = function( content ) {
+ return content
+ .replace(/&/g, "&")
+ .replace(//g, ">")
+ .replace(/"/g, """)
+ .replace(/'/g, "'");
+ };
+
+ this.render = function( fragment ) {
+ // FIXME: annotation should be the relative annotation list to speed things up
+ var annotationSet = []; // set of applicable annotation at current position
+ var annotationStack = []; // stack of applied (valid) annotation at current position
+
+ var relativeAnnotation = fragment.annotations.getEventList();
+ var content = fragment.text.toString();
+
+ var renderedHTML = '';
+ var cursor = 0;
+
+ while ( relativeAnnotation.length ) {
+
+ var annotationChangeSet = relativeAnnotation.shift();
+ var annotationAdded = []; // list of annotation added in this change set
+ var annotationSetOnce = []; // list of annotation that can not have children, needs no close
+ for ( i=0, l=annotationChangeSet.markup.length; i 0 ) {
+ if (diffHTML && (
+ diffHTML.indexOf("
") !== -1 ||
+ diffHTML.indexOf("
") !== -1 ||
+ diffHTML.indexOf("
/g, ">");
+ n = n.replace(/"/g, """);
+
+ return n;
+ };
+
+ this.diffString = function( o, n ) {
+ o = o.replace(/\s+$/, '');
+ n = n.replace(/\s+$/, '');
+
+ var out = this.diff(o === "" ? [] : o.split(/\s+/), n === "" ? [] : n.split(/\s+/) );
+ var str = "";
+
+ var oSpace = o.match(/\s+/g);
+ if (oSpace === null) {
+ oSpace = ["\n"];
+ } else {
+ oSpace.push("\n");
+ }
+ var nSpace = n.match(/\s+/g);
+ if (nSpace === null) {
+ nSpace = ["\n"];
+ } else {
+ nSpace.push("\n");
+ }
+
+ var i;
+ if (out.n.length === 0) {
+ for ( i = 0; i < out.o.length; i++) {
+ str += '' + this.escape(out.o[i]) + oSpace[i] + "";
+ }
+ } else {
+ if (out.n[0].text === null) {
+ for (n = 0; n < out.o.length && out.o[n].text === null; n++) {
+ str += '' + this.escape(out.o[n]) + oSpace[n] + "";
+ }
+ }
+
+ for ( i = 0; i < out.n.length; i++ ) {
+ if (out.n[i].text === null) {
+ str += '' + this.escape(out.n[i]) + nSpace[i] + "";
+ } else {
+ var pre = "";
+
+ for (n = out.n[i].row + 1; n < out.o.length && out.o[n].text === null; n++ ) {
+ pre += '' + this.escape(out.o[n]) + oSpace[n] + "";
+ }
+ str += " " + out.n[i].text + nSpace[i] + pre;
+ }
+ }
+ }
+
+ return str;
+ };
+
+ this.randomColor = function() {
+ return "rgb(" + (Math.random() * 100) + "%, " +
+ (Math.random() * 100) + "%, " +
+ (Math.random() * 100) + "%)";
+ };
+
+ this.diffString2 = function( o, n ) {
+ o = o.replace(/\s+$/, '');
+ n = n.replace(/\s+$/, '');
+
+ var out = this.diff(o === "" ? [] : o.split(/\s+/), n === "" ? [] : n.split(/\s+/) );
+
+ var oSpace = o.match(/\s+/g);
+ if (oSpace === null) {
+ oSpace = ["\n"];
+ } else {
+ oSpace.push("\n");
+ }
+ var nSpace = n.match(/\s+/g);
+ if (nSpace === null) {
+ nSpace = ["\n"];
+ } else {
+ nSpace.push("\n");
+ }
+
+ var os = "";
+ var colors = [];
+ var i;
+ for (i = 0; i < out.o.length; i++) {
+ colors[i] = this.randomColor();
+
+ if (out.o[i].text !== null) {
+ os += '' +
+ this.escape(out.o[i].text) + oSpace[i] + "";
+ } else {
+ os += "" + this.escape(out.o[i]) + oSpace[i] + "";
+ }
+ }
+
+ var ns = "";
+ for (i = 0; i < out.n.length; i++) {
+ if (out.n[i].text !== null) {
+ ns += '' +
+ this.escape(out.n[i].text) + nSpace[i] + "";
+ } else {
+ ns += "" + this.escape(out.n[i]) + nSpace[i] + "";
+ }
+ }
+
+ return { o : os , n : ns };
+ };
+
+ this.diff = function( o, n ) {
+ var ns = {};
+ var os = {};
+ var i;
+
+ for ( i = 0; i < n.length; i++ ) {
+ if ( ns[ n[i] ] === null )
+ ns[ n[i] ] = { rows: [], o: null };
+ ns[ n[i] ].rows.push( i );
+ }
+
+ for ( i = 0; i < o.length; i++ ) {
+ if ( os[ o[i] ] === null )
+ os[ o[i] ] = { rows: [], n: null };
+ os[ o[i] ].rows.push( i );
+ }
+
+ for ( i in ns ) {
+ if ( ns[i].rows.length == 1 && typeof(os[i]) != "undefined" && os[i].rows.length == 1 ) {
+ n[ ns[i].rows[0] ] = { text: n[ ns[i].rows[0] ], row: os[i].rows[0] };
+ o[ os[i].rows[0] ] = { text: o[ os[i].rows[0] ], row: ns[i].rows[0] };
+ }
+ }
+
+ for ( i = 0; i < n.length - 1; i++ ) {
+ if ( n[i].text !== null && n[i+1].text === null && n[i].row + 1 < o.length && o[ n[i].row + 1 ].text === null &&
+ n[i+1] == o[ n[i].row + 1 ] )
+ {
+ n[i+1] = { text: n[i+1], row: n[i].row + 1 };
+ o[n[i].row+1] = { text: o[n[i].row+1], row: i + 1 };
+ }
+ }
+
+ for ( i = n.length - 1; i > 0; i-- ) {
+ if ( n[i].text !== null && n[i-1].text === null && n[i].row > 0 && o[ n[i].row - 1 ].text === null &&
+ n[i-1] == o[ n[i].row - 1 ] )
+ {
+ n[i-1] = { text: n[i-1], row: n[i].row - 1 };
+ o[n[i].row-1] = { text: o[n[i].row-1], row: i - 1 };
+ }
+ }
+
+ return { o: o, n: n };
+ };
+
+
+ this.isArray = function( o ) {
+ if ( Object.prototype.toString.call( o ) === '[object Array]' ) {
+ return true;
+ }
+ return false;
+ };
+
+ this.getPrototypeName = function( o ) {
+ return Object.prototype.toString.call( o );
+ };
+
+ this.isString = function( o ) {
+ if ( typeof o === 'string' ) {
+ return true;
+ }
+ return false;
+ };
+
+ this.assertTrue = function( expression ) {
+ this.countAssert++;
+ if ( expression !== true ) {
+ this.errors.push( 'test failed: expression not true at ' + this.currentTest + ' assertion ' + this.countAssert );
+ this.write( this.errors[ this.errors.length - 1 ] );
+ } else {
+ this.success++;
+ }
+ };
+
+ this.assertFalse = function( expression ) {
+ this.countAssert++;
+ if ( expression !== false ) {
+ this.errors.push( 'test failed: expression not false at ' + this.currentTest + ' assertion ' + this.countAssert );
+ this.write( this.errors[ this.errors.length - 1 ] );
+ } else {
+ this.success++;
+ }
+ };
+
+ this.assertEquals = function( var1, var2 ) {
+ var i, ii, l;
+ this.countAssert++;
+ if ( var1 === var2 ) {
+ this.success++;
+ } else {
+ var reason = '';
+ var diff;
+ if ( (typeof var1) !== (typeof var2) ) {
+ reason = 'typeof var1 '+(typeof var1)+' is not typeof var2 '+(typeof var2);
+ } else if ( var1 instanceof Object && ( this.getPrototypeName(var1) !== this.getPrototypeName(var2) ) ) {
+ reason = 'prototype of var1 '+this.getPrototypeName(var1)+' is not prototype of var2 '+this.getPrototypeName(var2);
+ } else if ( this.isString(var1) ) {
+ diff = this.diffString2(var1, var2);
+ reason = 'difference: ';
+
+ } else if ( this.isArray(var1) ) {
+ diff = [];
+ for ( i=0, l=var1.length; i 0 ) {
+ reason = 'arraydiff: ' + diff.join("\n");
+ }
+ } else if ( var1 instanceof Object ) {
+ diff = [];
+ var seen = {};
+ var count = 0;
+ for ( i in var1 ) {
+ if ( var1[i] !== var2[i] ) {
+ diff[count++] = i + ': ' + var1[i] + ' is not ' + var2[i];
+ seen[i] = true;
+ }
+ }
+ for (i in var2 ) {
+ if ( !seen[i] && var1[i] != var2[i] ) {
+ diff[count++] = i + ': ' + var1[i] + ' is not ' + var2[i];
+ }
+ }
+ if ( diff.length > 0 ) {
+ reason = 'objectdiff: ' + diff.join("\n");
+ }
+ } else {
+ reason = var1 + ' != ' + var2;
+ }
+ if ( reason ) {
+ this.errors.push( 'test failed: variables not equal at ' + this.currentTest + ' assertion ' + this.countAssert + ' reason: ' + reason );
+ this.write( this.errors[ this.errors.length - 1 ] );
+ } else {
+ this.success++;
+ }
+ }
+ };
+
+ this.run = function() {
+ this.errors = [];
+ this.success = 0;
+ for ( var i in this ) {
+ if ( i.substr(0,4)=='test' ) {
+ this.currentTest = i;
+ this.countAssert = 0;
+ this[i].call();
+ }
+ }
+ this.write( this.errors.length + ' errors; ' + this.success + ' tests succeeded.');
+ };
+
+ this.write = function( message ) {
+ var output = document.getElementById('hopeTestOutput');
+ if ( output ) {
+ //message = document.createTextNode( message );
+ var messageDiv = document.createElement( 'div' );
+ messageDiv.innerHTML = message; //appendChild( message );
+ output.appendChild( messageDiv );
+ } else {
+ console.log( message );
+ }
+ };
+
+ }
+
+ this.create = function() {
+ return new hopeTest();
+ };
+
+});
\ No newline at end of file
diff --git a/www/hope/pack b/www/hope/pack
new file mode 100755
index 0000000..3943b5f
--- /dev/null
+++ b/www/hope/pack
@@ -0,0 +1 @@
+cat hope.js hope.polyfills.js hope.range.js hope.annotation.js hope.fragment.js hope.fragment.annotations.js hope.fragment.text.js hope.render.html.js hope.events.js hope.keyboard.js hope.editor.js hope.editor.selection.js > hope.packed.js
diff --git a/www/index.html b/www/index.html
index 3cd3ef9..8446464 100644
--- a/www/index.html
+++ b/www/index.html
@@ -5,6 +5,8 @@