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