├── LICENSE ├── README ├── examples └── hope.editor.html ├── index.html ├── readme.md ├── src ├── hope.annotation.js ├── hope.editor.events.js ├── hope.editor.js ├── hope.editor.selection.js ├── hope.events.js ├── hope.fragment.annotations.js ├── hope.fragment.js ├── hope.fragment.text.js ├── hope.js ├── hope.keyboard.js ├── hope.mime.js ├── hope.polyfills.js ├── hope.range.js ├── hope.render.html.js └── hope.test.js └── tests ├── hope.fragment.annotations.html ├── hope.fragment.html ├── hope.fragment.text.html ├── hope.html └── hope.range.html /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Auke van Slooten 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README: -------------------------------------------------------------------------------- 1 | ===Hope is an alternative to markup languages.=== 2 | 3 | It is currently a proof of concept prototype to see where the ideas of Ted Nelson can lead us. 4 | 5 | Ted Nelson came up with the term Hypertext and famously defined 17 rules that a Hypertext system should conform to. His attempts to implement these in his Xanadu system haven't resulted in a finished system. But what if it had? 6 | 7 | Hope is an attempt to implement one small part of Ted's Xanadu dream: Annotation based markup. Instead of mixing the markup with the content, Hope keeps them rigorously apart. The content is nothing more than plain-text. The markup is in a seperate text file, only referencing the content using character ranges. 8 | 9 | If you are interested in researching this concept, grab the code from github and start experimenting. 10 | 11 | -------------------------------------------------------------------------------- /examples/hope.editor.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Hope: html out-of-band 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 61 | 62 | 63 | 70 |
71 |
72 |
73 | 108 | 133 |
134 |
135 |
136 |
137 | 138 |
139 |
140 |
141 | 144 | 145 | 146 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Hope: html out-of-band 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 60 | 61 | 62 | 69 |
70 |
71 |
72 | 110 | 139 |
140 |
141 |
142 |
143 | 144 |
145 |
146 |
147 | 150 | Fork me on GitHub 151 | 152 | 153 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # Hope is an alternative to markup languages. 2 | 3 | **Note: this project is superceded by [cobalt](https://github.com/poef/cobalt)** 4 | 5 | It is currently a proof of concept prototype to see where the ideas of Ted Nelson can lead us. 6 | 7 | Ted Nelson came up with the term Hypertext and famously defined 17 rules that a Hypertext system should conform to. His attempts to implement these in his Xanadu system haven't resulted in a finished system. But what if it had? 8 | 9 | Hope is an attempt to implement one small part of Ted's Xanadu dream: out-of-line markup. Instead of mixing the markup with the content, Hope keeps them rigorously apart. The content is nothing more than plain-text. The markup is in a seperate text file, only referencing the content using character ranges. 10 | 11 | The immediate goal is to build a wysiwyg editor to edit a hope fragment, because it is impossible to keep two files in sync using a normal text editor. 12 | 13 | To keep the problem as simple as possible, I'm building this as a web application. This allows me to focus only on the markup and text, leaving rendering and styling to the browser. 14 | 15 | If you are interested in researching this concept, grab the code from github and start experimenting. 16 | 17 | ## Todo 18 | 19 | - normalize annotation fragments automatically 20 | - improve html rendering 21 | - add toolbars 22 | - add common commands 23 | - add undo/redo 24 | - create a web component for the editor 25 | - build all operations from Core Range Algebra into hope.range.js 26 | - allow markup that references the whole document, not a range 27 | - allow range sequences in an annotation 28 | - allow insertion points instead of a range in an annotation 29 | - remove dependency on contenteditable for cursor handling 30 | - allow overlapping links 31 | - allow multiple annotation sets/fragments 32 | - create server-side storage with version support and full set of insertions/deletions between revisions 33 | - implement transclusion 34 | 35 | ## Issues 36 | 37 | - No support for tags like that don't enclose text 38 | - html rendering is incorrect 39 | 40 | ## Similar projects 41 | There is a relatively large interest for out-of-line markup, but it is generally trying to solve a different problem and usually uses an xml or html document as the source document to annotate. You can find these using the search terms 'standoff markup', 'parallel markup' or 'out-of-line markup'. 42 | 43 | The ones that come closest to Hope are: 44 | - [Ool - Out-of-line XML](http://simonstl.com/projects/ool/) 45 | - [Multi-Version Documents](http://multiversiondocs.blogspot.nl/) 46 | 47 | # Goals / Hopes 48 | I've been writing web software, frontend and backend since 1995 and I found that some problems haven't gone away.The most obvious one is security, e.g. Cross-Site Scripting attacks (XSS). But some problems have only grown. The entire knowledge stack needed to write web applications today is vastly more expansive and complex than in 1995, or even 2001. No browser even attempts to fully implement the current standards, or even fix bugs in years old modules. Worse the standard itself is years in the making, not just because of politics but also because of the inherent complexity of it. 49 | 50 | What the web needs is not mode high-level constructs and api's, but less. Web browsers shouldn't be trying to be full operating systems /and/ all services in one monolythic application. 51 | 52 | Take the contenteditable/designMode feature of modern browsers. Not only is the API high-level, without access to lower level abstractions. The implementation varies wildly across browsers, with numerous bugs and misfeatures. Simple extensions require writing hairy, hacky, complex code. 53 | 54 | My hope is that hope will show that most of that complexity isn't needed, if the data type you are operating on is inherently less complex. So far, it looks like this might in fact be true. 55 | 56 | # References 57 | - [Core Range Algebra](http://conferences.idealliance.org/extreme/html/2002/Nicol01/EML2002Nicol01.html) 58 | - [Embedded Markup Considered Harmfull](http://www.xml.com/pub/a/w3j/s3.nelson.html) 59 | - [Xanalogical Media: Needed Now More Than Ever](http://www.xanadu.net/NOWMORETHANEVER/XuSum99.html) 60 | 61 | -------------------------------------------------------------------------------- /src/hope.annotation.js: -------------------------------------------------------------------------------- 1 | hope.register( 'hope.annotation', function() { 2 | 3 | function hopeAnnotation(range, tag) { 4 | this.range = hope.range.create(range); 5 | this.tag = tag; 6 | Object.freeze(this); 7 | } 8 | 9 | hopeAnnotation.prototype.delete = function( range ) { 10 | return new hopeAnnotation( this.range.delete( range ), this.tag ); 11 | } 12 | 13 | hopeAnnotation.prototype.copy = function( range ) { 14 | return new hopeAnnotation( this.range.copy( range ), this.tag ); 15 | } 16 | 17 | hopeAnnotation.prototype.compare = function( annotation ) { 18 | return this.range.compare( annotation.range ); 19 | } 20 | 21 | hopeAnnotation.prototype.has = function( tag ) { 22 | //FIXME: should be able to specify attributes and attribute values as well 23 | return this.stripTag() == hope.annotation.stripTag(tag); 24 | } 25 | 26 | hopeAnnotation.prototype.toString = function() { 27 | return this.range + ':' + this.tag; 28 | } 29 | 30 | hopeAnnotation.prototype.stripTag = function() { 31 | return hope.annotation.stripTag(this.tag); 32 | } 33 | 34 | hopeAnnotation.prototype.isBlock = function() { 35 | return ( ['h1','h2','h3','p','li'].indexOf(hope.annotation.stripTag(this.tag)) != -1 ); 36 | } 37 | 38 | this.create = function( range, tag ) { 39 | return new hopeAnnotation( range, tag ); 40 | } 41 | 42 | this.stripTag = function(tag) { 43 | return tag.split(' ')[0]; 44 | } 45 | 46 | }); 47 | 48 | -------------------------------------------------------------------------------- /src/hope.editor.events.js: -------------------------------------------------------------------------------- 1 | hope.register('hope.editor.events', function() { 2 | 3 | if ( typeof hope.global.addEventListener != 'undefined' ) { 4 | this.listen = function( el, event, callback, capture ) { 5 | return el.addEventListener( event, callback, capture ); 6 | }; 7 | } else if ( typeof hope.global.attachEvent != 'undefined' ) { 8 | this.listen = function( el, event, callback, capture ) { 9 | return el.attachEvent( 'on' + event, function() { 10 | var evt = hope.global.event; 11 | var self = evt.srcElement; 12 | if ( !self ) { 13 | self = hope.global; 14 | } 15 | return callback.call( self, evt ); 16 | } ); 17 | }; 18 | } else { 19 | throw new hope.Exception( 'Browser is not supported', 'hope.editor.events.1' ); 20 | } 21 | 22 | this.cancel = function( evt ) { 23 | if ( typeof evt.stopPropagation != 'undefined' ) { 24 | evt.stopPropagation(); 25 | } 26 | if ( typeof evt.preventDefault != 'undefined' ) { 27 | evt.preventDefault(); 28 | } 29 | if ( typeof evt.cancelBubble != 'undefined' ) { 30 | evt.cancelBubble = true; 31 | } 32 | return false; 33 | } 34 | 35 | } ); -------------------------------------------------------------------------------- /src/hope.editor.js: -------------------------------------------------------------------------------- 1 | hope.register( 'hope.editor', function() { 2 | 3 | function hopeEditor( textEl, annotationsEl, outputEl, renderEl ) { 4 | this.refs = { 5 | text: textEl, 6 | annotations: annotationsEl, 7 | output: outputEl, 8 | render: renderEl 9 | }; 10 | this.selection = hope.editor.selection.create(0,0,this); 11 | this.commandsKeyUp = {}; 12 | 13 | var text = this.refs.text.value; 14 | var annotations = this.refs.annotations.value; 15 | this.fragment = hope.fragment.create( text, annotations ); 16 | this.refs.output.contentEditable = true; 17 | this.update(); 18 | initEvents(this); 19 | } 20 | 21 | function initEvents(editor) { 22 | hope.events.listen(editor.refs.output, 'keypress', function( evt ) { 23 | if ( !evt.ctrlKey && !evt.altKey ) { 24 | // check selection length 25 | // remove text in selection 26 | // add character 27 | var range = editor.selection.getRange(); 28 | var charCode = evt.which || evt.keyCode; 29 | var charTyped = String.fromCharCode(charCode); 30 | if ( charTyped ) { // ignore non printable characters 31 | if ( range.length ) { 32 | editor.fragment = editor.fragment.delete(range); 33 | } 34 | editor.fragment = editor.fragment.insert(range.start, charTyped ); 35 | editor.selection.collapse().move(1); 36 | setTimeout( function() { 37 | editor.update(); 38 | }, 0 ); 39 | } 40 | } 41 | return hope.events.cancel(evt); 42 | }); 43 | 44 | hope.events.listen(editor.refs.output, 'keydown', function( evt ) { 45 | var key = hope.keyboard.getKey( evt ); 46 | if ( editor.commands[key] ) { 47 | var range = editor.selection.getRange(); 48 | editor.commands[key].call(editor, range); 49 | setTimeout( function() { 50 | editor.update(); 51 | }, 0); 52 | return hope.events.cancel(evt); 53 | } else if ( evt.ctrlKey || evt.altKey ) { 54 | return hope.events.cancel(evt); 55 | } 56 | }); 57 | 58 | hope.events.listen(editor.refs.output, 'keyup', function( evt ) { 59 | var key = hope.keyboard.getKey( evt ); 60 | if ( editor.selection.cursorCommands.indexOf(key)<0 ) { 61 | if ( editor.commandsKeyUp[key] ) { 62 | var range = editor.selection.getRange(); 63 | editor.commandsKeyUp[key].call(editor, range); 64 | setTimeout( function() { 65 | editor.update(); 66 | }, 0); 67 | } 68 | return hope.events.cancel(evt); 69 | } 70 | }); 71 | 72 | } 73 | 74 | hopeEditor.prototype.getEditorRange = function(start, end ) { 75 | var treeWalker = document.createTreeWalker( 76 | this.refs.output, 77 | NodeFilter.SHOW_TEXT, 78 | function(node) { 79 | return NodeFilter.FILTER_ACCEPT; 80 | }, 81 | false 82 | ); 83 | var offset = 0; 84 | var node = null; 85 | var range = document.createRange(); 86 | var lastNode = null; 87 | do { 88 | lastNode = node; 89 | node = treeWalker.nextNode(); 90 | if ( node ) { 91 | offset += node.textContent.length; 92 | } 93 | } while ( offset < start && node ); 94 | if ( !node ) { 95 | range.setStart(lastNode, lastNode.textContent.length ); 96 | range.setEnd(lastNode, lastNode.textContent.length ); 97 | return range; 98 | } 99 | var preOffset = offset - node.textContent.length; 100 | range.setStart(node, start - preOffset ); 101 | while ( offset < end && node ) { 102 | node = treeWalker.nextNode(); 103 | if ( node ) { 104 | offset += node.textContent.length; 105 | } 106 | } 107 | if ( !node ) { 108 | range.setEnd(lastNode, lastNode.textContent); 109 | return range; 110 | } 111 | var preOffset = offset - node.textContent.length; 112 | range.setEnd(node, end - preOffset ); 113 | return range; 114 | } 115 | 116 | hopeEditor.prototype.showCursor = function() { 117 | var range = this.selection.getRange(); 118 | var selection = this.getEditorRange(range.start, range.end); 119 | var htmlSelection = window.getSelection(); 120 | htmlSelection.removeAllRanges(); 121 | htmlSelection.addRange(selection); 122 | } 123 | 124 | 125 | hopeEditor.prototype.getBlockAnnotation = function( position ) { 126 | var annotations = this.fragment.annotations.getAt( position ); 127 | for ( var i=annotations.length()-1; i>=0; i--) { 128 | if ( this.isBlockTag( annotations.get(i).tag ) ) { 129 | // this is the nearest defined block annotation 130 | return annotations.get(i); 131 | } 132 | } 133 | return null; // FIXME: define a null block annotation and return it with full range of document 134 | } 135 | 136 | hopeEditor.prototype.isBlockTag = function( tag ) { 137 | tag = hope.annotation.stripTag(tag); 138 | return ['h1','h2','h3','p','li'].includes(tag); 139 | } 140 | 141 | hopeEditor.prototype.getNextBlockTag = function( tag ) { 142 | tag = hope.annotation.stripTag(tag); 143 | var tagOrder = { 144 | 'h1' : 'p', 145 | 'h2' : 'p', 146 | 'h3' : 'p', 147 | 'p' : 'p', 148 | 'li' : 'li' 149 | }; 150 | if ( typeof tagOrder[tag] != 'undefined' ) { 151 | return tagOrder[tag]; 152 | } 153 | return 'p'; 154 | } 155 | 156 | hopeEditor.prototype.commands = { 157 | 'Control+b': function(range) { 158 | this.fragment = this.fragment.toggle(range, 'strong'); 159 | }, 160 | 'Control+i': function(range) { 161 | this.fragment = this.fragment.toggle(range, 'em'); 162 | }, 163 | 'Backspace' : function(range) { 164 | if ( range.isEmpty() ) { 165 | range = range.extend(1, -1); 166 | } 167 | // check if range extends over multiple block annotations 168 | // if so remove all block annotations except the first 169 | // and expand that to cover full range of first upto last block annotation 170 | this.fragment = this.fragment.delete( range ); 171 | this.selection.collapse().move(-1); 172 | }, 173 | 'Delete' : function(range) { 174 | if ( range.isEmpty() ) { 175 | range = range.extend(1, 1); 176 | } 177 | this.fragment = this.fragment.delete( range ); 178 | this.selection.collapse(); 179 | }, 180 | 'Shift+Enter' : function(range) { 181 | this.fragment = this.fragment 182 | .delete( range ) 183 | .insert( range.start, "\n" ) 184 | .apply( [ range.start, range.start + 1 ], 'br' ); 185 | this.selection.collapse().move(1); 186 | }, 187 | 'Enter' : function(range) { 188 | var br = this.fragment.has( [range.start-1, range.start], 'br' ); 189 | if ( br ) { 190 | var blockAnnotation = this.getBlockAnnotation( range.start ); 191 | // close it and find which annotation to apply next 192 | var closingAnnotation = hope.annotation.create( [ blockAnnotation.range.start, br.range.start ], blockAnnotation.tag ); 193 | var openingAnnotation = hope.annotation.create( [ range.start, blockAnnotation.range.end + 1 ], this.getNextBlockTag( blockAnnotation.tag ) ); 194 | this.fragment = this.fragment 195 | .remove( br.range, br.tag ) 196 | .remove( blockAnnotation.range, blockAnnotation.tag ) 197 | .insert(range.start, '\n') 198 | .apply( closingAnnotation.range, closingAnnotation.tag ) 199 | .apply( openingAnnotation.range, openingAnnotation.tag ) 200 | ; 201 | } else { 202 | this.fragment = this.fragment 203 | .delete( range ) 204 | .insert( range.start, "\n" ) 205 | .apply( [ range.start, range.start + 1 ], 'br' ); 206 | } 207 | this.selection.collapse().move(1); 208 | } 209 | }; 210 | 211 | hopeEditor.prototype.update = function() { 212 | var html = hope.render.html.render( this.fragment ); 213 | this.refs.output.innerHTML = html; 214 | this.showCursor(); 215 | 216 | if ( this.refs.text ) { 217 | this.refs.text.value = ''+this.fragment.text; 218 | } 219 | if ( this.refs.render ) { 220 | this.refs.render.innerHTML = html.replace('&','&').replace('<', '<').replace('>', '>'); 221 | } 222 | if ( this.refs.annotations ) { 223 | this.refs.annotations.innerHTML = this.fragment.annotations+''; 224 | } 225 | } 226 | 227 | hopeEditor.prototype.command = function( key, callback, keyup ) { 228 | if ( keyup ) { 229 | this.commandsKeyUp[key] = callback; 230 | } else { 231 | this.commands[key] = callback; 232 | } 233 | } 234 | 235 | this.create = function( textEl, annotationsEl, outputEl, previewEl ) { 236 | return new hopeEditor( textEl, annotationsEl, outputEl, previewEl); 237 | } 238 | 239 | }); -------------------------------------------------------------------------------- /src/hope.editor.selection.js: -------------------------------------------------------------------------------- 1 | hope.register( 'hope.editor.selection', function() { 2 | 3 | function hopeEditorSelection(start, end, editor) { 4 | this.start = start; 5 | this.end = end; 6 | this.editor = editor; 7 | var self = this; 8 | hope.events.listen(this.editor.refs.output, 'keyup', function(evt) { 9 | var key = hope.keyboard.getKey( evt ); 10 | switch ( key ) { 11 | case 'Shift+Home': 12 | case 'Shift+End': 13 | case 'Shift+PageDown': 14 | case 'Shift+PageUp' : 15 | case 'Shift+ArrowDown' : 16 | case 'Shift+ArrowUp': 17 | case 'Shift+ArrowLeft': 18 | case 'Shift+ArrowRight': 19 | var sel = window.getSelection(); 20 | self.end = self.getTotalOffset( sel.focusNode ) + sel.focusOffset; 21 | break; 22 | case 'Home': 23 | case 'End': 24 | case 'PageDown': 25 | case 'PageUp' : 26 | case 'ArrowDown' : 27 | case 'ArrowUp': 28 | case 'ArrowLeft': 29 | case 'ArrowRight': 30 | var sel = window.getSelection(); 31 | self.start = self.end = self.getTotalOffset( sel.focusNode ) + sel.focusOffset; 32 | break; 33 | } 34 | }); 35 | 36 | hope.events.listen(this.editor.refs.output, 'mouseup', function(evt) { 37 | var sel = window.getSelection(); 38 | self.end = self.getTotalOffset( sel.focusNode ) + sel.focusOffset; 39 | self.start = self.getTotalOffset( sel.anchorNode ) + sel.anchorOffset; 40 | }); 41 | 42 | } 43 | 44 | hopeEditorSelection.prototype.cursorCommands = [ 45 | 'Shift+Home', 46 | 'Shift+End', 47 | 'Shift+PageDown', 48 | 'Shift+PageUp', 49 | 'Shift+ArrowDown', 50 | 'Shift+ArrowUp', 51 | 'Shift+ArrowLeft', 52 | 'Shift+ArrowRight', 53 | 'Home', 54 | 'End', 55 | 'PageDown', 56 | 'PageUp', 57 | 'ArrowDown', 58 | 'ArrowUp', 59 | 'ArrowLeft', 60 | 'ArrowRight', 61 | ]; 62 | 63 | hopeEditorSelection.prototype.getRange = function() { 64 | if ( this.start <= this.end ) { 65 | return hope.range.create( this.start, this.end ); 66 | } else { 67 | return hope.range.create( this.end, this.start ); 68 | } 69 | } 70 | 71 | hopeEditorSelection.prototype.getCursor = function () { 72 | return this.end; 73 | } 74 | 75 | hopeEditorSelection.prototype.collapse = function(toEnd) { 76 | var r = this.getRange().collapse(toEnd); 77 | this.start = r.start; 78 | this.end = r.end; 79 | return this; 80 | } 81 | 82 | hopeEditorSelection.prototype.move = function(distance) { 83 | this.start = Math.min( editor.fragment.text.length, Math.max( 0, this.start + distance ) ); 84 | this.end = Math.min( editor.fragment.text.length, Math.max( 0, this.end + distance ) ); 85 | return this; 86 | } 87 | 88 | hopeEditorSelection.prototype.isEmpty = function() { 89 | return ( this.start==this.end ); 90 | } 91 | 92 | hopeEditorSelection.prototype.grow = function(size) { 93 | this.end = Math.min( editor.fragment.text.length, Math.max( 0, this.end + size ) ); 94 | return this; 95 | } 96 | 97 | hopeEditorSelection.prototype.getNextTextNode = function(textNode) { 98 | var treeWalker = document.createTreeWalker( 99 | this.editor.refs.output, 100 | NodeFilter.SHOW_TEXT, 101 | function(node) { 102 | return NodeFilter.FILTER_ACCEPT; 103 | }, 104 | false 105 | ); 106 | treeWalker.currentNode = textNode; 107 | return treeWalker.nextNode(); 108 | } 109 | 110 | hopeEditorSelection.prototype.getPrevTextNode = function(textNode) { 111 | var treeWalker = document.createTreeWalker( 112 | this.editor.refs.output, 113 | NodeFilter.SHOW_TEXT, 114 | function(node) { 115 | return NodeFilter.FILTER_ACCEPT; 116 | }, 117 | false 118 | ); 119 | treeWalker.currentNode = textNode; 120 | return treeWalker.previousNode(); 121 | } 122 | 123 | hopeEditorSelection.prototype.getTotalOffset = function( node ) { 124 | offset = 0; 125 | while ( node = this.getPrevTextNode(node) ) { 126 | offset += node.textContent.length; 127 | } 128 | return offset; 129 | } 130 | 131 | hopeEditorSelection.prototype.getArrowDownPosition = function() { 132 | // FIXME: handle columns, floats, etc. 133 | // naive version here expects lines of similar size and position 134 | // without changes in textflow 135 | var cursorEl = this.editor.refs.output.ownerDocument.getElementById('hopeCursor'); 136 | if ( !cursorEl ) { 137 | return null; 138 | } 139 | var cursorRect = cursorEl.getBoundingClientRect(); 140 | if ( this.xBias == null ) { 141 | this.xBias = cursorRect.left; 142 | console.log('set xbias: '+this.xBias); 143 | } 144 | var node = cursorEl; // will this work? -> not a text node 145 | var nodeRect = null; 146 | var range = null; 147 | var rangeRect = null; 148 | var yBias = null; 149 | // find textnode to place cursor in 150 | do { 151 | node = this.getNextTextNode(node); 152 | if ( node ) { 153 | range = document.createRange(); 154 | range.setStart(node, 0); 155 | range.setEnd(node, node.textContent.length); 156 | nodeRect = range.getBoundingClientRect(); 157 | if ( !yBias ) { 158 | if ( nodeRect.top > cursorRect.top ) { 159 | yBias = nodeRect.top; 160 | } else { 161 | yBias = cursorRect.top; 162 | } 163 | } 164 | } 165 | } while ( node && nodeRect.height!=0 && nodeRect.top <= yBias ); //< cursorRect.bottom ); //left >= this.xBias ); 166 | 167 | if ( node && nodeRect.right >= this.xBias ) { 168 | // find range in textnode to set cursor to 169 | var nodeLength = node.textContent.length; 170 | range.setEnd( node, 0 ); 171 | var offset = 0; 172 | do { 173 | offset++; 174 | range.setStart( node, offset) 175 | range.setEnd( node, offset); 176 | rangeRect = range.getBoundingClientRect(); 177 | } while ( 178 | offset < nodeLength 179 | && ( (rangeRect.top <= yBias ) 180 | || ( rangeRect.right < this.xBias) ) 181 | ); 182 | 183 | return range.endOffset + this.getTotalOffset(node); // should check distance for end-1 as well 184 | } else if ( node && range ) { 185 | range.setStart( range.endContainer, range.endOffset ); 186 | rangeRect = range.getBoundingClientRect(); 187 | if ( rangeRect.top > yBias ) { 188 | // cannot set cursor to x pos > xBias, so get rightmost position in current node 189 | range.setEnd(node, node.textContent.length); 190 | return range.endOffset + this.getTotalOffset(node); 191 | } else { 192 | // cursor cannot advance further 193 | return this.getCursor(); 194 | } 195 | } else { 196 | return this.getCursor(); 197 | } 198 | } 199 | 200 | 201 | this.create = function(start, end, editor) { 202 | return new hopeEditorSelection(start, end, editor); 203 | } 204 | 205 | }); -------------------------------------------------------------------------------- /src/hope.events.js: -------------------------------------------------------------------------------- 1 | hope.register('hope.events', function() { 2 | 3 | if ( typeof hope.global.addEventListener != 'undefined' ) { 4 | this.listen = function( el, event, callback, capture ) { 5 | return el.addEventListener( event, callback, capture ); 6 | }; 7 | } else if ( typeof hope.global.attachEvent != 'undefined' ) { 8 | this.listen = function( el, event, callback, capture ) { 9 | return el.attachEvent( 'on' + event, function() { 10 | var evt = hope.global.event; 11 | var self = evt.srcElement; 12 | if ( !self ) { 13 | self = hope.global; 14 | } 15 | return callback.call( self, evt ); 16 | } ); 17 | }; 18 | } else { 19 | throw new hope.Exception( 'Browser is not supported', 'hope.editor.events.1' ); 20 | } 21 | 22 | this.cancel = function( evt ) { 23 | if ( typeof evt.stopPropagation != 'undefined' ) { 24 | evt.stopPropagation(); 25 | } 26 | if ( typeof evt.preventDefault != 'undefined' ) { 27 | evt.preventDefault(); 28 | } 29 | if ( typeof evt.cancelBubble != 'undefined' ) { 30 | evt.cancelBubble = true; 31 | } 32 | return false; 33 | } 34 | 35 | } ); -------------------------------------------------------------------------------- /src/hope.fragment.annotations.js: -------------------------------------------------------------------------------- 1 | hope.register( 'hope.fragment.annotations', function() { 2 | 3 | function parseMarkup( annotations ) { 4 | var reMarkupLine = /^(?:([0-9]+)(?:-([0-9]+))?:)?(.*)$/m; 5 | var matches = []; 6 | var list = []; 7 | var annotation = null; 8 | while ( annotations && ( matches = annotations.match(reMarkupLine) ) ) { 9 | if ( matches[2] ) { 10 | annotation = hope.annotation.create( 11 | [ parseInt(matches[1]), parseInt(matches[2]) ], 12 | matches[3] 13 | ); 14 | } else { 15 | annotation = hope.annotation.create( 16 | matches[1], matches[3] 17 | ); 18 | } 19 | list.push(annotation); 20 | annotations = annotations.substr( matches[0].length + 1 ); 21 | } 22 | return list; 23 | } 24 | 25 | function hopeAnnotationList( annotations ) { 26 | this.list = []; 27 | if ( annotations instanceof hopeAnnotationList ) { 28 | this.list = annotations.list; 29 | } else if ( Array.isArray( annotations) ) { 30 | this.list = annotations; 31 | } else { 32 | this.list = parseMarkup( annotations + '' ); 33 | } 34 | this.list.sort( function( a, b ) { 35 | return a.compare( b ); 36 | }); 37 | } 38 | 39 | hopeAnnotationList.prototype.toString = function() { 40 | var result = ''; 41 | for ( var i=0, l=this.list.length; i0); 51 | //}); 52 | list.sort( function( a, b ) { 53 | return a.compare( b ); 54 | }); 55 | return new hopeAnnotationList(list); 56 | }; 57 | 58 | hopeAnnotationList.prototype.apply = function( range, tag ) { 59 | var list = this.list.slice(); 60 | list.push( hope.annotation.create( range, tag ) ); 61 | return new hopeAnnotationList(list).clean(); 62 | }; 63 | 64 | hopeAnnotationList.prototype.grow = function( position, size ) { 65 | function getBlockIndexes(list, index, position) { 66 | var blockIndexes = []; 67 | for ( var i=index-1; i>=0; i-- ) { 68 | if ( list[i].range.contains([position-1, position]) && list[i].isBlock() ) { 69 | blockIndexes.push(i); 70 | } 71 | } 72 | return blockIndexes; 73 | } 74 | 75 | var list = this.list.slice(); 76 | var removeRange = false; 77 | var growRange = false; 78 | var removeList = []; 79 | if ( size < 0 ) { 80 | var removeRange = hope.range.create( position + size, position ); 81 | } else { 82 | var growRange = hope.range.create( position, position + size ); 83 | } 84 | for ( var i=0, l=list.length; i= list[i].range.start && removeRange.start <= list[i].range.start ) { 89 | // range to remove overlaps start of this range, but is not equal 90 | if ( list[i].isBlock() ) { 91 | // block annotation must be merged with previous annotation, if available 92 | // get block annotation at start of removeRange 93 | var prevBlockIndexes = getBlockIndexes(list, i, removeRange.start); 94 | if ( prevBlockIndexes.length == 0 ) { 95 | // no block element in removeRange.start, so just move this block element 96 | list[i] = hope.annotation.create( list[i].range.delete( removeRange ), list[i].tag ); 97 | } else { 98 | // prevBlocks must now contain this block 99 | for ( var ii=0, ll=prevBlockIndexes.length; ii= list[i].range.end ) { 114 | // range to remove overlaps end of this range, but is not equal 115 | // if this range needs to be extended, that will done when we find the next block range 116 | // so just shrink this range 117 | list[i] = hope.annotation.create( list[i].range.delete( removeRange ), list[i].tag ); 118 | } 119 | } else if (growRange) { 120 | if ( list[i].range.start > position ) { 121 | var range = list[i].range.move( size, position ); 122 | list[i] = hope.annotation.create( range, list[i].tag ); 123 | } else if ( list[i].range.end >= position ) { 124 | var range = list[i].range.grow( size ); 125 | list[i] = hope.annotation.create( range, list[i].tag ); 126 | } 127 | } 128 | } 129 | // now remove indexes in removeList from list 130 | for ( var i=removeList.length-1; i>=0; i--) { 131 | list.splice( removeList[i], 1); 132 | } 133 | return new hopeAnnotationList(list).clean(); 134 | }; 135 | 136 | hopeAnnotationList.prototype.clear = function( range ) { 137 | range = hope.range.create(range); 138 | var list = this.list.slice(); 139 | var remove = []; 140 | for ( var i=0, l=list.length; i range.start ) { 147 | list[i] = hope.annotation.create( [range.end, listRange.end], list[i].tag ); 148 | } else if ( listRange.end <= range.end ) { 149 | list[i] = hope.annotation.create( [listRange.start, range.start], list[i].tag ); 150 | } 151 | } 152 | } 153 | for ( var i=remove.length-1; i>=0; i--) { 154 | list.splice( remove[i], 1); 155 | } 156 | return new hopeAnnotationList(list).clean(); 157 | } 158 | 159 | hopeAnnotationList.prototype.remove = function( range, tag ) { 160 | range = hope.range.create(range); 161 | var list = this.list.slice(); 162 | var remove = []; 163 | var add = []; 164 | for ( var i=0, l=list.length; irange.end) { 180 | // range is enclosed entirely in annotation range 181 | list[i] = hope.annotation.create( 182 | [ listRange.start, range.start ], 183 | list[i].tag 184 | ); 185 | add.push( hope.annotation.create( 186 | [ range.end, listRange.end ], 187 | list[i].tag 188 | )); 189 | } else if ( listRange.start < range.start ) { 190 | // range overlaps annotation to the right 191 | list[i] = hope.annotation.create( 192 | [ listRange.start, range.start ], 193 | list[i].tag 194 | ); 195 | } else if ( listRange.end > range.end ) { 196 | // range overlaps annotation to the left 197 | list[i] = hope.annotation.create( 198 | [ range.end, listRange.end ], 199 | list[i].tag 200 | ); 201 | } 202 | 203 | } 204 | for ( var i=remove.length-1;i>=0; i--) { 205 | list.splice( remove[i], 1); 206 | } 207 | list = list.concat(add); 208 | return new hopeAnnotationList(list).clean(); 209 | } 210 | 211 | hopeAnnotationList.prototype.delete = function( range ) { 212 | range = hope.range.create(range); 213 | return this.grow( range.end, -range.length ); 214 | }; 215 | 216 | hopeAnnotationList.prototype.copy = function( range ) { 217 | range = hope.range.create(range); 218 | var copy = []; 219 | for ( var i=0, l=this.list.length; i 0 ) { 303 | current++; 304 | } 305 | if ( current < 0 ) { 306 | current = 0; 307 | } 308 | if ( !groupedList[current] ) { 309 | groupedList[current] = { offset: eventList[i].offset, markup: [] }; 310 | } 311 | groupedList[current].markup.push( { type: eventList[i].type, index: eventList[i].index } ); 312 | } 313 | return groupedList; 314 | } 315 | 316 | var relativeList = getUnsortedEventList.call(this); 317 | relativeList.sort(function(a,b) { 318 | if ( a.offset < b.offset ) { 319 | return -1; 320 | } else if ( a.offset > b.offset ) { 321 | return 1; 322 | } 323 | return 0; 324 | }); 325 | relativeList = calculateRelativeOffsets.call( this, relativeList ); 326 | relativeList = groupByOffset.call( this, relativeList ); 327 | return relativeList; 328 | } 329 | 330 | this.create = function( annotations ) { 331 | return new hopeAnnotationList( annotations ); 332 | } 333 | 334 | }); -------------------------------------------------------------------------------- /src/hope.fragment.js: -------------------------------------------------------------------------------- 1 | hope.register( 'hope.fragment', function() { 2 | 3 | var self = this; 4 | 5 | function hopeFragment( text, annotations ) { 6 | this.text = hope.fragment.text.create( text ); 7 | this.annotations = hope.fragment.annotations.create( annotations ); 8 | Object.freeze(this); 9 | } 10 | 11 | hopeFragment.prototype.delete = function( range ) { 12 | return new hopeFragment( 13 | this.text.delete( range ), 14 | this.annotations.delete( range ) 15 | ); 16 | }; 17 | 18 | hopeFragment.prototype.copy = function( range ) { 19 | // return copy fragment at range with the content and annotations at that range 20 | return new hopeFragment( 21 | this.text.copy( range ), 22 | this.annotations.copy( range ).delete( hope.range.create( 0, range.start ) ) 23 | ); 24 | }; 25 | 26 | hopeFragment.prototype.insert = function( position, fragment ) { 27 | if ( ! ( fragment instanceof hopeFragment ) ) { 28 | fragment = new hopeFragment( fragment ); 29 | } 30 | var result = new hopeFragment( 31 | this.text.insert(position, fragment.text), 32 | this.annotations.grow(position, fragment.text.length ) 33 | ); 34 | for ( var i=0, l=fragment.annotations.length; i= range.end ) { 18 | return this; 19 | } else { 20 | return new hopeTextFragment( this.content.slice( 0, range.start ) + this.content.slice( range.end ) ); 21 | } 22 | }; 23 | 24 | hopeTextFragment.prototype.copy = function( range ) { 25 | range = hope.range.create(range); 26 | // return copy of content at range 27 | return new hopeTextFragment( this.content.slice( range.start, range.end ) ); 28 | }; 29 | 30 | hopeTextFragment.prototype.insert = function( position, content ) { 31 | // insert fragment at range, return cut fragment 32 | return new hopeTextFragment( this.content.slice( 0, position ) + content + this.content.slice( position ) ); 33 | }; 34 | 35 | hopeTextFragment.prototype.toString = function() { 36 | return this.content; 37 | } 38 | 39 | hopeTextFragment.prototype.search = function( re, matchIndex ) { 40 | function escapeRegExp(s) { 41 | return s.replace(/[-/\\^$*+?.()|[\]{}]/g, '\\$&') 42 | } 43 | if ( ! ( re instanceof RegExp ) ) { 44 | re = new RegExp( escapeRegExp( re ) , 'g' ); 45 | } 46 | var result = []; 47 | var match = null; 48 | if ( !matchIndex ) { 49 | matchIndex = 0; 50 | } 51 | while ( ( match = re.exec( this.content ) ) != null ) { 52 | result.push( hope.range.create( match.index, match.index + match[matchIndex].length ) ); 53 | if ( !re.global ) { 54 | break; 55 | } 56 | } 57 | return result; 58 | } 59 | 60 | this.create = function( content ) { 61 | return new hopeTextFragment( content ); 62 | } 63 | 64 | }); -------------------------------------------------------------------------------- /src/hope.js: -------------------------------------------------------------------------------- 1 | var hope = this.hope = ( function( global ) { 2 | 3 | var registered = {}; 4 | var hope = {}; 5 | 6 | function _namespaceWalk( module, handler ) { 7 | var rest = module.replace(/^\s+|\s+$/g, ''); //trim 8 | var name = ''; 9 | var temp = hope.global; 10 | var i = rest.indexOf( '.' ); 11 | while ( i != -1 ) { 12 | name = rest.substring( 0, i ); 13 | if ( !temp[name]) { 14 | temp = handler(temp, name); 15 | if (!temp) { 16 | return temp; 17 | } 18 | } 19 | temp = temp[name]; 20 | rest = rest.substring( i + 1 ); 21 | i = rest.indexOf( '.' ); 22 | } 23 | if ( rest ) { 24 | if ( !temp[rest] ) { 25 | temp = handler(temp, rest); 26 | if (!temp) { 27 | return temp; 28 | } 29 | } 30 | temp = temp[rest]; 31 | } 32 | return temp; 33 | } 34 | 35 | hope.global = global; 36 | 37 | hope.register = function( module, implementation ) { 38 | var moduleInstance = _namespaceWalk( module, function(ob, name) { 39 | ob[name] = {}; 40 | return ob; 41 | }); 42 | registered[module]=true; 43 | if (typeof implementation == 'function') { 44 | implementation.call(moduleInstance); 45 | } 46 | return moduleInstance; 47 | }; 48 | 49 | hope.Exception = function(message, code) { 50 | this.message = message; 51 | this.code = code; 52 | this.name = 'hope.Exception'; 53 | } 54 | 55 | return hope; 56 | 57 | } )(this); 58 | 59 | -------------------------------------------------------------------------------- /src/hope.keyboard.js: -------------------------------------------------------------------------------- 1 | hope.register( 'hope.keyboard', function() { 2 | 3 | var self = this; 4 | 5 | var keyCodes = []; 6 | keyCodes[3] = 'Cancel'; 7 | keyCodes[6] = 'Help'; 8 | keyCodes[8] = 'Backspace'; 9 | keyCodes[9] = 'Tab'; 10 | keyCodes[12] = 'Numlock-5'; 11 | keyCodes[13] = 'Enter'; 12 | 13 | keyCodes[16] = 'Shift'; 14 | keyCodes[17] = 'Control'; 15 | keyCodes[18] = 'Alt'; 16 | keyCodes[19] = 'Pause'; 17 | keyCodes[20] = 'CapsLock'; 18 | keyCodes[21] = 'KanaMode'; //HANGUL 19 | 20 | keyCodes[23] = 'JunjaMode'; 21 | keyCodes[24] = 'FinalMode'; 22 | keyCodes[25] = 'HanjaMode'; //KANJI 23 | 24 | keyCodes[27] = 'Escape'; 25 | keyCodes[28] = 'Convert'; 26 | keyCodes[29] = 'NonConvert'; 27 | keyCodes[30] = 'Accept'; 28 | keyCodes[31] = 'ModeChange'; 29 | keyCodes[32] = 'Spacebar'; 30 | keyCodes[33] = 'PageUp'; 31 | keyCodes[34] = 'PageDown'; 32 | keyCodes[35] = 'End'; 33 | keyCodes[36] = 'Home'; 34 | keyCodes[37] = 'ArrowLeft'; 35 | keyCodes[38] = 'ArrowUp'; 36 | keyCodes[39] = 'ArrowRight'; // opera has this as a "'" as well... 37 | keyCodes[40] = 'ArrowDown'; 38 | keyCodes[41] = 'Select'; 39 | keyCodes[42] = 'Print'; 40 | keyCodes[43] = 'Execute'; 41 | keyCodes[44] = 'PrintScreen'; // opera ';'; 42 | keyCodes[45] = 'Insert'; // opera has this as a '-' as well... 43 | keyCodes[46] = 'Delete'; // opera - ','; 44 | keyCodes[47] = '/'; // opera 45 | 46 | keyCodes[59] = ';'; 47 | keyCodes[60] = '<'; 48 | keyCodes[61] = '='; 49 | keyCodes[62] = '>'; 50 | keyCodes[63] = '?'; 51 | keyCodes[64] = '@'; 52 | 53 | keyCodes[91] = 'OS'; // opera '['; 54 | keyCodes[92] = 'OS'; // opera '\\'; 55 | keyCodes[93] = 'ContextMenu'; // opera ']'; 56 | keyCodes[95] = 'Sleep'; 57 | keyCodes[96] = '`'; 58 | 59 | keyCodes[106] = '*'; // keypad 60 | keyCodes[107] = '+'; // keypad 61 | keyCodes[109] = '-'; // keypad 62 | keyCodes[110] = 'Separator'; 63 | keyCodes[111] = '/'; // keypad 64 | 65 | keyCodes[144] = 'NumLock'; 66 | keyCodes[145] = 'ScrollLock'; 67 | 68 | keyCodes[160] = '^'; 69 | keyCodes[161] = '!'; 70 | keyCodes[162] = '"'; 71 | keyCodes[163] = '#'; 72 | keyCodes[164] = '$'; 73 | keyCodes[165] = '%'; 74 | keyCodes[166] = '&'; 75 | keyCodes[167] = '_'; 76 | keyCodes[168] = '('; 77 | keyCodes[169] = ')'; 78 | keyCodes[170] = '*'; 79 | keyCodes[171] = '+'; 80 | keyCodes[172] = '|'; 81 | keyCodes[173] = '-'; 82 | keyCodes[174] = '{'; 83 | keyCodes[175] = '}'; 84 | keyCodes[176] = '~'; 85 | 86 | keyCodes[181] = 'VolumeMute'; 87 | keyCodes[182] = 'VolumeDown'; 88 | keyCodes[183] = 'VolumeUp'; 89 | 90 | keyCodes[186] = ';'; 91 | keyCodes[187] = '='; 92 | keyCodes[188] = ','; 93 | keyCodes[189] = '-'; 94 | keyCodes[190] = '.'; 95 | keyCodes[191] = '/'; 96 | keyCodes[192] = '`'; 97 | 98 | keyCodes[219] = '['; 99 | keyCodes[220] = '\\'; 100 | keyCodes[221] = ']'; 101 | keyCodes[222] = "'"; 102 | keyCodes[224] = 'Meta'; 103 | keyCodes[225] = 'AltGraph'; 104 | 105 | keyCodes[246] = 'Attn'; 106 | keyCodes[247] = 'CrSel'; 107 | keyCodes[248] = 'ExSel'; 108 | keyCodes[249] = 'EREOF'; 109 | keyCodes[250] = 'Play'; 110 | keyCodes[251] = 'Zoom'; 111 | keyCodes[254] = 'Clear'; 112 | 113 | // a-z 114 | for ( var i=65; i<=90; i++ ) { 115 | keyCodes[i] = String.fromCharCode( i ).toLowerCase(); 116 | } 117 | 118 | // 0-9 119 | for ( var i=48; i<=57; i++ ) { 120 | keyCodes[i] = String.fromCharCode( i ); 121 | } 122 | // 0-9 keypad 123 | for ( var i=96; i<=105; i++ ) { 124 | keyCodes[i] = ''+(i-95); 125 | } 126 | 127 | // F1 - F24 128 | for ( var i=112; i<=135; i++ ) { 129 | keyCodes[i] = 'F'+(i-111); 130 | } 131 | 132 | function convertKeyNames( key ) { 133 | switch ( key ) { 134 | case ' ': 135 | return 'Spacebar'; 136 | case 'Esc' : 137 | return 'Escape'; 138 | case 'Left' : 139 | case 'Up' : 140 | case 'Right' : 141 | case 'Down' : 142 | return 'Arrow'+key; 143 | case 'Del' : 144 | return 'Delete'; 145 | case 'Scroll' : 146 | return 'ScrollLock'; 147 | case 'MediaNextTrack' : 148 | return 'MediaTrackNext'; 149 | case 'MediaPreviousTrack' : 150 | return 'MediaTrackPrevious'; 151 | case 'Crsel' : 152 | return 'CrSel'; 153 | case 'Exsel' : 154 | return 'ExSel'; 155 | case 'Zoom' : 156 | return 'ZoomToggle'; 157 | case 'Multiply' : 158 | return '*'; 159 | case 'Add' : 160 | return '+'; 161 | case 'Subtract' : 162 | return '-'; 163 | case 'Decimal' : 164 | return '.'; 165 | case 'Divide' : 166 | return '/'; 167 | case 'Apps' : 168 | return 'Menu'; 169 | default: 170 | return key; 171 | } 172 | } 173 | 174 | this.getKey = function( evt ) { 175 | var keyInfo = ''; 176 | if ( evt.ctrlKey && evt.keyCode != 17 ) { 177 | keyInfo += 'Control+'; 178 | } 179 | if ( evt.metaKey && evt.keyCode != 224 ) { 180 | keyInfo += 'Meta+'; 181 | } 182 | if ( evt.altKey && evt.keyCode != 18 ) { 183 | keyInfo += 'Alt+'; 184 | } 185 | if ( evt.shiftKey && evt.keyCode != 16 ) { 186 | keyInfo += 'Shift+'; 187 | } 188 | // evt.key turns shift+a into A, while keeping shiftKey, so it becomes Shift+A, instead of Shift+a. 189 | // so while it may be the future, i'm not using it here. 190 | if ( evt.charCode ) { 191 | keyInfo += String.fromCharCode( evt.charCode ).toLowerCase(); 192 | } else if ( evt.keyCode ) { 193 | if ( typeof keyCodes[evt.keyCode] == 'undefined' ) { 194 | keyInfo += '('+evt.keyCode+')'; 195 | } else { 196 | keyInfo += keyCodes[evt.keyCode]; 197 | } 198 | } else { 199 | keyInfo += 'Unknown'; 200 | } 201 | return keyInfo; 202 | } 203 | 204 | this.listen = function( el, key, callback, capture ) { 205 | return hope.editor.events.listen( el, 'keydown', function(evt) { 206 | var pressedKey = self.getKey( evt ); 207 | if ( key == pressedKey ) { 208 | callback.call( this, evt ); 209 | } 210 | }, capture); 211 | } 212 | 213 | } ); -------------------------------------------------------------------------------- /src/hope.mime.js: -------------------------------------------------------------------------------- 1 | hope.register( 'hope.mime', function() { 2 | // minimal mime encoding/decoding, stolen from https://github.com/andris9/mimelib/blob/master/lib/mimelib.js 3 | var self = this; 4 | 5 | self.getHeaders = function( message ) { 6 | var parseHeader = function( line ) { 7 | if (!line) { 8 | return {}; 9 | } 10 | 11 | var result = {}, parts = line.split(";"), 12 | pos; 13 | 14 | for (var i = 0, len = parts.length; i < len; i++) { 15 | pos = parts[i].indexOf("="); 16 | if (pos < 0) { 17 | pos = parts[i].indexOf(':'); 18 | } 19 | if ( pos < 0 ) { 20 | result[!i ? "defaultValue" : "i-" + i] = parts[i].trim(); 21 | } else { 22 | result[parts[i].substr(0, pos).trim().toLowerCase()] = parts[i].substr(pos + 1).trim(); 23 | } 24 | } 25 | return result; 26 | }; 27 | var line = null; 28 | var headers = {}; 29 | var temp = {}; 30 | while ( line = message.match(/^.*$/m)[0] ) { 31 | message = message.substring( line.length ); 32 | temp = parseHeader( line ); 33 | for ( var i in temp ) { 34 | headers[i] = temp[i]; 35 | } 36 | var returns = message.match(/^\r?\n|\r/); 37 | if ( returns[0] ) { 38 | message = message.substring( returns[0].length ); 39 | } 40 | } 41 | return { 42 | headers: headers, 43 | message: message.substring(1) 44 | } 45 | } 46 | 47 | self.encode = function( parts, message, headers ) { 48 | var boundary = 'hopeBoundary'+Date.now(); 49 | var result = 'MIME-Version: 1.0\n'; 50 | if ( headers ) { 51 | result += headers.join("\n"); 52 | } 53 | result += 'Content-Type: multipart/related; boundary='+boundary+'\n\n'; 54 | if ( message ) { 55 | result += message; 56 | } 57 | for ( var i=0, l=parts.length; i= 0) { 14 | k = n; 15 | } else { 16 | k = len + n; 17 | if (k < 0) {k = 0;} 18 | } 19 | var currentElement; 20 | while (k < len) { 21 | currentElement = O[k]; 22 | if (searchElement === currentElement || 23 | (searchElement !== searchElement && currentElement !== currentElement)) { 24 | return true; 25 | } 26 | k++; 27 | } 28 | return false; 29 | }; 30 | } -------------------------------------------------------------------------------- /src/hope.range.js: -------------------------------------------------------------------------------- 1 | /** 2 | * hope.range 3 | * 4 | * This implements an immutable range object. 5 | * Once created using hope.range.create() a range cannot change its start and end properties. 6 | * Any method that needs to change start or end, will instead create a new range with the new start/end 7 | * values and return that. 8 | * If you need to change a range value in place, you can assign the return value back to the original variable, e.g.: 9 | * range = range.collapse(); 10 | */ 11 | 12 | hope.register( 'hope.range', function() { 13 | 14 | 15 | function hopeRange( start, end ) { 16 | if ( typeof end == 'undefined' || end < start ) { 17 | end = start; 18 | } 19 | this.start = start; 20 | this.end = end; 21 | Object.freeze(this); 22 | } 23 | 24 | hopeRange.prototype = { 25 | constructor: hopeRange, 26 | get length () { 27 | return this.end - this.start; 28 | } 29 | } 30 | 31 | hopeRange.prototype.collapse = function( toEnd ) { 32 | var start = this.start; 33 | var end = this.end; 34 | if ( toEnd ) { 35 | start = end; 36 | } else { 37 | end = start; 38 | } 39 | return new hopeRange(start, end ); 40 | }; 41 | 42 | hopeRange.prototype.compare = function( range ) { 43 | range = hope.range.create(range); 44 | if ( range.start < this.start ) { 45 | return 1; 46 | } else if ( range.start > this.start ) { 47 | return -1; 48 | } else if ( range.end < this.end ) { 49 | return 1; 50 | } else if ( range.end > this.end ) { 51 | return -1; 52 | } 53 | return 0; 54 | } 55 | 56 | hopeRange.prototype.equals = function( range ) { 57 | return this.compare(range)==0; 58 | } 59 | 60 | hopeRange.prototype.smallerThan = function( range ) { 61 | return ( this.compare( range ) == -1 ); 62 | } 63 | 64 | hopeRange.prototype.largerThan = function( range ) { 65 | return ( this.compare( range ) == 1 ); 66 | } 67 | 68 | hopeRange.prototype.contains = function( range ) { 69 | range = hope.range.create(range); 70 | return this.start <= range.start && this.end >= range.end; 71 | } 72 | 73 | hopeRange.prototype.overlaps = function( range ) { 74 | range = hope.range.create(range); 75 | return ( range.start < this.end && range.end > this.start ); 76 | } 77 | 78 | hopeRange.prototype.isEmpty = function() { 79 | return this.start >= this.end; 80 | } 81 | 82 | hopeRange.prototype.overlap = function( range ) { 83 | range = hope.range.create(range); 84 | var start = 0; 85 | var end = 0; 86 | if ( this.overlaps( range ) ) { 87 | if ( range.start < this.start ) { 88 | start = this.start; 89 | } else { 90 | start = range.start; 91 | } 92 | if ( range.end < this.end ) { 93 | end = range.end; 94 | } else { 95 | end = this.end; 96 | } 97 | } 98 | return new hopeRange(start, end); // FIXME: is this range( 0, 0 ) a useful return value when there is no overlap? 99 | } 100 | 101 | hopeRange.prototype.exclude = function( range ) { 102 | // return parts of this that do not overlap with range 103 | var left = null; 104 | var right = null; 105 | if ( this.equals(range) ) { 106 | // nop 107 | } else if ( this.overlaps( range ) ) { 108 | left = new hopeRange( this.start, range.start ); 109 | right = new hopeRange( range.end, this.end ); 110 | if ( left.isEmpty() ) { 111 | left = null; 112 | } 113 | if ( right.isEmpty() ) { 114 | right = null; 115 | } 116 | } else if ( this.largerThan(range) ) { 117 | left = null; 118 | right = right; 119 | } else { 120 | left = this; 121 | right = left; 122 | } 123 | return [ left, right ]; 124 | } 125 | 126 | hopeRange.prototype.excludeLeft = function( range ) { 127 | return this.exclude(range)[0]; 128 | } 129 | 130 | hopeRange.prototype.excludeRight = function( range ) { 131 | return this.exclude(range)[1]; 132 | } 133 | 134 | /** 135 | * remove overlapping part of range from this range 136 | * [ 5 .. 20 ].delete( 10, 25 ) => [ 5 .. 10 ] 137 | * [ 5 .. 20 ].delete( 10, 15) => [ 5 .. 15 ] 138 | * [ 5 .. 20 ].delete( 5, 20 ) => [ 5 .. 5 ] 139 | * [ 5 .. 20 ].delete( 0, 10 ) => [ 0 .. 10 ] ? 140 | */ 141 | hopeRange.prototype.delete = function( range ) { 142 | range = hope.range.create(range); 143 | var moveLeft = 0; 144 | var end = this.end; 145 | if ( this.overlaps(range) ) { 146 | var cutRange = this.overlap( range ); 147 | var cutLength = cutRange.length; 148 | end -= cutLength; 149 | } 150 | var result = new hopeRange( this.start, end ); 151 | var exclude = range.excludeLeft( this ); 152 | if ( exclude ) { 153 | result = result.move( -exclude.length ); 154 | } 155 | return result; 156 | } 157 | 158 | hopeRange.prototype.copy = function( range ) { 159 | range = hope.range.create(range); 160 | return new hopeRange( 0, this.overlap( range ).length ); 161 | } 162 | 163 | hopeRange.prototype.extend = function( length, direction ) { 164 | var start = this.start; 165 | var end = this.end; 166 | if ( !direction ) { 167 | direction = 1; 168 | } 169 | if ( direction == 1 ) { 170 | end += length; 171 | } else { 172 | start = Math.max( 0, start - length ); 173 | } 174 | return new hopeRange(start, end); 175 | } 176 | 177 | hopeRange.prototype.toString = function() { 178 | if ( this.start != this.end ) { 179 | return this.start + '-' + this.end; 180 | } else { 181 | return this.start + ''; 182 | } 183 | } 184 | 185 | hopeRange.prototype.grow = function( size ) { 186 | var end = this.end + size; 187 | if ( end < this.start ) { 188 | end = this.start; 189 | } 190 | return new hopeRange(this.start, end); 191 | } 192 | 193 | hopeRange.prototype.shrink = function( size ) { 194 | return this.grow( -size ); 195 | } 196 | 197 | hopeRange.prototype.move = function( length, min, max ) { 198 | var start = this.start; 199 | var end = this.end; 200 | start += length; 201 | end += length; 202 | if ( !min ) { 203 | min = 0; 204 | } 205 | start = Math.max( min, start ); 206 | end = Math.max( start, end ); 207 | if ( max ) { 208 | start = Math.min( max, start ); 209 | end = Math.min( max, start ); 210 | } 211 | return new hopeRange(start, end); 212 | } 213 | 214 | this.create = function( start, end ) { 215 | if ( start instanceof hopeRange ) { 216 | return start; 217 | } 218 | if ( typeof end =='undefined' && parseInt(start,10)==start ) { 219 | end = start; 220 | } else if ( Array.isArray(start) && typeof start[1] != 'undefined' ) { 221 | end = start[1]; 222 | start = start[0]; 223 | } 224 | return new hopeRange( start, end ); 225 | } 226 | 227 | }); -------------------------------------------------------------------------------- /src/hope.render.html.js: -------------------------------------------------------------------------------- 1 | hope.register( 'hope.render.html', function() { 2 | 3 | 4 | var nestingSets = { 5 | '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' ], 6 | 'block' : [ 'address', 'dir', 'menu', 'hr', 'table', 'p', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'pre', 'ul', 'ol', 'dl', 'div', 'blockquote', 'iframe' ] 7 | }; 8 | 9 | nestingSets['all'] = nestingSets.block.concat( nestingSets.inline ); 10 | 11 | this.rules = { 12 | nesting: { 13 | 'a' : nestingSets.inline.filter( function(element) { return element != 'a' } ), 14 | 'abbr' : nestingSets.inline, 15 | 'acronym' : nestingSets.inline, 16 | 'address' : [ 'p' ].concat( nestingSets.inline ), 17 | 'bdo' : nestingSets.inline, 18 | 'blockquote': nestingSets.all, 19 | 'br' : [], 20 | 'caption' : nestingSets.inline, 21 | 'cite' : nestingSets.inline, 22 | 'code' : nestingSets.inline, 23 | 'col' : [], 24 | 'colgroup' : [ 'col' ], 25 | 'dd' : nestingSets.all, 26 | 'dfn' : nestingSets.inline, 27 | 'dir' : [ 'li' ], 28 | 'div' : nestingSets.all, 29 | 'dl' : [ 'dt', 'dd' ], 30 | 'dt' : nestingSets.inline, 31 | 'em' : nestingSets.inline, 32 | 'h1' : nestingSets.inline, 33 | 'h2' : nestingSets.inline, 34 | 'h3' : nestingSets.inline, 35 | 'h4' : nestingSets.inline, 36 | 'h5' : nestingSets.inline, 37 | 'h6' : nestingSets.inline, 38 | 'hr' : [], 39 | 'img' : [], 40 | 'kbd' : nestingSets.inline, 41 | 'li' : [ 'ol', 'ul', nestingSets.inline ], 42 | 'menu' : [ 'li' ], 43 | 'object' : [ 'param' ].concat( nestingSets.all ), 44 | 'ol' : [ 'li' ], 45 | 'p' : nestingSets.inline, 46 | 'pre' : nestingSets.inline, 47 | 'q' : nestingSets.inline, 48 | 'samp' : nestingSets.inline, 49 | 'span' : nestingSets.inline, 50 | 'strike' : nestingSets.inline, 51 | 'strong' : nestingSets.inline, 52 | 'sub' : nestingSets.inline, 53 | 'sup' : nestingSets.inline, 54 | 'table' : [ 'caption', 'colgroup', 'col', 'thead', 'tbody' ], 55 | 'tbody' : [ 'tr' ], 56 | 'td' : nestingSets.all, 57 | 'th' : nestingSets.all, 58 | 'thead' : [ 'tr' ], 59 | 'tr' : [ 'td', 'th' ], 60 | 'tt' : nestingSets.inline, 61 | 'u' : nestingSets.inline, 62 | 'ul' : [ 'li' ], 63 | 'var' : nestingSets.inline 64 | }, 65 | // which html elements can not have child elements at all and shouldn't be closed 66 | 'noChildren' : [ 'hr', 'br', 'col', 'img' ], 67 | // which html elements must have a specific child element 68 | 'obligChild' : { 69 | 'ol' : [ 'li' ], 70 | 'ul' : [ 'li' ], 71 | 'dl' : [ 'dt', 'dd' ] 72 | }, 73 | // which html elements must have a specific parent element 74 | 'obligParent' : { 75 | 'li' : [ 'ul', 'ol', 'dir', 'menu' ], 76 | 'dt' : [ 'dl' ], 77 | 'dd' : [ 'dl' ] 78 | }, 79 | // which html elements to allow as the top level, default is only block elements 80 | 'toplevel' : nestingSets.block + [ 'li', 'img', 'span' ] 81 | } 82 | 83 | this.getTag = function( markup ) { 84 | return markup.split(' ')[0].toLowerCase(); // FIXME: more robust parsing needed 85 | } 86 | 87 | this.getAnnotationStack = function( annotationSet ) { 88 | // { index:nextAnnotationEntry.index, entry:nextAnnotation } 89 | // { start:, end:, annotation: } 90 | // assert: annotationSet must only contain annotation that has overlapping ranges 91 | // if not results will be unpredictable 92 | var annotationStack = []; 93 | if ( !annotationSet.length ) { 94 | return []; 95 | } 96 | annotationSet.sort( function( a, b ) { 97 | if ( a.range.start < b.range.start ) { 98 | return -1; 99 | } else if ( a.range.start > b.range.start ) { 100 | return 1; 101 | } else if ( a.range.end > b.range.end ) { 102 | return -1; 103 | } else if ( a.range.end < b.range.end ) { 104 | return 1; 105 | } 106 | return 0; 107 | }); 108 | var unfilteredStack = []; 109 | for ( var i=0, l=annotationSet.length; icommonIndex; i-- ) { 175 | annotationDiff.push( { type : 'close', annotation : annotationStackFrom[i] } ); 176 | } 177 | for ( var i=commonIndex+1, l=annotationStackTo.length; i'; 192 | } 193 | } else if ( annotationDiff[i].type == 'insert' ) { 194 | renderedDiff += '<' + annotationDiff[i].annotation + '>'; 195 | var annotationTag = this.getTag( annotationDiff[i].annotation ); 196 | if ( this.rules.noChildren.indexOf( annotationTag ) == -1 ) { 197 | renderedDiff += ''; 198 | } 199 | } else { 200 | renderedDiff += '<' + annotationDiff[i].annotation + '>'; 201 | } 202 | } 203 | return renderedDiff; 204 | } 205 | 206 | this.escape = function( content ) { 207 | return content 208 | .replace(/&/g, "&") 209 | .replace(//g, ">") 211 | .replace(/"/g, """) 212 | .replace(/'/g, "'"); 213 | } 214 | 215 | this.render = function( fragment ) { 216 | // FIXME: annotation should be the relative annotation list to speed things up 217 | var annotationSet = []; // set of applicable annotation at current position 218 | var annotationStack = []; // stack of applied (valid) annotation at current position 219 | 220 | var relativeAnnotation = fragment.annotations.getEventList(); 221 | var content = fragment.text.toString().replace(/ /g, ' \u00A0'); 222 | 223 | var renderedHTML = ''; 224 | var cursor = 0; 225 | 226 | while ( relativeAnnotation.length ) { 227 | 228 | var annotationChangeSet = relativeAnnotation.shift(); 229 | var annotationAdded = []; // list of annotation added in this change set 230 | var annotationSetOnce = []; // list of annotation that can not have children, needs no close 231 | for ( i=0, l=annotationChangeSet.markup.length; i 0 ) { 252 | renderedHTML += this.escape( content.substr(cursor, offset) ); 253 | cursor+=offset; 254 | } 255 | offset = 0; 256 | 257 | // calculate the valid annotation stack from a given set 258 | var newAnnotationStack = this.getAnnotationStack( annotationSet ); //.concat( annotationSetOnce ) ); 259 | var newAnnotationsOnce = this.getAnnotationStack( annotationSetOnce ); 260 | // calculate the difference - how to get from stack one to stack two with the minimum of tags 261 | var diff = this.getAnnotationDiff( annotationStack, newAnnotationStack, newAnnotationsOnce ); 262 | // remove autoclosing annotation from the newAnnotationStack 263 | var newAnnotationStack = this.getAnnotationStack( annotationSet ); 264 | var diffHTML = this.renderAnnotationDiff( diff ); 265 | renderedHTML += diffHTML; 266 | annotationStack = newAnnotationStack; 267 | 268 | } while( relativeAnnotation.length ); 269 | 270 | if ( cursor < content.length ) { 271 | renderedHTML += this.escape( content.substr( cursor ) ); 272 | } 273 | 274 | return renderedHTML; 275 | } 276 | 277 | } ); -------------------------------------------------------------------------------- /src/hope.test.js: -------------------------------------------------------------------------------- 1 | hope.register( 'hope.test', function() { 2 | 3 | function hopeTest() { 4 | 5 | this.currentTest = null; 6 | this.errors = []; 7 | this.success = 0; 8 | this.countAssert = 0; 9 | 10 | /* 11 | * Javascript Diff Algorithm 12 | * By John Resig (http://ejohn.org/) 13 | * Modified by Chu Alan "sprite" 14 | * 15 | * Released under the MIT license. 16 | * 17 | * More Info: 18 | * http://ejohn.org/projects/javascript-diff-algorithm/ 19 | */ 20 | 21 | this.escape = function(s) { 22 | var n = s; 23 | n = n.replace(/&/g, "&"); 24 | n = n.replace(//g, ">"); 26 | n = n.replace(/"/g, """); 27 | 28 | return n; 29 | } 30 | 31 | this.diffString = function( o, n ) { 32 | o = o.replace(/\s+$/, ''); 33 | n = n.replace(/\s+$/, ''); 34 | 35 | var out = this.diff(o == "" ? [] : o.split(/\s+/), n == "" ? [] : n.split(/\s+/) ); 36 | var str = ""; 37 | 38 | var oSpace = o.match(/\s+/g); 39 | if (oSpace == null) { 40 | oSpace = ["\n"]; 41 | } else { 42 | oSpace.push("\n"); 43 | } 44 | var nSpace = n.match(/\s+/g); 45 | if (nSpace == null) { 46 | nSpace = ["\n"]; 47 | } else { 48 | nSpace.push("\n"); 49 | } 50 | 51 | if (out.n.length == 0) { 52 | for (var i = 0; i < out.o.length; i++) { 53 | str += '' + this.escape(out.o[i]) + oSpace[i] + ""; 54 | } 55 | } else { 56 | if (out.n[0].text == null) { 57 | for (n = 0; n < out.o.length && out.o[n].text == null; n++) { 58 | str += '' + this.escape(out.o[n]) + oSpace[n] + ""; 59 | } 60 | } 61 | 62 | for ( var i = 0; i < out.n.length; i++ ) { 63 | if (out.n[i].text == null) { 64 | str += '' + this.escape(out.n[i]) + nSpace[i] + ""; 65 | } else { 66 | var pre = ""; 67 | 68 | for (n = out.n[i].row + 1; n < out.o.length && out.o[n].text == null; n++ ) { 69 | pre += '' + this.escape(out.o[n]) + oSpace[n] + ""; 70 | } 71 | str += " " + out.n[i].text + nSpace[i] + pre; 72 | } 73 | } 74 | } 75 | 76 | return str; 77 | } 78 | 79 | this.randomColor = function() { 80 | return "rgb(" + (Math.random() * 100) + "%, " + 81 | (Math.random() * 100) + "%, " + 82 | (Math.random() * 100) + "%)"; 83 | } 84 | 85 | this.diffString2 = function( o, n ) { 86 | o = o.replace(/\s+$/, ''); 87 | n = n.replace(/\s+$/, ''); 88 | 89 | var out = this.diff(o == "" ? [] : o.split(/\s+/), n == "" ? [] : n.split(/\s+/) ); 90 | 91 | var oSpace = o.match(/\s+/g); 92 | if (oSpace == null) { 93 | oSpace = ["\n"]; 94 | } else { 95 | oSpace.push("\n"); 96 | } 97 | var nSpace = n.match(/\s+/g); 98 | if (nSpace == null) { 99 | nSpace = ["\n"]; 100 | } else { 101 | nSpace.push("\n"); 102 | } 103 | 104 | var os = ""; 105 | var colors = new Array(); 106 | for (var i = 0; i < out.o.length; i++) { 107 | colors[i] = this.randomColor(); 108 | 109 | if (out.o[i].text != null) { 110 | os += '' + 111 | this.escape(out.o[i].text) + oSpace[i] + ""; 112 | } else { 113 | os += "" + this.escape(out.o[i]) + oSpace[i] + ""; 114 | } 115 | } 116 | 117 | var ns = ""; 118 | for (var i = 0; i < out.n.length; i++) { 119 | if (out.n[i].text != null) { 120 | ns += '' + 121 | this.escape(out.n[i].text) + nSpace[i] + ""; 122 | } else { 123 | ns += "" + this.escape(out.n[i]) + nSpace[i] + ""; 124 | } 125 | } 126 | 127 | return { o : os , n : ns }; 128 | } 129 | 130 | this.diff = function( o, n ) { 131 | var ns = new Object(); 132 | var os = new Object(); 133 | 134 | for ( var i = 0; i < n.length; i++ ) { 135 | if ( ns[ n[i] ] == null ) 136 | ns[ n[i] ] = { rows: new Array(), o: null }; 137 | ns[ n[i] ].rows.push( i ); 138 | } 139 | 140 | for ( var i = 0; i < o.length; i++ ) { 141 | if ( os[ o[i] ] == null ) 142 | os[ o[i] ] = { rows: new Array(), n: null }; 143 | os[ o[i] ].rows.push( i ); 144 | } 145 | 146 | for ( var i in ns ) { 147 | if ( ns[i].rows.length == 1 && typeof(os[i]) != "undefined" && os[i].rows.length == 1 ) { 148 | n[ ns[i].rows[0] ] = { text: n[ ns[i].rows[0] ], row: os[i].rows[0] }; 149 | o[ os[i].rows[0] ] = { text: o[ os[i].rows[0] ], row: ns[i].rows[0] }; 150 | } 151 | } 152 | 153 | for ( var i = 0; i < n.length - 1; i++ ) { 154 | if ( n[i].text != null && n[i+1].text == null && n[i].row + 1 < o.length && o[ n[i].row + 1 ].text == null && 155 | n[i+1] == o[ n[i].row + 1 ] ) 156 | { 157 | n[i+1] = { text: n[i+1], row: n[i].row + 1 }; 158 | o[n[i].row+1] = { text: o[n[i].row+1], row: i + 1 }; 159 | } 160 | } 161 | 162 | for ( var i = n.length - 1; i > 0; i-- ) { 163 | if ( n[i].text != null && n[i-1].text == null && n[i].row > 0 && o[ n[i].row - 1 ].text == null && 164 | n[i-1] == o[ n[i].row - 1 ] ) 165 | { 166 | n[i-1] = { text: n[i-1], row: n[i].row - 1 }; 167 | o[n[i].row-1] = { text: o[n[i].row-1], row: i - 1 }; 168 | } 169 | } 170 | 171 | return { o: o, n: n }; 172 | } 173 | 174 | 175 | this.isArray = function( o ) { 176 | if ( Object.prototype.toString.call( o ) === '[object Array]' ) { 177 | return true; 178 | } 179 | return false; 180 | } 181 | 182 | this.getPrototypeName = function( o ) { 183 | return Object.prototype.toString.call( o ); 184 | } 185 | 186 | this.isString = function( o ) { 187 | if ( typeof o === 'string' ) { 188 | return true; 189 | } 190 | return false; 191 | } 192 | 193 | this.assertTrue = function( expression ) { 194 | this.countAssert++; 195 | if ( expression !== true ) { 196 | this.errors.push( 'test failed: expression not true at ' + this.currentTest + ' assertion ' + this.countAssert ); 197 | this.write( this.errors[ this.errors.length - 1 ] ); 198 | } else { 199 | this.success++; 200 | } 201 | }; 202 | 203 | this.assertFalse = function( expression ) { 204 | this.countAssert++; 205 | if ( expression !== false ) { 206 | this.errors.push( 'test failed: expression not false at ' + this.currentTest + ' assertion ' + this.countAssert ); 207 | this.write( this.errors[ this.errors.length - 1 ] ); 208 | } else { 209 | this.success++; 210 | } 211 | } 212 | 213 | this.assertEquals = function( var1, var2 ) { 214 | this.countAssert++; 215 | if ( var1 === var2 ) { 216 | this.success++ 217 | } else { 218 | var reason = ''; 219 | if ( (typeof var1) !== (typeof var2) ) { 220 | reason = 'typeof var1 '+(typeof var1)+' is not typeof var2 '+(typeof var2); 221 | } else if ( var1 instanceof Object && ( this.getPrototypeName(var1) !== this.getPrototypeName(var2) ) ) { 222 | reason = 'prototype of var1 '+this.getPrototypeName(var1)+' is not prototype of var2 '+this.getPrototypeName(var2); 223 | } else if ( this.isString(var1) ) { 224 | var diff = this.diffString2(var1, var2); 225 | reason = 'difference:
'+diff.o+'
'+diff.n+'
'; 226 | 227 | } else if ( this.isArray(var1) ) { 228 | var diff1 = []; 229 | for ( var i=0, l=var1.length; i 0 ) { 240 | reason = 'arraydiff: ' + diff.join("\n"); 241 | } 242 | } else if ( var1 instanceof Object ) { 243 | var diff = []; 244 | var seen = {}; 245 | var count = 0; 246 | for ( var i in var1 ) { 247 | if ( var1[i] !== var2[i] ) { 248 | diff[count++] = i + ': ' + var1[i] + ' is not ' + var2[i]; 249 | seen[i] = true; 250 | } 251 | } 252 | for ( var i in var2 ) { 253 | if ( !seen[i] && var1[i] != var2[i] ) { 254 | diff[count++] = i + ': ' + var1[i] + ' is not ' + var2[i]; 255 | } 256 | } 257 | if ( diff.length > 0 ) { 258 | reason = 'objectdiff: ' + diff.join("\n"); 259 | } 260 | } else { 261 | reason = var1 + ' != ' + var2; 262 | } 263 | if ( reason ) { 264 | this.errors.push( 'test failed: variables not equal at ' + this.currentTest + ' assertion ' + this.countAssert + ' reason: ' + reason ); 265 | this.write( this.errors[ this.errors.length - 1 ] ); 266 | } else { 267 | this.success++; 268 | } 269 | } 270 | } 271 | 272 | this.run = function() { 273 | this.errors = []; 274 | this.success = 0; 275 | for ( var i in this ) { 276 | if ( i.substr(0,4)=='test' ) { 277 | this.currentTest = i; 278 | this.countAssert = 0; 279 | this[i].call(); 280 | } 281 | } 282 | this.write( this.errors.length + ' errors; ' + this.success + ' tests succeeded.'); 283 | }; 284 | 285 | this.write = function( message ) { 286 | var output = document.getElementById('hopeTestOutput'); 287 | if ( output ) { 288 | //message = document.createTextNode( message ); 289 | var messageDiv = document.createElement( 'div' ); 290 | messageDiv.innerHTML = message; //appendChild( message ); 291 | output.appendChild( messageDiv ); 292 | } else { 293 | console.log( message ); 294 | } 295 | } 296 | 297 | } 298 | 299 | this.create = function() { 300 | return new hopeTest(); 301 | } 302 | 303 | }); -------------------------------------------------------------------------------- /tests/hope.fragment.annotations.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 |
12 | 115 | 116 | 117 | -------------------------------------------------------------------------------- /tests/hope.fragment.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 |
18 | 80 | 81 | 82 | -------------------------------------------------------------------------------- /tests/hope.fragment.text.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
11 | 63 | 64 | 65 | -------------------------------------------------------------------------------- /tests/hope.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Hope: html out-of-band 5 | 6 | 7 | 8 | 9 | 10 | 11 | 42 | 43 | 44 | 51 |
52 |
53 |
54 | 88 | 113 |
114 |
115 |
116 |
117 | 118 |
119 |
120 |
121 | 124 | 125 | 126 | -------------------------------------------------------------------------------- /tests/hope.range.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 | 142 | 143 | 144 | --------------------------------------------------------------------------------