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 runtime, core, ops, odf*/
 26 
 27 (function () {
 28     "use strict";
 29 
 30     /**
 31      *
 32      * @constructor
 33      * @param {!Element} rootNode
 34      * @param {!core.PositionIterator} iterator
 35      * @param {!core.PositionFilter} filter
 36      * @param {!number} bucketSize  Minimum number of steps between cache points
 37      */
 38     ops.OdtStepsTranslator = function OdtStepsTranslator(rootNode, iterator, filter, bucketSize) {
 39         var /**@type{!ops.StepsCache}*/
 40             stepsCache,
 41             odfUtils = odf.OdfUtils,
 42             domUtils = core.DomUtils,
 43             /**@const*/
 44             FILTER_ACCEPT = core.PositionFilter.FilterResult.FILTER_ACCEPT,
 45             /**@const*/
 46             PREVIOUS = core.StepDirection.PREVIOUS,
 47             /**@const*/
 48             NEXT = core.StepDirection.NEXT;
 49 
 50         /**
 51          * Update the steps cache based on the current iterator position. This can either add new
 52          * bookmarks or update existing references and repair damaged regions of the cache.
 53          *
 54          * @param {!number} steps
 55          * @param {!core.PositionIterator} iterator
 56          * @param {!boolean} isStep
 57          * @return {undefined}
 58          */
 59         function updateCache(steps, iterator, isStep) {
 60             var node = iterator.getCurrentNode();
 61 
 62             if (iterator.isBeforeNode() && odfUtils.isParagraph(node)) {
 63                 if (!isStep) {
 64                     // Paragraph bookmarks indicate "first position in the paragraph"
 65                     // If the current stable point is before the first walkable position (as often happens)
 66                     // simply increase the step number by 1 to move to within the paragraph node
 67                     steps += 1;
 68                 }
 69                 stepsCache.updateBookmark(steps, node);
 70             }
 71         }
 72 
 73         /**
 74          * Saved bookmarks always represent the first step inside the corresponding paragraph or node. Based on the
 75          * current TextPositionFilter impl, this means rounding up if the current iterator position is not on a step.
 76          * @param {!number} steps
 77          * @param {!core.PositionIterator} iterator
 78          * @return {undefined}
 79          */
 80         function roundUpToStep(steps, iterator) {
 81             do {
 82                 if (filter.acceptPosition(iterator) === FILTER_ACCEPT) {
 83                     // Have reached the step represented by the paragraph bookmark
 84                     updateCache(steps, iterator, true);
 85                     break;
 86                 }
 87                 // This logic inverts the +1 logic in updateCache. Conceptually speaking, the stored
 88                 // bookmark represents the first step in the paragraph. Until the first step is found,
 89                 // the iterator is still technically on steps-1.
 90                 updateCache(steps - 1, iterator, false);
 91             } while (iterator.nextPosition());
 92         }
 93 
 94         /**
 95          * Convert the requested steps from root into the equivalent DOM node & offset pair. If the
 96          * requested step is before the start or past the end of the document, a RangeError will be thrown.
 97          * @param {!number} steps
 98          * @return {!{node: !Node, offset: !number}}
 99          */
100         this.convertStepsToDomPoint = function (steps) {
101             var /**@type{!number}*/
102                 stepsFromRoot,
103                 isStep;
104 
105             if (isNaN(steps)) {
106                 throw new TypeError("Requested steps is not numeric (" + steps + ")");
107             }
108             if (steps < 0) {
109                 throw new RangeError("Requested steps is negative (" + steps + ")");
110             }
111             stepsFromRoot = stepsCache.setToClosestStep(steps, iterator);
112 
113             while (stepsFromRoot < steps && iterator.nextPosition()) {
114                 isStep = filter.acceptPosition(iterator) === FILTER_ACCEPT;
115                 if (isStep) {
116                     stepsFromRoot += 1;
117                 }
118                 updateCache(stepsFromRoot, iterator, isStep);
119             }
120             if (stepsFromRoot !== steps) {
121                 throw new RangeError("Requested steps (" + steps + ") exceeds available steps (" + stepsFromRoot + ")");
122             }
123             return {
124                 node: iterator.container(),
125                 offset: iterator.unfilteredDomOffset()
126             };
127         };
128 
129         /**
130          * Uses the provided delegate to choose between rounding up or rounding down to the nearest step.
131          * @param {!core.PositionIterator} iterator
132          * @param {function(!core.StepDirection, !Node, !number):boolean=} roundDirection
133          * @return {!boolean} Returns true if an accepted position is found, otherwise returns false.
134          */
135         function roundToPreferredStep(iterator, roundDirection) {
136             if (!roundDirection || filter.acceptPosition(iterator) === FILTER_ACCEPT) {
137                 return true;
138             }
139 
140             while (iterator.previousPosition()) {
141                 if (filter.acceptPosition(iterator) === FILTER_ACCEPT) {
142                     if (roundDirection(PREVIOUS, iterator.container(), iterator.unfilteredDomOffset())) {
143                         return true;
144                     }
145                     break;
146                 }
147             }
148 
149             while (iterator.nextPosition()) {
150                 if (filter.acceptPosition(iterator) === FILTER_ACCEPT) {
151                     if (roundDirection(NEXT, iterator.container(), iterator.unfilteredDomOffset())) {
152                         return true;
153                     }
154                     break;
155                 }
156             }
157 
158             return false;
159         }
160 
161         /**
162          * Convert the supplied DOM node & offset pair into it's equivalent steps from root
163          * If the node & offset is not in an accepted location, the
164          * roundDirection delegate is used to choose between rounding up or
165          * rounding down to the nearest step. If not provided, the default
166          * behaviour is to round down.
167          * @param {!Node} node
168          * @param {!number} offset
169          * @param {function(!core.StepDirection, !Node, !number):!boolean=} roundDirection
170          * @return {!number}
171          */
172         this.convertDomPointToSteps = function (node, offset, roundDirection) {
173             var stepsFromRoot,
174                 beforeRoot,
175                 destinationNode,
176                 destinationOffset,
177                 rounding = 0,
178                 isStep;
179 
180             if (!domUtils.containsNode(rootNode, node)) {
181                 beforeRoot = domUtils.comparePoints(rootNode, 0, node, offset) < 0;
182                 node = /**@type{!Node}*/(rootNode);
183                 offset = beforeRoot ? 0 : /**@type{!Element}*/(rootNode).childNodes.length;
184             }
185 
186             iterator.setUnfilteredPosition(node, offset);
187             // if the user has set provided a rounding selection delegate, use that to select the previous or next
188             // step if the (node, offset) position is not accepted by the filter
189             if (!roundToPreferredStep(iterator, roundDirection)) {
190                 // The rounding selection delegate rejected both. Revert back to the previous step
191                 iterator.setUnfilteredPosition(node, offset);
192             }
193 
194             // Get the iterator equivalent position of the current node & offset
195             // This ensures the while loop will match the exact container and offset during iteration
196             destinationNode = iterator.container();
197             destinationOffset = iterator.unfilteredDomOffset();
198 
199             stepsFromRoot = stepsCache.setToClosestDomPoint(destinationNode, destinationOffset, iterator);
200             if (domUtils.comparePoints(iterator.container(), iterator.unfilteredDomOffset(), destinationNode, destinationOffset) < 0) {
201                 // Special case: the requested DOM point is between the bookmark node and walkable step it represents
202                 return stepsFromRoot > 0 ? stepsFromRoot - 1 : stepsFromRoot;
203             }
204 
205             while (!(iterator.container() === destinationNode && iterator.unfilteredDomOffset() === destinationOffset)
206                     && iterator.nextPosition()) {
207                 isStep = filter.acceptPosition(iterator) === FILTER_ACCEPT;
208                 if (isStep) {
209                     stepsFromRoot += 1;
210                 }
211                 updateCache(stepsFromRoot, iterator, isStep);
212             }
213             return stepsFromRoot + rounding;
214         };
215 
216         /**
217          * Iterates over all available positions starting at the root node and primes the cache
218          * @return {undefined}
219          */
220         this.prime = function () {
221             var stepsFromRoot,
222                 isStep;
223 
224             stepsFromRoot = stepsCache.setToClosestStep(0, iterator);
225             while (iterator.nextPosition()) {
226                 isStep = filter.acceptPosition(iterator) === FILTER_ACCEPT;
227                 if (isStep) {
228                     stepsFromRoot += 1;
229                 }
230                 updateCache(stepsFromRoot, iterator, isStep);
231             }
232         };
233 
234         /**
235          * @param {!{position: !number}} eventArgs
236          * @return {undefined}
237          */
238         this.handleStepsInserted = function (eventArgs) {
239             // Old position = position
240             // New position = position + length
241             // E.g., {position: 10, length: 1} indicates 10 => 10, New => 11, 11 => 12, 12 => 13
242             stepsCache.damageCacheAfterStep(eventArgs.position);
243         };
244 
245         /**
246          * @param {!{position: !number}} eventArgs
247          * @return {undefined}
248          */
249         this.handleStepsRemoved = function (eventArgs) {
250             // Old position = position + length
251             // New position = position
252             // E.g., {position: 10, length: 1} indicates 10 => 10, 11 => 10, 12 => 11
253 
254             // TODO OpRemoveText inaccurately reports the position making it necessary subtract 1
255             // Paragraph merge behaviours might result in the paragraph exactly at the reported position being
256             // replaced by a later paragraph. Conceptually, this means the last unmodified position is
257             // actually 1 step prior to the replace paragraph.
258             stepsCache.damageCacheAfterStep(eventArgs.position - 1);
259         };
260 
261         function init() {
262             stepsCache = new ops.StepsCache(rootNode, bucketSize, roundUpToStep);
263         }
264         init();
265     };
266 }());
267