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, odf, runtime, core*/
 26 
 27 /**
 28  * Class for applying a supplied text style to the given text nodes.
 29  * @constructor
 30  * @param {!odf.ObjectNameGenerator} objectNameGenerator Source for generating unique automatic style names
 31  * @param {!odf.Formatting} formatting Formatting retrieval and computation store
 32  * @param {!Node} automaticStyles Root element for automatic styles
 33  */
 34 odf.TextStyleApplicator = function TextStyleApplicator(objectNameGenerator, formatting, automaticStyles) {
 35     "use strict";
 36     var domUtils = core.DomUtils,
 37         /**@const*/
 38         textns = odf.Namespaces.textns,
 39         /**@const*/
 40         stylens = odf.Namespaces.stylens,
 41         /**@const*/
 42         textProperties = "style:text-properties",
 43         /**@const*/
 44         webodfns = "urn:webodf:names:scope";
 45 
 46     /**
 47      * @constructor
 48      * @param {!Object} info Style information
 49      */
 50     function StyleLookup(info) {
 51         var cachedAppliedStyles = {};
 52 
 53         /**
 54          * @param {!Object} expected
 55          * @param {Object|undefined} actual
 56          * @return {boolean}
 57          */
 58         function compare(expected, actual) {
 59             if (typeof expected === "object" && typeof actual === "object") {
 60                 return Object.keys(expected).every(function (key) {
 61                     return compare(expected[key], actual[key]);
 62                 });
 63             }
 64             return expected === actual;
 65         }
 66 
 67         /**
 68          * @param {!CharacterData} textNode
 69          * @return {boolean}
 70          */
 71         this.isStyleApplied = function (textNode) {
 72             // TODO can direct style to element just be removed somewhere to end up with desired style?
 73             var appliedStyle = formatting.getAppliedStylesForElement(textNode, cachedAppliedStyles).styleProperties;
 74             return compare(info, appliedStyle);
 75         };
 76     }
 77 
 78     /**
 79      * Responsible for maintaining a collection of creates auto-styles for
 80      * re-use on styling new containers.
 81      * @constructor
 82      * @param {!Object} info Style information
 83      */
 84     function StyleManager(info) {
 85         var /**@type{!Object.<string,!Element>}*/
 86             createdStyles = {};
 87 
 88         /**
 89          * @param {string} existingStyleName
 90          * @param {Document} document
 91          * @return {!Element}
 92          */
 93         function createDirectFormat(existingStyleName, document) {
 94             var derivedStyleInfo, derivedStyleNode;
 95 
 96             derivedStyleInfo = existingStyleName ? formatting.createDerivedStyleObject(existingStyleName, "text", info) : info;
 97             derivedStyleNode = document.createElementNS(stylens, "style:style");
 98             formatting.updateStyle(derivedStyleNode, derivedStyleInfo);
 99             derivedStyleNode.setAttributeNS(stylens, "style:name", objectNameGenerator.generateStyleName());
100             derivedStyleNode.setAttributeNS(stylens, "style:family", "text"); // The family will not have been specified if just using info
101             derivedStyleNode.setAttributeNS(webodfns, "scope", "document-content");
102             automaticStyles.appendChild(derivedStyleNode);
103             return derivedStyleNode;
104         }
105 
106         /**
107          * @param {string} existingStyleName
108          * @param {Document} document
109          * @return {string}
110          */
111         function getDirectStyle(existingStyleName, document) {
112             existingStyleName = existingStyleName || "";
113             if (!createdStyles.hasOwnProperty(existingStyleName)) {
114                 createdStyles[existingStyleName] = createDirectFormat(existingStyleName, document);
115             }
116             return createdStyles[existingStyleName].getAttributeNS(stylens, "name");
117         }
118 
119         /**
120          * Applies the required styling changes to the supplied container.
121          * @param {!Element} container
122          */
123         this.applyStyleToContainer = function (container) {
124             // container will be a span by this point, and the style-name can only appear in one place
125             var name = getDirectStyle(container.getAttributeNS(textns, "style-name"), container.ownerDocument);
126             container.setAttributeNS(textns, "text:style-name", name);
127         };
128     }
129 
130     /**
131      * Returns true if the passed in node is an ODT text span
132      * @param {!Node} node
133      * @return {!boolean}
134      */
135     function isTextSpan(node) {
136         return node.localName === "span" && node.namespaceURI === textns;
137     }
138 
139     /**
140      * Moves the specified node and all further siblings within the outer range into a new standalone container
141      * @param {!CharacterData} startNode Node to start movement to new container
142      * @param {!Range} range style application bounds
143      * @return {!Element}  Returns the container node that is to be restyled
144      */
145     function moveToNewSpan(startNode, range) {
146         var document = startNode.ownerDocument,
147             originalContainer = /**@type{!Element}*/(startNode.parentNode),
148             styledContainer,
149             trailingContainer,
150             moveTrailing,
151             node,
152             nextNode,
153             loopGuard = new core.LoopWatchDog(10000),
154             /**@type{!Array.<!Node>}*/
155             styledNodes = [];
156 
157         // Starting at the startNode, iterate forward until leaving the affected range
158         styledNodes.push(startNode);
159         node = startNode.nextSibling;
160         // Need to fetch all nodes to move before starting to move any, in case
161         // the range actually reference one of the nodes this loop is about to relocate
162         while (node && domUtils.rangeContainsNode(range, node)) {
163             loopGuard.check();
164             styledNodes.push(node);
165             node = node.nextSibling;
166         }
167 
168         // Do we need a new style container?
169         if (!isTextSpan(originalContainer)) {
170             // Yes, text node has no wrapping span
171             styledContainer = document.createElementNS(textns, "text:span");
172             originalContainer.insertBefore(styledContainer, startNode);
173             moveTrailing = false;
174         } else if (startNode.previousSibling
175                 && !domUtils.rangeContainsNode(range, /**@type{!Element}*/(originalContainer.firstChild))) {
176             // Yes, text node has prior siblings that are not styled
177             // TODO what elements should be stripped when the clone occurs?
178             styledContainer = originalContainer.cloneNode(false);
179             originalContainer.parentNode.insertBefore(styledContainer, originalContainer.nextSibling);
180             moveTrailing = true;
181         } else {
182             // No, repossess the current container
183             styledContainer = originalContainer;
184             moveTrailing = true;
185         }
186 
187         styledNodes.forEach(function (n) {
188             if (n.parentNode !== styledContainer) {
189                 styledContainer.appendChild(n);
190             }
191         });
192 
193         // Any trailing nodes?
194         if (node && moveTrailing) {
195             // Yes, create a trailing container
196             trailingContainer = styledContainer.cloneNode(false);
197             styledContainer.parentNode.insertBefore(trailingContainer, styledContainer.nextSibling);
198 
199             // Starting at the first node outside the affected range, move each node across
200             while (node) {
201                 loopGuard.check();
202                 nextNode = node.nextSibling;
203                 trailingContainer.appendChild(node);
204                 node = nextNode;
205             }
206         }
207 
208         // TODO clean up empty spans that are left behind
209         return /**@type {!Element}*/ (styledContainer);
210     }
211 
212     /**
213      * Apply the specified text style to the given text nodes
214      * @param {!Array.<!CharacterData>} textNodes
215      * @param {!Range} range style application bounds
216      * @param {!Object} info Style information. Only data within "style:text-properties" will be considered and applied
217      * @return {undefined}
218      */
219     this.applyStyle = function (textNodes, range, info) {
220         var textPropsOnly = {},
221             isStyled,
222             container,
223             /**@type{!StyleManager}*/
224             styleCache,
225             /**@type{!StyleLookup}*/
226             styleLookup;
227         runtime.assert(info && info.hasOwnProperty(textProperties), "applyStyle without any text properties");
228         textPropsOnly[textProperties] = info[textProperties];
229         styleCache = new StyleManager(textPropsOnly);
230         styleLookup = new StyleLookup(textPropsOnly);
231 
232         /**
233          * @param {!CharacterData} n
234          */
235         function apply(n) {
236             isStyled = styleLookup.isStyleApplied(n);
237             if (isStyled === false) {
238                 container = moveToNewSpan(n, range);
239                 styleCache.applyStyleToContainer(container);
240             }
241         }
242         textNodes.forEach(apply);
243     };
244 };
245