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