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 += ''; + } + } 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("= 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 += ''; + } + } 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:
'+diff.o+'
'+diff.n+'
'; + + } 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 @@