Index: trunk/parsers/wikidom/tests/synth/index.html |
— | — | @@ -0,0 +1,21 @@ |
| 2 | +<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" |
| 3 | + "http://www.w3.org/TR/html4/loose.dtd"> |
| 4 | +<html> |
| 5 | + <head> |
| 6 | + <title>Synth Tests</title> |
| 7 | + <link rel="stylesheet" href="../../lib/qunit.css" type="text/css" /> |
| 8 | + </head> |
| 9 | + <body> |
| 10 | + <h1 id="qunit-header">Synth Tests</h1> |
| 11 | + <h2 id="qunit-banner"></h2> |
| 12 | + <h2 id="qunit-userAgent"></h2> |
| 13 | + <ol id="qunit-tests"></ol> |
| 14 | + <script src="../../lib/synth/es.js"></script> |
| 15 | + <script src="../../lib/synth/es.EventEmitter.js"></script> |
| 16 | + <script src="../../lib/synth/es.Container.js"></script> |
| 17 | + <script src="../../lib/synth/es.ContainerItem.js"></script> |
| 18 | + <script src="../../lib/jquery.js"></script> |
| 19 | + <script src="../../lib/qunit.js"></script> |
| 20 | + <script src="test.js"></script> |
| 21 | + </body> |
| 22 | +</html> |
Property changes on: trunk/parsers/wikidom/tests/synth/index.html |
___________________________________________________________________ |
Added: svn:mime-type |
1 | 23 | + text/plain |
Index: trunk/parsers/wikidom/tests/synth/test.js |
— | — | @@ -0,0 +1,158 @@ |
| 2 | +module( 'Base classes' ); |
| 3 | + |
| 4 | +test( 'Containers', function() { |
| 5 | + var container1 = new es.Container( 'a' ), |
| 6 | + container2 = new es.Container( 'b' ); |
| 7 | + deepEqual( container1.a, [], 'Container list name can be specialized' ); |
| 8 | + deepEqual( container2.b, [], 'Container list name can be specialized' ); |
| 9 | + |
| 10 | + var updates = 0; |
| 11 | + container1.on( 'update', function( item ) { |
| 12 | + updates++ |
| 13 | + } ); |
| 14 | + |
| 15 | + // Creating |
| 16 | + |
| 17 | + var item1 = new es.ContainerItem( 'a' ), |
| 18 | + item2 = new es.ContainerItem( 'b' ), |
| 19 | + item3 = new es.ContainerItem( 'c' ); |
| 20 | + |
| 21 | + strictEqual( item1.a, null, 'Item container name can be specialized' ); |
| 22 | + strictEqual( item2.b, null, 'Item container name can be specialized' ); |
| 23 | + strictEqual( item3.c, null, 'Item container name can be specialized' ); |
| 24 | + |
| 25 | + // Adding |
| 26 | + |
| 27 | + container1.append( item1 ); |
| 28 | + equal( updates, 1, 'es.Container emits update events on append' ); |
| 29 | + strictEqual( item1.a, container1, 'es.Container.append attaches item to container' ); |
| 30 | + container1.append( item2 ); |
| 31 | + equal( updates, 2, 'es.Container emits update events on append' ); |
| 32 | + strictEqual( item2.b, container1, 'es.Container.append attaches item to container' ); |
| 33 | + container1.append( item3 ); |
| 34 | + equal( updates, 3, 'es.Container emits update events on append' ); |
| 35 | + strictEqual( item3.c, container1, 'es.Container.append attaches item to container' ); |
| 36 | + |
| 37 | + // Accessing |
| 38 | + |
| 39 | + deepEqual( container1.items(), [item1, item2, item3], 'es.Container.items returns all items' ) |
| 40 | + |
| 41 | + strictEqual( container1.get( 0 ), item1, 'es.Container.get returns correct item at index' ); |
| 42 | + strictEqual( container1.get( 1 ), item2, 'es.Container.get returns correct item at index' ); |
| 43 | + strictEqual( container1.get( 2 ), item3, 'es.Container.get returns correct item at index' ); |
| 44 | + strictEqual( container1.get( 3 ), null, 'es.Container.get returns null for invalid item' ); |
| 45 | + |
| 46 | + equal( container1.indexOf( item1 ), 0, 'es.Container.indexOf returns correct index of item' ); |
| 47 | + equal( container1.indexOf( item2 ), 1, 'es.Container.indexOf returns correct index of item' ); |
| 48 | + equal( container1.indexOf( item3 ), 2, 'es.Container.indexOf returns correct index of item' ); |
| 49 | + equal( container1.indexOf( null ), -1, 'es.Container.indexOf returns -1 for nonexistent items' ); |
| 50 | + |
| 51 | + equal( container1.getLength(), 3, 'es.Container.getLength returns correct number of items' ); |
| 52 | + strictEqual( container1.first(), item1, 'es.Container.first returns correct first item' ); |
| 53 | + strictEqual( container1.last(), item3, 'es.Container.last returns correct first item' ); |
| 54 | + |
| 55 | + // Iterating |
| 56 | + |
| 57 | + var items = [], |
| 58 | + indexes = []; |
| 59 | + container1.each( function( item, index ) { |
| 60 | + items.push( item ); |
| 61 | + indexes.push( index ); |
| 62 | + } ); |
| 63 | + equal( items.length, 3, 'es.Container.each iterates over all items' ); |
| 64 | + deepEqual( items, [item1, item2, item3], 'es.Container.each provides item as first argument' ); |
| 65 | + deepEqual( indexes, [0, 1, 2], 'es.Container.each provides index as first argument' ); |
| 66 | + |
| 67 | + var count = 0; |
| 68 | + container1.each( function( item, index ) { |
| 69 | + count++; |
| 70 | + if ( index === 1 ) { |
| 71 | + return false; |
| 72 | + } |
| 73 | + } ); |
| 74 | + equal( count, 2, 'es.Container.each stops iterating when a callback returns false' ) |
| 75 | + |
| 76 | + // Updating |
| 77 | + |
| 78 | + updates = 0; |
| 79 | + |
| 80 | + item1.emit( 'update' ); |
| 81 | + equal( updates, 1, 'es.Container relays update events from items' ); |
| 82 | + item2.emit( 'update' ); |
| 83 | + equal( updates, 2, 'es.Container relays update events from items' ); |
| 84 | + item3.emit( 'update' ); |
| 85 | + equal( updates, 3, 'es.Container relays update events from items' ); |
| 86 | + |
| 87 | + // Removing |
| 88 | + |
| 89 | + updates = 0; |
| 90 | + |
| 91 | + container1.remove( item3 ); |
| 92 | + equal( updates, 1, 'es.Container emits update event on item removal' ); |
| 93 | + strictEqual( item3.c, null, 'es.Container.append detaches item to container' ); |
| 94 | + |
| 95 | + equal( container1.getLength(), 2, 'es.Container.getLength returns correct number of items' ); |
| 96 | + |
| 97 | + container1.remove( item1 ); |
| 98 | + equal( updates, 2, 'es.Container emits update event on item removal' ); |
| 99 | + strictEqual( item1.a, null, 'es.Container.append detaches item to container' ); |
| 100 | + |
| 101 | + equal( container1.getLength(), 1, 'es.Container.getLength returns correct number of items' ); |
| 102 | + strictEqual( container1.first(), item2, 'es.Container.first returns correct first item' ); |
| 103 | + strictEqual( container1.last(), item2, 'es.Container.last returns correct first item' ); |
| 104 | + |
| 105 | + container1.remove( item2 ); |
| 106 | + equal( updates, 3, 'es.Container emits update event on item removal' ); |
| 107 | + strictEqual( item2.b, null, 'es.Container.append detaches item to container' ); |
| 108 | + |
| 109 | + equal( container1.getLength(), 0, 'es.Container.getLength returns correct number of items' ); |
| 110 | + strictEqual( container1.first(), null, 'es.Container.first returns null when empty' ); |
| 111 | + strictEqual( container1.last(), null, 'es.Container.last returns null when empty' ); |
| 112 | + |
| 113 | + updates = 0; |
| 114 | + |
| 115 | + item1.emit( 'update' ); |
| 116 | + equal( updates, 0, 'es.Container does not relay events from removed items' ); |
| 117 | + item2.emit( 'update' ); |
| 118 | + equal( updates, 0, 'es.Container does not relay events from removed items' ); |
| 119 | + item3.emit( 'update' ); |
| 120 | + equal( updates, 0, 'es.Container does not relay events from removed items' ); |
| 121 | + |
| 122 | + // Inserting |
| 123 | + |
| 124 | + container1.append( item1 ); |
| 125 | + deepEqual( container1.items(), [item1], 'es.Container.append adds item to end' ); |
| 126 | + container1.prepend( item3 ); |
| 127 | + deepEqual( container1.items(), [item3, item1], 'es.Container.prepend adds item to begining' ); |
| 128 | + container1.insertBefore( item2, item1 ); |
| 129 | + deepEqual( |
| 130 | + container1.items(), |
| 131 | + [item3, item2, item1], |
| 132 | + 'es.Container.insertBefore inserts item before another' |
| 133 | + ); |
| 134 | + container1.insertBefore( item2, item3 ); |
| 135 | + deepEqual( |
| 136 | + container1.items(), |
| 137 | + [item2, item3, item1], |
| 138 | + 'es.Container.insertBefore moves item before another' |
| 139 | + ); |
| 140 | + |
| 141 | + container2.prepend( item1 ); |
| 142 | + deepEqual( container2.items(), [item1], 'es.Container.prepend adds item to begining' ); |
| 143 | + container2.append( item3 ); |
| 144 | + deepEqual( container2.items(), [item1, item3], 'es.Container.append adds item to end' ); |
| 145 | + container2.insertAfter( item2, item1 ); |
| 146 | + deepEqual( |
| 147 | + container2.items(), |
| 148 | + [item1, item2, item3], |
| 149 | + 'es.Container.insertAfter inserts item after another' |
| 150 | + ); |
| 151 | + container2.insertAfter( item1, item2 ); |
| 152 | + deepEqual( |
| 153 | + container2.items(), |
| 154 | + [item2, item1, item3], |
| 155 | + 'es.Container.insertAfter moves item after another' |
| 156 | + ); |
| 157 | + |
| 158 | + // TODO: Events for appending, prepending, inserting and removing |
| 159 | +} ); |
Property changes on: trunk/parsers/wikidom/tests/synth/test.js |
___________________________________________________________________ |
Added: svn:mime-type |
1 | 160 | + text/plain |
Added: svn:eol-style |
2 | 161 | + native |
Index: trunk/parsers/wikidom/lib/synth/es.js |
— | — | @@ -0,0 +1,96 @@ |
| 2 | +/** |
| 3 | + * EditSurface namespace. |
| 4 | + * |
| 5 | + * All classes and functions will be attached to this object to keep the global namespace clean. |
| 6 | + */ |
| 7 | +var es = {}; |
| 8 | + |
| 9 | +/* Functions */ |
| 10 | + |
| 11 | +/** |
| 12 | + * Extends a constructor with the prototype of another. |
| 13 | + * |
| 14 | + * When using this, it's required to include a call to the constructor of the parent class as the |
| 15 | + * first code in the child class's constructor. |
| 16 | + * |
| 17 | + * @example |
| 18 | + * // Define parent class |
| 19 | + * function Foo() { |
| 20 | + * // code here |
| 21 | + * } |
| 22 | + * // Define child class |
| 23 | + * function Bar() { |
| 24 | + * // Call parent constructor |
| 25 | + * Foo.call( this ); |
| 26 | + * } |
| 27 | + * // Extend prototype |
| 28 | + * extend( Bar, Foo ); |
| 29 | + * |
| 30 | + * @static |
| 31 | + * @method |
| 32 | + * @param dst {Function} Class to extend |
| 33 | + * @param src {Function} Base class to use methods from |
| 34 | + */ |
| 35 | +es.extend = function( dst, src ) { |
| 36 | + var base = new src(); |
| 37 | + for ( var method in base ) { |
| 38 | + if ( typeof base[method] === 'function' && !( method in dst.prototype ) ) { |
| 39 | + dst.prototype[method] = base[method]; |
| 40 | + } |
| 41 | + } |
| 42 | +}; |
| 43 | + |
| 44 | +/** |
| 45 | + * Recursively compares string and number property between two objects. |
| 46 | + * |
| 47 | + * A false result may be caused by property inequality or by properties in one object missing from |
| 48 | + * the other. An asymmetrical test may also be performed, which checks only that properties in the |
| 49 | + * first object are present in the second object, but not the inverse. |
| 50 | + * |
| 51 | + * @static |
| 52 | + * @method |
| 53 | + * @param a {Object} First object to compare |
| 54 | + * @param b {Object} Second object to compare |
| 55 | + * @param asymmetrical {Boolean} Whether to check only that b contains values from a |
| 56 | + * @returns {Boolean} If the objects contain the same values as each other |
| 57 | + */ |
| 58 | +es.compareObjects = function( a, b, asymmetrical ) { |
| 59 | + var aValue, bValue, aType, bType; |
| 60 | + var k; |
| 61 | + for ( k in a ) { |
| 62 | + aValue = a[k]; |
| 63 | + bValue = b[k]; |
| 64 | + aType = typeof aValue; |
| 65 | + bType = typeof bValue; |
| 66 | + if ( aType !== bType |
| 67 | + || ( ( aType === 'string' || aType === 'number' ) && aValue !== bValue ) |
| 68 | + || ( $.isPlainObject( aValue ) && !es.compareObjects( aValue, bValue ) ) ) { |
| 69 | + return false; |
| 70 | + } |
| 71 | + } |
| 72 | + // If the check is not asymmetrical, recursing with the arguments swapped will verify our result |
| 73 | + return asymmetrical ? true : es.compareObjects( b, a, true ); |
| 74 | +}; |
| 75 | + |
| 76 | +/** |
| 77 | + * Gets a recursive copy of an object's string, number and plain-object property. |
| 78 | + * |
| 79 | + * @static |
| 80 | + * @method |
| 81 | + * @param source {Object} Object to copy |
| 82 | + * @returns {Object} Copy of source object |
| 83 | + */ |
| 84 | +es.copyObject = function( source ) { |
| 85 | + var destination = {}; |
| 86 | + var key; |
| 87 | + for ( key in source ) { |
| 88 | + sourceValue = source[key]; |
| 89 | + sourceType = typeof sourceValue; |
| 90 | + if ( sourceType === 'string' || sourceType === 'number' ) { |
| 91 | + destination[key] = sourceValue; |
| 92 | + } else if ( $.isPlainObject( sourceValue ) ) { |
| 93 | + destination[key] = es.copyObject( sourceValue ); |
| 94 | + } |
| 95 | + } |
| 96 | + return destination; |
| 97 | +}; |
Property changes on: trunk/parsers/wikidom/lib/synth/es.js |
___________________________________________________________________ |
Added: svn:mime-type |
1 | 98 | + text/plain |
Added: svn:eol-style |
2 | 99 | + native |
Index: trunk/parsers/wikidom/lib/synth/models/es.CommentBlockModel.js |
— | — | @@ -0,0 +1,54 @@ |
| 2 | +/** |
| 3 | + * Creates an es.CommentBlockModel object. |
| 4 | + * |
| 5 | + * @class |
| 6 | + * @constructor |
| 7 | + * @param text {String} |
| 8 | + * @property text {String} |
| 9 | + */ |
| 10 | +es.CommentBlockModel = function( text ) { |
| 11 | + es.BlockModel.call( this, ['hasContent'] ); |
| 12 | + this.text = text || ''; |
| 13 | +}; |
| 14 | + |
| 15 | +/* Static Methods */ |
| 16 | + |
| 17 | +/** |
| 18 | + * Creates an CommentBlockModel object from a plain object. |
| 19 | + * |
| 20 | + * @method |
| 21 | + * @static |
| 22 | + * @param obj {Object} |
| 23 | + */ |
| 24 | +es.CommentBlockModel.newFromPlainObject = function( obj ) { |
| 25 | + return new es.CommentBlockModel( obj.text ); |
| 26 | +}; |
| 27 | + |
| 28 | +/* Methods */ |
| 29 | + |
| 30 | +/** |
| 31 | + * Gets the length of all content. |
| 32 | + * |
| 33 | + * @method |
| 34 | + * @returns {Integer} Length of all content |
| 35 | + */ |
| 36 | +es.CommentBlockModel.prototype.getContentLength = function() { |
| 37 | + return this.text.length; |
| 38 | +}; |
| 39 | + |
| 40 | +/** |
| 41 | + * Gets a plain comment block object. |
| 42 | + * |
| 43 | + * @method |
| 44 | + * @returns obj {Object} |
| 45 | + */ |
| 46 | +es.CommentBlockModel.prototype.getPlainObject = function() { |
| 47 | + return { 'type': 'comment', 'text': this.text }; |
| 48 | +}; |
| 49 | + |
| 50 | +// Register constructor |
| 51 | +es.BlockModel.constructors['comment'] = es.CommentBlockModel; |
| 52 | + |
| 53 | +/* Inheritance */ |
| 54 | + |
| 55 | +es.extend( es.CommentBlockModel, es.BlockModel ); |
Property changes on: trunk/parsers/wikidom/lib/synth/models/es.CommentBlockModel.js |
___________________________________________________________________ |
Added: svn:mime-type |
1 | 56 | + text/plain |
Added: svn:eol-style |
2 | 57 | + native |
Index: trunk/parsers/wikidom/lib/synth/models/es.DocumentModel.js |
— | — | @@ -0,0 +1,65 @@ |
| 2 | +/** |
| 3 | + * Creates an es.DocumentModel object. |
| 4 | + * |
| 5 | + * @class |
| 6 | + * @constructor |
| 7 | + * @param blocks {Array} |
| 8 | + * @param attributes {Object} |
| 9 | + * @property blocks {Array} |
| 10 | + * @property attributes {Object} |
| 11 | + */ |
| 12 | +es.DocumentModel = function( blocks, attributes ) { |
| 13 | + es.EventEmitter.call( this ); |
| 14 | + es.Container.call( this, 'blocks' ); |
| 15 | + this.blocks = new es.ContentSeries( blocks || [] ); |
| 16 | + this.attributes = attributes || {}; |
| 17 | +}; |
| 18 | + |
| 19 | +/* Static Methods */ |
| 20 | + |
| 21 | +/** |
| 22 | + * Creates an es.DocumentModel object from a plain object. |
| 23 | + * |
| 24 | + * @method |
| 25 | + * @static |
| 26 | + * @param obj {Object} |
| 27 | + */ |
| 28 | +es.DocumentModel.newFromPlainObject = function( obj ) { |
| 29 | + var types = es.BlockModel.constructors; |
| 30 | + return new es.DocumentModel( |
| 31 | + // Blocks - if given, convert all plain "block" objects to es.WikiDom* objects |
| 32 | + !$.isArray( obj.blocks ) ? [] : $.map( obj.blocks, function( block ) { |
| 33 | + return es.BlockModel.newFromPlainObject( block ); |
| 34 | + }, |
| 35 | + // Attributes - if given, make a deep copy of attributes |
| 36 | + !$.isPlainObject( obj.attributes ) ? {} : $.extend( true, {}, obj.attributes ) |
| 37 | + ); |
| 38 | +}; |
| 39 | + |
| 40 | +/* Methods */ |
| 41 | + |
| 42 | +es.DocumentModel.prototype.getPlainObject = function() { |
| 43 | + var obj = {}; |
| 44 | + if ( this.blocks.length ) { |
| 45 | + obj.blocks = []; |
| 46 | + for ( var i = 0; i < this.blocks.length; i++ ) { |
| 47 | + obj.blocks.push( this.blocks[i].getPlainObject() ); |
| 48 | + } |
| 49 | + } |
| 50 | + if ( !$.isEmptyObject( this.attributes ) ) { |
| 51 | + obj.attributes = $.extend( true, {}, this.attributes ); |
| 52 | + } |
| 53 | +}; |
| 54 | + |
| 55 | +/** |
| 56 | + * Gets the size of the of the contents of all blocks. |
| 57 | + * |
| 58 | + * @method |
| 59 | + * @returns {Integer} |
| 60 | + */ |
| 61 | +es.DocumentModel.prototype.getContentLength = function() { |
| 62 | + return this.blocks.getContentLength(); |
| 63 | +}; |
| 64 | + |
| 65 | +es.extend( es.DocumentModel, es.EventEmitter ); |
| 66 | +es.extend( es.DocumentModel, es.Container ); |
Property changes on: trunk/parsers/wikidom/lib/synth/models/es.DocumentModel.js |
___________________________________________________________________ |
Added: svn:mime-type |
1 | 67 | + text/plain |
Added: svn:eol-style |
2 | 68 | + native |
Index: trunk/parsers/wikidom/lib/synth/models/es.ParagraphBlockModel.js |
— | — | @@ -0,0 +1,54 @@ |
| 2 | +/** |
| 3 | + * Creates an es.ParagraphBlockModel object. |
| 4 | + * |
| 5 | + * @class |
| 6 | + * @constructor |
| 7 | + * @param content {es.ContentModel} |
| 8 | + * @property content {es.ContentModel} |
| 9 | + */ |
| 10 | +es.ParagraphBlockModel = function( content ) { |
| 11 | + es.BlockModel.call( this, ['hasContent', 'isAnnotatable'] ); |
| 12 | + this.content = content || null; |
| 13 | +}; |
| 14 | + |
| 15 | +/* Static Methods */ |
| 16 | + |
| 17 | +/** |
| 18 | + * Creates an ParagraphBlockModel object from a plain object. |
| 19 | + * |
| 20 | + * @method |
| 21 | + * @static |
| 22 | + * @param obj {Object} |
| 23 | + */ |
| 24 | +es.ParagraphBlockModel.newFromPlainObject = function( obj ) { |
| 25 | + return new es.ParagraphBlockModel( es.ContentModel.newFromPlainObject( obj ) ); |
| 26 | +}; |
| 27 | + |
| 28 | +/* Methods */ |
| 29 | + |
| 30 | +/** |
| 31 | + * Gets the length of all content. |
| 32 | + * |
| 33 | + * @method |
| 34 | + * @returns {Integer} Length of all content |
| 35 | + */ |
| 36 | +es.ParagraphBlockModel.prototype.getContentLength = function() { |
| 37 | + return this.content.getLength(); |
| 38 | +}; |
| 39 | + |
| 40 | +/** |
| 41 | + * Gets a plain paragraph block object. |
| 42 | + * |
| 43 | + * @method |
| 44 | + * @returns obj {Object} |
| 45 | + */ |
| 46 | +es.ParagraphBlockModel.prototype.getPlainObject = function() { |
| 47 | + return { 'type': 'paragraph', 'content': this.content.getPlainObject() }; |
| 48 | +}; |
| 49 | + |
| 50 | +// Register constructor |
| 51 | +es.BlockModel.constructors['paragraph'] = es.ParagraphBlockModel; |
| 52 | + |
| 53 | +/* Inheritance */ |
| 54 | + |
| 55 | +es.extend( es.ParagraphBlockModel, es.BlockModel ); |
Property changes on: trunk/parsers/wikidom/lib/synth/models/es.ParagraphBlockModel.js |
___________________________________________________________________ |
Added: svn:mime-type |
1 | 56 | + text/plain |
Added: svn:eol-style |
2 | 57 | + native |
Index: trunk/parsers/wikidom/lib/synth/models/es.TableBlockModel.js |
— | — | @@ -0,0 +1,73 @@ |
| 2 | +/** |
| 3 | + * Creates an es.TableBlockModel object. |
| 4 | + * |
| 5 | + * @class |
| 6 | + * @constructor |
| 7 | + * @param rows {Array} |
| 8 | + * @param attributes {Object} |
| 9 | + * @property cells {Array} |
| 10 | + * @property attributes {Object} |
| 11 | + */ |
| 12 | +es.TableBlockModel = function( rows, attributes ) { |
| 13 | + es.BlockModel.call( this, ['isDocumentContainer', 'isAggregate'] ); |
| 14 | + this.rows = new es.ContentSeries( rows || [] ); |
| 15 | + this.attributes = attributes || {}; |
| 16 | +}; |
| 17 | + |
| 18 | +/* Static Methods */ |
| 19 | + |
| 20 | +/** |
| 21 | + * Creates an TableBlockModel object from a plain object. |
| 22 | + * |
| 23 | + * @method |
| 24 | + * @static |
| 25 | + * @param obj {Object} |
| 26 | + */ |
| 27 | +es.TableBlockModel.newFromPlainObject = function( obj ) { |
| 28 | + return new es.TableBlockModel( |
| 29 | + // Cells - if given, convert plain "item" objects to es.ListModelItem objects |
| 30 | + !$.isArray( obj.rows ) ? [] : $.map( obj.rows, function( row ) { |
| 31 | + return !$.isPlainObject( row ) ? null : es.TableBlockRowModel.newFromPlainObject( row ) |
| 32 | + } ) |
| 33 | + // Attributes - if given, make a deep copy of attributes |
| 34 | + !$.isPlainObject( obj.attributes ) ? {} : $.extend( true, {}, obj.attributes ) |
| 35 | + ); |
| 36 | +}; |
| 37 | + |
| 38 | +/* Methods */ |
| 39 | + |
| 40 | +/** |
| 41 | + * Gets the length of all content. |
| 42 | + * |
| 43 | + * @method |
| 44 | + * @returns {Integer} Length of all content |
| 45 | + */ |
| 46 | +es.TableBlockModel.prototype.getContentLength = function() { |
| 47 | + return this.rows.getContentLength(); |
| 48 | +}; |
| 49 | + |
| 50 | +/** |
| 51 | + * Gets a plain table block object. |
| 52 | + * |
| 53 | + * @method |
| 54 | + * @returns obj {Object} |
| 55 | + */ |
| 56 | +es.TableBlockModel.prototype.getPlainObject = function() { |
| 57 | + var obj = { 'type': 'table' }; |
| 58 | + if ( this.rows.length ) { |
| 59 | + obj.rows = $.map( this.rows, function( row ) { |
| 60 | + return row.getPlainObject(); |
| 61 | + } ); |
| 62 | + } |
| 63 | + if ( !$.isEmptyObject( this.attributes ) ) { |
| 64 | + obj.attributes = $.extend( true, {}. this.attributes ); |
| 65 | + } |
| 66 | + return obj; |
| 67 | +}; |
| 68 | + |
| 69 | +// Register constructor |
| 70 | +es.BlockModel.constructors['table'] = es.TableBlockModel; |
| 71 | + |
| 72 | +/* Inheritance */ |
| 73 | + |
| 74 | +es.extend( es.TableBlockModel, es.BlockModel ); |
Property changes on: trunk/parsers/wikidom/lib/synth/models/es.TableBlockModel.js |
___________________________________________________________________ |
Added: svn:mime-type |
1 | 75 | + text/plain |
Added: svn:eol-style |
2 | 76 | + native |
Index: trunk/parsers/wikidom/lib/synth/models/es.ListBlockItemModel.js |
— | — | @@ -0,0 +1,46 @@ |
| 2 | +/** |
| 3 | + * Creates an es.ListBlockItemModel object. |
| 4 | + * |
| 5 | + * @class |
| 6 | + * @constructor |
| 7 | + * @param content {es.ContentModel} |
| 8 | + * @param styles {Array} |
| 9 | + * @property content {es.ContentModel} |
| 10 | + * @property styles {Array} |
| 11 | + */ |
| 12 | +es.ListBlockItemModel = function( content, styles ) { |
| 13 | + this.content = content || null; |
| 14 | + this.styles = styles || ['bullet']; |
| 15 | +}; |
| 16 | + |
| 17 | +/* Methods */ |
| 18 | + |
| 19 | +/** |
| 20 | + * Gets the length of all content. |
| 21 | + * |
| 22 | + * @method |
| 23 | + * @returns {Integer} Length of all content |
| 24 | + */ |
| 25 | +es.ListBlockItemModel.prototype.getContentLength = function() { |
| 26 | + return content.getLength(); |
| 27 | +}; |
| 28 | + |
| 29 | +/** |
| 30 | + * Gets a plain list block item object. |
| 31 | + * |
| 32 | + * @method |
| 33 | + * @returns obj {Object} |
| 34 | + */ |
| 35 | +es.ListBlockItemModel.prototype.getPlainObject = function() { |
| 36 | + return { 'content': this.content.getPlainObject(), 'styles': this.styles.slice( 0 ) }; |
| 37 | +}; |
| 38 | + |
| 39 | +/** |
| 40 | + * Gets a sum of cell content lengths. |
| 41 | + * |
| 42 | + * @method |
| 43 | + * @returns {Integer} |
| 44 | + */ |
| 45 | +es.TableBlockRowModel.prototype.getLength = function() { |
| 46 | + return this.cells.getLength(); |
| 47 | +}; |
Property changes on: trunk/parsers/wikidom/lib/synth/models/es.ListBlockItemModel.js |
___________________________________________________________________ |
Added: svn:mime-type |
1 | 48 | + text/plain |
Added: svn:eol-style |
2 | 49 | + native |
Index: trunk/parsers/wikidom/lib/synth/models/es.ListBlockModel.js |
— | — | @@ -0,0 +1,106 @@ |
| 2 | +/** |
| 3 | + * Creates an es.ListBlockModel object. |
| 4 | + * |
| 5 | + * @class |
| 6 | + * @constructor |
| 7 | + * @param items {Array} |
| 8 | + * @property items {Array} |
| 9 | + */ |
| 10 | +es.ListBlockModel = function( items ) { |
| 11 | + es.BlockModel.call( this, ['hasContent', 'isAnnotatable', 'isAggregate'] ); |
| 12 | + this.items = new es.ContentSeries( items || [] ); |
| 13 | +}; |
| 14 | + |
| 15 | +/* Static Methods */ |
| 16 | + |
| 17 | +/** |
| 18 | + * Converts a plain object tree-structure of a list with items, each containing content and one or |
| 19 | + * more lists to a flat array of es.ListBlockItemModel objects. |
| 20 | + * |
| 21 | + * This is a helper function for es.ListBlockModel.newFromPlainObject. Parent list styles are |
| 22 | + * retained in the styles array of the es.ListBlockItemModel objects. |
| 23 | + * |
| 24 | + * If a plain list object does not have a style property, "bullet" will be used by default. |
| 25 | + * |
| 26 | + * @method |
| 27 | + * @static |
| 28 | + * @param obj {Object} |
| 29 | + * @param styles {Array} |
| 30 | + */ |
| 31 | +es.ListBlockModel.flattenPlainObject = function( obj, styles ) { |
| 32 | + if ( !$.isArray( styles ) ) { |
| 33 | + styles = []; |
| 34 | + } |
| 35 | + styles.push( obj.style || 'bullet' ); |
| 36 | + var items = []; |
| 37 | + if ( $.isArray( obj.items ) ) { |
| 38 | + $.each( obj.items, function( item ) { |
| 39 | + if ( $.isPlainObject( item.content ) ) { |
| 40 | + items.push( |
| 41 | + new es.ListBlockItemModel( |
| 42 | + es.ContentModel.newFromPlainObject( item.content ), |
| 43 | + styles.slice( 0 ) |
| 44 | + ) |
| 45 | + ); |
| 46 | + } |
| 47 | + if ( $.isArray( item.lists ) ) { |
| 48 | + $.each( item.lists, function( list ) { |
| 49 | + items = items.concat( es.ListBlockList.flattenList( list, styles ) ); |
| 50 | + } ); |
| 51 | + } |
| 52 | + } ); |
| 53 | + } |
| 54 | + styles.pop(); |
| 55 | + return items; |
| 56 | +}; |
| 57 | + |
| 58 | +/** |
| 59 | + * Creates an es.ListBlockModel object from a plain object. |
| 60 | + * |
| 61 | + * @method |
| 62 | + * @static |
| 63 | + * @param obj {Object} |
| 64 | + */ |
| 65 | +es.ListBlockModel.newFromPlainObject = function( obj ) { |
| 66 | + return new es.ListBlockModel( |
| 67 | + // Items - if given, convert plain "list" object from a tree structure to a flat array of |
| 68 | + // es.ListBlockItemModel objects |
| 69 | + !$.isArray( obj.items ) ? [] : es.ListBlockModel.flattenPlainObject( obj ) |
| 70 | + ); |
| 71 | +}; |
| 72 | + |
| 73 | +/* Methods */ |
| 74 | + |
| 75 | +/** |
| 76 | + * Gets the length of all content. |
| 77 | + * |
| 78 | + * @method |
| 79 | + * @returns {Integer} Length of all content |
| 80 | + */ |
| 81 | +es.ListBlockModel.prototype.getContentLength = function() { |
| 82 | + return this.items.getContentLength(); |
| 83 | +}; |
| 84 | + |
| 85 | +/** |
| 86 | + * Gets a plain list block object. |
| 87 | + * |
| 88 | + * @method |
| 89 | + * @returns obj {Object} |
| 90 | + */ |
| 91 | +es.ListBlockModel.prototype.getPlainObject = function() { |
| 92 | + var obj = { 'type': 'list' }; |
| 93 | + if ( this.items.length ) { |
| 94 | + obj.items = []; |
| 95 | + for ( var i = 0; i < this.items.length; i++ ) { |
| 96 | + obj.items.push( this.items[i].getPlainObject() ); |
| 97 | + } |
| 98 | + } |
| 99 | + return obj; |
| 100 | +}; |
| 101 | + |
| 102 | +// Register constructor |
| 103 | +es.BlockModel.constructors['list'] = es.ListBlockModel; |
| 104 | + |
| 105 | +/* Inheritance */ |
| 106 | + |
| 107 | +es.extend( es.ListBlockModel, es.BlockModel ); |
Property changes on: trunk/parsers/wikidom/lib/synth/models/es.ListBlockModel.js |
___________________________________________________________________ |
Added: svn:mime-type |
1 | 108 | + text/plain |
Added: svn:eol-style |
2 | 109 | + native |
Index: trunk/parsers/wikidom/lib/synth/models/es.BlockModel.js |
— | — | @@ -0,0 +1,170 @@ |
| 2 | +/** |
| 3 | + * Creates an es.BlockModel object. |
| 4 | + * |
| 5 | + * @class |
| 6 | + * @extends {es.EventEmitter} |
| 7 | + * @abstract |
| 8 | + * @constructor |
| 9 | + * @param traits {Array} List of trait names |
| 10 | + * @property traits {Array} List of trait names |
| 11 | + */ |
| 12 | +es.BlockModel = function( traits ) { |
| 13 | + es.EventEmitter.call( this ); |
| 14 | + es.ContainerItem.call( this, 'document' ); |
| 15 | + this.traits = traits || []; |
| 16 | +}; |
| 17 | + |
| 18 | +/* Static Members */ |
| 19 | + |
| 20 | +/** |
| 21 | + * Registry of constructors, mapping symbolic block type names to block constructors. |
| 22 | + * |
| 23 | + * @member |
| 24 | + * @static |
| 25 | + * @type {Object} |
| 26 | + */ |
| 27 | +es.BlockModel.constructors = {}; |
| 28 | + |
| 29 | +/* Static Methods */ |
| 30 | + |
| 31 | +/** |
| 32 | + * Creates an object that's a subclass of es.BlockModel from a plain block object. |
| 33 | + * |
| 34 | + * @method |
| 35 | + * @static |
| 36 | + * @param obj {Object} Plain block object |
| 37 | + * @returns {es.BlockModel} Object that's a subclass of es.BlockModel, or null of type was unknown |
| 38 | + */ |
| 39 | +es.BlockModel.newFromPlainObject = function( obj ) { |
| 40 | + if ( obj.type in es.BlockModel.constructors ) { |
| 41 | + return new es.BlockModel.constructors[obj.type]( obj ); |
| 42 | + } |
| 43 | + return null; |
| 44 | +}; |
| 45 | + |
| 46 | +/* Methods */ |
| 47 | + |
| 48 | +/** |
| 49 | + * Checks for a trait. |
| 50 | + * |
| 51 | + * Traits are boolean flags that indicate supported behavior |
| 52 | + * |
| 53 | + * "hasContent": Has text content |
| 54 | + * "isAnnotatable": Content may be annotated |
| 55 | + * "isAggregate": Has more than one content object |
| 56 | + * "isDocumentContainer": Contains child documents |
| 57 | + * |
| 58 | + * @method |
| 59 | + * @param trait {String} Trait name to check for |
| 60 | + * @returns {Boolean} If the block has the trait |
| 61 | + */ |
| 62 | +es.BlockModel.prototype.hasTrait = function( trait ) { |
| 63 | + return this.traits.indexOf( trait ) !== -1; |
| 64 | +}; |
| 65 | + |
| 66 | +/** |
| 67 | + * Gets the length of all content. |
| 68 | + * |
| 69 | + * @method |
| 70 | + * @returns {Integer} Length of all content |
| 71 | + */ |
| 72 | +es.BlockModel.prototype.getContentLength = function() { |
| 73 | + throw 'BlockModel.getContentLength not implemented in this subclass.'; |
| 74 | +}; |
| 75 | + |
| 76 | +/** |
| 77 | + * Inserts content at an offset. |
| 78 | + * |
| 79 | + * @method |
| 80 | + * @param offset {Integer} Offset to insert content at |
| 81 | + * @param content {es.ContentModel} Content to insert |
| 82 | + */ |
| 83 | +es.BlockModel.prototype.insertContent = function( offset, content ) { |
| 84 | + throw 'BlockModel.insertContent not implemented in this subclass.'; |
| 85 | +}; |
| 86 | + |
| 87 | +/** |
| 88 | + * Removes a range of content. |
| 89 | + * |
| 90 | + * @method |
| 91 | + * @param range {es.Range} Range of content to remove |
| 92 | + */ |
| 93 | +es.BlockModel.prototype.removeContent = function( range ) { |
| 94 | + throw 'BlockModel.removeContent not implemented in this subclass.'; |
| 95 | +}; |
| 96 | + |
| 97 | +/** |
| 98 | + * Removes all content. |
| 99 | + * |
| 100 | + * @method |
| 101 | + */ |
| 102 | +es.BlockModel.prototype.clearContent = function() { |
| 103 | + throw 'BlockModel.clearContent not implemented in this subclass.'; |
| 104 | +}; |
| 105 | + |
| 106 | +/** |
| 107 | + * Applies annotation to a range of content. |
| 108 | + * |
| 109 | + * @method |
| 110 | + * @param method {String} Method of annotation; "add", "remove" or "toggle" |
| 111 | + * @param range {es.Range} Range to annotate |
| 112 | + * @param annotation {Object} Annotation to apply |
| 113 | + */ |
| 114 | +es.BlockModel.prototype.annotateContent = function( method, range, annotation ) { |
| 115 | + throw 'BlockModel.annotateContent not implemented in this subclass.'; |
| 116 | +}; |
| 117 | + |
| 118 | +/** |
| 119 | + * Gets a list of all content objects. |
| 120 | + * |
| 121 | + * @method |
| 122 | + * @returns {Array} List of content objects |
| 123 | + */ |
| 124 | +es.BlockModel.prototype.getContents = function() { |
| 125 | + throw 'BlockModel.getContents not implemented in this subclass.'; |
| 126 | +}; |
| 127 | + |
| 128 | +/** |
| 129 | + * Gets a plain text rendering of contents. |
| 130 | + * |
| 131 | + * @method |
| 132 | + * @returns {String} Plain text rendering of contents. |
| 133 | + */ |
| 134 | +es.BlockModel.prototype.getText = function() { |
| 135 | + throw 'BlockModel.getText not implemented in this subclass.'; |
| 136 | +}; |
| 137 | + |
| 138 | +/** |
| 139 | + * Gets a range that surrounds the word closest to an offset. |
| 140 | + * |
| 141 | + * @method |
| 142 | + * @param offset {Integer} Offset to find word boundaries for |
| 143 | + * @returns {es.Range} Range of word closest to offset |
| 144 | + */ |
| 145 | +es.BlockModel.getWordBoundaries = function( offset ) { |
| 146 | + throw 'BlockModel.getWordBoundaries not implemented in this subclass.'; |
| 147 | +}; |
| 148 | + |
| 149 | +/** |
| 150 | + * Gets a range that surrounds the section closest to an offset. |
| 151 | + * |
| 152 | + * @method |
| 153 | + * @param offset {Integer} Offset to find section boundaries for |
| 154 | + * @returns {es.Range} Range of section closest to offset |
| 155 | + */ |
| 156 | +es.BlockModel.getSectionBoundaries = function( offset ) { |
| 157 | + throw 'BlockModel.getSectionBoundaries not implemented in this subclass.'; |
| 158 | +}; |
| 159 | + |
| 160 | +/** |
| 161 | + * Gets a plain object representation of block, for serialization. |
| 162 | + * |
| 163 | + * @method |
| 164 | + * @returns {Object} Plain object representation |
| 165 | + */ |
| 166 | +es.BlockModel.prototype.getPlainObject = function() { |
| 167 | + throw 'BlockModel.getPlainObject not implemented in this subclass.'; |
| 168 | +}; |
| 169 | + |
| 170 | +es.extend( es.BlockModel, es.EventEmitter ); |
| 171 | +es.extend( es.BlockModel, es.ContainerItem ); |
Property changes on: trunk/parsers/wikidom/lib/synth/models/es.BlockModel.js |
___________________________________________________________________ |
Added: svn:mime-type |
1 | 172 | + text/plain |
Added: svn:eol-style |
2 | 173 | + native |
Index: trunk/parsers/wikidom/lib/synth/models/es.ContentModel.js |
— | — | @@ -0,0 +1,574 @@ |
| 2 | +/** |
| 3 | + * Creates an es.ContentModel object. |
| 4 | + * |
| 5 | + * @class |
| 6 | + * @constructor |
| 7 | + * @param text {String} |
| 8 | + * @param annotations {Array} |
| 9 | + * @property text {String} |
| 10 | + * @property annotations {Array} |
| 11 | + */ |
| 12 | +es.ContentModel = function( data, attributes ) { |
| 13 | + es.EventEmitter.call( this ); |
| 14 | + this.data = data || []; |
| 15 | + this.attributes = attributes || {}; |
| 16 | +}; |
| 17 | + |
| 18 | +/* Static Methods */ |
| 19 | + |
| 20 | +/** |
| 21 | + * Creates an es.ContentModel object from plain text. |
| 22 | + * |
| 23 | + * @static |
| 24 | + * @method |
| 25 | + * @param text {String} Text to convert |
| 26 | + * @returns {es.ContentModel} Content object containing converted text |
| 27 | + */ |
| 28 | +es.Content.newFromText = function( text ) { |
| 29 | + return new es.ContentModel( text.split('') ); |
| 30 | +}; |
| 31 | + |
| 32 | +/** |
| 33 | + * Creates an es.ContentModel object from a plain content object. |
| 34 | + * |
| 35 | + * A plain content object contains plain text and a series of annotations to be applied to ranges of |
| 36 | + * the text. |
| 37 | + * |
| 38 | + * @example |
| 39 | + * var content = es.ContentModel.newFromPlainObject( { |
| 40 | + * 'text': '1234', |
| 41 | + * 'attributes': { |
| 42 | + * 'type': 'list', |
| 43 | + * 'styles': ['bullet'] |
| 44 | + * }, |
| 45 | + * 'annotations': [ |
| 46 | + * // Makes "23" bold |
| 47 | + * { |
| 48 | + * 'type': 'bold', |
| 49 | + * 'range': { |
| 50 | + * 'start': 1, |
| 51 | + * 'end': 3 |
| 52 | + * } |
| 53 | + * } |
| 54 | + * ] |
| 55 | + * } ); |
| 56 | + * |
| 57 | + * @static |
| 58 | + * @method |
| 59 | + * @param obj {Object} Plain content object, containing a "text" property and optionally |
| 60 | + * an "annotations" property, the latter of which being an array of annotation objects including |
| 61 | + * range information |
| 62 | + * @returns {es.ContentModel} |
| 63 | + */ |
| 64 | +es.ContentModel.newFromPlainObject = function( obj ) { |
| 65 | + var data, |
| 66 | + attributes; |
| 67 | + if ( !$.isPlainObject( obj ) ) { |
| 68 | + // Use empty content |
| 69 | + data = []; |
| 70 | + } else { |
| 71 | + // Convert string to array of characters |
| 72 | + data = obj.text.split(''); |
| 73 | + // Set attributes |
| 74 | + attributes = !$.isPlainObject( obj.attributes ) ? {} : $.extend( true, {}, obj.attributes ); |
| 75 | + // Render annotations |
| 76 | + if ( $.isArray( obj.annotations ) ) { |
| 77 | + $.each( obj.annotations, function( src ) { |
| 78 | + // Build simplified annotation object |
| 79 | + var dst = { 'type': src.type }; |
| 80 | + if ( 'data' in src ) { |
| 81 | + dst.data = es.copyObject( src.data ); |
| 82 | + } |
| 83 | + // Apply annotation to range |
| 84 | + if ( src.range.start < 0 ) { |
| 85 | + // TODO: This is invalid data! Throw error? |
| 86 | + src.range.start = 0; |
| 87 | + } |
| 88 | + if ( src.range.end > data.length ) { |
| 89 | + // TODO: This is invalid data! Throw error? |
| 90 | + src.range.end = data.length; |
| 91 | + } |
| 92 | + for ( var i = src.range.start; i < src.range.end; i++ ) { |
| 93 | + // Auto-convert to array |
| 94 | + typeof data[i] === 'string' && ( data[i] = [data[i]] ); |
| 95 | + // Append |
| 96 | + data[i].push( dst ); |
| 97 | + } |
| 98 | + } ); |
| 99 | + } |
| 100 | + } |
| 101 | + return new es.ContentModel( data, attributes ); |
| 102 | +}; |
| 103 | + |
| 104 | +/* Methods */ |
| 105 | + |
| 106 | +/** |
| 107 | + * Gets the length of the content data. |
| 108 | + * |
| 109 | + * @method |
| 110 | + * @returns {Integer} Length of content data |
| 111 | + */ |
| 112 | +es.Content.prototype.getLength = function() { |
| 113 | + return this.data.length; |
| 114 | +}; |
| 115 | + |
| 116 | +/** |
| 117 | + * Gets the value of an attribute. |
| 118 | + * |
| 119 | + * @method |
| 120 | + * @param name {String} Name of attribute to get value for |
| 121 | + * @returns {Mixed} Value of attribute, or undefined if attribute does not exist |
| 122 | + */ |
| 123 | +es.Content.prototype.getAttribute = function( name ) { |
| 124 | + return this.attributes[name]; |
| 125 | +}; |
| 126 | + |
| 127 | +/** |
| 128 | + * Sets the value of an attribute. |
| 129 | + * |
| 130 | + * @method |
| 131 | + * @param name {String} Name of attribute to set value for |
| 132 | + * @param value {Mixed} Value to set attribute to |
| 133 | + */ |
| 134 | +es.Content.prototype.setAttribute = function( name, value ) { |
| 135 | + this.attributes[name] = value; |
| 136 | +}; |
| 137 | + |
| 138 | + |
| 139 | +/** |
| 140 | + * Gets plain text version of the content within a specific range. |
| 141 | + * |
| 142 | + * Range arguments (start and end) are clamped if out of range. |
| 143 | + * |
| 144 | + * TODO: Implement render option, which will allow annotations to influence output, such as an |
| 145 | + * image outputting it's URL |
| 146 | + * |
| 147 | + * @method |
| 148 | + * @param range {es.Range} Range of text to get |
| 149 | + * @param start {Integer} Optional beginning of range, if omitted range will begin at 0 |
| 150 | + * @param end {Integer} Optional end of range, if omitted range will end a this.data.length |
| 151 | + * @param render {Boolean} If annotations should have any influence on output |
| 152 | + * @returns {String} Text within given range |
| 153 | + */ |
| 154 | +es.Content.prototype.getText = function( range, render ) { |
| 155 | + if ( !range ) { |
| 156 | + range = new es.Range( 0, this.data.length ); |
| 157 | + } else { |
| 158 | + range.normalize(); |
| 159 | + } |
| 160 | + // Copy characters |
| 161 | + var text = ''; |
| 162 | + var i; |
| 163 | + for ( i = range.start; i < range.end; i++ ) { |
| 164 | + // If not using in IE6 or IE7 (which do not support array access for strings) use this.. |
| 165 | + // text += this.data[i][0]; |
| 166 | + // Otherwise use this... |
| 167 | + text += typeof this.data[i] === 'string' ? this.data[i] : this.data[i][0]; |
| 168 | + } |
| 169 | + return text; |
| 170 | +}; |
| 171 | + |
| 172 | +/** |
| 173 | + * Gets an es.ContentModel object containing data within a specific range. |
| 174 | + * |
| 175 | + * @method |
| 176 | + * @param range {es.Range} Range of content to get |
| 177 | + * @returns {es.ContentModel} Content object containing data within range |
| 178 | + */ |
| 179 | +es.Content.prototype.getContent = function( range ) { |
| 180 | + if ( !range ) { |
| 181 | + range = new es.Range( 0, this.data.length ); |
| 182 | + } |
| 183 | + range.normalize(); |
| 184 | + return new es.Content( this.data.slice( range.start, range.end ) ); |
| 185 | +}; |
| 186 | + |
| 187 | +/** |
| 188 | + * Gets data within a specific range. |
| 189 | + * |
| 190 | + * @method |
| 191 | + * @param range {es.Range} Range of data to get |
| 192 | + * @returns {Array} Array of plain text and/or annotated characters within range |
| 193 | + */ |
| 194 | +es.Content.prototype.getData = function( range ) { |
| 195 | + if ( !range ) { |
| 196 | + range = new es.Range( 0, this.data.length ); |
| 197 | + } |
| 198 | + range.normalize(); |
| 199 | + return this.data.slice( range.start, range.end ); |
| 200 | +}; |
| 201 | + |
| 202 | +/** |
| 203 | + * Checks if a range of content contains any floating objects. |
| 204 | + * |
| 205 | + * @method |
| 206 | + * @param range {es.Range} Range of content to check |
| 207 | + * @returns {Boolean} If there's any floating objects in range |
| 208 | + */ |
| 209 | +es.Content.prototype.hasFloatingObjects = function( range ) { |
| 210 | + if ( !range ) { |
| 211 | + range = new es.Range( 0, this.data.length ); |
| 212 | + } |
| 213 | + range.normalize(); |
| 214 | + for ( var i = 0; i < this.data.length; i++ ) { |
| 215 | + if ( this.data[i].length > 1 ) { |
| 216 | + for ( var j = 1; j < this.data[i].length; j++ ) { |
| 217 | + var float = es.Content.annotationRenderers[this.data[i][j].type].float; |
| 218 | + if ( float && typeof float === 'function' ) { |
| 219 | + float = float( this.data[i][j].data ); |
| 220 | + } |
| 221 | + if ( float ) { |
| 222 | + return true; |
| 223 | + } |
| 224 | + } |
| 225 | + } |
| 226 | + } |
| 227 | + return false; |
| 228 | +}; |
| 229 | + |
| 230 | +/** |
| 231 | + * Gets the start and end points of the word closest to a given offset. |
| 232 | + * |
| 233 | + * @method |
| 234 | + * @param offset {Integer} Offset to find word nearest to |
| 235 | + * @returns {Object} Range object of boundaries |
| 236 | + */ |
| 237 | +es.Content.prototype.getWordBoundaries = function( offset ) { |
| 238 | + if ( offset < 0 || offset > this.data.length ) { |
| 239 | + throw 'Out of bounds error. Offset expected to be >= 0 and <= to ' + this.data.length; |
| 240 | + } |
| 241 | + var start = offset, |
| 242 | + end = offset, |
| 243 | + char; |
| 244 | + while ( start > 0 ) { |
| 245 | + start--; |
| 246 | + char = ( typeof this.data[start] === 'string' ? this.data[start] : this.data[start][0] ); |
| 247 | + if ( char.match( /\B/ ) ) { |
| 248 | + start++; |
| 249 | + break; |
| 250 | + } |
| 251 | + } |
| 252 | + while ( end < this.data.length ) { |
| 253 | + char = ( typeof this.data[end] === 'string' ? this.data[end] : this.data[end][0] ); |
| 254 | + if ( char.match( /\B/ ) ) { |
| 255 | + break; |
| 256 | + } |
| 257 | + end++; |
| 258 | + } |
| 259 | + return new es.Range( start, end ); |
| 260 | +}; |
| 261 | + |
| 262 | +/** |
| 263 | + * Get a plain content object. |
| 264 | + * |
| 265 | + * @method |
| 266 | + * @returns {Object} |
| 267 | + */ |
| 268 | +es.Content.prototype.getPlainObject = function() { |
| 269 | + var stack = []; |
| 270 | + // Text and annotations |
| 271 | + function start( offset, annotation ) { |
| 272 | + stack.push( $.extend( true, {}, annotation, { 'range': { 'start': offset } } ) ); |
| 273 | + } |
| 274 | + function end( offset, annotation ) { |
| 275 | + for ( var i = stack.length - 1; i >= 0; i-- ) { |
| 276 | + if ( !stack[i].range.end ) { |
| 277 | + if ( annotation ) { |
| 278 | + if ( stack[i].type === annotation.type |
| 279 | + && es.compareObjects( stack[i].data, annotation.data ) ) { |
| 280 | + stack[i].range.end = offset; |
| 281 | + break; |
| 282 | + } |
| 283 | + } else { |
| 284 | + stack[i].range.end = offset; |
| 285 | + } |
| 286 | + } |
| 287 | + } |
| 288 | + } |
| 289 | + var left = '', |
| 290 | + right, |
| 291 | + leftPlain, |
| 292 | + rightPlain, |
| 293 | + i, j, k, // iterators |
| 294 | + obj = { 'text': '' }, |
| 295 | + offset = 0; |
| 296 | + for ( i = 0; i < this.data.length; i++ ) { |
| 297 | + right = this.data[i]; |
| 298 | + leftPlain = typeof left === 'string'; |
| 299 | + rightPlain = typeof right === 'string'; |
| 300 | + // Open or close annotations |
| 301 | + if ( !leftPlain && rightPlain ) { |
| 302 | + // [formatted][plain] pair, close any annotations for left |
| 303 | + end( i - offset ); |
| 304 | + } else if ( leftPlain && !rightPlain ) { |
| 305 | + // [plain][formatted] pair, open any annotations for right |
| 306 | + for ( j = 1; j < right.length; j++ ) { |
| 307 | + start( i - offset, right[j] ); |
| 308 | + } |
| 309 | + } else if ( !leftPlain && !rightPlain ) { |
| 310 | + // [formatted][formatted] pair, open/close any differences |
| 311 | + for ( j = 1; j < left.length; j++ ) { |
| 312 | + if ( this.indexOfAnnotation( i , left[j], true ) === -1 ) { |
| 313 | + end( i - offset, left[j] ); |
| 314 | + } |
| 315 | + } |
| 316 | + for ( j = 1; j < right.length; j++ ) { |
| 317 | + if ( this.indexOfAnnotation( i - 1, right[j], true ) === -1 ) { |
| 318 | + start( i - offset, right[j] ); |
| 319 | + } |
| 320 | + } |
| 321 | + } |
| 322 | + obj.text += rightPlain ? right : right[0]; |
| 323 | + left = right; |
| 324 | + } |
| 325 | + if ( this.data.length ) { |
| 326 | + end( i - offset ); |
| 327 | + } |
| 328 | + if ( stack.length ) { |
| 329 | + obj.annotation = stack; |
| 330 | + } |
| 331 | + // Copy attributes if there are any set |
| 332 | + if ( !$.isEmptyObject( this.attributes ) ) { |
| 333 | + obj.attributes = $.extend( true, {}, this.attributes ); |
| 334 | + } |
| 335 | + return obj; |
| 336 | +}; |
| 337 | + |
| 338 | +/** |
| 339 | + * Gets a list of indexes of annotated characters which have a given annotation applied to them. |
| 340 | + * |
| 341 | + * Comparison is done first by type, and optionally also by data values (strict), not by reference |
| 342 | + * identity, thus considering annotations with identical values to be identical, even if they are |
| 343 | + * different objects. |
| 344 | + * |
| 345 | + * TODO: Since new line characters are never annotated, they are always considered covered - this |
| 346 | + * may not be ideal behavior. Consider solutions of returning more logical results. |
| 347 | + * |
| 348 | + * @method |
| 349 | + * @param range {es.Range} Range of content to analyze |
| 350 | + * @param annotation {Object} Annotation to compare with |
| 351 | + * @param strict {Boolean} Optionally compare annotation data as well as type |
| 352 | + * @returns {Array} List of indexes of covered characters within content data |
| 353 | + */ |
| 354 | +es.Content.prototype.coverageOfAnnotation = function( range, annotation, strict ) { |
| 355 | + var coverage = []; |
| 356 | + var i, index; |
| 357 | + for ( i = range.start; i < range.end; i++ ) { |
| 358 | + index = this.indexOfAnnotation( i, annotation ); |
| 359 | + if ( typeof this.data[i] !== 'string' && index !== -1 ) { |
| 360 | + if ( strict ) { |
| 361 | + if ( es.compareObjects( this.data[i][index].data, annotation.data ) ) { |
| 362 | + coverage.push( i ); |
| 363 | + } |
| 364 | + } else { |
| 365 | + coverage.push( i ); |
| 366 | + } |
| 367 | + } else if ( this.data[i] === '\n' ) { |
| 368 | + coverage.push( i ); |
| 369 | + } |
| 370 | + } |
| 371 | + return coverage; |
| 372 | +}; |
| 373 | + |
| 374 | +/** |
| 375 | + * Gets the first index within an annotated character that matches a given annotation. |
| 376 | + * |
| 377 | + * Comparison is done first by type, and optionally also by data values (strict), not by reference |
| 378 | + * identity, thus considering annotations with identical values to be identical, even if they are |
| 379 | + * different objects. |
| 380 | + * |
| 381 | + * @method |
| 382 | + * @param offset {Integer} Index of character within content data to find annotation within |
| 383 | + * @param annotation {Object} Annotation to compare with |
| 384 | + * @param strict {Boolean} Optionally compare annotation data as well as type |
| 385 | + * @returns {Integer} Index of first instance of annotation in content |
| 386 | + */ |
| 387 | +es.Content.prototype.indexOfAnnotation = function( offset, annotation, strict ) { |
| 388 | + var annotatedChar = this.data[offset]; |
| 389 | + var i; |
| 390 | + if ( typeof annotatedChar !== 'string' ) { |
| 391 | + for ( i = 1; i < this.data[offset].length; i++ ) { |
| 392 | + if ( annotatedChar[i].type === annotation.type ) { |
| 393 | + if ( strict ) { |
| 394 | + if ( es.compareObjects( annotatedChar[i].data, annotation.data ) ) { |
| 395 | + return i; |
| 396 | + } |
| 397 | + } else { |
| 398 | + return i; |
| 399 | + } |
| 400 | + } |
| 401 | + } |
| 402 | + } |
| 403 | + return -1; |
| 404 | +}; |
| 405 | + |
| 406 | +/** |
| 407 | + * Inserts content data at a specific position. |
| 408 | + * |
| 409 | + * Inserted content can inherit annotations from neighboring content (autoAnnotate). |
| 410 | + * |
| 411 | + * @method |
| 412 | + * @param offset {Integer} Position to insert content at |
| 413 | + * @param content {Array} Content data to insert |
| 414 | + * @emits "insert" with offset and content data properties |
| 415 | + * @emits "change" with type:"insert" data property |
| 416 | + */ |
| 417 | +es.Content.prototype.insert = function( offset, content, autoAnnotate ) { |
| 418 | + if ( autoAnnotate ) { |
| 419 | + // TODO: Prefer to not take annotations from a neighbor that's a space character |
| 420 | + var neighbor = this.data[Math.max( offset - 1, 0 )]; |
| 421 | + if ( $.isArray( neighbor ) ) { |
| 422 | + var annotations = neighbor.slice( 1 ); |
| 423 | + var i; |
| 424 | + for ( i = 0; i < content.length; i++ ) { |
| 425 | + if ( typeof content[i] === 'string' ) { |
| 426 | + content[i] = [content[i]]; |
| 427 | + } |
| 428 | + content[i] = content[i].concat( annotations ); |
| 429 | + } |
| 430 | + } |
| 431 | + } |
| 432 | + Array.prototype.splice.apply( this.data, [offset, 0].concat( content ) ); |
| 433 | + this.emit( 'insert', { |
| 434 | + 'offset': offset, |
| 435 | + 'content': content |
| 436 | + } ); |
| 437 | + this.emit( 'change', { 'type': 'insert' } ); |
| 438 | +}; |
| 439 | + |
| 440 | +/** |
| 441 | + * Removes content data within a specific range. |
| 442 | + * |
| 443 | + * @method |
| 444 | + * @param range {Range} Range of content to remove |
| 445 | + * @emits "remove" with range data property |
| 446 | + * @emits "change" with type:"remove" data property |
| 447 | + */ |
| 448 | +es.Content.prototype.remove = function( range ) { |
| 449 | + range.normalize(); |
| 450 | + this.data.splice( range.start, range.getLength() ); |
| 451 | + this.emit( 'remove', { |
| 452 | + 'range': range |
| 453 | + } ); |
| 454 | + this.emit( 'change', { 'type': 'remove' } ); |
| 455 | +}; |
| 456 | + |
| 457 | +/** |
| 458 | + * Removes all content data. |
| 459 | + * |
| 460 | + * @method |
| 461 | + * @emits "clear" |
| 462 | + * @emits "change" with type:"clear" data property |
| 463 | + */ |
| 464 | +es.Content.prototype.clear = function() { |
| 465 | + this.data = []; |
| 466 | + this.emit( 'clear' ); |
| 467 | + this.emit( 'change', { 'type': 'clear' } ); |
| 468 | +}; |
| 469 | + |
| 470 | +/** |
| 471 | + * Applies an annotation to content data within a given range. |
| 472 | + * |
| 473 | + * If a range arguments are not provided, all content will be annotated. New line characters are |
| 474 | + * never annotated. The add method will replace and the remove method will delete any existing |
| 475 | + * annotations with the same type as the annotation argument, regardless of their data properties. |
| 476 | + * The toggle method will use add if any of the content within the range is not already covered by |
| 477 | + * the annotation, or remove if all of it is. |
| 478 | + * |
| 479 | + * @method |
| 480 | + * @param method {String} Way to apply annotation ("toggle", "add" or "remove") |
| 481 | + * @param annotation {Object} Annotation to apply |
| 482 | + * @param range {es.Range} Range of content to annotate |
| 483 | + * @emits "annotate" with method, annotation and range data properties |
| 484 | + * @emits "change" with type:"annotate" data property |
| 485 | + */ |
| 486 | +es.Content.prototype.annotate = function( method, annotation, range ) { |
| 487 | + // Support calling without a range argument, using the full content range as default |
| 488 | + if ( !range ) { |
| 489 | + range = new es.Range( 0, this.data.length ); |
| 490 | + } else { |
| 491 | + range.normalize(); |
| 492 | + } |
| 493 | + /* |
| 494 | + * Content isolation |
| 495 | + * |
| 496 | + * Because content data is an array of either strings containing a single character each or |
| 497 | + * references to arrays containing a single character string followed by a series of references |
| 498 | + * to annotation objects, making a "copy" by slicing content data will cause references to |
| 499 | + * annotated characters in the content data to be shared between the original and the "copy". To |
| 500 | + * ensure that modifications to annotated characters in the content data do not affect the data |
| 501 | + * of other content objects, annotated characters must be sliced individually. This is too |
| 502 | + * expensive to do on all content on every copy, so we only do it when we are going to modify |
| 503 | + * the annotation information, and on as few annotated characters as possible. |
| 504 | + */ |
| 505 | + for ( var i = range.start; i < range.end; i++ ) { |
| 506 | + if ( typeof this.data[i] !== 'string' ) { |
| 507 | + this.data[i] = this.data[i].slice( 0 ); |
| 508 | + } |
| 509 | + } |
| 510 | + /* |
| 511 | + * Support toggle method by automatically choosing add or remove based on the coverage of the |
| 512 | + * content being annotated; if the content is not covered or partially covered "add" will be |
| 513 | + * used, if the content is completely covered, "remove" will be used. |
| 514 | + */ |
| 515 | + if ( method === 'toggle' ) { |
| 516 | + var coverage = this.coverageOfAnnotation( range, annotation, false ); |
| 517 | + if ( coverage.length === range.getLength() ) { |
| 518 | + var strictCoverage = this.coverageOfAnnotation( range, annotation, true ); |
| 519 | + method = strictCoverage.length === coverage.length ? 'remove' : 'add'; |
| 520 | + } else { |
| 521 | + method = 'add'; |
| 522 | + } |
| 523 | + } |
| 524 | + if ( method === 'add' ) { |
| 525 | + var duplicate; |
| 526 | + for ( var i = range.start; i < range.end; i++ ) { |
| 527 | + duplicate = -1; |
| 528 | + if ( typeof this.data[i] === 'string' ) { |
| 529 | + // Never annotate new lines |
| 530 | + if ( this.data[i] === '\n' ) { |
| 531 | + continue; |
| 532 | + } |
| 533 | + // Auto-convert to annotated character format |
| 534 | + this.data[i] = [this.data[i]]; |
| 535 | + } else { |
| 536 | + // Detect duplicate annotation |
| 537 | + duplicate = this.indexOfAnnotation( i, annotation ); |
| 538 | + } |
| 539 | + if ( duplicate === -1 ) { |
| 540 | + // Append new annotation |
| 541 | + this.data[i].push( annotation ); |
| 542 | + } else { |
| 543 | + // Replace existing annotation |
| 544 | + this.data[i][duplicate] = annotation; |
| 545 | + } |
| 546 | + } |
| 547 | + } else if ( method === 'remove' ) { |
| 548 | + for ( var i = range.start; i < range.end; i++ ) { |
| 549 | + if ( typeof this.data[i] !== 'string' ) { |
| 550 | + if ( annotation.type === 'all' ) { |
| 551 | + // Remove all annotations by converting the annotated character to a plain |
| 552 | + // character |
| 553 | + this.data[i] = this.data[i][0]; |
| 554 | + } |
| 555 | + // Remove all matching instances of annotation |
| 556 | + var j; |
| 557 | + while ( ( j = this.indexOfAnnotation( i, annotation ) ) !== -1 ) { |
| 558 | + this.data[i].splice( j, 1 ); |
| 559 | + } |
| 560 | + // Auto-convert to plain character format |
| 561 | + if ( this.data[i].length === 1 ) { |
| 562 | + this.data[i] = this.data[i][0]; |
| 563 | + } |
| 564 | + } |
| 565 | + } |
| 566 | + } |
| 567 | + this.emit( 'annotate', { |
| 568 | + 'method': method, |
| 569 | + 'annotation': annotation, |
| 570 | + 'range': range |
| 571 | + } ); |
| 572 | + this.emit( 'change', { 'type': 'annotate' } ); |
| 573 | +}; |
| 574 | + |
| 575 | +es.extend( es.ContentModel, es.EventEmitter ); |
Property changes on: trunk/parsers/wikidom/lib/synth/models/es.ContentModel.js |
___________________________________________________________________ |
Added: svn:mime-type |
1 | 576 | + text/plain |
Added: svn:eol-style |
2 | 577 | + native |
Index: trunk/parsers/wikidom/lib/synth/models/es.HeadingBlockModel.js |
— | — | @@ -0,0 +1,57 @@ |
| 2 | +/** |
| 3 | + * Creates an es.HeadingBlockModel object. |
| 4 | + * |
| 5 | + * @class |
| 6 | + * @constructor |
| 7 | + * @param content {es.ContentModel} |
| 8 | + * @param level {Integer} |
| 9 | + * @property content {es.ContentModel} |
| 10 | + * @property level {Integer} |
| 11 | + */ |
| 12 | +es.HeadingBlockModel = function( content, level ) { |
| 13 | + es.BlockModel.call( this, ['hasContent', 'isAnnotatable'] ); |
| 14 | + this.content = content || null; |
| 15 | + this.level = level || 0; |
| 16 | +}; |
| 17 | + |
| 18 | +/* Static Methods */ |
| 19 | + |
| 20 | +/** |
| 21 | + * Creates an HeadingBlockModel object from a plain object. |
| 22 | + * |
| 23 | + * @method |
| 24 | + * @static |
| 25 | + * @param obj {Object} |
| 26 | + */ |
| 27 | +es.HeadingBlockModel.newFromPlainObject = function( obj ) { |
| 28 | + return new es.HeadingBlockModel( obj.content, obj.level ); |
| 29 | +}; |
| 30 | + |
| 31 | +/* Methods */ |
| 32 | + |
| 33 | +/** |
| 34 | + * Gets the length of all content. |
| 35 | + * |
| 36 | + * @method |
| 37 | + * @returns {Integer} Length of all content |
| 38 | + */ |
| 39 | +es.HeadingBlockModel.prototype.getContentLength = function() { |
| 40 | + return this.content.getLength(); |
| 41 | +}; |
| 42 | + |
| 43 | +/** |
| 44 | + * Gets a plain heading block object. |
| 45 | + * |
| 46 | + * @method |
| 47 | + * @returns obj {Object} |
| 48 | + */ |
| 49 | +es.HeadingBlockModel.prototype.getPlainObject = function() { |
| 50 | + return { 'type': 'heading', 'content': this.content.getPlainObject(), 'level': this.level }; |
| 51 | +}; |
| 52 | + |
| 53 | +// Register constructor |
| 54 | +es.BlockModel.constructors['heading'] = es.HeadingBlockModel; |
| 55 | + |
| 56 | +/* Inheritance */ |
| 57 | + |
| 58 | +es.extend( es.HeadingBlockModel, es.BlockModel ); |
Property changes on: trunk/parsers/wikidom/lib/synth/models/es.HeadingBlockModel.js |
___________________________________________________________________ |
Added: svn:mime-type |
1 | 59 | + text/plain |
Added: svn:eol-style |
2 | 60 | + native |
Index: trunk/parsers/wikidom/lib/synth/models/es.TableBlockRowModel.js |
— | — | @@ -0,0 +1,63 @@ |
| 2 | +/** |
| 3 | + * Creates an es.TableBlockRowModel object. |
| 4 | + * |
| 5 | + * @class |
| 6 | + * @constructor |
| 7 | + * @param content {es.ContentModel} |
| 8 | + * @param styles {Array} |
| 9 | + * @property content {es.ContentModel} |
| 10 | + * @property styles {Array} |
| 11 | + */ |
| 12 | +es.TableBlockRowModel = function( cells, attributes ) { |
| 13 | + this.cells = new es.ContentSeries( cells || [] ); |
| 14 | + this.attributes = attributes || {}; |
| 15 | +}; |
| 16 | + |
| 17 | +/** |
| 18 | + * Creates an TableBlockModel object from a plain object. |
| 19 | + * |
| 20 | + * @method |
| 21 | + * @static |
| 22 | + * @param obj {Object} |
| 23 | + */ |
| 24 | +es.TableBlockRowModel.newFromPlainObject = function( obj ) { |
| 25 | + return new es.TableBlockRowModel( |
| 26 | + // Cells - if given, convert plain "item" objects to es.ListModelItem objects |
| 27 | + !$.isArray( obj.cells ) ? [] : $.map( obj.cells, function( cell ) { |
| 28 | + return !$.isPlainObject( cell ) ? null : es.DocumentModel.newFromPlainObject( cell ) |
| 29 | + } ) |
| 30 | + // Attributes - if given, make a deep copy of attributes |
| 31 | + !$.isPlainObject( obj.attributes ) ? {} : $.extend( true, {}, obj.attributes ) |
| 32 | + ); |
| 33 | +}; |
| 34 | + |
| 35 | +/* Methods */ |
| 36 | + |
| 37 | +/** |
| 38 | + * Gets the length of all content. |
| 39 | + * |
| 40 | + * @method |
| 41 | + * @returns {Integer} Length of all content |
| 42 | + */ |
| 43 | +es.TableBlockRowModel.prototype.getContentLength = function() { |
| 44 | + return this.cells.getContentLength(); |
| 45 | +}; |
| 46 | + |
| 47 | +/** |
| 48 | + * Gets a plain list block item object. |
| 49 | + * |
| 50 | + * @method |
| 51 | + * @returns obj {Object} |
| 52 | + */ |
| 53 | +es.TableBlockRowModel.prototype.getPlainObject = function() { |
| 54 | + var obj = {}; |
| 55 | + if ( this.cells.length ) { |
| 56 | + obj.cells = $.map( this.cells, function( cell ) { |
| 57 | + return cell.getPlainObject(); |
| 58 | + } ); |
| 59 | + } |
| 60 | + if ( !$.isEmptyObject( this.attributes ) ) { |
| 61 | + obj.attributes = $.extend( true, {}. this.attributes ); |
| 62 | + } |
| 63 | + return obj; |
| 64 | +}; |
Property changes on: trunk/parsers/wikidom/lib/synth/models/es.TableBlockRowModel.js |
___________________________________________________________________ |
Added: svn:mime-type |
1 | 65 | + text/plain |
Added: svn:eol-style |
2 | 66 | + native |
Index: trunk/parsers/wikidom/lib/synth/models/es.HorizontalRuleBlockModel.js |
— | — | @@ -0,0 +1,51 @@ |
| 2 | +/** |
| 3 | + * Creates an es.HorizontalRuleBlockModel object. |
| 4 | + * |
| 5 | + * @class |
| 6 | + * @constructor |
| 7 | + */ |
| 8 | +es.HorizontalRuleBlockModel = function() { |
| 9 | + es.BlockModel.call( this ); |
| 10 | +}; |
| 11 | + |
| 12 | +/* Static Methods */ |
| 13 | + |
| 14 | +/** |
| 15 | + * Creates an HorizontalRuleBlockModel object from a plain object. |
| 16 | + * |
| 17 | + * @method |
| 18 | + * @static |
| 19 | + * @param obj {Object} |
| 20 | + */ |
| 21 | +es.HorizontalRuleBlockModel.newFromPlainObject = function( obj ) { |
| 22 | + return new es.HorizontalRuleBlockModel(); |
| 23 | +}; |
| 24 | + |
| 25 | +/* Methods */ |
| 26 | + |
| 27 | +/** |
| 28 | + * Gets the length of all content - always 0. |
| 29 | + * |
| 30 | + * @method |
| 31 | + * @returns {Integer} Length of all content - always 0 |
| 32 | + */ |
| 33 | +es.HorizontalBlockModel.prototype.getContentLength = function() { |
| 34 | + return 0; |
| 35 | +}; |
| 36 | + |
| 37 | +/** |
| 38 | + * Gets a plain horizontal rule block object. |
| 39 | + * |
| 40 | + * @method |
| 41 | + * @returns obj {Object} |
| 42 | + */ |
| 43 | +es.HorizontalRuleBlockModel.prototype.getPlainObject = function() { |
| 44 | + return { 'type': 'horizontal-rule' }; |
| 45 | +}; |
| 46 | + |
| 47 | +// Register constructor |
| 48 | +es.BlockModel.constructors['horizontal-rule'] = es.HorizontalRuleBlockModel; |
| 49 | + |
| 50 | +/* Inheritance */ |
| 51 | + |
| 52 | +es.extend( es.HorizontalRuleBlockModel, es.BlockModel ); |
Property changes on: trunk/parsers/wikidom/lib/synth/models/es.HorizontalRuleBlockModel.js |
___________________________________________________________________ |
Added: svn:mime-type |
1 | 53 | + text/plain |
Added: svn:eol-style |
2 | 54 | + native |
Index: trunk/parsers/wikidom/lib/synth/controllers/es.BlockController.js |
— | — | @@ -0,0 +1,23 @@ |
| 2 | +es.BlockController = function() { |
| 3 | + // manage interactions |
| 4 | +}; |
| 5 | + |
| 6 | +es.BlockController.prototype.commit = function( transaction ) { |
| 7 | + // commit transaction |
| 8 | +}; |
| 9 | + |
| 10 | +es.BlockController.prototype.rollback = function( transaction ) { |
| 11 | + // rollback transaction |
| 12 | +}; |
| 13 | + |
| 14 | +es.BlockController.prototype.prepareInsert = function( offset, content ) { |
| 15 | + // generate transaction |
| 16 | +}; |
| 17 | + |
| 18 | +es.BlockController.prototype.prepareRemove = function( range ) { |
| 19 | + // generate transaction |
| 20 | +}; |
| 21 | + |
| 22 | +es.BlockController.prototype.prepareAnnotate = function( range, annotation ) { |
| 23 | + // generate transaction |
| 24 | +}; |
Property changes on: trunk/parsers/wikidom/lib/synth/controllers/es.BlockController.js |
___________________________________________________________________ |
Added: svn:mime-type |
1 | 25 | + text/plain |
Added: svn:eol-style |
2 | 26 | + native |
Index: trunk/parsers/wikidom/lib/synth/controllers/es.DocumentController.js |
— | — | @@ -0,0 +1,47 @@ |
| 2 | +es.DocumentController = function() { |
| 3 | + // manage interactions |
| 4 | +}; |
| 5 | + |
| 6 | +es.DocumentController.prototype.commit = function( transaction ) { |
| 7 | + // commit transaction |
| 8 | +}; |
| 9 | + |
| 10 | +es.DocumentController.prototype.rollback = function( transaction ) { |
| 11 | + // rollback transaction |
| 12 | +}; |
| 13 | + |
| 14 | +es.DocumentController.prototype.prepareInsertContent = function( offset, content ) { |
| 15 | + // generate transaction |
| 16 | +}; |
| 17 | + |
| 18 | +es.DocumentController.prototype.prepareRemoveContent = function( range ) { |
| 19 | + // generate transaction |
| 20 | +}; |
| 21 | + |
| 22 | +es.DocumentController.prototype.prepareAnnotateContent = function( range, annotation ) { |
| 23 | + // generate transaction |
| 24 | +}; |
| 25 | + |
| 26 | +es.DocumentController.prototype.prepareInsertBlock = function( index, block ) { |
| 27 | + // generate transaction |
| 28 | +}; |
| 29 | + |
| 30 | +es.DocumentController.prototype.prepareRemoveBlock = function( index ) { |
| 31 | + // generate transaction |
| 32 | +}; |
| 33 | + |
| 34 | +es.DocumentController.prototype.prepareMoveBlock = function( index, index ) { |
| 35 | + // generate transaction |
| 36 | +}; |
| 37 | + |
| 38 | +es.DocumentController.prototype.prepareMergeBlocks = function( range ) { |
| 39 | + // generate transaction |
| 40 | +}; |
| 41 | + |
| 42 | +es.DocumentController.prototype.prepareSplitBlocks = function( offset ) { |
| 43 | + // generate transaction |
| 44 | +}; |
| 45 | + |
| 46 | +es.DocumentController.prototype.prepareConvertBlock = function( index, type ) { |
| 47 | + // generate transaction |
| 48 | +}; |
Property changes on: trunk/parsers/wikidom/lib/synth/controllers/es.DocumentController.js |
___________________________________________________________________ |
Added: svn:mime-type |
1 | 49 | + text/plain |
Added: svn:eol-style |
2 | 50 | + native |
Index: trunk/parsers/wikidom/lib/synth/bases/es.ContainerItem.js |
— | — | @@ -0,0 +1,112 @@ |
| 2 | +/** |
| 3 | + * Generic Object container item. |
| 4 | + * |
| 5 | + * @class |
| 6 | + * @constructor |
| 7 | + * @extends {es.EventEmitter} |
| 8 | + * @param containerName {String} Name of container type |
| 9 | + * @property [containerName] {Object} Reference to container, if attached |
| 10 | + */ |
| 11 | +es.ContainerItem = function( containerName ) { |
| 12 | + es.EventEmitter.call( this ); |
| 13 | + if ( typeof containerName !== 'string' ) { |
| 14 | + containerName = 'container'; |
| 15 | + } |
| 16 | + this._containerName = containerName; |
| 17 | + this[this._containerName] = null; |
| 18 | +}; |
| 19 | + |
| 20 | +/* Methods */ |
| 21 | + |
| 22 | +es.ContainerItem.prototype.parent = function() { |
| 23 | + return this[this._containerName]; |
| 24 | +}; |
| 25 | + |
| 26 | +/** |
| 27 | + * Attaches item to a container. |
| 28 | + * |
| 29 | + * @method |
| 30 | + * @param container {es.Container} Container to attach to |
| 31 | + * @emits "attach" with container argument |
| 32 | + */ |
| 33 | +es.ContainerItem.prototype.attach = function( container ) { |
| 34 | + this[this._containerName] = container; |
| 35 | + this.emit( 'attach', container ); |
| 36 | +}; |
| 37 | + |
| 38 | +/** |
| 39 | + * Detaches item from a container. |
| 40 | + * |
| 41 | + * @method |
| 42 | + * @emits "detach" with container argument |
| 43 | + */ |
| 44 | +es.ContainerItem.prototype.detach = function() { |
| 45 | + var container = this[this._containerName]; |
| 46 | + this[this._containerName] = null; |
| 47 | + this.emit( 'detach', container ); |
| 48 | +}; |
| 49 | + |
| 50 | +/** |
| 51 | + * Gets the previous item in container. |
| 52 | + * |
| 53 | + * @method |
| 54 | + * @returns {Object} Previous item, or null if none exists |
| 55 | + * @throws Missing container error if getting the previous item item failed |
| 56 | + */ |
| 57 | +es.ContainerItem.prototype.previous = function() { |
| 58 | + try { |
| 59 | + return this[this._containerName].get( this[this._containerName].indexOf( this ) - 1 ); |
| 60 | + } catch ( e ) { |
| 61 | + throw 'Missing container error. Can not get previous item in missing container. ' + e; |
| 62 | + } |
| 63 | +}; |
| 64 | + |
| 65 | +/** |
| 66 | + * Gets the next item in container. |
| 67 | + * |
| 68 | + * @method |
| 69 | + * @returns {Object} Next item, or null if none exists |
| 70 | + * @throws Missing container error if getting the next item item failed |
| 71 | + */ |
| 72 | +es.ContainerItem.prototype.next = function() { |
| 73 | + try { |
| 74 | + return this[this._containerName].get( this[this._containerName].indexOf( this ) + 1 ); |
| 75 | + } catch ( e ) { |
| 76 | + throw 'Missing container error. Can not get next item in missing container. ' + e; |
| 77 | + } |
| 78 | +}; |
| 79 | + |
| 80 | +/** |
| 81 | + * Checks if this item is the first in it's container. |
| 82 | + * |
| 83 | + * @method |
| 84 | + * @returns {Boolean} If item is the first in it's container |
| 85 | + * @throws Missing container error if getting the index of this item failed |
| 86 | + */ |
| 87 | +es.ContainerItem.prototype.isFirst = function() { |
| 88 | + try { |
| 89 | + return this[this._containerName].indexOf( this ) === 0; |
| 90 | + } catch ( e ) { |
| 91 | + throw 'Missing container error. Can not get index of item in missing container. ' + e; |
| 92 | + } |
| 93 | +}; |
| 94 | + |
| 95 | +/** |
| 96 | + * Checks if this item is the last in it's container. |
| 97 | + * |
| 98 | + * @method |
| 99 | + * @returns {Boolean} If item is the last in it's container |
| 100 | + * @throws Missing container error if getting the index of this item failed |
| 101 | + */ |
| 102 | +es.ContainerItem.prototype.isLast = function() { |
| 103 | + try { |
| 104 | + return this[this._containerName].indexOf( this ) |
| 105 | + === this[this._containerName].getLength() - 1; |
| 106 | + } catch ( e ) { |
| 107 | + throw 'Missing container error. Can not get index of item in missing container. ' + e; |
| 108 | + } |
| 109 | +}; |
| 110 | + |
| 111 | +/* Inheritance */ |
| 112 | + |
| 113 | +es.extend( es.ContainerItem, es.EventEmitter ); |
Property changes on: trunk/parsers/wikidom/lib/synth/bases/es.ContainerItem.js |
___________________________________________________________________ |
Added: svn:mime-type |
1 | 114 | + text/plain |
Added: svn:eol-style |
2 | 115 | + native |
Index: trunk/parsers/wikidom/lib/synth/bases/es.DomContainerItem.js |
— | — | @@ -0,0 +1,25 @@ |
| 2 | +/** |
| 3 | + * Generic synchronized Object/Element container item. |
| 4 | + * |
| 5 | + * @class |
| 6 | + * @constructor |
| 7 | + * @extends {es.EventEmitter} |
| 8 | + * @param containerName {String} Name of container type |
| 9 | + * @param typeName {String} Name to use in CSS classes and HTML element data |
| 10 | + * @param tagName {String} HTML element name to use (optional, default: "div") |
| 11 | + * @property $ {jQuery} Container element |
| 12 | + */ |
| 13 | +es.DomContainerItem = function( containerName, typeName, tagName ) { |
| 14 | + if ( typeof tagName !== 'string' ) { |
| 15 | + tagName = 'div'; |
| 16 | + } |
| 17 | + this.$ = $( '<' + tagName + '/>' ) |
| 18 | + .addClass( 'editSurface-' + typeName ) |
| 19 | + .data( typeName, this ); |
| 20 | + |
| 21 | + es.ContainerItem.call( this, containerName ); |
| 22 | +}; |
| 23 | + |
| 24 | +/* Inheritance */ |
| 25 | + |
| 26 | +es.extend( es.DomContainerItem, es.ContainerItem ); |
Property changes on: trunk/parsers/wikidom/lib/synth/bases/es.DomContainerItem.js |
___________________________________________________________________ |
Added: svn:mime-type |
1 | 27 | + text/plain |
Added: svn:eol-style |
2 | 28 | + native |
Index: trunk/parsers/wikidom/lib/synth/bases/es.Container.js |
— | — | @@ -0,0 +1,249 @@ |
| 2 | +/** |
| 3 | + * Generic Object container. |
| 4 | + * |
| 5 | + * Child objects must extend es.ContainerItem. |
| 6 | + * |
| 7 | + * @class |
| 8 | + * @constructor |
| 9 | + * @extends {es.EventEmitter} |
| 10 | + * @param listName {String} Property name for list of items |
| 11 | + * @property [listName] {Array} list of items |
| 12 | + */ |
| 13 | +es.Container = function( listName ) { |
| 14 | + es.EventEmitter.call( this ); |
| 15 | + if ( typeof listName !== 'string' ) { |
| 16 | + listName = 'items'; |
| 17 | + } |
| 18 | + this._listName = listName; |
| 19 | + this[this._listName] = []; |
| 20 | + var container = this; |
| 21 | + this.relayUpdate = function() { |
| 22 | + container.emit( 'update' ); |
| 23 | + }; |
| 24 | +}; |
| 25 | + |
| 26 | +/* Methods */ |
| 27 | + |
| 28 | +/** |
| 29 | + * Gets an item at a specific index. |
| 30 | + * |
| 31 | + * @method |
| 32 | + * @returns {Object} Child object at index |
| 33 | + */ |
| 34 | +es.Container.prototype.get = function( index ) { |
| 35 | + return this[this._listName][index] || null; |
| 36 | +}; |
| 37 | + |
| 38 | +/** |
| 39 | + * Gets all items. |
| 40 | + * |
| 41 | + * @method |
| 42 | + * @returns {Array} List of all items. |
| 43 | + */ |
| 44 | +es.Container.prototype.items = function() { |
| 45 | + return this[this._listName]; |
| 46 | +}; |
| 47 | + |
| 48 | +/** |
| 49 | + * Gets the number of items in container. |
| 50 | + * |
| 51 | + * @method |
| 52 | + * @returns {Integer} Number of items in container |
| 53 | + */ |
| 54 | +es.Container.prototype.getLength = function() { |
| 55 | + return this[this._listName].length |
| 56 | +}; |
| 57 | + |
| 58 | +/** |
| 59 | + * Gets the index of an item. |
| 60 | + * |
| 61 | + * @method |
| 62 | + * @returns {Integer} Index of item, -1 if item is not in container |
| 63 | + */ |
| 64 | +es.Container.prototype.indexOf = function( item ) { |
| 65 | + return this[this._listName].indexOf( item ); |
| 66 | +}; |
| 67 | + |
| 68 | +/** |
| 69 | + * Gets the first item in the container. |
| 70 | + * |
| 71 | + * @method |
| 72 | + * @returns {Object} First item |
| 73 | + */ |
| 74 | +es.Container.prototype.first = function() { |
| 75 | + return this[this._listName].length ? this[this._listName][0] : null; |
| 76 | +}; |
| 77 | + |
| 78 | +/** |
| 79 | + * Gets the last item in the container. |
| 80 | + * |
| 81 | + * @method |
| 82 | + * @returns {Object} Last item |
| 83 | + */ |
| 84 | +es.Container.prototype.last = function() { |
| 85 | + return this[this._listName].length |
| 86 | + ? this[this._listName][this[this._listName].length - 1] : null; |
| 87 | +}; |
| 88 | + |
| 89 | +/** |
| 90 | + * Iterates over items, executing a callback for each. |
| 91 | + * |
| 92 | + * Returning false in the callback will stop iteration. |
| 93 | + * |
| 94 | + * @method |
| 95 | + * @param callback {Function} Function to call on each item which takes item and index arguments |
| 96 | + */ |
| 97 | +es.Container.prototype.each = function( callback ) { |
| 98 | + for ( var i = 0; i < this[this._listName].length; i++ ) { |
| 99 | + if ( callback( this[this._listName][i], i ) === false ) { |
| 100 | + break; |
| 101 | + } |
| 102 | + } |
| 103 | +}; |
| 104 | + |
| 105 | +/** |
| 106 | + * Adds an item to the end of the container. |
| 107 | + * |
| 108 | + * Also inserts item's Element object to the DOM and adds a listener to its "update" events. |
| 109 | + * |
| 110 | + * @method |
| 111 | + * @param item {Object} Item to append |
| 112 | + * @emits "update" |
| 113 | + */ |
| 114 | +es.Container.prototype.append = function( item ) { |
| 115 | + var parent = item.parent(); |
| 116 | + if ( parent === this ) { |
| 117 | + this[this._listName].splice( this.indexOf( item ), 1 ); |
| 118 | + this[this._listName].push( item ); |
| 119 | + } else { |
| 120 | + if ( parent ) { |
| 121 | + parent.remove( item ); |
| 122 | + } |
| 123 | + this[this._listName].push( item ); |
| 124 | + var container = this; |
| 125 | + item.on( 'update', this.relayUpdate ); |
| 126 | + item.attach( this ); |
| 127 | + } |
| 128 | + this.emit( 'append', item ); |
| 129 | + this.emit( 'update' ); |
| 130 | +}; |
| 131 | + |
| 132 | +/** |
| 133 | + * Adds an item to the beginning of the container. |
| 134 | + * |
| 135 | + * Also inserts item's Element object to the DOM and adds a listener to its "update" events. |
| 136 | + * |
| 137 | + * @method |
| 138 | + * @param item {Object} Item to prepend |
| 139 | + * @emits "update" |
| 140 | + */ |
| 141 | +es.Container.prototype.prepend = function( item ) { |
| 142 | + var parent = item.parent(); |
| 143 | + if ( parent === this ) { |
| 144 | + this[this._listName].splice( this.indexOf( item ), 1 ); |
| 145 | + this[this._listName].unshift( item ); |
| 146 | + } else { |
| 147 | + if ( parent ) { |
| 148 | + parent.remove( item ); |
| 149 | + } |
| 150 | + this[this._listName].unshift( item ); |
| 151 | + var container = this; |
| 152 | + item.on( 'update', this.relayUpdate ); |
| 153 | + item.attach( this ); |
| 154 | + } |
| 155 | + this.emit( 'prepend', item ); |
| 156 | + this.emit( 'update' ); |
| 157 | +}; |
| 158 | + |
| 159 | +/** |
| 160 | + * Adds an item to the container after an existing item. |
| 161 | + * |
| 162 | + * Also inserts item's Element object to the DOM and adds a listener to its "update" events. |
| 163 | + * |
| 164 | + * @method |
| 165 | + * @param item {Object} Item to insert |
| 166 | + * @param before {Object} Item to insert before, if null then item will be inserted at the end |
| 167 | + * @emits "update" |
| 168 | + */ |
| 169 | +es.Container.prototype.insertBefore = function( item, before ) { |
| 170 | + var parent = item.parent(); |
| 171 | + if ( parent === this ) { |
| 172 | + this[this._listName].splice( this.indexOf( item ), 1 ); |
| 173 | + if ( before ) { |
| 174 | + this[this._listName].splice( this[this._listName].indexOf( before ), 0, item ); |
| 175 | + } else { |
| 176 | + this[this._listName].unshift( item ); |
| 177 | + } |
| 178 | + } else { |
| 179 | + if ( parent ) { |
| 180 | + parent.remove( item ); |
| 181 | + } |
| 182 | + if ( before ) { |
| 183 | + this[this._listName].splice( this[this._listName].indexOf( before ), 0, item ); |
| 184 | + } else { |
| 185 | + this[this._listName].unshift( item ); |
| 186 | + } |
| 187 | + var container = this; |
| 188 | + item.on( 'update', this.relayUpdate ); |
| 189 | + item.attach( this ); |
| 190 | + } |
| 191 | + this.emit( 'insertBefore', item, before ); |
| 192 | + this.emit( 'update' ); |
| 193 | +}; |
| 194 | + |
| 195 | +/** |
| 196 | + * Adds an item to the container after an existing item. |
| 197 | + * |
| 198 | + * Also inserts item's Element object to the DOM and adds a listener to its "update" events. |
| 199 | + * |
| 200 | + * @method |
| 201 | + * @param item {Object} Item to insert |
| 202 | + * @param after {Object} Item to insert after, if null item will be inserted at the end |
| 203 | + * @emits "update" |
| 204 | + */ |
| 205 | +es.Container.prototype.insertAfter = function( item, after ) { |
| 206 | + var parent = item.parent(); |
| 207 | + if ( parent === this ) { |
| 208 | + this[this._listName].splice( this.indexOf( item ), 1 ); |
| 209 | + if ( after ) { |
| 210 | + this[this._listName].splice( this[this._listName].indexOf( after ) + 1, 0, item ); |
| 211 | + } else { |
| 212 | + this[this._listName].push( item ); |
| 213 | + } |
| 214 | + } else { |
| 215 | + if ( parent ) { |
| 216 | + parent.remove( item ); |
| 217 | + } |
| 218 | + if ( after ) { |
| 219 | + this[this._listName].splice( this[this._listName].indexOf( after ) + 1, 0, item ); |
| 220 | + } else { |
| 221 | + this[this._listName].push( item ); |
| 222 | + } |
| 223 | + var container = this; |
| 224 | + item.on( 'update', this.relayUpdate ); |
| 225 | + item.attach( this ); |
| 226 | + } |
| 227 | + this.emit( 'insertAfter', item, after ); |
| 228 | + this.emit( 'update' ); |
| 229 | +}; |
| 230 | + |
| 231 | +/** |
| 232 | + * Removes an item from the container. |
| 233 | + * |
| 234 | + * Also detaches item's Element object to the DOM and removes all listeners its "update" events. |
| 235 | + * |
| 236 | + * @method |
| 237 | + * @param item {Object} Item to remove |
| 238 | + * @emits "update" |
| 239 | + */ |
| 240 | +es.Container.prototype.remove = function( item ) { |
| 241 | + item.removeListener( 'update', this.relayUpdate ); |
| 242 | + this[this._listName].splice( this.indexOf( item ), 1 ); |
| 243 | + item.detach(); |
| 244 | + this.emit( 'remove', item ); |
| 245 | + this.emit( 'update' ); |
| 246 | +}; |
| 247 | + |
| 248 | +/* Inheritance */ |
| 249 | + |
| 250 | +es.extend( es.Container, es.EventEmitter ); |
Property changes on: trunk/parsers/wikidom/lib/synth/bases/es.Container.js |
___________________________________________________________________ |
Added: svn:mime-type |
1 | 251 | + text/plain |
Added: svn:eol-style |
2 | 252 | + native |
Index: trunk/parsers/wikidom/lib/synth/bases/es.DomContainer.js |
— | — | @@ -0,0 +1,49 @@ |
| 2 | +/** |
| 3 | + * Generic synchronized Object/Element container. |
| 4 | + * |
| 5 | + * Items must extend es.DomContainerItem. |
| 6 | + * |
| 7 | + * @class |
| 8 | + * @constructor |
| 9 | + * @extends {es.EventEmitter} |
| 10 | + * @param listName {String} Property name for list of items |
| 11 | + * @param typeName {String} Name to use in CSS classes and HTML element data |
| 12 | + * @param tagName {String} HTML element name to use (optional, default: "div") |
| 13 | + * @property $ {jQuery} Container element |
| 14 | + */ |
| 15 | +es.DomContainer = function( listName, typeName, tagName ) { |
| 16 | + if ( typeof tagName !== 'string' ) { |
| 17 | + tagName = 'div'; |
| 18 | + } |
| 19 | + this.$ = $( '<' + tagName + '/>' ) |
| 20 | + .addClass( 'editSurface-' + typeName ) |
| 21 | + .data( typeName, this ); |
| 22 | + es.Container.call( this, listName ); |
| 23 | + this.on( 'prepend', function( item ) { |
| 24 | + this.$.prepend( item.$ ); |
| 25 | + } ); |
| 26 | + this.on( 'append', function( item ) { |
| 27 | + this.$.append( item.$ ); |
| 28 | + } ); |
| 29 | + this.on( 'insertBefore', function( item, before ) { |
| 30 | + if ( before ) { |
| 31 | + item.$.insertBefore( before.$ ); |
| 32 | + } else { |
| 33 | + this.$.append( item.$ ); |
| 34 | + } |
| 35 | + } ); |
| 36 | + this.on( 'insertAfter', function( item, after ) { |
| 37 | + if ( after ) { |
| 38 | + item.$.insertAfter( after.$ ); |
| 39 | + } else { |
| 40 | + this.$.append( item.$ ); |
| 41 | + } |
| 42 | + } ); |
| 43 | + this.on( 'remove', function( item ) { |
| 44 | + item.$.detach(); |
| 45 | + } ); |
| 46 | +}; |
| 47 | + |
| 48 | +/* Inheritance */ |
| 49 | + |
| 50 | +es.extend( es.DomContainer, es.Container ); |
Property changes on: trunk/parsers/wikidom/lib/synth/bases/es.DomContainer.js |
___________________________________________________________________ |
Added: svn:mime-type |
1 | 51 | + text/plain |
Added: svn:eol-style |
2 | 52 | + native |
Index: trunk/parsers/wikidom/lib/synth/bases/es.ContentSeries.js |
— | — | @@ -0,0 +1,84 @@ |
| 2 | +/** |
| 3 | + * Creates an es.ContentSeries object. |
| 4 | + * |
| 5 | + * A content series is an array of items which have a getLength method. |
| 6 | + */ |
| 7 | +es.ContentSeries = function() { |
| 8 | + var series = []; |
| 9 | + $.extend( series, this ); |
| 10 | + return series; |
| 11 | +}; |
| 12 | + |
| 13 | +es.ContentSeries.prototype.lookup = function( offset ) { |
| 14 | + if ( this.length ) { |
| 15 | + var i = 0, |
| 16 | + legnth = this.length, |
| 17 | + left = 0, |
| 18 | + right; |
| 19 | + while ( i < length ) { |
| 20 | + right = left + this[i].getLength(); |
| 21 | + if ( offset >= left && offset < right ) { |
| 22 | + return this[i]; |
| 23 | + } |
| 24 | + left = right; |
| 25 | + i++; |
| 26 | + } |
| 27 | + } |
| 28 | + return null; |
| 29 | +}; |
| 30 | + |
| 31 | +es.ContentSeries.prototype.offsetOf = function( item ) { |
| 32 | + if ( this.length ) { |
| 33 | + var i = 0, |
| 34 | + legnth = this.length, |
| 35 | + left = 0; |
| 36 | + while ( i < length ) { |
| 37 | + if ( this[i] === item ) { |
| 38 | + return new es.Range( left, left + this[i].getLength() ); |
| 39 | + } |
| 40 | + left += this[i].getLength(); |
| 41 | + i++; |
| 42 | + } |
| 43 | + } |
| 44 | + return null; |
| 45 | +}; |
| 46 | + |
| 47 | +es.ContentSeries.prototype.size = function() { |
| 48 | + var sum = 0; |
| 49 | + for ( var i = 0, length = this.length; i++ ) { |
| 50 | + sum += this[i].getLength(); |
| 51 | + } |
| 52 | + return sum; |
| 53 | +}; |
| 54 | + |
| 55 | +es.ContentSeries.prototype.select = function( start, end ) { |
| 56 | + // Support es.Range object as first argument |
| 57 | + if ( typeof start.from !== undefined && typeof start.to !== undefined ) { |
| 58 | + start.normalize(); |
| 59 | + end = start.end; |
| 60 | + start = start.start; |
| 61 | + } |
| 62 | + var items = []; |
| 63 | + if ( this.length ) { |
| 64 | + var i = 0, |
| 65 | + legnth = this.length, |
| 66 | + left = 0, |
| 67 | + right, |
| 68 | + inside = false; |
| 69 | + while ( i < length ) { |
| 70 | + right = left + this[i].getLength(); |
| 71 | + if ( inside ) { |
| 72 | + items.push( this[i] ); |
| 73 | + if ( end >= left && end < right ) { |
| 74 | + break; |
| 75 | + } |
| 76 | + } else if ( start >= left && start < right ) { |
| 77 | + inside = true; |
| 78 | + items.push( this[i] ); |
| 79 | + } |
| 80 | + left = right; |
| 81 | + i++; |
| 82 | + } |
| 83 | + } |
| 84 | + return items; |
| 85 | +}; |
Property changes on: trunk/parsers/wikidom/lib/synth/bases/es.ContentSeries.js |
___________________________________________________________________ |
Added: svn:mime-type |
1 | 86 | + text/plain |
Added: svn:eol-style |
2 | 87 | + native |
Index: trunk/parsers/wikidom/lib/synth/bases/es.EventEmitter.js |
— | — | @@ -0,0 +1,137 @@ |
| 2 | +/** |
| 3 | + * Event emitter. |
| 4 | + * |
| 5 | + * @class |
| 6 | + * @constructor |
| 7 | + * @property events {Object} |
| 8 | + */ |
| 9 | +es.EventEmitter = function() { |
| 10 | + this.events = {}; |
| 11 | +} |
| 12 | + |
| 13 | +/* Methods */ |
| 14 | + |
| 15 | +/** |
| 16 | + * Emits an event. |
| 17 | + * |
| 18 | + * @method |
| 19 | + * @param type {String} Type of event |
| 20 | + * @param args {Mixed} First in a list of variadic arguments passed to event handler (optional) |
| 21 | + * @returns {Boolean} If event was handled by at least one listener |
| 22 | + */ |
| 23 | +es.EventEmitter.prototype.emit = function( type ) { |
| 24 | + if ( type === 'error' && !( 'error' in this.events ) ) { |
| 25 | + throw 'Missing error handler error.'; |
| 26 | + } |
| 27 | + if ( !( type in this.events ) ) { |
| 28 | + return false; |
| 29 | + } |
| 30 | + var listeners = this.events[type].slice(); |
| 31 | + var args = Array.prototype.slice.call( arguments, 1 ); |
| 32 | + for ( var i = 0; i < listeners.length; i++ ) { |
| 33 | + listeners[i].apply( this, args ); |
| 34 | + } |
| 35 | + return true; |
| 36 | +}; |
| 37 | + |
| 38 | +/** |
| 39 | + * Adds a listener to events of a specific type. |
| 40 | + * |
| 41 | + * @method |
| 42 | + * @param type {String} Type of event to listen to |
| 43 | + * @param listener {Function} Listener to call when event occurs |
| 44 | + * @returns {es.EventEmitter} This object |
| 45 | + * @throws "Invalid listener error" if listener argument is not a function |
| 46 | + */ |
| 47 | +es.EventEmitter.prototype.addListener = function( type, listener ) { |
| 48 | + if ( typeof listener !== 'function' ) { |
| 49 | + throw 'Invalid listener error. Function expected.'; |
| 50 | + } |
| 51 | + this.emit( 'newListener', type, listener ); |
| 52 | + if ( type in this.events ) { |
| 53 | + this.events[type].push( listener ); |
| 54 | + } else { |
| 55 | + this.events[type] = [listener]; |
| 56 | + } |
| 57 | + return this; |
| 58 | +}; |
| 59 | + |
| 60 | +/** |
| 61 | + * Alias for addListener |
| 62 | + * |
| 63 | + * @method |
| 64 | + */ |
| 65 | +es.EventEmitter.prototype.on = es.EventEmitter.prototype.addListener; |
| 66 | + |
| 67 | +/** |
| 68 | + * Adds a one-time listener to a specific event. |
| 69 | + * |
| 70 | + * @method |
| 71 | + * @param type {String} Type of event to listen to |
| 72 | + * @param listener {Function} Listener to call when event occurs |
| 73 | + * @returns {es.EventEmitter} This object |
| 74 | + */ |
| 75 | +es.EventEmitter.prototype.once = function( type, listener ) { |
| 76 | + var eventEmitter = this; |
| 77 | + return this.addListener( type, function listenerWrapper() { |
| 78 | + eventEmitter.removeListener( type, listenerWrapper ); |
| 79 | + listener.apply( eventEmitter, arguments ); |
| 80 | + } ); |
| 81 | +}; |
| 82 | + |
| 83 | +/** |
| 84 | + * Removes a specific listener from a specific event. |
| 85 | + * |
| 86 | + * @method |
| 87 | + * @param type {String} Type of event to remove listener from |
| 88 | + * @param listener {Function} Listener to remove |
| 89 | + * @returns {es.EventEmitter} This object |
| 90 | + * @throws "Invalid listener error" if listener argument is not a function |
| 91 | + */ |
| 92 | +es.EventEmitter.prototype.removeListener = function( type, listener ) { |
| 93 | + if ( typeof listener !== 'function' ) { |
| 94 | + throw 'Invalid listener error. Function expected.'; |
| 95 | + } |
| 96 | + if ( !( type in this.events ) || !this.events[type].length ) { |
| 97 | + return this; |
| 98 | + } |
| 99 | + var handlers = this.events[type]; |
| 100 | + if ( handlers.length == 1 && handlers[0] === listener ) { |
| 101 | + delete this.events[type]; |
| 102 | + } else { |
| 103 | + var i = handlers.indexOf( listener ); |
| 104 | + if ( i < 0 ) { |
| 105 | + return this; |
| 106 | + } |
| 107 | + handlers.splice( i, 1 ); |
| 108 | + if ( handlers.length == 0 ) { |
| 109 | + delete this.events[type]; |
| 110 | + } |
| 111 | + } |
| 112 | + return this; |
| 113 | +}; |
| 114 | + |
| 115 | +/** |
| 116 | + * Removes all listeners from a specific event. |
| 117 | + * |
| 118 | + * @method |
| 119 | + * @param type {String} Type of event to remove listeners from |
| 120 | + * @returns {es.EventEmitter} This object |
| 121 | + */ |
| 122 | +es.EventEmitter.prototype.removeAllListeners = function( type ) { |
| 123 | + if ( type in this.events ) { |
| 124 | + delete this.events[type]; |
| 125 | + } |
| 126 | + return this; |
| 127 | +}; |
| 128 | + |
| 129 | +/** |
| 130 | + * Gets a list of listeners attached to a specific event. |
| 131 | + * |
| 132 | + * @method |
| 133 | + * @param type {String} Type of event to get listeners for |
| 134 | + * @returns {Array} List of listeners to an event |
| 135 | + */ |
| 136 | +es.EventEmitter.prototype.listeners = function( type ) { |
| 137 | + return type in this.events ? this.events[type] : []; |
| 138 | +}; |
Property changes on: trunk/parsers/wikidom/lib/synth/bases/es.EventEmitter.js |
___________________________________________________________________ |
Added: svn:mime-type |
1 | 139 | + text/plain |
Added: svn:eol-style |
2 | 140 | + native |
Index: trunk/parsers/wikidom/lib/synth/views/es.BlockView.js |
— | — | @@ -0,0 +1,43 @@ |
| 2 | +/** |
| 3 | + * Creates an es.BlockView object. |
| 4 | + * |
| 5 | + * @class |
| 6 | + * @extends {es.DomContainerItem} |
| 7 | + * @abstract |
| 8 | + * @constructor |
| 9 | + * @param typeName {String} Name of block type (optional, default: "block") |
| 10 | + * @param tagName {String} HTML tag name to use in rendering (optional, default: "div") |
| 11 | + */ |
| 12 | +es.BlockView = function( typeName, tagName ) { |
| 13 | + es.DomContainerItem.call( this, 'document', typeName || 'block', tagName || 'div' ); |
| 14 | +}; |
| 15 | + |
| 16 | +/** |
| 17 | + * Render content. |
| 18 | + */ |
| 19 | +es.BlockView.prototype.renderContent = function() { |
| 20 | + throw 'BlockView.renderContent not implemented in this subclass.'; |
| 21 | +}; |
| 22 | + |
| 23 | +/** |
| 24 | + * Gets offset within content of position. |
| 25 | + */ |
| 26 | +es.BlockView.getContentOffset = function( position ) { |
| 27 | + throw 'BlockView.getContentOffset not implemented in this subclass.'; |
| 28 | +}; |
| 29 | + |
| 30 | +/** |
| 31 | + * Gets rendered position of offset within content. |
| 32 | + */ |
| 33 | +es.BlockView.getRenderedPosition = function( offset ) { |
| 34 | + throw 'BlockView.getRenderedPosition not implemented in this subclass.'; |
| 35 | +}; |
| 36 | + |
| 37 | +/** |
| 38 | + * Gets rendered line index of offset within content. |
| 39 | + */ |
| 40 | +es.BlockView.getRenderedLineIndex = function( offset ) { |
| 41 | + throw 'BlockView.getRenderedLineIndex not implemented in this subclass.'; |
| 42 | +}; |
| 43 | + |
| 44 | +es.extend( es.BlockView, es.DomContainerItem ); |
Property changes on: trunk/parsers/wikidom/lib/synth/views/es.BlockView.js |
___________________________________________________________________ |
Added: svn:mime-type |
1 | 45 | + text/plain |
Added: svn:eol-style |
2 | 46 | + native |
Index: trunk/parsers/wikidom/lib/synth/views/es.ContentView.js |
— | — | @@ -0,0 +1,756 @@ |
| 2 | +/** |
| 3 | + * Creates an es.ContentView object. |
| 4 | + * |
| 5 | + * A content view flows text into a DOM element and provides methods to get information about the |
| 6 | + * rendered output. HTML serialized specifically for rendering into and editing surface. |
| 7 | + * |
| 8 | + * Rendering occurs automatically when content is modified, by responding to "insert", "remove", |
| 9 | + * "clear" and "annotate" events. Rendering is iterative and interruptable to reduce user feedback |
| 10 | + * latency. |
| 11 | + * |
| 12 | + * TODO: Cleanup code and comments |
| 13 | + * |
| 14 | + * @class |
| 15 | + * @constructor |
| 16 | + * @extends {es.EventEmitter} |
| 17 | + * @param $container {jQuery} Element to render into |
| 18 | + * @param model {es.ContentModel} Content model to view |
| 19 | + * @property $ {jQuery} |
| 20 | + * @property model {es.ContentModel} |
| 21 | + * @property boundaries {Array} |
| 22 | + * @property lines {Array} |
| 23 | + * @property width {Integer} |
| 24 | + * @property bondaryTest {RegExp} |
| 25 | + * @property widthCache {Object} |
| 26 | + * @property renderState {Object} |
| 27 | + */ |
| 28 | +es.ContentView = function( $container, model ) { |
| 29 | + es.EventEmitter.call( this ); |
| 30 | + this.$ = $container; |
| 31 | + this.model = model || new es.ContentModel(); |
| 32 | + this.boundaries = []; |
| 33 | + this.lines = []; |
| 34 | + this.width = null; |
| 35 | + this.boundaryTest = /([ \-\t\r\n\f])/g; |
| 36 | + this.widthCache = {}; |
| 37 | + this.renderState = {}; |
| 38 | + var that = this; |
| 39 | + function render( args ) { |
| 40 | + that.scanBoundaries(); |
| 41 | + that.render( args ? args.offset : 0 ); |
| 42 | + } |
| 43 | + this.model.on( 'insert', render ); |
| 44 | + this.model.on( 'remove', render ); |
| 45 | + this.model.on( 'clear', render ); |
| 46 | + this.model.on( 'annotate', render ); |
| 47 | + this.scanBoundaries(); |
| 48 | +}; |
| 49 | + |
| 50 | +/* Static Members */ |
| 51 | + |
| 52 | +/** |
| 53 | + * List of annotation rendering implementations. |
| 54 | + * |
| 55 | + * Each supported annotation renderer must have an open and close property, each either a string or |
| 56 | + * a function which accepts a data argument. |
| 57 | + * |
| 58 | + * @static |
| 59 | + * @member |
| 60 | + */ |
| 61 | +es.ContentView.annotationRenderers = { |
| 62 | + 'template': { |
| 63 | + 'open': function( data ) { |
| 64 | + return '<span class="editSurface-format-object">' + data.html; |
| 65 | + }, |
| 66 | + 'close': '</span>', |
| 67 | + 'float': function( data ) { |
| 68 | + console.log( data.html ); |
| 69 | + return $( data.html ).css( 'float' ); |
| 70 | + } |
| 71 | + }, |
| 72 | + 'bold': { |
| 73 | + 'open': '<span class="editSurface-format-bold">', |
| 74 | + 'close': '</span>', |
| 75 | + 'float': false |
| 76 | + }, |
| 77 | + 'italic': { |
| 78 | + 'open': '<span class="editSurface-format-italic">', |
| 79 | + 'close': '</span>', |
| 80 | + 'float': false |
| 81 | + }, |
| 82 | + 'size': { |
| 83 | + 'open': function( data ) { |
| 84 | + return '<span class="editSurface-format-' + data.type + '">'; |
| 85 | + }, |
| 86 | + 'close': '</span>', |
| 87 | + 'float': false |
| 88 | + }, |
| 89 | + 'script': { |
| 90 | + 'open': function( data ) { |
| 91 | + return '<span class="editSurface-format-' + data.type + '">'; |
| 92 | + }, |
| 93 | + 'close': '</span>', |
| 94 | + 'float': false |
| 95 | + }, |
| 96 | + 'xlink': { |
| 97 | + 'open': function( data ) { |
| 98 | + return '<span class="editSurface-format-link" data-href="' + data.href + '">'; |
| 99 | + }, |
| 100 | + 'close': '</span>', |
| 101 | + 'float': false |
| 102 | + }, |
| 103 | + 'ilink': { |
| 104 | + 'open': function( data ) { |
| 105 | + return '<span class="editSurface-format-link" data-title="' + data.title + '">'; |
| 106 | + }, |
| 107 | + 'close': '</span>', |
| 108 | + 'float': false |
| 109 | + } |
| 110 | +}; |
| 111 | + |
| 112 | +/** |
| 113 | + * Mapping of character and HTML entities or renderings. |
| 114 | + * |
| 115 | + * @static |
| 116 | + * @member |
| 117 | + */ |
| 118 | +es.ContentView.htmlCharacters = { |
| 119 | + '&': '&', |
| 120 | + '<': '<', |
| 121 | + '>': '>', |
| 122 | + '\'': ''', |
| 123 | + '"': '"', |
| 124 | + ' ': ' ', |
| 125 | + '\n': '<span class="editSurface-whitespace">¶</span>', |
| 126 | + '\t': '<span class="editSurface-whitespace">⇾</span>' |
| 127 | +}; |
| 128 | + |
| 129 | +/* Static Methods */ |
| 130 | + |
| 131 | +/** |
| 132 | + * Gets a rendered opening or closing of an annotation. |
| 133 | + * |
| 134 | + * Tag nesting is handled using a stack, which keeps track of what is currently open. A common stack |
| 135 | + * argument should be used while rendering content. |
| 136 | + * |
| 137 | + * @static |
| 138 | + * @method |
| 139 | + * @param bias {String} Which side of the annotation to render, either "open" or "close" |
| 140 | + * @param annotation {Object} Annotation to render |
| 141 | + * @param stack {Array} List of currently open annotations |
| 142 | + * @returns {String} Rendered annotation |
| 143 | + */ |
| 144 | +es.ContentView.renderAnnotation = function( bias, annotation, stack ) { |
| 145 | + var renderers = es.ContentView.annotationRenderers, |
| 146 | + type = annotation.type, |
| 147 | + out = ''; |
| 148 | + if ( type in renderers ) { |
| 149 | + if ( bias === 'open' ) { |
| 150 | + // Add annotation to the top of the stack |
| 151 | + stack.push( annotation ); |
| 152 | + // Open annotation |
| 153 | + out += typeof renderers[type]['open'] === 'function' |
| 154 | + ? renderers[type]['open']( annotation.data ) |
| 155 | + : renderers[type]['open']; |
| 156 | + } else { |
| 157 | + if ( stack[stack.length - 1] === annotation ) { |
| 158 | + // Remove annotation from top of the stack |
| 159 | + stack.pop(); |
| 160 | + // Close annotation |
| 161 | + out += typeof renderers[type]['close'] === 'function' |
| 162 | + ? renderers[type]['close']( annotation.data ) |
| 163 | + : renderers[type]['close']; |
| 164 | + } else { |
| 165 | + // Find the annotation in the stack |
| 166 | + var depth = stack.indexOf( annotation ); |
| 167 | + if ( depth === -1 ) { |
| 168 | + throw 'Invalid stack error. An element is missing from the stack.'; |
| 169 | + } |
| 170 | + // Close each already opened annotation |
| 171 | + for ( var i = stack.length - 1; i >= depth + 1; i-- ) { |
| 172 | + out += typeof renderers[stack[i].type]['close'] === 'function' |
| 173 | + ? renderers[stack[i].type]['close']( stack[i].data ) |
| 174 | + : renderers[stack[i].type]['close']; |
| 175 | + } |
| 176 | + // Close the buried annotation |
| 177 | + out += typeof renderers[type]['close'] === 'function' |
| 178 | + ? renderers[type]['close']( annotation.data ) |
| 179 | + : renderers[type]['close']; |
| 180 | + // Re-open each previously opened annotation |
| 181 | + for ( var i = depth + 1; i < stack.length; i++ ) { |
| 182 | + out += typeof renderers[stack[i].type]['open'] === 'function' |
| 183 | + ? renderers[stack[i].type]['open']( stack[i].data ) |
| 184 | + : renderers[stack[i].type]['open']; |
| 185 | + } |
| 186 | + // Remove the annotation from the middle of the stack |
| 187 | + stack.splice( depth, 1 ); |
| 188 | + } |
| 189 | + } |
| 190 | + } |
| 191 | + return out; |
| 192 | +}; |
| 193 | + |
| 194 | +es.ContentView.prototype.getLineIndex = function( offset ) { |
| 195 | + for ( var i = 0; i < this.lines.length; i++ ) { |
| 196 | + if ( this.lines[i].range.containsOffset( offset ) ) { |
| 197 | + return i; |
| 198 | + } |
| 199 | + } |
| 200 | + return this.lines.length - 1; |
| 201 | +}; |
| 202 | + |
| 203 | +/** |
| 204 | + * Gets offset within content model closest to of a given position. |
| 205 | + * |
| 206 | + * Position is assumed to be local to the container the text is being flowed in. |
| 207 | + * |
| 208 | + * @param position {Object} Position to find offset for |
| 209 | + * @param position.left {Integer} Horizontal position in pixels |
| 210 | + * @param position.top {Integer} Vertical position in pixels |
| 211 | + * @return {Integer} Offset within content model nearest the given coordinates |
| 212 | + */ |
| 213 | +es.ContentView.prototype.getOffset = function( position ) { |
| 214 | + // Empty content model shortcut |
| 215 | + if ( this.model.getLength() === 0 ) { |
| 216 | + return 0; |
| 217 | + } |
| 218 | + /* |
| 219 | + * Line finding |
| 220 | + * |
| 221 | + * If the position is above the first line, the offset will always be 0, and if the position is |
| 222 | + * below the last line the offset will always be {this.model.length}. All other vertical |
| 223 | + * vertical positions will fall inside of one of the lines. |
| 224 | + */ |
| 225 | + var lineCount = this.lines.length; |
| 226 | + // Positions above the first line always jump to the first offset |
| 227 | + if ( !lineCount || position.top < 0 ) { |
| 228 | + return 0; |
| 229 | + } |
| 230 | + // Find which line the position is inside of |
| 231 | + var i = 0, |
| 232 | + top = 0; |
| 233 | + while ( i < lineCount ) { |
| 234 | + top += this.lines[i].height; |
| 235 | + if ( position.top <= top ) { |
| 236 | + break; |
| 237 | + } |
| 238 | + i++; |
| 239 | + } |
| 240 | + // Positions below the last line always jump to the last offset |
| 241 | + if ( i == lineCount ) { |
| 242 | + return this.model.getLength(); |
| 243 | + } |
| 244 | + // Alias current line object |
| 245 | + var line = this.lines[i]; |
| 246 | + /* |
| 247 | + * Offset finding |
| 248 | + * |
| 249 | + * Now that we know which line we are on, we can just use the "fitCharacters" method to get the |
| 250 | + * last offset before "position.left". |
| 251 | + * |
| 252 | + * TODO: The offset needs to be chosen based on nearest offset to the cursor, not offset before |
| 253 | + * the cursor. |
| 254 | + */ |
| 255 | + var $ruler = $( '<div class="editSurface-ruler"></div>' ).appendTo( this.$ ), |
| 256 | + ruler = $ruler[0], |
| 257 | + fit = this.fitCharacters( line.range, ruler, position.left ); |
| 258 | + ruler.innerHTML = this.serialize( new es.Range( line.range.start, fit.end ) ); |
| 259 | + var left = ruler.clientWidth; |
| 260 | + ruler.innerHTML = this.serialize( new es.Range( line.range.start, fit.end + 1 ) ); |
| 261 | + var right = ruler.clientWidth; |
| 262 | + var center = Math.round( left + ( ( right - left ) / 2 ) ); |
| 263 | + $ruler.remove(); |
| 264 | + // Reset RegExp object's state |
| 265 | + this.boundaryTest.lastIndex = 0; |
| 266 | + return Math.min( |
| 267 | + // If the position is right of the center of the character it's on top of, increment offset |
| 268 | + fit.end + ( position.left >= center ? 1 : 0 ), |
| 269 | + // If the line ends in a non-boundary character, decrement offset |
| 270 | + line.range.end + ( this.boundaryTest.exec( line.text.substr( -1 ) ) ? -1 : 0 ) |
| 271 | + ); |
| 272 | +}; |
| 273 | + |
| 274 | +/** |
| 275 | + * Gets position coordinates of a given offset. |
| 276 | + * |
| 277 | + * Offsets are boundaries between plain or annotated characters within content model. Results are given in |
| 278 | + * left, top and bottom positions, which could be used to draw a cursor, highlighting, etc. |
| 279 | + * |
| 280 | + * @param offset {Integer} Offset within content model |
| 281 | + * @return {Object} Object containing left, top and bottom properties, each positions in pixels as |
| 282 | + * well as a line index |
| 283 | + */ |
| 284 | +es.ContentView.prototype.getPosition = function( offset ) { |
| 285 | + /* |
| 286 | + * Range validation |
| 287 | + * |
| 288 | + * Rather than clamping the range, which can hide errors, exceptions will be thrown if offset is |
| 289 | + * less than 0 or greater than the length of the content model. |
| 290 | + */ |
| 291 | + if ( offset < 0 ) { |
| 292 | + throw 'Out of range error. Offset is expected to be greater than or equal to 0.'; |
| 293 | + } else if ( offset > this.model.getLength() ) { |
| 294 | + throw 'Out of range error. Offset is expected to be less than or equal to text length.'; |
| 295 | + } |
| 296 | + /* |
| 297 | + * Line finding |
| 298 | + * |
| 299 | + * It's possible that a more efficient method could be used here, but the number of lines to be |
| 300 | + * iterated through will rarely be over 100, so it's unlikely that any significant gains will be |
| 301 | + * had. Plus, as long as we are iterating over each line, we can also sum up the top and bottom |
| 302 | + * positions, which is a nice benefit of this method. |
| 303 | + */ |
| 304 | + var line, |
| 305 | + lineCount = this.lines.length, |
| 306 | + lineIndex = 0, |
| 307 | + position = { |
| 308 | + 'left': 0, |
| 309 | + 'top': 0, |
| 310 | + 'bottom': 0 |
| 311 | + }; |
| 312 | + while ( lineIndex < lineCount ) { |
| 313 | + line = this.lines[lineIndex]; |
| 314 | + if ( line.range.containsOffset( offset ) ) { |
| 315 | + position.bottom = position.top + line.height; |
| 316 | + break; |
| 317 | + } |
| 318 | + position.top += line.height; |
| 319 | + lineIndex++; |
| 320 | + } |
| 321 | + /* |
| 322 | + * Virtual n+1 position |
| 323 | + * |
| 324 | + * To allow access to position information of the right side of the last character on the last |
| 325 | + * line, a virtual n+1 position is supported. Offsets beyond this virtual position will cause |
| 326 | + * an exception to be thrown. |
| 327 | + */ |
| 328 | + if ( lineIndex === lineCount ) { |
| 329 | + position.bottom = position.top; |
| 330 | + position.top -= line.height; |
| 331 | + } |
| 332 | + /* |
| 333 | + * Offset measuring |
| 334 | + * |
| 335 | + * Since the left position will be zero for the first character in the line, so we can skip |
| 336 | + * measuring for those cases. |
| 337 | + */ |
| 338 | + if ( line.range.start < offset ) { |
| 339 | + var $ruler = $( '<div class="editSurface-ruler"></div>' ).appendTo( this.$ ), |
| 340 | + ruler = $ruler[0]; |
| 341 | + ruler.innerHTML = this.serialize( new es.Range( line.range.start, offset ) ); |
| 342 | + position.left = ruler.clientWidth; |
| 343 | + $ruler.remove(); |
| 344 | + } |
| 345 | + return position; |
| 346 | +}; |
| 347 | + |
| 348 | +/** |
| 349 | + * Updates the word boundary cache, which is used for word fitting. |
| 350 | + */ |
| 351 | +es.ContentView.prototype.scanBoundaries = function() { |
| 352 | + /* |
| 353 | + * Word boundary scan |
| 354 | + * |
| 355 | + * To perform binary-search on words, rather than characters, we need to collect word boundary |
| 356 | + * offsets into an array. The offset of the right side of the breaking character is stored, so |
| 357 | + * the gaps between stored offsets always include the breaking character at the end. |
| 358 | + * |
| 359 | + * To avoid encoding the same words as HTML over and over while fitting text to lines, we also |
| 360 | + * build a list of HTML escaped strings for each gap between the offsets stored in the |
| 361 | + * "boundaries" array. Slices of the "words" array can be joined, producing the escaped HTML of |
| 362 | + * the words. |
| 363 | + */ |
| 364 | + var text = this.model.getText(); |
| 365 | + // Purge "boundaries" and "words" arrays |
| 366 | + this.boundaries = [0]; |
| 367 | + // Reset RegExp object's state |
| 368 | + this.boundaryTest.lastIndex = 0; |
| 369 | + // Iterate over each word+boundary sequence, capturing offsets and encoding text as we go |
| 370 | + var match, |
| 371 | + end; |
| 372 | + while ( match = this.boundaryTest.exec( text ) ) { |
| 373 | + // Include the boundary character in the range |
| 374 | + end = match.index + 1; |
| 375 | + // Store the boundary offset |
| 376 | + this.boundaries.push( end ); |
| 377 | + } |
| 378 | + // If the last character is not a boundary character, we need to append the final range to the |
| 379 | + // "boundaries" and "words" arrays |
| 380 | + if ( end < text.length || this.boundaries.length === 1 ) { |
| 381 | + this.boundaries.push( text.length ); |
| 382 | + } |
| 383 | +}; |
| 384 | + |
| 385 | +/** |
| 386 | + * Renders a batch of lines and then yields execution before rendering another batch. |
| 387 | + * |
| 388 | + * In cases where a single word is too long to fit on a line, the word will be "virtually" wrapped, |
| 389 | + * causing them to be fragmented. Word fragments are rendered on their own lines, except for their |
| 390 | + * remainder, which is combined with whatever proceeding words can fit on the same line. |
| 391 | + */ |
| 392 | +es.ContentView.prototype.renderIteration = function( limit ) { |
| 393 | + var rs = this.renderState, |
| 394 | + iteration = 0, |
| 395 | + fractional = false, |
| 396 | + lineStart = this.boundaries[rs.wordOffset], |
| 397 | + lineEnd, |
| 398 | + wordFit = null, |
| 399 | + charOffset = 0, |
| 400 | + charFit = null, |
| 401 | + wordCount = this.boundaries.length; |
| 402 | + while ( ++iteration <= limit && rs.wordOffset < wordCount - 1 ) { |
| 403 | + wordFit = this.fitWords( new es.Range( rs.wordOffset, wordCount - 1 ), rs.ruler, rs.width ); |
| 404 | + fractional = false; |
| 405 | + if ( wordFit.width > rs.width ) { |
| 406 | + // The first word didn't fit, we need to split it up |
| 407 | + charOffset = lineStart; |
| 408 | + var lineOffset = rs.wordOffset; |
| 409 | + rs.wordOffset++; |
| 410 | + lineEnd = this.boundaries[rs.wordOffset]; |
| 411 | + do { |
| 412 | + charFit = this.fitCharacters( |
| 413 | + new es.Range( charOffset, lineEnd ), rs.ruler, rs.width |
| 414 | + ); |
| 415 | + // If we were able to get the rest of the characters on the line OK |
| 416 | + if ( charFit.end === lineEnd) { |
| 417 | + // Try to fit more words on the line |
| 418 | + wordFit = this.fitWords( |
| 419 | + new es.Range( rs.wordOffset, wordCount - 1 ), |
| 420 | + rs.ruler, |
| 421 | + rs.width - charFit.width |
| 422 | + ); |
| 423 | + if ( wordFit.end > rs.wordOffset ) { |
| 424 | + lineOffset = rs.wordOffset; |
| 425 | + rs.wordOffset = wordFit.end; |
| 426 | + charFit.end = lineEnd = this.boundaries[rs.wordOffset]; |
| 427 | + } |
| 428 | + } |
| 429 | + this.appendLine( new es.Range( charOffset, charFit.end ), lineOffset, fractional ); |
| 430 | + // Move on to another line |
| 431 | + charOffset = charFit.end; |
| 432 | + // Mark the next line as fractional |
| 433 | + fractional = true; |
| 434 | + } while ( charOffset < lineEnd ); |
| 435 | + } else { |
| 436 | + lineEnd = this.boundaries[wordFit.end]; |
| 437 | + this.appendLine( new es.Range( lineStart, lineEnd ), rs.wordOffset, fractional ); |
| 438 | + rs.wordOffset = wordFit.end; |
| 439 | + } |
| 440 | + lineStart = lineEnd; |
| 441 | + } |
| 442 | + // Only perform on actual last iteration |
| 443 | + if ( rs.wordOffset >= wordCount - 1 ) { |
| 444 | + // Cleanup |
| 445 | + rs.$ruler.remove(); |
| 446 | + this.lines = rs.lines; |
| 447 | + this.$.find( '.editSurface-line[line-index=' + ( this.lines.length - 1 ) + ']' ) |
| 448 | + .nextAll() |
| 449 | + .remove(); |
| 450 | + rs.timeout = undefined; |
| 451 | + this.emit( 'render' ); |
| 452 | + } else { |
| 453 | + rs.ruler.innerHTML = ''; |
| 454 | + var that = this; |
| 455 | + rs.timeout = setTimeout( function() { |
| 456 | + that.renderIteration( 3 ); |
| 457 | + }, 0 ); |
| 458 | + } |
| 459 | +}; |
| 460 | + |
| 461 | +/** |
| 462 | + * Renders text into a series of HTML elements, each a single line of wrapped text. |
| 463 | + * |
| 464 | + * The offset parameter can be used to reduce the amount of work involved in re-rendering the same |
| 465 | + * text, but will be automatically ignored if the text or width of the container has changed. |
| 466 | + * |
| 467 | + * Rendering happens asynchronously, and yields execution between iterations. Iterative rendering |
| 468 | + * provides the JavaScript engine an ability to process events between rendering batches of lines, |
| 469 | + * allowing rendering to be interrupted and restarted if changes to content model are happening before |
| 470 | + * rendering of all lines is complete. |
| 471 | + * |
| 472 | + * @param offset {Integer} Offset to re-render from, if possible (not yet implemented) |
| 473 | + */ |
| 474 | +es.ContentView.prototype.render = function( offset ) { |
| 475 | + var rs = this.renderState; |
| 476 | + // Check if rendering is currently underway |
| 477 | + if ( rs.timeout !== undefined ) { |
| 478 | + // Cancel the active rendering process |
| 479 | + clearTimeout( rs.timeout ); |
| 480 | + // Cleanup |
| 481 | + rs.$ruler.remove(); |
| 482 | + } |
| 483 | + // Clear caches that were specific to the previous render |
| 484 | + this.widthCache = {}; |
| 485 | + // In case of empty content model we still want to display empty with non-breaking space inside |
| 486 | + // This is very important for lists |
| 487 | + if(this.model.getLength() === 0) { |
| 488 | + var $line = $( '<div class="editSurface-line" line-index="0"> </div>' ); |
| 489 | + this.$.empty().append( $line ); |
| 490 | + this.lines = [{ |
| 491 | + 'text': ' ', |
| 492 | + 'range': new es.Range( 0,0 ), |
| 493 | + 'width': 0, |
| 494 | + 'height': $line.outerHeight(), |
| 495 | + 'wordOffset': 0, |
| 496 | + 'fractional': false |
| 497 | + }]; |
| 498 | + this.emit( 'render' ); |
| 499 | + return; |
| 500 | + } |
| 501 | + /* |
| 502 | + * Container measurement |
| 503 | + * |
| 504 | + * To get an accurate measurement of the inside of the container, without having to deal with |
| 505 | + * inconsistencies between browsers and box models, we can just create an element inside the |
| 506 | + * container and measure it. |
| 507 | + */ |
| 508 | + rs.$ruler = $( '<div> </div>' ).appendTo( this.$ ); |
| 509 | + rs.width = rs.$ruler.innerWidth(); |
| 510 | + rs.ruler = rs.$ruler.addClass('editSurface-ruler')[0]; |
| 511 | + // Ignore offset optimization if the width has changed or the text has never been flowed before |
| 512 | + if (this.width !== rs.width) { |
| 513 | + offset = undefined; |
| 514 | + } |
| 515 | + this.width = rs.width; |
| 516 | + // Reset the render state |
| 517 | + if ( offset ) { |
| 518 | + var gap, |
| 519 | + currentLine = this.lines.length - 1; |
| 520 | + for ( var i = this.lines.length - 1; i >= 0; i-- ) { |
| 521 | + var line = this.lines[i]; |
| 522 | + if ( line.range.start < offset && line.range.end > offset ) { |
| 523 | + currentLine = i; |
| 524 | + } |
| 525 | + if ( ( line.range.end < offset && !line.fractional ) || i === 0 ) { |
| 526 | + rs.lines = this.lines.slice( 0, i ); |
| 527 | + rs.wordOffset = line.wordOffset; |
| 528 | + gap = currentLine - i; |
| 529 | + break; |
| 530 | + } |
| 531 | + } |
| 532 | + this.renderIteration( 2 + gap ); |
| 533 | + } else { |
| 534 | + rs.lines = []; |
| 535 | + rs.wordOffset = 0; |
| 536 | + this.renderIteration( 3 ); |
| 537 | + } |
| 538 | +}; |
| 539 | + |
| 540 | +/** |
| 541 | + * Adds a line containing a given range of text to the end of the DOM and the "lines" array. |
| 542 | + * |
| 543 | + * @param range {es.Range} Range of data within content model to append |
| 544 | + * @param start {Integer} Beginning of text range for line |
| 545 | + * @param end {Integer} Ending of text range for line |
| 546 | + * @param wordOffset {Integer} Index within this.words which the line begins with |
| 547 | + * @param fractional {Boolean} If the line begins in the middle of a word |
| 548 | + */ |
| 549 | +es.ContentView.prototype.appendLine = function( range, wordOffset, fractional ) { |
| 550 | + var rs = this.renderState, |
| 551 | + lineCount = rs.lines.length; |
| 552 | + $line = this.$.children( '[line-index=' + lineCount + ']' ); |
| 553 | + if ( !$line.length ) { |
| 554 | + $line = $( '<div class="editSurface-line" line-index="' + lineCount + '"></div>' ); |
| 555 | + this.$.append( $line ); |
| 556 | + } |
| 557 | + $line[0].innerHTML = this.serialize( range ); |
| 558 | + // Collect line information |
| 559 | + rs.lines.push({ |
| 560 | + 'text': this.model.getText( range ), |
| 561 | + 'range': range, |
| 562 | + 'width': $line.outerWidth(), |
| 563 | + 'height': $line.outerHeight(), |
| 564 | + 'wordOffset': wordOffset, |
| 565 | + 'fractional': fractional |
| 566 | + }); |
| 567 | + // Disable links within rendered content |
| 568 | + $line.find( '.editSurface-format-object a' ) |
| 569 | + .mousedown( function( e ) { |
| 570 | + e.preventDefault(); |
| 571 | + } ) |
| 572 | + .click( function( e ) { |
| 573 | + e.preventDefault(); |
| 574 | + } ); |
| 575 | +}; |
| 576 | + |
| 577 | +/** |
| 578 | + * Gets the index of the boundary of last word that fits inside the line |
| 579 | + * |
| 580 | + * The "words" and "boundaries" arrays provide linear access to the offsets around non-breakable |
| 581 | + * areas within the text. Using these, we can perform a binary-search for the best fit of words |
| 582 | + * within a line, just as we would with characters. |
| 583 | + * |
| 584 | + * Results are given as an object containing both an index and a width, the later of which can be |
| 585 | + * used to detect when the first word was too long to fit on a line. In such cases the result will |
| 586 | + * contain the index of the boundary of the first word and it's width. |
| 587 | + * |
| 588 | + * TODO: Because limit is most likely given as "words.length", it may be possible to improve the |
| 589 | + * efficiency of this code by making a best guess and working from there, rather than always |
| 590 | + * starting with [offset .. limit], which usually results in reducing the end position in all but |
| 591 | + * the last line, and in most cases more than 3 times, before changing directions. |
| 592 | + * |
| 593 | + * @param range {es.Range} Range of data within content model to try to fit |
| 594 | + * @param ruler {HTMLElement} Element to take measurements with |
| 595 | + * @param width {Integer} Maximum width to allow the line to extend to |
| 596 | + * @return {Integer} Last index within "words" that contains a word that fits |
| 597 | + */ |
| 598 | +es.ContentView.prototype.fitWords = function( range, ruler, width ) { |
| 599 | + var offset = range.start, |
| 600 | + start = range.start, |
| 601 | + end = range.end, |
| 602 | + charOffset = this.boundaries[offset], |
| 603 | + middle, |
| 604 | + lineWidth, |
| 605 | + cacheKey; |
| 606 | + do { |
| 607 | + // Place "middle" directly in the center of "start" and "end" |
| 608 | + middle = Math.ceil( ( start + end ) / 2 ); |
| 609 | + charMiddle = this.boundaries[middle]; |
| 610 | + // Measure and cache width of substring |
| 611 | + cacheKey = charOffset + ':' + charMiddle; |
| 612 | + // Prepare the line for measurement using pre-escaped HTML |
| 613 | + ruler.innerHTML = this.serialize( new es.Range( charOffset, charMiddle ) ); |
| 614 | + // Test for over/under using width of the rendered line |
| 615 | + this.widthCache[cacheKey] = lineWidth = ruler.clientWidth; |
| 616 | + // Test for over/under using width of the rendered line |
| 617 | + if ( lineWidth > width ) { |
| 618 | + // Detect impossible fit (the first word won't fit by itself) |
| 619 | + if (middle - offset === 1) { |
| 620 | + start = middle; |
| 621 | + break; |
| 622 | + } |
| 623 | + // Words after "middle" won't fit |
| 624 | + end = middle - 1; |
| 625 | + } else { |
| 626 | + // Words before "middle" will fit |
| 627 | + start = middle; |
| 628 | + } |
| 629 | + } while ( start < end ); |
| 630 | + // Check if we ended by moving end to the left of middle |
| 631 | + if ( end === middle - 1 ) { |
| 632 | + // A final measurement is required |
| 633 | + var charStart = this.boundaries[start]; |
| 634 | + ruler.innerHTML = this.serialize( new es.Range( charOffset, charStart ) ); |
| 635 | + lineWidth = this.widthCache[charOffset + ':' + charStart] = ruler.clientWidth; |
| 636 | + } |
| 637 | + return { 'end': start, 'width': lineWidth }; |
| 638 | +}; |
| 639 | + |
| 640 | +/** |
| 641 | + * Gets the index of the boundary of the last character that fits inside the line |
| 642 | + * |
| 643 | + * Results are given as an object containing both an index and a width, the later of which can be |
| 644 | + * used to detect when the first character was too long to fit on a line. In such cases the result |
| 645 | + * will contain the index of the first character and it's width. |
| 646 | + * |
| 647 | + * @param range {es.Range} Range of data within content model to try to fit |
| 648 | + * @param ruler {HTMLElement} Element to take measurements with |
| 649 | + * @param width {Integer} Maximum width to allow the line to extend to |
| 650 | + * @return {Integer} Last index within "text" that contains a character that fits |
| 651 | + */ |
| 652 | +es.ContentView.prototype.fitCharacters = function( range, ruler, width ) { |
| 653 | + var offset = range.start, |
| 654 | + start = range.start, |
| 655 | + end = range.end, |
| 656 | + middle, |
| 657 | + lineWidth, |
| 658 | + cacheKey; |
| 659 | + do { |
| 660 | + // Place "middle" directly in the center of "start" and "end" |
| 661 | + middle = Math.ceil( ( start + end ) / 2 ); |
| 662 | + // Measure and cache width of substring |
| 663 | + cacheKey = offset + ':' + middle; |
| 664 | + if ( cacheKey in this.widthCache ) { |
| 665 | + lineWidth = this.widthCache[cacheKey]; |
| 666 | + } else { |
| 667 | + // Fill the line with a portion of the text, escaped as HTML |
| 668 | + ruler.innerHTML = this.serialize( new es.Range( offset, middle ) ); |
| 669 | + // Test for over/under using width of the rendered line |
| 670 | + this.widthCache[cacheKey] = lineWidth = ruler.clientWidth; |
| 671 | + } |
| 672 | + if ( lineWidth > width ) { |
| 673 | + // Detect impossible fit (the first character won't fit by itself) |
| 674 | + if (middle - offset === 1) { |
| 675 | + start = middle - 1; |
| 676 | + break; |
| 677 | + } |
| 678 | + // Words after "middle" won't fit |
| 679 | + end = middle - 1; |
| 680 | + } else { |
| 681 | + // Words before "middle" will fit |
| 682 | + start = middle; |
| 683 | + } |
| 684 | + } while ( start < end ); |
| 685 | + // Check if we ended by moving end to the left of middle |
| 686 | + if ( end === middle - 1 ) { |
| 687 | + // Try for cache hit |
| 688 | + cacheKey = offset + ':' + start; |
| 689 | + if ( cacheKey in this.widthCache ) { |
| 690 | + lineWidth = this.widthCache[cacheKey]; |
| 691 | + } else { |
| 692 | + // A final measurement is required |
| 693 | + ruler.innerHTML = this.serialize( new es.Range( offset, start ) ); |
| 694 | + lineWidth = this.widthCache[cacheKey] = ruler.clientWidth; |
| 695 | + } |
| 696 | + } |
| 697 | + return { 'end': start, 'width': lineWidth }; |
| 698 | +}; |
| 699 | + |
| 700 | +/** |
| 701 | + * Gets an HTML serialization of a range of data within content model. |
| 702 | + * |
| 703 | + * @method |
| 704 | + * @param start {Integer} Beginning of range |
| 705 | + * @param end {Integer} End of range |
| 706 | + * @param {String} Rendered HTML of data within content model |
| 707 | + */ |
| 708 | +es.ContentView.prototype.serialize = function( range ) { |
| 709 | + var data = this.model.getData( range ), |
| 710 | + render = es.ContentView.renderAnnotation, |
| 711 | + htmlChars = es.ContentView.htmlCharacters; |
| 712 | + var out = '', |
| 713 | + left = '', |
| 714 | + right, |
| 715 | + leftPlain, |
| 716 | + rightPlain, |
| 717 | + stack = []; |
| 718 | + for ( var i = 0; i < data.length; i++ ) { |
| 719 | + right = data[i]; |
| 720 | + leftPlain = typeof left === 'string'; |
| 721 | + rightPlain = typeof right === 'string'; |
| 722 | + if ( !leftPlain && rightPlain ) { |
| 723 | + // [formatted][plain] pair, close any annotations for left |
| 724 | + for ( var j = 1; j < left.length; j++ ) { |
| 725 | + out += render( 'close', left[j], stack ); |
| 726 | + } |
| 727 | + } else if ( leftPlain && !rightPlain ) { |
| 728 | + // [plain][formatted] pair, open any annotations for right |
| 729 | + for ( var j = 1; j < right.length; j++ ) { |
| 730 | + out += render( 'open', right[j], stack ); |
| 731 | + } |
| 732 | + } else if ( !leftPlain && !rightPlain ) { |
| 733 | + // [formatted][formatted] pair, open/close any differences |
| 734 | + for ( var j = 1; j < left.length; j++ ) { |
| 735 | + if ( right.indexOf( left[j] ) === -1 ) { |
| 736 | + out += render( 'close', left[j], stack ); |
| 737 | + } |
| 738 | + } |
| 739 | + for ( var j = 1; j < right.length; j++ ) { |
| 740 | + if ( left.indexOf( right[j] ) === -1 ) { |
| 741 | + out += render( 'open', right[j], stack ); |
| 742 | + } |
| 743 | + } |
| 744 | + } |
| 745 | + out += right[0] in htmlChars ? htmlChars[right[0]] : right[0]; |
| 746 | + left = right; |
| 747 | + } |
| 748 | + // Close all remaining tags at the end of the content |
| 749 | + if ( !rightPlain && right ) { |
| 750 | + for ( var j = 1; j < right.length; j++ ) { |
| 751 | + out += render( 'close', right[j], stack ); |
| 752 | + } |
| 753 | + } |
| 754 | + return out; |
| 755 | +}; |
| 756 | + |
| 757 | +es.extend( es.ContentView, es.EventEmitter ); |
Property changes on: trunk/parsers/wikidom/lib/synth/views/es.ContentView.js |
___________________________________________________________________ |
Added: svn:mime-type |
1 | 758 | + text/plain |
Added: svn:eol-style |
2 | 759 | + native |
Index: trunk/parsers/wikidom/lib/synth/views/es.DocumentView.js |
— | — | @@ -0,0 +1,7 @@ |
| 2 | +es.DocumentView = function( blockViews ) { |
| 3 | + es.DomContainer.call( this, 'blocks' ); |
| 4 | +}; |
| 5 | + |
| 6 | +/* Inheritance */ |
| 7 | + |
| 8 | +es.extend( es.DocumentView, es.DomContainer ); |
Property changes on: trunk/parsers/wikidom/lib/synth/views/es.DocumentView.js |
___________________________________________________________________ |
Added: svn:mime-type |
1 | 9 | + text/plain |
Added: svn:eol-style |
2 | 10 | + native |