1 /** 2 * Copyright (C) 2010-2014 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 odf, core, runtime*/ 26 27 (function () { 28 "use strict"; 29 30 var /**@const 31 @type{!string}*/ 32 fons = odf.Namespaces.fons, 33 /**@const 34 @type{!string}*/ 35 stylens = odf.Namespaces.stylens, 36 /**@const 37 @type{!string}*/ 38 textns = odf.Namespaces.textns, 39 /**@const 40 @type{!string}*/ 41 xmlns = odf.Namespaces.xmlns, 42 /**@const 43 @type{!string}*/ 44 helperns = "urn:webodf:names:helper", 45 /**@const 46 @type{!string}*/ 47 listCounterIdSuffix = "webodf-listLevel", 48 /**@const 49 @type{!Object.<string,string>}*/ 50 stylemap = { 51 '1': 'decimal', 52 'a': 'lower-latin', 53 'A': 'upper-latin', 54 'i': 'lower-roman', 55 'I': 'upper-roman' 56 }; 57 58 /** 59 * Appends the rule into the stylesheets and logs any errors that occur 60 * @param {!CSSStyleSheet} styleSheet 61 * @param {!string} rule 62 * @return {undefined} 63 */ 64 function appendRule(styleSheet, rule) { 65 try { 66 styleSheet.insertRule(rule, styleSheet.cssRules.length); 67 } catch (/**@type{!DOMException}*/e) { 68 runtime.log("cannot load rule: " + rule + " - " + e); 69 } 70 } 71 72 /** 73 * Holds the current state of parsing the text:list elements in the DOM 74 * @param {!Object.<!string, !string>} contentRules 75 * @param {!Array.<!string>} continuedCounterIdStack 76 * @constructor 77 * @struct 78 */ 79 function ParseState(contentRules, continuedCounterIdStack) { 80 /** 81 * The number of list counters created for a list 82 * This is just a number appended to the list counter identifier to make it unique within the list 83 * @type {!number} 84 */ 85 this.listCounterCount = 0; 86 87 /** 88 * The CSS generated content rule keyed by list level 89 * @type {!Object.<!string, !string>} 90 */ 91 this.contentRules = contentRules; 92 93 /** 94 * The stack of counters for the list being processed 95 * @type {!Array.<!string>} 96 */ 97 this.counterIdStack = []; 98 99 /** 100 * The stack of counters the list should continue from if any 101 * @type {!Array.<!string>} 102 */ 103 this.continuedCounterIdStack = continuedCounterIdStack; 104 } 105 106 /** 107 * Assigns globally unique CSS list counters to each text:list element in the document. 108 * The reason a global list counter is required is due to how the scope of CSS counters works 109 * which is described here http://www.w3.org/TR/CSS21/generate.html#scope 110 * 111 * The relevant part is that the scope of the counter applies to the element that the counter-reset rule 112 * was applied to and any children or siblings of that element. Applying another counter-reset rule to the 113 * same counter resets this scope and previous values of the counter are lost. These values are also inaccessible 114 * if we inspect the value of the counter outside of the scope and we simply get the default value of zero. 115 * 116 * The above is important for the case of continued numbering combined with multi-level list numbering. 117 * Multi-level lists use a separate counter for each list level and joins each counter value together. 118 * Continued numbering takes the list counter from the list we want to continue and uses it for the list 119 * that is being continued. Combining these two we get the approach of taking the list counter at each list level 120 * from the list that is being continued and then using these counters at each level in the continued list. 121 * 122 * However the scope rules prevent us from continuing counters at any level deeper than the first level and 123 * this behaviour is illustrated in an example of some list content below. 124 * <office:document> 125 * <text:list> 126 * <text:list-item> counter: level1 value: 1 127 * <text:list> 128 * <text:list-item><text:p>Item</text:p></text:list-item> counter: level2 value: 1 129 * </text:list> 130 * </text:list-item> 131 * </text:list> 132 * other doc content 133 * <text:list text:continue-numbering="true"> 134 * <text:list-item> 135 * <text:list> 136 * <text:list-item><text:p>Item</text:p></text:list-item> counter: level2 value: 0 137 * </text:list> 138 * </text:list-item> 139 * </text:list> 140 * </office:document> 141 * 142 * The solution to this was to hoist the counter initialisation up to the document level so that the counter 143 * scope applies to all lists in the document. Then each text:list element is given a unique counter by default. 144 * Having unique counters is only really required for continuing a list based on its xml:id but having it for 145 * all lists makes the code simpler and reduces the amount of CSS rules being overridden. Hence we end up with a 146 * list counter setup as below. 147 * <office:document> counter-reset: list1-1 list1-2 148 * <text:list> 149 * <text:list-item> counter: list1-1 value: 1 150 * <text:list> 151 * <text:list-item><text:p>Item</text:p></text:list-item> counter: list1-2 value: 1 152 * </text:list> 153 * </text:list-item> 154 * </text:list> 155 * other doc content 156 * <text:list text:continue-numbering="true"> 157 * <text:list-item> 158 * <text:list> 159 * <text:list-item><text:p>Item</text:p></text:list-item> counter: list1-2 value: 2 160 * </text:list> 161 * </text:list-item> 162 * </text:list> 163 * </office:document> 164 * 165 * @param {!CSSStyleSheet} styleSheet 166 * @constructor 167 */ 168 function UniqueListCounter(styleSheet) { 169 var /**@type{!number}*/ 170 customListIdIndex = 0, 171 /**@type{!string}*/ 172 globalCounterResetRule = "", 173 /**@type{!Object.<!string,!Array.<!string>>}*/ 174 counterIdStacks = {}; 175 176 /** 177 * Gets the stack of list counters for the given list. 178 * Counter stacks are keyed by the list counter id of the first list level. 179 * Returns a deep copy of the counter stack so it can be modified 180 * @param {!Element|undefined} list 181 * @return {!Array.<!string>} 182 */ 183 function getCounterIdStack(list) { 184 var counterId, 185 stack = []; 186 187 if (list) { 188 counterId = list.getAttributeNS(helperns, "counter-id"); 189 stack = counterIdStacks[counterId].slice(0); 190 } 191 return stack; 192 } 193 194 /** 195 * Assigns a unique global CSS list counter to this text:list element 196 * @param {!string} topLevelListId This is used to generate a unique identifier for this element 197 * @param {!Element} listElement This is always a text:list element 198 * @param {!number} listLevel 199 * @param {!ParseState} parseState 200 * @return {undefined} 201 */ 202 function createCssRulesForList(topLevelListId, listElement, listLevel, parseState) { 203 var /**@type{!string}*/ 204 newListSelectorId, 205 newListCounterId, 206 newRule, 207 contentRule, 208 i; 209 210 // increment counters and create a new identifier for this text:list element 211 // this identifier will be used as the CSS counter name if this list is not continuing another list 212 parseState.listCounterCount += 1; 213 newListSelectorId = topLevelListId + "-level" + listLevel + "-" + parseState.listCounterCount; 214 listElement.setAttributeNS(helperns, "counter-id", newListSelectorId); 215 216 // if we need to continue from a previous list then get the counter from the stack 217 // of the continued list and use it as the counter for this list element 218 newListCounterId = parseState.continuedCounterIdStack.shift(); 219 if (!newListCounterId) { 220 newListCounterId = newListSelectorId; 221 222 // add the newly created counter to the counter reset rule so it can be 223 // initialised later once we have parsed all the lists in the document. 224 // In the case of a multi-level list with no items the counter increment rule 225 // will not apply. To fix this issue we initialise the counters to a value of 1 226 // instead of the default of 0. 227 globalCounterResetRule += newListSelectorId + ' 1 '; 228 229 // CSS counters increment the value before displaying the rendered list label. This is not an issue but as 230 // we initialise the counters to a value of 1 above to handle lists with no list items it means that 231 // lists that actually have list items will all start with the counter value of 2 which is not desirable. 232 // To fix this we apply another CSS rule here that overrides the counter increment rule above and 233 // prevents incrementing the counter on the FIRST list item that has content (AKA a visible list label). 234 newRule = 'text|list[webodfhelper|counter-id="' + newListSelectorId + '"]'; 235 newRule += ' > text|list-item:first-child > :not(text|list):first-child:before'; 236 newRule += '{'; 237 // Due to https://bugs.webkit.org/show_bug.cgi?id=84985 a value of "none" is ignored by some version of WebKit 238 // (specifically the ones shipped with the Cocoa frameworks on OSX 10.7 + 10.8). 239 // Override the counter-increment on this counter by name to workaround this 240 newRule += 'counter-increment: ' + newListCounterId + ' 0;'; 241 newRule += '}'; 242 appendRule(styleSheet, newRule); 243 } 244 245 // remove any counters from the stack that are deeper than the current list level 246 // and push the newly created or continued counter on to the stack 247 while (parseState.counterIdStack.length >= listLevel) { 248 parseState.counterIdStack.pop(); 249 } 250 parseState.counterIdStack.push(newListCounterId); 251 252 // substitute the unique list counters in for each level up to the current one 253 // this only replaces the first occurrence in the string as the generated rule 254 // will have a different counter for each list level and multi level counter rules 255 // are created by joining counters from different levels together 256 contentRule = parseState.contentRules[listLevel.toString()] || ""; 257 for (i = 1; i <= listLevel; i += 1) { 258 contentRule = contentRule.replace(i + listCounterIdSuffix, parseState.counterIdStack[i - 1]); 259 } 260 261 // Apply the counter increment to EVERY list item in this list that has content (AKA a visible list label) 262 newRule = 'text|list[webodfhelper|counter-id="' + newListSelectorId + '"]'; 263 newRule += ' > text|list-item > :not(text|list):first-child:before'; 264 newRule += '{'; 265 newRule += contentRule; 266 newRule += 'counter-increment: ' + newListCounterId + ';'; 267 newRule += '}'; 268 appendRule(styleSheet, newRule); 269 } 270 271 /** 272 * Takes an element and parses it and its subtree for any text:list elements. 273 * The text:list elements then have CSS rules applied that give each one 274 * a unique global CSS counter for the purpose of list numbering. 275 * @param {!string} topLevelListId 276 * @param {!Element} element 277 * @param {!number} listLevel 278 * @param {!ParseState} parseState 279 * @return {undefined} 280 */ 281 function iterateOverChildListElements(topLevelListId, element, listLevel, parseState) { 282 var isListElement = element.namespaceURI === textns && element.localName === "list", 283 isListItemElement = element.namespaceURI === textns && element.localName === "list-item", 284 childElement; 285 286 // don't continue iterating over elements that aren't text:list or text:list-item 287 if (!isListElement && !isListItemElement) { 288 parseState.continuedCounterIdStack = []; 289 return; 290 } 291 292 if (isListElement) { 293 listLevel += 1; 294 createCssRulesForList(topLevelListId, element, listLevel, parseState); 295 } 296 297 childElement = element.firstElementChild; 298 while (childElement) { 299 iterateOverChildListElements(topLevelListId, childElement, listLevel, parseState); 300 childElement = childElement.nextElementSibling; 301 } 302 } 303 304 /** 305 * Takes a text:list element and creates CSS counter rules used for numbering 306 * @param {!Object.<!string, !string>} contentRules 307 * @param {!Element} list 308 * @param {!Element=} continuedList 309 * @return {undefined} 310 */ 311 this.createCounterRules = function (contentRules, list, continuedList) { 312 var /**@type{!string}*/ 313 listId = list.getAttributeNS(xmlns, "id"), 314 currentParseState = new ParseState(contentRules, getCounterIdStack(continuedList)); 315 316 // ensure there is a unique identifier for each list if it does not have one 317 if (!listId) { 318 customListIdIndex += 1; 319 listId = "X" + customListIdIndex; 320 } else { 321 listId = "Y" + listId; 322 } 323 324 iterateOverChildListElements(listId, list, 0, currentParseState); 325 326 counterIdStacks[listId + "-level1-1"] = currentParseState.counterIdStack; 327 }; 328 329 /** 330 * Initialises all CSS counters created so far by this UniqueListCounter with a counter-reset rule. 331 * Calling this method twice will cause the previous counter reset CSS rule to be overridden 332 * @return {undefined} 333 */ 334 this.initialiseCreatedCounters = function () { 335 var newRule; 336 337 newRule = 'office|document'; 338 newRule += '{'; 339 newRule += 'counter-reset: ' + globalCounterResetRule + ';'; 340 newRule += "}"; 341 appendRule(styleSheet, newRule); 342 }; 343 } 344 345 /** 346 * @constructor 347 */ 348 odf.ListStyleToCss = function ListStyleToCss() { 349 350 var cssUnits = new core.CSSUnits(), 351 odfUtils = odf.OdfUtils; 352 353 /** 354 * Takes a value with a valid CSS unit and converts it to a CSS pixel value 355 * @param {!string} value 356 * @return {!number} 357 */ 358 function convertToPxValue(value) { 359 var parsedLength = odfUtils.parseLength(value); 360 if (!parsedLength) { 361 runtime.log("Could not parse value '" + value + "'."); 362 // Return 0 as fallback, might have least bad results if used 363 return 0; 364 } 365 return cssUnits.convert(parsedLength.value, parsedLength.unit, "px"); 366 } 367 368 /** 369 * Return the supplied value with any backslashes escaped, and double-quotes escaped 370 * @param {!string} value 371 * @return {!string} 372 */ 373 function escapeCSSString(value) { 374 return value.replace(/\\/g, "\\\\").replace(/"/g, "\\\""); 375 } 376 377 /** 378 * Determines whether the list element style-name matches the style-name we require 379 * @param {!Element|undefined} list 380 * @param {!string} matchingStyleName 381 * @return {!boolean} 382 */ 383 function isMatchingListStyle(list, matchingStyleName) { 384 var styleName; 385 if (list) { 386 styleName = list.getAttributeNS(textns, "style-name"); 387 } 388 return styleName === matchingStyleName; 389 } 390 391 /** 392 * Gets the CSS content for a numbered list 393 * @param {!Element} node 394 * @return {!string} 395 */ 396 function getNumberRule(node) { 397 var style = node.getAttributeNS(stylens, "num-format"), 398 /**@type{!string}*/ 399 suffix = node.getAttributeNS(stylens, "num-suffix") || "", 400 /**@type{!string}*/ 401 prefix = node.getAttributeNS(stylens, "num-prefix") || "", 402 /**@type{!string}*/ 403 content = "", 404 textLevel = node.getAttributeNS(textns, "level"), 405 displayLevels = node.getAttributeNS(textns, "display-levels"); 406 if (prefix) { 407 // Content needs to be on a new line if it contains slashes due to a bug in older versions of webkit 408 // E.g., the one used in the qt runtime tests - https://bugs.webkit.org/show_bug.cgi?id=35010 409 content += '"' + escapeCSSString(prefix) + '"\n'; 410 } 411 if (stylemap.hasOwnProperty(style)) { 412 textLevel = textLevel ? parseInt(textLevel, 10) : 1; 413 displayLevels = displayLevels ? parseInt(displayLevels, 10) : 1; 414 415 // as we might want to display a subset of the counters 416 // we assume a different counter for each list level 417 // and concatenate them for multi level lists 418 // https://wiki.openoffice.org/wiki/Number_labels 419 while (displayLevels > 0) { 420 content += " counter(" + (textLevel - displayLevels + 1) + listCounterIdSuffix + "," + stylemap[style] + ")"; 421 if (displayLevels > 1) { 422 content += '"."'; 423 } 424 displayLevels -= 1; 425 } 426 } else if (style) { 427 content += ' "' + style + '"'; 428 } else { 429 content += ' ""'; 430 } 431 return 'content:' + content + ' "' + escapeCSSString(suffix) + '"'; 432 } 433 434 /** 435 * Gets the CSS content for a image bullet list 436 * @return {!string} 437 */ 438 function getImageRule() { 439 return "content: none"; 440 } 441 442 /** 443 * Gets the CSS content for a bullet list 444 * @param {!Element} node 445 * @return {!string} 446 */ 447 function getBulletRule(node) { 448 var bulletChar = node.getAttributeNS(textns, "bullet-char"); 449 return 'content: "' + escapeCSSString(bulletChar) + '"'; 450 } 451 452 /** 453 * Gets the CSS generated content rule for the list style 454 * @param {!Element} node 455 * @return {!string} 456 */ 457 function getContentRule(node) { 458 var contentRule = "", 459 listLevelProps, 460 listLevelPositionSpaceMode, 461 listLevelLabelAlign, 462 followedBy; 463 464 if (node.localName === "list-level-style-number") { 465 contentRule = getNumberRule(node); 466 } else if (node.localName === "list-level-style-image") { 467 contentRule = getImageRule(); 468 } else if (node.localName === "list-level-style-bullet") { 469 contentRule = getBulletRule(node); 470 } 471 472 listLevelProps = /**@type{!Element}*/(node.getElementsByTagNameNS(stylens, "list-level-properties")[0]); 473 if (listLevelProps) { 474 listLevelPositionSpaceMode = listLevelProps.getAttributeNS(textns, "list-level-position-and-space-mode"); 475 476 if (listLevelPositionSpaceMode === "label-alignment") { 477 listLevelLabelAlign = /**@type{!Element}*/(listLevelProps.getElementsByTagNameNS(stylens, "list-level-label-alignment")[0]); 478 if (listLevelLabelAlign) { 479 followedBy = listLevelLabelAlign.getAttributeNS(textns, "label-followed-by"); 480 } 481 482 if (followedBy === "space") { 483 contentRule += ' "\\a0"'; 484 } 485 } 486 } 487 488 // Content needs to be on a new line if it contains slashes due to a bug in older versions of webkit 489 // E.g., the one used in the qt runtime tests - https://bugs.webkit.org/show_bug.cgi?id=35010 490 return '\n' + contentRule + ';\n'; 491 } 492 493 /** 494 * Takes a text:list-style element and returns the generated CSS 495 * content rules for each list level in the list style 496 * @param {!Element} listStyleNode 497 * @return {!Object.<!string, !string>} 498 */ 499 function getAllContentRules(listStyleNode) { 500 var childNode = listStyleNode.firstElementChild, 501 level, 502 rules = {}; 503 504 while (childNode) { 505 level = childNode.getAttributeNS(textns, "level"); 506 level = level && parseInt(level, 10); 507 rules[level] = getContentRule(childNode); 508 childNode = childNode.nextElementSibling; 509 } 510 return rules; 511 } 512 513 /** 514 * In label-width-and-position mode of specifying list layout the margin and indent specified in 515 * the paragraph style is additive to the layout specified in the list style. 516 * 517 * fo:margin-left text:space-before fo:text-indent +-----------+ 518 * +---------------->+------------------>+----------------->| label | LIST TEXT 519 * +-----------+ 520 * +---------------->+------------------>+-------------------->LIST TEXT LIST TEXT LIST TEXT 521 * text:min-label-width 522 * 523 * To get this additive behaviour we calculate an offset from the left side of the page which is 524 * the space-before + min-label-width. We then apply this offset to each text:list-item 525 * element and apply the negative value of the offset to each text:list element. This allows the positioning 526 * provided in the list style to apply relative to the paragraph style as we desired. Then on each 527 * ::before pseudo-element which holds the label we apply the negative value of the min-label-width to complete 528 * the alignment from the left side of the page. We then apply the min-label-distance as padding to the right 529 * of the ::before psuedo-element to complete the list label placement. 530 * 531 * For the label-alignment mode the paragraph style overrides the list style but we specify offsets for 532 * the text:list and text:list-item elements to keep the code consistent between the modes 533 * 534 * Diagram and implementation based on: https://wiki.openoffice.org/wiki/Number_layout 535 * 536 * @param {!CSSStyleSheet} styleSheet 537 * @param {!string} name 538 * @param {!Element} node 539 * @return {undefined} 540 */ 541 function addListStyleRule(styleSheet, name, node) { 542 var selector = 'text|list[text|style-name="' + name + '"]', 543 level = node.getAttributeNS(textns, "level"), 544 selectorLevel, 545 listItemRule, 546 listLevelProps, 547 listLevelPositionSpaceMode, 548 listLevelLabelAlign, 549 listIndent, 550 textAlign, 551 bulletWidth, 552 labelDistance, 553 bulletIndent, 554 followedBy, 555 leftOffset; 556 557 // style:list-level-properties is an optional element. Since the rest of this function 558 // depends on its existence, return from it if it is not found. 559 listLevelProps = /**@type{!Element|undefined}*/(node.getElementsByTagNameNS(stylens, "list-level-properties")[0]); 560 listLevelPositionSpaceMode = listLevelProps && listLevelProps.getAttributeNS(textns, "list-level-position-and-space-mode"); 561 listLevelLabelAlign = /**@type{!Element|undefined}*/(listLevelProps) && 562 /**@type{!Element|undefined}*/(listLevelProps.getElementsByTagNameNS(stylens, "list-level-label-alignment")[0]); 563 564 // calculate CSS selector based on list level 565 level = level && parseInt(level, 10); 566 selectorLevel = level; 567 while (selectorLevel > 1) { 568 selector += ' > text|list-item > text|list'; 569 selectorLevel -= 1; 570 } 571 572 // TODO: fo:text-align is only an optional attribute with <style:list-level-properties>, 573 // needs to be found what should be done if not present. For now falling back to "left" 574 textAlign = (listLevelProps && listLevelProps.getAttributeNS(fons, "text-align")) || "left"; 575 // convert the start and end text alignments to left and right as 576 // IE does not support the start and end values for text alignment 577 switch (textAlign) { 578 case "end": 579 textAlign = "right"; 580 break; 581 case "start": 582 textAlign = "left"; 583 break; 584 } 585 586 // get relevant properties from the style based on the list label positioning mode 587 if (listLevelPositionSpaceMode === "label-alignment") { 588 // TODO: fetch the margin and indent from the paragraph style if it is defined there 589 // http://docs.oasis-open.org/office/v1.2/os/OpenDocument-v1.2-os-part1.html#element-style_list-level-label-alignment 590 // for now just fallback to "0px" if not defined on <style:list-level-label-alignment> 591 listIndent = (listLevelLabelAlign && listLevelLabelAlign.getAttributeNS(fons, "margin-left")) || "0px"; 592 bulletIndent = (listLevelLabelAlign && listLevelLabelAlign.getAttributeNS(fons, "text-indent")) || "0px"; 593 followedBy = listLevelLabelAlign && listLevelLabelAlign.getAttributeNS(textns, "label-followed-by"); 594 leftOffset = convertToPxValue(listIndent); 595 596 } else { 597 // this block is entered if list-level-position-and-space-mode 598 // has the value label-width-and-position or is not present 599 // TODO: fallback values should be read from parent styles or (system) defaults 600 listIndent = (listLevelProps && listLevelProps.getAttributeNS(textns, "space-before")) || "0px"; 601 bulletWidth = (listLevelProps && listLevelProps.getAttributeNS(textns, "min-label-width")) || "0px"; 602 labelDistance = (listLevelProps && listLevelProps.getAttributeNS(textns, "min-label-distance")) || "0px"; 603 leftOffset = convertToPxValue(listIndent) + convertToPxValue(bulletWidth); 604 } 605 606 listItemRule = selector + ' > text|list-item'; 607 listItemRule += '{'; 608 listItemRule += 'margin-left: ' + leftOffset + 'px;'; 609 listItemRule += '}'; 610 appendRule(styleSheet, listItemRule); 611 612 listItemRule = selector + ' > text|list-item > text|list'; 613 listItemRule += '{'; 614 listItemRule += 'margin-left: ' + (-leftOffset) + 'px;'; 615 listItemRule += '}'; 616 appendRule(styleSheet, listItemRule); 617 618 // insert the list label before every immediate child of the list-item, except for lists 619 listItemRule = selector + ' > text|list-item > :not(text|list):first-child:before'; 620 listItemRule += '{'; 621 listItemRule += 'text-align: ' + textAlign + ';'; 622 listItemRule += 'display: inline-block;'; 623 624 if (listLevelPositionSpaceMode === "label-alignment") { 625 listItemRule += 'margin-left: ' + bulletIndent + ';'; 626 if (followedBy === "listtab") { 627 // TODO: remove this padding once text:label-followed-by="listtab" is implemented 628 // http://docs.oasis-open.org/office/v1.2/os/OpenDocument-v1.2-os-part1.html#attribute-text_label-followed-by 629 listItemRule += 'padding-right: 0.2cm;'; 630 } 631 } else { 632 listItemRule += 'min-width: ' + bulletWidth + ';'; 633 listItemRule += 'margin-left: ' + (parseFloat(bulletWidth) === 0 ? '' : '-') + bulletWidth + ';'; 634 listItemRule += 'padding-right: ' + labelDistance + ';'; 635 } 636 listItemRule += '}'; 637 appendRule(styleSheet, listItemRule); 638 } 639 640 /** 641 * Adds a CSS rule for every ODF list style 642 * @param {!CSSStyleSheet} styleSheet 643 * @param {!string} name 644 * @param {!Element} node 645 * @return {undefined} 646 */ 647 function addRule(styleSheet, name, node) { 648 var n = node.firstElementChild; 649 while (n) { 650 if (n.namespaceURI === textns) { 651 addListStyleRule(styleSheet, name, n); 652 } 653 n = n.nextElementSibling; 654 } 655 } 656 657 /** 658 * Adds new CSS rules based on any properties in 659 * the ODF list content if they affect the final style 660 * @param {!CSSStyleSheet} styleSheet 661 * @param {!Element} odfBody 662 * @param {!Object.<!string, !odf.StyleTreeNode>} listStyles 663 * @return {undefined} 664 */ 665 function applyContentBasedStyles(styleSheet, odfBody, listStyles) { 666 var lists = odfBody.getElementsByTagNameNS(textns, "list"), 667 listCounter = new UniqueListCounter(styleSheet), 668 list, 669 previousList, 670 continueNumbering, 671 continueListXmlId, 672 xmlId, 673 styleName, 674 contentRules, 675 listsWithXmlId = {}, 676 i; 677 678 for (i = 0; i < lists.length; i += 1) { 679 list = /**@type{!Element}*/(lists.item(i)); 680 styleName = list.getAttributeNS(textns, "style-name"); 681 682 // TODO: Handle default list style 683 // lists that have no text:style-name attribute defined and do not have a parent text:list that 684 // defines a style name use a default implementation defined style as per the spec 685 // http://docs.oasis-open.org/office/v1.2/os/OpenDocument-v1.2-os-part1.html#attribute-text_style-name_element-text_list 686 687 // lists that have no text:style-name attribute defined but do have a parent list that defines a 688 // style name will inherit that style and will be handled correctly as any text:list with a style defined 689 // will have CSS rules applied to its child text:list elements 690 if (styleName) { 691 continueNumbering = list.getAttributeNS(textns, "continue-numbering"); 692 continueListXmlId = list.getAttributeNS(textns, "continue-list"); 693 xmlId = list.getAttributeNS(xmlns, "id"); 694 695 // store the list keyed by the xml:id 696 if (xmlId) { 697 listsWithXmlId[xmlId] = list; 698 } 699 700 contentRules = getAllContentRules(listStyles[styleName].element); 701 702 // lists with different styles cannot be continued 703 // https://tools.oasis-open.org/issues/browse/OFFICE-3558 704 if (continueNumbering && !continueListXmlId && isMatchingListStyle(previousList, styleName)) { 705 listCounter.createCounterRules(contentRules, list, previousList); 706 } else if (continueListXmlId && isMatchingListStyle(listsWithXmlId[continueListXmlId], styleName)) { 707 listCounter.createCounterRules(contentRules, list, listsWithXmlId[continueListXmlId]); 708 } else { 709 listCounter.createCounterRules(contentRules, list); 710 } 711 previousList = list; 712 } 713 } 714 715 listCounter.initialiseCreatedCounters(); 716 } 717 718 /** 719 * Creates CSS styles from the given ODF list styles and applies them to the stylesheet 720 * @param {!CSSStyleSheet} styleSheet 721 * @param {!odf.StyleTree.Tree} styleTree 722 * @param {!Element} odfBody 723 * @return {undefined} 724 */ 725 this.applyListStyles = function (styleSheet, styleTree, odfBody) { 726 var styleFamilyTree, 727 node; 728 729 /*jslint sub:true*/ 730 // The available families are defined in StyleUtils.familyNamespacePrefixes. 731 styleFamilyTree = (styleTree["list"]); 732 /*jslint sub:false*/ 733 if (styleFamilyTree) { 734 Object.keys(styleFamilyTree).forEach(function (styleName) { 735 node = /**@type{!odf.StyleTreeNode}*/(styleFamilyTree[styleName]); 736 addRule(styleSheet, styleName, node.element); 737 }); 738 } 739 740 applyContentBasedStyles(styleSheet, odfBody, styleFamilyTree); 741 }; 742 }; 743 }()); 744 745