Added Android code
[wl-app.git] / Android / folioreader / src / main / assets / js / rangy-classapplier.js
1 /**
2  * Class Applier module for Rangy.
3  * Adds, removes and toggles classes on Ranges and Selections
4  *
5  * Part of Rangy, a cross-browser JavaScript range and selection library
6  * https://github.com/timdown/rangy
7  *
8  * Depends on Rangy core.
9  *
10  * Copyright 2015, Tim Down
11  * Licensed under the MIT license.
12  * Version: 1.3.0
13  * Build date: 10 May 2015
14  */
15 (function(factory, root) {
16     if (typeof define == "function" && define.amd) {
17         // AMD. Register as an anonymous module with a dependency on Rangy.
18         define(["./rangy-core"], factory);
19     } else if (typeof module != "undefined" && typeof exports == "object") {
20         // Node/CommonJS style
21         module.exports = factory( require("rangy") );
22     } else {
23         // No AMD or CommonJS support so we use the rangy property of root (probably the global variable)
24         factory(root.rangy);
25     }
26 })(function(rangy) {
27     rangy.createModule("ClassApplier", ["WrappedSelection"], function(api, module) {
28         var dom = api.dom;
29         var DomPosition = dom.DomPosition;
30         var contains = dom.arrayContains;
31         var util = api.util;
32         var forEach = util.forEach;
33
34
35         var defaultTagName = "span";
36         var createElementNSSupported = util.isHostMethod(document, "createElementNS");
37
38         function each(obj, func) {
39             for (var i in obj) {
40                 if (obj.hasOwnProperty(i)) {
41                     if (func(i, obj[i]) === false) {
42                         return false;
43                     }
44                 }
45             }
46             return true;
47         }
48
49         function trim(str) {
50             return str.replace(/^\s\s*/, "").replace(/\s\s*$/, "");
51         }
52
53         function classNameContainsClass(fullClassName, className) {
54             return !!fullClassName && new RegExp("(?:^|\\s)" + className + "(?:\\s|$)").test(fullClassName);
55         }
56
57         // Inefficient, inelegant nonsense for IE's svg element, which has no classList and non-HTML className implementation
58         function hasClass(el, className) {
59             if (typeof el.classList == "object") {
60                 return el.classList.contains(className);
61             } else {
62                 var classNameSupported = (typeof el.className == "string");
63                 var elClass = classNameSupported ? el.className : el.getAttribute("class");
64                 return classNameContainsClass(elClass, className);
65             }
66         }
67
68         function addClass(el, className,serializedHighlight) {
69             if (typeof el.classList == "object") {
70                 el.classList.add(className);
71                 el.setAttribute("id", serializedHighlight);
72                 el.setAttribute("onclick","callHighlightURL(this)");
73             } else {
74                 var classNameSupported = (typeof el.className == "string");
75                 var elClass = classNameSupported ? el.className : el.getAttribute("class");
76                 if (elClass) {
77                     if (!classNameContainsClass(elClass, className)) {
78                         elClass += " " + className;
79                     }
80                 } else {
81                     elClass = className;
82                 }
83                 if (classNameSupported) {
84                     el.className = elClass;
85                 } else {
86                     el.setAttribute("class", elClass);
87                 }
88             }
89         }
90
91         var removeClass = (function() {
92             function replacer(matched, whiteSpaceBefore, whiteSpaceAfter) {
93                 return (whiteSpaceBefore && whiteSpaceAfter) ? " " : "";
94             }
95
96             return function(el, className) {
97                 if (typeof el.classList == "object") {
98                     el.classList.remove(className);
99                     el.removeAttribute('onclick');
100                     el.removeAttribute('id');
101                 } else {
102                     var classNameSupported = (typeof el.className == "string");
103                     var elClass = classNameSupported ? el.className : el.getAttribute("class");
104                     elClass = elClass.replace(new RegExp("(^|\\s)" + className + "(\\s|$)"), replacer);
105                     if (classNameSupported) {
106                         el.className = elClass;
107                     } else {
108                         el.setAttribute("class", elClass);
109                     }
110                 }
111             };
112         })();
113
114         function getClass(el) {
115             var classNameSupported = (typeof el.className == "string");
116             return classNameSupported ? el.className : el.getAttribute("class");
117         }
118
119         function sortClassName(className) {
120             return className && className.split(/\s+/).sort().join(" ");
121         }
122
123         function getSortedClassName(el) {
124             return sortClassName( getClass(el) );
125         }
126
127         function haveSameClasses(el1, el2) {
128             return getSortedClassName(el1) == getSortedClassName(el2);
129         }
130
131         function hasAllClasses(el, className) {
132             var classes = className.split(/\s+/);
133             for (var i = 0, len = classes.length; i < len; ++i) {
134                 if (!hasClass(el, trim(classes[i]))) {
135                     return false;
136                 }
137             }
138             return true;
139         }
140
141         function canTextBeStyled(textNode) {
142             var parent = textNode.parentNode;
143             return (parent && parent.nodeType == 1 && !/^(textarea|style|script|select|iframe)$/i.test(parent.nodeName));
144         }
145
146         function movePosition(position, oldParent, oldIndex, newParent, newIndex) {
147             var posNode = position.node, posOffset = position.offset;
148             var newNode = posNode, newOffset = posOffset;
149
150             if (posNode == newParent && posOffset > newIndex) {
151                 ++newOffset;
152             }
153
154             if (posNode == oldParent && (posOffset == oldIndex  || posOffset == oldIndex + 1)) {
155                 newNode = newParent;
156                 newOffset += newIndex - oldIndex;
157             }
158
159             if (posNode == oldParent && posOffset > oldIndex + 1) {
160                 --newOffset;
161             }
162
163             position.node = newNode;
164             position.offset = newOffset;
165         }
166
167         function movePositionWhenRemovingNode(position, parentNode, index) {
168             if (position.node == parentNode && position.offset > index) {
169                 --position.offset;
170             }
171         }
172
173         function movePreservingPositions(node, newParent, newIndex, positionsToPreserve) {
174             // For convenience, allow newIndex to be -1 to mean "insert at the end".
175             if (newIndex == -1) {
176                 newIndex = newParent.childNodes.length;
177             }
178
179             var oldParent = node.parentNode;
180             var oldIndex = dom.getNodeIndex(node);
181
182             forEach(positionsToPreserve, function(position) {
183                 movePosition(position, oldParent, oldIndex, newParent, newIndex);
184             });
185
186             // Now actually move the node.
187             if (newParent.childNodes.length == newIndex) {
188                 newParent.appendChild(node);
189             } else {
190                 newParent.insertBefore(node, newParent.childNodes[newIndex]);
191             }
192         }
193
194         function removePreservingPositions(node, positionsToPreserve) {
195
196             var oldParent = node.parentNode;
197             var oldIndex = dom.getNodeIndex(node);
198
199             forEach(positionsToPreserve, function(position) {
200                 movePositionWhenRemovingNode(position, oldParent, oldIndex);
201             });
202
203             dom.removeNode(node);
204         }
205
206         function moveChildrenPreservingPositions(node, newParent, newIndex, removeNode, positionsToPreserve) {
207             var child, children = [];
208             while ( (child = node.firstChild) ) {
209                 movePreservingPositions(child, newParent, newIndex++, positionsToPreserve);
210                 children.push(child);
211             }
212             if (removeNode) {
213                 removePreservingPositions(node, positionsToPreserve);
214             }
215             return children;
216         }
217
218         function replaceWithOwnChildrenPreservingPositions(element, positionsToPreserve) {
219             return moveChildrenPreservingPositions(element, element.parentNode, dom.getNodeIndex(element), true, positionsToPreserve);
220         }
221
222         function rangeSelectsAnyText(range, textNode) {
223             var textNodeRange = range.cloneRange();
224             textNodeRange.selectNodeContents(textNode);
225
226             var intersectionRange = textNodeRange.intersection(range);
227             var text = intersectionRange ? intersectionRange.toString() : "";
228
229             return text != "";
230         }
231
232         function getEffectiveTextNodes(range) {
233             var nodes = range.getNodes([3]);
234
235             // Optimization as per issue 145
236
237             // Remove non-intersecting text nodes from the start of the range
238             var start = 0, node;
239             while ( (node = nodes[start]) && !rangeSelectsAnyText(range, node) ) {
240                 ++start;
241             }
242
243             // Remove non-intersecting text nodes from the start of the range
244             var end = nodes.length - 1;
245             while ( (node = nodes[end]) && !rangeSelectsAnyText(range, node) ) {
246                 --end;
247             }
248
249             return nodes.slice(start, end + 1);
250         }
251
252         function elementsHaveSameNonClassAttributes(el1, el2) {
253             if (el1.attributes.length != el2.attributes.length) return false;
254             for (var i = 0, len = el1.attributes.length, attr1, attr2, name; i < len; ++i) {
255                 attr1 = el1.attributes[i];
256                 name = attr1.name;
257                 if (name != "class") {
258                     attr2 = el2.attributes.getNamedItem(name);
259                     if ( (attr1 === null) != (attr2 === null) ) return false;
260                     if (attr1.specified != attr2.specified) return false;
261                     if (attr1.specified && attr1.nodeValue !== attr2.nodeValue) return false;
262                 }
263             }
264             return true;
265         }
266
267         function elementHasNonClassAttributes(el, exceptions) {
268             for (var i = 0, len = el.attributes.length, attrName; i < len; ++i) {
269                 attrName = el.attributes[i].name;
270                 if ( !(exceptions && contains(exceptions, attrName)) && el.attributes[i].specified && attrName != "class") {
271                     return true;
272                 }
273             }
274             return false;
275         }
276
277         var getComputedStyleProperty = dom.getComputedStyleProperty;
278         var isEditableElement = (function() {
279             var testEl = document.createElement("div");
280             return typeof testEl.isContentEditable == "boolean" ?
281                 function (node) {
282                     return node && node.nodeType == 1 && node.isContentEditable;
283                 } :
284                 function (node) {
285                     if (!node || node.nodeType != 1 || node.contentEditable == "false") {
286                         return false;
287                     }
288                     return node.contentEditable == "true" || isEditableElement(node.parentNode);
289                 };
290         })();
291
292         function isEditingHost(node) {
293             var parent;
294             return node && node.nodeType == 1 &&
295                 (( (parent = node.parentNode) && parent.nodeType == 9 && parent.designMode == "on") ||
296                 (isEditableElement(node) && !isEditableElement(node.parentNode)));
297         }
298
299         function isEditable(node) {
300             return (isEditableElement(node) || (node.nodeType != 1 && isEditableElement(node.parentNode))) && !isEditingHost(node);
301         }
302
303         var inlineDisplayRegex = /^inline(-block|-table)?$/i;
304
305         function isNonInlineElement(node) {
306             return node && node.nodeType == 1 && !inlineDisplayRegex.test(getComputedStyleProperty(node, "display"));
307         }
308
309         // White space characters as defined by HTML 4 (http://www.w3.org/TR/html401/struct/text.html)
310         var htmlNonWhiteSpaceRegex = /[^\r\n\t\f \u200B]/;
311
312         function isUnrenderedWhiteSpaceNode(node) {
313             if (node.data.length == 0) {
314                 return true;
315             }
316             if (htmlNonWhiteSpaceRegex.test(node.data)) {
317                 return false;
318             }
319             var cssWhiteSpace = getComputedStyleProperty(node.parentNode, "whiteSpace");
320             switch (cssWhiteSpace) {
321                 case "pre":
322                 case "pre-wrap":
323                 case "-moz-pre-wrap":
324                     return false;
325                 case "pre-line":
326                     if (/[\r\n]/.test(node.data)) {
327                         return false;
328                     }
329             }
330
331             // We now have a whitespace-only text node that may be rendered depending on its context. If it is adjacent to a
332             // non-inline element, it will not be rendered. This seems to be a good enough definition.
333             return isNonInlineElement(node.previousSibling) || isNonInlineElement(node.nextSibling);
334         }
335
336         function getRangeBoundaries(ranges) {
337             var positions = [], i, range;
338             for (i = 0; range = ranges[i++]; ) {
339                 positions.push(
340                     new DomPosition(range.startContainer, range.startOffset),
341                     new DomPosition(range.endContainer, range.endOffset)
342                 );
343             }
344             return positions;
345         }
346
347         function updateRangesFromBoundaries(ranges, positions) {
348             for (var i = 0, range, start, end, len = ranges.length; i < len; ++i) {
349                 range = ranges[i];
350                 start = positions[i * 2];
351                 end = positions[i * 2 + 1];
352                 range.setStartAndEnd(start.node, start.offset, end.node, end.offset);
353             }
354         }
355
356         function isSplitPoint(node, offset) {
357             if (dom.isCharacterDataNode(node)) {
358                 if (offset == 0) {
359                     return !!node.previousSibling;
360                 } else if (offset == node.length) {
361                     return !!node.nextSibling;
362                 } else {
363                     return true;
364                 }
365             }
366
367             return offset > 0 && offset < node.childNodes.length;
368         }
369
370         function splitNodeAt(node, descendantNode, descendantOffset, positionsToPreserve) {
371             var newNode, parentNode;
372             var splitAtStart = (descendantOffset == 0);
373
374             if (dom.isAncestorOf(descendantNode, node)) {
375                 return node;
376             }
377
378             if (dom.isCharacterDataNode(descendantNode)) {
379                 var descendantIndex = dom.getNodeIndex(descendantNode);
380                 if (descendantOffset == 0) {
381                     descendantOffset = descendantIndex;
382                 } else if (descendantOffset == descendantNode.length) {
383                     descendantOffset = descendantIndex + 1;
384                 } else {
385                     throw module.createError("splitNodeAt() should not be called with offset in the middle of a data node (" +
386                         descendantOffset + " in " + descendantNode.data);
387                 }
388                 descendantNode = descendantNode.parentNode;
389             }
390
391             if (isSplitPoint(descendantNode, descendantOffset)) {
392                 // descendantNode is now guaranteed not to be a text or other character node
393                 newNode = descendantNode.cloneNode(false);
394                 parentNode = descendantNode.parentNode;
395                 if (newNode.id) {
396                     newNode.removeAttribute("id");
397                 }
398                 var child, newChildIndex = 0;
399
400                 while ( (child = descendantNode.childNodes[descendantOffset]) ) {
401                     movePreservingPositions(child, newNode, newChildIndex++, positionsToPreserve);
402                 }
403                 movePreservingPositions(newNode, parentNode, dom.getNodeIndex(descendantNode) + 1, positionsToPreserve);
404                 return (descendantNode == node) ? newNode : splitNodeAt(node, parentNode, dom.getNodeIndex(newNode), positionsToPreserve);
405             } else if (node != descendantNode) {
406                 newNode = descendantNode.parentNode;
407
408                 // Work out a new split point in the parent node
409                 var newNodeIndex = dom.getNodeIndex(descendantNode);
410
411                 if (!splitAtStart) {
412                     newNodeIndex++;
413                 }
414                 return splitNodeAt(node, newNode, newNodeIndex, positionsToPreserve);
415             }
416             return node;
417         }
418
419         function areElementsMergeable(el1, el2) {
420             return el1.namespaceURI == el2.namespaceURI &&
421                 el1.tagName.toLowerCase() == el2.tagName.toLowerCase() &&
422                 haveSameClasses(el1, el2) &&
423                 elementsHaveSameNonClassAttributes(el1, el2) &&
424                 getComputedStyleProperty(el1, "display") == "inline" &&
425                 getComputedStyleProperty(el2, "display") == "inline";
426         }
427
428         function createAdjacentMergeableTextNodeGetter(forward) {
429             var siblingPropName = forward ? "nextSibling" : "previousSibling";
430
431             return function(textNode, checkParentElement) {
432                 var el = textNode.parentNode;
433                 var adjacentNode = textNode[siblingPropName];
434                 if (adjacentNode) {
435                     // Can merge if the node's previous/next sibling is a text node
436                     if (adjacentNode && adjacentNode.nodeType == 3) {
437                         return adjacentNode;
438                     }
439                 } else if (checkParentElement) {
440                     // Compare text node parent element with its sibling
441                     adjacentNode = el[siblingPropName];
442                     if (adjacentNode && adjacentNode.nodeType == 1 && areElementsMergeable(el, adjacentNode)) {
443                         var adjacentNodeChild = adjacentNode[forward ? "firstChild" : "lastChild"];
444                         if (adjacentNodeChild && adjacentNodeChild.nodeType == 3) {
445                             return adjacentNodeChild;
446                         }
447                     }
448                 }
449                 return null;
450             };
451         }
452
453         var getPreviousMergeableTextNode = createAdjacentMergeableTextNodeGetter(false),
454             getNextMergeableTextNode = createAdjacentMergeableTextNodeGetter(true);
455
456     
457         function Merge(firstNode) {
458             this.isElementMerge = (firstNode.nodeType == 1);
459             this.textNodes = [];
460             var firstTextNode = this.isElementMerge ? firstNode.lastChild : firstNode;
461             if (firstTextNode) {
462                 this.textNodes[0] = firstTextNode;
463             }
464         }
465
466         Merge.prototype = {
467             doMerge: function(positionsToPreserve) {
468                 var textNodes = this.textNodes;
469                 var firstTextNode = textNodes[0];
470                 if (textNodes.length > 1) {
471                     var firstTextNodeIndex = dom.getNodeIndex(firstTextNode);
472                     var textParts = [], combinedTextLength = 0, textNode, parent;
473                     forEach(textNodes, function(textNode, i) {
474                         parent = textNode.parentNode;
475                         if (i > 0) {
476                             parent.removeChild(textNode);
477                             if (!parent.hasChildNodes()) {
478                                 dom.removeNode(parent);
479                             }
480                             if (positionsToPreserve) {
481                                 forEach(positionsToPreserve, function(position) {
482                                     // Handle case where position is inside the text node being merged into a preceding node
483                                     if (position.node == textNode) {
484                                         position.node = firstTextNode;
485                                         position.offset += combinedTextLength;
486                                     }
487                                     // Handle case where both text nodes precede the position within the same parent node
488                                     if (position.node == parent && position.offset > firstTextNodeIndex) {
489                                         --position.offset;
490                                         if (position.offset == firstTextNodeIndex + 1 && i < len - 1) {
491                                             position.node = firstTextNode;
492                                             position.offset = combinedTextLength;
493                                         }
494                                     }
495                                 });
496                             }
497                         }
498                         textParts[i] = textNode.data;
499                         combinedTextLength += textNode.data.length;
500                     });
501                     firstTextNode.data = textParts.join("");
502                 }
503                 return firstTextNode.data;
504             },
505
506             getLength: function() {
507                 var i = this.textNodes.length, len = 0;
508                 while (i--) {
509                     len += this.textNodes[i].length;
510                 }
511                 return len;
512             },
513
514             toString: function() {
515                 var textParts = [];
516                 forEach(this.textNodes, function(textNode, i) {
517                     textParts[i] = "'" + textNode.data + "'";
518                 });
519                 return "[Merge(" + textParts.join(",") + ")]";
520             }
521         };
522
523         var optionProperties = ["elementTagName", "ignoreWhiteSpace", "applyToEditableOnly", "useExistingElements",
524             "removeEmptyElements", "onElementCreate"];
525
526         // TODO: Populate this with every attribute name that corresponds to a property with a different name. Really??
527         var attrNamesForProperties = {};
528
529         function ClassApplier(className, options, tagNames) {
530             var normalize, i, len, propName, applier = this;
531             applier.cssClass = applier.className = className; // cssClass property is for backward compatibility
532
533             var elementPropertiesFromOptions = null, elementAttributes = {};
534
535             // Initialize from options object
536             if (typeof options == "object" && options !== null) {
537                 if (typeof options.elementTagName !== "undefined") {
538                     options.elementTagName = options.elementTagName.toLowerCase();
539                 }
540                 tagNames = options.tagNames;
541                 elementPropertiesFromOptions = options.elementProperties;
542                 elementAttributes = options.elementAttributes;
543
544                 for (i = 0; propName = optionProperties[i++]; ) {
545                     if (options.hasOwnProperty(propName)) {
546                         applier[propName] = options[propName];
547                     }
548                 }
549                 normalize = options.normalize;
550             } else {
551                 normalize = options;
552             }
553
554             // Backward compatibility: the second parameter can also be a Boolean indicating to normalize after unapplying
555             applier.normalize = (typeof normalize == "undefined") ? true : normalize;
556
557             // Initialize element properties and attribute exceptions
558             applier.attrExceptions = [];
559             var el = document.createElement(applier.elementTagName);
560             applier.elementProperties = applier.copyPropertiesToElement(elementPropertiesFromOptions, el, true);
561             each(elementAttributes, function(attrName, attrValue) {
562                 applier.attrExceptions.push(attrName);
563                 // Ensure each attribute value is a string
564                 elementAttributes[attrName] = "" + attrValue;
565             });
566             applier.elementAttributes = elementAttributes;
567
568             applier.elementSortedClassName = applier.elementProperties.hasOwnProperty("className") ?
569                 sortClassName(applier.elementProperties.className + " " + className) : className;
570
571             // Initialize tag names
572             applier.applyToAnyTagName = false;
573             var type = typeof tagNames;
574             if (type == "string") {
575                 if (tagNames == "*") {
576                     applier.applyToAnyTagName = true;
577                 } else {
578                     applier.tagNames = trim(tagNames.toLowerCase()).split(/\s*,\s*/);
579                 }
580             } else if (type == "object" && typeof tagNames.length == "number") {
581                 applier.tagNames = [];
582                 for (i = 0, len = tagNames.length; i < len; ++i) {
583                     if (tagNames[i] == "*") {
584                         applier.applyToAnyTagName = true;
585                     } else {
586                         applier.tagNames.push(tagNames[i].toLowerCase());
587                     }
588                 }
589             } else {
590                 applier.tagNames = [applier.elementTagName];
591             }
592         }
593
594         ClassApplier.prototype = {
595             elementTagName: defaultTagName,
596             elementProperties: {},
597             elementAttributes: {},
598             ignoreWhiteSpace: true,
599             applyToEditableOnly: false,
600             useExistingElements: true,
601             removeEmptyElements: true,
602             onElementCreate: null,
603
604             copyPropertiesToElement: function(props, el, createCopy) {
605                 var s, elStyle, elProps = {}, elPropsStyle, propValue, elPropValue, attrName;
606
607                 for (var p in props) {
608                     if (props.hasOwnProperty(p)) {
609                         propValue = props[p];
610                         elPropValue = el[p];
611
612                         // Special case for class. The copied properties object has the applier's class as well as its own
613                         // to simplify checks when removing styling elements
614                         if (p == "className") {
615                             addClass(el, propValue);
616                             addClass(el, this.className);
617                             el[p] = sortClassName(el[p]);
618                             if (createCopy) {
619                                 elProps[p] = propValue;
620                             }
621                         }
622
623                         // Special case for style
624                         else if (p == "style") {
625                             elStyle = elPropValue;
626                             if (createCopy) {
627                                 elProps[p] = elPropsStyle = {};
628                             }
629                             for (s in props[p]) {
630                                 if (props[p].hasOwnProperty(s)) {
631                                     elStyle[s] = propValue[s];
632                                     if (createCopy) {
633                                         elPropsStyle[s] = elStyle[s];
634                                     }
635                                 }
636                             }
637                             this.attrExceptions.push(p);
638                         } else {
639                             el[p] = propValue;
640                             // Copy the property back from the dummy element so that later comparisons to check whether
641                             // elements may be removed are checking against the right value. For example, the href property
642                             // of an element returns a fully qualified URL even if it was previously assigned a relative
643                             // URL.
644                             if (createCopy) {
645                                 elProps[p] = el[p];
646
647                                 // Not all properties map to identically-named attributes
648                                 attrName = attrNamesForProperties.hasOwnProperty(p) ? attrNamesForProperties[p] : p;
649                                 this.attrExceptions.push(attrName);
650                             }
651                         }
652                     }
653                 }
654
655                 return createCopy ? elProps : "";
656             },
657
658             copyAttributesToElement: function(attrs, el) {
659                 for (var attrName in attrs) {
660                     if (attrs.hasOwnProperty(attrName) && !/^class(?:Name)?$/i.test(attrName)) {
661                         el.setAttribute(attrName, attrs[attrName]);
662                     }
663                 }
664             },
665
666             appliesToElement: function(el) {
667                 return contains(this.tagNames, el.tagName.toLowerCase());
668             },
669
670             getEmptyElements: function(range) {
671                 var applier = this;
672                 return range.getNodes([1], function(el) {
673                     return applier.appliesToElement(el) && !el.hasChildNodes();
674                 });
675             },
676
677             hasClass: function(node) {
678                 return node.nodeType == 1 &&
679                     (this.applyToAnyTagName || this.appliesToElement(node)) &&
680                     hasClass(node, this.className);
681             },
682
683             getSelfOrAncestorWithClass: function(node) {
684                 while (node) {
685                     if (this.hasClass(node)) {
686                         return node;
687                     }
688                     node = node.parentNode;
689                 }
690                 return null;
691             },
692
693             isModifiable: function(node) {
694                 return !this.applyToEditableOnly || isEditable(node);
695             },
696
697             // White space adjacent to an unwrappable node can be ignored for wrapping
698             isIgnorableWhiteSpaceNode: function(node) {
699                 return this.ignoreWhiteSpace && node && node.nodeType == 3 && isUnrenderedWhiteSpaceNode(node);
700             },
701
702             // Normalizes nodes after applying a class to a Range.
703             postApply: function(textNodes, range, positionsToPreserve, isUndo) {
704                 var firstNode = textNodes[0], lastNode = textNodes[textNodes.length - 1];
705
706                 var merges = [], currentMerge;
707
708                 var rangeStartNode = firstNode, rangeEndNode = lastNode;
709                 var rangeStartOffset = 0, rangeEndOffset = lastNode.length;
710
711                 var textNode, precedingTextNode;
712
713                 // Check for every required merge and create a Merge object for each
714                 forEach(textNodes, function(textNode) {
715                     precedingTextNode = getPreviousMergeableTextNode(textNode, !isUndo);
716                     if (precedingTextNode) {
717                         if (!currentMerge) {
718                             currentMerge = new Merge(precedingTextNode);
719                             merges.push(currentMerge);
720                         }
721                         currentMerge.textNodes.push(textNode);
722                         if (textNode === firstNode) {
723                             rangeStartNode = currentMerge.textNodes[0];
724                             rangeStartOffset = rangeStartNode.length;
725                         }
726                         if (textNode === lastNode) {
727                             rangeEndNode = currentMerge.textNodes[0];
728                             rangeEndOffset = currentMerge.getLength();
729                         }
730                     } else {
731                         currentMerge = null;
732                     }
733                 });
734
735                 // Test whether the first node after the range needs merging
736                 var nextTextNode = getNextMergeableTextNode(lastNode, !isUndo);
737
738                 if (nextTextNode) {
739                     if (!currentMerge) {
740                         currentMerge = new Merge(lastNode);
741                         merges.push(currentMerge);
742                     }
743                     currentMerge.textNodes.push(nextTextNode);
744                 }
745
746                 // Apply the merges
747                 if (merges.length) {
748                     for (i = 0, len = merges.length; i < len; ++i) {
749                         merges[i].doMerge(positionsToPreserve);
750                     }
751
752                     // Set the range boundaries
753                     range.setStartAndEnd(rangeStartNode, rangeStartOffset, rangeEndNode, rangeEndOffset);
754                 }
755             },
756
757             createContainer: function(parentNode,serializedHighlight) {
758                 var doc = dom.getDocument(parentNode);
759                 var namespace;
760                 var el = createElementNSSupported && !dom.isHtmlNamespace(parentNode) && (namespace = parentNode.namespaceURI) ?
761                     doc.createElementNS(parentNode.namespaceURI, this.elementTagName) :
762                     doc.createElement(this.elementTagName);
763
764                 this.copyPropertiesToElement(this.elementProperties, el, false);
765                 this.copyAttributesToElement(this.elementAttributes, el);
766                  //Highlight.printData("createContainer");
767                 addClass(el, this.className, serializedHighlight);
768                 if (this.onElementCreate) {
769                     this.onElementCreate(el, this);
770                 }
771                 return el;
772             },
773
774             elementHasProperties: function(el, props) {
775                 var applier = this;
776                 return each(props, function(p, propValue) {
777                     if (p == "className") {
778                         // For checking whether we should reuse an existing element, we just want to check that the element
779                         // has all the classes specified in the className property. When deciding whether the element is
780                         // removable when unapplying a class, there is separate special handling to check whether the
781                         // element has extra classes so the same simple check will do.
782                         return hasAllClasses(el, propValue);
783                     } else if (typeof propValue == "object") {
784                         if (!applier.elementHasProperties(el[p], propValue)) {
785                             return false;
786                         }
787                     } else if (el[p] !== propValue) {
788                         return false;
789                     }
790                 });
791             },
792
793             elementHasAttributes: function(el, attrs) {
794                 return each(attrs, function(name, value) {
795                     if (el.getAttribute(name) !== value) {
796                         return false;
797                     }
798                 });
799             },
800
801             applyToTextNode: function(textNode, positionsToPreserve,serializedHighlight) {
802
803                 // Check whether the text node can be styled. Text within a <style> or <script> element, for example,
804                 // should not be styled. See issue 283.
805                 if (canTextBeStyled(textNode)) {
806                     var parent = textNode.parentNode;
807                     if (parent.childNodes.length == 1 &&
808                         this.useExistingElements &&
809                         this.appliesToElement(parent) &&
810                         this.elementHasProperties(parent, this.elementProperties) &&
811                         this.elementHasAttributes(parent, this.elementAttributes)) {
812                         addClass(parent, this.className, serializedHighlight);
813                     } else {
814                         var textNodeParent = textNode.parentNode;
815                         var el = this.createContainer(textNodeParent, serializedHighlight);
816                         textNodeParent.insertBefore(el, textNode);
817                         el.appendChild(textNode);
818                     }
819                 }
820
821             },
822
823             isRemovable: function(el) {
824                 return el.tagName.toLowerCase() == this.elementTagName &&
825                     getSortedClassName(el) == this.elementSortedClassName &&
826                     this.elementHasProperties(el, this.elementProperties) &&
827                     !elementHasNonClassAttributes(el, this.attrExceptions) &&
828                     this.elementHasAttributes(el, this.elementAttributes) &&
829                     this.isModifiable(el);
830             },
831
832             isEmptyContainer: function(el) {
833                 var childNodeCount = el.childNodes.length;
834                 return el.nodeType == 1 &&
835                     this.isRemovable(el) &&
836                     (childNodeCount == 0 || (childNodeCount == 1 && this.isEmptyContainer(el.firstChild)));
837             },
838
839             removeEmptyContainers: function(range) {
840                 var applier = this;
841                 var nodesToRemove = range.getNodes([1], function(el) {
842                     return applier.isEmptyContainer(el);
843                 });
844
845                 var rangesToPreserve = [range];
846                 var positionsToPreserve = getRangeBoundaries(rangesToPreserve);
847
848                 forEach(nodesToRemove, function(node) {
849                     removePreservingPositions(node, positionsToPreserve);
850                 });
851
852                 // Update the range from the preserved boundary positions
853                 updateRangesFromBoundaries(rangesToPreserve, positionsToPreserve);
854             },
855
856             undoToTextNode: function(textNode, range, ancestorWithClass, positionsToPreserve) {
857                 if (!range.containsNode(ancestorWithClass)) {
858                     // Split out the portion of the ancestor from which we can remove the class
859                     //var parent = ancestorWithClass.parentNode, index = dom.getNodeIndex(ancestorWithClass);
860                     var ancestorRange = range.cloneRange();
861                     ancestorRange.selectNode(ancestorWithClass);
862                     if (ancestorRange.isPointInRange(range.endContainer, range.endOffset)) {
863                         splitNodeAt(ancestorWithClass, range.endContainer, range.endOffset, positionsToPreserve);
864                         range.setEndAfter(ancestorWithClass);
865                     }
866                     if (ancestorRange.isPointInRange(range.startContainer, range.startOffset)) {
867                         ancestorWithClass = splitNodeAt(ancestorWithClass, range.startContainer, range.startOffset, positionsToPreserve);
868                     }
869                 }
870
871                 if (this.isRemovable(ancestorWithClass)) {
872                     replaceWithOwnChildrenPreservingPositions(ancestorWithClass, positionsToPreserve);
873                 } else {
874                     removeClass(ancestorWithClass, this.className);
875                 }
876             },
877
878             splitAncestorWithClass: function(container, offset, positionsToPreserve) {
879                 var ancestorWithClass = this.getSelfOrAncestorWithClass(container);
880                 if (ancestorWithClass) {
881                     splitNodeAt(ancestorWithClass, container, offset, positionsToPreserve);
882                 }
883             },
884
885             undoToAncestor: function(ancestorWithClass, positionsToPreserve) {
886                 if (this.isRemovable(ancestorWithClass)) {
887                     replaceWithOwnChildrenPreservingPositions(ancestorWithClass, positionsToPreserve);
888                 } else {
889                     removeClass(ancestorWithClass, this.className);
890                 }
891             },
892
893             applyToRange: function(range, rangesToPreserve, serializedHighlight) {
894                 var applier = this;
895                 rangesToPreserve = rangesToPreserve || [];
896
897                 // Create an array of range boundaries to preserve
898                 var positionsToPreserve = getRangeBoundaries(rangesToPreserve || []);
899
900                 range.splitBoundariesPreservingPositions(positionsToPreserve);
901
902                 // Tidy up the DOM by removing empty containers
903                 if (applier.removeEmptyElements) {
904                     applier.removeEmptyContainers(range);
905                 }
906
907                 var textNodes = getEffectiveTextNodes(range);
908
909                 if (textNodes.length) {
910                     forEach(textNodes, function(textNode) {
911                         if (!applier.isIgnorableWhiteSpaceNode(textNode) && !applier.getSelfOrAncestorWithClass(textNode) &&
912                                 applier.isModifiable(textNode)) {
913
914                                 applier.applyToTextNode(textNode, positionsToPreserve,serializedHighlight);
915                         }
916                     });
917                     var lastTextNode = textNodes[textNodes.length - 1];
918                     range.setStartAndEnd(textNodes[0], 0, lastTextNode, lastTextNode.length);
919                     if (applier.normalize) {
920                         applier.postApply(textNodes, range, positionsToPreserve, false);
921                     }
922
923                     // Update the ranges from the preserved boundary positions
924                     updateRangesFromBoundaries(rangesToPreserve, positionsToPreserve);
925                 }
926
927                 // Apply classes to any appropriate empty elements
928                 var emptyElements = applier.getEmptyElements(range);
929
930                 forEach(emptyElements, function(el) {
931                     addClass(el, applier.className);
932                 });
933             },
934
935             applyToRanges: function(ranges) {
936
937                 var i = ranges.length;
938                 while (i--) {
939                     this.applyToRange(ranges[i], ranges);
940                     //Highlight.printData("**ranges"+ranges[i]);
941                 }
942
943
944                 return ranges;
945             },
946
947             applyToSelection: function(win) {
948                 var sel = api.getSelection(win);
949                 sel.setRanges( this.applyToRanges(sel.getAllRanges()) );
950             },
951
952             undoToRange: function(range, rangesToPreserve) {
953                 var applier = this;
954                 // Create an array of range boundaries to preserve
955                 rangesToPreserve = rangesToPreserve || [];
956                 var positionsToPreserve = getRangeBoundaries(rangesToPreserve);
957
958
959                 range.splitBoundariesPreservingPositions(positionsToPreserve);
960
961                 // Tidy up the DOM by removing empty containers
962                 if (applier.removeEmptyElements) {
963                     applier.removeEmptyContainers(range, positionsToPreserve);
964                 }
965
966                 var textNodes = getEffectiveTextNodes(range);
967                 var textNode, ancestorWithClass;
968                 var lastTextNode = textNodes[textNodes.length - 1];
969
970                 if (textNodes.length) {
971                     applier.splitAncestorWithClass(range.endContainer, range.endOffset, positionsToPreserve);
972                     applier.splitAncestorWithClass(range.startContainer, range.startOffset, positionsToPreserve);
973                     for (var i = 0, len = textNodes.length; i < len; ++i) {
974                         textNode = textNodes[i];
975                         ancestorWithClass = applier.getSelfOrAncestorWithClass(textNode);
976                         if (ancestorWithClass && applier.isModifiable(textNode)) {
977                             applier.undoToAncestor(ancestorWithClass, positionsToPreserve);
978                         }
979                     }
980                     // Ensure the range is still valid
981                     range.setStartAndEnd(textNodes[0], 0, lastTextNode, lastTextNode.length);
982
983
984                     if (applier.normalize) {
985                         applier.postApply(textNodes, range, positionsToPreserve, true);
986                     }
987
988                     // Update the ranges from the preserved boundary positions
989                     updateRangesFromBoundaries(rangesToPreserve, positionsToPreserve);
990                 }
991
992                 // Remove class from any appropriate empty elements
993                 var emptyElements = applier.getEmptyElements(range);
994
995                 forEach(emptyElements, function(el) {
996                     removeClass(el, applier.className);
997                 });
998             },
999
1000             undoToRanges: function(ranges) {
1001                 // Get ranges returned in document order
1002                 var i = ranges.length;
1003
1004                 while (i--) {
1005                     this.undoToRange(ranges[i], ranges);
1006                 }
1007
1008                 return ranges;
1009             },
1010
1011             undoToSelection: function(win) {
1012                 var sel = api.getSelection(win);
1013                 var ranges = api.getSelection(win).getAllRanges();
1014                 this.undoToRanges(ranges);
1015                 sel.setRanges(ranges);
1016             },
1017
1018             isAppliedToRange: function(range) {
1019                 if (range.collapsed || range.toString() == "") {
1020                     return !!this.getSelfOrAncestorWithClass(range.commonAncestorContainer);
1021                 } else {
1022                     var textNodes = range.getNodes( [3] );
1023                     if (textNodes.length)
1024                     for (var i = 0, textNode; textNode = textNodes[i++]; ) {
1025                         if (!this.isIgnorableWhiteSpaceNode(textNode) && rangeSelectsAnyText(range, textNode) &&
1026                                 this.isModifiable(textNode) && !this.getSelfOrAncestorWithClass(textNode)) {
1027                             return false;
1028                         }
1029                     }
1030                     return true;
1031                 }
1032             },
1033
1034             isAppliedToRanges: function(ranges) {
1035                 var i = ranges.length;
1036                 if (i == 0) {
1037                     return false;
1038                 }
1039                 while (i--) {
1040                     if (!this.isAppliedToRange(ranges[i])) {
1041                         return false;
1042                     }
1043                 }
1044                 return true;
1045             },
1046
1047             isAppliedToSelection: function(win) {
1048                 var sel = api.getSelection(win);
1049                 return this.isAppliedToRanges(sel.getAllRanges());
1050             },
1051
1052             toggleRange: function(range) {
1053                 if (this.isAppliedToRange(range)) {
1054                     this.undoToRange(range);
1055                 } else {
1056                     this.applyToRange(range);
1057                     //Highlight.printData("**toggleRange"+ranges[i]);
1058                 }
1059             },
1060
1061             toggleSelection: function(win) {
1062                 if (this.isAppliedToSelection(win)) {
1063                     this.undoToSelection(win);
1064                 } else {
1065                     this.applyToSelection(win);
1066                 }
1067             },
1068
1069             getElementsWithClassIntersectingRange: function(range) {
1070                 var elements = [];
1071                 var applier = this;
1072                 range.getNodes([3], function(textNode) {
1073                     var el = applier.getSelfOrAncestorWithClass(textNode);
1074                     if (el && !contains(elements, el)) {
1075                         elements.push(el);
1076                     }
1077                 });
1078                 return elements;
1079             },
1080
1081             detach: function() {}
1082         };
1083
1084         function createClassApplier(className, options, tagNames) {
1085             return new ClassApplier(className, options, tagNames);
1086         }
1087
1088         ClassApplier.util = {
1089             hasClass: hasClass,
1090             addClass: addClass,
1091             removeClass: removeClass,
1092             getClass: getClass,
1093             hasSameClasses: haveSameClasses,
1094             hasAllClasses: hasAllClasses,
1095             replaceWithOwnChildren: replaceWithOwnChildrenPreservingPositions,
1096             elementsHaveSameNonClassAttributes: elementsHaveSameNonClassAttributes,
1097             elementHasNonClassAttributes: elementHasNonClassAttributes,
1098             splitNodeAt: splitNodeAt,
1099             isEditableElement: isEditableElement,
1100             isEditingHost: isEditingHost,
1101             isEditable: isEditable
1102         };
1103
1104         api.CssClassApplier = api.ClassApplier = ClassApplier;
1105         api.createClassApplier = createClassApplier;
1106         util.createAliasForDeprecatedMethod(api, "createCssClassApplier", "createClassApplier", module);
1107     });
1108     
1109     return rangy;
1110 }, this);