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