r95828 MediaWiki - Code Review archive

Repository:MediaWiki
Revision:r95827‎ | r95828 | r95829 >
Date:00:58, 31 August 2011
Author:tparscal
Status:deferred
Tags:
Comment:
Added unfinished refactoring of es
Modified paths:
  • /trunk/parsers/wikidom/lib/synth (added) (history)
  • /trunk/parsers/wikidom/lib/synth/bases (added) (history)
  • /trunk/parsers/wikidom/lib/synth/bases/es.Container.js (added) (history)
  • /trunk/parsers/wikidom/lib/synth/bases/es.ContainerItem.js (added) (history)
  • /trunk/parsers/wikidom/lib/synth/bases/es.ContentSeries.js (added) (history)
  • /trunk/parsers/wikidom/lib/synth/bases/es.DomContainer.js (added) (history)
  • /trunk/parsers/wikidom/lib/synth/bases/es.DomContainerItem.js (added) (history)
  • /trunk/parsers/wikidom/lib/synth/bases/es.EventEmitter.js (added) (history)
  • /trunk/parsers/wikidom/lib/synth/controllers (added) (history)
  • /trunk/parsers/wikidom/lib/synth/controllers/es.BlockController.js (added) (history)
  • /trunk/parsers/wikidom/lib/synth/controllers/es.DocumentController.js (added) (history)
  • /trunk/parsers/wikidom/lib/synth/es.js (added) (history)
  • /trunk/parsers/wikidom/lib/synth/models (added) (history)
  • /trunk/parsers/wikidom/lib/synth/models/es.BlockModel.js (added) (history)
  • /trunk/parsers/wikidom/lib/synth/models/es.CommentBlockModel.js (added) (history)
  • /trunk/parsers/wikidom/lib/synth/models/es.ContentModel.js (added) (history)
  • /trunk/parsers/wikidom/lib/synth/models/es.DocumentModel.js (added) (history)
  • /trunk/parsers/wikidom/lib/synth/models/es.HeadingBlockModel.js (added) (history)
  • /trunk/parsers/wikidom/lib/synth/models/es.HorizontalRuleBlockModel.js (added) (history)
  • /trunk/parsers/wikidom/lib/synth/models/es.ListBlockItemModel.js (added) (history)
  • /trunk/parsers/wikidom/lib/synth/models/es.ListBlockModel.js (added) (history)
  • /trunk/parsers/wikidom/lib/synth/models/es.ParagraphBlockModel.js (added) (history)
  • /trunk/parsers/wikidom/lib/synth/models/es.TableBlockModel.js (added) (history)
  • /trunk/parsers/wikidom/lib/synth/models/es.TableBlockRowModel.js (added) (history)
  • /trunk/parsers/wikidom/lib/synth/views (added) (history)
  • /trunk/parsers/wikidom/lib/synth/views/es.BlockView.js (added) (history)
  • /trunk/parsers/wikidom/lib/synth/views/es.ContentView.js (added) (history)
  • /trunk/parsers/wikidom/lib/synth/views/es.DocumentView.js (added) (history)
  • /trunk/parsers/wikidom/tests/synth (added) (history)
  • /trunk/parsers/wikidom/tests/synth/index.html (added) (history)
  • /trunk/parsers/wikidom/tests/synth/test.js (added) (history)

Diff [purge]

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
123 + 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
1160 + text/plain
Added: svn:eol-style
2161 + 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
198 + text/plain
Added: svn:eol-style
299 + 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
156 + text/plain
Added: svn:eol-style
257 + 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
167 + text/plain
Added: svn:eol-style
268 + 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
156 + text/plain
Added: svn:eol-style
257 + 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
175 + text/plain
Added: svn:eol-style
276 + 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
148 + text/plain
Added: svn:eol-style
249 + 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
1108 + text/plain
Added: svn:eol-style
2109 + 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
1172 + text/plain
Added: svn:eol-style
2173 + 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
1576 + text/plain
Added: svn:eol-style
2577 + 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
159 + text/plain
Added: svn:eol-style
260 + 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
165 + text/plain
Added: svn:eol-style
266 + 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
153 + text/plain
Added: svn:eol-style
254 + 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
125 + text/plain
Added: svn:eol-style
226 + 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
149 + text/plain
Added: svn:eol-style
250 + 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
1114 + text/plain
Added: svn:eol-style
2115 + 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
127 + text/plain
Added: svn:eol-style
228 + 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
1251 + text/plain
Added: svn:eol-style
2252 + 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
151 + text/plain
Added: svn:eol-style
252 + 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
186 + text/plain
Added: svn:eol-style
287 + 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
1139 + text/plain
Added: svn:eol-style
2140 + 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
145 + text/plain
Added: svn:eol-style
246 + 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+ '&': '&amp;',
 120+ '<': '&lt;',
 121+ '>': '&gt;',
 122+ '\'': '&#039;',
 123+ '"': '&quot;',
 124+ ' ': '&nbsp;',
 125+ '\n': '<span class="editSurface-whitespace">&#182;</span>',
 126+ '\t': '<span class="editSurface-whitespace">&#8702;</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">&nbsp;</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>&nbsp;</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
1758 + text/plain
Added: svn:eol-style
2759 + 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
19 + text/plain
Added: svn:eol-style
210 + native

Status & tagging log