r103063 MediaWiki - Code Review archive

Repository:MediaWiki
Revision:r103062‎ | r103063 | r103064 >
Date:23:04, 14 November 2011
Author:tparscal
Status:deferred
Tags:
Comment:
Moved transaction processing code to new class, es.TransactionProcessor
Modified paths:
  • /trunk/extensions/VisualEditor/demo/index.html (modified) (history)
  • /trunk/extensions/VisualEditor/modules/es/bases/es.DocumentBranchNode.js (modified) (history)
  • /trunk/extensions/VisualEditor/modules/es/es.TransactionProcessor.js (added) (history)
  • /trunk/extensions/VisualEditor/modules/es/models/es.DocumentModel.js (modified) (history)
  • /trunk/extensions/VisualEditor/tests/es/index.html (modified) (history)

Diff [purge]

Index: trunk/extensions/VisualEditor/tests/es/index.html
@@ -19,6 +19,7 @@
2020 <script src="../../modules/es/es.js"></script>
2121 <script src="../../modules/es/es.Range.js"></script>
2222 <script src="../../modules/es/es.Transaction.js"></script>
 23+ <script src="../../modules/es/es.TransactionProcessor.js"></script>
2324
2425 <!-- Bases -->
2526 <script src="../../modules/es/bases/es.EventEmitter.js"></script>
Index: trunk/extensions/VisualEditor/demo/index.html
@@ -59,6 +59,7 @@
6060 <script src="../modules/es/es.Position.js"></script>
6161 <script src="../modules/es/es.Range.js"></script>
6262 <script src="../modules/es/es.Transaction.js"></script>
 63+ <script src="../modules/es/es.TransactionProcessor.js"></script>
6364
6465 <!-- Serializers -->
6566 <script src="../modules/es/serializers/es.AnnotationSerializer.js"></script>
Index: trunk/extensions/VisualEditor/modules/es/models/es.DocumentModel.js
@@ -59,271 +59,6 @@
6060 */
6161 es.DocumentModel.nodeRules = {};
6262
63 -/**
64 - * Mapping of operation types to pure functions.
65 - *
66 - * Each function is called in the context of a state, and takes an operation object as a parameter.
67 - */
68 -es.DocumentModel.operations = ( function() {
69 -
70 - // Pure functions
71 -
72 - function rebuild( newData, oldNodes ) {
73 - var parent = oldNodes[0].getParent(),
74 - index = parent.indexOf( oldNodes[0] );
75 - // Remove the node we are about to insert into from the model tree
76 - parent.splice( index, oldNodes.length );
77 - // Regenerate nodes for the data we've affected
78 - var newNodes = es.DocumentModel.createNodesFromData( newData );
79 - // Insert new elements into the tree where the old ones used to be
80 - parent.splice.apply( parent, [index, 0].concat( newNodes ) );
81 - }
82 -
83 - function scope( node, data ) {
84 - var i,
85 - length,
86 - level = 0,
87 - maxDepth = 0;
88 - for ( i = 0, length = data.length; i < length; i++ ) {
89 - if ( typeof data[i].type === 'string' ) {
90 - level += data[i].type.charAt( 0 ) === '/' ? -1 : 1;
91 - maxDepth = Math.max( maxDepth, -level );
92 - }
93 - }
94 - if ( maxDepth > 0 ) {
95 - for ( i = 0; i < maxDepth; i++ ) {
96 - node = node.getParent();
97 - }
98 - }
99 - return node;
100 - }
101 -
102 - // Methods (call in context of state)
103 -
104 - function retain( op ) {
105 - annotate.call( this, this.cursor + op.length );
106 - this.cursor += op.length;
107 - }
108 -
109 - function insert( op ) {
110 - if ( es.DocumentModel.isStructuralOffset( this.data, this.cursor ) ) {
111 - // TODO: Support tree updates when inserting between elements
112 - } else {
113 - // Get the node we are about to insert into at the lowest depth possible
114 - var node = scope( this.tree.getNodeFromOffset( this.cursor ), op.data );
115 - if ( !node ) {
116 - throw 'Missing node error. Scope could not be resolved';
117 - }
118 - // Figure out how deep the data goes
119 -
120 - var offset = this.tree.getOffsetFromNode( node );
121 - if ( es.DocumentModel.containsElementData( op.data ) ) {
122 - // Perform insert on linear data model
123 - es.insertIntoArray( this.data, this.cursor, op.data );
124 - annotate.call( this, this.cursor + op.data.length );
125 - // Synchronize model tree
126 - if ( offset === -1 ) {
127 - throw 'Invalid offset error. Node is not in model tree';
128 - }
129 - rebuild(
130 - this.data.slice( offset, offset + node.getElementLength() + op.data.length ),
131 - [node]
132 - );
133 - } else {
134 - // Perform insert on linear data model
135 - // TODO this is duplicated from above
136 - es.insertIntoArray( this.data, this.cursor, op.data );
137 - annotate.call( this, this.cursor + op.data.length );
138 - // Update model tree
139 - node.adjustContentLength( op.data.length, true );
140 - node.emit( 'update', this.cursor - offset );
141 - }
142 - }
143 - this.cursor += op.data.length;
144 - }
145 -
146 - function remove( op ) {
147 - if ( es.DocumentModel.containsElementData( op.data ) ) {
148 - // Figure out which nodes are covered by the removal
149 - var ranges = this.tree.selectNodes( new es.Range( this.cursor, this.cursor + op.data.length ) );
150 - var oldNodes = [], newData = [], firstKeptNode = true, lastElement;
151 - for ( var i = 0; i < ranges.length; i++ ) {
152 - oldNodes.push( ranges[i].node );
153 - if ( ranges[i].globalRange !== undefined ) {
154 - // We have to keep part of this node
155 - if ( firstKeptNode ) {
156 - // This is the first node we're keeping
157 - // Keep its opening as well
158 - newData.push( ranges[i].node.getElement() );
159 - firstKeptNode = false;
160 - }
161 -
162 - // Compute the start and end offset of this node
163 - // We could do that with getOffsetFromNode() but
164 - // we already have all the numbers we need so why would we
165 - var startOffset = ranges[i].globalRange.start - ranges[i].range.start,
166 - endOffset = startOffset + ranges[i].node.getContentLength(),
167 - // Get this node's data
168 - nodeData = this.data.slice( startOffset, endOffset );
169 - // Remove data covered by the range from nodeData
170 - nodeData.splice( ranges[i].range.start, ranges[i].range.end - ranges[i].range.start );
171 - // What remains in nodeData is the data we need to keep
172 - // Append it to newData
173 - newData = newData.concat( nodeData );
174 -
175 - lastElement = ranges[i].node.getElementType();
176 - }
177 - }
178 - if ( lastElement !== undefined ) {
179 - // Keep the closing of the last element that was partially kept
180 - newData.push( { 'type': '/' + lastElement } );
181 - }
182 -
183 - // Perform the rebuild. This updates the model tree
184 - rebuild( newData, oldNodes );
185 - } else {
186 - // We're removing content only. Take a shortcut
187 - // Get the node we are removing content from
188 - var node = this.tree.getNodeFromOffset( this.cursor );
189 - // Update model tree
190 - node.adjustContentLength( -op.data.length, true );
191 - node.emit( 'update', this.cursor - this.tree.getOffsetFromNode( node ) );
192 - }
193 -
194 - // Update the linear model
195 - this.data.splice( this.cursor, op.data.length );
196 - }
197 -
198 - function attribute( op, invert ) {
199 - var element = this.data[this.cursor];
200 - if ( element.type === undefined ) {
201 - throw 'Invalid element error. Can not set attributes on non-element data.';
202 - }
203 - if ( ( op.method === 'set' && !invert ) || ( op.method === 'clear' && invert ) ) {
204 - // Automatically initialize attributes object
205 - if ( !element.attributes ) {
206 - element.attributes = {};
207 - }
208 - element.attributes[op.key] = op.value;
209 - } else if ( ( op.method === 'clear' && !invert ) || ( op.method === 'set' && invert ) ) {
210 - if ( element.attributes ) {
211 - delete element.attributes[op.key];
212 - }
213 - // Automatically clean up attributes object
214 - var empty = true;
215 - for ( var key in element.attributes ) {
216 - empty = false;
217 - break;
218 - }
219 - if ( empty ) {
220 - delete element.attributes;
221 - }
222 - } else {
223 - throw 'Invalid method error. Can not operate attributes this way: ' + method;
224 - }
225 - }
226 -
227 - function annotate( to ) {
228 - var i,
229 - j,
230 - length,
231 - annotation;
232 - // Handle annotations
233 - if ( this.set.length ) {
234 - for ( i = 0, length = this.set.length; i < length; i++ ) {
235 - annotation = this.set[i];
236 - // Auto-build annotation hash
237 - if ( annotation.hash === undefined ) {
238 - annotation.hash = es.DocumentModel.getAnnotationHash( annotation );
239 - }
240 - for ( j = this.cursor; j < to; j++ ) {
241 - // Auto-convert to array
242 - if ( es.isArray( this.data[j] ) ) {
243 - this.data[j].push( annotation );
244 - } else {
245 - this.data[j] = [this.data[j], annotation];
246 - }
247 - }
248 - }
249 - }
250 - if ( this.clear.length ) {
251 - for ( i = 0, length = this.clear.length; i < length; i++ ) {
252 - annotation = this.clear[i];
253 - // Auto-build annotation hash
254 - if ( annotation.hash === undefined ) {
255 - annotation.hash = es.DocumentModel.getAnnotationHash( annotation );
256 - }
257 - for ( j = this.cursor; j < to; j++ ) {
258 - var index = es.DocumentModel.getIndexOfAnnotation( this.data[j], annotation );
259 - if ( index !== -1 ) {
260 - this.data[j].splice( index, 1 );
261 - }
262 - // Auto-convert to string
263 - if ( this.data[j].length === 1 ) {
264 - this.data[j] = this.data[j][0];
265 - }
266 - }
267 - }
268 - }
269 - }
270 -
271 - function mark( op, invert ) {
272 - var target;
273 - if ( ( op.method === 'set' && !invert ) || ( op.method === 'clear' && invert ) ) {
274 - target = this.set;
275 - } else if ( ( op.method === 'clear' && !invert ) || ( op.method === 'set' && invert ) ) {
276 - target = this.clear;
277 - } else {
278 - throw 'Invalid method error. Can not operate attributes this way: ' + method;
279 - }
280 - if ( op.bias === 'start' ) {
281 - target.push( op.annotation );
282 - } else if ( op.bias === 'stop' ) {
283 - var index = es.DocumentModel.getIndexOfAnnotation( target, op.annotation );
284 - if ( index === -1 ) {
285 - throw 'Annotation stack error. Annotation is missing.';
286 - }
287 - target.splice( index, 1 );
288 - }
289 - }
290 -
291 - return {
292 - // Retain
293 - 'retain': {
294 - 'commit': retain,
295 - 'rollback': retain
296 - },
297 - // Insert
298 - 'insert': {
299 - 'commit': insert,
300 - 'rollback': remove
301 - },
302 - // Remove
303 - 'remove': {
304 - 'commit': remove,
305 - 'rollback': insert
306 - },
307 - // Change element attributes
308 - 'attribute': {
309 - 'commit': function( op ) {
310 - attribute.call( this, op, false );
311 - },
312 - 'rollback': function( op ) {
313 - attribute.call( this, op, true );
314 - }
315 - },
316 - // Change content annotations
317 - 'annotate': {
318 - 'commit': function( op ) {
319 - mark.call( this, op, false );
320 - },
321 - 'rollback': function( op ) {
322 - mark.call( this, op, true );
323 - }
324 - }
325 - };
326 -} )();
327 -
32863 /* Static Methods */
32964
33065 /*
@@ -1276,22 +1011,7 @@
12771012 * @param {es.Transaction}
12781013 */
12791014 es.DocumentModel.prototype.commit = function( transaction ) {
1280 - var state = {
1281 - 'data': this.data,
1282 - 'tree': this,
1283 - 'cursor': 0,
1284 - 'set': [],
1285 - 'clear': []
1286 - },
1287 - operations = transaction.getOperations();
1288 - for ( var i = 0, length = operations.length; i < length; i++ ) {
1289 - var operation = operations[i];
1290 - if ( operation.type in es.DocumentModel.operations ) {
1291 - es.DocumentModel.operations[operation.type].commit.call( state, operation );
1292 - } else {
1293 - throw 'Invalid operation error. Operation type is not supported: ' + operation.type;
1294 - }
1295 - }
 1015+ es.TransactionProcessor.commit( this, transaction );
12961016 };
12971017
12981018 /**
@@ -1301,22 +1021,7 @@
13021022 * @param {es.Transaction}
13031023 */
13041024 es.DocumentModel.prototype.rollback = function( transaction ) {
1305 - var state = {
1306 - 'data': this.data,
1307 - 'tree': this,
1308 - 'cursor': 0,
1309 - 'set': [],
1310 - 'clear': []
1311 - },
1312 - operations = transaction.getOperations();
1313 - for ( var i = 0, length = operations.length; i < length; i++ ) {
1314 - var operation = operations[i];
1315 - if ( operation.type in es.DocumentModel.operations ) {
1316 - es.DocumentModel.operations[operation.type].rollback.call( state, operation );
1317 - } else {
1318 - throw 'Invalid operation error. Operation type is not supported: ' + operation.type;
1319 - }
1320 - }
 1025+ es.TransactionProcessor.rollback( this, transaction );
13211026 };
13221027
13231028 /* Inheritance */
Index: trunk/extensions/VisualEditor/modules/es/es.TransactionProcessor.js
@@ -0,0 +1,314 @@
 2+/**
 3+ * Creates an es.TransactionProcessor object.
 4+ *
 5+ * @class
 6+ * @constructor
 7+ */
 8+es.TransactionProcessor = function( model, transaction ) {
 9+ this.model = model;
 10+ this.transaction = transaction;
 11+ this.cursor = 0;
 12+ this.set = [];
 13+ this.clear = [];
 14+};
 15+
 16+/* Static Members */
 17+
 18+es.TransactionProcessor.operationMap = {
 19+ // Retain
 20+ 'retain': {
 21+ 'commit': function( op ) {
 22+ this.retain( op );
 23+ },
 24+ 'rollback': function( op ) {
 25+ this.retain( op );
 26+ }
 27+ },
 28+ // Insert
 29+ 'insert': {
 30+ 'commit': function( op ) {
 31+ this.insert( op );
 32+ },
 33+ 'rollback': function( op ) {
 34+ this.remove( op );
 35+ }
 36+ },
 37+ // Remove
 38+ 'remove': {
 39+ 'commit': function( op ) {
 40+ this.remove( op );
 41+ },
 42+ 'rollback': function( op ) {
 43+ this.insert( op );
 44+ }
 45+ },
 46+ // Change element attributes
 47+ 'attribute': {
 48+ 'commit': function( op ) {
 49+ this.attribute( op, false );
 50+ },
 51+ 'rollback': function( op ) {
 52+ this.attribute( op, true );
 53+ }
 54+ },
 55+ // Change content annotations
 56+ 'annotate': {
 57+ 'commit': function( op ) {
 58+ this.mark( op, false );
 59+ },
 60+ 'rollback': function( op ) {
 61+ this.mark( op, true );
 62+ }
 63+ }
 64+};
 65+
 66+/* Static Methods */
 67+
 68+es.TransactionProcessor.commit = function( doc, transaction ) {
 69+ var tp = new es.TransactionProcessor( doc, transaction );
 70+ tp.process( 'commit' );
 71+};
 72+
 73+es.TransactionProcessor.rollback = function( doc, transaction ) {
 74+ var tp = new es.TransactionProcessor( doc, transaction );
 75+ tp.process( 'rollback' );
 76+};
 77+
 78+/* Methods */
 79+
 80+es.TransactionProcessor.prototype.process = function( method ) {
 81+ var operations = this.transaction.getOperations();
 82+ for ( var i = 0, length = operations.length; i < length; i++ ) {
 83+ var operation = operations[i];
 84+ if ( operation.type in es.TransactionProcessor.operationMap ) {
 85+ es.TransactionProcessor.operationMap[operation.type][method].call( this, operation );
 86+ } else {
 87+ throw 'Invalid operation error. Operation type is not supported: ' + operation.type;
 88+ }
 89+ }
 90+};
 91+
 92+es.TransactionProcessor.prototype.rebuildNodes = function( newData, oldNodes ) {
 93+ var parent,
 94+ index;
 95+ if ( oldNodes[0] === oldNodes[0].getRoot() ) {
 96+ parent = oldNodes[0];
 97+ parent.splice( 0, parent.getChildren().length );
 98+ index = 0;
 99+ } else {
 100+ parent = oldNodes[0].getParent();
 101+ index = parent.indexOf( oldNodes[0] );
 102+ // Remove the node we are about to insert into from the model tree
 103+ parent.splice( index, oldNodes.length );
 104+ }
 105+ // Regenerate nodes for the data we've affected
 106+ var newNodes = es.DocumentModel.createNodesFromData( newData );
 107+ // Insert new elements into the tree where the old ones used to be
 108+ parent.splice.apply( parent, [index, 0].concat( newNodes ) );
 109+};
 110+
 111+es.TransactionProcessor.prototype.getScope = function( node, data ) {
 112+ var i,
 113+ length,
 114+ level = 0,
 115+ maxDepth = 0;
 116+ for ( i = 0, length = data.length; i < length; i++ ) {
 117+ if ( typeof data[i].type === 'string' ) {
 118+ level += data[i].type.charAt( 0 ) === '/' ? -1 : 1;
 119+ maxDepth = Math.max( maxDepth, -level );
 120+ }
 121+ }
 122+ if ( maxDepth > 0 ) {
 123+ for ( i = 0; i < maxDepth; i++ ) {
 124+ node = node.getParent();
 125+ }
 126+ }
 127+ return node;
 128+};
 129+
 130+es.TransactionProcessor.prototype.applyAnnotations = function( to ) {
 131+ var i,
 132+ j,
 133+ length,
 134+ annotation;
 135+ // Handle annotations
 136+ if ( this.set.length ) {
 137+ for ( i = 0, length = this.set.length; i < length; i++ ) {
 138+ annotation = this.set[i];
 139+ // Auto-build annotation hash
 140+ if ( annotation.hash === undefined ) {
 141+ annotation.hash = es.DocumentModel.getAnnotationHash( annotation );
 142+ }
 143+ for ( j = this.cursor; j < to; j++ ) {
 144+ // Auto-convert to array
 145+ if ( es.isArray( this.model.data[j] ) ) {
 146+ this.model.data[j].push( annotation );
 147+ } else {
 148+ this.model.data[j] = [this.model.data[j], annotation];
 149+ }
 150+ }
 151+ }
 152+ }
 153+ if ( this.clear.length ) {
 154+ for ( i = 0, length = this.clear.length; i < length; i++ ) {
 155+ annotation = this.clear[i];
 156+ // Auto-build annotation hash
 157+ if ( annotation.hash === undefined ) {
 158+ annotation.hash = es.DocumentModel.getAnnotationHash( annotation );
 159+ }
 160+ for ( j = this.cursor; j < to; j++ ) {
 161+ var index = es.DocumentModel.getIndexOfAnnotation( this.model.data[j], annotation );
 162+ if ( index !== -1 ) {
 163+ this.model.data[j].splice( index, 1 );
 164+ }
 165+ // Auto-convert to string
 166+ if ( this.model.data[j].length === 1 ) {
 167+ this.model.data[j] = this.model.data[j][0];
 168+ }
 169+ }
 170+ }
 171+ }
 172+};
 173+
 174+es.TransactionProcessor.prototype.retain = function( op ) {
 175+ this.applyAnnotations( this.cursor + op.length );
 176+ this.cursor += op.length;
 177+};
 178+
 179+es.TransactionProcessor.prototype.insert = function( op ) {
 180+ if ( es.DocumentModel.isStructuralOffset( this.model.data, this.cursor ) ) {
 181+ // TODO: Support tree updates when inserting between elements
 182+ } else {
 183+ // Get the node we are about to insert into at the lowest depth possible
 184+ var node = this.getScope( this.model.getNodeFromOffset( this.cursor ), op.data );
 185+ if ( !node ) {
 186+ throw 'Missing node error. Scope could not be resolved';
 187+ }
 188+ // Figure out how deep the data goes
 189+
 190+ var offset = this.model.getOffsetFromNode( node );
 191+ if ( es.DocumentModel.containsElementData( op.data ) ) {
 192+ // Perform insert on linear data model
 193+ es.insertIntoArray( this.model.data, this.cursor, op.data );
 194+ this.applyAnnotations( this.cursor + op.data.length );
 195+ // Synchronize model tree
 196+ if ( offset === -1 ) {
 197+ throw 'Invalid offset error. Node is not in model tree';
 198+ }
 199+ this.rebuildNodes(
 200+ this.model.data.slice( offset, offset + node.getElementLength() + op.data.length ),
 201+ [node]
 202+ );
 203+ } else {
 204+ // Perform insert on linear data model
 205+ // TODO this is duplicated from above
 206+ es.insertIntoArray( this.model.data, this.cursor, op.data );
 207+ this.applyAnnotations( this.cursor + op.data.length );
 208+ // Update model tree
 209+ node.adjustContentLength( op.data.length, true );
 210+ node.emit( 'update', this.cursor - offset );
 211+ }
 212+ }
 213+ this.cursor += op.data.length;
 214+};
 215+
 216+es.TransactionProcessor.prototype.remove = function( op ) {
 217+ if ( es.DocumentModel.containsElementData( op.data ) ) {
 218+ // Figure out which nodes are covered by the removal
 219+ var ranges = this.model.selectNodes( new es.Range( this.cursor, this.cursor + op.data.length ) );
 220+ var oldNodes = [], newData = [], firstKeptNode = true, lastElement;
 221+ for ( var i = 0; i < ranges.length; i++ ) {
 222+ oldNodes.push( ranges[i].node );
 223+ if ( ranges[i].globalRange !== undefined ) {
 224+ // We have to keep part of this node
 225+ if ( firstKeptNode ) {
 226+ // This is the first node we're keeping
 227+ // Keep its opening as well
 228+ newData.push( ranges[i].node.getElement() );
 229+ firstKeptNode = false;
 230+ }
 231+
 232+ // Compute the start and end offset of this node
 233+ // We could do that with getOffsetFromNode() but
 234+ // we already have all the numbers we need so why would we
 235+ var startOffset = ranges[i].globalRange.start - ranges[i].range.start,
 236+ endOffset = startOffset + ranges[i].node.getContentLength(),
 237+ // Get this node's data
 238+ nodeData = this.model.data.slice( startOffset, endOffset );
 239+ // Remove data covered by the range from nodeData
 240+ nodeData.splice( ranges[i].range.start, ranges[i].range.end - ranges[i].range.start );
 241+ // What remains in nodeData is the data we need to keep
 242+ // Append it to newData
 243+ newData = newData.concat( nodeData );
 244+
 245+ lastElement = ranges[i].node.getElementType();
 246+ }
 247+ }
 248+ if ( lastElement !== undefined ) {
 249+ // Keep the closing of the last element that was partially kept
 250+ newData.push( { 'type': '/' + lastElement } );
 251+ }
 252+
 253+ // Perform the rebuild. This updates the model tree
 254+ this.rebuildNodes( newData, oldNodes );
 255+ } else {
 256+ // We're removing content only. Take a shortcut
 257+ // Get the node we are removing content from
 258+ var node = this.model.getNodeFromOffset( this.cursor );
 259+ // Update model tree
 260+ node.adjustContentLength( -op.data.length, true );
 261+ node.emit( 'update', this.cursor - this.model.getOffsetFromNode( node ) );
 262+ }
 263+
 264+ // Update the linear model
 265+ this.model.data.splice( this.cursor, op.data.length );
 266+};
 267+
 268+es.TransactionProcessor.prototype.attribute = function( op, invert ) {
 269+ var element = this.model.data[this.cursor];
 270+ if ( element.type === undefined ) {
 271+ throw 'Invalid element error. Can not set attributes on non-element data.';
 272+ }
 273+ if ( ( op.method === 'set' && !invert ) || ( op.method === 'clear' && invert ) ) {
 274+ // Automatically initialize attributes object
 275+ if ( !element.attributes ) {
 276+ element.attributes = {};
 277+ }
 278+ element.attributes[op.key] = op.value;
 279+ } else if ( ( op.method === 'clear' && !invert ) || ( op.method === 'set' && invert ) ) {
 280+ if ( element.attributes ) {
 281+ delete element.attributes[op.key];
 282+ }
 283+ // Automatically clean up attributes object
 284+ var empty = true;
 285+ for ( var key in element.attributes ) {
 286+ empty = false;
 287+ break;
 288+ }
 289+ if ( empty ) {
 290+ delete element.attributes;
 291+ }
 292+ } else {
 293+ throw 'Invalid method error. Can not operate attributes this way: ' + method;
 294+ }
 295+};
 296+
 297+es.TransactionProcessor.prototype.mark = function( op, invert ) {
 298+ var target;
 299+ if ( ( op.method === 'set' && !invert ) || ( op.method === 'clear' && invert ) ) {
 300+ target = this.set;
 301+ } else if ( ( op.method === 'clear' && !invert ) || ( op.method === 'set' && invert ) ) {
 302+ target = this.clear;
 303+ } else {
 304+ throw 'Invalid method error. Can not operate attributes this way: ' + method;
 305+ }
 306+ if ( op.bias === 'start' ) {
 307+ target.push( op.annotation );
 308+ } else if ( op.bias === 'stop' ) {
 309+ var index = es.DocumentModel.getIndexOfAnnotation( target, op.annotation );
 310+ if ( index === -1 ) {
 311+ throw 'Annotation stack error. Annotation is missing.';
 312+ }
 313+ target.splice( index, 1 );
 314+ }
 315+};
Index: trunk/extensions/VisualEditor/modules/es/bases/es.DocumentBranchNode.js
@@ -78,6 +78,9 @@
7979 * @returns {Integer} Offset of node or -1 of node was not found
8080 */
8181 es.DocumentBranchNode.prototype.getOffsetFromNode = function( node, shallow ) {
 82+ if ( node === this ) {
 83+ return 0;
 84+ }
8285 if ( this.children.length ) {
8386 var offset = 0,
8487 childNode;
@@ -112,6 +115,9 @@
113116 * @returns {es.DocumentNode|null} Node at offset, or null if non was found
114117 */
115118 es.DocumentBranchNode.prototype.getNodeFromOffset = function( offset, shallow ) {
 119+ if ( offset === 0 ) {
 120+ return this;
 121+ }
116122 // TODO a lot of logic is duplicated in selectNodes(), abstract that into a traverser or something
117123 if ( this.children.length ) {
118124 var nodeOffset = 0,

Status & tagging log