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             /**@type{!Element}*/
149             styledContainer,
150             trailingContainer,
151             moveTrailing,
152             node,
153             nextNode,
154             loopGuard = new core.LoopWatchDog(10000),
155             /**@type{!Array.<!Node>}*/
156             styledNodes = [];
157 
158         // Starting at the startNode, iterate forward until leaving the affected range
159         styledNodes.push(startNode);
160         node = startNode.nextSibling;
161         // Need to fetch all nodes to move before starting to move any, in case
162         // the range actually reference one of the nodes this loop is about to relocate
163         while (node && domUtils.rangeContainsNode(range, node)) {
164             loopGuard.check();
165             styledNodes.push(node);
166             node = node.nextSibling;
167         }
168 
169         // Do we need a new style container?
170         if (!isTextSpan(originalContainer)) {
171             // Yes, text node has no wrapping span
172             styledContainer = document.createElementNS(textns, "text:span");
173             originalContainer.insertBefore(styledContainer, startNode);
174             moveTrailing = false;
175         } else if (startNode.previousSibling
176                 && !domUtils.rangeContainsNode(range, /**@type{!Element}*/(originalContainer.firstChild))) {
177             // Yes, text node has prior siblings that are not styled
178             // TODO what elements should be stripped when the clone occurs?
179             styledContainer = originalContainer.cloneNode(false);
180             originalContainer.parentNode.insertBefore(styledContainer, originalContainer.nextSibling);
181             moveTrailing = true;
182         } else {
183             // No, repossess the current container
184             styledContainer = originalContainer;
185             moveTrailing = true;
186         }
187 
188         styledNodes.forEach(function (n) {
189             if (n.parentNode !== styledContainer) {
190                 styledContainer.appendChild(n);
191             }
192         });
193 
194         // Any trailing nodes?
195         if (node && moveTrailing) {
196             // Yes, create a trailing container
197             trailingContainer = styledContainer.cloneNode(false);
198             styledContainer.parentNode.insertBefore(trailingContainer, styledContainer.nextSibling);
199 
200             // Starting at the first node outside the affected range, move each node across
201             while (node) {
202                 loopGuard.check();
203                 nextNode = node.nextSibling;
204                 trailingContainer.appendChild(node);
205                 node = nextNode;
206             }
207         }
208 
209         // TODO clean up empty spans that are left behind
210         return /**@type {!Element}*/ (styledContainer);
211     }
212 
213     /**
214      * Apply the specified text style to the given text nodes
215      * @param {!Array.<!CharacterData>} textNodes
216      * @param {!Range} range style application bounds
217      * @param {!Object} info Style information. Only data within "style:text-properties" will be considered and applied
218      * @return {undefined}
219      */
220     this.applyStyle = function (textNodes, range, info) {
221         var textPropsOnly = {},
222             isStyled,
223             container,
224             /**@type{!StyleManager}*/
225             styleCache,
226             /**@type{!StyleLookup}*/
227             styleLookup;
228         runtime.assert(info && info.hasOwnProperty(textProperties), "applyStyle without any text properties");
229         textPropsOnly[textProperties] = info[textProperties];
230         styleCache = new StyleManager(textPropsOnly);
231         styleLookup = new StyleLookup(textPropsOnly);
232 
233         /**
234          * @param {!CharacterData} n
235          */
236         function apply(n) {
237             isStyled = styleLookup.isStyleApplied(n);
238             if (isStyled === false) {
239                 container = moveToNewSpan(n, range);
240                 styleCache.applyStyleToContainer(container);
241             }
242         }
243         textNodes.forEach(apply);
244     };
245 };
246