├── 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 |
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 |
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 += '' + annotationTag + '>';
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: ';
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 |
119 |
120 |
121 |
124 |
125 |
126 |
--------------------------------------------------------------------------------
/tests/hope.range.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
142 |
143 |
144 |
--------------------------------------------------------------------------------