1 /**
  2  * Copyright (C) 2012-2013 KO GmbH <copyright@kogmbh.com>
  3  *
  4  * @licstart
  5  * This file is part of WebODF.
  6  *
  7  * WebODF is free software: you can redistribute it and/or modify it
  8  * under the terms of the GNU Affero General Public License (GNU AGPL)
  9  * as published by the Free Software Foundation, either version 3 of
 10  * the License, or (at your option) any later version.
 11  *
 12  * WebODF is distributed in the hope that it will be useful, but
 13  * WITHOUT ANY WARRANTY; without even the implied warranty of
 14  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 15  * GNU Affero General Public License for more details.
 16  *
 17  * You should have received a copy of the GNU Affero General Public License
 18  * along with WebODF.  If not, see <http://www.gnu.org/licenses/>.
 19  * @licend
 20  *
 21  * @source: http://www.webodf.org/
 22  * @source: https://github.com/kogmbh/WebODF/
 23  */
 24 
 25 /*global Node, runtime, core, gui, ops, odf, NodeFilter*/
 26 
 27 /**
 28  * A document that keeps all data related to the mapped document.
 29  * @constructor
 30  * @implements {ops.Document}
 31  * @implements {core.Destroyable}
 32  * @param {!odf.OdfCanvas} odfCanvas
 33  */
 34 ops.OdtDocument = function OdtDocument(odfCanvas) {
 35     "use strict";
 36 
 37     var self = this,
 38         /**@type{!odf.StepUtils}*/
 39         stepUtils,
 40         odfUtils = odf.OdfUtils,
 41         domUtils = core.DomUtils,
 42         /**!Object.<!ops.OdtCursor>*/
 43         cursors = {},
 44         /**!Object.<!ops.Member>*/
 45         members = {},
 46         eventNotifier = new core.EventNotifier([
 47             ops.Document.signalMemberAdded,
 48             ops.Document.signalMemberUpdated,
 49             ops.Document.signalMemberRemoved,
 50             ops.Document.signalCursorAdded,
 51             ops.Document.signalCursorRemoved,
 52             ops.Document.signalCursorMoved,
 53             ops.OdtDocument.signalParagraphChanged,
 54             ops.OdtDocument.signalParagraphStyleModified,
 55             ops.OdtDocument.signalCommonStyleCreated,
 56             ops.OdtDocument.signalCommonStyleDeleted,
 57             ops.OdtDocument.signalTableAdded,
 58             ops.OdtDocument.signalOperationStart,
 59             ops.OdtDocument.signalOperationEnd,
 60             ops.OdtDocument.signalProcessingBatchStart,
 61             ops.OdtDocument.signalProcessingBatchEnd,
 62             ops.OdtDocument.signalUndoStackChanged,
 63             ops.OdtDocument.signalStepsInserted,
 64             ops.OdtDocument.signalStepsRemoved,
 65             ops.OdtDocument.signalMetadataUpdated,
 66             ops.OdtDocument.signalAnnotationAdded
 67         ]),
 68         /**@const*/
 69         FILTER_ACCEPT = core.PositionFilter.FilterResult.FILTER_ACCEPT,
 70         /**@const*/
 71         FILTER_REJECT = core.PositionFilter.FilterResult.FILTER_REJECT,
 72         /**@const*/
 73         NEXT = core.StepDirection.NEXT,
 74         filter,
 75         /**@type{!ops.OdtStepsTranslator}*/
 76         stepsTranslator,
 77         lastEditingOp,
 78         unsupportedMetadataRemoved = false,
 79         /**@const*/ SHOW_ALL = NodeFilter.SHOW_ALL,
 80         blacklistedNodes = new gui.BlacklistNamespaceNodeFilter(["urn:webodf:names:cursor", "urn:webodf:names:editinfo"]),
 81         odfTextBodyFilter = new gui.OdfTextBodyNodeFilter(),
 82         defaultNodeFilter = new core.NodeFilterChain([blacklistedNodes, odfTextBodyFilter]);
 83 
 84     /**
 85      *
 86      * @param {!Node} rootNode
 87      * @return {!core.PositionIterator}
 88      */
 89     function createPositionIterator(rootNode) {
 90         return new core.PositionIterator(rootNode, SHOW_ALL, defaultNodeFilter, false);
 91     }
 92     this.createPositionIterator = createPositionIterator;
 93 
 94     /**
 95      * Return the office:text element of this document.
 96      * @return {!Element}
 97      */
 98     function getRootNode() {
 99         var element = odfCanvas.odfContainer().getContentElement(),
100             localName = element && element.localName;
101         runtime.assert(localName === "text", "Unsupported content element type '" + localName + "' for OdtDocument");
102         return element;
103     }
104     /**
105      * Return the office:document element of this document.
106      * @return {!Element}
107      */
108     this.getDocumentElement = function () {
109         return odfCanvas.odfContainer().rootElement;
110     };
111     /**
112      * @return {!Document}
113      */
114     this.getDOMDocument = function () {
115         return /**@type{!Document}*/(this.getDocumentElement().ownerDocument);
116     };
117 
118     this.cloneDocumentElement = function () {
119         var rootElement = self.getDocumentElement(),
120             annotationViewManager = odfCanvas.getAnnotationViewManager(),
121             initialDoc;
122 
123         if (annotationViewManager) {
124             annotationViewManager.forgetAnnotations();
125         }
126         initialDoc = rootElement.cloneNode(true);
127         odfCanvas.refreshAnnotations();
128         return initialDoc;
129     };
130 
131     /**
132      * @param {!Element} documentElement
133      */
134     this.setDocumentElement = function (documentElement) {
135         var odfContainer = odfCanvas.odfContainer();
136         // TODO Replace with a neater hack for reloading the Odt tree
137         // Once this is fixed, SelectionView.addOverlays & OdtStepsTranslator.verifyRootNode can be largely removed
138         odfContainer.setRootElement(documentElement);
139         odfCanvas.setOdfContainer(odfContainer, true);
140         odfCanvas.refreshCSS();
141     };
142 
143     /**
144      * @return {!Document}
145      */
146     function getDOMDocument() {
147         return /**@type{!Document}*/(self.getDocumentElement().ownerDocument);
148     }
149     this.getDOMDocument = getDOMDocument;
150     
151     /**
152      * @param {!Node} node
153      * @return {!boolean}
154      */
155     function isRoot(node) {
156         if ((node.namespaceURI === odf.Namespaces.officens
157              && node.localName === 'text'
158             ) || (node.namespaceURI === odf.Namespaces.officens
159                   && node.localName === 'annotation')) {
160             return true;
161         }
162         return false;
163     }
164 
165     /**
166      * @param {!Node} node
167      * @return {!Node}
168      */
169     function getRoot(node) {
170         while (node && !isRoot(node)) {
171             node = /**@type{!Node}*/(node.parentNode);
172         }
173         return node;
174     }
175     this.getRootElement = getRoot;
176 
177     /**
178      * A filter that allows a position if it has the same closest
179      * whitelisted root as the specified 'anchor', which can be the cursor
180      * of the given memberid, or a given node
181      * @constructor
182      * @implements {core.PositionFilter}
183      * @param {!string|!Node} anchor 
184      */
185     function RootFilter(anchor) {
186 
187         /**
188          * @param {!core.PositionIterator} iterator
189          * @return {!core.PositionFilter.FilterResult}
190          */
191         this.acceptPosition = function (iterator) {
192             var node = iterator.container(),
193                 anchorNode;
194 
195             if (typeof anchor === "string") {
196                 anchorNode = cursors[anchor].getNode();
197             } else {
198                 anchorNode = anchor;
199             }
200 
201             if (getRoot(node) === getRoot(anchorNode)) {
202                 return FILTER_ACCEPT;
203             }
204             return FILTER_REJECT;
205         };
206     }
207 
208     /**
209      * Create a new StepIterator instance set to the defined position
210      *
211      * @param {!Node} container
212      * @param {!number} offset
213      * @param {!Array.<!core.PositionFilter>} filters Filter to apply to the iterator positions. If multiple
214      *  iterators are provided, they will be combined in order using a PositionFilterChain.
215      * @param {!Node} subTree Subtree to search for step within. Generally a paragraph or document root. Choosing
216      *  a smaller subtree allows iteration to end quickly if there are no walkable steps remaining in a particular
217      *  direction. This can vastly improve performance.
218      *
219      * @return {!core.StepIterator}
220      */
221     function createStepIterator(container, offset, filters, subTree) {
222         var positionIterator = createPositionIterator(subTree),
223             filterOrChain,
224             stepIterator;
225 
226         if (filters.length === 1) {
227             filterOrChain = filters[0];
228         } else {
229             filterOrChain = new core.PositionFilterChain();
230             filters.forEach(filterOrChain.addFilter);
231         }
232 
233         stepIterator = new core.StepIterator(filterOrChain, positionIterator);
234         stepIterator.setPosition(container, offset);
235         return stepIterator;
236     }
237     this.createStepIterator = createStepIterator;
238 
239     /**
240      * Returns a PositionIterator instance at the
241      * specified starting position
242      * @param {!number} position
243      * @return {!core.PositionIterator}
244      */
245     function getIteratorAtPosition(position) {
246         var iterator = createPositionIterator(getRootNode()),
247             point = stepsTranslator.convertStepsToDomPoint(position);
248 
249         iterator.setUnfilteredPosition(point.node, point.offset);
250         return iterator;
251     }
252     this.getIteratorAtPosition = getIteratorAtPosition;
253 
254     /**
255      * Converts the requested step number from root into the equivalent DOM node & offset
256      * pair. If the step is outside the bounds of the document, a RangeError will be thrown.
257      * @param {!number} step
258      * @return {!{node: !Node, offset: !number}}
259      */
260     this.convertCursorStepToDomPoint = function (step) {
261         return stepsTranslator.convertStepsToDomPoint(step);
262     };
263 
264     /**
265      * Rounds to the first step within the paragraph
266      * @param {!core.StepDirection} step
267      * @return {!boolean}
268      */
269     function roundUp(step) {
270         return step === NEXT;
271     }
272 
273     /**
274      * Converts a DOM node and offset pair to a cursor step. If a rounding direction is not supplied then
275      * the default is to round down to the previous step.
276      * @param {!Node} node
277      * @param {!number} offset
278      * @param {core.StepDirection=} roundDirection Whether to round down to the previous step or round up
279      * to the next step. The default value if unspecified is core.StepDirection.PREVIOUS
280      * @return {!number}
281      */
282     this.convertDomPointToCursorStep = function (node, offset, roundDirection) {
283         var roundingFunc;
284         if(roundDirection === NEXT) {
285             roundingFunc = roundUp;
286         }
287 
288         return stepsTranslator.convertDomPointToSteps(node, offset, roundingFunc);
289     };
290 
291     /**
292      * @param {!{anchorNode: !Node, anchorOffset: !number, focusNode: !Node, focusOffset: !number}} selection
293      * @return {!{position: !number, length: number}}
294      */
295     this.convertDomToCursorRange = function (selection) {
296         var point1,
297             point2;
298 
299         point1 = stepsTranslator.convertDomPointToSteps(selection.anchorNode, selection.anchorOffset);
300         if (selection.anchorNode === selection.focusNode && selection.anchorOffset === selection.focusOffset) {
301             point2 = point1;
302         } else {
303             point2 = stepsTranslator.convertDomPointToSteps(selection.focusNode, selection.focusOffset);
304         }
305 
306         return {
307             position: point1,
308             length: point2 - point1
309         };
310     };
311 
312     /**
313      * Convert a cursor range to a DOM range
314      * @param {!number} position
315      * @param {!number} length
316      * @return {!Range}
317      */
318     this.convertCursorToDomRange = function (position, length) {
319         var range = getDOMDocument().createRange(),
320             point1,
321             point2;
322 
323         point1 = stepsTranslator.convertStepsToDomPoint(position);
324         if (length) {
325             point2 = stepsTranslator.convertStepsToDomPoint(position + length);
326             if (length > 0) {
327                 range.setStart(point1.node, point1.offset);
328                 range.setEnd(point2.node, point2.offset);
329             } else {
330                 range.setStart(point2.node, point2.offset);
331                 range.setEnd(point1.node, point1.offset);
332             }
333         } else {
334             range.setStart(point1.node, point1.offset);
335         }
336         return range;
337     };
338 
339     /**
340      * This function will iterate through positions allowed by the position
341      * iterator and count only the text positions. When the amount defined by
342      * offset has been counted, the Text node that that position is returned
343      * as well as the offset in that text node.
344      * Optionally takes a memberid of a cursor, to specifically return the
345      * text node positioned just behind that cursor.
346      * @param {!number} steps
347      * @param {!string=} memberid
348      * @return {?{textNode: !Text, offset: !number}}
349      */
350     function getTextNodeAtStep(steps, memberid) {
351         var iterator = getIteratorAtPosition(steps),
352             node = iterator.container(),
353             lastTextNode,
354             nodeOffset = 0,
355             cursorNode = null,
356             text;
357 
358         if (node.nodeType === Node.TEXT_NODE) {
359             // Iterator has stopped within an existing text node, to put that up as a possible target node
360             lastTextNode = /**@type{!Text}*/(node);
361             nodeOffset = /**@type{!number}*/(iterator.unfilteredDomOffset());
362             // Always cut in a new empty text node at the requested position.
363             // If this proves to be unnecessary, it will be cleaned up just before the return
364             // after all necessary cursor rearrangements have been performed
365             if (lastTextNode.length > 0) {
366                 // The node + offset returned make up the boundary just to the right of the requested step
367                 if (nodeOffset > 0) {
368                     // In this case, after the split, the right of the requested step is just after the new node
369                     lastTextNode = lastTextNode.splitText(nodeOffset);
370                 }
371                 lastTextNode.parentNode.insertBefore(getDOMDocument().createTextNode(""), lastTextNode);
372                 lastTextNode = /**@type{!Text}*/(lastTextNode.previousSibling);
373                 nodeOffset = 0;
374             }
375         } else {
376             // There is no text node at the current position, so insert a new one at the current position
377             lastTextNode = getDOMDocument().createTextNode("");
378             nodeOffset = 0;
379             node.insertBefore(lastTextNode, iterator.rightNode());
380         }
381 
382         if (memberid) {
383             // DEPRECATED: This branch is no longer the recommended way of handling cursor movements DO NOT USE
384             // If the member cursor is as the requested position
385             if (cursors[memberid] && self.getCursorPosition(memberid) === steps) {
386                 cursorNode = cursors[memberid].getNode();
387                 // Then move the member's cursor after all adjacent cursors
388                 while (cursorNode.nextSibling && cursorNode.nextSibling.localName === "cursor") {
389                     // TODO this re-arrange logic will break if there are non-cursor elements in the way
390                     // E.g., cursors occupy the same "step", but are on different sides of a span boundary
391                     // This is currently avoided by calling fixCursorPositions after (almost) every op
392                     // to re-arrange cursors together again
393                     cursorNode.parentNode.insertBefore(cursorNode.nextSibling, cursorNode);
394                 }
395                 if (lastTextNode.length > 0 && lastTextNode.nextSibling !== cursorNode) {
396                     // The last text node contains content but is not adjacent to the cursor
397                     // This can't be moved, as moving it would move the text content around as well. Yikes!
398                     // So, create a new text node to insert data into
399                     lastTextNode = getDOMDocument().createTextNode('');
400                     nodeOffset = 0;
401                 }
402                 // Keep the destination text node right next to the member's cursor, so inserted text pushes the cursor over
403                 cursorNode.parentNode.insertBefore(lastTextNode, cursorNode);
404             }
405         } else {
406             // Move all cursors BEFORE the new text node. Any cursors occupying the requested position should not
407             // move when new text is added in the position
408             while (lastTextNode.nextSibling && lastTextNode.nextSibling.localName === "cursor") {
409                 // TODO this re-arrange logic will break if there are non-cursor elements in the way
410                 // E.g., cursors occupy the same "step", but are on different sides of a span boundary
411                 // This is currently avoided by calling fixCursorPositions after (almost) every op
412                 // to re-arrange cursors together again
413                 lastTextNode.parentNode.insertBefore(lastTextNode.nextSibling, lastTextNode);
414             }
415         }
416 
417         // After the above cursor adjustments, if the lastTextNode
418         // has a text node previousSibling, merge them and make the result the lastTextNode
419         while (lastTextNode.previousSibling && lastTextNode.previousSibling.nodeType === Node.TEXT_NODE) {
420             text = /**@type{!Text}*/(lastTextNode.previousSibling);
421             text.appendData(lastTextNode.data);
422             nodeOffset = text.length;
423             lastTextNode = text;
424             lastTextNode.parentNode.removeChild(lastTextNode.nextSibling);
425         }
426 
427         // Empty text nodes can be left on either side of the split operations that have occurred
428         while (lastTextNode.nextSibling && lastTextNode.nextSibling.nodeType === Node.TEXT_NODE) {
429             text = /**@type{!Text}*/(lastTextNode.nextSibling);
430             lastTextNode.appendData(text.data);
431             lastTextNode.parentNode.removeChild(text);
432         }
433 
434         return {textNode: lastTextNode, offset: nodeOffset };
435     }
436 
437     /**
438      * Called after an operation is executed, this
439      * function will check if the operation is an
440      * 'edit', and in that case will update the
441      * document's metadata, such as dc:creator,
442      * meta:editing-cycles, and dc:creator.
443      * @param {!ops.Operation} op
444      */
445     function handleOperationExecuted(op) {
446         var opspec = op.spec(),
447             memberId = opspec.memberid,
448             date = new Date(opspec.timestamp).toISOString(),
449             odfContainer = odfCanvas.odfContainer(),
450             /**@type{!{setProperties: !Object, removedProperties: ?Array.<!string>}}*/
451             changedMetadata = {
452                 setProperties: {},
453                 removedProperties: []
454             },
455             fullName;
456 
457         // If the operation is an edit (that changes the
458         // ODF that will be saved), then update metadata.
459         if (op.isEdit) {
460             if (opspec.optype === "UpdateMetadata") {
461                 // HACK: Cannot typecast this to OpUpdateMetadata's spec because that would be a cyclic dependency,
462                 // therefore forcibly typecast this to advertise the two required properties. Also, deep clone to avoid
463                 // unintended modification of the op spec.
464                 changedMetadata.setProperties = /**@type{!Object}*/(JSON.parse(JSON.stringify(/**@type{!{setProperties: !Object}}*/(opspec).setProperties)));
465                 if (/**@type{!{removedProperties: ?{attributes: !string}}}*/(opspec).removedProperties) {
466                     changedMetadata.removedProperties = /**@type{!{removedProperties: ?{attributes: !string}}}*/(opspec).removedProperties.attributes.split(',');
467                 }
468             }
469 
470             fullName = self.getMember(memberId).getProperties().fullName;
471             odfContainer.setMetadata({
472                 "dc:creator": fullName,
473                 "dc:date": date
474             }, null);
475             changedMetadata.setProperties["dc:creator"] = fullName;
476             changedMetadata.setProperties["dc:date"] = date;
477 
478             // If no previous op was found in this session,
479             // then increment meta:editing-cycles by 1.
480             if (!lastEditingOp) {
481                 changedMetadata.setProperties["meta:editing-cycles"] = odfContainer.incrementEditingCycles();
482                 // Remove certain metadata fields that
483                 // should be updated as soon as edits happen,
484                 // but cannot be because we don't support those yet.
485                 if (!unsupportedMetadataRemoved) {
486                     odfContainer.setMetadata(null, [
487                         "meta:editing-duration",
488                         "meta:document-statistic"
489                     ]);
490                 }
491             }
492 
493             lastEditingOp = op;
494             self.emit(ops.OdtDocument.signalMetadataUpdated, changedMetadata);
495         }
496     }
497 
498     /**
499      * Upgrades literal whitespaces (' ') to <text:s> </text:s>,
500      * when given a textNode containing the whitespace and an offset
501      * indicating the location of the whitespace in it.
502      * @param {!Text} textNode
503      * @param {!number} offset
504      * @return {!Element}
505      */
506     function upgradeWhitespaceToElement(textNode, offset) {
507         runtime.assert(textNode.data[offset] === ' ', "upgradeWhitespaceToElement: textNode.data[offset] should be a literal space");
508 
509         var space = textNode.ownerDocument.createElementNS(odf.Namespaces.textns, 'text:s'),
510             container = textNode.parentNode,
511             adjacentNode = textNode;
512 
513         space.appendChild(textNode.ownerDocument.createTextNode(' '));
514 
515         if (textNode.length === 1) {
516             // The space is the only element in this node. Can simply replace it
517             container.replaceChild(space, textNode);
518         } else {
519             textNode.deleteData(offset, 1);
520             if (offset > 0) { // Don't create an empty text node if the offset is 0...
521                 if (offset < textNode.length) {
522                     // Don't split if offset === textNode.length as this would add an empty text node after
523                     textNode.splitText(offset);
524                 }
525                 adjacentNode = textNode.nextSibling;
526             }
527             container.insertBefore(space, adjacentNode);
528         }
529         return space;
530     }
531 
532     /**
533      * @param {!number} step
534      * @return {undefined}
535      */
536     function upgradeWhitespacesAtPosition(step) {
537         var positionIterator = getIteratorAtPosition(step),
538             stepIterator = new core.StepIterator(filter, positionIterator),
539             contentBounds,
540             /**@type{?Node}*/
541             container,
542             offset,
543             stepsToUpgrade = 2;
544 
545         // The step passed into this function is the point of change. Need to
546         // upgrade whitespace to the left of the current step, and to the left of the next step
547         runtime.assert(stepIterator.isStep(), "positionIterator is not at a step (requested step: " + step + ")");
548 
549         do {
550             contentBounds = stepUtils.getContentBounds(stepIterator);
551             if (contentBounds) {
552                 container = contentBounds.container;
553                 offset = contentBounds.startOffset;
554                 if (container.nodeType === Node.TEXT_NODE
555                     && odfUtils.isSignificantWhitespace(/**@type{!Text}*/(container), offset)) {
556                     container = upgradeWhitespaceToElement(/**@type{!Text}*/(container), offset);
557                     // Reset the iterator position to the same step it was just on, which was just to
558                     // the right of a space
559                     stepIterator.setPosition(container, container.childNodes.length);
560                     stepIterator.roundToPreviousStep();
561                 }
562             }
563             stepsToUpgrade -= 1;
564         } while (stepsToUpgrade > 0 && stepIterator.nextStep());
565     }
566 
567     /**
568      * Upgrades any significant whitespace at the requested step, and one step right of the given
569      * position to space elements.
570      * @param {!number} step
571      * @return {undefined}
572      */
573     this.upgradeWhitespacesAtPosition = upgradeWhitespacesAtPosition;
574 
575     /**
576      * Returns the maximum available offset for the specified node.
577      * @param {!Node} node
578      * @return {!number}
579      */
580     function maxOffset(node) {
581         return node.nodeType === Node.TEXT_NODE ? /**@type{!Text}*/(node).length : node.childNodes.length;
582     }
583 
584     /**
585      * Downgrades white space elements to normal spaces at the step iterators current step, and one step
586      * to the right.
587      *
588      * @param {!core.StepIterator} stepIterator
589      * @return {undefined}
590      */
591     function downgradeWhitespaces(stepIterator) {
592         var contentBounds,
593             /**@type{!Node}*/
594             container,
595             modifiedNodes = [],
596             lastChild,
597             stepsToUpgrade = 2;
598 
599         // The step passed into this function is the point of change. Need to
600         // downgrade whitespace to the left of the current step, and to the left of the next step
601         runtime.assert(stepIterator.isStep(), "positionIterator is not at a step");
602 
603         do {
604             contentBounds = stepUtils.getContentBounds(stepIterator);
605             if (contentBounds) {
606                 container = contentBounds.container;
607                 if (odfUtils.isDowngradableSpaceElement(container)) {
608                     lastChild = /**@type{!Node}*/(container.lastChild);
609                     while(container.firstChild) {
610                         // Merge contained space text node up to replace container
611                         modifiedNodes.push(container.firstChild);
612                         container.parentNode.insertBefore(container.firstChild, container);
613                     }
614                     container.parentNode.removeChild(container);
615                     // Reset the iterator position to the same step it was just on, which was just to
616                     // the right of a space
617                     stepIterator.setPosition(lastChild, maxOffset(lastChild));
618                     stepIterator.roundToPreviousStep();
619                 }
620             }
621             stepsToUpgrade -= 1;
622         } while (stepsToUpgrade > 0 && stepIterator.nextStep());
623 
624         modifiedNodes.forEach(domUtils.normalizeTextNodes);
625     }
626     this.downgradeWhitespaces = downgradeWhitespaces;
627 
628     /**
629      * Downgrades white space elements to normal spaces at the specified step, and one step
630      * to the right.
631      *
632      * @param {!number} step
633      * @return {undefined}
634      */
635     this.downgradeWhitespacesAtPosition = function (step) {
636         var positionIterator = getIteratorAtPosition(step),
637             stepIterator = new core.StepIterator(filter, positionIterator);
638 
639         downgradeWhitespaces(stepIterator);
640     };
641 
642     /**
643      * This function will return the Text node as well as the offset in that text node
644      * of the cursor.
645      * @param {!number} position
646      * @param {!string=} memberid
647      * @return {?{textNode: !Text, offset: !number}}
648      */
649     this.getTextNodeAtStep = getTextNodeAtStep;
650 
651     /**
652      * Returns the closest parent paragraph or root to the supplied container and offset
653      * @param {!Node} container
654      * @param {!number} offset
655      * @param {!Node} root
656      *
657      * @return {!Node}
658      */
659     function paragraphOrRoot(container, offset, root) {
660         var node = container.childNodes.item(offset) || container,
661             paragraph = odfUtils.getParagraphElement(node);
662         if (paragraph && domUtils.containsNode(root, paragraph)) {
663             // Only return the paragraph if it is contained within the destination root
664             return /**@type{!Node}*/(paragraph);
665         }
666         // Otherwise the step filter should be contained within the supplied root
667         return root;
668     }
669 
670     /**
671      * Iterates through all cursors and checks if they are in
672      * walkable positions; if not, move the cursor 1 filtered step backward
673      * which guarantees walkable state for all cursors,
674      * while keeping them inside the same root. An event will be raised for this cursor if it is moved
675      */
676     this.fixCursorPositions = function () {
677         Object.keys(cursors).forEach(function (memberId) {
678             var cursor = cursors[memberId],
679                 root = getRoot(cursor.getNode()),
680                 rootFilter = self.createRootFilter(root),
681                 subTree,
682                 startPoint,
683                 endPoint,
684                 selectedRange,
685                 cursorMoved = false;
686 
687             selectedRange = cursor.getSelectedRange();
688             subTree = paragraphOrRoot(/**@type{!Node}*/(selectedRange.startContainer), selectedRange.startOffset, root);
689             startPoint = createStepIterator(/**@type{!Node}*/(selectedRange.startContainer), selectedRange.startOffset,
690                 [filter, rootFilter], subTree);
691 
692             if (!selectedRange.collapsed) {
693                 subTree = paragraphOrRoot(/**@type{!Node}*/(selectedRange.endContainer), selectedRange.endOffset, root);
694                 endPoint = createStepIterator(/**@type{!Node}*/(selectedRange.endContainer), selectedRange.endOffset,
695                     [filter, rootFilter], subTree);
696             } else {
697                 endPoint = startPoint;
698             }
699 
700             if (!startPoint.isStep() || !endPoint.isStep()) {
701                 cursorMoved = true;
702                 runtime.assert(startPoint.roundToClosestStep(), "No walkable step found for cursor owned by " + memberId);
703                 selectedRange.setStart(startPoint.container(), startPoint.offset());
704                 runtime.assert(endPoint.roundToClosestStep(), "No walkable step found for cursor owned by " + memberId);
705                 selectedRange.setEnd(endPoint.container(), endPoint.offset());
706             } else if (startPoint.container() === endPoint.container() && startPoint.offset() === endPoint.offset()) {
707                 // The range *should* be collapsed
708                 if (!selectedRange.collapsed || cursor.getAnchorNode() !== cursor.getNode()) {
709                     // It might not be collapsed if there are other unwalkable nodes (e.g., cursors)
710                     // between the cursor and anchor nodes. In this case, force the cursor to collapse
711                     cursorMoved = true;
712                     selectedRange.setStart(startPoint.container(), startPoint.offset());
713                     selectedRange.collapse(true);
714                 }
715             }
716 
717             if (cursorMoved) {
718                 cursor.setSelectedRange(selectedRange, cursor.hasForwardSelection());
719                 self.emit(ops.Document.signalCursorMoved, cursor);
720             }
721         });
722     };
723 
724     /**
725      * This function returns the position in ODF world of the cursor of the member.
726      * @param {!string} memberid
727      * @return {!number}
728      */
729     this.getCursorPosition = function (memberid) {
730         var cursor = cursors[memberid];
731         return cursor ? stepsTranslator.convertDomPointToSteps(cursor.getNode(), 0) : 0;
732     };
733 
734     /**
735      * This function returns the position and selection length in ODF world of
736      * the cursor of the member.
737      * position is always the number of steps from root node to the anchor node
738      * length is the number of steps from anchor node to focus node
739      * !IMPORTANT! length is a vector, and may be negative if the cursor selection
740      * is reversed (i.e., user clicked and dragged the cursor backwards)
741      * @param {!string} memberid
742      * @return {{position: !number, length: !number}}
743      */
744     this.getCursorSelection = function (memberid) {
745         var cursor = cursors[memberid],
746             focusPosition = 0,
747             anchorPosition = 0;
748         if (cursor) {
749             focusPosition = stepsTranslator.convertDomPointToSteps(cursor.getNode(), 0);
750             anchorPosition = stepsTranslator.convertDomPointToSteps(cursor.getAnchorNode(), 0);
751         }
752         return {
753             position: anchorPosition,
754             length: focusPosition - anchorPosition
755         };
756     };
757     /**
758      * @return {!core.PositionFilter}
759      */
760     this.getPositionFilter = function () {
761         return filter;
762     };
763 
764     /**
765      * @return {!odf.OdfCanvas}
766      */
767     this.getOdfCanvas = function () {
768         return odfCanvas;
769     };
770 
771     /**
772      * @return {!ops.Canvas}
773      */
774     this.getCanvas = function () {
775         return odfCanvas;
776     };
777 
778     /**
779      * @return {!Element}
780      */
781     this.getRootNode = getRootNode;
782 
783     /**
784      * @param {!ops.Member} member
785      * @return {undefined}
786      */
787     this.addMember = function (member) {
788         runtime.assert(members[member.getMemberId()] === undefined, "This member already exists");
789         members[member.getMemberId()] = member;
790     };
791 
792     /**
793      * @param {!string} memberId
794      * @return {?ops.Member}
795      */
796     this.getMember = function (memberId) {
797         return members.hasOwnProperty(memberId) ? members[memberId] : null;
798     };
799 
800     /**
801      * @param {!string} memberId
802      * @return {undefined}
803      */
804     this.removeMember = function (memberId) {
805         delete members[memberId];
806     };
807 
808     /**
809      * @param {!string} memberid
810      * @return {ops.OdtCursor}
811      */
812     this.getCursor = function (memberid) {
813         return cursors[memberid];
814     };
815 
816     /**
817      * @return {!Array.<string>}
818      */
819     this.getMemberIds = function () {
820         var list = [],
821             /**@type{string}*/
822             i;
823         for (i in cursors) {
824             if (cursors.hasOwnProperty(i)) {
825                 list.push(cursors[i].getMemberId());
826             }
827         }
828         return list;
829     };
830 
831     /**
832      * Adds the specified cursor to the ODT document. The cursor will be collapsed
833      * to the first available cursor position in the document.
834      * @param {!ops.OdtCursor} cursor
835      * @return {undefined}
836      */
837     this.addCursor = function (cursor) {
838         runtime.assert(Boolean(cursor), "OdtDocument::addCursor without cursor");
839         var memberid = cursor.getMemberId(),
840             initialSelection = self.convertCursorToDomRange(0, 0);
841 
842         runtime.assert(typeof memberid === "string", "OdtDocument::addCursor has cursor without memberid");
843         runtime.assert(!cursors[memberid], "OdtDocument::addCursor is adding a duplicate cursor with memberid " + memberid);
844         cursor.setSelectedRange(initialSelection, true);
845 
846         cursors[memberid] = cursor;
847     };
848 
849     /**
850      * @param {!string} memberid
851      * @return {!boolean}
852      */
853     this.removeCursor = function (memberid) {
854         var cursor = cursors[memberid];
855         if (cursor) {
856             cursor.removeFromDocument();
857             delete cursors[memberid];
858             self.emit(ops.Document.signalCursorRemoved, memberid);
859             return true;
860         }
861         return false;
862     };
863 
864     /**
865      * Moves the cursor/selection of a given memberid to the
866      * given position+length combination and adopts the given
867      * selectionType.
868      * It is the caller's responsibility to decide if and when
869      * to subsequently fire signalCursorMoved.
870      * @param {!string} memberid
871      * @param {!number} position
872      * @param {!number} length
873      * @param {!string=} selectionType
874      * @return {undefined}
875      */
876     this.moveCursor = function (memberid, position, length, selectionType) {
877         var cursor = cursors[memberid],
878             selectionRange = self.convertCursorToDomRange(position, length);
879         if (cursor) {
880             cursor.setSelectedRange(selectionRange, length >= 0);
881             cursor.setSelectionType(selectionType || ops.OdtCursor.RangeSelection);
882         }
883     };
884 
885     /**
886      * @return {!odf.Formatting}
887      */
888     this.getFormatting = function () {
889         return odfCanvas.getFormatting();
890     };
891 
892     /**
893      * @param {!string} eventid
894      * @param {*} args
895      * @return {undefined}
896      */
897     this.emit = function (eventid, args) {
898         eventNotifier.emit(eventid, args);
899     };
900 
901     /**
902      * @param {!string} eventid
903      * @param {!Function} cb
904      * @return {undefined}
905      */
906     this.subscribe = function (eventid, cb) {
907         eventNotifier.subscribe(eventid, cb);
908     };
909 
910     /**
911      * @param {!string} eventid
912      * @param {!Function} cb
913      * @return {undefined}
914      */
915     this.unsubscribe = function (eventid, cb) {
916         eventNotifier.unsubscribe(eventid, cb);
917     };
918 
919     /**
920      * @param {string|!Node} inputMemberId
921      * @return {!RootFilter}
922      */
923     this.createRootFilter = function (inputMemberId) {
924         return new RootFilter(inputMemberId);
925     };
926 
927     /**
928      * @param {!function(!Object=)} callback, passing an error object in case of error
929      * @return {undefined}
930      */
931     this.close = function (callback) {
932         // TODO: check if anything needs to be cleaned up
933         callback();
934     };
935 
936     /**
937      * @param {!function(!Error=)} callback, passing an error object in case of error
938      * @return {undefined}
939      */
940     this.destroy = function (callback) {
941         callback();
942     };
943 
944     /**
945      * @return {undefined}
946      */
947     function init() {
948         filter = new ops.TextPositionFilter();
949         stepUtils = new odf.StepUtils();
950         stepsTranslator = new ops.OdtStepsTranslator(getRootNode, createPositionIterator, filter, 500);
951         eventNotifier.subscribe(ops.OdtDocument.signalStepsInserted, stepsTranslator.handleStepsInserted);
952         eventNotifier.subscribe(ops.OdtDocument.signalStepsRemoved, stepsTranslator.handleStepsRemoved);
953         eventNotifier.subscribe(ops.OdtDocument.signalOperationEnd, handleOperationExecuted);
954         eventNotifier.subscribe(ops.OdtDocument.signalProcessingBatchEnd, core.Task.processTasks);
955     }
956     init();
957 };
958 
959 /**@const*/ops.OdtDocument.signalParagraphChanged = "paragraph/changed";
960 /**@const*/ops.OdtDocument.signalTableAdded = "table/added";
961 /**@const*/ops.OdtDocument.signalCommonStyleCreated = "style/created";
962 /**@const*/ops.OdtDocument.signalCommonStyleDeleted = "style/deleted";
963 /**@const*/ops.OdtDocument.signalParagraphStyleModified = "paragraphstyle/modified";
964 /**@const*/ops.OdtDocument.signalOperationStart = "operation/start";
965 /**@const*/ops.OdtDocument.signalOperationEnd = "operation/end";
966 /**@const*/ops.OdtDocument.signalProcessingBatchStart = "router/batchstart";
967 /**@const*/ops.OdtDocument.signalProcessingBatchEnd = "router/batchend";
968 /**@const*/ops.OdtDocument.signalUndoStackChanged = "undo/changed";
969 /**@const*/ops.OdtDocument.signalStepsInserted = "steps/inserted";
970 /**@const*/ops.OdtDocument.signalStepsRemoved = "steps/removed";
971 /**@const*/ops.OdtDocument.signalMetadataUpdated = "metadata/updated";
972 /**@const*/ops.OdtDocument.signalAnnotationAdded = "annotation/added";
973 
974 // vim:expandtab
975