dom/imptests/editing/implementation.js

branch
TOR_BUG_3246
changeset 7
129ffea94266
equal deleted inserted replaced
-1:000000000000 0:a0a36cc9103f
1 "use strict";
2
3 var htmlNamespace = "http://www.w3.org/1999/xhtml";
4
5 var cssStylingFlag = false;
6
7 var defaultSingleLineContainerName = "p";
8
9 // This is bad :(
10 var globalRange = null;
11
12 // Commands are stored in a dictionary where we call their actions and such
13 var commands = {};
14
15 ///////////////////////////////////////////////////////////////////////////////
16 ////////////////////////////// Utility functions //////////////////////////////
17 ///////////////////////////////////////////////////////////////////////////////
18 //@{
19
20 function nextNode(node) {
21 if (node.hasChildNodes()) {
22 return node.firstChild;
23 }
24 return nextNodeDescendants(node);
25 }
26
27 function previousNode(node) {
28 if (node.previousSibling) {
29 node = node.previousSibling;
30 while (node.hasChildNodes()) {
31 node = node.lastChild;
32 }
33 return node;
34 }
35 if (node.parentNode
36 && node.parentNode.nodeType == Node.ELEMENT_NODE) {
37 return node.parentNode;
38 }
39 return null;
40 }
41
42 function nextNodeDescendants(node) {
43 while (node && !node.nextSibling) {
44 node = node.parentNode;
45 }
46 if (!node) {
47 return null;
48 }
49 return node.nextSibling;
50 }
51
52 /**
53 * Returns true if ancestor is an ancestor of descendant, false otherwise.
54 */
55 function isAncestor(ancestor, descendant) {
56 return ancestor
57 && descendant
58 && Boolean(ancestor.compareDocumentPosition(descendant) & Node.DOCUMENT_POSITION_CONTAINED_BY);
59 }
60
61 /**
62 * Returns true if ancestor is an ancestor of or equal to descendant, false
63 * otherwise.
64 */
65 function isAncestorContainer(ancestor, descendant) {
66 return (ancestor || descendant)
67 && (ancestor == descendant || isAncestor(ancestor, descendant));
68 }
69
70 /**
71 * Returns true if descendant is a descendant of ancestor, false otherwise.
72 */
73 function isDescendant(descendant, ancestor) {
74 return ancestor
75 && descendant
76 && Boolean(ancestor.compareDocumentPosition(descendant) & Node.DOCUMENT_POSITION_CONTAINED_BY);
77 }
78
79 /**
80 * Returns true if node1 is before node2 in tree order, false otherwise.
81 */
82 function isBefore(node1, node2) {
83 return Boolean(node1.compareDocumentPosition(node2) & Node.DOCUMENT_POSITION_FOLLOWING);
84 }
85
86 /**
87 * Returns true if node1 is after node2 in tree order, false otherwise.
88 */
89 function isAfter(node1, node2) {
90 return Boolean(node1.compareDocumentPosition(node2) & Node.DOCUMENT_POSITION_PRECEDING);
91 }
92
93 function getAncestors(node) {
94 var ancestors = [];
95 while (node.parentNode) {
96 ancestors.unshift(node.parentNode);
97 node = node.parentNode;
98 }
99 return ancestors;
100 }
101
102 function getInclusiveAncestors(node) {
103 return getAncestors(node).concat(node);
104 }
105
106 function getDescendants(node) {
107 var descendants = [];
108 var stop = nextNodeDescendants(node);
109 while ((node = nextNode(node))
110 && node != stop) {
111 descendants.push(node);
112 }
113 return descendants;
114 }
115
116 function getInclusiveDescendants(node) {
117 return [node].concat(getDescendants(node));
118 }
119
120 function convertProperty(property) {
121 // Special-case for now
122 var map = {
123 "fontFamily": "font-family",
124 "fontSize": "font-size",
125 "fontStyle": "font-style",
126 "fontWeight": "font-weight",
127 "textDecoration": "text-decoration",
128 };
129 if (typeof map[property] != "undefined") {
130 return map[property];
131 }
132
133 return property;
134 }
135
136 // Return the <font size=X> value for the given CSS size, or undefined if there
137 // is none.
138 function cssSizeToLegacy(cssVal) {
139 return {
140 "x-small": 1,
141 "small": 2,
142 "medium": 3,
143 "large": 4,
144 "x-large": 5,
145 "xx-large": 6,
146 "xxx-large": 7
147 }[cssVal];
148 }
149
150 // Return the CSS size given a legacy size.
151 function legacySizeToCss(legacyVal) {
152 return {
153 1: "x-small",
154 2: "small",
155 3: "medium",
156 4: "large",
157 5: "x-large",
158 6: "xx-large",
159 7: "xxx-large",
160 }[legacyVal];
161 }
162
163 // Opera 11 puts HTML elements in the null namespace, it seems.
164 function isHtmlNamespace(ns) {
165 return ns === null
166 || ns === htmlNamespace;
167 }
168
169 // "the directionality" from HTML. I don't bother caring about non-HTML
170 // elements.
171 //
172 // "The directionality of an element is either 'ltr' or 'rtl', and is
173 // determined as per the first appropriate set of steps from the following
174 // list:"
175 function getDirectionality(element) {
176 // "If the element's dir attribute is in the ltr state
177 // The directionality of the element is 'ltr'."
178 if (element.dir == "ltr") {
179 return "ltr";
180 }
181
182 // "If the element's dir attribute is in the rtl state
183 // The directionality of the element is 'rtl'."
184 if (element.dir == "rtl") {
185 return "rtl";
186 }
187
188 // "If the element's dir attribute is in the auto state
189 // "If the element is a bdi element and the dir attribute is not in a
190 // defined state (i.e. it is not present or has an invalid value)
191 // [lots of complicated stuff]
192 //
193 // Skip this, since no browser implements it anyway.
194
195 // "If the element is a root element and the dir attribute is not in a
196 // defined state (i.e. it is not present or has an invalid value)
197 // The directionality of the element is 'ltr'."
198 if (!isHtmlElement(element.parentNode)) {
199 return "ltr";
200 }
201
202 // "If the element has a parent element and the dir attribute is not in a
203 // defined state (i.e. it is not present or has an invalid value)
204 // The directionality of the element is the same as the element's
205 // parent element's directionality."
206 return getDirectionality(element.parentNode);
207 }
208
209 //@}
210
211 ///////////////////////////////////////////////////////////////////////////////
212 ///////////////////////////// DOM Range functions /////////////////////////////
213 ///////////////////////////////////////////////////////////////////////////////
214 //@{
215
216 function getNodeIndex(node) {
217 var ret = 0;
218 while (node.previousSibling) {
219 ret++;
220 node = node.previousSibling;
221 }
222 return ret;
223 }
224
225 // "The length of a Node node is the following, depending on node:
226 //
227 // ProcessingInstruction
228 // DocumentType
229 // Always 0.
230 // Text
231 // Comment
232 // node's length.
233 // Any other node
234 // node's childNodes's length."
235 function getNodeLength(node) {
236 switch (node.nodeType) {
237 case Node.PROCESSING_INSTRUCTION_NODE:
238 case Node.DOCUMENT_TYPE_NODE:
239 return 0;
240
241 case Node.TEXT_NODE:
242 case Node.COMMENT_NODE:
243 return node.length;
244
245 default:
246 return node.childNodes.length;
247 }
248 }
249
250 /**
251 * The position of two boundary points relative to one another, as defined by
252 * DOM Range.
253 */
254 function getPosition(nodeA, offsetA, nodeB, offsetB) {
255 // "If node A is the same as node B, return equal if offset A equals offset
256 // B, before if offset A is less than offset B, and after if offset A is
257 // greater than offset B."
258 if (nodeA == nodeB) {
259 if (offsetA == offsetB) {
260 return "equal";
261 }
262 if (offsetA < offsetB) {
263 return "before";
264 }
265 if (offsetA > offsetB) {
266 return "after";
267 }
268 }
269
270 // "If node A is after node B in tree order, compute the position of (node
271 // B, offset B) relative to (node A, offset A). If it is before, return
272 // after. If it is after, return before."
273 if (nodeB.compareDocumentPosition(nodeA) & Node.DOCUMENT_POSITION_FOLLOWING) {
274 var pos = getPosition(nodeB, offsetB, nodeA, offsetA);
275 if (pos == "before") {
276 return "after";
277 }
278 if (pos == "after") {
279 return "before";
280 }
281 }
282
283 // "If node A is an ancestor of node B:"
284 if (nodeB.compareDocumentPosition(nodeA) & Node.DOCUMENT_POSITION_CONTAINS) {
285 // "Let child equal node B."
286 var child = nodeB;
287
288 // "While child is not a child of node A, set child to its parent."
289 while (child.parentNode != nodeA) {
290 child = child.parentNode;
291 }
292
293 // "If the index of child is less than offset A, return after."
294 if (getNodeIndex(child) < offsetA) {
295 return "after";
296 }
297 }
298
299 // "Return before."
300 return "before";
301 }
302
303 /**
304 * Returns the furthest ancestor of a Node as defined by DOM Range.
305 */
306 function getFurthestAncestor(node) {
307 var root = node;
308 while (root.parentNode != null) {
309 root = root.parentNode;
310 }
311 return root;
312 }
313
314 /**
315 * "contained" as defined by DOM Range: "A Node node is contained in a range
316 * range if node's furthest ancestor is the same as range's root, and (node, 0)
317 * is after range's start, and (node, length of node) is before range's end."
318 */
319 function isContained(node, range) {
320 var pos1 = getPosition(node, 0, range.startContainer, range.startOffset);
321 var pos2 = getPosition(node, getNodeLength(node), range.endContainer, range.endOffset);
322
323 return getFurthestAncestor(node) == getFurthestAncestor(range.startContainer)
324 && pos1 == "after"
325 && pos2 == "before";
326 }
327
328 /**
329 * Return all nodes contained in range that the provided function returns true
330 * for, omitting any with an ancestor already being returned.
331 */
332 function getContainedNodes(range, condition) {
333 if (typeof condition == "undefined") {
334 condition = function() { return true };
335 }
336 var node = range.startContainer;
337 if (node.hasChildNodes()
338 && range.startOffset < node.childNodes.length) {
339 // A child is contained
340 node = node.childNodes[range.startOffset];
341 } else if (range.startOffset == getNodeLength(node)) {
342 // No descendant can be contained
343 node = nextNodeDescendants(node);
344 } else {
345 // No children; this node at least can't be contained
346 node = nextNode(node);
347 }
348
349 var stop = range.endContainer;
350 if (stop.hasChildNodes()
351 && range.endOffset < stop.childNodes.length) {
352 // The node after the last contained node is a child
353 stop = stop.childNodes[range.endOffset];
354 } else {
355 // This node and/or some of its children might be contained
356 stop = nextNodeDescendants(stop);
357 }
358
359 var nodeList = [];
360 while (isBefore(node, stop)) {
361 if (isContained(node, range)
362 && condition(node)) {
363 nodeList.push(node);
364 node = nextNodeDescendants(node);
365 continue;
366 }
367 node = nextNode(node);
368 }
369 return nodeList;
370 }
371
372 /**
373 * As above, but includes nodes with an ancestor that's already been returned.
374 */
375 function getAllContainedNodes(range, condition) {
376 if (typeof condition == "undefined") {
377 condition = function() { return true };
378 }
379 var node = range.startContainer;
380 if (node.hasChildNodes()
381 && range.startOffset < node.childNodes.length) {
382 // A child is contained
383 node = node.childNodes[range.startOffset];
384 } else if (range.startOffset == getNodeLength(node)) {
385 // No descendant can be contained
386 node = nextNodeDescendants(node);
387 } else {
388 // No children; this node at least can't be contained
389 node = nextNode(node);
390 }
391
392 var stop = range.endContainer;
393 if (stop.hasChildNodes()
394 && range.endOffset < stop.childNodes.length) {
395 // The node after the last contained node is a child
396 stop = stop.childNodes[range.endOffset];
397 } else {
398 // This node and/or some of its children might be contained
399 stop = nextNodeDescendants(stop);
400 }
401
402 var nodeList = [];
403 while (isBefore(node, stop)) {
404 if (isContained(node, range)
405 && condition(node)) {
406 nodeList.push(node);
407 }
408 node = nextNode(node);
409 }
410 return nodeList;
411 }
412
413 // Returns either null, or something of the form rgb(x, y, z), or something of
414 // the form rgb(x, y, z, w) with w != 0.
415 function normalizeColor(color) {
416 if (color.toLowerCase() == "currentcolor") {
417 return null;
418 }
419
420 if (normalizeColor.resultCache === undefined) {
421 normalizeColor.resultCache = {};
422 }
423
424 if (normalizeColor.resultCache[color] !== undefined) {
425 return normalizeColor.resultCache[color];
426 }
427
428 var originalColor = color;
429
430 var outerSpan = document.createElement("span");
431 document.body.appendChild(outerSpan);
432 outerSpan.style.color = "black";
433
434 var innerSpan = document.createElement("span");
435 outerSpan.appendChild(innerSpan);
436 innerSpan.style.color = color;
437 color = getComputedStyle(innerSpan).color;
438
439 if (color == "rgb(0, 0, 0)") {
440 // Maybe it's really black, maybe it's invalid.
441 outerSpan.color = "white";
442 color = getComputedStyle(innerSpan).color;
443 if (color != "rgb(0, 0, 0)") {
444 return normalizeColor.resultCache[originalColor] = null;
445 }
446 }
447
448 document.body.removeChild(outerSpan);
449
450 // I rely on the fact that browsers generally provide consistent syntax for
451 // getComputedStyle(), although it's not standardized. There are only
452 // three exceptions I found:
453 if (/^rgba\([0-9]+, [0-9]+, [0-9]+, 1\)$/.test(color)) {
454 // IE10PP2 seems to do this sometimes.
455 return normalizeColor.resultCache[originalColor] =
456 color.replace("rgba", "rgb").replace(", 1)", ")");
457 }
458 if (color == "transparent") {
459 // IE10PP2, Firefox 7.0a2, and Opera 11.50 all return "transparent" if
460 // the specified value is "transparent".
461 return normalizeColor.resultCache[originalColor] =
462 "rgba(0, 0, 0, 0)";
463 }
464 // Chrome 15 dev adds way too many significant figures. This isn't a full
465 // fix, it just fixes one case that comes up in tests.
466 color = color.replace(/, 0.496094\)$/, ", 0.5)");
467 return normalizeColor.resultCache[originalColor] = color;
468 }
469
470 // Returns either null, or something of the form #xxxxxx.
471 function parseSimpleColor(color) {
472 color = normalizeColor(color);
473 var matches = /^rgb\(([0-9]+), ([0-9]+), ([0-9]+)\)$/.exec(color);
474 if (matches) {
475 return "#"
476 + parseInt(matches[1]).toString(16).replace(/^.$/, "0$&")
477 + parseInt(matches[2]).toString(16).replace(/^.$/, "0$&")
478 + parseInt(matches[3]).toString(16).replace(/^.$/, "0$&");
479 }
480 return null;
481 }
482
483 //@}
484
485 //////////////////////////////////////////////////////////////////////////////
486 /////////////////////////// Edit command functions ///////////////////////////
487 //////////////////////////////////////////////////////////////////////////////
488
489 /////////////////////////////////////////////////
490 ///// Methods of the HTMLDocument interface /////
491 /////////////////////////////////////////////////
492 //@{
493
494 var executionStackDepth = 0;
495
496 // Helper function for common behavior.
497 function editCommandMethod(command, range, callback) {
498 // Set up our global range magic, but only if we're the outermost function
499 if (executionStackDepth == 0 && typeof range != "undefined") {
500 globalRange = range;
501 } else if (executionStackDepth == 0) {
502 globalRange = null;
503 globalRange = getActiveRange();
504 }
505
506 executionStackDepth++;
507 try {
508 var ret = callback();
509 } catch(e) {
510 executionStackDepth--;
511 throw e;
512 }
513 executionStackDepth--;
514 return ret;
515 }
516
517 function myExecCommand(command, showUi, value, range) {
518 // "All of these methods must treat their command argument ASCII
519 // case-insensitively."
520 command = command.toLowerCase();
521
522 // "If only one argument was provided, let show UI be false."
523 //
524 // If range was passed, I can't actually detect how many args were passed
525 // . . .
526 if (arguments.length == 1
527 || (arguments.length >=4 && typeof showUi == "undefined")) {
528 showUi = false;
529 }
530
531 // "If only one or two arguments were provided, let value be the empty
532 // string."
533 if (arguments.length <= 2
534 || (arguments.length >=4 && typeof value == "undefined")) {
535 value = "";
536 }
537
538 return editCommandMethod(command, range, (function(command, showUi, value) { return function() {
539 // "If command is not supported or not enabled, return false."
540 if (!(command in commands) || !myQueryCommandEnabled(command)) {
541 return false;
542 }
543
544 // "Take the action for command, passing value to the instructions as an
545 // argument."
546 var ret = commands[command].action(value);
547
548 // Check for bugs
549 if (ret !== true && ret !== false) {
550 throw "execCommand() didn't return true or false: " + ret;
551 }
552
553 // "If the previous step returned false, return false."
554 if (ret === false) {
555 return false;
556 }
557
558 // "Return true."
559 return true;
560 }})(command, showUi, value));
561 }
562
563 function myQueryCommandEnabled(command, range) {
564 // "All of these methods must treat their command argument ASCII
565 // case-insensitively."
566 command = command.toLowerCase();
567
568 return editCommandMethod(command, range, (function(command) { return function() {
569 // "Return true if command is both supported and enabled, false
570 // otherwise."
571 if (!(command in commands)) {
572 return false;
573 }
574
575 // "Among commands defined in this specification, those listed in
576 // Miscellaneous commands are always enabled, except for the cut
577 // command and the paste command. The other commands defined here are
578 // enabled if the active range is not null, its start node is either
579 // editable or an editing host, its end node is either editable or an
580 // editing host, and there is some editing host that is an inclusive
581 // ancestor of both its start node and its end node."
582 return ["copy", "defaultparagraphseparator", "selectall", "stylewithcss",
583 "usecss"].indexOf(command) != -1
584 || (
585 getActiveRange() !== null
586 && (isEditable(getActiveRange().startContainer) || isEditingHost(getActiveRange().startContainer))
587 && (isEditable(getActiveRange().endContainer) || isEditingHost(getActiveRange().endContainer))
588 && (getInclusiveAncestors(getActiveRange().commonAncestorContainer).some(isEditingHost))
589 );
590 }})(command));
591 }
592
593 function myQueryCommandIndeterm(command, range) {
594 // "All of these methods must treat their command argument ASCII
595 // case-insensitively."
596 command = command.toLowerCase();
597
598 return editCommandMethod(command, range, (function(command) { return function() {
599 // "If command is not supported or has no indeterminacy, return false."
600 if (!(command in commands) || !("indeterm" in commands[command])) {
601 return false;
602 }
603
604 // "Return true if command is indeterminate, otherwise false."
605 return commands[command].indeterm();
606 }})(command));
607 }
608
609 function myQueryCommandState(command, range) {
610 // "All of these methods must treat their command argument ASCII
611 // case-insensitively."
612 command = command.toLowerCase();
613
614 return editCommandMethod(command, range, (function(command) { return function() {
615 // "If command is not supported or has no state, return false."
616 if (!(command in commands) || !("state" in commands[command])) {
617 return false;
618 }
619
620 // "If the state override for command is set, return it."
621 if (typeof getStateOverride(command) != "undefined") {
622 return getStateOverride(command);
623 }
624
625 // "Return true if command's state is true, otherwise false."
626 return commands[command].state();
627 }})(command));
628 }
629
630 // "When the queryCommandSupported(command) method on the HTMLDocument
631 // interface is invoked, the user agent must return true if command is
632 // supported, and false otherwise."
633 function myQueryCommandSupported(command) {
634 // "All of these methods must treat their command argument ASCII
635 // case-insensitively."
636 command = command.toLowerCase();
637
638 return command in commands;
639 }
640
641 function myQueryCommandValue(command, range) {
642 // "All of these methods must treat their command argument ASCII
643 // case-insensitively."
644 command = command.toLowerCase();
645
646 return editCommandMethod(command, range, function() {
647 // "If command is not supported or has no value, return the empty string."
648 if (!(command in commands) || !("value" in commands[command])) {
649 return "";
650 }
651
652 // "If command is "fontSize" and its value override is set, convert the
653 // value override to an integer number of pixels and return the legacy
654 // font size for the result."
655 if (command == "fontsize"
656 && getValueOverride("fontsize") !== undefined) {
657 return getLegacyFontSize(getValueOverride("fontsize"));
658 }
659
660 // "If the value override for command is set, return it."
661 if (typeof getValueOverride(command) != "undefined") {
662 return getValueOverride(command);
663 }
664
665 // "Return command's value."
666 return commands[command].value();
667 });
668 }
669 //@}
670
671 //////////////////////////////
672 ///// Common definitions /////
673 //////////////////////////////
674 //@{
675
676 // "An HTML element is an Element whose namespace is the HTML namespace."
677 //
678 // I allow an extra argument to more easily check whether something is a
679 // particular HTML element, like isHtmlElement(node, "OL"). It accepts arrays
680 // too, like isHtmlElement(node, ["OL", "UL"]) to check if it's an ol or ul.
681 function isHtmlElement(node, tags) {
682 if (typeof tags == "string") {
683 tags = [tags];
684 }
685 if (typeof tags == "object") {
686 tags = tags.map(function(tag) { return tag.toUpperCase() });
687 }
688 return node
689 && node.nodeType == Node.ELEMENT_NODE
690 && isHtmlNamespace(node.namespaceURI)
691 && (typeof tags == "undefined" || tags.indexOf(node.tagName) != -1);
692 }
693
694 // "A prohibited paragraph child name is "address", "article", "aside",
695 // "blockquote", "caption", "center", "col", "colgroup", "dd", "details",
696 // "dir", "div", "dl", "dt", "fieldset", "figcaption", "figure", "footer",
697 // "form", "h1", "h2", "h3", "h4", "h5", "h6", "header", "hgroup", "hr", "li",
698 // "listing", "menu", "nav", "ol", "p", "plaintext", "pre", "section",
699 // "summary", "table", "tbody", "td", "tfoot", "th", "thead", "tr", "ul", or
700 // "xmp"."
701 var prohibitedParagraphChildNames = ["address", "article", "aside",
702 "blockquote", "caption", "center", "col", "colgroup", "dd", "details",
703 "dir", "div", "dl", "dt", "fieldset", "figcaption", "figure", "footer",
704 "form", "h1", "h2", "h3", "h4", "h5", "h6", "header", "hgroup", "hr", "li",
705 "listing", "menu", "nav", "ol", "p", "plaintext", "pre", "section",
706 "summary", "table", "tbody", "td", "tfoot", "th", "thead", "tr", "ul",
707 "xmp"];
708
709 // "A prohibited paragraph child is an HTML element whose local name is a
710 // prohibited paragraph child name."
711 function isProhibitedParagraphChild(node) {
712 return isHtmlElement(node, prohibitedParagraphChildNames);
713 }
714
715 // "A block node is either an Element whose "display" property does not have
716 // resolved value "inline" or "inline-block" or "inline-table" or "none", or a
717 // Document, or a DocumentFragment."
718 function isBlockNode(node) {
719 return node
720 && ((node.nodeType == Node.ELEMENT_NODE && ["inline", "inline-block", "inline-table", "none"].indexOf(getComputedStyle(node).display) == -1)
721 || node.nodeType == Node.DOCUMENT_NODE
722 || node.nodeType == Node.DOCUMENT_FRAGMENT_NODE);
723 }
724
725 // "An inline node is a node that is not a block node."
726 function isInlineNode(node) {
727 return node && !isBlockNode(node);
728 }
729
730 // "An editing host is a node that is either an HTML element with a
731 // contenteditable attribute set to the true state, or the HTML element child
732 // of a Document whose designMode is enabled."
733 function isEditingHost(node) {
734 return node
735 && isHtmlElement(node)
736 && (node.contentEditable == "true"
737 || (node.parentNode
738 && node.parentNode.nodeType == Node.DOCUMENT_NODE
739 && node.parentNode.designMode == "on"));
740 }
741
742 // "Something is editable if it is a node; it is not an editing host; it does
743 // not have a contenteditable attribute set to the false state; its parent is
744 // an editing host or editable; and either it is an HTML element, or it is an
745 // svg or math element, or it is not an Element and its parent is an HTML
746 // element."
747 function isEditable(node) {
748 return node
749 && !isEditingHost(node)
750 && (node.nodeType != Node.ELEMENT_NODE || node.contentEditable != "false")
751 && (isEditingHost(node.parentNode) || isEditable(node.parentNode))
752 && (isHtmlElement(node)
753 || (node.nodeType == Node.ELEMENT_NODE && node.namespaceURI == "http://www.w3.org/2000/svg" && node.localName == "svg")
754 || (node.nodeType == Node.ELEMENT_NODE && node.namespaceURI == "http://www.w3.org/1998/Math/MathML" && node.localName == "math")
755 || (node.nodeType != Node.ELEMENT_NODE && isHtmlElement(node.parentNode)));
756 }
757
758 // Helper function, not defined in the spec
759 function hasEditableDescendants(node) {
760 for (var i = 0; i < node.childNodes.length; i++) {
761 if (isEditable(node.childNodes[i])
762 || hasEditableDescendants(node.childNodes[i])) {
763 return true;
764 }
765 }
766 return false;
767 }
768
769 // "The editing host of node is null if node is neither editable nor an editing
770 // host; node itself, if node is an editing host; or the nearest ancestor of
771 // node that is an editing host, if node is editable."
772 function getEditingHostOf(node) {
773 if (isEditingHost(node)) {
774 return node;
775 } else if (isEditable(node)) {
776 var ancestor = node.parentNode;
777 while (!isEditingHost(ancestor)) {
778 ancestor = ancestor.parentNode;
779 }
780 return ancestor;
781 } else {
782 return null;
783 }
784 }
785
786 // "Two nodes are in the same editing host if the editing host of the first is
787 // non-null and the same as the editing host of the second."
788 function inSameEditingHost(node1, node2) {
789 return getEditingHostOf(node1)
790 && getEditingHostOf(node1) == getEditingHostOf(node2);
791 }
792
793 // "A collapsed line break is a br that begins a line box which has nothing
794 // else in it, and therefore has zero height."
795 function isCollapsedLineBreak(br) {
796 if (!isHtmlElement(br, "br")) {
797 return false;
798 }
799
800 // Add a zwsp after it and see if that changes the height of the nearest
801 // non-inline parent. Note: this is not actually reliable, because the
802 // parent might have a fixed height or something.
803 var ref = br.parentNode;
804 while (getComputedStyle(ref).display == "inline") {
805 ref = ref.parentNode;
806 }
807 var refStyle = ref.hasAttribute("style") ? ref.getAttribute("style") : null;
808 ref.style.height = "auto";
809 ref.style.maxHeight = "none";
810 ref.style.minHeight = "0";
811 var space = document.createTextNode("\u200b");
812 var origHeight = ref.offsetHeight;
813 if (origHeight == 0) {
814 throw "isCollapsedLineBreak: original height is zero, bug?";
815 }
816 br.parentNode.insertBefore(space, br.nextSibling);
817 var finalHeight = ref.offsetHeight;
818 space.parentNode.removeChild(space);
819 if (refStyle === null) {
820 // Without the setAttribute() line, removeAttribute() doesn't work in
821 // Chrome 14 dev. I have no idea why.
822 ref.setAttribute("style", "");
823 ref.removeAttribute("style");
824 } else {
825 ref.setAttribute("style", refStyle);
826 }
827
828 // Allow some leeway in case the zwsp didn't create a whole new line, but
829 // only made an existing line slightly higher. Firefox 6.0a2 shows this
830 // behavior when the first line is bold.
831 return origHeight < finalHeight - 5;
832 }
833
834 // "An extraneous line break is a br that has no visual effect, in that
835 // removing it from the DOM would not change layout, except that a br that is
836 // the sole child of an li is not extraneous."
837 //
838 // FIXME: This doesn't work in IE, since IE ignores display: none in
839 // contenteditable.
840 function isExtraneousLineBreak(br) {
841 if (!isHtmlElement(br, "br")) {
842 return false;
843 }
844
845 if (isHtmlElement(br.parentNode, "li")
846 && br.parentNode.childNodes.length == 1) {
847 return false;
848 }
849
850 // Make the line break disappear and see if that changes the block's
851 // height. Yes, this is an absurd hack. We have to reset height etc. on
852 // the reference node because otherwise its height won't change if it's not
853 // auto.
854 var ref = br.parentNode;
855 while (getComputedStyle(ref).display == "inline") {
856 ref = ref.parentNode;
857 }
858 var refStyle = ref.hasAttribute("style") ? ref.getAttribute("style") : null;
859 ref.style.height = "auto";
860 ref.style.maxHeight = "none";
861 ref.style.minHeight = "0";
862 var brStyle = br.hasAttribute("style") ? br.getAttribute("style") : null;
863 var origHeight = ref.offsetHeight;
864 if (origHeight == 0) {
865 throw "isExtraneousLineBreak: original height is zero, bug?";
866 }
867 br.setAttribute("style", "display:none");
868 var finalHeight = ref.offsetHeight;
869 if (refStyle === null) {
870 // Without the setAttribute() line, removeAttribute() doesn't work in
871 // Chrome 14 dev. I have no idea why.
872 ref.setAttribute("style", "");
873 ref.removeAttribute("style");
874 } else {
875 ref.setAttribute("style", refStyle);
876 }
877 if (brStyle === null) {
878 br.removeAttribute("style");
879 } else {
880 br.setAttribute("style", brStyle);
881 }
882
883 return origHeight == finalHeight;
884 }
885
886 // "A whitespace node is either a Text node whose data is the empty string; or
887 // a Text node whose data consists only of one or more tabs (0x0009), line
888 // feeds (0x000A), carriage returns (0x000D), and/or spaces (0x0020), and whose
889 // parent is an Element whose resolved value for "white-space" is "normal" or
890 // "nowrap"; or a Text node whose data consists only of one or more tabs
891 // (0x0009), carriage returns (0x000D), and/or spaces (0x0020), and whose
892 // parent is an Element whose resolved value for "white-space" is "pre-line"."
893 function isWhitespaceNode(node) {
894 return node
895 && node.nodeType == Node.TEXT_NODE
896 && (node.data == ""
897 || (
898 /^[\t\n\r ]+$/.test(node.data)
899 && node.parentNode
900 && node.parentNode.nodeType == Node.ELEMENT_NODE
901 && ["normal", "nowrap"].indexOf(getComputedStyle(node.parentNode).whiteSpace) != -1
902 ) || (
903 /^[\t\r ]+$/.test(node.data)
904 && node.parentNode
905 && node.parentNode.nodeType == Node.ELEMENT_NODE
906 && getComputedStyle(node.parentNode).whiteSpace == "pre-line"
907 ));
908 }
909
910 // "node is a collapsed whitespace node if the following algorithm returns
911 // true:"
912 function isCollapsedWhitespaceNode(node) {
913 // "If node is not a whitespace node, return false."
914 if (!isWhitespaceNode(node)) {
915 return false;
916 }
917
918 // "If node's data is the empty string, return true."
919 if (node.data == "") {
920 return true;
921 }
922
923 // "Let ancestor be node's parent."
924 var ancestor = node.parentNode;
925
926 // "If ancestor is null, return true."
927 if (!ancestor) {
928 return true;
929 }
930
931 // "If the "display" property of some ancestor of node has resolved value
932 // "none", return true."
933 if (getAncestors(node).some(function(ancestor) {
934 return ancestor.nodeType == Node.ELEMENT_NODE
935 && getComputedStyle(ancestor).display == "none";
936 })) {
937 return true;
938 }
939
940 // "While ancestor is not a block node and its parent is not null, set
941 // ancestor to its parent."
942 while (!isBlockNode(ancestor)
943 && ancestor.parentNode) {
944 ancestor = ancestor.parentNode;
945 }
946
947 // "Let reference be node."
948 var reference = node;
949
950 // "While reference is a descendant of ancestor:"
951 while (reference != ancestor) {
952 // "Let reference be the node before it in tree order."
953 reference = previousNode(reference);
954
955 // "If reference is a block node or a br, return true."
956 if (isBlockNode(reference)
957 || isHtmlElement(reference, "br")) {
958 return true;
959 }
960
961 // "If reference is a Text node that is not a whitespace node, or is an
962 // img, break from this loop."
963 if ((reference.nodeType == Node.TEXT_NODE && !isWhitespaceNode(reference))
964 || isHtmlElement(reference, "img")) {
965 break;
966 }
967 }
968
969 // "Let reference be node."
970 reference = node;
971
972 // "While reference is a descendant of ancestor:"
973 var stop = nextNodeDescendants(ancestor);
974 while (reference != stop) {
975 // "Let reference be the node after it in tree order, or null if there
976 // is no such node."
977 reference = nextNode(reference);
978
979 // "If reference is a block node or a br, return true."
980 if (isBlockNode(reference)
981 || isHtmlElement(reference, "br")) {
982 return true;
983 }
984
985 // "If reference is a Text node that is not a whitespace node, or is an
986 // img, break from this loop."
987 if ((reference && reference.nodeType == Node.TEXT_NODE && !isWhitespaceNode(reference))
988 || isHtmlElement(reference, "img")) {
989 break;
990 }
991 }
992
993 // "Return false."
994 return false;
995 }
996
997 // "Something is visible if it is a node that either is a block node, or a Text
998 // node that is not a collapsed whitespace node, or an img, or a br that is not
999 // an extraneous line break, or any node with a visible descendant; excluding
1000 // any node with an ancestor container Element whose "display" property has
1001 // resolved value "none"."
1002 function isVisible(node) {
1003 if (!node) {
1004 return false;
1005 }
1006
1007 if (getAncestors(node).concat(node)
1008 .filter(function(node) { return node.nodeType == Node.ELEMENT_NODE })
1009 .some(function(node) { return getComputedStyle(node).display == "none" })) {
1010 return false;
1011 }
1012
1013 if (isBlockNode(node)
1014 || (node.nodeType == Node.TEXT_NODE && !isCollapsedWhitespaceNode(node))
1015 || isHtmlElement(node, "img")
1016 || (isHtmlElement(node, "br") && !isExtraneousLineBreak(node))) {
1017 return true;
1018 }
1019
1020 for (var i = 0; i < node.childNodes.length; i++) {
1021 if (isVisible(node.childNodes[i])) {
1022 return true;
1023 }
1024 }
1025
1026 return false;
1027 }
1028
1029 // "Something is invisible if it is a node that is not visible."
1030 function isInvisible(node) {
1031 return node && !isVisible(node);
1032 }
1033
1034 // "A collapsed block prop is either a collapsed line break that is not an
1035 // extraneous line break, or an Element that is an inline node and whose
1036 // children are all either invisible or collapsed block props and that has at
1037 // least one child that is a collapsed block prop."
1038 function isCollapsedBlockProp(node) {
1039 if (isCollapsedLineBreak(node)
1040 && !isExtraneousLineBreak(node)) {
1041 return true;
1042 }
1043
1044 if (!isInlineNode(node)
1045 || node.nodeType != Node.ELEMENT_NODE) {
1046 return false;
1047 }
1048
1049 var hasCollapsedBlockPropChild = false;
1050 for (var i = 0; i < node.childNodes.length; i++) {
1051 if (!isInvisible(node.childNodes[i])
1052 && !isCollapsedBlockProp(node.childNodes[i])) {
1053 return false;
1054 }
1055 if (isCollapsedBlockProp(node.childNodes[i])) {
1056 hasCollapsedBlockPropChild = true;
1057 }
1058 }
1059
1060 return hasCollapsedBlockPropChild;
1061 }
1062
1063 // "The active range is the range of the selection given by calling
1064 // getSelection() on the context object. (Thus the active range may be null.)"
1065 //
1066 // We cheat and return globalRange if that's defined. We also ensure that the
1067 // active range meets the requirements that selection boundary points are
1068 // supposed to meet, i.e., that the nodes are both Text or Element nodes that
1069 // descend from a Document.
1070 function getActiveRange() {
1071 var ret;
1072 if (globalRange) {
1073 ret = globalRange;
1074 } else if (getSelection().rangeCount) {
1075 ret = getSelection().getRangeAt(0);
1076 } else {
1077 return null;
1078 }
1079 if ([Node.TEXT_NODE, Node.ELEMENT_NODE].indexOf(ret.startContainer.nodeType) == -1
1080 || [Node.TEXT_NODE, Node.ELEMENT_NODE].indexOf(ret.endContainer.nodeType) == -1
1081 || !ret.startContainer.ownerDocument
1082 || !ret.endContainer.ownerDocument
1083 || !isDescendant(ret.startContainer, ret.startContainer.ownerDocument)
1084 || !isDescendant(ret.endContainer, ret.endContainer.ownerDocument)) {
1085 throw "Invalid active range; test bug?";
1086 }
1087 return ret;
1088 }
1089
1090 // "For some commands, each HTMLDocument must have a boolean state override
1091 // and/or a string value override. These do not change the command's state or
1092 // value, but change the way some algorithms behave, as specified in those
1093 // algorithms' definitions. Initially, both must be unset for every command.
1094 // Whenever the number of ranges in the Selection changes to something
1095 // different, and whenever a boundary point of the range at a given index in
1096 // the Selection changes to something different, the state override and value
1097 // override must be unset for every command."
1098 //
1099 // We implement this crudely by using setters and getters. To verify that the
1100 // selection hasn't changed, we copy the active range and just check the
1101 // endpoints match. This isn't really correct, but it's good enough for us.
1102 // Unset state/value overrides are undefined. We put everything in a function
1103 // so no one can access anything except via the provided functions, since
1104 // otherwise callers might mistakenly use outdated overrides (if the selection
1105 // has changed).
1106 var getStateOverride, setStateOverride, unsetStateOverride,
1107 getValueOverride, setValueOverride, unsetValueOverride;
1108 (function() {
1109 var stateOverrides = {};
1110 var valueOverrides = {};
1111 var storedRange = null;
1112
1113 function resetOverrides() {
1114 if (!storedRange
1115 || storedRange.startContainer != getActiveRange().startContainer
1116 || storedRange.endContainer != getActiveRange().endContainer
1117 || storedRange.startOffset != getActiveRange().startOffset
1118 || storedRange.endOffset != getActiveRange().endOffset) {
1119 stateOverrides = {};
1120 valueOverrides = {};
1121 storedRange = getActiveRange().cloneRange();
1122 }
1123 }
1124
1125 getStateOverride = function(command) {
1126 resetOverrides();
1127 return stateOverrides[command];
1128 };
1129
1130 setStateOverride = function(command, newState) {
1131 resetOverrides();
1132 stateOverrides[command] = newState;
1133 };
1134
1135 unsetStateOverride = function(command) {
1136 resetOverrides();
1137 delete stateOverrides[command];
1138 }
1139
1140 getValueOverride = function(command) {
1141 resetOverrides();
1142 return valueOverrides[command];
1143 }
1144
1145 // "The value override for the backColor command must be the same as the
1146 // value override for the hiliteColor command, such that setting one sets
1147 // the other to the same thing and unsetting one unsets the other."
1148 setValueOverride = function(command, newValue) {
1149 resetOverrides();
1150 valueOverrides[command] = newValue;
1151 if (command == "backcolor") {
1152 valueOverrides.hilitecolor = newValue;
1153 } else if (command == "hilitecolor") {
1154 valueOverrides.backcolor = newValue;
1155 }
1156 }
1157
1158 unsetValueOverride = function(command) {
1159 resetOverrides();
1160 delete valueOverrides[command];
1161 if (command == "backcolor") {
1162 delete valueOverrides.hilitecolor;
1163 } else if (command == "hilitecolor") {
1164 delete valueOverrides.backcolor;
1165 }
1166 }
1167 })();
1168
1169 //@}
1170
1171 /////////////////////////////
1172 ///// Common algorithms /////
1173 /////////////////////////////
1174
1175 ///// Assorted common algorithms /////
1176 //@{
1177
1178 // Magic array of extra ranges whose endpoints we want to preserve.
1179 var extraRanges = [];
1180
1181 function movePreservingRanges(node, newParent, newIndex) {
1182 // For convenience, I allow newIndex to be -1 to mean "insert at the end".
1183 if (newIndex == -1) {
1184 newIndex = newParent.childNodes.length;
1185 }
1186
1187 // "When the user agent is to move a Node to a new location, preserving
1188 // ranges, it must remove the Node from its original parent (if any), then
1189 // insert it in the new location. In doing so, however, it must ignore the
1190 // regular range mutation rules, and instead follow these rules:"
1191
1192 // "Let node be the moved Node, old parent and old index be the old parent
1193 // (which may be null) and index, and new parent and new index be the new
1194 // parent and index."
1195 var oldParent = node.parentNode;
1196 var oldIndex = getNodeIndex(node);
1197
1198 // We preserve the global range object, the ranges in the selection, and
1199 // any range that's in the extraRanges array. Any other ranges won't get
1200 // updated, because we have no references to them.
1201 var ranges = [globalRange].concat(extraRanges);
1202 for (var i = 0; i < getSelection().rangeCount; i++) {
1203 ranges.push(getSelection().getRangeAt(i));
1204 }
1205 var boundaryPoints = [];
1206 ranges.forEach(function(range) {
1207 boundaryPoints.push([range.startContainer, range.startOffset]);
1208 boundaryPoints.push([range.endContainer, range.endOffset]);
1209 });
1210
1211 boundaryPoints.forEach(function(boundaryPoint) {
1212 // "If a boundary point's node is the same as or a descendant of node,
1213 // leave it unchanged, so it moves to the new location."
1214 //
1215 // No modifications necessary.
1216
1217 // "If a boundary point's node is new parent and its offset is greater
1218 // than new index, add one to its offset."
1219 if (boundaryPoint[0] == newParent
1220 && boundaryPoint[1] > newIndex) {
1221 boundaryPoint[1]++;
1222 }
1223
1224 // "If a boundary point's node is old parent and its offset is old index or
1225 // old index + 1, set its node to new parent and add new index − old index
1226 // to its offset."
1227 if (boundaryPoint[0] == oldParent
1228 && (boundaryPoint[1] == oldIndex
1229 || boundaryPoint[1] == oldIndex + 1)) {
1230 boundaryPoint[0] = newParent;
1231 boundaryPoint[1] += newIndex - oldIndex;
1232 }
1233
1234 // "If a boundary point's node is old parent and its offset is greater than
1235 // old index + 1, subtract one from its offset."
1236 if (boundaryPoint[0] == oldParent
1237 && boundaryPoint[1] > oldIndex + 1) {
1238 boundaryPoint[1]--;
1239 }
1240 });
1241
1242 // Now actually move it and preserve the ranges.
1243 if (newParent.childNodes.length == newIndex) {
1244 newParent.appendChild(node);
1245 } else {
1246 newParent.insertBefore(node, newParent.childNodes[newIndex]);
1247 }
1248
1249 globalRange.setStart(boundaryPoints[0][0], boundaryPoints[0][1]);
1250 globalRange.setEnd(boundaryPoints[1][0], boundaryPoints[1][1]);
1251
1252 for (var i = 0; i < extraRanges.length; i++) {
1253 extraRanges[i].setStart(boundaryPoints[2*i + 2][0], boundaryPoints[2*i + 2][1]);
1254 extraRanges[i].setEnd(boundaryPoints[2*i + 3][0], boundaryPoints[2*i + 3][1]);
1255 }
1256
1257 getSelection().removeAllRanges();
1258 for (var i = 1 + extraRanges.length; i < ranges.length; i++) {
1259 var newRange = document.createRange();
1260 newRange.setStart(boundaryPoints[2*i][0], boundaryPoints[2*i][1]);
1261 newRange.setEnd(boundaryPoints[2*i + 1][0], boundaryPoints[2*i + 1][1]);
1262 getSelection().addRange(newRange);
1263 }
1264 }
1265
1266 function setTagName(element, newName) {
1267 // "If element is an HTML element with local name equal to new name, return
1268 // element."
1269 if (isHtmlElement(element, newName.toUpperCase())) {
1270 return element;
1271 }
1272
1273 // "If element's parent is null, return element."
1274 if (!element.parentNode) {
1275 return element;
1276 }
1277
1278 // "Let replacement element be the result of calling createElement(new
1279 // name) on the ownerDocument of element."
1280 var replacementElement = element.ownerDocument.createElement(newName);
1281
1282 // "Insert replacement element into element's parent immediately before
1283 // element."
1284 element.parentNode.insertBefore(replacementElement, element);
1285
1286 // "Copy all attributes of element to replacement element, in order."
1287 for (var i = 0; i < element.attributes.length; i++) {
1288 replacementElement.setAttributeNS(element.attributes[i].namespaceURI, element.attributes[i].name, element.attributes[i].value);
1289 }
1290
1291 // "While element has children, append the first child of element as the
1292 // last child of replacement element, preserving ranges."
1293 while (element.childNodes.length) {
1294 movePreservingRanges(element.firstChild, replacementElement, replacementElement.childNodes.length);
1295 }
1296
1297 // "Remove element from its parent."
1298 element.parentNode.removeChild(element);
1299
1300 // "Return replacement element."
1301 return replacementElement;
1302 }
1303
1304 function removeExtraneousLineBreaksBefore(node) {
1305 // "Let ref be the previousSibling of node."
1306 var ref = node.previousSibling;
1307
1308 // "If ref is null, abort these steps."
1309 if (!ref) {
1310 return;
1311 }
1312
1313 // "While ref has children, set ref to its lastChild."
1314 while (ref.hasChildNodes()) {
1315 ref = ref.lastChild;
1316 }
1317
1318 // "While ref is invisible but not an extraneous line break, and ref does
1319 // not equal node's parent, set ref to the node before it in tree order."
1320 while (isInvisible(ref)
1321 && !isExtraneousLineBreak(ref)
1322 && ref != node.parentNode) {
1323 ref = previousNode(ref);
1324 }
1325
1326 // "If ref is an editable extraneous line break, remove it from its
1327 // parent."
1328 if (isEditable(ref)
1329 && isExtraneousLineBreak(ref)) {
1330 ref.parentNode.removeChild(ref);
1331 }
1332 }
1333
1334 function removeExtraneousLineBreaksAtTheEndOf(node) {
1335 // "Let ref be node."
1336 var ref = node;
1337
1338 // "While ref has children, set ref to its lastChild."
1339 while (ref.hasChildNodes()) {
1340 ref = ref.lastChild;
1341 }
1342
1343 // "While ref is invisible but not an extraneous line break, and ref does
1344 // not equal node, set ref to the node before it in tree order."
1345 while (isInvisible(ref)
1346 && !isExtraneousLineBreak(ref)
1347 && ref != node) {
1348 ref = previousNode(ref);
1349 }
1350
1351 // "If ref is an editable extraneous line break:"
1352 if (isEditable(ref)
1353 && isExtraneousLineBreak(ref)) {
1354 // "While ref's parent is editable and invisible, set ref to its
1355 // parent."
1356 while (isEditable(ref.parentNode)
1357 && isInvisible(ref.parentNode)) {
1358 ref = ref.parentNode;
1359 }
1360
1361 // "Remove ref from its parent."
1362 ref.parentNode.removeChild(ref);
1363 }
1364 }
1365
1366 // "To remove extraneous line breaks from a node, first remove extraneous line
1367 // breaks before it, then remove extraneous line breaks at the end of it."
1368 function removeExtraneousLineBreaksFrom(node) {
1369 removeExtraneousLineBreaksBefore(node);
1370 removeExtraneousLineBreaksAtTheEndOf(node);
1371 }
1372
1373 //@}
1374 ///// Wrapping a list of nodes /////
1375 //@{
1376
1377 function wrap(nodeList, siblingCriteria, newParentInstructions) {
1378 // "If not provided, sibling criteria returns false and new parent
1379 // instructions returns null."
1380 if (typeof siblingCriteria == "undefined") {
1381 siblingCriteria = function() { return false };
1382 }
1383 if (typeof newParentInstructions == "undefined") {
1384 newParentInstructions = function() { return null };
1385 }
1386
1387 // "If every member of node list is invisible, and none is a br, return
1388 // null and abort these steps."
1389 if (nodeList.every(isInvisible)
1390 && !nodeList.some(function(node) { return isHtmlElement(node, "br") })) {
1391 return null;
1392 }
1393
1394 // "If node list's first member's parent is null, return null and abort
1395 // these steps."
1396 if (!nodeList[0].parentNode) {
1397 return null;
1398 }
1399
1400 // "If node list's last member is an inline node that's not a br, and node
1401 // list's last member's nextSibling is a br, append that br to node list."
1402 if (isInlineNode(nodeList[nodeList.length - 1])
1403 && !isHtmlElement(nodeList[nodeList.length - 1], "br")
1404 && isHtmlElement(nodeList[nodeList.length - 1].nextSibling, "br")) {
1405 nodeList.push(nodeList[nodeList.length - 1].nextSibling);
1406 }
1407
1408 // "While node list's first member's previousSibling is invisible, prepend
1409 // it to node list."
1410 while (isInvisible(nodeList[0].previousSibling)) {
1411 nodeList.unshift(nodeList[0].previousSibling);
1412 }
1413
1414 // "While node list's last member's nextSibling is invisible, append it to
1415 // node list."
1416 while (isInvisible(nodeList[nodeList.length - 1].nextSibling)) {
1417 nodeList.push(nodeList[nodeList.length - 1].nextSibling);
1418 }
1419
1420 // "If the previousSibling of the first member of node list is editable and
1421 // running sibling criteria on it returns true, let new parent be the
1422 // previousSibling of the first member of node list."
1423 var newParent;
1424 if (isEditable(nodeList[0].previousSibling)
1425 && siblingCriteria(nodeList[0].previousSibling)) {
1426 newParent = nodeList[0].previousSibling;
1427
1428 // "Otherwise, if the nextSibling of the last member of node list is
1429 // editable and running sibling criteria on it returns true, let new parent
1430 // be the nextSibling of the last member of node list."
1431 } else if (isEditable(nodeList[nodeList.length - 1].nextSibling)
1432 && siblingCriteria(nodeList[nodeList.length - 1].nextSibling)) {
1433 newParent = nodeList[nodeList.length - 1].nextSibling;
1434
1435 // "Otherwise, run new parent instructions, and let new parent be the
1436 // result."
1437 } else {
1438 newParent = newParentInstructions();
1439 }
1440
1441 // "If new parent is null, abort these steps and return null."
1442 if (!newParent) {
1443 return null;
1444 }
1445
1446 // "If new parent's parent is null:"
1447 if (!newParent.parentNode) {
1448 // "Insert new parent into the parent of the first member of node list
1449 // immediately before the first member of node list."
1450 nodeList[0].parentNode.insertBefore(newParent, nodeList[0]);
1451
1452 // "If any range has a boundary point with node equal to the parent of
1453 // new parent and offset equal to the index of new parent, add one to
1454 // that boundary point's offset."
1455 //
1456 // Only try to fix the global range.
1457 if (globalRange.startContainer == newParent.parentNode
1458 && globalRange.startOffset == getNodeIndex(newParent)) {
1459 globalRange.setStart(globalRange.startContainer, globalRange.startOffset + 1);
1460 }
1461 if (globalRange.endContainer == newParent.parentNode
1462 && globalRange.endOffset == getNodeIndex(newParent)) {
1463 globalRange.setEnd(globalRange.endContainer, globalRange.endOffset + 1);
1464 }
1465 }
1466
1467 // "Let original parent be the parent of the first member of node list."
1468 var originalParent = nodeList[0].parentNode;
1469
1470 // "If new parent is before the first member of node list in tree order:"
1471 if (isBefore(newParent, nodeList[0])) {
1472 // "If new parent is not an inline node, but the last visible child of
1473 // new parent and the first visible member of node list are both inline
1474 // nodes, and the last child of new parent is not a br, call
1475 // createElement("br") on the ownerDocument of new parent and append
1476 // the result as the last child of new parent."
1477 if (!isInlineNode(newParent)
1478 && isInlineNode([].filter.call(newParent.childNodes, isVisible).slice(-1)[0])
1479 && isInlineNode(nodeList.filter(isVisible)[0])
1480 && !isHtmlElement(newParent.lastChild, "BR")) {
1481 newParent.appendChild(newParent.ownerDocument.createElement("br"));
1482 }
1483
1484 // "For each node in node list, append node as the last child of new
1485 // parent, preserving ranges."
1486 for (var i = 0; i < nodeList.length; i++) {
1487 movePreservingRanges(nodeList[i], newParent, -1);
1488 }
1489
1490 // "Otherwise:"
1491 } else {
1492 // "If new parent is not an inline node, but the first visible child of
1493 // new parent and the last visible member of node list are both inline
1494 // nodes, and the last member of node list is not a br, call
1495 // createElement("br") on the ownerDocument of new parent and insert
1496 // the result as the first child of new parent."
1497 if (!isInlineNode(newParent)
1498 && isInlineNode([].filter.call(newParent.childNodes, isVisible)[0])
1499 && isInlineNode(nodeList.filter(isVisible).slice(-1)[0])
1500 && !isHtmlElement(nodeList[nodeList.length - 1], "BR")) {
1501 newParent.insertBefore(newParent.ownerDocument.createElement("br"), newParent.firstChild);
1502 }
1503
1504 // "For each node in node list, in reverse order, insert node as the
1505 // first child of new parent, preserving ranges."
1506 for (var i = nodeList.length - 1; i >= 0; i--) {
1507 movePreservingRanges(nodeList[i], newParent, 0);
1508 }
1509 }
1510
1511 // "If original parent is editable and has no children, remove it from its
1512 // parent."
1513 if (isEditable(originalParent) && !originalParent.hasChildNodes()) {
1514 originalParent.parentNode.removeChild(originalParent);
1515 }
1516
1517 // "If new parent's nextSibling is editable and running sibling criteria on
1518 // it returns true:"
1519 if (isEditable(newParent.nextSibling)
1520 && siblingCriteria(newParent.nextSibling)) {
1521 // "If new parent is not an inline node, but new parent's last child
1522 // and new parent's nextSibling's first child are both inline nodes,
1523 // and new parent's last child is not a br, call createElement("br") on
1524 // the ownerDocument of new parent and append the result as the last
1525 // child of new parent."
1526 if (!isInlineNode(newParent)
1527 && isInlineNode(newParent.lastChild)
1528 && isInlineNode(newParent.nextSibling.firstChild)
1529 && !isHtmlElement(newParent.lastChild, "BR")) {
1530 newParent.appendChild(newParent.ownerDocument.createElement("br"));
1531 }
1532
1533 // "While new parent's nextSibling has children, append its first child
1534 // as the last child of new parent, preserving ranges."
1535 while (newParent.nextSibling.hasChildNodes()) {
1536 movePreservingRanges(newParent.nextSibling.firstChild, newParent, -1);
1537 }
1538
1539 // "Remove new parent's nextSibling from its parent."
1540 newParent.parentNode.removeChild(newParent.nextSibling);
1541 }
1542
1543 // "Remove extraneous line breaks from new parent."
1544 removeExtraneousLineBreaksFrom(newParent);
1545
1546 // "Return new parent."
1547 return newParent;
1548 }
1549
1550
1551 //@}
1552 ///// Allowed children /////
1553 //@{
1554
1555 // "A name of an element with inline contents is "a", "abbr", "b", "bdi",
1556 // "bdo", "cite", "code", "dfn", "em", "h1", "h2", "h3", "h4", "h5", "h6", "i",
1557 // "kbd", "mark", "p", "pre", "q", "rp", "rt", "ruby", "s", "samp", "small",
1558 // "span", "strong", "sub", "sup", "u", "var", "acronym", "listing", "strike",
1559 // "xmp", "big", "blink", "font", "marquee", "nobr", or "tt"."
1560 var namesOfElementsWithInlineContents = ["a", "abbr", "b", "bdi", "bdo",
1561 "cite", "code", "dfn", "em", "h1", "h2", "h3", "h4", "h5", "h6", "i",
1562 "kbd", "mark", "p", "pre", "q", "rp", "rt", "ruby", "s", "samp", "small",
1563 "span", "strong", "sub", "sup", "u", "var", "acronym", "listing", "strike",
1564 "xmp", "big", "blink", "font", "marquee", "nobr", "tt"];
1565
1566 // "An element with inline contents is an HTML element whose local name is a
1567 // name of an element with inline contents."
1568 function isElementWithInlineContents(node) {
1569 return isHtmlElement(node, namesOfElementsWithInlineContents);
1570 }
1571
1572 function isAllowedChild(child, parent_) {
1573 // "If parent is "colgroup", "table", "tbody", "tfoot", "thead", "tr", or
1574 // an HTML element with local name equal to one of those, and child is a
1575 // Text node whose data does not consist solely of space characters, return
1576 // false."
1577 if ((["colgroup", "table", "tbody", "tfoot", "thead", "tr"].indexOf(parent_) != -1
1578 || isHtmlElement(parent_, ["colgroup", "table", "tbody", "tfoot", "thead", "tr"]))
1579 && typeof child == "object"
1580 && child.nodeType == Node.TEXT_NODE
1581 && !/^[ \t\n\f\r]*$/.test(child.data)) {
1582 return false;
1583 }
1584
1585 // "If parent is "script", "style", "plaintext", or "xmp", or an HTML
1586 // element with local name equal to one of those, and child is not a Text
1587 // node, return false."
1588 if ((["script", "style", "plaintext", "xmp"].indexOf(parent_) != -1
1589 || isHtmlElement(parent_, ["script", "style", "plaintext", "xmp"]))
1590 && (typeof child != "object" || child.nodeType != Node.TEXT_NODE)) {
1591 return false;
1592 }
1593
1594 // "If child is a Document, DocumentFragment, or DocumentType, return
1595 // false."
1596 if (typeof child == "object"
1597 && (child.nodeType == Node.DOCUMENT_NODE
1598 || child.nodeType == Node.DOCUMENT_FRAGMENT_NODE
1599 || child.nodeType == Node.DOCUMENT_TYPE_NODE)) {
1600 return false;
1601 }
1602
1603 // "If child is an HTML element, set child to the local name of child."
1604 if (isHtmlElement(child)) {
1605 child = child.tagName.toLowerCase();
1606 }
1607
1608 // "If child is not a string, return true."
1609 if (typeof child != "string") {
1610 return true;
1611 }
1612
1613 // "If parent is an HTML element:"
1614 if (isHtmlElement(parent_)) {
1615 // "If child is "a", and parent or some ancestor of parent is an a,
1616 // return false."
1617 //
1618 // "If child is a prohibited paragraph child name and parent or some
1619 // ancestor of parent is an element with inline contents, return
1620 // false."
1621 //
1622 // "If child is "h1", "h2", "h3", "h4", "h5", or "h6", and parent or
1623 // some ancestor of parent is an HTML element with local name "h1",
1624 // "h2", "h3", "h4", "h5", or "h6", return false."
1625 var ancestor = parent_;
1626 while (ancestor) {
1627 if (child == "a" && isHtmlElement(ancestor, "a")) {
1628 return false;
1629 }
1630 if (prohibitedParagraphChildNames.indexOf(child) != -1
1631 && isElementWithInlineContents(ancestor)) {
1632 return false;
1633 }
1634 if (/^h[1-6]$/.test(child)
1635 && isHtmlElement(ancestor)
1636 && /^H[1-6]$/.test(ancestor.tagName)) {
1637 return false;
1638 }
1639 ancestor = ancestor.parentNode;
1640 }
1641
1642 // "Let parent be the local name of parent."
1643 parent_ = parent_.tagName.toLowerCase();
1644 }
1645
1646 // "If parent is an Element or DocumentFragment, return true."
1647 if (typeof parent_ == "object"
1648 && (parent_.nodeType == Node.ELEMENT_NODE
1649 || parent_.nodeType == Node.DOCUMENT_FRAGMENT_NODE)) {
1650 return true;
1651 }
1652
1653 // "If parent is not a string, return false."
1654 if (typeof parent_ != "string") {
1655 return false;
1656 }
1657
1658 // "If parent is on the left-hand side of an entry on the following list,
1659 // then return true if child is listed on the right-hand side of that
1660 // entry, and false otherwise."
1661 switch (parent_) {
1662 case "colgroup":
1663 return child == "col";
1664 case "table":
1665 return ["caption", "col", "colgroup", "tbody", "td", "tfoot", "th", "thead", "tr"].indexOf(child) != -1;
1666 case "tbody":
1667 case "thead":
1668 case "tfoot":
1669 return ["td", "th", "tr"].indexOf(child) != -1;
1670 case "tr":
1671 return ["td", "th"].indexOf(child) != -1;
1672 case "dl":
1673 return ["dt", "dd"].indexOf(child) != -1;
1674 case "dir":
1675 case "ol":
1676 case "ul":
1677 return ["dir", "li", "ol", "ul"].indexOf(child) != -1;
1678 case "hgroup":
1679 return /^h[1-6]$/.test(child);
1680 }
1681
1682 // "If child is "body", "caption", "col", "colgroup", "frame", "frameset",
1683 // "head", "html", "tbody", "td", "tfoot", "th", "thead", or "tr", return
1684 // false."
1685 if (["body", "caption", "col", "colgroup", "frame", "frameset", "head",
1686 "html", "tbody", "td", "tfoot", "th", "thead", "tr"].indexOf(child) != -1) {
1687 return false;
1688 }
1689
1690 // "If child is "dd" or "dt" and parent is not "dl", return false."
1691 if (["dd", "dt"].indexOf(child) != -1
1692 && parent_ != "dl") {
1693 return false;
1694 }
1695
1696 // "If child is "li" and parent is not "ol" or "ul", return false."
1697 if (child == "li"
1698 && parent_ != "ol"
1699 && parent_ != "ul") {
1700 return false;
1701 }
1702
1703 // "If parent is on the left-hand side of an entry on the following list
1704 // and child is listed on the right-hand side of that entry, return false."
1705 var table = [
1706 [["a"], ["a"]],
1707 [["dd", "dt"], ["dd", "dt"]],
1708 [["h1", "h2", "h3", "h4", "h5", "h6"], ["h1", "h2", "h3", "h4", "h5", "h6"]],
1709 [["li"], ["li"]],
1710 [["nobr"], ["nobr"]],
1711 [namesOfElementsWithInlineContents, prohibitedParagraphChildNames],
1712 [["td", "th"], ["caption", "col", "colgroup", "tbody", "td", "tfoot", "th", "thead", "tr"]],
1713 ];
1714 for (var i = 0; i < table.length; i++) {
1715 if (table[i][0].indexOf(parent_) != -1
1716 && table[i][1].indexOf(child) != -1) {
1717 return false;
1718 }
1719 }
1720
1721 // "Return true."
1722 return true;
1723 }
1724
1725
1726 //@}
1727
1728 //////////////////////////////////////
1729 ///// Inline formatting commands /////
1730 //////////////////////////////////////
1731
1732 ///// Inline formatting command definitions /////
1733 //@{
1734
1735 // "A node node is effectively contained in a range range if range is not
1736 // collapsed, and at least one of the following holds:"
1737 function isEffectivelyContained(node, range) {
1738 if (range.collapsed) {
1739 return false;
1740 }
1741
1742 // "node is contained in range."
1743 if (isContained(node, range)) {
1744 return true;
1745 }
1746
1747 // "node is range's start node, it is a Text node, and its length is
1748 // different from range's start offset."
1749 if (node == range.startContainer
1750 && node.nodeType == Node.TEXT_NODE
1751 && getNodeLength(node) != range.startOffset) {
1752 return true;
1753 }
1754
1755 // "node is range's end node, it is a Text node, and range's end offset is
1756 // not 0."
1757 if (node == range.endContainer
1758 && node.nodeType == Node.TEXT_NODE
1759 && range.endOffset != 0) {
1760 return true;
1761 }
1762
1763 // "node has at least one child; and all its children are effectively
1764 // contained in range; and either range's start node is not a descendant of
1765 // node or is not a Text node or range's start offset is zero; and either
1766 // range's end node is not a descendant of node or is not a Text node or
1767 // range's end offset is its end node's length."
1768 if (node.hasChildNodes()
1769 && [].every.call(node.childNodes, function(child) { return isEffectivelyContained(child, range) })
1770 && (!isDescendant(range.startContainer, node)
1771 || range.startContainer.nodeType != Node.TEXT_NODE
1772 || range.startOffset == 0)
1773 && (!isDescendant(range.endContainer, node)
1774 || range.endContainer.nodeType != Node.TEXT_NODE
1775 || range.endOffset == getNodeLength(range.endContainer))) {
1776 return true;
1777 }
1778
1779 return false;
1780 }
1781
1782 // Like get(All)ContainedNodes(), but for effectively contained nodes.
1783 function getEffectivelyContainedNodes(range, condition) {
1784 if (typeof condition == "undefined") {
1785 condition = function() { return true };
1786 }
1787 var node = range.startContainer;
1788 while (isEffectivelyContained(node.parentNode, range)) {
1789 node = node.parentNode;
1790 }
1791
1792 var stop = nextNodeDescendants(range.endContainer);
1793
1794 var nodeList = [];
1795 while (isBefore(node, stop)) {
1796 if (isEffectivelyContained(node, range)
1797 && condition(node)) {
1798 nodeList.push(node);
1799 node = nextNodeDescendants(node);
1800 continue;
1801 }
1802 node = nextNode(node);
1803 }
1804 return nodeList;
1805 }
1806
1807 function getAllEffectivelyContainedNodes(range, condition) {
1808 if (typeof condition == "undefined") {
1809 condition = function() { return true };
1810 }
1811 var node = range.startContainer;
1812 while (isEffectivelyContained(node.parentNode, range)) {
1813 node = node.parentNode;
1814 }
1815
1816 var stop = nextNodeDescendants(range.endContainer);
1817
1818 var nodeList = [];
1819 while (isBefore(node, stop)) {
1820 if (isEffectivelyContained(node, range)
1821 && condition(node)) {
1822 nodeList.push(node);
1823 }
1824 node = nextNode(node);
1825 }
1826 return nodeList;
1827 }
1828
1829 // "A modifiable element is a b, em, i, s, span, strong, sub, sup, or u element
1830 // with no attributes except possibly style; or a font element with no
1831 // attributes except possibly style, color, face, and/or size; or an a element
1832 // with no attributes except possibly style and/or href."
1833 function isModifiableElement(node) {
1834 if (!isHtmlElement(node)) {
1835 return false;
1836 }
1837
1838 if (["B", "EM", "I", "S", "SPAN", "STRIKE", "STRONG", "SUB", "SUP", "U"].indexOf(node.tagName) != -1) {
1839 if (node.attributes.length == 0) {
1840 return true;
1841 }
1842
1843 if (node.attributes.length == 1
1844 && node.hasAttribute("style")) {
1845 return true;
1846 }
1847 }
1848
1849 if (node.tagName == "FONT" || node.tagName == "A") {
1850 var numAttrs = node.attributes.length;
1851
1852 if (node.hasAttribute("style")) {
1853 numAttrs--;
1854 }
1855
1856 if (node.tagName == "FONT") {
1857 if (node.hasAttribute("color")) {
1858 numAttrs--;
1859 }
1860
1861 if (node.hasAttribute("face")) {
1862 numAttrs--;
1863 }
1864
1865 if (node.hasAttribute("size")) {
1866 numAttrs--;
1867 }
1868 }
1869
1870 if (node.tagName == "A"
1871 && node.hasAttribute("href")) {
1872 numAttrs--;
1873 }
1874
1875 if (numAttrs == 0) {
1876 return true;
1877 }
1878 }
1879
1880 return false;
1881 }
1882
1883 function isSimpleModifiableElement(node) {
1884 // "A simple modifiable element is an HTML element for which at least one
1885 // of the following holds:"
1886 if (!isHtmlElement(node)) {
1887 return false;
1888 }
1889
1890 // Only these elements can possibly be a simple modifiable element.
1891 if (["A", "B", "EM", "FONT", "I", "S", "SPAN", "STRIKE", "STRONG", "SUB", "SUP", "U"].indexOf(node.tagName) == -1) {
1892 return false;
1893 }
1894
1895 // "It is an a, b, em, font, i, s, span, strike, strong, sub, sup, or u
1896 // element with no attributes."
1897 if (node.attributes.length == 0) {
1898 return true;
1899 }
1900
1901 // If it's got more than one attribute, everything after this fails.
1902 if (node.attributes.length > 1) {
1903 return false;
1904 }
1905
1906 // "It is an a, b, em, font, i, s, span, strike, strong, sub, sup, or u
1907 // element with exactly one attribute, which is style, which sets no CSS
1908 // properties (including invalid or unrecognized properties)."
1909 //
1910 // Not gonna try for invalid or unrecognized.
1911 if (node.hasAttribute("style")
1912 && node.style.length == 0) {
1913 return true;
1914 }
1915
1916 // "It is an a element with exactly one attribute, which is href."
1917 if (node.tagName == "A"
1918 && node.hasAttribute("href")) {
1919 return true;
1920 }
1921
1922 // "It is a font element with exactly one attribute, which is either color,
1923 // face, or size."
1924 if (node.tagName == "FONT"
1925 && (node.hasAttribute("color")
1926 || node.hasAttribute("face")
1927 || node.hasAttribute("size")
1928 )) {
1929 return true;
1930 }
1931
1932 // "It is a b or strong element with exactly one attribute, which is style,
1933 // and the style attribute sets exactly one CSS property (including invalid
1934 // or unrecognized properties), which is "font-weight"."
1935 if ((node.tagName == "B" || node.tagName == "STRONG")
1936 && node.hasAttribute("style")
1937 && node.style.length == 1
1938 && node.style.fontWeight != "") {
1939 return true;
1940 }
1941
1942 // "It is an i or em element with exactly one attribute, which is style,
1943 // and the style attribute sets exactly one CSS property (including invalid
1944 // or unrecognized properties), which is "font-style"."
1945 if ((node.tagName == "I" || node.tagName == "EM")
1946 && node.hasAttribute("style")
1947 && node.style.length == 1
1948 && node.style.fontStyle != "") {
1949 return true;
1950 }
1951
1952 // "It is an a, font, or span element with exactly one attribute, which is
1953 // style, and the style attribute sets exactly one CSS property (including
1954 // invalid or unrecognized properties), and that property is not
1955 // "text-decoration"."
1956 if ((node.tagName == "A" || node.tagName == "FONT" || node.tagName == "SPAN")
1957 && node.hasAttribute("style")
1958 && node.style.length == 1
1959 && node.style.textDecoration == "") {
1960 return true;
1961 }
1962
1963 // "It is an a, font, s, span, strike, or u element with exactly one
1964 // attribute, which is style, and the style attribute sets exactly one CSS
1965 // property (including invalid or unrecognized properties), which is
1966 // "text-decoration", which is set to "line-through" or "underline" or
1967 // "overline" or "none"."
1968 //
1969 // The weird extra node.style.length check is for Firefox, which as of
1970 // 8.0a2 has annoying and weird behavior here.
1971 if (["A", "FONT", "S", "SPAN", "STRIKE", "U"].indexOf(node.tagName) != -1
1972 && node.hasAttribute("style")
1973 && (node.style.length == 1
1974 || (node.style.length == 4
1975 && "MozTextBlink" in node.style
1976 && "MozTextDecorationColor" in node.style
1977 && "MozTextDecorationLine" in node.style
1978 && "MozTextDecorationStyle" in node.style)
1979 )
1980 && (node.style.textDecoration == "line-through"
1981 || node.style.textDecoration == "underline"
1982 || node.style.textDecoration == "overline"
1983 || node.style.textDecoration == "none")) {
1984 return true;
1985 }
1986
1987 return false;
1988 }
1989
1990 // "A formattable node is an editable visible node that is either a Text node,
1991 // an img, or a br."
1992 function isFormattableNode(node) {
1993 return isEditable(node)
1994 && isVisible(node)
1995 && (node.nodeType == Node.TEXT_NODE
1996 || isHtmlElement(node, ["img", "br"]));
1997 }
1998
1999 // "Two quantities are equivalent values for a command if either both are null,
2000 // or both are strings and they're equal and the command does not define any
2001 // equivalent values, or both are strings and the command defines equivalent
2002 // values and they match the definition."
2003 function areEquivalentValues(command, val1, val2) {
2004 if (val1 === null && val2 === null) {
2005 return true;
2006 }
2007
2008 if (typeof val1 == "string"
2009 && typeof val2 == "string"
2010 && val1 == val2
2011 && !("equivalentValues" in commands[command])) {
2012 return true;
2013 }
2014
2015 if (typeof val1 == "string"
2016 && typeof val2 == "string"
2017 && "equivalentValues" in commands[command]
2018 && commands[command].equivalentValues(val1, val2)) {
2019 return true;
2020 }
2021
2022 return false;
2023 }
2024
2025 // "Two quantities are loosely equivalent values for a command if either they
2026 // are equivalent values for the command, or if the command is the fontSize
2027 // command; one of the quantities is one of "x-small", "small", "medium",
2028 // "large", "x-large", "xx-large", or "xxx-large"; and the other quantity is
2029 // the resolved value of "font-size" on a font element whose size attribute has
2030 // the corresponding value set ("1" through "7" respectively)."
2031 function areLooselyEquivalentValues(command, val1, val2) {
2032 if (areEquivalentValues(command, val1, val2)) {
2033 return true;
2034 }
2035
2036 if (command != "fontsize"
2037 || typeof val1 != "string"
2038 || typeof val2 != "string") {
2039 return false;
2040 }
2041
2042 // Static variables in JavaScript?
2043 var callee = areLooselyEquivalentValues;
2044 if (callee.sizeMap === undefined) {
2045 callee.sizeMap = {};
2046 var font = document.createElement("font");
2047 document.body.appendChild(font);
2048 ["x-small", "small", "medium", "large", "x-large", "xx-large",
2049 "xxx-large"].forEach(function(keyword) {
2050 font.size = cssSizeToLegacy(keyword);
2051 callee.sizeMap[keyword] = getComputedStyle(font).fontSize;
2052 });
2053 document.body.removeChild(font);
2054 }
2055
2056 return val1 === callee.sizeMap[val2]
2057 || val2 === callee.sizeMap[val1];
2058 }
2059
2060 //@}
2061 ///// Assorted inline formatting command algorithms /////
2062 //@{
2063
2064 function getEffectiveCommandValue(node, command) {
2065 // "If neither node nor its parent is an Element, return null."
2066 if (node.nodeType != Node.ELEMENT_NODE
2067 && (!node.parentNode || node.parentNode.nodeType != Node.ELEMENT_NODE)) {
2068 return null;
2069 }
2070
2071 // "If node is not an Element, return the effective command value of its
2072 // parent for command."
2073 if (node.nodeType != Node.ELEMENT_NODE) {
2074 return getEffectiveCommandValue(node.parentNode, command);
2075 }
2076
2077 // "If command is "createLink" or "unlink":"
2078 if (command == "createlink" || command == "unlink") {
2079 // "While node is not null, and is not an a element that has an href
2080 // attribute, set node to its parent."
2081 while (node
2082 && (!isHtmlElement(node)
2083 || node.tagName != "A"
2084 || !node.hasAttribute("href"))) {
2085 node = node.parentNode;
2086 }
2087
2088 // "If node is null, return null."
2089 if (!node) {
2090 return null;
2091 }
2092
2093 // "Return the value of node's href attribute."
2094 return node.getAttribute("href");
2095 }
2096
2097 // "If command is "backColor" or "hiliteColor":"
2098 if (command == "backcolor"
2099 || command == "hilitecolor") {
2100 // "While the resolved value of "background-color" on node is any
2101 // fully transparent value, and node's parent is an Element, set
2102 // node to its parent."
2103 //
2104 // Another lame hack to avoid flawed APIs.
2105 while ((getComputedStyle(node).backgroundColor == "rgba(0, 0, 0, 0)"
2106 || getComputedStyle(node).backgroundColor === ""
2107 || getComputedStyle(node).backgroundColor == "transparent")
2108 && node.parentNode
2109 && node.parentNode.nodeType == Node.ELEMENT_NODE) {
2110 node = node.parentNode;
2111 }
2112
2113 // "Return the resolved value of "background-color" for node."
2114 return getComputedStyle(node).backgroundColor;
2115 }
2116
2117 // "If command is "subscript" or "superscript":"
2118 if (command == "subscript" || command == "superscript") {
2119 // "Let affected by subscript and affected by superscript be two
2120 // boolean variables, both initially false."
2121 var affectedBySubscript = false;
2122 var affectedBySuperscript = false;
2123
2124 // "While node is an inline node:"
2125 while (isInlineNode(node)) {
2126 var verticalAlign = getComputedStyle(node).verticalAlign;
2127
2128 // "If node is a sub, set affected by subscript to true."
2129 if (isHtmlElement(node, "sub")) {
2130 affectedBySubscript = true;
2131 // "Otherwise, if node is a sup, set affected by superscript to
2132 // true."
2133 } else if (isHtmlElement(node, "sup")) {
2134 affectedBySuperscript = true;
2135 }
2136
2137 // "Set node to its parent."
2138 node = node.parentNode;
2139 }
2140
2141 // "If affected by subscript and affected by superscript are both true,
2142 // return the string "mixed"."
2143 if (affectedBySubscript && affectedBySuperscript) {
2144 return "mixed";
2145 }
2146
2147 // "If affected by subscript is true, return "subscript"."
2148 if (affectedBySubscript) {
2149 return "subscript";
2150 }
2151
2152 // "If affected by superscript is true, return "superscript"."
2153 if (affectedBySuperscript) {
2154 return "superscript";
2155 }
2156
2157 // "Return null."
2158 return null;
2159 }
2160
2161 // "If command is "strikethrough", and the "text-decoration" property of
2162 // node or any of its ancestors has resolved value containing
2163 // "line-through", return "line-through". Otherwise, return null."
2164 if (command == "strikethrough") {
2165 do {
2166 if (getComputedStyle(node).textDecoration.indexOf("line-through") != -1) {
2167 return "line-through";
2168 }
2169 node = node.parentNode;
2170 } while (node && node.nodeType == Node.ELEMENT_NODE);
2171 return null;
2172 }
2173
2174 // "If command is "underline", and the "text-decoration" property of node
2175 // or any of its ancestors has resolved value containing "underline",
2176 // return "underline". Otherwise, return null."
2177 if (command == "underline") {
2178 do {
2179 if (getComputedStyle(node).textDecoration.indexOf("underline") != -1) {
2180 return "underline";
2181 }
2182 node = node.parentNode;
2183 } while (node && node.nodeType == Node.ELEMENT_NODE);
2184 return null;
2185 }
2186
2187 if (!("relevantCssProperty" in commands[command])) {
2188 throw "Bug: no relevantCssProperty for " + command + " in getEffectiveCommandValue";
2189 }
2190
2191 // "Return the resolved value for node of the relevant CSS property for
2192 // command."
2193 return getComputedStyle(node)[commands[command].relevantCssProperty];
2194 }
2195
2196 function getSpecifiedCommandValue(element, command) {
2197 // "If command is "backColor" or "hiliteColor" and element's display
2198 // property does not have resolved value "inline", return null."
2199 if ((command == "backcolor" || command == "hilitecolor")
2200 && getComputedStyle(element).display != "inline") {
2201 return null;
2202 }
2203
2204 // "If command is "createLink" or "unlink":"
2205 if (command == "createlink" || command == "unlink") {
2206 // "If element is an a element and has an href attribute, return the
2207 // value of that attribute."
2208 if (isHtmlElement(element)
2209 && element.tagName == "A"
2210 && element.hasAttribute("href")) {
2211 return element.getAttribute("href");
2212 }
2213
2214 // "Return null."
2215 return null;
2216 }
2217
2218 // "If command is "subscript" or "superscript":"
2219 if (command == "subscript" || command == "superscript") {
2220 // "If element is a sup, return "superscript"."
2221 if (isHtmlElement(element, "sup")) {
2222 return "superscript";
2223 }
2224
2225 // "If element is a sub, return "subscript"."
2226 if (isHtmlElement(element, "sub")) {
2227 return "subscript";
2228 }
2229
2230 // "Return null."
2231 return null;
2232 }
2233
2234 // "If command is "strikethrough", and element has a style attribute set,
2235 // and that attribute sets "text-decoration":"
2236 if (command == "strikethrough"
2237 && element.style.textDecoration != "") {
2238 // "If element's style attribute sets "text-decoration" to a value
2239 // containing "line-through", return "line-through"."
2240 if (element.style.textDecoration.indexOf("line-through") != -1) {
2241 return "line-through";
2242 }
2243
2244 // "Return null."
2245 return null;
2246 }
2247
2248 // "If command is "strikethrough" and element is a s or strike element,
2249 // return "line-through"."
2250 if (command == "strikethrough"
2251 && isHtmlElement(element, ["S", "STRIKE"])) {
2252 return "line-through";
2253 }
2254
2255 // "If command is "underline", and element has a style attribute set, and
2256 // that attribute sets "text-decoration":"
2257 if (command == "underline"
2258 && element.style.textDecoration != "") {
2259 // "If element's style attribute sets "text-decoration" to a value
2260 // containing "underline", return "underline"."
2261 if (element.style.textDecoration.indexOf("underline") != -1) {
2262 return "underline";
2263 }
2264
2265 // "Return null."
2266 return null;
2267 }
2268
2269 // "If command is "underline" and element is a u element, return
2270 // "underline"."
2271 if (command == "underline"
2272 && isHtmlElement(element, "U")) {
2273 return "underline";
2274 }
2275
2276 // "Let property be the relevant CSS property for command."
2277 var property = commands[command].relevantCssProperty;
2278
2279 // "If property is null, return null."
2280 if (property === null) {
2281 return null;
2282 }
2283
2284 // "If element has a style attribute set, and that attribute has the
2285 // effect of setting property, return the value that it sets property to."
2286 if (element.style[property] != "") {
2287 return element.style[property];
2288 }
2289
2290 // "If element is a font element that has an attribute whose effect is
2291 // to create a presentational hint for property, return the value that the
2292 // hint sets property to. (For a size of 7, this will be the non-CSS value
2293 // "xxx-large".)"
2294 if (isHtmlNamespace(element.namespaceURI)
2295 && element.tagName == "FONT") {
2296 if (property == "color" && element.hasAttribute("color")) {
2297 return element.color;
2298 }
2299 if (property == "fontFamily" && element.hasAttribute("face")) {
2300 return element.face;
2301 }
2302 if (property == "fontSize" && element.hasAttribute("size")) {
2303 // This is not even close to correct in general.
2304 var size = parseInt(element.size);
2305 if (size < 1) {
2306 size = 1;
2307 }
2308 if (size > 7) {
2309 size = 7;
2310 }
2311 return {
2312 1: "x-small",
2313 2: "small",
2314 3: "medium",
2315 4: "large",
2316 5: "x-large",
2317 6: "xx-large",
2318 7: "xxx-large"
2319 }[size];
2320 }
2321 }
2322
2323 // "If element is in the following list, and property is equal to the
2324 // CSS property name listed for it, return the string listed for it."
2325 //
2326 // A list follows, whose meaning is copied here.
2327 if (property == "fontWeight"
2328 && (element.tagName == "B" || element.tagName == "STRONG")) {
2329 return "bold";
2330 }
2331 if (property == "fontStyle"
2332 && (element.tagName == "I" || element.tagName == "EM")) {
2333 return "italic";
2334 }
2335
2336 // "Return null."
2337 return null;
2338 }
2339
2340 function reorderModifiableDescendants(node, command, newValue) {
2341 // "Let candidate equal node."
2342 var candidate = node;
2343
2344 // "While candidate is a modifiable element, and candidate has exactly one
2345 // child, and that child is also a modifiable element, and candidate is not
2346 // a simple modifiable element or candidate's specified command value for
2347 // command is not equivalent to new value, set candidate to its child."
2348 while (isModifiableElement(candidate)
2349 && candidate.childNodes.length == 1
2350 && isModifiableElement(candidate.firstChild)
2351 && (!isSimpleModifiableElement(candidate)
2352 || !areEquivalentValues(command, getSpecifiedCommandValue(candidate, command), newValue))) {
2353 candidate = candidate.firstChild;
2354 }
2355
2356 // "If candidate is node, or is not a simple modifiable element, or its
2357 // specified command value is not equivalent to new value, or its effective
2358 // command value is not loosely equivalent to new value, abort these
2359 // steps."
2360 if (candidate == node
2361 || !isSimpleModifiableElement(candidate)
2362 || !areEquivalentValues(command, getSpecifiedCommandValue(candidate, command), newValue)
2363 || !areLooselyEquivalentValues(command, getEffectiveCommandValue(candidate, command), newValue)) {
2364 return;
2365 }
2366
2367 // "While candidate has children, insert the first child of candidate into
2368 // candidate's parent immediately before candidate, preserving ranges."
2369 while (candidate.hasChildNodes()) {
2370 movePreservingRanges(candidate.firstChild, candidate.parentNode, getNodeIndex(candidate));
2371 }
2372
2373 // "Insert candidate into node's parent immediately after node."
2374 node.parentNode.insertBefore(candidate, node.nextSibling);
2375
2376 // "Append the node as the last child of candidate, preserving ranges."
2377 movePreservingRanges(node, candidate, -1);
2378 }
2379
2380 function recordValues(nodeList) {
2381 // "Let values be a list of (node, command, specified command value)
2382 // triples, initially empty."
2383 var values = [];
2384
2385 // "For each node in node list, for each command in the list "subscript",
2386 // "bold", "fontName", "fontSize", "foreColor", "hiliteColor", "italic",
2387 // "strikethrough", and "underline" in that order:"
2388 nodeList.forEach(function(node) {
2389 ["subscript", "bold", "fontname", "fontsize", "forecolor",
2390 "hilitecolor", "italic", "strikethrough", "underline"].forEach(function(command) {
2391 // "Let ancestor equal node."
2392 var ancestor = node;
2393
2394 // "If ancestor is not an Element, set it to its parent."
2395 if (ancestor.nodeType != Node.ELEMENT_NODE) {
2396 ancestor = ancestor.parentNode;
2397 }
2398
2399 // "While ancestor is an Element and its specified command value
2400 // for command is null, set it to its parent."
2401 while (ancestor
2402 && ancestor.nodeType == Node.ELEMENT_NODE
2403 && getSpecifiedCommandValue(ancestor, command) === null) {
2404 ancestor = ancestor.parentNode;
2405 }
2406
2407 // "If ancestor is an Element, add (node, command, ancestor's
2408 // specified command value for command) to values. Otherwise add
2409 // (node, command, null) to values."
2410 if (ancestor && ancestor.nodeType == Node.ELEMENT_NODE) {
2411 values.push([node, command, getSpecifiedCommandValue(ancestor, command)]);
2412 } else {
2413 values.push([node, command, null]);
2414 }
2415 });
2416 });
2417
2418 // "Return values."
2419 return values;
2420 }
2421
2422 function restoreValues(values) {
2423 // "For each (node, command, value) triple in values:"
2424 values.forEach(function(triple) {
2425 var node = triple[0];
2426 var command = triple[1];
2427 var value = triple[2];
2428
2429 // "Let ancestor equal node."
2430 var ancestor = node;
2431
2432 // "If ancestor is not an Element, set it to its parent."
2433 if (!ancestor || ancestor.nodeType != Node.ELEMENT_NODE) {
2434 ancestor = ancestor.parentNode;
2435 }
2436
2437 // "While ancestor is an Element and its specified command value for
2438 // command is null, set it to its parent."
2439 while (ancestor
2440 && ancestor.nodeType == Node.ELEMENT_NODE
2441 && getSpecifiedCommandValue(ancestor, command) === null) {
2442 ancestor = ancestor.parentNode;
2443 }
2444
2445 // "If value is null and ancestor is an Element, push down values on
2446 // node for command, with new value null."
2447 if (value === null
2448 && ancestor
2449 && ancestor.nodeType == Node.ELEMENT_NODE) {
2450 pushDownValues(node, command, null);
2451
2452 // "Otherwise, if ancestor is an Element and its specified command
2453 // value for command is not equivalent to value, or if ancestor is not
2454 // an Element and value is not null, force the value of command to
2455 // value on node."
2456 } else if ((ancestor
2457 && ancestor.nodeType == Node.ELEMENT_NODE
2458 && !areEquivalentValues(command, getSpecifiedCommandValue(ancestor, command), value))
2459 || ((!ancestor || ancestor.nodeType != Node.ELEMENT_NODE)
2460 && value !== null)) {
2461 forceValue(node, command, value);
2462 }
2463 });
2464 }
2465
2466
2467 //@}
2468 ///// Clearing an element's value /////
2469 //@{
2470
2471 function clearValue(element, command) {
2472 // "If element is not editable, return the empty list."
2473 if (!isEditable(element)) {
2474 return [];
2475 }
2476
2477 // "If element's specified command value for command is null, return the
2478 // empty list."
2479 if (getSpecifiedCommandValue(element, command) === null) {
2480 return [];
2481 }
2482
2483 // "If element is a simple modifiable element:"
2484 if (isSimpleModifiableElement(element)) {
2485 // "Let children be the children of element."
2486 var children = Array.prototype.slice.call(element.childNodes);
2487
2488 // "For each child in children, insert child into element's parent
2489 // immediately before element, preserving ranges."
2490 for (var i = 0; i < children.length; i++) {
2491 movePreservingRanges(children[i], element.parentNode, getNodeIndex(element));
2492 }
2493
2494 // "Remove element from its parent."
2495 element.parentNode.removeChild(element);
2496
2497 // "Return children."
2498 return children;
2499 }
2500
2501 // "If command is "strikethrough", and element has a style attribute that
2502 // sets "text-decoration" to some value containing "line-through", delete
2503 // "line-through" from the value."
2504 if (command == "strikethrough"
2505 && element.style.textDecoration.indexOf("line-through") != -1) {
2506 if (element.style.textDecoration == "line-through") {
2507 element.style.textDecoration = "";
2508 } else {
2509 element.style.textDecoration = element.style.textDecoration.replace("line-through", "");
2510 }
2511 if (element.getAttribute("style") == "") {
2512 element.removeAttribute("style");
2513 }
2514 }
2515
2516 // "If command is "underline", and element has a style attribute that sets
2517 // "text-decoration" to some value containing "underline", delete
2518 // "underline" from the value."
2519 if (command == "underline"
2520 && element.style.textDecoration.indexOf("underline") != -1) {
2521 if (element.style.textDecoration == "underline") {
2522 element.style.textDecoration = "";
2523 } else {
2524 element.style.textDecoration = element.style.textDecoration.replace("underline", "");
2525 }
2526 if (element.getAttribute("style") == "") {
2527 element.removeAttribute("style");
2528 }
2529 }
2530
2531 // "If the relevant CSS property for command is not null, unset the CSS
2532 // property property of element."
2533 if (commands[command].relevantCssProperty !== null) {
2534 element.style[commands[command].relevantCssProperty] = '';
2535 if (element.getAttribute("style") == "") {
2536 element.removeAttribute("style");
2537 }
2538 }
2539
2540 // "If element is a font element:"
2541 if (isHtmlNamespace(element.namespaceURI) && element.tagName == "FONT") {
2542 // "If command is "foreColor", unset element's color attribute, if set."
2543 if (command == "forecolor") {
2544 element.removeAttribute("color");
2545 }
2546
2547 // "If command is "fontName", unset element's face attribute, if set."
2548 if (command == "fontname") {
2549 element.removeAttribute("face");
2550 }
2551
2552 // "If command is "fontSize", unset element's size attribute, if set."
2553 if (command == "fontsize") {
2554 element.removeAttribute("size");
2555 }
2556 }
2557
2558 // "If element is an a element and command is "createLink" or "unlink",
2559 // unset the href property of element."
2560 if (isHtmlElement(element, "A")
2561 && (command == "createlink" || command == "unlink")) {
2562 element.removeAttribute("href");
2563 }
2564
2565 // "If element's specified command value for command is null, return the
2566 // empty list."
2567 if (getSpecifiedCommandValue(element, command) === null) {
2568 return [];
2569 }
2570
2571 // "Set the tag name of element to "span", and return the one-node list
2572 // consisting of the result."
2573 return [setTagName(element, "span")];
2574 }
2575
2576
2577 //@}
2578 ///// Pushing down values /////
2579 //@{
2580
2581 function pushDownValues(node, command, newValue) {
2582 // "If node's parent is not an Element, abort this algorithm."
2583 if (!node.parentNode
2584 || node.parentNode.nodeType != Node.ELEMENT_NODE) {
2585 return;
2586 }
2587
2588 // "If the effective command value of command is loosely equivalent to new
2589 // value on node, abort this algorithm."
2590 if (areLooselyEquivalentValues(command, getEffectiveCommandValue(node, command), newValue)) {
2591 return;
2592 }
2593
2594 // "Let current ancestor be node's parent."
2595 var currentAncestor = node.parentNode;
2596
2597 // "Let ancestor list be a list of Nodes, initially empty."
2598 var ancestorList = [];
2599
2600 // "While current ancestor is an editable Element and the effective command
2601 // value of command is not loosely equivalent to new value on it, append
2602 // current ancestor to ancestor list, then set current ancestor to its
2603 // parent."
2604 while (isEditable(currentAncestor)
2605 && currentAncestor.nodeType == Node.ELEMENT_NODE
2606 && !areLooselyEquivalentValues(command, getEffectiveCommandValue(currentAncestor, command), newValue)) {
2607 ancestorList.push(currentAncestor);
2608 currentAncestor = currentAncestor.parentNode;
2609 }
2610
2611 // "If ancestor list is empty, abort this algorithm."
2612 if (!ancestorList.length) {
2613 return;
2614 }
2615
2616 // "Let propagated value be the specified command value of command on the
2617 // last member of ancestor list."
2618 var propagatedValue = getSpecifiedCommandValue(ancestorList[ancestorList.length - 1], command);
2619
2620 // "If propagated value is null and is not equal to new value, abort this
2621 // algorithm."
2622 if (propagatedValue === null && propagatedValue != newValue) {
2623 return;
2624 }
2625
2626 // "If the effective command value for the parent of the last member of
2627 // ancestor list is not loosely equivalent to new value, and new value is
2628 // not null, abort this algorithm."
2629 if (newValue !== null
2630 && !areLooselyEquivalentValues(command, getEffectiveCommandValue(ancestorList[ancestorList.length - 1].parentNode, command), newValue)) {
2631 return;
2632 }
2633
2634 // "While ancestor list is not empty:"
2635 while (ancestorList.length) {
2636 // "Let current ancestor be the last member of ancestor list."
2637 // "Remove the last member from ancestor list."
2638 var currentAncestor = ancestorList.pop();
2639
2640 // "If the specified command value of current ancestor for command is
2641 // not null, set propagated value to that value."
2642 if (getSpecifiedCommandValue(currentAncestor, command) !== null) {
2643 propagatedValue = getSpecifiedCommandValue(currentAncestor, command);
2644 }
2645
2646 // "Let children be the children of current ancestor."
2647 var children = Array.prototype.slice.call(currentAncestor.childNodes);
2648
2649 // "If the specified command value of current ancestor for command is
2650 // not null, clear the value of current ancestor."
2651 if (getSpecifiedCommandValue(currentAncestor, command) !== null) {
2652 clearValue(currentAncestor, command);
2653 }
2654
2655 // "For every child in children:"
2656 for (var i = 0; i < children.length; i++) {
2657 var child = children[i];
2658
2659 // "If child is node, continue with the next child."
2660 if (child == node) {
2661 continue;
2662 }
2663
2664 // "If child is an Element whose specified command value for
2665 // command is neither null nor equivalent to propagated value,
2666 // continue with the next child."
2667 if (child.nodeType == Node.ELEMENT_NODE
2668 && getSpecifiedCommandValue(child, command) !== null
2669 && !areEquivalentValues(command, propagatedValue, getSpecifiedCommandValue(child, command))) {
2670 continue;
2671 }
2672
2673 // "If child is the last member of ancestor list, continue with the
2674 // next child."
2675 if (child == ancestorList[ancestorList.length - 1]) {
2676 continue;
2677 }
2678
2679 // "Force the value of child, with command as in this algorithm
2680 // and new value equal to propagated value."
2681 forceValue(child, command, propagatedValue);
2682 }
2683 }
2684 }
2685
2686
2687 //@}
2688 ///// Forcing the value of a node /////
2689 //@{
2690
2691 function forceValue(node, command, newValue) {
2692 // "If node's parent is null, abort this algorithm."
2693 if (!node.parentNode) {
2694 return;
2695 }
2696
2697 // "If new value is null, abort this algorithm."
2698 if (newValue === null) {
2699 return;
2700 }
2701
2702 // "If node is an allowed child of "span":"
2703 if (isAllowedChild(node, "span")) {
2704 // "Reorder modifiable descendants of node's previousSibling."
2705 reorderModifiableDescendants(node.previousSibling, command, newValue);
2706
2707 // "Reorder modifiable descendants of node's nextSibling."
2708 reorderModifiableDescendants(node.nextSibling, command, newValue);
2709
2710 // "Wrap the one-node list consisting of node, with sibling criteria
2711 // returning true for a simple modifiable element whose specified
2712 // command value is equivalent to new value and whose effective command
2713 // value is loosely equivalent to new value and false otherwise, and
2714 // with new parent instructions returning null."
2715 wrap([node],
2716 function(node) {
2717 return isSimpleModifiableElement(node)
2718 && areEquivalentValues(command, getSpecifiedCommandValue(node, command), newValue)
2719 && areLooselyEquivalentValues(command, getEffectiveCommandValue(node, command), newValue);
2720 },
2721 function() { return null }
2722 );
2723 }
2724
2725 // "If node is invisible, abort this algorithm."
2726 if (isInvisible(node)) {
2727 return;
2728 }
2729
2730 // "If the effective command value of command is loosely equivalent to new
2731 // value on node, abort this algorithm."
2732 if (areLooselyEquivalentValues(command, getEffectiveCommandValue(node, command), newValue)) {
2733 return;
2734 }
2735
2736 // "If node is not an allowed child of "span":"
2737 if (!isAllowedChild(node, "span")) {
2738 // "Let children be all children of node, omitting any that are
2739 // Elements whose specified command value for command is neither null
2740 // nor equivalent to new value."
2741 var children = [];
2742 for (var i = 0; i < node.childNodes.length; i++) {
2743 if (node.childNodes[i].nodeType == Node.ELEMENT_NODE) {
2744 var specifiedValue = getSpecifiedCommandValue(node.childNodes[i], command);
2745
2746 if (specifiedValue !== null
2747 && !areEquivalentValues(command, newValue, specifiedValue)) {
2748 continue;
2749 }
2750 }
2751 children.push(node.childNodes[i]);
2752 }
2753
2754 // "Force the value of each Node in children, with command and new
2755 // value as in this invocation of the algorithm."
2756 for (var i = 0; i < children.length; i++) {
2757 forceValue(children[i], command, newValue);
2758 }
2759
2760 // "Abort this algorithm."
2761 return;
2762 }
2763
2764 // "If the effective command value of command is loosely equivalent to new
2765 // value on node, abort this algorithm."
2766 if (areLooselyEquivalentValues(command, getEffectiveCommandValue(node, command), newValue)) {
2767 return;
2768 }
2769
2770 // "Let new parent be null."
2771 var newParent = null;
2772
2773 // "If the CSS styling flag is false:"
2774 if (!cssStylingFlag) {
2775 // "If command is "bold" and new value is "bold", let new parent be the
2776 // result of calling createElement("b") on the ownerDocument of node."
2777 if (command == "bold" && (newValue == "bold" || newValue == "700")) {
2778 newParent = node.ownerDocument.createElement("b");
2779 }
2780
2781 // "If command is "italic" and new value is "italic", let new parent be
2782 // the result of calling createElement("i") on the ownerDocument of
2783 // node."
2784 if (command == "italic" && newValue == "italic") {
2785 newParent = node.ownerDocument.createElement("i");
2786 }
2787
2788 // "If command is "strikethrough" and new value is "line-through", let
2789 // new parent be the result of calling createElement("s") on the
2790 // ownerDocument of node."
2791 if (command == "strikethrough" && newValue == "line-through") {
2792 newParent = node.ownerDocument.createElement("s");
2793 }
2794
2795 // "If command is "underline" and new value is "underline", let new
2796 // parent be the result of calling createElement("u") on the
2797 // ownerDocument of node."
2798 if (command == "underline" && newValue == "underline") {
2799 newParent = node.ownerDocument.createElement("u");
2800 }
2801
2802 // "If command is "foreColor", and new value is fully opaque with red,
2803 // green, and blue components in the range 0 to 255:"
2804 if (command == "forecolor" && parseSimpleColor(newValue)) {
2805 // "Let new parent be the result of calling createElement("font")
2806 // on the ownerDocument of node."
2807 newParent = node.ownerDocument.createElement("font");
2808
2809 // "Set the color attribute of new parent to the result of applying
2810 // the rules for serializing simple color values to new value
2811 // (interpreted as a simple color)."
2812 newParent.setAttribute("color", parseSimpleColor(newValue));
2813 }
2814
2815 // "If command is "fontName", let new parent be the result of calling
2816 // createElement("font") on the ownerDocument of node, then set the
2817 // face attribute of new parent to new value."
2818 if (command == "fontname") {
2819 newParent = node.ownerDocument.createElement("font");
2820 newParent.face = newValue;
2821 }
2822 }
2823
2824 // "If command is "createLink" or "unlink":"
2825 if (command == "createlink" || command == "unlink") {
2826 // "Let new parent be the result of calling createElement("a") on the
2827 // ownerDocument of node."
2828 newParent = node.ownerDocument.createElement("a");
2829
2830 // "Set the href attribute of new parent to new value."
2831 newParent.setAttribute("href", newValue);
2832
2833 // "Let ancestor be node's parent."
2834 var ancestor = node.parentNode;
2835
2836 // "While ancestor is not null:"
2837 while (ancestor) {
2838 // "If ancestor is an a, set the tag name of ancestor to "span",
2839 // and let ancestor be the result."
2840 if (isHtmlElement(ancestor, "A")) {
2841 ancestor = setTagName(ancestor, "span");
2842 }
2843
2844 // "Set ancestor to its parent."
2845 ancestor = ancestor.parentNode;
2846 }
2847 }
2848
2849 // "If command is "fontSize"; and new value is one of "x-small", "small",
2850 // "medium", "large", "x-large", "xx-large", or "xxx-large"; and either the
2851 // CSS styling flag is false, or new value is "xxx-large": let new parent
2852 // be the result of calling createElement("font") on the ownerDocument of
2853 // node, then set the size attribute of new parent to the number from the
2854 // following table based on new value: [table omitted]"
2855 if (command == "fontsize"
2856 && ["x-small", "small", "medium", "large", "x-large", "xx-large", "xxx-large"].indexOf(newValue) != -1
2857 && (!cssStylingFlag || newValue == "xxx-large")) {
2858 newParent = node.ownerDocument.createElement("font");
2859 newParent.size = cssSizeToLegacy(newValue);
2860 }
2861
2862 // "If command is "subscript" or "superscript" and new value is
2863 // "subscript", let new parent be the result of calling
2864 // createElement("sub") on the ownerDocument of node."
2865 if ((command == "subscript" || command == "superscript")
2866 && newValue == "subscript") {
2867 newParent = node.ownerDocument.createElement("sub");
2868 }
2869
2870 // "If command is "subscript" or "superscript" and new value is
2871 // "superscript", let new parent be the result of calling
2872 // createElement("sup") on the ownerDocument of node."
2873 if ((command == "subscript" || command == "superscript")
2874 && newValue == "superscript") {
2875 newParent = node.ownerDocument.createElement("sup");
2876 }
2877
2878 // "If new parent is null, let new parent be the result of calling
2879 // createElement("span") on the ownerDocument of node."
2880 if (!newParent) {
2881 newParent = node.ownerDocument.createElement("span");
2882 }
2883
2884 // "Insert new parent in node's parent before node."
2885 node.parentNode.insertBefore(newParent, node);
2886
2887 // "If the effective command value of command for new parent is not loosely
2888 // equivalent to new value, and the relevant CSS property for command is
2889 // not null, set that CSS property of new parent to new value (if the new
2890 // value would be valid)."
2891 var property = commands[command].relevantCssProperty;
2892 if (property !== null
2893 && !areLooselyEquivalentValues(command, getEffectiveCommandValue(newParent, command), newValue)) {
2894 newParent.style[property] = newValue;
2895 }
2896
2897 // "If command is "strikethrough", and new value is "line-through", and the
2898 // effective command value of "strikethrough" for new parent is not
2899 // "line-through", set the "text-decoration" property of new parent to
2900 // "line-through"."
2901 if (command == "strikethrough"
2902 && newValue == "line-through"
2903 && getEffectiveCommandValue(newParent, "strikethrough") != "line-through") {
2904 newParent.style.textDecoration = "line-through";
2905 }
2906
2907 // "If command is "underline", and new value is "underline", and the
2908 // effective command value of "underline" for new parent is not
2909 // "underline", set the "text-decoration" property of new parent to
2910 // "underline"."
2911 if (command == "underline"
2912 && newValue == "underline"
2913 && getEffectiveCommandValue(newParent, "underline") != "underline") {
2914 newParent.style.textDecoration = "underline";
2915 }
2916
2917 // "Append node to new parent as its last child, preserving ranges."
2918 movePreservingRanges(node, newParent, newParent.childNodes.length);
2919
2920 // "If node is an Element and the effective command value of command for
2921 // node is not loosely equivalent to new value:"
2922 if (node.nodeType == Node.ELEMENT_NODE
2923 && !areEquivalentValues(command, getEffectiveCommandValue(node, command), newValue)) {
2924 // "Insert node into the parent of new parent before new parent,
2925 // preserving ranges."
2926 movePreservingRanges(node, newParent.parentNode, getNodeIndex(newParent));
2927
2928 // "Remove new parent from its parent."
2929 newParent.parentNode.removeChild(newParent);
2930
2931 // "Let children be all children of node, omitting any that are
2932 // Elements whose specified command value for command is neither null
2933 // nor equivalent to new value."
2934 var children = [];
2935 for (var i = 0; i < node.childNodes.length; i++) {
2936 if (node.childNodes[i].nodeType == Node.ELEMENT_NODE) {
2937 var specifiedValue = getSpecifiedCommandValue(node.childNodes[i], command);
2938
2939 if (specifiedValue !== null
2940 && !areEquivalentValues(command, newValue, specifiedValue)) {
2941 continue;
2942 }
2943 }
2944 children.push(node.childNodes[i]);
2945 }
2946
2947 // "Force the value of each Node in children, with command and new
2948 // value as in this invocation of the algorithm."
2949 for (var i = 0; i < children.length; i++) {
2950 forceValue(children[i], command, newValue);
2951 }
2952 }
2953 }
2954
2955
2956 //@}
2957 ///// Setting the selection's value /////
2958 //@{
2959
2960 function setSelectionValue(command, newValue) {
2961 // "If there is no formattable node effectively contained in the active
2962 // range:"
2963 if (!getAllEffectivelyContainedNodes(getActiveRange())
2964 .some(isFormattableNode)) {
2965 // "If command has inline command activated values, set the state
2966 // override to true if new value is among them and false if it's not."
2967 if ("inlineCommandActivatedValues" in commands[command]) {
2968 setStateOverride(command, commands[command].inlineCommandActivatedValues
2969 .indexOf(newValue) != -1);
2970 }
2971
2972 // "If command is "subscript", unset the state override for
2973 // "superscript"."
2974 if (command == "subscript") {
2975 unsetStateOverride("superscript");
2976 }
2977
2978 // "If command is "superscript", unset the state override for
2979 // "subscript"."
2980 if (command == "superscript") {
2981 unsetStateOverride("subscript");
2982 }
2983
2984 // "If new value is null, unset the value override (if any)."
2985 if (newValue === null) {
2986 unsetValueOverride(command);
2987
2988 // "Otherwise, if command is "createLink" or it has a value specified,
2989 // set the value override to new value."
2990 } else if (command == "createlink" || "value" in commands[command]) {
2991 setValueOverride(command, newValue);
2992 }
2993
2994 // "Abort these steps."
2995 return;
2996 }
2997
2998 // "If the active range's start node is an editable Text node, and its
2999 // start offset is neither zero nor its start node's length, call
3000 // splitText() on the active range's start node, with argument equal to the
3001 // active range's start offset. Then set the active range's start node to
3002 // the result, and its start offset to zero."
3003 if (isEditable(getActiveRange().startContainer)
3004 && getActiveRange().startContainer.nodeType == Node.TEXT_NODE
3005 && getActiveRange().startOffset != 0
3006 && getActiveRange().startOffset != getNodeLength(getActiveRange().startContainer)) {
3007 // Account for browsers not following range mutation rules
3008 var newActiveRange = document.createRange();
3009 var newNode;
3010 if (getActiveRange().startContainer == getActiveRange().endContainer) {
3011 var newEndOffset = getActiveRange().endOffset - getActiveRange().startOffset;
3012 newNode = getActiveRange().startContainer.splitText(getActiveRange().startOffset);
3013 newActiveRange.setEnd(newNode, newEndOffset);
3014 getActiveRange().setEnd(newNode, newEndOffset);
3015 } else {
3016 newNode = getActiveRange().startContainer.splitText(getActiveRange().startOffset);
3017 }
3018 newActiveRange.setStart(newNode, 0);
3019 getSelection().removeAllRanges();
3020 getSelection().addRange(newActiveRange);
3021
3022 getActiveRange().setStart(newNode, 0);
3023 }
3024
3025 // "If the active range's end node is an editable Text node, and its end
3026 // offset is neither zero nor its end node's length, call splitText() on
3027 // the active range's end node, with argument equal to the active range's
3028 // end offset."
3029 if (isEditable(getActiveRange().endContainer)
3030 && getActiveRange().endContainer.nodeType == Node.TEXT_NODE
3031 && getActiveRange().endOffset != 0
3032 && getActiveRange().endOffset != getNodeLength(getActiveRange().endContainer)) {
3033 // IE seems to mutate the range incorrectly here, so we need correction
3034 // here as well. The active range will be temporarily in orphaned
3035 // nodes, so calling getActiveRange() after splitText() but before
3036 // fixing the range will throw an exception.
3037 var activeRange = getActiveRange();
3038 var newStart = [activeRange.startContainer, activeRange.startOffset];
3039 var newEnd = [activeRange.endContainer, activeRange.endOffset];
3040 activeRange.endContainer.splitText(activeRange.endOffset);
3041 activeRange.setStart(newStart[0], newStart[1]);
3042 activeRange.setEnd(newEnd[0], newEnd[1]);
3043
3044 getSelection().removeAllRanges();
3045 getSelection().addRange(activeRange);
3046 }
3047
3048 // "Let element list be all editable Elements effectively contained in the
3049 // active range.
3050 //
3051 // "For each element in element list, clear the value of element."
3052 getAllEffectivelyContainedNodes(getActiveRange(), function(node) {
3053 return isEditable(node) && node.nodeType == Node.ELEMENT_NODE;
3054 }).forEach(function(element) {
3055 clearValue(element, command);
3056 });
3057
3058 // "Let node list be all editable nodes effectively contained in the active
3059 // range.
3060 //
3061 // "For each node in node list:"
3062 getAllEffectivelyContainedNodes(getActiveRange(), isEditable).forEach(function(node) {
3063 // "Push down values on node."
3064 pushDownValues(node, command, newValue);
3065
3066 // "If node is an allowed child of span, force the value of node."
3067 if (isAllowedChild(node, "span")) {
3068 forceValue(node, command, newValue);
3069 }
3070 });
3071 }
3072
3073
3074 //@}
3075 ///// The backColor command /////
3076 //@{
3077 commands.backcolor = {
3078 // Copy-pasted, same as hiliteColor
3079 action: function(value) {
3080 // Action is further copy-pasted, same as foreColor
3081
3082 // "If value is not a valid CSS color, prepend "#" to it."
3083 //
3084 // "If value is still not a valid CSS color, or if it is currentColor,
3085 // return false."
3086 //
3087 // Cheap hack for testing, no attempt to be comprehensive.
3088 if (/^([0-9a-fA-F]{3}){1,2}$/.test(value)) {
3089 value = "#" + value;
3090 }
3091 if (!/^(rgba?|hsla?)\(.*\)$/.test(value)
3092 && !parseSimpleColor(value)
3093 && value.toLowerCase() != "transparent") {
3094 return false;
3095 }
3096
3097 // "Set the selection's value to value."
3098 setSelectionValue("backcolor", value);
3099
3100 // "Return true."
3101 return true;
3102 }, standardInlineValueCommand: true, relevantCssProperty: "backgroundColor",
3103 equivalentValues: function(val1, val2) {
3104 // "Either both strings are valid CSS colors and have the same red,
3105 // green, blue, and alpha components, or neither string is a valid CSS
3106 // color."
3107 return normalizeColor(val1) === normalizeColor(val2);
3108 },
3109 };
3110
3111 //@}
3112 ///// The bold command /////
3113 //@{
3114 commands.bold = {
3115 action: function() {
3116 // "If queryCommandState("bold") returns true, set the selection's
3117 // value to "normal". Otherwise set the selection's value to "bold".
3118 // Either way, return true."
3119 if (myQueryCommandState("bold")) {
3120 setSelectionValue("bold", "normal");
3121 } else {
3122 setSelectionValue("bold", "bold");
3123 }
3124 return true;
3125 }, inlineCommandActivatedValues: ["bold", "600", "700", "800", "900"],
3126 relevantCssProperty: "fontWeight",
3127 equivalentValues: function(val1, val2) {
3128 // "Either the two strings are equal, or one is "bold" and the other is
3129 // "700", or one is "normal" and the other is "400"."
3130 return val1 == val2
3131 || (val1 == "bold" && val2 == "700")
3132 || (val1 == "700" && val2 == "bold")
3133 || (val1 == "normal" && val2 == "400")
3134 || (val1 == "400" && val2 == "normal");
3135 },
3136 };
3137
3138 //@}
3139 ///// The createLink command /////
3140 //@{
3141 commands.createlink = {
3142 action: function(value) {
3143 // "If value is the empty string, return false."
3144 if (value === "") {
3145 return false;
3146 }
3147
3148 // "For each editable a element that has an href attribute and is an
3149 // ancestor of some node effectively contained in the active range, set
3150 // that a element's href attribute to value."
3151 //
3152 // TODO: We don't actually do this in tree order, not that it matters
3153 // unless you're spying with mutation events.
3154 getAllEffectivelyContainedNodes(getActiveRange()).forEach(function(node) {
3155 getAncestors(node).forEach(function(ancestor) {
3156 if (isEditable(ancestor)
3157 && isHtmlElement(ancestor, "a")
3158 && ancestor.hasAttribute("href")) {
3159 ancestor.setAttribute("href", value);
3160 }
3161 });
3162 });
3163
3164 // "Set the selection's value to value."
3165 setSelectionValue("createlink", value);
3166
3167 // "Return true."
3168 return true;
3169 }
3170 };
3171
3172 //@}
3173 ///// The fontName command /////
3174 //@{
3175 commands.fontname = {
3176 action: function(value) {
3177 // "Set the selection's value to value, then return true."
3178 setSelectionValue("fontname", value);
3179 return true;
3180 }, standardInlineValueCommand: true, relevantCssProperty: "fontFamily"
3181 };
3182
3183 //@}
3184 ///// The fontSize command /////
3185 //@{
3186
3187 // Helper function for fontSize's action plus queryOutputHelper. It's just the
3188 // middle of fontSize's action, ripped out into its own function. Returns null
3189 // if the size is invalid.
3190 function normalizeFontSize(value) {
3191 // "Strip leading and trailing whitespace from value."
3192 //
3193 // Cheap hack, not following the actual algorithm.
3194 value = value.trim();
3195
3196 // "If value is not a valid floating point number, and would not be a valid
3197 // floating point number if a single leading "+" character were stripped,
3198 // return false."
3199 if (!/^[-+]?[0-9]+(\.[0-9]+)?([eE][-+]?[0-9]+)?$/.test(value)) {
3200 return null;
3201 }
3202
3203 var mode;
3204
3205 // "If the first character of value is "+", delete the character and let
3206 // mode be "relative-plus"."
3207 if (value[0] == "+") {
3208 value = value.slice(1);
3209 mode = "relative-plus";
3210 // "Otherwise, if the first character of value is "-", delete the character
3211 // and let mode be "relative-minus"."
3212 } else if (value[0] == "-") {
3213 value = value.slice(1);
3214 mode = "relative-minus";
3215 // "Otherwise, let mode be "absolute"."
3216 } else {
3217 mode = "absolute";
3218 }
3219
3220 // "Apply the rules for parsing non-negative integers to value, and let
3221 // number be the result."
3222 //
3223 // Another cheap hack.
3224 var num = parseInt(value);
3225
3226 // "If mode is "relative-plus", add three to number."
3227 if (mode == "relative-plus") {
3228 num += 3;
3229 }
3230
3231 // "If mode is "relative-minus", negate number, then add three to it."
3232 if (mode == "relative-minus") {
3233 num = 3 - num;
3234 }
3235
3236 // "If number is less than one, let number equal 1."
3237 if (num < 1) {
3238 num = 1;
3239 }
3240
3241 // "If number is greater than seven, let number equal 7."
3242 if (num > 7) {
3243 num = 7;
3244 }
3245
3246 // "Set value to the string here corresponding to number:" [table omitted]
3247 value = {
3248 1: "x-small",
3249 2: "small",
3250 3: "medium",
3251 4: "large",
3252 5: "x-large",
3253 6: "xx-large",
3254 7: "xxx-large"
3255 }[num];
3256
3257 return value;
3258 }
3259
3260 commands.fontsize = {
3261 action: function(value) {
3262 value = normalizeFontSize(value);
3263 if (value === null) {
3264 return false;
3265 }
3266
3267 // "Set the selection's value to value."
3268 setSelectionValue("fontsize", value);
3269
3270 // "Return true."
3271 return true;
3272 }, indeterm: function() {
3273 // "True if among formattable nodes that are effectively contained in
3274 // the active range, there are two that have distinct effective command
3275 // values. Otherwise false."
3276 return getAllEffectivelyContainedNodes(getActiveRange(), isFormattableNode)
3277 .map(function(node) {
3278 return getEffectiveCommandValue(node, "fontsize");
3279 }).filter(function(value, i, arr) {
3280 return arr.slice(0, i).indexOf(value) == -1;
3281 }).length >= 2;
3282 }, value: function() {
3283 // "If the active range is null, return the empty string."
3284 if (!getActiveRange()) {
3285 return "";
3286 }
3287
3288 // "Let pixel size be the effective command value of the first
3289 // formattable node that is effectively contained in the active range,
3290 // or if there is no such node, the effective command value of the
3291 // active range's start node, in either case interpreted as a number of
3292 // pixels."
3293 var node = getAllEffectivelyContainedNodes(getActiveRange(), isFormattableNode)[0];
3294 if (node === undefined) {
3295 node = getActiveRange().startContainer;
3296 }
3297 var pixelSize = getEffectiveCommandValue(node, "fontsize");
3298
3299 // "Return the legacy font size for pixel size."
3300 return getLegacyFontSize(pixelSize);
3301 }, relevantCssProperty: "fontSize"
3302 };
3303
3304 function getLegacyFontSize(size) {
3305 if (getLegacyFontSize.resultCache === undefined) {
3306 getLegacyFontSize.resultCache = {};
3307 }
3308
3309 if (getLegacyFontSize.resultCache[size] !== undefined) {
3310 return getLegacyFontSize.resultCache[size];
3311 }
3312
3313 // For convenience in other places in my code, I handle all sizes, not just
3314 // pixel sizes as the spec says. This means pixel sizes have to be passed
3315 // in suffixed with "px", not as plain numbers.
3316 if (normalizeFontSize(size) !== null) {
3317 return getLegacyFontSize.resultCache[size] = cssSizeToLegacy(normalizeFontSize(size));
3318 }
3319
3320 if (["x-small", "x-small", "small", "medium", "large", "x-large", "xx-large", "xxx-large"].indexOf(size) == -1
3321 && !/^[0-9]+(\.[0-9]+)?(cm|mm|in|pt|pc|px)$/.test(size)) {
3322 // There is no sensible legacy size for things like "2em".
3323 return getLegacyFontSize.resultCache[size] = null;
3324 }
3325
3326 var font = document.createElement("font");
3327 document.body.appendChild(font);
3328 if (size == "xxx-large") {
3329 font.size = 7;
3330 } else {
3331 font.style.fontSize = size;
3332 }
3333 var pixelSize = parseInt(getComputedStyle(font).fontSize);
3334 document.body.removeChild(font);
3335
3336 // "Let returned size be 1."
3337 var returnedSize = 1;
3338
3339 // "While returned size is less than 7:"
3340 while (returnedSize < 7) {
3341 // "Let lower bound be the resolved value of "font-size" in pixels
3342 // of a font element whose size attribute is set to returned size."
3343 var font = document.createElement("font");
3344 font.size = returnedSize;
3345 document.body.appendChild(font);
3346 var lowerBound = parseInt(getComputedStyle(font).fontSize);
3347
3348 // "Let upper bound be the resolved value of "font-size" in pixels
3349 // of a font element whose size attribute is set to one plus
3350 // returned size."
3351 font.size = 1 + returnedSize;
3352 var upperBound = parseInt(getComputedStyle(font).fontSize);
3353 document.body.removeChild(font);
3354
3355 // "Let average be the average of upper bound and lower bound."
3356 var average = (upperBound + lowerBound)/2;
3357
3358 // "If pixel size is less than average, return the one-element
3359 // string consisting of the digit returned size."
3360 if (pixelSize < average) {
3361 return getLegacyFontSize.resultCache[size] = String(returnedSize);
3362 }
3363
3364 // "Add one to returned size."
3365 returnedSize++;
3366 }
3367
3368 // "Return "7"."
3369 return getLegacyFontSize.resultCache[size] = "7";
3370 }
3371
3372 //@}
3373 ///// The foreColor command /////
3374 //@{
3375 commands.forecolor = {
3376 action: function(value) {
3377 // Copy-pasted, same as backColor and hiliteColor
3378
3379 // "If value is not a valid CSS color, prepend "#" to it."
3380 //
3381 // "If value is still not a valid CSS color, or if it is currentColor,
3382 // return false."
3383 //
3384 // Cheap hack for testing, no attempt to be comprehensive.
3385 if (/^([0-9a-fA-F]{3}){1,2}$/.test(value)) {
3386 value = "#" + value;
3387 }
3388 if (!/^(rgba?|hsla?)\(.*\)$/.test(value)
3389 && !parseSimpleColor(value)
3390 && value.toLowerCase() != "transparent") {
3391 return false;
3392 }
3393
3394 // "Set the selection's value to value."
3395 setSelectionValue("forecolor", value);
3396
3397 // "Return true."
3398 return true;
3399 }, standardInlineValueCommand: true, relevantCssProperty: "color",
3400 equivalentValues: function(val1, val2) {
3401 // "Either both strings are valid CSS colors and have the same red,
3402 // green, blue, and alpha components, or neither string is a valid CSS
3403 // color."
3404 return normalizeColor(val1) === normalizeColor(val2);
3405 },
3406 };
3407
3408 //@}
3409 ///// The hiliteColor command /////
3410 //@{
3411 commands.hilitecolor = {
3412 // Copy-pasted, same as backColor
3413 action: function(value) {
3414 // Action is further copy-pasted, same as foreColor
3415
3416 // "If value is not a valid CSS color, prepend "#" to it."
3417 //
3418 // "If value is still not a valid CSS color, or if it is currentColor,
3419 // return false."
3420 //
3421 // Cheap hack for testing, no attempt to be comprehensive.
3422 if (/^([0-9a-fA-F]{3}){1,2}$/.test(value)) {
3423 value = "#" + value;
3424 }
3425 if (!/^(rgba?|hsla?)\(.*\)$/.test(value)
3426 && !parseSimpleColor(value)
3427 && value.toLowerCase() != "transparent") {
3428 return false;
3429 }
3430
3431 // "Set the selection's value to value."
3432 setSelectionValue("hilitecolor", value);
3433
3434 // "Return true."
3435 return true;
3436 }, indeterm: function() {
3437 // "True if among editable Text nodes that are effectively contained in
3438 // the active range, there are two that have distinct effective command
3439 // values. Otherwise false."
3440 return getAllEffectivelyContainedNodes(getActiveRange(), function(node) {
3441 return isEditable(node) && node.nodeType == Node.TEXT_NODE;
3442 }).map(function(node) {
3443 return getEffectiveCommandValue(node, "hilitecolor");
3444 }).filter(function(value, i, arr) {
3445 return arr.slice(0, i).indexOf(value) == -1;
3446 }).length >= 2;
3447 }, standardInlineValueCommand: true, relevantCssProperty: "backgroundColor",
3448 equivalentValues: function(val1, val2) {
3449 // "Either both strings are valid CSS colors and have the same red,
3450 // green, blue, and alpha components, or neither string is a valid CSS
3451 // color."
3452 return normalizeColor(val1) === normalizeColor(val2);
3453 },
3454 };
3455
3456 //@}
3457 ///// The italic command /////
3458 //@{
3459 commands.italic = {
3460 action: function() {
3461 // "If queryCommandState("italic") returns true, set the selection's
3462 // value to "normal". Otherwise set the selection's value to "italic".
3463 // Either way, return true."
3464 if (myQueryCommandState("italic")) {
3465 setSelectionValue("italic", "normal");
3466 } else {
3467 setSelectionValue("italic", "italic");
3468 }
3469 return true;
3470 }, inlineCommandActivatedValues: ["italic", "oblique"],
3471 relevantCssProperty: "fontStyle"
3472 };
3473
3474 //@}
3475 ///// The removeFormat command /////
3476 //@{
3477 commands.removeformat = {
3478 action: function() {
3479 // "A removeFormat candidate is an editable HTML element with local
3480 // name "abbr", "acronym", "b", "bdi", "bdo", "big", "blink", "cite",
3481 // "code", "dfn", "em", "font", "i", "ins", "kbd", "mark", "nobr", "q",
3482 // "s", "samp", "small", "span", "strike", "strong", "sub", "sup",
3483 // "tt", "u", or "var"."
3484 function isRemoveFormatCandidate(node) {
3485 return isEditable(node)
3486 && isHtmlElement(node, ["abbr", "acronym", "b", "bdi", "bdo",
3487 "big", "blink", "cite", "code", "dfn", "em", "font", "i",
3488 "ins", "kbd", "mark", "nobr", "q", "s", "samp", "small",
3489 "span", "strike", "strong", "sub", "sup", "tt", "u", "var"]);
3490 }
3491
3492 // "Let elements to remove be a list of every removeFormat candidate
3493 // effectively contained in the active range."
3494 var elementsToRemove = getAllEffectivelyContainedNodes(getActiveRange(), isRemoveFormatCandidate);
3495
3496 // "For each element in elements to remove:"
3497 elementsToRemove.forEach(function(element) {
3498 // "While element has children, insert the first child of element
3499 // into the parent of element immediately before element,
3500 // preserving ranges."
3501 while (element.hasChildNodes()) {
3502 movePreservingRanges(element.firstChild, element.parentNode, getNodeIndex(element));
3503 }
3504
3505 // "Remove element from its parent."
3506 element.parentNode.removeChild(element);
3507 });
3508
3509 // "If the active range's start node is an editable Text node, and its
3510 // start offset is neither zero nor its start node's length, call
3511 // splitText() on the active range's start node, with argument equal to
3512 // the active range's start offset. Then set the active range's start
3513 // node to the result, and its start offset to zero."
3514 if (isEditable(getActiveRange().startContainer)
3515 && getActiveRange().startContainer.nodeType == Node.TEXT_NODE
3516 && getActiveRange().startOffset != 0
3517 && getActiveRange().startOffset != getNodeLength(getActiveRange().startContainer)) {
3518 // Account for browsers not following range mutation rules
3519 if (getActiveRange().startContainer == getActiveRange().endContainer) {
3520 var newEnd = getActiveRange().endOffset - getActiveRange().startOffset;
3521 var newNode = getActiveRange().startContainer.splitText(getActiveRange().startOffset);
3522 getActiveRange().setStart(newNode, 0);
3523 getActiveRange().setEnd(newNode, newEnd);
3524 } else {
3525 getActiveRange().setStart(getActiveRange().startContainer.splitText(getActiveRange().startOffset), 0);
3526 }
3527 }
3528
3529 // "If the active range's end node is an editable Text node, and its
3530 // end offset is neither zero nor its end node's length, call
3531 // splitText() on the active range's end node, with argument equal to
3532 // the active range's end offset."
3533 if (isEditable(getActiveRange().endContainer)
3534 && getActiveRange().endContainer.nodeType == Node.TEXT_NODE
3535 && getActiveRange().endOffset != 0
3536 && getActiveRange().endOffset != getNodeLength(getActiveRange().endContainer)) {
3537 // IE seems to mutate the range incorrectly here, so we need
3538 // correction here as well. Have to be careful to set the range to
3539 // something not including the text node so that getActiveRange()
3540 // doesn't throw an exception due to a temporarily detached
3541 // endpoint.
3542 var newStart = [getActiveRange().startContainer, getActiveRange().startOffset];
3543 var newEnd = [getActiveRange().endContainer, getActiveRange().endOffset];
3544 getActiveRange().setEnd(document.documentElement, 0);
3545 newEnd[0].splitText(newEnd[1]);
3546 getActiveRange().setStart(newStart[0], newStart[1]);
3547 getActiveRange().setEnd(newEnd[0], newEnd[1]);
3548 }
3549
3550 // "Let node list consist of all editable nodes effectively contained
3551 // in the active range."
3552 //
3553 // "For each node in node list, while node's parent is a removeFormat
3554 // candidate in the same editing host as node, split the parent of the
3555 // one-node list consisting of node."
3556 getAllEffectivelyContainedNodes(getActiveRange(), isEditable).forEach(function(node) {
3557 while (isRemoveFormatCandidate(node.parentNode)
3558 && inSameEditingHost(node.parentNode, node)) {
3559 splitParent([node]);
3560 }
3561 });
3562
3563 // "For each of the entries in the following list, in the given order,
3564 // set the selection's value to null, with command as given."
3565 [
3566 "subscript",
3567 "bold",
3568 "fontname",
3569 "fontsize",
3570 "forecolor",
3571 "hilitecolor",
3572 "italic",
3573 "strikethrough",
3574 "underline",
3575 ].forEach(function(command) {
3576 setSelectionValue(command, null);
3577 });
3578
3579 // "Return true."
3580 return true;
3581 }
3582 };
3583
3584 //@}
3585 ///// The strikethrough command /////
3586 //@{
3587 commands.strikethrough = {
3588 action: function() {
3589 // "If queryCommandState("strikethrough") returns true, set the
3590 // selection's value to null. Otherwise set the selection's value to
3591 // "line-through". Either way, return true."
3592 if (myQueryCommandState("strikethrough")) {
3593 setSelectionValue("strikethrough", null);
3594 } else {
3595 setSelectionValue("strikethrough", "line-through");
3596 }
3597 return true;
3598 }, inlineCommandActivatedValues: ["line-through"]
3599 };
3600
3601 //@}
3602 ///// The subscript command /////
3603 //@{
3604 commands.subscript = {
3605 action: function() {
3606 // "Call queryCommandState("subscript"), and let state be the result."
3607 var state = myQueryCommandState("subscript");
3608
3609 // "Set the selection's value to null."
3610 setSelectionValue("subscript", null);
3611
3612 // "If state is false, set the selection's value to "subscript"."
3613 if (!state) {
3614 setSelectionValue("subscript", "subscript");
3615 }
3616
3617 // "Return true."
3618 return true;
3619 }, indeterm: function() {
3620 // "True if either among formattable nodes that are effectively
3621 // contained in the active range, there is at least one with effective
3622 // command value "subscript" and at least one with some other effective
3623 // command value; or if there is some formattable node effectively
3624 // contained in the active range with effective command value "mixed".
3625 // Otherwise false."
3626 var nodes = getAllEffectivelyContainedNodes(getActiveRange(), isFormattableNode);
3627 return (nodes.some(function(node) { return getEffectiveCommandValue(node, "subscript") == "subscript" })
3628 && nodes.some(function(node) { return getEffectiveCommandValue(node, "subscript") != "subscript" }))
3629 || nodes.some(function(node) { return getEffectiveCommandValue(node, "subscript") == "mixed" });
3630 }, inlineCommandActivatedValues: ["subscript"],
3631 };
3632
3633 //@}
3634 ///// The superscript command /////
3635 //@{
3636 commands.superscript = {
3637 action: function() {
3638 // "Call queryCommandState("superscript"), and let state be the
3639 // result."
3640 var state = myQueryCommandState("superscript");
3641
3642 // "Set the selection's value to null."
3643 setSelectionValue("superscript", null);
3644
3645 // "If state is false, set the selection's value to "superscript"."
3646 if (!state) {
3647 setSelectionValue("superscript", "superscript");
3648 }
3649
3650 // "Return true."
3651 return true;
3652 }, indeterm: function() {
3653 // "True if either among formattable nodes that are effectively
3654 // contained in the active range, there is at least one with effective
3655 // command value "superscript" and at least one with some other
3656 // effective command value; or if there is some formattable node
3657 // effectively contained in the active range with effective command
3658 // value "mixed". Otherwise false."
3659 var nodes = getAllEffectivelyContainedNodes(getActiveRange(), isFormattableNode);
3660 return (nodes.some(function(node) { return getEffectiveCommandValue(node, "superscript") == "superscript" })
3661 && nodes.some(function(node) { return getEffectiveCommandValue(node, "superscript") != "superscript" }))
3662 || nodes.some(function(node) { return getEffectiveCommandValue(node, "superscript") == "mixed" });
3663 }, inlineCommandActivatedValues: ["superscript"],
3664 };
3665
3666 //@}
3667 ///// The underline command /////
3668 //@{
3669 commands.underline = {
3670 action: function() {
3671 // "If queryCommandState("underline") returns true, set the selection's
3672 // value to null. Otherwise set the selection's value to "underline".
3673 // Either way, return true."
3674 if (myQueryCommandState("underline")) {
3675 setSelectionValue("underline", null);
3676 } else {
3677 setSelectionValue("underline", "underline");
3678 }
3679 return true;
3680 }, inlineCommandActivatedValues: ["underline"]
3681 };
3682
3683 //@}
3684 ///// The unlink command /////
3685 //@{
3686 commands.unlink = {
3687 action: function() {
3688 // "Let hyperlinks be a list of every a element that has an href
3689 // attribute and is contained in the active range or is an ancestor of
3690 // one of its boundary points."
3691 //
3692 // As usual, take care to ensure it's tree order. The correctness of
3693 // the following is left as an exercise for the reader.
3694 var range = getActiveRange();
3695 var hyperlinks = [];
3696 for (
3697 var node = range.startContainer;
3698 node;
3699 node = node.parentNode
3700 ) {
3701 if (isHtmlElement(node, "A")
3702 && node.hasAttribute("href")) {
3703 hyperlinks.unshift(node);
3704 }
3705 }
3706 for (
3707 var node = range.startContainer;
3708 node != nextNodeDescendants(range.endContainer);
3709 node = nextNode(node)
3710 ) {
3711 if (isHtmlElement(node, "A")
3712 && node.hasAttribute("href")
3713 && (isContained(node, range)
3714 || isAncestor(node, range.endContainer)
3715 || node == range.endContainer)) {
3716 hyperlinks.push(node);
3717 }
3718 }
3719
3720 // "Clear the value of each member of hyperlinks."
3721 for (var i = 0; i < hyperlinks.length; i++) {
3722 clearValue(hyperlinks[i], "unlink");
3723 }
3724
3725 // "Return true."
3726 return true;
3727 }
3728 };
3729
3730 //@}
3731
3732 /////////////////////////////////////
3733 ///// Block formatting commands /////
3734 /////////////////////////////////////
3735
3736 ///// Block formatting command definitions /////
3737 //@{
3738
3739 // "An indentation element is either a blockquote, or a div that has a style
3740 // attribute that sets "margin" or some subproperty of it."
3741 function isIndentationElement(node) {
3742 if (!isHtmlElement(node)) {
3743 return false;
3744 }
3745
3746 if (node.tagName == "BLOCKQUOTE") {
3747 return true;
3748 }
3749
3750 if (node.tagName != "DIV") {
3751 return false;
3752 }
3753
3754 for (var i = 0; i < node.style.length; i++) {
3755 // Approximate check
3756 if (/^(-[a-z]+-)?margin/.test(node.style[i])) {
3757 return true;
3758 }
3759 }
3760
3761 return false;
3762 }
3763
3764 // "A simple indentation element is an indentation element that has no
3765 // attributes except possibly
3766 //
3767 // * "a style attribute that sets no properties other than "margin",
3768 // "border", "padding", or subproperties of those; and/or
3769 // * "a dir attribute."
3770 function isSimpleIndentationElement(node) {
3771 if (!isIndentationElement(node)) {
3772 return false;
3773 }
3774
3775 for (var i = 0; i < node.attributes.length; i++) {
3776 if (!isHtmlNamespace(node.attributes[i].namespaceURI)
3777 || ["style", "dir"].indexOf(node.attributes[i].name) == -1) {
3778 return false;
3779 }
3780 }
3781
3782 for (var i = 0; i < node.style.length; i++) {
3783 // This is approximate, but it works well enough for my purposes.
3784 if (!/^(-[a-z]+-)?(margin|border|padding)/.test(node.style[i])) {
3785 return false;
3786 }
3787 }
3788
3789 return true;
3790 }
3791
3792 // "A non-list single-line container is an HTML element with local name
3793 // "address", "div", "h1", "h2", "h3", "h4", "h5", "h6", "listing", "p", "pre",
3794 // or "xmp"."
3795 function isNonListSingleLineContainer(node) {
3796 return isHtmlElement(node, ["address", "div", "h1", "h2", "h3", "h4", "h5",
3797 "h6", "listing", "p", "pre", "xmp"]);
3798 }
3799
3800 // "A single-line container is either a non-list single-line container, or an
3801 // HTML element with local name "li", "dt", or "dd"."
3802 function isSingleLineContainer(node) {
3803 return isNonListSingleLineContainer(node)
3804 || isHtmlElement(node, ["li", "dt", "dd"]);
3805 }
3806
3807 function getBlockNodeOf(node) {
3808 // "While node is an inline node, set node to its parent."
3809 while (isInlineNode(node)) {
3810 node = node.parentNode;
3811 }
3812
3813 // "Return node."
3814 return node;
3815 }
3816
3817 //@}
3818 ///// Assorted block formatting command algorithms /////
3819 //@{
3820
3821 function fixDisallowedAncestors(node) {
3822 // "If node is not editable, abort these steps."
3823 if (!isEditable(node)) {
3824 return;
3825 }
3826
3827 // "If node is not an allowed child of any of its ancestors in the same
3828 // editing host:"
3829 if (getAncestors(node).every(function(ancestor) {
3830 return !inSameEditingHost(node, ancestor)
3831 || !isAllowedChild(node, ancestor)
3832 })) {
3833 // "If node is a dd or dt, wrap the one-node list consisting of node,
3834 // with sibling criteria returning true for any dl with no attributes
3835 // and false otherwise, and new parent instructions returning the
3836 // result of calling createElement("dl") on the context object. Then
3837 // abort these steps."
3838 if (isHtmlElement(node, ["dd", "dt"])) {
3839 wrap([node],
3840 function(sibling) { return isHtmlElement(sibling, "dl") && !sibling.attributes.length },
3841 function() { return document.createElement("dl") });
3842 return;
3843 }
3844
3845 // "If "p" is not an allowed child of the editing host of node, abort
3846 // these steps."
3847 if (!isAllowedChild("p", getEditingHostOf(node))) {
3848 return;
3849 }
3850
3851 // "If node is not a prohibited paragraph child, abort these steps."
3852 if (!isProhibitedParagraphChild(node)) {
3853 return;
3854 }
3855
3856 // "Set the tag name of node to the default single-line container name,
3857 // and let node be the result."
3858 node = setTagName(node, defaultSingleLineContainerName);
3859
3860 // "Fix disallowed ancestors of node."
3861 fixDisallowedAncestors(node);
3862
3863 // "Let children be node's children."
3864 var children = [].slice.call(node.childNodes);
3865
3866 // "For each child in children, if child is a prohibited paragraph
3867 // child:"
3868 children.filter(isProhibitedParagraphChild)
3869 .forEach(function(child) {
3870 // "Record the values of the one-node list consisting of child, and
3871 // let values be the result."
3872 var values = recordValues([child]);
3873
3874 // "Split the parent of the one-node list consisting of child."
3875 splitParent([child]);
3876
3877 // "Restore the values from values."
3878 restoreValues(values);
3879 });
3880
3881 // "Abort these steps."
3882 return;
3883 }
3884
3885 // "Record the values of the one-node list consisting of node, and let
3886 // values be the result."
3887 var values = recordValues([node]);
3888
3889 // "While node is not an allowed child of its parent, split the parent of
3890 // the one-node list consisting of node."
3891 while (!isAllowedChild(node, node.parentNode)) {
3892 splitParent([node]);
3893 }
3894
3895 // "Restore the values from values."
3896 restoreValues(values);
3897 }
3898
3899 function normalizeSublists(item) {
3900 // "If item is not an li or it is not editable or its parent is not
3901 // editable, abort these steps."
3902 if (!isHtmlElement(item, "LI")
3903 || !isEditable(item)
3904 || !isEditable(item.parentNode)) {
3905 return;
3906 }
3907
3908 // "Let new item be null."
3909 var newItem = null;
3910
3911 // "While item has an ol or ul child:"
3912 while ([].some.call(item.childNodes, function (node) { return isHtmlElement(node, ["OL", "UL"]) })) {
3913 // "Let child be the last child of item."
3914 var child = item.lastChild;
3915
3916 // "If child is an ol or ul, or new item is null and child is a Text
3917 // node whose data consists of zero of more space characters:"
3918 if (isHtmlElement(child, ["OL", "UL"])
3919 || (!newItem && child.nodeType == Node.TEXT_NODE && /^[ \t\n\f\r]*$/.test(child.data))) {
3920 // "Set new item to null."
3921 newItem = null;
3922
3923 // "Insert child into the parent of item immediately following
3924 // item, preserving ranges."
3925 movePreservingRanges(child, item.parentNode, 1 + getNodeIndex(item));
3926
3927 // "Otherwise:"
3928 } else {
3929 // "If new item is null, let new item be the result of calling
3930 // createElement("li") on the ownerDocument of item, then insert
3931 // new item into the parent of item immediately after item."
3932 if (!newItem) {
3933 newItem = item.ownerDocument.createElement("li");
3934 item.parentNode.insertBefore(newItem, item.nextSibling);
3935 }
3936
3937 // "Insert child into new item as its first child, preserving
3938 // ranges."
3939 movePreservingRanges(child, newItem, 0);
3940 }
3941 }
3942 }
3943
3944 function getSelectionListState() {
3945 // "If the active range is null, return "none"."
3946 if (!getActiveRange()) {
3947 return "none";
3948 }
3949
3950 // "Block-extend the active range, and let new range be the result."
3951 var newRange = blockExtend(getActiveRange());
3952
3953 // "Let node list be a list of nodes, initially empty."
3954 //
3955 // "For each node contained in new range, append node to node list if the
3956 // last member of node list (if any) is not an ancestor of node; node is
3957 // editable; node is not an indentation element; and node is either an ol
3958 // or ul, or the child of an ol or ul, or an allowed child of "li"."
3959 var nodeList = getContainedNodes(newRange, function(node) {
3960 return isEditable(node)
3961 && !isIndentationElement(node)
3962 && (isHtmlElement(node, ["ol", "ul"])
3963 || isHtmlElement(node.parentNode, ["ol", "ul"])
3964 || isAllowedChild(node, "li"));
3965 });
3966
3967 // "If node list is empty, return "none"."
3968 if (!nodeList.length) {
3969 return "none";
3970 }
3971
3972 // "If every member of node list is either an ol or the child of an ol or
3973 // the child of an li child of an ol, and none is a ul or an ancestor of a
3974 // ul, return "ol"."
3975 if (nodeList.every(function(node) {
3976 return isHtmlElement(node, "ol")
3977 || isHtmlElement(node.parentNode, "ol")
3978 || (isHtmlElement(node.parentNode, "li") && isHtmlElement(node.parentNode.parentNode, "ol"));
3979 })
3980 && !nodeList.some(function(node) { return isHtmlElement(node, "ul") || ("querySelector" in node && node.querySelector("ul")) })) {
3981 return "ol";
3982 }
3983
3984 // "If every member of node list is either a ul or the child of a ul or the
3985 // child of an li child of a ul, and none is an ol or an ancestor of an ol,
3986 // return "ul"."
3987 if (nodeList.every(function(node) {
3988 return isHtmlElement(node, "ul")
3989 || isHtmlElement(node.parentNode, "ul")
3990 || (isHtmlElement(node.parentNode, "li") && isHtmlElement(node.parentNode.parentNode, "ul"));
3991 })
3992 && !nodeList.some(function(node) { return isHtmlElement(node, "ol") || ("querySelector" in node && node.querySelector("ol")) })) {
3993 return "ul";
3994 }
3995
3996 var hasOl = nodeList.some(function(node) {
3997 return isHtmlElement(node, "ol")
3998 || isHtmlElement(node.parentNode, "ol")
3999 || ("querySelector" in node && node.querySelector("ol"))
4000 || (isHtmlElement(node.parentNode, "li") && isHtmlElement(node.parentNode.parentNode, "ol"));
4001 });
4002 var hasUl = nodeList.some(function(node) {
4003 return isHtmlElement(node, "ul")
4004 || isHtmlElement(node.parentNode, "ul")
4005 || ("querySelector" in node && node.querySelector("ul"))
4006 || (isHtmlElement(node.parentNode, "li") && isHtmlElement(node.parentNode.parentNode, "ul"));
4007 });
4008 // "If some member of node list is either an ol or the child or ancestor of
4009 // an ol or the child of an li child of an ol, and some member of node list
4010 // is either a ul or the child or ancestor of a ul or the child of an li
4011 // child of a ul, return "mixed"."
4012 if (hasOl && hasUl) {
4013 return "mixed";
4014 }
4015
4016 // "If some member of node list is either an ol or the child or ancestor of
4017 // an ol or the child of an li child of an ol, return "mixed ol"."
4018 if (hasOl) {
4019 return "mixed ol";
4020 }
4021
4022 // "If some member of node list is either a ul or the child or ancestor of
4023 // a ul or the child of an li child of a ul, return "mixed ul"."
4024 if (hasUl) {
4025 return "mixed ul";
4026 }
4027
4028 // "Return "none"."
4029 return "none";
4030 }
4031
4032 function getAlignmentValue(node) {
4033 // "While node is neither null nor an Element, or it is an Element but its
4034 // "display" property has resolved value "inline" or "none", set node to
4035 // its parent."
4036 while ((node && node.nodeType != Node.ELEMENT_NODE)
4037 || (node.nodeType == Node.ELEMENT_NODE
4038 && ["inline", "none"].indexOf(getComputedStyle(node).display) != -1)) {
4039 node = node.parentNode;
4040 }
4041
4042 // "If node is not an Element, return "left"."
4043 if (!node || node.nodeType != Node.ELEMENT_NODE) {
4044 return "left";
4045 }
4046
4047 var resolvedValue = getComputedStyle(node).textAlign
4048 // Hack around browser non-standardness
4049 .replace(/^-(moz|webkit)-/, "")
4050 .replace(/^auto$/, "start");
4051
4052 // "If node's "text-align" property has resolved value "start", return
4053 // "left" if the directionality of node is "ltr", "right" if it is "rtl"."
4054 if (resolvedValue == "start") {
4055 return getDirectionality(node) == "ltr" ? "left" : "right";
4056 }
4057
4058 // "If node's "text-align" property has resolved value "end", return
4059 // "right" if the directionality of node is "ltr", "left" if it is "rtl"."
4060 if (resolvedValue == "end") {
4061 return getDirectionality(node) == "ltr" ? "right" : "left";
4062 }
4063
4064 // "If node's "text-align" property has resolved value "center", "justify",
4065 // "left", or "right", return that value."
4066 if (["center", "justify", "left", "right"].indexOf(resolvedValue) != -1) {
4067 return resolvedValue;
4068 }
4069
4070 // "Return "left"."
4071 return "left";
4072 }
4073
4074 function getNextEquivalentPoint(node, offset) {
4075 // "If node's length is zero, return null."
4076 if (getNodeLength(node) == 0) {
4077 return null;
4078 }
4079
4080 // "If offset is node's length, and node's parent is not null, and node is
4081 // an inline node, return (node's parent, 1 + node's index)."
4082 if (offset == getNodeLength(node)
4083 && node.parentNode
4084 && isInlineNode(node)) {
4085 return [node.parentNode, 1 + getNodeIndex(node)];
4086 }
4087
4088 // "If node has a child with index offset, and that child's length is not
4089 // zero, and that child is an inline node, return (that child, 0)."
4090 if (0 <= offset
4091 && offset < node.childNodes.length
4092 && getNodeLength(node.childNodes[offset]) != 0
4093 && isInlineNode(node.childNodes[offset])) {
4094 return [node.childNodes[offset], 0];
4095 }
4096
4097 // "Return null."
4098 return null;
4099 }
4100
4101 function getPreviousEquivalentPoint(node, offset) {
4102 // "If node's length is zero, return null."
4103 if (getNodeLength(node) == 0) {
4104 return null;
4105 }
4106
4107 // "If offset is 0, and node's parent is not null, and node is an inline
4108 // node, return (node's parent, node's index)."
4109 if (offset == 0
4110 && node.parentNode
4111 && isInlineNode(node)) {
4112 return [node.parentNode, getNodeIndex(node)];
4113 }
4114
4115 // "If node has a child with index offset − 1, and that child's length is
4116 // not zero, and that child is an inline node, return (that child, that
4117 // child's length)."
4118 if (0 <= offset - 1
4119 && offset - 1 < node.childNodes.length
4120 && getNodeLength(node.childNodes[offset - 1]) != 0
4121 && isInlineNode(node.childNodes[offset - 1])) {
4122 return [node.childNodes[offset - 1], getNodeLength(node.childNodes[offset - 1])];
4123 }
4124
4125 // "Return null."
4126 return null;
4127 }
4128
4129 function getFirstEquivalentPoint(node, offset) {
4130 // "While (node, offset)'s previous equivalent point is not null, set
4131 // (node, offset) to its previous equivalent point."
4132 var prev;
4133 while (prev = getPreviousEquivalentPoint(node, offset)) {
4134 node = prev[0];
4135 offset = prev[1];
4136 }
4137
4138 // "Return (node, offset)."
4139 return [node, offset];
4140 }
4141
4142 function getLastEquivalentPoint(node, offset) {
4143 // "While (node, offset)'s next equivalent point is not null, set (node,
4144 // offset) to its next equivalent point."
4145 var next;
4146 while (next = getNextEquivalentPoint(node, offset)) {
4147 node = next[0];
4148 offset = next[1];
4149 }
4150
4151 // "Return (node, offset)."
4152 return [node, offset];
4153 }
4154
4155 //@}
4156 ///// Block-extending a range /////
4157 //@{
4158
4159 // "A boundary point (node, offset) is a block start point if either node's
4160 // parent is null and offset is zero; or node has a child with index offset −
4161 // 1, and that child is either a visible block node or a visible br."
4162 function isBlockStartPoint(node, offset) {
4163 return (!node.parentNode && offset == 0)
4164 || (0 <= offset - 1
4165 && offset - 1 < node.childNodes.length
4166 && isVisible(node.childNodes[offset - 1])
4167 && (isBlockNode(node.childNodes[offset - 1])
4168 || isHtmlElement(node.childNodes[offset - 1], "br")));
4169 }
4170
4171 // "A boundary point (node, offset) is a block end point if either node's
4172 // parent is null and offset is node's length; or node has a child with index
4173 // offset, and that child is a visible block node."
4174 function isBlockEndPoint(node, offset) {
4175 return (!node.parentNode && offset == getNodeLength(node))
4176 || (offset < node.childNodes.length
4177 && isVisible(node.childNodes[offset])
4178 && isBlockNode(node.childNodes[offset]));
4179 }
4180
4181 // "A boundary point is a block boundary point if it is either a block start
4182 // point or a block end point."
4183 function isBlockBoundaryPoint(node, offset) {
4184 return isBlockStartPoint(node, offset)
4185 || isBlockEndPoint(node, offset);
4186 }
4187
4188 function blockExtend(range) {
4189 // "Let start node, start offset, end node, and end offset be the start
4190 // and end nodes and offsets of the range."
4191 var startNode = range.startContainer;
4192 var startOffset = range.startOffset;
4193 var endNode = range.endContainer;
4194 var endOffset = range.endOffset;
4195
4196 // "If some ancestor container of start node is an li, set start offset to
4197 // the index of the last such li in tree order, and set start node to that
4198 // li's parent."
4199 var liAncestors = getAncestors(startNode).concat(startNode)
4200 .filter(function(ancestor) { return isHtmlElement(ancestor, "li") })
4201 .slice(-1);
4202 if (liAncestors.length) {
4203 startOffset = getNodeIndex(liAncestors[0]);
4204 startNode = liAncestors[0].parentNode;
4205 }
4206
4207 // "If (start node, start offset) is not a block start point, repeat the
4208 // following steps:"
4209 if (!isBlockStartPoint(startNode, startOffset)) do {
4210 // "If start offset is zero, set it to start node's index, then set
4211 // start node to its parent."
4212 if (startOffset == 0) {
4213 startOffset = getNodeIndex(startNode);
4214 startNode = startNode.parentNode;
4215
4216 // "Otherwise, subtract one from start offset."
4217 } else {
4218 startOffset--;
4219 }
4220
4221 // "If (start node, start offset) is a block boundary point, break from
4222 // this loop."
4223 } while (!isBlockBoundaryPoint(startNode, startOffset));
4224
4225 // "While start offset is zero and start node's parent is not null, set
4226 // start offset to start node's index, then set start node to its parent."
4227 while (startOffset == 0
4228 && startNode.parentNode) {
4229 startOffset = getNodeIndex(startNode);
4230 startNode = startNode.parentNode;
4231 }
4232
4233 // "If some ancestor container of end node is an li, set end offset to one
4234 // plus the index of the last such li in tree order, and set end node to
4235 // that li's parent."
4236 var liAncestors = getAncestors(endNode).concat(endNode)
4237 .filter(function(ancestor) { return isHtmlElement(ancestor, "li") })
4238 .slice(-1);
4239 if (liAncestors.length) {
4240 endOffset = 1 + getNodeIndex(liAncestors[0]);
4241 endNode = liAncestors[0].parentNode;
4242 }
4243
4244 // "If (end node, end offset) is not a block end point, repeat the
4245 // following steps:"
4246 if (!isBlockEndPoint(endNode, endOffset)) do {
4247 // "If end offset is end node's length, set it to one plus end node's
4248 // index, then set end node to its parent."
4249 if (endOffset == getNodeLength(endNode)) {
4250 endOffset = 1 + getNodeIndex(endNode);
4251 endNode = endNode.parentNode;
4252
4253 // "Otherwise, add one to end offset.
4254 } else {
4255 endOffset++;
4256 }
4257
4258 // "If (end node, end offset) is a block boundary point, break from
4259 // this loop."
4260 } while (!isBlockBoundaryPoint(endNode, endOffset));
4261
4262 // "While end offset is end node's length and end node's parent is not
4263 // null, set end offset to one plus end node's index, then set end node to
4264 // its parent."
4265 while (endOffset == getNodeLength(endNode)
4266 && endNode.parentNode) {
4267 endOffset = 1 + getNodeIndex(endNode);
4268 endNode = endNode.parentNode;
4269 }
4270
4271 // "Let new range be a new range whose start and end nodes and offsets
4272 // are start node, start offset, end node, and end offset."
4273 var newRange = startNode.ownerDocument.createRange();
4274 newRange.setStart(startNode, startOffset);
4275 newRange.setEnd(endNode, endOffset);
4276
4277 // "Return new range."
4278 return newRange;
4279 }
4280
4281 function followsLineBreak(node) {
4282 // "Let offset be zero."
4283 var offset = 0;
4284
4285 // "While (node, offset) is not a block boundary point:"
4286 while (!isBlockBoundaryPoint(node, offset)) {
4287 // "If node has a visible child with index offset minus one, return
4288 // false."
4289 if (0 <= offset - 1
4290 && offset - 1 < node.childNodes.length
4291 && isVisible(node.childNodes[offset - 1])) {
4292 return false;
4293 }
4294
4295 // "If offset is zero or node has no children, set offset to node's
4296 // index, then set node to its parent."
4297 if (offset == 0
4298 || !node.hasChildNodes()) {
4299 offset = getNodeIndex(node);
4300 node = node.parentNode;
4301
4302 // "Otherwise, set node to its child with index offset minus one, then
4303 // set offset to node's length."
4304 } else {
4305 node = node.childNodes[offset - 1];
4306 offset = getNodeLength(node);
4307 }
4308 }
4309
4310 // "Return true."
4311 return true;
4312 }
4313
4314 function precedesLineBreak(node) {
4315 // "Let offset be node's length."
4316 var offset = getNodeLength(node);
4317
4318 // "While (node, offset) is not a block boundary point:"
4319 while (!isBlockBoundaryPoint(node, offset)) {
4320 // "If node has a visible child with index offset, return false."
4321 if (offset < node.childNodes.length
4322 && isVisible(node.childNodes[offset])) {
4323 return false;
4324 }
4325
4326 // "If offset is node's length or node has no children, set offset to
4327 // one plus node's index, then set node to its parent."
4328 if (offset == getNodeLength(node)
4329 || !node.hasChildNodes()) {
4330 offset = 1 + getNodeIndex(node);
4331 node = node.parentNode;
4332
4333 // "Otherwise, set node to its child with index offset and set offset
4334 // to zero."
4335 } else {
4336 node = node.childNodes[offset];
4337 offset = 0;
4338 }
4339 }
4340
4341 // "Return true."
4342 return true;
4343 }
4344
4345 //@}
4346 ///// Recording and restoring overrides /////
4347 //@{
4348
4349 function recordCurrentOverrides() {
4350 // "Let overrides be a list of (string, string or boolean) ordered pairs,
4351 // initially empty."
4352 var overrides = [];
4353
4354 // "If there is a value override for "createLink", add ("createLink", value
4355 // override for "createLink") to overrides."
4356 if (getValueOverride("createlink") !== undefined) {
4357 overrides.push(["createlink", getValueOverride("createlink")]);
4358 }
4359
4360 // "For each command in the list "bold", "italic", "strikethrough",
4361 // "subscript", "superscript", "underline", in order: if there is a state
4362 // override for command, add (command, command's state override) to
4363 // overrides."
4364 ["bold", "italic", "strikethrough", "subscript", "superscript",
4365 "underline"].forEach(function(command) {
4366 if (getStateOverride(command) !== undefined) {
4367 overrides.push([command, getStateOverride(command)]);
4368 }
4369 });
4370
4371 // "For each command in the list "fontName", "fontSize", "foreColor",
4372 // "hiliteColor", in order: if there is a value override for command, add
4373 // (command, command's value override) to overrides."
4374 ["fontname", "fontsize", "forecolor",
4375 "hilitecolor"].forEach(function(command) {
4376 if (getValueOverride(command) !== undefined) {
4377 overrides.push([command, getValueOverride(command)]);
4378 }
4379 });
4380
4381 // "Return overrides."
4382 return overrides;
4383 }
4384
4385 function recordCurrentStatesAndValues() {
4386 // "Let overrides be a list of (string, string or boolean) ordered pairs,
4387 // initially empty."
4388 var overrides = [];
4389
4390 // "Let node be the first formattable node effectively contained in the
4391 // active range, or null if there is none."
4392 var node = getAllEffectivelyContainedNodes(getActiveRange())
4393 .filter(isFormattableNode)[0];
4394
4395 // "If node is null, return overrides."
4396 if (!node) {
4397 return overrides;
4398 }
4399
4400 // "Add ("createLink", node's effective command value for "createLink") to
4401 // overrides."
4402 overrides.push(["createlink", getEffectiveCommandValue(node, "createlink")]);
4403
4404 // "For each command in the list "bold", "italic", "strikethrough",
4405 // "subscript", "superscript", "underline", in order: if node's effective
4406 // command value for command is one of its inline command activated values,
4407 // add (command, true) to overrides, and otherwise add (command, false) to
4408 // overrides."
4409 ["bold", "italic", "strikethrough", "subscript", "superscript",
4410 "underline"].forEach(function(command) {
4411 if (commands[command].inlineCommandActivatedValues
4412 .indexOf(getEffectiveCommandValue(node, command)) != -1) {
4413 overrides.push([command, true]);
4414 } else {
4415 overrides.push([command, false]);
4416 }
4417 });
4418
4419 // "For each command in the list "fontName", "foreColor", "hiliteColor", in
4420 // order: add (command, command's value) to overrides."
4421 ["fontname", "fontsize", "forecolor", "hilitecolor"].forEach(function(command) {
4422 overrides.push([command, commands[command].value()]);
4423 });
4424
4425 // "Add ("fontSize", node's effective command value for "fontSize") to
4426 // overrides."
4427 overrides.push(["fontsize", getEffectiveCommandValue(node, "fontsize")]);
4428
4429 // "Return overrides."
4430 return overrides;
4431 }
4432
4433 function restoreStatesAndValues(overrides) {
4434 // "Let node be the first formattable node effectively contained in the
4435 // active range, or null if there is none."
4436 var node = getAllEffectivelyContainedNodes(getActiveRange())
4437 .filter(isFormattableNode)[0];
4438
4439 // "If node is not null, then for each (command, override) pair in
4440 // overrides, in order:"
4441 if (node) {
4442 for (var i = 0; i < overrides.length; i++) {
4443 var command = overrides[i][0];
4444 var override = overrides[i][1];
4445
4446 // "If override is a boolean, and queryCommandState(command)
4447 // returns something different from override, take the action for
4448 // command, with value equal to the empty string."
4449 if (typeof override == "boolean"
4450 && myQueryCommandState(command) != override) {
4451 commands[command].action("");
4452
4453 // "Otherwise, if override is a string, and command is neither
4454 // "createLink" nor "fontSize", and queryCommandValue(command)
4455 // returns something not equivalent to override, take the action
4456 // for command, with value equal to override."
4457 } else if (typeof override == "string"
4458 && command != "createlink"
4459 && command != "fontsize"
4460 && !areEquivalentValues(command, myQueryCommandValue(command), override)) {
4461 commands[command].action(override);
4462
4463 // "Otherwise, if override is a string; and command is
4464 // "createLink"; and either there is a value override for
4465 // "createLink" that is not equal to override, or there is no value
4466 // override for "createLink" and node's effective command value for
4467 // "createLink" is not equal to override: take the action for
4468 // "createLink", with value equal to override."
4469 } else if (typeof override == "string"
4470 && command == "createlink"
4471 && (
4472 (
4473 getValueOverride("createlink") !== undefined
4474 && getValueOverride("createlink") !== override
4475 ) || (
4476 getValueOverride("createlink") === undefined
4477 && getEffectiveCommandValue(node, "createlink") !== override
4478 )
4479 )) {
4480 commands.createlink.action(override);
4481
4482 // "Otherwise, if override is a string; and command is "fontSize";
4483 // and either there is a value override for "fontSize" that is not
4484 // equal to override, or there is no value override for "fontSize"
4485 // and node's effective command value for "fontSize" is not loosely
4486 // equivalent to override:"
4487 } else if (typeof override == "string"
4488 && command == "fontsize"
4489 && (
4490 (
4491 getValueOverride("fontsize") !== undefined
4492 && getValueOverride("fontsize") !== override
4493 ) || (
4494 getValueOverride("fontsize") === undefined
4495 && !areLooselyEquivalentValues(command, getEffectiveCommandValue(node, "fontsize"), override)
4496 )
4497 )) {
4498 // "Convert override to an integer number of pixels, and set
4499 // override to the legacy font size for the result."
4500 override = getLegacyFontSize(override);
4501
4502 // "Take the action for "fontSize", with value equal to
4503 // override."
4504 commands.fontsize.action(override);
4505
4506 // "Otherwise, continue this loop from the beginning."
4507 } else {
4508 continue;
4509 }
4510
4511 // "Set node to the first formattable node effectively contained in
4512 // the active range, if there is one."
4513 node = getAllEffectivelyContainedNodes(getActiveRange())
4514 .filter(isFormattableNode)[0]
4515 || node;
4516 }
4517
4518 // "Otherwise, for each (command, override) pair in overrides, in order:"
4519 } else {
4520 for (var i = 0; i < overrides.length; i++) {
4521 var command = overrides[i][0];
4522 var override = overrides[i][1];
4523
4524 // "If override is a boolean, set the state override for command to
4525 // override."
4526 if (typeof override == "boolean") {
4527 setStateOverride(command, override);
4528 }
4529
4530 // "If override is a string, set the value override for command to
4531 // override."
4532 if (typeof override == "string") {
4533 setValueOverride(command, override);
4534 }
4535 }
4536 }
4537 }
4538
4539 //@}
4540 ///// Deleting the selection /////
4541 //@{
4542
4543 // The flags argument is a dictionary that can have blockMerging,
4544 // stripWrappers, and/or direction as keys.
4545 function deleteSelection(flags) {
4546 if (flags === undefined) {
4547 flags = {};
4548 }
4549
4550 var blockMerging = "blockMerging" in flags ? Boolean(flags.blockMerging) : true;
4551 var stripWrappers = "stripWrappers" in flags ? Boolean(flags.stripWrappers) : true;
4552 var direction = "direction" in flags ? flags.direction : "forward";
4553
4554 // "If the active range is null, abort these steps and do nothing."
4555 if (!getActiveRange()) {
4556 return;
4557 }
4558
4559 // "Canonicalize whitespace at the active range's start."
4560 canonicalizeWhitespace(getActiveRange().startContainer, getActiveRange().startOffset);
4561
4562 // "Canonicalize whitespace at the active range's end."
4563 canonicalizeWhitespace(getActiveRange().endContainer, getActiveRange().endOffset);
4564
4565 // "Let (start node, start offset) be the last equivalent point for the
4566 // active range's start."
4567 var start = getLastEquivalentPoint(getActiveRange().startContainer, getActiveRange().startOffset);
4568 var startNode = start[0];
4569 var startOffset = start[1];
4570
4571 // "Let (end node, end offset) be the first equivalent point for the active
4572 // range's end."
4573 var end = getFirstEquivalentPoint(getActiveRange().endContainer, getActiveRange().endOffset);
4574 var endNode = end[0];
4575 var endOffset = end[1];
4576
4577 // "If (end node, end offset) is not after (start node, start offset):"
4578 if (getPosition(endNode, endOffset, startNode, startOffset) !== "after") {
4579 // "If direction is "forward", call collapseToStart() on the context
4580 // object's Selection."
4581 //
4582 // Here and in a few other places, we check rangeCount to work around a
4583 // WebKit bug: it will sometimes incorrectly remove ranges from the
4584 // selection if nodes are removed, so collapseToStart() will throw.
4585 // This will break everything if we're using an actual selection, but
4586 // if getActiveRange() is really just returning globalRange and that's
4587 // all we care about, it will work fine. I only add the extra check
4588 // for errors I actually hit in testing.
4589 if (direction == "forward") {
4590 if (getSelection().rangeCount) {
4591 getSelection().collapseToStart();
4592 }
4593 getActiveRange().collapse(true);
4594
4595 // "Otherwise, call collapseToEnd() on the context object's Selection."
4596 } else {
4597 getSelection().collapseToEnd();
4598 getActiveRange().collapse(false);
4599 }
4600
4601 // "Abort these steps."
4602 return;
4603 }
4604
4605 // "If start node is a Text node and start offset is 0, set start offset to
4606 // the index of start node, then set start node to its parent."
4607 if (startNode.nodeType == Node.TEXT_NODE
4608 && startOffset == 0) {
4609 startOffset = getNodeIndex(startNode);
4610 startNode = startNode.parentNode;
4611 }
4612
4613 // "If end node is a Text node and end offset is its length, set end offset
4614 // to one plus the index of end node, then set end node to its parent."
4615 if (endNode.nodeType == Node.TEXT_NODE
4616 && endOffset == getNodeLength(endNode)) {
4617 endOffset = 1 + getNodeIndex(endNode);
4618 endNode = endNode.parentNode;
4619 }
4620
4621 // "Call collapse(start node, start offset) on the context object's
4622 // Selection."
4623 getSelection().collapse(startNode, startOffset);
4624 getActiveRange().setStart(startNode, startOffset);
4625
4626 // "Call extend(end node, end offset) on the context object's Selection."
4627 getSelection().extend(endNode, endOffset);
4628 getActiveRange().setEnd(endNode, endOffset);
4629
4630 // "Let start block be the active range's start node."
4631 var startBlock = getActiveRange().startContainer;
4632
4633 // "While start block's parent is in the same editing host and start block
4634 // is an inline node, set start block to its parent."
4635 while (inSameEditingHost(startBlock, startBlock.parentNode)
4636 && isInlineNode(startBlock)) {
4637 startBlock = startBlock.parentNode;
4638 }
4639
4640 // "If start block is neither a block node nor an editing host, or "span"
4641 // is not an allowed child of start block, or start block is a td or th,
4642 // set start block to null."
4643 if ((!isBlockNode(startBlock) && !isEditingHost(startBlock))
4644 || !isAllowedChild("span", startBlock)
4645 || isHtmlElement(startBlock, ["td", "th"])) {
4646 startBlock = null;
4647 }
4648
4649 // "Let end block be the active range's end node."
4650 var endBlock = getActiveRange().endContainer;
4651
4652 // "While end block's parent is in the same editing host and end block is
4653 // an inline node, set end block to its parent."
4654 while (inSameEditingHost(endBlock, endBlock.parentNode)
4655 && isInlineNode(endBlock)) {
4656 endBlock = endBlock.parentNode;
4657 }
4658
4659 // "If end block is neither a block node nor an editing host, or "span" is
4660 // not an allowed child of end block, or end block is a td or th, set end
4661 // block to null."
4662 if ((!isBlockNode(endBlock) && !isEditingHost(endBlock))
4663 || !isAllowedChild("span", endBlock)
4664 || isHtmlElement(endBlock, ["td", "th"])) {
4665 endBlock = null;
4666 }
4667
4668 // "Record current states and values, and let overrides be the result."
4669 var overrides = recordCurrentStatesAndValues();
4670
4671 // "If start node and end node are the same, and start node is an editable
4672 // Text node:"
4673 if (startNode == endNode
4674 && isEditable(startNode)
4675 && startNode.nodeType == Node.TEXT_NODE) {
4676 // "Call deleteData(start offset, end offset − start offset) on start
4677 // node."
4678 startNode.deleteData(startOffset, endOffset - startOffset);
4679
4680 // "Canonicalize whitespace at (start node, start offset), with fix
4681 // collapsed space false."
4682 canonicalizeWhitespace(startNode, startOffset, false);
4683
4684 // "If direction is "forward", call collapseToStart() on the context
4685 // object's Selection."
4686 if (direction == "forward") {
4687 if (getSelection().rangeCount) {
4688 getSelection().collapseToStart();
4689 }
4690 getActiveRange().collapse(true);
4691
4692 // "Otherwise, call collapseToEnd() on the context object's Selection."
4693 } else {
4694 getSelection().collapseToEnd();
4695 getActiveRange().collapse(false);
4696 }
4697
4698 // "Restore states and values from overrides."
4699 restoreStatesAndValues(overrides);
4700
4701 // "Abort these steps."
4702 return;
4703 }
4704
4705 // "If start node is an editable Text node, call deleteData() on it, with
4706 // start offset as the first argument and (length of start node − start
4707 // offset) as the second argument."
4708 if (isEditable(startNode)
4709 && startNode.nodeType == Node.TEXT_NODE) {
4710 startNode.deleteData(startOffset, getNodeLength(startNode) - startOffset);
4711 }
4712
4713 // "Let node list be a list of nodes, initially empty."
4714 //
4715 // "For each node contained in the active range, append node to node list
4716 // if the last member of node list (if any) is not an ancestor of node;
4717 // node is editable; and node is not a thead, tbody, tfoot, tr, th, or td."
4718 var nodeList = getContainedNodes(getActiveRange(),
4719 function(node) {
4720 return isEditable(node)
4721 && !isHtmlElement(node, ["thead", "tbody", "tfoot", "tr", "th", "td"]);
4722 }
4723 );
4724
4725 // "For each node in node list:"
4726 for (var i = 0; i < nodeList.length; i++) {
4727 var node = nodeList[i];
4728
4729 // "Let parent be the parent of node."
4730 var parent_ = node.parentNode;
4731
4732 // "Remove node from parent."
4733 parent_.removeChild(node);
4734
4735 // "If the block node of parent has no visible children, and parent is
4736 // editable or an editing host, call createElement("br") on the context
4737 // object and append the result as the last child of parent."
4738 if (![].some.call(getBlockNodeOf(parent_).childNodes, isVisible)
4739 && (isEditable(parent_) || isEditingHost(parent_))) {
4740 parent_.appendChild(document.createElement("br"));
4741 }
4742
4743 // "If strip wrappers is true or parent is not an ancestor container of
4744 // start node, while parent is an editable inline node with length 0,
4745 // let grandparent be the parent of parent, then remove parent from
4746 // grandparent, then set parent to grandparent."
4747 if (stripWrappers
4748 || (!isAncestor(parent_, startNode) && parent_ != startNode)) {
4749 while (isEditable(parent_)
4750 && isInlineNode(parent_)
4751 && getNodeLength(parent_) == 0) {
4752 var grandparent = parent_.parentNode;
4753 grandparent.removeChild(parent_);
4754 parent_ = grandparent;
4755 }
4756 }
4757 }
4758
4759 // "If end node is an editable Text node, call deleteData(0, end offset) on
4760 // it."
4761 if (isEditable(endNode)
4762 && endNode.nodeType == Node.TEXT_NODE) {
4763 endNode.deleteData(0, endOffset);
4764 }
4765
4766 // "Canonicalize whitespace at the active range's start, with fix collapsed
4767 // space false."
4768 canonicalizeWhitespace(getActiveRange().startContainer, getActiveRange().startOffset, false);
4769
4770 // "Canonicalize whitespace at the active range's end, with fix collapsed
4771 // space false."
4772 canonicalizeWhitespace(getActiveRange().endContainer, getActiveRange().endOffset, false);
4773
4774 // "If block merging is false, or start block or end block is null, or
4775 // start block is not in the same editing host as end block, or start block
4776 // and end block are the same:"
4777 if (!blockMerging
4778 || !startBlock
4779 || !endBlock
4780 || !inSameEditingHost(startBlock, endBlock)
4781 || startBlock == endBlock) {
4782 // "If direction is "forward", call collapseToStart() on the context
4783 // object's Selection."
4784 if (direction == "forward") {
4785 if (getSelection().rangeCount) {
4786 getSelection().collapseToStart();
4787 }
4788 getActiveRange().collapse(true);
4789
4790 // "Otherwise, call collapseToEnd() on the context object's Selection."
4791 } else {
4792 if (getSelection().rangeCount) {
4793 getSelection().collapseToEnd();
4794 }
4795 getActiveRange().collapse(false);
4796 }
4797
4798 // "Restore states and values from overrides."
4799 restoreStatesAndValues(overrides);
4800
4801 // "Abort these steps."
4802 return;
4803 }
4804
4805 // "If start block has one child, which is a collapsed block prop, remove
4806 // its child from it."
4807 if (startBlock.children.length == 1
4808 && isCollapsedBlockProp(startBlock.firstChild)) {
4809 startBlock.removeChild(startBlock.firstChild);
4810 }
4811
4812 // "If start block is an ancestor of end block:"
4813 if (isAncestor(startBlock, endBlock)) {
4814 // "Let reference node be end block."
4815 var referenceNode = endBlock;
4816
4817 // "While reference node is not a child of start block, set reference
4818 // node to its parent."
4819 while (referenceNode.parentNode != startBlock) {
4820 referenceNode = referenceNode.parentNode;
4821 }
4822
4823 // "Call collapse() on the context object's Selection, with first
4824 // argument start block and second argument the index of reference
4825 // node."
4826 getSelection().collapse(startBlock, getNodeIndex(referenceNode));
4827 getActiveRange().setStart(startBlock, getNodeIndex(referenceNode));
4828 getActiveRange().collapse(true);
4829
4830 // "If end block has no children:"
4831 if (!endBlock.hasChildNodes()) {
4832 // "While end block is editable and is the only child of its parent
4833 // and is not a child of start block, let parent equal end block,
4834 // then remove end block from parent, then set end block to
4835 // parent."
4836 while (isEditable(endBlock)
4837 && endBlock.parentNode.childNodes.length == 1
4838 && endBlock.parentNode != startBlock) {
4839 var parent_ = endBlock;
4840 parent_.removeChild(endBlock);
4841 endBlock = parent_;
4842 }
4843
4844 // "If end block is editable and is not an inline node, and its
4845 // previousSibling and nextSibling are both inline nodes, call
4846 // createElement("br") on the context object and insert it into end
4847 // block's parent immediately after end block."
4848 if (isEditable(endBlock)
4849 && !isInlineNode(endBlock)
4850 && isInlineNode(endBlock.previousSibling)
4851 && isInlineNode(endBlock.nextSibling)) {
4852 endBlock.parentNode.insertBefore(document.createElement("br"), endBlock.nextSibling);
4853 }
4854
4855 // "If end block is editable, remove it from its parent."
4856 if (isEditable(endBlock)) {
4857 endBlock.parentNode.removeChild(endBlock);
4858 }
4859
4860 // "Restore states and values from overrides."
4861 restoreStatesAndValues(overrides);
4862
4863 // "Abort these steps."
4864 return;
4865 }
4866
4867 // "If end block's firstChild is not an inline node, restore states and
4868 // values from overrides, then abort these steps."
4869 if (!isInlineNode(endBlock.firstChild)) {
4870 restoreStatesAndValues(overrides);
4871 return;
4872 }
4873
4874 // "Let children be a list of nodes, initially empty."
4875 var children = [];
4876
4877 // "Append the first child of end block to children."
4878 children.push(endBlock.firstChild);
4879
4880 // "While children's last member is not a br, and children's last
4881 // member's nextSibling is an inline node, append children's last
4882 // member's nextSibling to children."
4883 while (!isHtmlElement(children[children.length - 1], "br")
4884 && isInlineNode(children[children.length - 1].nextSibling)) {
4885 children.push(children[children.length - 1].nextSibling);
4886 }
4887
4888 // "Record the values of children, and let values be the result."
4889 var values = recordValues(children);
4890
4891 // "While children's first member's parent is not start block, split
4892 // the parent of children."
4893 while (children[0].parentNode != startBlock) {
4894 splitParent(children);
4895 }
4896
4897 // "If children's first member's previousSibling is an editable br,
4898 // remove that br from its parent."
4899 if (isEditable(children[0].previousSibling)
4900 && isHtmlElement(children[0].previousSibling, "br")) {
4901 children[0].parentNode.removeChild(children[0].previousSibling);
4902 }
4903
4904 // "Otherwise, if start block is a descendant of end block:"
4905 } else if (isDescendant(startBlock, endBlock)) {
4906 // "Call collapse() on the context object's Selection, with first
4907 // argument start block and second argument start block's length."
4908 getSelection().collapse(startBlock, getNodeLength(startBlock));
4909 getActiveRange().setStart(startBlock, getNodeLength(startBlock));
4910 getActiveRange().collapse(true);
4911
4912 // "Let reference node be start block."
4913 var referenceNode = startBlock;
4914
4915 // "While reference node is not a child of end block, set reference
4916 // node to its parent."
4917 while (referenceNode.parentNode != endBlock) {
4918 referenceNode = referenceNode.parentNode;
4919 }
4920
4921 // "If reference node's nextSibling is an inline node and start block's
4922 // lastChild is a br, remove start block's lastChild from it."
4923 if (isInlineNode(referenceNode.nextSibling)
4924 && isHtmlElement(startBlock.lastChild, "br")) {
4925 startBlock.removeChild(startBlock.lastChild);
4926 }
4927
4928 // "Let nodes to move be a list of nodes, initially empty."
4929 var nodesToMove = [];
4930
4931 // "If reference node's nextSibling is neither null nor a block node,
4932 // append it to nodes to move."
4933 if (referenceNode.nextSibling
4934 && !isBlockNode(referenceNode.nextSibling)) {
4935 nodesToMove.push(referenceNode.nextSibling);
4936 }
4937
4938 // "While nodes to move is nonempty and its last member isn't a br and
4939 // its last member's nextSibling is neither null nor a block node,
4940 // append its last member's nextSibling to nodes to move."
4941 if (nodesToMove.length
4942 && !isHtmlElement(nodesToMove[nodesToMove.length - 1], "br")
4943 && nodesToMove[nodesToMove.length - 1].nextSibling
4944 && !isBlockNode(nodesToMove[nodesToMove.length - 1].nextSibling)) {
4945 nodesToMove.push(nodesToMove[nodesToMove.length - 1].nextSibling);
4946 }
4947
4948 // "Record the values of nodes to move, and let values be the result."
4949 var values = recordValues(nodesToMove);
4950
4951 // "For each node in nodes to move, append node as the last child of
4952 // start block, preserving ranges."
4953 nodesToMove.forEach(function(node) {
4954 movePreservingRanges(node, startBlock, -1);
4955 });
4956
4957 // "Otherwise:"
4958 } else {
4959 // "Call collapse() on the context object's Selection, with first
4960 // argument start block and second argument start block's length."
4961 getSelection().collapse(startBlock, getNodeLength(startBlock));
4962 getActiveRange().setStart(startBlock, getNodeLength(startBlock));
4963 getActiveRange().collapse(true);
4964
4965 // "If end block's firstChild is an inline node and start block's
4966 // lastChild is a br, remove start block's lastChild from it."
4967 if (isInlineNode(endBlock.firstChild)
4968 && isHtmlElement(startBlock.lastChild, "br")) {
4969 startBlock.removeChild(startBlock.lastChild);
4970 }
4971
4972 // "Record the values of end block's children, and let values be the
4973 // result."
4974 var values = recordValues([].slice.call(endBlock.childNodes));
4975
4976 // "While end block has children, append the first child of end block
4977 // to start block, preserving ranges."
4978 while (endBlock.hasChildNodes()) {
4979 movePreservingRanges(endBlock.firstChild, startBlock, -1);
4980 }
4981
4982 // "While end block has no children, let parent be the parent of end
4983 // block, then remove end block from parent, then set end block to
4984 // parent."
4985 while (!endBlock.hasChildNodes()) {
4986 var parent_ = endBlock.parentNode;
4987 parent_.removeChild(endBlock);
4988 endBlock = parent_;
4989 }
4990 }
4991
4992 // "Let ancestor be start block."
4993 var ancestor = startBlock;
4994
4995 // "While ancestor has an inclusive ancestor ol in the same editing host
4996 // whose nextSibling is also an ol in the same editing host, or an
4997 // inclusive ancestor ul in the same editing host whose nextSibling is also
4998 // a ul in the same editing host:"
4999 while (getInclusiveAncestors(ancestor).some(function(node) {
5000 return inSameEditingHost(ancestor, node)
5001 && (
5002 (isHtmlElement(node, "ol") && isHtmlElement(node.nextSibling, "ol"))
5003 || (isHtmlElement(node, "ul") && isHtmlElement(node.nextSibling, "ul"))
5004 ) && inSameEditingHost(ancestor, node.nextSibling);
5005 })) {
5006 // "While ancestor and its nextSibling are not both ols in the same
5007 // editing host, and are also not both uls in the same editing host,
5008 // set ancestor to its parent."
5009 while (!(
5010 isHtmlElement(ancestor, "ol")
5011 && isHtmlElement(ancestor.nextSibling, "ol")
5012 && inSameEditingHost(ancestor, ancestor.nextSibling)
5013 ) && !(
5014 isHtmlElement(ancestor, "ul")
5015 && isHtmlElement(ancestor.nextSibling, "ul")
5016 && inSameEditingHost(ancestor, ancestor.nextSibling)
5017 )) {
5018 ancestor = ancestor.parentNode;
5019 }
5020
5021 // "While ancestor's nextSibling has children, append ancestor's
5022 // nextSibling's firstChild as the last child of ancestor, preserving
5023 // ranges."
5024 while (ancestor.nextSibling.hasChildNodes()) {
5025 movePreservingRanges(ancestor.nextSibling.firstChild, ancestor, -1);
5026 }
5027
5028 // "Remove ancestor's nextSibling from its parent."
5029 ancestor.parentNode.removeChild(ancestor.nextSibling);
5030 }
5031
5032 // "Restore the values from values."
5033 restoreValues(values);
5034
5035 // "If start block has no children, call createElement("br") on the context
5036 // object and append the result as the last child of start block."
5037 if (!startBlock.hasChildNodes()) {
5038 startBlock.appendChild(document.createElement("br"));
5039 }
5040
5041 // "Remove extraneous line breaks at the end of start block."
5042 removeExtraneousLineBreaksAtTheEndOf(startBlock);
5043
5044 // "Restore states and values from overrides."
5045 restoreStatesAndValues(overrides);
5046 }
5047
5048
5049 //@}
5050 ///// Splitting a node list's parent /////
5051 //@{
5052
5053 function splitParent(nodeList) {
5054 // "Let original parent be the parent of the first member of node list."
5055 var originalParent = nodeList[0].parentNode;
5056
5057 // "If original parent is not editable or its parent is null, do nothing
5058 // and abort these steps."
5059 if (!isEditable(originalParent)
5060 || !originalParent.parentNode) {
5061 return;
5062 }
5063
5064 // "If the first child of original parent is in node list, remove
5065 // extraneous line breaks before original parent."
5066 if (nodeList.indexOf(originalParent.firstChild) != -1) {
5067 removeExtraneousLineBreaksBefore(originalParent);
5068 }
5069
5070 // "If the first child of original parent is in node list, and original
5071 // parent follows a line break, set follows line break to true. Otherwise,
5072 // set follows line break to false."
5073 var followsLineBreak_ = nodeList.indexOf(originalParent.firstChild) != -1
5074 && followsLineBreak(originalParent);
5075
5076 // "If the last child of original parent is in node list, and original
5077 // parent precedes a line break, set precedes line break to true.
5078 // Otherwise, set precedes line break to false."
5079 var precedesLineBreak_ = nodeList.indexOf(originalParent.lastChild) != -1
5080 && precedesLineBreak(originalParent);
5081
5082 // "If the first child of original parent is not in node list, but its last
5083 // child is:"
5084 if (nodeList.indexOf(originalParent.firstChild) == -1
5085 && nodeList.indexOf(originalParent.lastChild) != -1) {
5086 // "For each node in node list, in reverse order, insert node into the
5087 // parent of original parent immediately after original parent,
5088 // preserving ranges."
5089 for (var i = nodeList.length - 1; i >= 0; i--) {
5090 movePreservingRanges(nodeList[i], originalParent.parentNode, 1 + getNodeIndex(originalParent));
5091 }
5092
5093 // "If precedes line break is true, and the last member of node list
5094 // does not precede a line break, call createElement("br") on the
5095 // context object and insert the result immediately after the last
5096 // member of node list."
5097 if (precedesLineBreak_
5098 && !precedesLineBreak(nodeList[nodeList.length - 1])) {
5099 nodeList[nodeList.length - 1].parentNode.insertBefore(document.createElement("br"), nodeList[nodeList.length - 1].nextSibling);
5100 }
5101
5102 // "Remove extraneous line breaks at the end of original parent."
5103 removeExtraneousLineBreaksAtTheEndOf(originalParent);
5104
5105 // "Abort these steps."
5106 return;
5107 }
5108
5109 // "If the first child of original parent is not in node list:"
5110 if (nodeList.indexOf(originalParent.firstChild) == -1) {
5111 // "Let cloned parent be the result of calling cloneNode(false) on
5112 // original parent."
5113 var clonedParent = originalParent.cloneNode(false);
5114
5115 // "If original parent has an id attribute, unset it."
5116 originalParent.removeAttribute("id");
5117
5118 // "Insert cloned parent into the parent of original parent immediately
5119 // before original parent."
5120 originalParent.parentNode.insertBefore(clonedParent, originalParent);
5121
5122 // "While the previousSibling of the first member of node list is not
5123 // null, append the first child of original parent as the last child of
5124 // cloned parent, preserving ranges."
5125 while (nodeList[0].previousSibling) {
5126 movePreservingRanges(originalParent.firstChild, clonedParent, clonedParent.childNodes.length);
5127 }
5128 }
5129
5130 // "For each node in node list, insert node into the parent of original
5131 // parent immediately before original parent, preserving ranges."
5132 for (var i = 0; i < nodeList.length; i++) {
5133 movePreservingRanges(nodeList[i], originalParent.parentNode, getNodeIndex(originalParent));
5134 }
5135
5136 // "If follows line break is true, and the first member of node list does
5137 // not follow a line break, call createElement("br") on the context object
5138 // and insert the result immediately before the first member of node list."
5139 if (followsLineBreak_
5140 && !followsLineBreak(nodeList[0])) {
5141 nodeList[0].parentNode.insertBefore(document.createElement("br"), nodeList[0]);
5142 }
5143
5144 // "If the last member of node list is an inline node other than a br, and
5145 // the first child of original parent is a br, and original parent is not
5146 // an inline node, remove the first child of original parent from original
5147 // parent."
5148 if (isInlineNode(nodeList[nodeList.length - 1])
5149 && !isHtmlElement(nodeList[nodeList.length - 1], "br")
5150 && isHtmlElement(originalParent.firstChild, "br")
5151 && !isInlineNode(originalParent)) {
5152 originalParent.removeChild(originalParent.firstChild);
5153 }
5154
5155 // "If original parent has no children:"
5156 if (!originalParent.hasChildNodes()) {
5157 // "Remove original parent from its parent."
5158 originalParent.parentNode.removeChild(originalParent);
5159
5160 // "If precedes line break is true, and the last member of node list
5161 // does not precede a line break, call createElement("br") on the
5162 // context object and insert the result immediately after the last
5163 // member of node list."
5164 if (precedesLineBreak_
5165 && !precedesLineBreak(nodeList[nodeList.length - 1])) {
5166 nodeList[nodeList.length - 1].parentNode.insertBefore(document.createElement("br"), nodeList[nodeList.length - 1].nextSibling);
5167 }
5168
5169 // "Otherwise, remove extraneous line breaks before original parent."
5170 } else {
5171 removeExtraneousLineBreaksBefore(originalParent);
5172 }
5173
5174 // "If node list's last member's nextSibling is null, but its parent is not
5175 // null, remove extraneous line breaks at the end of node list's last
5176 // member's parent."
5177 if (!nodeList[nodeList.length - 1].nextSibling
5178 && nodeList[nodeList.length - 1].parentNode) {
5179 removeExtraneousLineBreaksAtTheEndOf(nodeList[nodeList.length - 1].parentNode);
5180 }
5181 }
5182
5183 // "To remove a node node while preserving its descendants, split the parent of
5184 // node's children if it has any. If it has no children, instead remove it from
5185 // its parent."
5186 function removePreservingDescendants(node) {
5187 if (node.hasChildNodes()) {
5188 splitParent([].slice.call(node.childNodes));
5189 } else {
5190 node.parentNode.removeChild(node);
5191 }
5192 }
5193
5194
5195 //@}
5196 ///// Canonical space sequences /////
5197 //@{
5198
5199 function canonicalSpaceSequence(n, nonBreakingStart, nonBreakingEnd) {
5200 // "If n is zero, return the empty string."
5201 if (n == 0) {
5202 return "";
5203 }
5204
5205 // "If n is one and both non-breaking start and non-breaking end are false,
5206 // return a single space (U+0020)."
5207 if (n == 1 && !nonBreakingStart && !nonBreakingEnd) {
5208 return " ";
5209 }
5210
5211 // "If n is one, return a single non-breaking space (U+00A0)."
5212 if (n == 1) {
5213 return "\xa0";
5214 }
5215
5216 // "Let buffer be the empty string."
5217 var buffer = "";
5218
5219 // "If non-breaking start is true, let repeated pair be U+00A0 U+0020.
5220 // Otherwise, let it be U+0020 U+00A0."
5221 var repeatedPair;
5222 if (nonBreakingStart) {
5223 repeatedPair = "\xa0 ";
5224 } else {
5225 repeatedPair = " \xa0";
5226 }
5227
5228 // "While n is greater than three, append repeated pair to buffer and
5229 // subtract two from n."
5230 while (n > 3) {
5231 buffer += repeatedPair;
5232 n -= 2;
5233 }
5234
5235 // "If n is three, append a three-element string to buffer depending on
5236 // non-breaking start and non-breaking end:"
5237 if (n == 3) {
5238 buffer +=
5239 !nonBreakingStart && !nonBreakingEnd ? " \xa0 "
5240 : nonBreakingStart && !nonBreakingEnd ? "\xa0\xa0 "
5241 : !nonBreakingStart && nonBreakingEnd ? " \xa0\xa0"
5242 : nonBreakingStart && nonBreakingEnd ? "\xa0 \xa0"
5243 : "impossible";
5244
5245 // "Otherwise, append a two-element string to buffer depending on
5246 // non-breaking start and non-breaking end:"
5247 } else {
5248 buffer +=
5249 !nonBreakingStart && !nonBreakingEnd ? "\xa0 "
5250 : nonBreakingStart && !nonBreakingEnd ? "\xa0 "
5251 : !nonBreakingStart && nonBreakingEnd ? " \xa0"
5252 : nonBreakingStart && nonBreakingEnd ? "\xa0\xa0"
5253 : "impossible";
5254 }
5255
5256 // "Return buffer."
5257 return buffer;
5258 }
5259
5260 function canonicalizeWhitespace(node, offset, fixCollapsedSpace) {
5261 if (fixCollapsedSpace === undefined) {
5262 // "an optional boolean argument fix collapsed space that defaults to
5263 // true"
5264 fixCollapsedSpace = true;
5265 }
5266
5267 // "If node is neither editable nor an editing host, abort these steps."
5268 if (!isEditable(node) && !isEditingHost(node)) {
5269 return;
5270 }
5271
5272 // "Let start node equal node and let start offset equal offset."
5273 var startNode = node;
5274 var startOffset = offset;
5275
5276 // "Repeat the following steps:"
5277 while (true) {
5278 // "If start node has a child in the same editing host with index start
5279 // offset minus one, set start node to that child, then set start
5280 // offset to start node's length."
5281 if (0 <= startOffset - 1
5282 && inSameEditingHost(startNode, startNode.childNodes[startOffset - 1])) {
5283 startNode = startNode.childNodes[startOffset - 1];
5284 startOffset = getNodeLength(startNode);
5285
5286 // "Otherwise, if start offset is zero and start node does not follow a
5287 // line break and start node's parent is in the same editing host, set
5288 // start offset to start node's index, then set start node to its
5289 // parent."
5290 } else if (startOffset == 0
5291 && !followsLineBreak(startNode)
5292 && inSameEditingHost(startNode, startNode.parentNode)) {
5293 startOffset = getNodeIndex(startNode);
5294 startNode = startNode.parentNode;
5295
5296 // "Otherwise, if start node is a Text node and its parent's resolved
5297 // value for "white-space" is neither "pre" nor "pre-wrap" and start
5298 // offset is not zero and the (start offset − 1)st element of start
5299 // node's data is a space (0x0020) or non-breaking space (0x00A0),
5300 // subtract one from start offset."
5301 } else if (startNode.nodeType == Node.TEXT_NODE
5302 && ["pre", "pre-wrap"].indexOf(getComputedStyle(startNode.parentNode).whiteSpace) == -1
5303 && startOffset != 0
5304 && /[ \xa0]/.test(startNode.data[startOffset - 1])) {
5305 startOffset--;
5306
5307 // "Otherwise, break from this loop."
5308 } else {
5309 break;
5310 }
5311 }
5312
5313 // "Let end node equal start node and end offset equal start offset."
5314 var endNode = startNode;
5315 var endOffset = startOffset;
5316
5317 // "Let length equal zero."
5318 var length = 0;
5319
5320 // "Let collapse spaces be true if start offset is zero and start node
5321 // follows a line break, otherwise false."
5322 var collapseSpaces = startOffset == 0 && followsLineBreak(startNode);
5323
5324 // "Repeat the following steps:"
5325 while (true) {
5326 // "If end node has a child in the same editing host with index end
5327 // offset, set end node to that child, then set end offset to zero."
5328 if (endOffset < endNode.childNodes.length
5329 && inSameEditingHost(endNode, endNode.childNodes[endOffset])) {
5330 endNode = endNode.childNodes[endOffset];
5331 endOffset = 0;
5332
5333 // "Otherwise, if end offset is end node's length and end node does not
5334 // precede a line break and end node's parent is in the same editing
5335 // host, set end offset to one plus end node's index, then set end node
5336 // to its parent."
5337 } else if (endOffset == getNodeLength(endNode)
5338 && !precedesLineBreak(endNode)
5339 && inSameEditingHost(endNode, endNode.parentNode)) {
5340 endOffset = 1 + getNodeIndex(endNode);
5341 endNode = endNode.parentNode;
5342
5343 // "Otherwise, if end node is a Text node and its parent's resolved
5344 // value for "white-space" is neither "pre" nor "pre-wrap" and end
5345 // offset is not end node's length and the end offsetth element of
5346 // end node's data is a space (0x0020) or non-breaking space (0x00A0):"
5347 } else if (endNode.nodeType == Node.TEXT_NODE
5348 && ["pre", "pre-wrap"].indexOf(getComputedStyle(endNode.parentNode).whiteSpace) == -1
5349 && endOffset != getNodeLength(endNode)
5350 && /[ \xa0]/.test(endNode.data[endOffset])) {
5351 // "If fix collapsed space is true, and collapse spaces is true,
5352 // and the end offsetth code unit of end node's data is a space
5353 // (0x0020): call deleteData(end offset, 1) on end node, then
5354 // continue this loop from the beginning."
5355 if (fixCollapsedSpace
5356 && collapseSpaces
5357 && " " == endNode.data[endOffset]) {
5358 endNode.deleteData(endOffset, 1);
5359 continue;
5360 }
5361
5362 // "Set collapse spaces to true if the end offsetth element of end
5363 // node's data is a space (0x0020), false otherwise."
5364 collapseSpaces = " " == endNode.data[endOffset];
5365
5366 // "Add one to end offset."
5367 endOffset++;
5368
5369 // "Add one to length."
5370 length++;
5371
5372 // "Otherwise, break from this loop."
5373 } else {
5374 break;
5375 }
5376 }
5377
5378 // "If fix collapsed space is true, then while (start node, start offset)
5379 // is before (end node, end offset):"
5380 if (fixCollapsedSpace) {
5381 while (getPosition(startNode, startOffset, endNode, endOffset) == "before") {
5382 // "If end node has a child in the same editing host with index end
5383 // offset − 1, set end node to that child, then set end offset to end
5384 // node's length."
5385 if (0 <= endOffset - 1
5386 && endOffset - 1 < endNode.childNodes.length
5387 && inSameEditingHost(endNode, endNode.childNodes[endOffset - 1])) {
5388 endNode = endNode.childNodes[endOffset - 1];
5389 endOffset = getNodeLength(endNode);
5390
5391 // "Otherwise, if end offset is zero and end node's parent is in the
5392 // same editing host, set end offset to end node's index, then set end
5393 // node to its parent."
5394 } else if (endOffset == 0
5395 && inSameEditingHost(endNode, endNode.parentNode)) {
5396 endOffset = getNodeIndex(endNode);
5397 endNode = endNode.parentNode;
5398
5399 // "Otherwise, if end node is a Text node and its parent's resolved
5400 // value for "white-space" is neither "pre" nor "pre-wrap" and end
5401 // offset is end node's length and the last code unit of end node's
5402 // data is a space (0x0020) and end node precedes a line break:"
5403 } else if (endNode.nodeType == Node.TEXT_NODE
5404 && ["pre", "pre-wrap"].indexOf(getComputedStyle(endNode.parentNode).whiteSpace) == -1
5405 && endOffset == getNodeLength(endNode)
5406 && endNode.data[endNode.data.length - 1] == " "
5407 && precedesLineBreak(endNode)) {
5408 // "Subtract one from end offset."
5409 endOffset--;
5410
5411 // "Subtract one from length."
5412 length--;
5413
5414 // "Call deleteData(end offset, 1) on end node."
5415 endNode.deleteData(endOffset, 1);
5416
5417 // "Otherwise, break from this loop."
5418 } else {
5419 break;
5420 }
5421 }
5422 }
5423
5424 // "Let replacement whitespace be the canonical space sequence of length
5425 // length. non-breaking start is true if start offset is zero and start
5426 // node follows a line break, and false otherwise. non-breaking end is true
5427 // if end offset is end node's length and end node precedes a line break,
5428 // and false otherwise."
5429 var replacementWhitespace = canonicalSpaceSequence(length,
5430 startOffset == 0 && followsLineBreak(startNode),
5431 endOffset == getNodeLength(endNode) && precedesLineBreak(endNode));
5432
5433 // "While (start node, start offset) is before (end node, end offset):"
5434 while (getPosition(startNode, startOffset, endNode, endOffset) == "before") {
5435 // "If start node has a child with index start offset, set start node
5436 // to that child, then set start offset to zero."
5437 if (startOffset < startNode.childNodes.length) {
5438 startNode = startNode.childNodes[startOffset];
5439 startOffset = 0;
5440
5441 // "Otherwise, if start node is not a Text node or if start offset is
5442 // start node's length, set start offset to one plus start node's
5443 // index, then set start node to its parent."
5444 } else if (startNode.nodeType != Node.TEXT_NODE
5445 || startOffset == getNodeLength(startNode)) {
5446 startOffset = 1 + getNodeIndex(startNode);
5447 startNode = startNode.parentNode;
5448
5449 // "Otherwise:"
5450 } else {
5451 // "Remove the first element from replacement whitespace, and let
5452 // element be that element."
5453 var element = replacementWhitespace[0];
5454 replacementWhitespace = replacementWhitespace.slice(1);
5455
5456 // "If element is not the same as the start offsetth element of
5457 // start node's data:"
5458 if (element != startNode.data[startOffset]) {
5459 // "Call insertData(start offset, element) on start node."
5460 startNode.insertData(startOffset, element);
5461
5462 // "Call deleteData(start offset + 1, 1) on start node."
5463 startNode.deleteData(startOffset + 1, 1);
5464 }
5465
5466 // "Add one to start offset."
5467 startOffset++;
5468 }
5469 }
5470 }
5471
5472
5473 //@}
5474 ///// Indenting and outdenting /////
5475 //@{
5476
5477 function indentNodes(nodeList) {
5478 // "If node list is empty, do nothing and abort these steps."
5479 if (!nodeList.length) {
5480 return;
5481 }
5482
5483 // "Let first node be the first member of node list."
5484 var firstNode = nodeList[0];
5485
5486 // "If first node's parent is an ol or ul:"
5487 if (isHtmlElement(firstNode.parentNode, ["OL", "UL"])) {
5488 // "Let tag be the local name of the parent of first node."
5489 var tag = firstNode.parentNode.tagName;
5490
5491 // "Wrap node list, with sibling criteria returning true for an HTML
5492 // element with local name tag and false otherwise, and new parent
5493 // instructions returning the result of calling createElement(tag) on
5494 // the ownerDocument of first node."
5495 wrap(nodeList,
5496 function(node) { return isHtmlElement(node, tag) },
5497 function() { return firstNode.ownerDocument.createElement(tag) });
5498
5499 // "Abort these steps."
5500 return;
5501 }
5502
5503 // "Wrap node list, with sibling criteria returning true for a simple
5504 // indentation element and false otherwise, and new parent instructions
5505 // returning the result of calling createElement("blockquote") on the
5506 // ownerDocument of first node. Let new parent be the result."
5507 var newParent = wrap(nodeList,
5508 function(node) { return isSimpleIndentationElement(node) },
5509 function() { return firstNode.ownerDocument.createElement("blockquote") });
5510
5511 // "Fix disallowed ancestors of new parent."
5512 fixDisallowedAncestors(newParent);
5513 }
5514
5515 function outdentNode(node) {
5516 // "If node is not editable, abort these steps."
5517 if (!isEditable(node)) {
5518 return;
5519 }
5520
5521 // "If node is a simple indentation element, remove node, preserving its
5522 // descendants. Then abort these steps."
5523 if (isSimpleIndentationElement(node)) {
5524 removePreservingDescendants(node);
5525 return;
5526 }
5527
5528 // "If node is an indentation element:"
5529 if (isIndentationElement(node)) {
5530 // "Unset the dir attribute of node, if any."
5531 node.removeAttribute("dir");
5532
5533 // "Unset the margin, padding, and border CSS properties of node."
5534 node.style.margin = "";
5535 node.style.padding = "";
5536 node.style.border = "";
5537 if (node.getAttribute("style") == ""
5538 // Crazy WebKit bug: https://bugs.webkit.org/show_bug.cgi?id=68551
5539 || node.getAttribute("style") == "border-width: initial; border-color: initial; ") {
5540 node.removeAttribute("style");
5541 }
5542
5543 // "Set the tag name of node to "div"."
5544 setTagName(node, "div");
5545
5546 // "Abort these steps."
5547 return;
5548 }
5549
5550 // "Let current ancestor be node's parent."
5551 var currentAncestor = node.parentNode;
5552
5553 // "Let ancestor list be a list of nodes, initially empty."
5554 var ancestorList = [];
5555
5556 // "While current ancestor is an editable Element that is neither a simple
5557 // indentation element nor an ol nor a ul, append current ancestor to
5558 // ancestor list and then set current ancestor to its parent."
5559 while (isEditable(currentAncestor)
5560 && currentAncestor.nodeType == Node.ELEMENT_NODE
5561 && !isSimpleIndentationElement(currentAncestor)
5562 && !isHtmlElement(currentAncestor, ["ol", "ul"])) {
5563 ancestorList.push(currentAncestor);
5564 currentAncestor = currentAncestor.parentNode;
5565 }
5566
5567 // "If current ancestor is not an editable simple indentation element:"
5568 if (!isEditable(currentAncestor)
5569 || !isSimpleIndentationElement(currentAncestor)) {
5570 // "Let current ancestor be node's parent."
5571 currentAncestor = node.parentNode;
5572
5573 // "Let ancestor list be the empty list."
5574 ancestorList = [];
5575
5576 // "While current ancestor is an editable Element that is neither an
5577 // indentation element nor an ol nor a ul, append current ancestor to
5578 // ancestor list and then set current ancestor to its parent."
5579 while (isEditable(currentAncestor)
5580 && currentAncestor.nodeType == Node.ELEMENT_NODE
5581 && !isIndentationElement(currentAncestor)
5582 && !isHtmlElement(currentAncestor, ["ol", "ul"])) {
5583 ancestorList.push(currentAncestor);
5584 currentAncestor = currentAncestor.parentNode;
5585 }
5586 }
5587
5588 // "If node is an ol or ul and current ancestor is not an editable
5589 // indentation element:"
5590 if (isHtmlElement(node, ["OL", "UL"])
5591 && (!isEditable(currentAncestor)
5592 || !isIndentationElement(currentAncestor))) {
5593 // "Unset the reversed, start, and type attributes of node, if any are
5594 // set."
5595 node.removeAttribute("reversed");
5596 node.removeAttribute("start");
5597 node.removeAttribute("type");
5598
5599 // "Let children be the children of node."
5600 var children = [].slice.call(node.childNodes);
5601
5602 // "If node has attributes, and its parent is not an ol or ul, set the
5603 // tag name of node to "div"."
5604 if (node.attributes.length
5605 && !isHtmlElement(node.parentNode, ["OL", "UL"])) {
5606 setTagName(node, "div");
5607
5608 // "Otherwise:"
5609 } else {
5610 // "Record the values of node's children, and let values be the
5611 // result."
5612 var values = recordValues([].slice.call(node.childNodes));
5613
5614 // "Remove node, preserving its descendants."
5615 removePreservingDescendants(node);
5616
5617 // "Restore the values from values."
5618 restoreValues(values);
5619 }
5620
5621 // "Fix disallowed ancestors of each member of children."
5622 for (var i = 0; i < children.length; i++) {
5623 fixDisallowedAncestors(children[i]);
5624 }
5625
5626 // "Abort these steps."
5627 return;
5628 }
5629
5630 // "If current ancestor is not an editable indentation element, abort these
5631 // steps."
5632 if (!isEditable(currentAncestor)
5633 || !isIndentationElement(currentAncestor)) {
5634 return;
5635 }
5636
5637 // "Append current ancestor to ancestor list."
5638 ancestorList.push(currentAncestor);
5639
5640 // "Let original ancestor be current ancestor."
5641 var originalAncestor = currentAncestor;
5642
5643 // "While ancestor list is not empty:"
5644 while (ancestorList.length) {
5645 // "Let current ancestor be the last member of ancestor list."
5646 //
5647 // "Remove the last member of ancestor list."
5648 currentAncestor = ancestorList.pop();
5649
5650 // "Let target be the child of current ancestor that is equal to either
5651 // node or the last member of ancestor list."
5652 var target = node.parentNode == currentAncestor
5653 ? node
5654 : ancestorList[ancestorList.length - 1];
5655
5656 // "If target is an inline node that is not a br, and its nextSibling
5657 // is a br, remove target's nextSibling from its parent."
5658 if (isInlineNode(target)
5659 && !isHtmlElement(target, "BR")
5660 && isHtmlElement(target.nextSibling, "BR")) {
5661 target.parentNode.removeChild(target.nextSibling);
5662 }
5663
5664 // "Let preceding siblings be the preceding siblings of target, and let
5665 // following siblings be the following siblings of target."
5666 var precedingSiblings = [].slice.call(currentAncestor.childNodes, 0, getNodeIndex(target));
5667 var followingSiblings = [].slice.call(currentAncestor.childNodes, 1 + getNodeIndex(target));
5668
5669 // "Indent preceding siblings."
5670 indentNodes(precedingSiblings);
5671
5672 // "Indent following siblings."
5673 indentNodes(followingSiblings);
5674 }
5675
5676 // "Outdent original ancestor."
5677 outdentNode(originalAncestor);
5678 }
5679
5680
5681 //@}
5682 ///// Toggling lists /////
5683 //@{
5684
5685 function toggleLists(tagName) {
5686 // "Let mode be "disable" if the selection's list state is tag name, and
5687 // "enable" otherwise."
5688 var mode = getSelectionListState() == tagName ? "disable" : "enable";
5689
5690 var range = getActiveRange();
5691 tagName = tagName.toUpperCase();
5692
5693 // "Let other tag name be "ol" if tag name is "ul", and "ul" if tag name is
5694 // "ol"."
5695 var otherTagName = tagName == "OL" ? "UL" : "OL";
5696
5697 // "Let items be a list of all lis that are ancestor containers of the
5698 // range's start and/or end node."
5699 //
5700 // It's annoying to get this in tree order using functional stuff without
5701 // doing getDescendants(document), which is slow, so I do it imperatively.
5702 var items = [];
5703 (function(){
5704 for (
5705 var ancestorContainer = range.endContainer;
5706 ancestorContainer != range.commonAncestorContainer;
5707 ancestorContainer = ancestorContainer.parentNode
5708 ) {
5709 if (isHtmlElement(ancestorContainer, "li")) {
5710 items.unshift(ancestorContainer);
5711 }
5712 }
5713 for (
5714 var ancestorContainer = range.startContainer;
5715 ancestorContainer;
5716 ancestorContainer = ancestorContainer.parentNode
5717 ) {
5718 if (isHtmlElement(ancestorContainer, "li")) {
5719 items.unshift(ancestorContainer);
5720 }
5721 }
5722 })();
5723
5724 // "For each item in items, normalize sublists of item."
5725 items.forEach(normalizeSublists);
5726
5727 // "Block-extend the range, and let new range be the result."
5728 var newRange = blockExtend(range);
5729
5730 // "If mode is "enable", then let lists to convert consist of every
5731 // editable HTML element with local name other tag name that is contained
5732 // in new range, and for every list in lists to convert:"
5733 if (mode == "enable") {
5734 getAllContainedNodes(newRange, function(node) {
5735 return isEditable(node)
5736 && isHtmlElement(node, otherTagName);
5737 }).forEach(function(list) {
5738 // "If list's previousSibling or nextSibling is an editable HTML
5739 // element with local name tag name:"
5740 if ((isEditable(list.previousSibling) && isHtmlElement(list.previousSibling, tagName))
5741 || (isEditable(list.nextSibling) && isHtmlElement(list.nextSibling, tagName))) {
5742 // "Let children be list's children."
5743 var children = [].slice.call(list.childNodes);
5744
5745 // "Record the values of children, and let values be the
5746 // result."
5747 var values = recordValues(children);
5748
5749 // "Split the parent of children."
5750 splitParent(children);
5751
5752 // "Wrap children, with sibling criteria returning true for an
5753 // HTML element with local name tag name and false otherwise."
5754 wrap(children, function(node) { return isHtmlElement(node, tagName) });
5755
5756 // "Restore the values from values."
5757 restoreValues(values);
5758
5759 // "Otherwise, set the tag name of list to tag name."
5760 } else {
5761 setTagName(list, tagName);
5762 }
5763 });
5764 }
5765
5766 // "Let node list be a list of nodes, initially empty."
5767 //
5768 // "For each node node contained in new range, if node is editable; the
5769 // last member of node list (if any) is not an ancestor of node; node
5770 // is not an indentation element; and either node is an ol or ul, or its
5771 // parent is an ol or ul, or it is an allowed child of "li"; then append
5772 // node to node list."
5773 var nodeList = getContainedNodes(newRange, function(node) {
5774 return isEditable(node)
5775 && !isIndentationElement(node)
5776 && (isHtmlElement(node, ["OL", "UL"])
5777 || isHtmlElement(node.parentNode, ["OL", "UL"])
5778 || isAllowedChild(node, "li"));
5779 });
5780
5781 // "If mode is "enable", remove from node list any ol or ul whose parent is
5782 // not also an ol or ul."
5783 if (mode == "enable") {
5784 nodeList = nodeList.filter(function(node) {
5785 return !isHtmlElement(node, ["ol", "ul"])
5786 || isHtmlElement(node.parentNode, ["ol", "ul"]);
5787 });
5788 }
5789
5790 // "If mode is "disable", then while node list is not empty:"
5791 if (mode == "disable") {
5792 while (nodeList.length) {
5793 // "Let sublist be an empty list of nodes."
5794 var sublist = [];
5795
5796 // "Remove the first member from node list and append it to
5797 // sublist."
5798 sublist.push(nodeList.shift());
5799
5800 // "If the first member of sublist is an HTML element with local
5801 // name tag name, outdent it and continue this loop from the
5802 // beginning."
5803 if (isHtmlElement(sublist[0], tagName)) {
5804 outdentNode(sublist[0]);
5805 continue;
5806 }
5807
5808 // "While node list is not empty, and the first member of node list
5809 // is the nextSibling of the last member of sublist and is not an
5810 // HTML element with local name tag name, remove the first member
5811 // from node list and append it to sublist."
5812 while (nodeList.length
5813 && nodeList[0] == sublist[sublist.length - 1].nextSibling
5814 && !isHtmlElement(nodeList[0], tagName)) {
5815 sublist.push(nodeList.shift());
5816 }
5817
5818 // "Record the values of sublist, and let values be the result."
5819 var values = recordValues(sublist);
5820
5821 // "Split the parent of sublist."
5822 splitParent(sublist);
5823
5824 // "Fix disallowed ancestors of each member of sublist."
5825 for (var i = 0; i < sublist.length; i++) {
5826 fixDisallowedAncestors(sublist[i]);
5827 }
5828
5829 // "Restore the values from values."
5830 restoreValues(values);
5831 }
5832
5833 // "Otherwise, while node list is not empty:"
5834 } else {
5835 while (nodeList.length) {
5836 // "Let sublist be an empty list of nodes."
5837 var sublist = [];
5838
5839 // "While either sublist is empty, or node list is not empty and
5840 // its first member is the nextSibling of sublist's last member:"
5841 while (!sublist.length
5842 || (nodeList.length
5843 && nodeList[0] == sublist[sublist.length - 1].nextSibling)) {
5844 // "If node list's first member is a p or div, set the tag name
5845 // of node list's first member to "li", and append the result
5846 // to sublist. Remove the first member from node list."
5847 if (isHtmlElement(nodeList[0], ["p", "div"])) {
5848 sublist.push(setTagName(nodeList[0], "li"));
5849 nodeList.shift();
5850
5851 // "Otherwise, if the first member of node list is an li or ol
5852 // or ul, remove it from node list and append it to sublist."
5853 } else if (isHtmlElement(nodeList[0], ["li", "ol", "ul"])) {
5854 sublist.push(nodeList.shift());
5855
5856 // "Otherwise:"
5857 } else {
5858 // "Let nodes to wrap be a list of nodes, initially empty."
5859 var nodesToWrap = [];
5860
5861 // "While nodes to wrap is empty, or node list is not empty
5862 // and its first member is the nextSibling of nodes to
5863 // wrap's last member and the first member of node list is
5864 // an inline node and the last member of nodes to wrap is
5865 // an inline node other than a br, remove the first member
5866 // from node list and append it to nodes to wrap."
5867 while (!nodesToWrap.length
5868 || (nodeList.length
5869 && nodeList[0] == nodesToWrap[nodesToWrap.length - 1].nextSibling
5870 && isInlineNode(nodeList[0])
5871 && isInlineNode(nodesToWrap[nodesToWrap.length - 1])
5872 && !isHtmlElement(nodesToWrap[nodesToWrap.length - 1], "br"))) {
5873 nodesToWrap.push(nodeList.shift());
5874 }
5875
5876 // "Wrap nodes to wrap, with new parent instructions
5877 // returning the result of calling createElement("li") on
5878 // the context object. Append the result to sublist."
5879 sublist.push(wrap(nodesToWrap,
5880 undefined,
5881 function() { return document.createElement("li") }));
5882 }
5883 }
5884
5885 // "If sublist's first member's parent is an HTML element with
5886 // local name tag name, or if every member of sublist is an ol or
5887 // ul, continue this loop from the beginning."
5888 if (isHtmlElement(sublist[0].parentNode, tagName)
5889 || sublist.every(function(node) { return isHtmlElement(node, ["ol", "ul"]) })) {
5890 continue;
5891 }
5892
5893 // "If sublist's first member's parent is an HTML element with
5894 // local name other tag name:"
5895 if (isHtmlElement(sublist[0].parentNode, otherTagName)) {
5896 // "Record the values of sublist, and let values be the
5897 // result."
5898 var values = recordValues(sublist);
5899
5900 // "Split the parent of sublist."
5901 splitParent(sublist);
5902
5903 // "Wrap sublist, with sibling criteria returning true for an
5904 // HTML element with local name tag name and false otherwise,
5905 // and new parent instructions returning the result of calling
5906 // createElement(tag name) on the context object."
5907 wrap(sublist,
5908 function(node) { return isHtmlElement(node, tagName) },
5909 function() { return document.createElement(tagName) });
5910
5911 // "Restore the values from values."
5912 restoreValues(values);
5913
5914 // "Continue this loop from the beginning."
5915 continue;
5916 }
5917
5918 // "Wrap sublist, with sibling criteria returning true for an HTML
5919 // element with local name tag name and false otherwise, and new
5920 // parent instructions being the following:"
5921 // . . .
5922 // "Fix disallowed ancestors of the previous step's result."
5923 fixDisallowedAncestors(wrap(sublist,
5924 function(node) { return isHtmlElement(node, tagName) },
5925 function() {
5926 // "If sublist's first member's parent is not an editable
5927 // simple indentation element, or sublist's first member's
5928 // parent's previousSibling is not an editable HTML element
5929 // with local name tag name, call createElement(tag name)
5930 // on the context object and return the result."
5931 if (!isEditable(sublist[0].parentNode)
5932 || !isSimpleIndentationElement(sublist[0].parentNode)
5933 || !isEditable(sublist[0].parentNode.previousSibling)
5934 || !isHtmlElement(sublist[0].parentNode.previousSibling, tagName)) {
5935 return document.createElement(tagName);
5936 }
5937
5938 // "Let list be sublist's first member's parent's
5939 // previousSibling."
5940 var list = sublist[0].parentNode.previousSibling;
5941
5942 // "Normalize sublists of list's lastChild."
5943 normalizeSublists(list.lastChild);
5944
5945 // "If list's lastChild is not an editable HTML element
5946 // with local name tag name, call createElement(tag name)
5947 // on the context object, and append the result as the last
5948 // child of list."
5949 if (!isEditable(list.lastChild)
5950 || !isHtmlElement(list.lastChild, tagName)) {
5951 list.appendChild(document.createElement(tagName));
5952 }
5953
5954 // "Return the last child of list."
5955 return list.lastChild;
5956 }
5957 ));
5958 }
5959 }
5960 }
5961
5962
5963 //@}
5964 ///// Justifying the selection /////
5965 //@{
5966
5967 function justifySelection(alignment) {
5968 // "Block-extend the active range, and let new range be the result."
5969 var newRange = blockExtend(globalRange);
5970
5971 // "Let element list be a list of all editable Elements contained in new
5972 // range that either has an attribute in the HTML namespace whose local
5973 // name is "align", or has a style attribute that sets "text-align", or is
5974 // a center."
5975 var elementList = getAllContainedNodes(newRange, function(node) {
5976 return node.nodeType == Node.ELEMENT_NODE
5977 && isEditable(node)
5978 // Ignoring namespaces here
5979 && (
5980 node.hasAttribute("align")
5981 || node.style.textAlign != ""
5982 || isHtmlElement(node, "center")
5983 );
5984 });
5985
5986 // "For each element in element list:"
5987 for (var i = 0; i < elementList.length; i++) {
5988 var element = elementList[i];
5989
5990 // "If element has an attribute in the HTML namespace whose local name
5991 // is "align", remove that attribute."
5992 element.removeAttribute("align");
5993
5994 // "Unset the CSS property "text-align" on element, if it's set by a
5995 // style attribute."
5996 element.style.textAlign = "";
5997 if (element.getAttribute("style") == "") {
5998 element.removeAttribute("style");
5999 }
6000
6001 // "If element is a div or span or center with no attributes, remove
6002 // it, preserving its descendants."
6003 if (isHtmlElement(element, ["div", "span", "center"])
6004 && !element.attributes.length) {
6005 removePreservingDescendants(element);
6006 }
6007
6008 // "If element is a center with one or more attributes, set the tag
6009 // name of element to "div"."
6010 if (isHtmlElement(element, "center")
6011 && element.attributes.length) {
6012 setTagName(element, "div");
6013 }
6014 }
6015
6016 // "Block-extend the active range, and let new range be the result."
6017 newRange = blockExtend(globalRange);
6018
6019 // "Let node list be a list of nodes, initially empty."
6020 var nodeList = [];
6021
6022 // "For each node node contained in new range, append node to node list if
6023 // the last member of node list (if any) is not an ancestor of node; node
6024 // is editable; node is an allowed child of "div"; and node's alignment
6025 // value is not alignment."
6026 nodeList = getContainedNodes(newRange, function(node) {
6027 return isEditable(node)
6028 && isAllowedChild(node, "div")
6029 && getAlignmentValue(node) != alignment;
6030 });
6031
6032 // "While node list is not empty:"
6033 while (nodeList.length) {
6034 // "Let sublist be a list of nodes, initially empty."
6035 var sublist = [];
6036
6037 // "Remove the first member of node list and append it to sublist."
6038 sublist.push(nodeList.shift());
6039
6040 // "While node list is not empty, and the first member of node list is
6041 // the nextSibling of the last member of sublist, remove the first
6042 // member of node list and append it to sublist."
6043 while (nodeList.length
6044 && nodeList[0] == sublist[sublist.length - 1].nextSibling) {
6045 sublist.push(nodeList.shift());
6046 }
6047
6048 // "Wrap sublist. Sibling criteria returns true for any div that has
6049 // one or both of the following two attributes and no other attributes,
6050 // and false otherwise:"
6051 //
6052 // * "An align attribute whose value is an ASCII case-insensitive
6053 // match for alignment.
6054 // * "A style attribute which sets exactly one CSS property
6055 // (including unrecognized or invalid attributes), which is
6056 // "text-align", which is set to alignment.
6057 //
6058 // "New parent instructions are to call createElement("div") on the
6059 // context object, then set its CSS property "text-align" to alignment
6060 // and return the result."
6061 wrap(sublist,
6062 function(node) {
6063 return isHtmlElement(node, "div")
6064 && [].every.call(node.attributes, function(attr) {
6065 return (attr.name == "align" && attr.value.toLowerCase() == alignment)
6066 || (attr.name == "style" && node.style.length == 1 && node.style.textAlign == alignment);
6067 });
6068 },
6069 function() {
6070 var newParent = document.createElement("div");
6071 newParent.setAttribute("style", "text-align: " + alignment);
6072 return newParent;
6073 }
6074 );
6075 }
6076 }
6077
6078
6079 //@}
6080 ///// Automatic linking /////
6081 //@{
6082 // "An autolinkable URL is a string of the following form:"
6083 var autolinkableUrlRegexp =
6084 // "Either a string matching the scheme pattern from RFC 3986 section 3.1
6085 // followed by the literal string ://, or the literal string mailto:;
6086 // followed by"
6087 //
6088 // From the RFC: scheme = ALPHA *( ALPHA / DIGIT / "+" / "-" / "." )
6089 "([a-zA-Z][a-zA-Z0-9+.-]*://|mailto:)"
6090 // "Zero or more characters other than space characters; followed by"
6091 + "[^ \t\n\f\r]*"
6092 // "A character that is not one of the ASCII characters !"'(),-.:;<>[]`{}."
6093 + "[^!\"'(),\\-.:;<>[\\]`{}]";
6094
6095 // "A valid e-mail address is a string that matches the ABNF production 1*(
6096 // atext / "." ) "@" ldh-str *( "." ldh-str ) where atext is defined in RFC
6097 // 5322 section 3.2.3, and ldh-str is defined in RFC 1034 section 3.5."
6098 //
6099 // atext: ALPHA / DIGIT / "!" / "#" / "$" / "%" / "&" / "'" / "*" / "+" / "-" /
6100 // "/" / "=" / "?" / "^" / "_" / "`" / "{" / "|" / "}" / "~"
6101 //
6102 //<ldh-str> ::= <let-dig-hyp> | <let-dig-hyp> <ldh-str>
6103 //<let-dig-hyp> ::= <let-dig> | "-"
6104 //<let-dig> ::= <letter> | <digit>
6105 var validEmailRegexp =
6106 "[a-zA-Z0-9!#$%&'*+\\-/=?^_`{|}~.]+@[a-zA-Z0-9-]+(\.[a-zA-Z0-9-]+)*";
6107
6108 function autolink(node, endOffset) {
6109 // "While (node, end offset)'s previous equivalent point is not null, set
6110 // it to its previous equivalent point."
6111 while (getPreviousEquivalentPoint(node, endOffset)) {
6112 var prev = getPreviousEquivalentPoint(node, endOffset);
6113 node = prev[0];
6114 endOffset = prev[1];
6115 }
6116
6117 // "If node is not a Text node, or has an a ancestor, do nothing and abort
6118 // these steps."
6119 if (node.nodeType != Node.TEXT_NODE
6120 || getAncestors(node).some(function(ancestor) { return isHtmlElement(ancestor, "a") })) {
6121 return;
6122 }
6123
6124 // "Let search be the largest substring of node's data whose end is end
6125 // offset and that contains no space characters."
6126 var search = /[^ \t\n\f\r]*$/.exec(node.substringData(0, endOffset))[0];
6127
6128 // "If some substring of search is an autolinkable URL:"
6129 if (new RegExp(autolinkableUrlRegexp).test(search)) {
6130 // "While there is no substring of node's data ending at end offset
6131 // that is an autolinkable URL, decrement end offset."
6132 while (!(new RegExp(autolinkableUrlRegexp + "$").test(node.substringData(0, endOffset)))) {
6133 endOffset--;
6134 }
6135
6136 // "Let start offset be the start index of the longest substring of
6137 // node's data that is an autolinkable URL ending at end offset."
6138 var startOffset = new RegExp(autolinkableUrlRegexp + "$").exec(node.substringData(0, endOffset)).index;
6139
6140 // "Let href be the substring of node's data starting at start offset
6141 // and ending at end offset."
6142 var href = node.substringData(startOffset, endOffset - startOffset);
6143
6144 // "Otherwise, if some substring of search is a valid e-mail address:"
6145 } else if (new RegExp(validEmailRegexp).test(search)) {
6146 // "While there is no substring of node's data ending at end offset
6147 // that is a valid e-mail address, decrement end offset."
6148 while (!(new RegExp(validEmailRegexp + "$").test(node.substringData(0, endOffset)))) {
6149 endOffset--;
6150 }
6151
6152 // "Let start offset be the start index of the longest substring of
6153 // node's data that is a valid e-mail address ending at end offset."
6154 var startOffset = new RegExp(validEmailRegexp + "$").exec(node.substringData(0, endOffset)).index;
6155
6156 // "Let href be "mailto:" concatenated with the substring of node's
6157 // data starting at start offset and ending at end offset."
6158 var href = "mailto:" + node.substringData(startOffset, endOffset - startOffset);
6159
6160 // "Otherwise, do nothing and abort these steps."
6161 } else {
6162 return;
6163 }
6164
6165 // "Let original range be the active range."
6166 var originalRange = getActiveRange();
6167
6168 // "Create a new range with start (node, start offset) and end (node, end
6169 // offset), and set the context object's selection's range to it."
6170 var newRange = document.createRange();
6171 newRange.setStart(node, startOffset);
6172 newRange.setEnd(node, endOffset);
6173 getSelection().removeAllRanges();
6174 getSelection().addRange(newRange);
6175 globalRange = newRange;
6176
6177 // "Take the action for "createLink", with value equal to href."
6178 commands.createlink.action(href);
6179
6180 // "Set the context object's selection's range to original range."
6181 getSelection().removeAllRanges();
6182 getSelection().addRange(originalRange);
6183 globalRange = originalRange;
6184 }
6185 //@}
6186 ///// The delete command /////
6187 //@{
6188 commands["delete"] = {
6189 preservesOverrides: true,
6190 action: function() {
6191 // "If the active range is not collapsed, delete the selection and
6192 // return true."
6193 if (!getActiveRange().collapsed) {
6194 deleteSelection();
6195 return true;
6196 }
6197
6198 // "Canonicalize whitespace at the active range's start."
6199 canonicalizeWhitespace(getActiveRange().startContainer, getActiveRange().startOffset);
6200
6201 // "Let node and offset be the active range's start node and offset."
6202 var node = getActiveRange().startContainer;
6203 var offset = getActiveRange().startOffset;
6204
6205 // "Repeat the following steps:"
6206 while (true) {
6207 // "If offset is zero and node's previousSibling is an editable
6208 // invisible node, remove node's previousSibling from its parent."
6209 if (offset == 0
6210 && isEditable(node.previousSibling)
6211 && isInvisible(node.previousSibling)) {
6212 node.parentNode.removeChild(node.previousSibling);
6213
6214 // "Otherwise, if node has a child with index offset − 1 and that
6215 // child is an editable invisible node, remove that child from
6216 // node, then subtract one from offset."
6217 } else if (0 <= offset - 1
6218 && offset - 1 < node.childNodes.length
6219 && isEditable(node.childNodes[offset - 1])
6220 && isInvisible(node.childNodes[offset - 1])) {
6221 node.removeChild(node.childNodes[offset - 1]);
6222 offset--;
6223
6224 // "Otherwise, if offset is zero and node is an inline node, or if
6225 // node is an invisible node, set offset to the index of node, then
6226 // set node to its parent."
6227 } else if ((offset == 0
6228 && isInlineNode(node))
6229 || isInvisible(node)) {
6230 offset = getNodeIndex(node);
6231 node = node.parentNode;
6232
6233 // "Otherwise, if node has a child with index offset − 1 and that
6234 // child is an editable a, remove that child from node, preserving
6235 // its descendants. Then return true."
6236 } else if (0 <= offset - 1
6237 && offset - 1 < node.childNodes.length
6238 && isEditable(node.childNodes[offset - 1])
6239 && isHtmlElement(node.childNodes[offset - 1], "a")) {
6240 removePreservingDescendants(node.childNodes[offset - 1]);
6241 return true;
6242
6243 // "Otherwise, if node has a child with index offset − 1 and that
6244 // child is not a block node or a br or an img, set node to that
6245 // child, then set offset to the length of node."
6246 } else if (0 <= offset - 1
6247 && offset - 1 < node.childNodes.length
6248 && !isBlockNode(node.childNodes[offset - 1])
6249 && !isHtmlElement(node.childNodes[offset - 1], ["br", "img"])) {
6250 node = node.childNodes[offset - 1];
6251 offset = getNodeLength(node);
6252
6253 // "Otherwise, break from this loop."
6254 } else {
6255 break;
6256 }
6257 }
6258
6259 // "If node is a Text node and offset is not zero, or if node is a
6260 // block node that has a child with index offset − 1 and that child is
6261 // a br or hr or img:"
6262 if ((node.nodeType == Node.TEXT_NODE
6263 && offset != 0)
6264 || (isBlockNode(node)
6265 && 0 <= offset - 1
6266 && offset - 1 < node.childNodes.length
6267 && isHtmlElement(node.childNodes[offset - 1], ["br", "hr", "img"]))) {
6268 // "Call collapse(node, offset) on the context object's Selection."
6269 getSelection().collapse(node, offset);
6270 getActiveRange().setEnd(node, offset);
6271
6272 // "Call extend(node, offset − 1) on the context object's
6273 // Selection."
6274 getSelection().extend(node, offset - 1);
6275 getActiveRange().setStart(node, offset - 1);
6276
6277 // "Delete the selection."
6278 deleteSelection();
6279
6280 // "Return true."
6281 return true;
6282 }
6283
6284 // "If node is an inline node, return true."
6285 if (isInlineNode(node)) {
6286 return true;
6287 }
6288
6289 // "If node is an li or dt or dd and is the first child of its parent,
6290 // and offset is zero:"
6291 if (isHtmlElement(node, ["li", "dt", "dd"])
6292 && node == node.parentNode.firstChild
6293 && offset == 0) {
6294 // "Let items be a list of all lis that are ancestors of node."
6295 //
6296 // Remember, must be in tree order.
6297 var items = [];
6298 for (var ancestor = node.parentNode; ancestor; ancestor = ancestor.parentNode) {
6299 if (isHtmlElement(ancestor, "li")) {
6300 items.unshift(ancestor);
6301 }
6302 }
6303
6304 // "Normalize sublists of each item in items."
6305 for (var i = 0; i < items.length; i++) {
6306 normalizeSublists(items[i]);
6307 }
6308
6309 // "Record the values of the one-node list consisting of node, and
6310 // let values be the result."
6311 var values = recordValues([node]);
6312
6313 // "Split the parent of the one-node list consisting of node."
6314 splitParent([node]);
6315
6316 // "Restore the values from values."
6317 restoreValues(values);
6318
6319 // "If node is a dd or dt, and it is not an allowed child of any of
6320 // its ancestors in the same editing host, set the tag name of node
6321 // to the default single-line container name and let node be the
6322 // result."
6323 if (isHtmlElement(node, ["dd", "dt"])
6324 && getAncestors(node).every(function(ancestor) {
6325 return !inSameEditingHost(node, ancestor)
6326 || !isAllowedChild(node, ancestor)
6327 })) {
6328 node = setTagName(node, defaultSingleLineContainerName);
6329 }
6330
6331 // "Fix disallowed ancestors of node."
6332 fixDisallowedAncestors(node);
6333
6334 // "Return true."
6335 return true;
6336 }
6337
6338 // "Let start node equal node and let start offset equal offset."
6339 var startNode = node;
6340 var startOffset = offset;
6341
6342 // "Repeat the following steps:"
6343 while (true) {
6344 // "If start offset is zero, set start offset to the index of start
6345 // node and then set start node to its parent."
6346 if (startOffset == 0) {
6347 startOffset = getNodeIndex(startNode);
6348 startNode = startNode.parentNode;
6349
6350 // "Otherwise, if start node has an editable invisible child with
6351 // index start offset minus one, remove it from start node and
6352 // subtract one from start offset."
6353 } else if (0 <= startOffset - 1
6354 && startOffset - 1 < startNode.childNodes.length
6355 && isEditable(startNode.childNodes[startOffset - 1])
6356 && isInvisible(startNode.childNodes[startOffset - 1])) {
6357 startNode.removeChild(startNode.childNodes[startOffset - 1]);
6358 startOffset--;
6359
6360 // "Otherwise, break from this loop."
6361 } else {
6362 break;
6363 }
6364 }
6365
6366 // "If offset is zero, and node has an editable ancestor container in
6367 // the same editing host that's an indentation element:"
6368 if (offset == 0
6369 && getAncestors(node).concat(node).filter(function(ancestor) {
6370 return isEditable(ancestor)
6371 && inSameEditingHost(ancestor, node)
6372 && isIndentationElement(ancestor);
6373 }).length) {
6374 // "Block-extend the range whose start and end are both (node, 0),
6375 // and let new range be the result."
6376 var newRange = document.createRange();
6377 newRange.setStart(node, 0);
6378 newRange = blockExtend(newRange);
6379
6380 // "Let node list be a list of nodes, initially empty."
6381 //
6382 // "For each node current node contained in new range, append
6383 // current node to node list if the last member of node list (if
6384 // any) is not an ancestor of current node, and current node is
6385 // editable but has no editable descendants."
6386 var nodeList = getContainedNodes(newRange, function(currentNode) {
6387 return isEditable(currentNode)
6388 && !hasEditableDescendants(currentNode);
6389 });
6390
6391 // "Outdent each node in node list."
6392 for (var i = 0; i < nodeList.length; i++) {
6393 outdentNode(nodeList[i]);
6394 }
6395
6396 // "Return true."
6397 return true;
6398 }
6399
6400 // "If the child of start node with index start offset is a table,
6401 // return true."
6402 if (isHtmlElement(startNode.childNodes[startOffset], "table")) {
6403 return true;
6404 }
6405
6406 // "If start node has a child with index start offset − 1, and that
6407 // child is a table:"
6408 if (0 <= startOffset - 1
6409 && startOffset - 1 < startNode.childNodes.length
6410 && isHtmlElement(startNode.childNodes[startOffset - 1], "table")) {
6411 // "Call collapse(start node, start offset − 1) on the context
6412 // object's Selection."
6413 getSelection().collapse(startNode, startOffset - 1);
6414 getActiveRange().setStart(startNode, startOffset - 1);
6415
6416 // "Call extend(start node, start offset) on the context object's
6417 // Selection."
6418 getSelection().extend(startNode, startOffset);
6419 getActiveRange().setEnd(startNode, startOffset);
6420
6421 // "Return true."
6422 return true;
6423 }
6424
6425 // "If offset is zero; and either the child of start node with index
6426 // start offset minus one is an hr, or the child is a br whose
6427 // previousSibling is either a br or not an inline node:"
6428 if (offset == 0
6429 && (isHtmlElement(startNode.childNodes[startOffset - 1], "hr")
6430 || (
6431 isHtmlElement(startNode.childNodes[startOffset - 1], "br")
6432 && (
6433 isHtmlElement(startNode.childNodes[startOffset - 1].previousSibling, "br")
6434 || !isInlineNode(startNode.childNodes[startOffset - 1].previousSibling)
6435 )
6436 )
6437 )) {
6438 // "Call collapse(start node, start offset − 1) on the context
6439 // object's Selection."
6440 getSelection().collapse(startNode, startOffset - 1);
6441 getActiveRange().setStart(startNode, startOffset - 1);
6442
6443 // "Call extend(start node, start offset) on the context object's
6444 // Selection."
6445 getSelection().extend(startNode, startOffset);
6446 getActiveRange().setEnd(startNode, startOffset);
6447
6448 // "Delete the selection."
6449 deleteSelection();
6450
6451 // "Call collapse(node, offset) on the Selection."
6452 getSelection().collapse(node, offset);
6453 getActiveRange().setStart(node, offset);
6454 getActiveRange().collapse(true);
6455
6456 // "Return true."
6457 return true;
6458 }
6459
6460 // "If the child of start node with index start offset is an li or dt
6461 // or dd, and that child's firstChild is an inline node, and start
6462 // offset is not zero:"
6463 if (isHtmlElement(startNode.childNodes[startOffset], ["li", "dt", "dd"])
6464 && isInlineNode(startNode.childNodes[startOffset].firstChild)
6465 && startOffset != 0) {
6466 // "Let previous item be the child of start node with index start
6467 // offset minus one."
6468 var previousItem = startNode.childNodes[startOffset - 1];
6469
6470 // "If previous item's lastChild is an inline node other than a br,
6471 // call createElement("br") on the context object and append the
6472 // result as the last child of previous item."
6473 if (isInlineNode(previousItem.lastChild)
6474 && !isHtmlElement(previousItem.lastChild, "br")) {
6475 previousItem.appendChild(document.createElement("br"));
6476 }
6477
6478 // "If previous item's lastChild is an inline node, call
6479 // createElement("br") on the context object and append the result
6480 // as the last child of previous item."
6481 if (isInlineNode(previousItem.lastChild)) {
6482 previousItem.appendChild(document.createElement("br"));
6483 }
6484 }
6485
6486 // "If start node's child with index start offset is an li or dt or dd,
6487 // and that child's previousSibling is also an li or dt or dd:"
6488 if (isHtmlElement(startNode.childNodes[startOffset], ["li", "dt", "dd"])
6489 && isHtmlElement(startNode.childNodes[startOffset].previousSibling, ["li", "dt", "dd"])) {
6490 // "Call cloneRange() on the active range, and let original range
6491 // be the result."
6492 //
6493 // We need to add it to extraRanges so it will actually get updated
6494 // when moving preserving ranges.
6495 var originalRange = getActiveRange().cloneRange();
6496 extraRanges.push(originalRange);
6497
6498 // "Set start node to its child with index start offset − 1."
6499 startNode = startNode.childNodes[startOffset - 1];
6500
6501 // "Set start offset to start node's length."
6502 startOffset = getNodeLength(startNode);
6503
6504 // "Set node to start node's nextSibling."
6505 node = startNode.nextSibling;
6506
6507 // "Call collapse(start node, start offset) on the context object's
6508 // Selection."
6509 getSelection().collapse(startNode, startOffset);
6510 getActiveRange().setStart(startNode, startOffset);
6511
6512 // "Call extend(node, 0) on the context object's Selection."
6513 getSelection().extend(node, 0);
6514 getActiveRange().setEnd(node, 0);
6515
6516 // "Delete the selection."
6517 deleteSelection();
6518
6519 // "Call removeAllRanges() on the context object's Selection."
6520 getSelection().removeAllRanges();
6521
6522 // "Call addRange(original range) on the context object's
6523 // Selection."
6524 getSelection().addRange(originalRange);
6525 getActiveRange().setStart(originalRange.startContainer, originalRange.startOffset);
6526 getActiveRange().setEnd(originalRange.endContainer, originalRange.endOffset);
6527
6528 // "Return true."
6529 extraRanges.pop();
6530 return true;
6531 }
6532
6533 // "While start node has a child with index start offset minus one:"
6534 while (0 <= startOffset - 1
6535 && startOffset - 1 < startNode.childNodes.length) {
6536 // "If start node's child with index start offset minus one is
6537 // editable and invisible, remove it from start node, then subtract
6538 // one from start offset."
6539 if (isEditable(startNode.childNodes[startOffset - 1])
6540 && isInvisible(startNode.childNodes[startOffset - 1])) {
6541 startNode.removeChild(startNode.childNodes[startOffset - 1]);
6542 startOffset--;
6543
6544 // "Otherwise, set start node to its child with index start offset
6545 // minus one, then set start offset to the length of start node."
6546 } else {
6547 startNode = startNode.childNodes[startOffset - 1];
6548 startOffset = getNodeLength(startNode);
6549 }
6550 }
6551
6552 // "Call collapse(start node, start offset) on the context object's
6553 // Selection."
6554 getSelection().collapse(startNode, startOffset);
6555 getActiveRange().setStart(startNode, startOffset);
6556
6557 // "Call extend(node, offset) on the context object's Selection."
6558 getSelection().extend(node, offset);
6559 getActiveRange().setEnd(node, offset);
6560
6561 // "Delete the selection, with direction "backward"."
6562 deleteSelection({direction: "backward"});
6563
6564 // "Return true."
6565 return true;
6566 }
6567 };
6568
6569 //@}
6570 ///// The formatBlock command /////
6571 //@{
6572 // "A formattable block name is "address", "dd", "div", "dt", "h1", "h2", "h3",
6573 // "h4", "h5", "h6", "p", or "pre"."
6574 var formattableBlockNames = ["address", "dd", "div", "dt", "h1", "h2", "h3",
6575 "h4", "h5", "h6", "p", "pre"];
6576
6577 commands.formatblock = {
6578 preservesOverrides: true,
6579 action: function(value) {
6580 // "If value begins with a "<" character and ends with a ">" character,
6581 // remove the first and last characters from it."
6582 if (/^<.*>$/.test(value)) {
6583 value = value.slice(1, -1);
6584 }
6585
6586 // "Let value be converted to ASCII lowercase."
6587 value = value.toLowerCase();
6588
6589 // "If value is not a formattable block name, return false."
6590 if (formattableBlockNames.indexOf(value) == -1) {
6591 return false;
6592 }
6593
6594 // "Block-extend the active range, and let new range be the result."
6595 var newRange = blockExtend(getActiveRange());
6596
6597 // "Let node list be an empty list of nodes."
6598 //
6599 // "For each node node contained in new range, append node to node list
6600 // if it is editable, the last member of original node list (if any) is
6601 // not an ancestor of node, node is either a non-list single-line
6602 // container or an allowed child of "p" or a dd or dt, and node is not
6603 // the ancestor of a prohibited paragraph child."
6604 var nodeList = getContainedNodes(newRange, function(node) {
6605 return isEditable(node)
6606 && (isNonListSingleLineContainer(node)
6607 || isAllowedChild(node, "p")
6608 || isHtmlElement(node, ["dd", "dt"]))
6609 && !getDescendants(node).some(isProhibitedParagraphChild);
6610 });
6611
6612 // "Record the values of node list, and let values be the result."
6613 var values = recordValues(nodeList);
6614
6615 // "For each node in node list, while node is the descendant of an
6616 // editable HTML element in the same editing host, whose local name is
6617 // a formattable block name, and which is not the ancestor of a
6618 // prohibited paragraph child, split the parent of the one-node list
6619 // consisting of node."
6620 for (var i = 0; i < nodeList.length; i++) {
6621 var node = nodeList[i];
6622 while (getAncestors(node).some(function(ancestor) {
6623 return isEditable(ancestor)
6624 && inSameEditingHost(ancestor, node)
6625 && isHtmlElement(ancestor, formattableBlockNames)
6626 && !getDescendants(ancestor).some(isProhibitedParagraphChild);
6627 })) {
6628 splitParent([node]);
6629 }
6630 }
6631
6632 // "Restore the values from values."
6633 restoreValues(values);
6634
6635 // "While node list is not empty:"
6636 while (nodeList.length) {
6637 var sublist;
6638
6639 // "If the first member of node list is a single-line
6640 // container:"
6641 if (isSingleLineContainer(nodeList[0])) {
6642 // "Let sublist be the children of the first member of node
6643 // list."
6644 sublist = [].slice.call(nodeList[0].childNodes);
6645
6646 // "Record the values of sublist, and let values be the
6647 // result."
6648 var values = recordValues(sublist);
6649
6650 // "Remove the first member of node list from its parent,
6651 // preserving its descendants."
6652 removePreservingDescendants(nodeList[0]);
6653
6654 // "Restore the values from values."
6655 restoreValues(values);
6656
6657 // "Remove the first member from node list."
6658 nodeList.shift();
6659
6660 // "Otherwise:"
6661 } else {
6662 // "Let sublist be an empty list of nodes."
6663 sublist = [];
6664
6665 // "Remove the first member of node list and append it to
6666 // sublist."
6667 sublist.push(nodeList.shift());
6668
6669 // "While node list is not empty, and the first member of
6670 // node list is the nextSibling of the last member of
6671 // sublist, and the first member of node list is not a
6672 // single-line container, and the last member of sublist is
6673 // not a br, remove the first member of node list and
6674 // append it to sublist."
6675 while (nodeList.length
6676 && nodeList[0] == sublist[sublist.length - 1].nextSibling
6677 && !isSingleLineContainer(nodeList[0])
6678 && !isHtmlElement(sublist[sublist.length - 1], "BR")) {
6679 sublist.push(nodeList.shift());
6680 }
6681 }
6682
6683 // "Wrap sublist. If value is "div" or "p", sibling criteria
6684 // returns false; otherwise it returns true for an HTML element
6685 // with local name value and no attributes, and false otherwise.
6686 // New parent instructions return the result of running
6687 // createElement(value) on the context object. Then fix disallowed
6688 // ancestors of the result."
6689 fixDisallowedAncestors(wrap(sublist,
6690 ["div", "p"].indexOf(value) == - 1
6691 ? function(node) { return isHtmlElement(node, value) && !node.attributes.length }
6692 : function() { return false },
6693 function() { return document.createElement(value) }));
6694 }
6695
6696 // "Return true."
6697 return true;
6698 }, indeterm: function() {
6699 // "If the active range is null, return false."
6700 if (!getActiveRange()) {
6701 return false;
6702 }
6703
6704 // "Block-extend the active range, and let new range be the result."
6705 var newRange = blockExtend(getActiveRange());
6706
6707 // "Let node list be all visible editable nodes that are contained in
6708 // new range and have no children."
6709 var nodeList = getAllContainedNodes(newRange, function(node) {
6710 return isVisible(node)
6711 && isEditable(node)
6712 && !node.hasChildNodes();
6713 });
6714
6715 // "If node list is empty, return false."
6716 if (!nodeList.length) {
6717 return false;
6718 }
6719
6720 // "Let type be null."
6721 var type = null;
6722
6723 // "For each node in node list:"
6724 for (var i = 0; i < nodeList.length; i++) {
6725 var node = nodeList[i];
6726
6727 // "While node's parent is editable and in the same editing host as
6728 // node, and node is not an HTML element whose local name is a
6729 // formattable block name, set node to its parent."
6730 while (isEditable(node.parentNode)
6731 && inSameEditingHost(node, node.parentNode)
6732 && !isHtmlElement(node, formattableBlockNames)) {
6733 node = node.parentNode;
6734 }
6735
6736 // "Let current type be the empty string."
6737 var currentType = "";
6738
6739 // "If node is an editable HTML element whose local name is a
6740 // formattable block name, and node is not the ancestor of a
6741 // prohibited paragraph child, set current type to node's local
6742 // name."
6743 if (isEditable(node)
6744 && isHtmlElement(node, formattableBlockNames)
6745 && !getDescendants(node).some(isProhibitedParagraphChild)) {
6746 currentType = node.tagName;
6747 }
6748
6749 // "If type is null, set type to current type."
6750 if (type === null) {
6751 type = currentType;
6752
6753 // "Otherwise, if type does not equal current type, return true."
6754 } else if (type != currentType) {
6755 return true;
6756 }
6757 }
6758
6759 // "Return false."
6760 return false;
6761 }, value: function() {
6762 // "If the active range is null, return the empty string."
6763 if (!getActiveRange()) {
6764 return "";
6765 }
6766
6767 // "Block-extend the active range, and let new range be the result."
6768 var newRange = blockExtend(getActiveRange());
6769
6770 // "Let node be the first visible editable node that is contained in
6771 // new range and has no children. If there is no such node, return the
6772 // empty string."
6773 var nodes = getAllContainedNodes(newRange, function(node) {
6774 return isVisible(node)
6775 && isEditable(node)
6776 && !node.hasChildNodes();
6777 });
6778 if (!nodes.length) {
6779 return "";
6780 }
6781 var node = nodes[0];
6782
6783 // "While node's parent is editable and in the same editing host as
6784 // node, and node is not an HTML element whose local name is a
6785 // formattable block name, set node to its parent."
6786 while (isEditable(node.parentNode)
6787 && inSameEditingHost(node, node.parentNode)
6788 && !isHtmlElement(node, formattableBlockNames)) {
6789 node = node.parentNode;
6790 }
6791
6792 // "If node is an editable HTML element whose local name is a
6793 // formattable block name, and node is not the ancestor of a prohibited
6794 // paragraph child, return node's local name, converted to ASCII
6795 // lowercase."
6796 if (isEditable(node)
6797 && isHtmlElement(node, formattableBlockNames)
6798 && !getDescendants(node).some(isProhibitedParagraphChild)) {
6799 return node.tagName.toLowerCase();
6800 }
6801
6802 // "Return the empty string."
6803 return "";
6804 }
6805 };
6806
6807 //@}
6808 ///// The forwardDelete command /////
6809 //@{
6810 commands.forwarddelete = {
6811 preservesOverrides: true,
6812 action: function() {
6813 // "If the active range is not collapsed, delete the selection and
6814 // return true."
6815 if (!getActiveRange().collapsed) {
6816 deleteSelection();
6817 return true;
6818 }
6819
6820 // "Canonicalize whitespace at the active range's start."
6821 canonicalizeWhitespace(getActiveRange().startContainer, getActiveRange().startOffset);
6822
6823 // "Let node and offset be the active range's start node and offset."
6824 var node = getActiveRange().startContainer;
6825 var offset = getActiveRange().startOffset;
6826
6827 // "Repeat the following steps:"
6828 while (true) {
6829 // "If offset is the length of node and node's nextSibling is an
6830 // editable invisible node, remove node's nextSibling from its
6831 // parent."
6832 if (offset == getNodeLength(node)
6833 && isEditable(node.nextSibling)
6834 && isInvisible(node.nextSibling)) {
6835 node.parentNode.removeChild(node.nextSibling);
6836
6837 // "Otherwise, if node has a child with index offset and that child
6838 // is an editable invisible node, remove that child from node."
6839 } else if (offset < node.childNodes.length
6840 && isEditable(node.childNodes[offset])
6841 && isInvisible(node.childNodes[offset])) {
6842 node.removeChild(node.childNodes[offset]);
6843
6844 // "Otherwise, if offset is the length of node and node is an
6845 // inline node, or if node is invisible, set offset to one plus the
6846 // index of node, then set node to its parent."
6847 } else if ((offset == getNodeLength(node)
6848 && isInlineNode(node))
6849 || isInvisible(node)) {
6850 offset = 1 + getNodeIndex(node);
6851 node = node.parentNode;
6852
6853 // "Otherwise, if node has a child with index offset and that child
6854 // is neither a block node nor a br nor an img nor a collapsed
6855 // block prop, set node to that child, then set offset to zero."
6856 } else if (offset < node.childNodes.length
6857 && !isBlockNode(node.childNodes[offset])
6858 && !isHtmlElement(node.childNodes[offset], ["br", "img"])
6859 && !isCollapsedBlockProp(node.childNodes[offset])) {
6860 node = node.childNodes[offset];
6861 offset = 0;
6862
6863 // "Otherwise, break from this loop."
6864 } else {
6865 break;
6866 }
6867 }
6868
6869 // "If node is a Text node and offset is not node's length:"
6870 if (node.nodeType == Node.TEXT_NODE
6871 && offset != getNodeLength(node)) {
6872 // "Let end offset be offset plus one."
6873 var endOffset = offset + 1;
6874
6875 // "While end offset is not node's length and the end offsetth
6876 // element of node's data has general category M when interpreted
6877 // as a Unicode code point, add one to end offset."
6878 //
6879 // TODO: Not even going to try handling anything beyond the most
6880 // basic combining marks, since I couldn't find a good list. I
6881 // special-case a few Hebrew diacritics too to test basic coverage
6882 // of non-Latin stuff.
6883 while (endOffset != node.length
6884 && /^[\u0300-\u036f\u0591-\u05bd\u05c1\u05c2]$/.test(node.data[endOffset])) {
6885 endOffset++;
6886 }
6887
6888 // "Call collapse(node, offset) on the context object's Selection."
6889 getSelection().collapse(node, offset);
6890 getActiveRange().setStart(node, offset);
6891
6892 // "Call extend(node, end offset) on the context object's
6893 // Selection."
6894 getSelection().extend(node, endOffset);
6895 getActiveRange().setEnd(node, endOffset);
6896
6897 // "Delete the selection."
6898 deleteSelection();
6899
6900 // "Return true."
6901 return true;
6902 }
6903
6904 // "If node is an inline node, return true."
6905 if (isInlineNode(node)) {
6906 return true;
6907 }
6908
6909 // "If node has a child with index offset and that child is a br or hr
6910 // or img, but is not a collapsed block prop:"
6911 if (offset < node.childNodes.length
6912 && isHtmlElement(node.childNodes[offset], ["br", "hr", "img"])
6913 && !isCollapsedBlockProp(node.childNodes[offset])) {
6914 // "Call collapse(node, offset) on the context object's Selection."
6915 getSelection().collapse(node, offset);
6916 getActiveRange().setStart(node, offset);
6917
6918 // "Call extend(node, offset + 1) on the context object's
6919 // Selection."
6920 getSelection().extend(node, offset + 1);
6921 getActiveRange().setEnd(node, offset + 1);
6922
6923 // "Delete the selection."
6924 deleteSelection();
6925
6926 // "Return true."
6927 return true;
6928 }
6929
6930 // "Let end node equal node and let end offset equal offset."
6931 var endNode = node;
6932 var endOffset = offset;
6933
6934 // "If end node has a child with index end offset, and that child is a
6935 // collapsed block prop, add one to end offset."
6936 if (endOffset < endNode.childNodes.length
6937 && isCollapsedBlockProp(endNode.childNodes[endOffset])) {
6938 endOffset++;
6939 }
6940
6941 // "Repeat the following steps:"
6942 while (true) {
6943 // "If end offset is the length of end node, set end offset to one
6944 // plus the index of end node and then set end node to its parent."
6945 if (endOffset == getNodeLength(endNode)) {
6946 endOffset = 1 + getNodeIndex(endNode);
6947 endNode = endNode.parentNode;
6948
6949 // "Otherwise, if end node has a an editable invisible child with
6950 // index end offset, remove it from end node."
6951 } else if (endOffset < endNode.childNodes.length
6952 && isEditable(endNode.childNodes[endOffset])
6953 && isInvisible(endNode.childNodes[endOffset])) {
6954 endNode.removeChild(endNode.childNodes[endOffset]);
6955
6956 // "Otherwise, break from this loop."
6957 } else {
6958 break;
6959 }
6960 }
6961
6962 // "If the child of end node with index end offset minus one is a
6963 // table, return true."
6964 if (isHtmlElement(endNode.childNodes[endOffset - 1], "table")) {
6965 return true;
6966 }
6967
6968 // "If the child of end node with index end offset is a table:"
6969 if (isHtmlElement(endNode.childNodes[endOffset], "table")) {
6970 // "Call collapse(end node, end offset) on the context object's
6971 // Selection."
6972 getSelection().collapse(endNode, endOffset);
6973 getActiveRange().setStart(endNode, endOffset);
6974
6975 // "Call extend(end node, end offset + 1) on the context object's
6976 // Selection."
6977 getSelection().extend(endNode, endOffset + 1);
6978 getActiveRange().setEnd(endNode, endOffset + 1);
6979
6980 // "Return true."
6981 return true;
6982 }
6983
6984 // "If offset is the length of node, and the child of end node with
6985 // index end offset is an hr or br:"
6986 if (offset == getNodeLength(node)
6987 && isHtmlElement(endNode.childNodes[endOffset], ["br", "hr"])) {
6988 // "Call collapse(end node, end offset) on the context object's
6989 // Selection."
6990 getSelection().collapse(endNode, endOffset);
6991 getActiveRange().setStart(endNode, endOffset);
6992
6993 // "Call extend(end node, end offset + 1) on the context object's
6994 // Selection."
6995 getSelection().extend(endNode, endOffset + 1);
6996 getActiveRange().setEnd(endNode, endOffset + 1);
6997
6998 // "Delete the selection."
6999 deleteSelection();
7000
7001 // "Call collapse(node, offset) on the Selection."
7002 getSelection().collapse(node, offset);
7003 getActiveRange().setStart(node, offset);
7004 getActiveRange().collapse(true);
7005
7006 // "Return true."
7007 return true;
7008 }
7009
7010 // "While end node has a child with index end offset:"
7011 while (endOffset < endNode.childNodes.length) {
7012 // "If end node's child with index end offset is editable and
7013 // invisible, remove it from end node."
7014 if (isEditable(endNode.childNodes[endOffset])
7015 && isInvisible(endNode.childNodes[endOffset])) {
7016 endNode.removeChild(endNode.childNodes[endOffset]);
7017
7018 // "Otherwise, set end node to its child with index end offset and
7019 // set end offset to zero."
7020 } else {
7021 endNode = endNode.childNodes[endOffset];
7022 endOffset = 0;
7023 }
7024 }
7025
7026 // "Call collapse(node, offset) on the context object's Selection."
7027 getSelection().collapse(node, offset);
7028 getActiveRange().setStart(node, offset);
7029
7030 // "Call extend(end node, end offset) on the context object's
7031 // Selection."
7032 getSelection().extend(endNode, endOffset);
7033 getActiveRange().setEnd(endNode, endOffset);
7034
7035 // "Delete the selection."
7036 deleteSelection();
7037
7038 // "Return true."
7039 return true;
7040 }
7041 };
7042
7043 //@}
7044 ///// The indent command /////
7045 //@{
7046 commands.indent = {
7047 preservesOverrides: true,
7048 action: function() {
7049 // "Let items be a list of all lis that are ancestor containers of the
7050 // active range's start and/or end node."
7051 //
7052 // Has to be in tree order, remember!
7053 var items = [];
7054 for (var node = getActiveRange().endContainer; node != getActiveRange().commonAncestorContainer; node = node.parentNode) {
7055 if (isHtmlElement(node, "LI")) {
7056 items.unshift(node);
7057 }
7058 }
7059 for (var node = getActiveRange().startContainer; node != getActiveRange().commonAncestorContainer; node = node.parentNode) {
7060 if (isHtmlElement(node, "LI")) {
7061 items.unshift(node);
7062 }
7063 }
7064 for (var node = getActiveRange().commonAncestorContainer; node; node = node.parentNode) {
7065 if (isHtmlElement(node, "LI")) {
7066 items.unshift(node);
7067 }
7068 }
7069
7070 // "For each item in items, normalize sublists of item."
7071 for (var i = 0; i < items.length; i++) {
7072 normalizeSublists(items[i]);
7073 }
7074
7075 // "Block-extend the active range, and let new range be the result."
7076 var newRange = blockExtend(getActiveRange());
7077
7078 // "Let node list be a list of nodes, initially empty."
7079 var nodeList = [];
7080
7081 // "For each node node contained in new range, if node is editable and
7082 // is an allowed child of "div" or "ol" and if the last member of node
7083 // list (if any) is not an ancestor of node, append node to node list."
7084 nodeList = getContainedNodes(newRange, function(node) {
7085 return isEditable(node)
7086 && (isAllowedChild(node, "div")
7087 || isAllowedChild(node, "ol"));
7088 });
7089
7090 // "If the first visible member of node list is an li whose parent is
7091 // an ol or ul:"
7092 if (isHtmlElement(nodeList.filter(isVisible)[0], "li")
7093 && isHtmlElement(nodeList.filter(isVisible)[0].parentNode, ["ol", "ul"])) {
7094 // "Let sibling be node list's first visible member's
7095 // previousSibling."
7096 var sibling = nodeList.filter(isVisible)[0].previousSibling;
7097
7098 // "While sibling is invisible, set sibling to its
7099 // previousSibling."
7100 while (isInvisible(sibling)) {
7101 sibling = sibling.previousSibling;
7102 }
7103
7104 // "If sibling is an li, normalize sublists of sibling."
7105 if (isHtmlElement(sibling, "li")) {
7106 normalizeSublists(sibling);
7107 }
7108 }
7109
7110 // "While node list is not empty:"
7111 while (nodeList.length) {
7112 // "Let sublist be a list of nodes, initially empty."
7113 var sublist = [];
7114
7115 // "Remove the first member of node list and append it to sublist."
7116 sublist.push(nodeList.shift());
7117
7118 // "While the first member of node list is the nextSibling of the
7119 // last member of sublist, remove the first member of node list and
7120 // append it to sublist."
7121 while (nodeList.length
7122 && nodeList[0] == sublist[sublist.length - 1].nextSibling) {
7123 sublist.push(nodeList.shift());
7124 }
7125
7126 // "Indent sublist."
7127 indentNodes(sublist);
7128 }
7129
7130 // "Return true."
7131 return true;
7132 }
7133 };
7134
7135 //@}
7136 ///// The insertHorizontalRule command /////
7137 //@{
7138 commands.inserthorizontalrule = {
7139 preservesOverrides: true,
7140 action: function() {
7141 // "Let start node, start offset, end node, and end offset be the
7142 // active range's start and end nodes and offsets."
7143 var startNode = getActiveRange().startContainer;
7144 var startOffset = getActiveRange().startOffset;
7145 var endNode = getActiveRange().endContainer;
7146 var endOffset = getActiveRange().endOffset;
7147
7148 // "While start offset is 0 and start node's parent is not null, set
7149 // start offset to start node's index, then set start node to its
7150 // parent."
7151 while (startOffset == 0
7152 && startNode.parentNode) {
7153 startOffset = getNodeIndex(startNode);
7154 startNode = startNode.parentNode;
7155 }
7156
7157 // "While end offset is end node's length, and end node's parent is not
7158 // null, set end offset to one plus end node's index, then set end node
7159 // to its parent."
7160 while (endOffset == getNodeLength(endNode)
7161 && endNode.parentNode) {
7162 endOffset = 1 + getNodeIndex(endNode);
7163 endNode = endNode.parentNode;
7164 }
7165
7166 // "Call collapse(start node, start offset) on the context object's
7167 // Selection."
7168 getSelection().collapse(startNode, startOffset);
7169 getActiveRange().setStart(startNode, startOffset);
7170
7171 // "Call extend(end node, end offset) on the context object's
7172 // Selection."
7173 getSelection().extend(endNode, endOffset);
7174 getActiveRange().setEnd(endNode, endOffset);
7175
7176 // "Delete the selection, with block merging false."
7177 deleteSelection({blockMerging: false});
7178
7179 // "If the active range's start node is neither editable nor an editing
7180 // host, return true."
7181 if (!isEditable(getActiveRange().startContainer)
7182 && !isEditingHost(getActiveRange().startContainer)) {
7183 return true;
7184 }
7185
7186 // "If the active range's start node is a Text node and its start
7187 // offset is zero, call collapse() on the context object's Selection,
7188 // with first argument the active range's start node's parent and
7189 // second argument the active range's start node's index."
7190 if (getActiveRange().startContainer.nodeType == Node.TEXT_NODE
7191 && getActiveRange().startOffset == 0) {
7192 var newNode = getActiveRange().startContainer.parentNode;
7193 var newOffset = getNodeIndex(getActiveRange().startContainer);
7194 getSelection().collapse(newNode, newOffset);
7195 getActiveRange().setStart(newNode, newOffset);
7196 getActiveRange().collapse(true);
7197 }
7198
7199 // "If the active range's start node is a Text node and its start
7200 // offset is the length of its start node, call collapse() on the
7201 // context object's Selection, with first argument the active range's
7202 // start node's parent, and the second argument one plus the active
7203 // range's start node's index."
7204 if (getActiveRange().startContainer.nodeType == Node.TEXT_NODE
7205 && getActiveRange().startOffset == getNodeLength(getActiveRange().startContainer)) {
7206 var newNode = getActiveRange().startContainer.parentNode;
7207 var newOffset = 1 + getNodeIndex(getActiveRange().startContainer);
7208 getSelection().collapse(newNode, newOffset);
7209 getActiveRange().setStart(newNode, newOffset);
7210 getActiveRange().collapse(true);
7211 }
7212
7213 // "Let hr be the result of calling createElement("hr") on the
7214 // context object."
7215 var hr = document.createElement("hr");
7216
7217 // "Run insertNode(hr) on the active range."
7218 getActiveRange().insertNode(hr);
7219
7220 // "Fix disallowed ancestors of hr."
7221 fixDisallowedAncestors(hr);
7222
7223 // "Run collapse() on the context object's Selection, with first
7224 // argument hr's parent and the second argument equal to one plus hr's
7225 // index."
7226 getSelection().collapse(hr.parentNode, 1 + getNodeIndex(hr));
7227 getActiveRange().setStart(hr.parentNode, 1 + getNodeIndex(hr));
7228 getActiveRange().collapse(true);
7229
7230 // "Return true."
7231 return true;
7232 }
7233 };
7234
7235 //@}
7236 ///// The insertHTML command /////
7237 //@{
7238 commands.inserthtml = {
7239 preservesOverrides: true,
7240 action: function(value) {
7241 // "Delete the selection."
7242 deleteSelection();
7243
7244 // "If the active range's start node is neither editable nor an editing
7245 // host, return true."
7246 if (!isEditable(getActiveRange().startContainer)
7247 && !isEditingHost(getActiveRange().startContainer)) {
7248 return true;
7249 }
7250
7251 // "Let frag be the result of calling createContextualFragment(value)
7252 // on the active range."
7253 var frag = getActiveRange().createContextualFragment(value);
7254
7255 // "Let last child be the lastChild of frag."
7256 var lastChild = frag.lastChild;
7257
7258 // "If last child is null, return true."
7259 if (!lastChild) {
7260 return true;
7261 }
7262
7263 // "Let descendants be all descendants of frag."
7264 var descendants = getDescendants(frag);
7265
7266 // "If the active range's start node is a block node:"
7267 if (isBlockNode(getActiveRange().startContainer)) {
7268 // "Let collapsed block props be all editable collapsed block prop
7269 // children of the active range's start node that have index
7270 // greater than or equal to the active range's start offset."
7271 //
7272 // "For each node in collapsed block props, remove node from its
7273 // parent."
7274 [].filter.call(getActiveRange().startContainer.childNodes, function(node) {
7275 return isEditable(node)
7276 && isCollapsedBlockProp(node)
7277 && getNodeIndex(node) >= getActiveRange().startOffset;
7278 }).forEach(function(node) {
7279 node.parentNode.removeChild(node);
7280 });
7281 }
7282
7283 // "Call insertNode(frag) on the active range."
7284 getActiveRange().insertNode(frag);
7285
7286 // "If the active range's start node is a block node with no visible
7287 // children, call createElement("br") on the context object and append
7288 // the result as the last child of the active range's start node."
7289 if (isBlockNode(getActiveRange().startContainer)
7290 && ![].some.call(getActiveRange().startContainer.childNodes, isVisible)) {
7291 getActiveRange().startContainer.appendChild(document.createElement("br"));
7292 }
7293
7294 // "Call collapse() on the context object's Selection, with last
7295 // child's parent as the first argument and one plus its index as the
7296 // second."
7297 getActiveRange().setStart(lastChild.parentNode, 1 + getNodeIndex(lastChild));
7298 getActiveRange().setEnd(lastChild.parentNode, 1 + getNodeIndex(lastChild));
7299
7300 // "Fix disallowed ancestors of each member of descendants."
7301 for (var i = 0; i < descendants.length; i++) {
7302 fixDisallowedAncestors(descendants[i]);
7303 }
7304
7305 // "Return true."
7306 return true;
7307 }
7308 };
7309
7310 //@}
7311 ///// The insertImage command /////
7312 //@{
7313 commands.insertimage = {
7314 preservesOverrides: true,
7315 action: function(value) {
7316 // "If value is the empty string, return false."
7317 if (value === "") {
7318 return false;
7319 }
7320
7321 // "Delete the selection, with strip wrappers false."
7322 deleteSelection({stripWrappers: false});
7323
7324 // "Let range be the active range."
7325 var range = getActiveRange();
7326
7327 // "If the active range's start node is neither editable nor an editing
7328 // host, return true."
7329 if (!isEditable(getActiveRange().startContainer)
7330 && !isEditingHost(getActiveRange().startContainer)) {
7331 return true;
7332 }
7333
7334 // "If range's start node is a block node whose sole child is a br, and
7335 // its start offset is 0, remove its start node's child from it."
7336 if (isBlockNode(range.startContainer)
7337 && range.startContainer.childNodes.length == 1
7338 && isHtmlElement(range.startContainer.firstChild, "br")
7339 && range.startOffset == 0) {
7340 range.startContainer.removeChild(range.startContainer.firstChild);
7341 }
7342
7343 // "Let img be the result of calling createElement("img") on the
7344 // context object."
7345 var img = document.createElement("img");
7346
7347 // "Run setAttribute("src", value) on img."
7348 img.setAttribute("src", value);
7349
7350 // "Run insertNode(img) on the range."
7351 range.insertNode(img);
7352
7353 // "Run collapse() on the Selection, with first argument equal to the
7354 // parent of img and the second argument equal to one plus the index of
7355 // img."
7356 //
7357 // Not everyone actually supports collapse(), so we do it manually
7358 // instead. Also, we need to modify the actual range we're given as
7359 // well, for the sake of autoimplementation.html's range-filling-in.
7360 range.setStart(img.parentNode, 1 + getNodeIndex(img));
7361 range.setEnd(img.parentNode, 1 + getNodeIndex(img));
7362 getSelection().removeAllRanges();
7363 getSelection().addRange(range);
7364
7365 // IE adds width and height attributes for some reason, so remove those
7366 // to actually do what the spec says.
7367 img.removeAttribute("width");
7368 img.removeAttribute("height");
7369
7370 // "Return true."
7371 return true;
7372 }
7373 };
7374
7375 //@}
7376 ///// The insertLineBreak command /////
7377 //@{
7378 commands.insertlinebreak = {
7379 preservesOverrides: true,
7380 action: function(value) {
7381 // "Delete the selection, with strip wrappers false."
7382 deleteSelection({stripWrappers: false});
7383
7384 // "If the active range's start node is neither editable nor an editing
7385 // host, return true."
7386 if (!isEditable(getActiveRange().startContainer)
7387 && !isEditingHost(getActiveRange().startContainer)) {
7388 return true;
7389 }
7390
7391 // "If the active range's start node is an Element, and "br" is not an
7392 // allowed child of it, return true."
7393 if (getActiveRange().startContainer.nodeType == Node.ELEMENT_NODE
7394 && !isAllowedChild("br", getActiveRange().startContainer)) {
7395 return true;
7396 }
7397
7398 // "If the active range's start node is not an Element, and "br" is not
7399 // an allowed child of the active range's start node's parent, return
7400 // true."
7401 if (getActiveRange().startContainer.nodeType != Node.ELEMENT_NODE
7402 && !isAllowedChild("br", getActiveRange().startContainer.parentNode)) {
7403 return true;
7404 }
7405
7406 // "If the active range's start node is a Text node and its start
7407 // offset is zero, call collapse() on the context object's Selection,
7408 // with first argument equal to the active range's start node's parent
7409 // and second argument equal to the active range's start node's index."
7410 if (getActiveRange().startContainer.nodeType == Node.TEXT_NODE
7411 && getActiveRange().startOffset == 0) {
7412 var newNode = getActiveRange().startContainer.parentNode;
7413 var newOffset = getNodeIndex(getActiveRange().startContainer);
7414 getSelection().collapse(newNode, newOffset);
7415 getActiveRange().setStart(newNode, newOffset);
7416 getActiveRange().setEnd(newNode, newOffset);
7417 }
7418
7419 // "If the active range's start node is a Text node and its start
7420 // offset is the length of its start node, call collapse() on the
7421 // context object's Selection, with first argument equal to the active
7422 // range's start node's parent and second argument equal to one plus
7423 // the active range's start node's index."
7424 if (getActiveRange().startContainer.nodeType == Node.TEXT_NODE
7425 && getActiveRange().startOffset == getNodeLength(getActiveRange().startContainer)) {
7426 var newNode = getActiveRange().startContainer.parentNode;
7427 var newOffset = 1 + getNodeIndex(getActiveRange().startContainer);
7428 getSelection().collapse(newNode, newOffset);
7429 getActiveRange().setStart(newNode, newOffset);
7430 getActiveRange().setEnd(newNode, newOffset);
7431 }
7432
7433 // "Let br be the result of calling createElement("br") on the context
7434 // object."
7435 var br = document.createElement("br");
7436
7437 // "Call insertNode(br) on the active range."
7438 getActiveRange().insertNode(br);
7439
7440 // "Call collapse() on the context object's Selection, with br's parent
7441 // as the first argument and one plus br's index as the second
7442 // argument."
7443 getSelection().collapse(br.parentNode, 1 + getNodeIndex(br));
7444 getActiveRange().setStart(br.parentNode, 1 + getNodeIndex(br));
7445 getActiveRange().setEnd(br.parentNode, 1 + getNodeIndex(br));
7446
7447 // "If br is a collapsed line break, call createElement("br") on the
7448 // context object and let extra br be the result, then call
7449 // insertNode(extra br) on the active range."
7450 if (isCollapsedLineBreak(br)) {
7451 getActiveRange().insertNode(document.createElement("br"));
7452
7453 // Compensate for nonstandard implementations of insertNode
7454 getSelection().collapse(br.parentNode, 1 + getNodeIndex(br));
7455 getActiveRange().setStart(br.parentNode, 1 + getNodeIndex(br));
7456 getActiveRange().setEnd(br.parentNode, 1 + getNodeIndex(br));
7457 }
7458
7459 // "Return true."
7460 return true;
7461 }
7462 };
7463
7464 //@}
7465 ///// The insertOrderedList command /////
7466 //@{
7467 commands.insertorderedlist = {
7468 preservesOverrides: true,
7469 // "Toggle lists with tag name "ol", then return true."
7470 action: function() { toggleLists("ol"); return true },
7471 // "True if the selection's list state is "mixed" or "mixed ol", false
7472 // otherwise."
7473 indeterm: function() { return /^mixed( ol)?$/.test(getSelectionListState()) },
7474 // "True if the selection's list state is "ol", false otherwise."
7475 state: function() { return getSelectionListState() == "ol" },
7476 };
7477
7478 //@}
7479 ///// The insertParagraph command /////
7480 //@{
7481 commands.insertparagraph = {
7482 preservesOverrides: true,
7483 action: function() {
7484 // "Delete the selection."
7485 deleteSelection();
7486
7487 // "If the active range's start node is neither editable nor an editing
7488 // host, return true."
7489 if (!isEditable(getActiveRange().startContainer)
7490 && !isEditingHost(getActiveRange().startContainer)) {
7491 return true;
7492 }
7493
7494 // "Let node and offset be the active range's start node and offset."
7495 var node = getActiveRange().startContainer;
7496 var offset = getActiveRange().startOffset;
7497
7498 // "If node is a Text node, and offset is neither 0 nor the length of
7499 // node, call splitText(offset) on node."
7500 if (node.nodeType == Node.TEXT_NODE
7501 && offset != 0
7502 && offset != getNodeLength(node)) {
7503 node.splitText(offset);
7504 }
7505
7506 // "If node is a Text node and offset is its length, set offset to one
7507 // plus the index of node, then set node to its parent."
7508 if (node.nodeType == Node.TEXT_NODE
7509 && offset == getNodeLength(node)) {
7510 offset = 1 + getNodeIndex(node);
7511 node = node.parentNode;
7512 }
7513
7514 // "If node is a Text or Comment node, set offset to the index of node,
7515 // then set node to its parent."
7516 if (node.nodeType == Node.TEXT_NODE
7517 || node.nodeType == Node.COMMENT_NODE) {
7518 offset = getNodeIndex(node);
7519 node = node.parentNode;
7520 }
7521
7522 // "Call collapse(node, offset) on the context object's Selection."
7523 getSelection().collapse(node, offset);
7524 getActiveRange().setStart(node, offset);
7525 getActiveRange().setEnd(node, offset);
7526
7527 // "Let container equal node."
7528 var container = node;
7529
7530 // "While container is not a single-line container, and container's
7531 // parent is editable and in the same editing host as node, set
7532 // container to its parent."
7533 while (!isSingleLineContainer(container)
7534 && isEditable(container.parentNode)
7535 && inSameEditingHost(node, container.parentNode)) {
7536 container = container.parentNode;
7537 }
7538
7539 // "If container is an editable single-line container in the same
7540 // editing host as node, and its local name is "p" or "div":"
7541 if (isEditable(container)
7542 && isSingleLineContainer(container)
7543 && inSameEditingHost(node, container.parentNode)
7544 && (container.tagName == "P" || container.tagName == "DIV")) {
7545 // "Let outer container equal container."
7546 var outerContainer = container;
7547
7548 // "While outer container is not a dd or dt or li, and outer
7549 // container's parent is editable, set outer container to its
7550 // parent."
7551 while (!isHtmlElement(outerContainer, ["dd", "dt", "li"])
7552 && isEditable(outerContainer.parentNode)) {
7553 outerContainer = outerContainer.parentNode;
7554 }
7555
7556 // "If outer container is a dd or dt or li, set container to outer
7557 // container."
7558 if (isHtmlElement(outerContainer, ["dd", "dt", "li"])) {
7559 container = outerContainer;
7560 }
7561 }
7562
7563 // "If container is not editable or not in the same editing host as
7564 // node or is not a single-line container:"
7565 if (!isEditable(container)
7566 || !inSameEditingHost(container, node)
7567 || !isSingleLineContainer(container)) {
7568 // "Let tag be the default single-line container name."
7569 var tag = defaultSingleLineContainerName;
7570
7571 // "Block-extend the active range, and let new range be the
7572 // result."
7573 var newRange = blockExtend(getActiveRange());
7574
7575 // "Let node list be a list of nodes, initially empty."
7576 //
7577 // "Append to node list the first node in tree order that is
7578 // contained in new range and is an allowed child of "p", if any."
7579 var nodeList = getContainedNodes(newRange, function(node) { return isAllowedChild(node, "p") })
7580 .slice(0, 1);
7581
7582 // "If node list is empty:"
7583 if (!nodeList.length) {
7584 // "If tag is not an allowed child of the active range's start
7585 // node, return true."
7586 if (!isAllowedChild(tag, getActiveRange().startContainer)) {
7587 return true;
7588 }
7589
7590 // "Set container to the result of calling createElement(tag)
7591 // on the context object."
7592 container = document.createElement(tag);
7593
7594 // "Call insertNode(container) on the active range."
7595 getActiveRange().insertNode(container);
7596
7597 // "Call createElement("br") on the context object, and append
7598 // the result as the last child of container."
7599 container.appendChild(document.createElement("br"));
7600
7601 // "Call collapse(container, 0) on the context object's
7602 // Selection."
7603 getSelection().collapse(container, 0);
7604 getActiveRange().setStart(container, 0);
7605 getActiveRange().setEnd(container, 0);
7606
7607 // "Return true."
7608 return true;
7609 }
7610
7611 // "While the nextSibling of the last member of node list is not
7612 // null and is an allowed child of "p", append it to node list."
7613 while (nodeList[nodeList.length - 1].nextSibling
7614 && isAllowedChild(nodeList[nodeList.length - 1].nextSibling, "p")) {
7615 nodeList.push(nodeList[nodeList.length - 1].nextSibling);
7616 }
7617
7618 // "Wrap node list, with sibling criteria returning false and new
7619 // parent instructions returning the result of calling
7620 // createElement(tag) on the context object. Set container to the
7621 // result."
7622 container = wrap(nodeList,
7623 function() { return false },
7624 function() { return document.createElement(tag) }
7625 );
7626 }
7627
7628 // "If container's local name is "address", "listing", or "pre":"
7629 if (container.tagName == "ADDRESS"
7630 || container.tagName == "LISTING"
7631 || container.tagName == "PRE") {
7632 // "Let br be the result of calling createElement("br") on the
7633 // context object."
7634 var br = document.createElement("br");
7635
7636 // "Call insertNode(br) on the active range."
7637 getActiveRange().insertNode(br);
7638
7639 // "Call collapse(node, offset + 1) on the context object's
7640 // Selection."
7641 getSelection().collapse(node, offset + 1);
7642 getActiveRange().setStart(node, offset + 1);
7643 getActiveRange().setEnd(node, offset + 1);
7644
7645 // "If br is the last descendant of container, let br be the result
7646 // of calling createElement("br") on the context object, then call
7647 // insertNode(br) on the active range."
7648 //
7649 // Work around browser bugs: some browsers select the
7650 // newly-inserted node, not per spec.
7651 if (!isDescendant(nextNode(br), container)) {
7652 getActiveRange().insertNode(document.createElement("br"));
7653 getSelection().collapse(node, offset + 1);
7654 getActiveRange().setEnd(node, offset + 1);
7655 }
7656
7657 // "Return true."
7658 return true;
7659 }
7660
7661 // "If container's local name is "li", "dt", or "dd"; and either it has
7662 // no children or it has a single child and that child is a br:"
7663 if (["LI", "DT", "DD"].indexOf(container.tagName) != -1
7664 && (!container.hasChildNodes()
7665 || (container.childNodes.length == 1
7666 && isHtmlElement(container.firstChild, "br")))) {
7667 // "Split the parent of the one-node list consisting of container."
7668 splitParent([container]);
7669
7670 // "If container has no children, call createElement("br") on the
7671 // context object and append the result as the last child of
7672 // container."
7673 if (!container.hasChildNodes()) {
7674 container.appendChild(document.createElement("br"));
7675 }
7676
7677 // "If container is a dd or dt, and it is not an allowed child of
7678 // any of its ancestors in the same editing host, set the tag name
7679 // of container to the default single-line container name and let
7680 // container be the result."
7681 if (isHtmlElement(container, ["dd", "dt"])
7682 && getAncestors(container).every(function(ancestor) {
7683 return !inSameEditingHost(container, ancestor)
7684 || !isAllowedChild(container, ancestor)
7685 })) {
7686 container = setTagName(container, defaultSingleLineContainerName);
7687 }
7688
7689 // "Fix disallowed ancestors of container."
7690 fixDisallowedAncestors(container);
7691
7692 // "Return true."
7693 return true;
7694 }
7695
7696 // "Let new line range be a new range whose start is the same as
7697 // the active range's, and whose end is (container, length of
7698 // container)."
7699 var newLineRange = document.createRange();
7700 newLineRange.setStart(getActiveRange().startContainer, getActiveRange().startOffset);
7701 newLineRange.setEnd(container, getNodeLength(container));
7702
7703 // "While new line range's start offset is zero and its start node is
7704 // not a prohibited paragraph child, set its start to (parent of start
7705 // node, index of start node)."
7706 while (newLineRange.startOffset == 0
7707 && !isProhibitedParagraphChild(newLineRange.startContainer)) {
7708 newLineRange.setStart(newLineRange.startContainer.parentNode, getNodeIndex(newLineRange.startContainer));
7709 }
7710
7711 // "While new line range's start offset is the length of its start node
7712 // and its start node is not a prohibited paragraph child, set its
7713 // start to (parent of start node, 1 + index of start node)."
7714 while (newLineRange.startOffset == getNodeLength(newLineRange.startContainer)
7715 && !isProhibitedParagraphChild(newLineRange.startContainer)) {
7716 newLineRange.setStart(newLineRange.startContainer.parentNode, 1 + getNodeIndex(newLineRange.startContainer));
7717 }
7718
7719 // "Let end of line be true if new line range contains either nothing
7720 // or a single br, and false otherwise."
7721 var containedInNewLineRange = getContainedNodes(newLineRange);
7722 var endOfLine = !containedInNewLineRange.length
7723 || (containedInNewLineRange.length == 1
7724 && isHtmlElement(containedInNewLineRange[0], "br"));
7725
7726 // "If the local name of container is "h1", "h2", "h3", "h4", "h5", or
7727 // "h6", and end of line is true, let new container name be the default
7728 // single-line container name."
7729 var newContainerName;
7730 if (/^H[1-6]$/.test(container.tagName)
7731 && endOfLine) {
7732 newContainerName = defaultSingleLineContainerName;
7733
7734 // "Otherwise, if the local name of container is "dt" and end of line
7735 // is true, let new container name be "dd"."
7736 } else if (container.tagName == "DT"
7737 && endOfLine) {
7738 newContainerName = "dd";
7739
7740 // "Otherwise, if the local name of container is "dd" and end of line
7741 // is true, let new container name be "dt"."
7742 } else if (container.tagName == "DD"
7743 && endOfLine) {
7744 newContainerName = "dt";
7745
7746 // "Otherwise, let new container name be the local name of container."
7747 } else {
7748 newContainerName = container.tagName.toLowerCase();
7749 }
7750
7751 // "Let new container be the result of calling createElement(new
7752 // container name) on the context object."
7753 var newContainer = document.createElement(newContainerName);
7754
7755 // "Copy all attributes of container to new container."
7756 for (var i = 0; i < container.attributes.length; i++) {
7757 newContainer.setAttributeNS(container.attributes[i].namespaceURI, container.attributes[i].name, container.attributes[i].value);
7758 }
7759
7760 // "If new container has an id attribute, unset it."
7761 newContainer.removeAttribute("id");
7762
7763 // "Insert new container into the parent of container immediately after
7764 // container."
7765 container.parentNode.insertBefore(newContainer, container.nextSibling);
7766
7767 // "Let contained nodes be all nodes contained in new line range."
7768 var containedNodes = getAllContainedNodes(newLineRange);
7769
7770 // "Let frag be the result of calling extractContents() on new line
7771 // range."
7772 var frag = newLineRange.extractContents();
7773
7774 // "Unset the id attribute (if any) of each Element descendant of frag
7775 // that is not in contained nodes."
7776 var descendants = getDescendants(frag);
7777 for (var i = 0; i < descendants.length; i++) {
7778 if (descendants[i].nodeType == Node.ELEMENT_NODE
7779 && containedNodes.indexOf(descendants[i]) == -1) {
7780 descendants[i].removeAttribute("id");
7781 }
7782 }
7783
7784 // "Call appendChild(frag) on new container."
7785 newContainer.appendChild(frag);
7786
7787 // "While container's lastChild is a prohibited paragraph child, set
7788 // container to its lastChild."
7789 while (isProhibitedParagraphChild(container.lastChild)) {
7790 container = container.lastChild;
7791 }
7792
7793 // "While new container's lastChild is a prohibited paragraph child,
7794 // set new container to its lastChild."
7795 while (isProhibitedParagraphChild(newContainer.lastChild)) {
7796 newContainer = newContainer.lastChild;
7797 }
7798
7799 // "If container has no visible children, call createElement("br") on
7800 // the context object, and append the result as the last child of
7801 // container."
7802 if (![].some.call(container.childNodes, isVisible)) {
7803 container.appendChild(document.createElement("br"));
7804 }
7805
7806 // "If new container has no visible children, call createElement("br")
7807 // on the context object, and append the result as the last child of
7808 // new container."
7809 if (![].some.call(newContainer.childNodes, isVisible)) {
7810 newContainer.appendChild(document.createElement("br"));
7811 }
7812
7813 // "Call collapse(new container, 0) on the context object's Selection."
7814 getSelection().collapse(newContainer, 0);
7815 getActiveRange().setStart(newContainer, 0);
7816 getActiveRange().setEnd(newContainer, 0);
7817
7818 // "Return true."
7819 return true;
7820 }
7821 };
7822
7823 //@}
7824 ///// The insertText command /////
7825 //@{
7826 commands.inserttext = {
7827 action: function(value) {
7828 // "Delete the selection, with strip wrappers false."
7829 deleteSelection({stripWrappers: false});
7830
7831 // "If the active range's start node is neither editable nor an editing
7832 // host, return true."
7833 if (!isEditable(getActiveRange().startContainer)
7834 && !isEditingHost(getActiveRange().startContainer)) {
7835 return true;
7836 }
7837
7838 // "If value's length is greater than one:"
7839 if (value.length > 1) {
7840 // "For each element el in value, take the action for the
7841 // insertText command, with value equal to el."
7842 for (var i = 0; i < value.length; i++) {
7843 commands.inserttext.action(value[i]);
7844 }
7845
7846 // "Return true."
7847 return true;
7848 }
7849
7850 // "If value is the empty string, return true."
7851 if (value == "") {
7852 return true;
7853 }
7854
7855 // "If value is a newline (U+00A0), take the action for the
7856 // insertParagraph command and return true."
7857 if (value == "\n") {
7858 commands.insertparagraph.action();
7859 return true;
7860 }
7861
7862 // "Let node and offset be the active range's start node and offset."
7863 var node = getActiveRange().startContainer;
7864 var offset = getActiveRange().startOffset;
7865
7866 // "If node has a child whose index is offset − 1, and that child is a
7867 // Text node, set node to that child, then set offset to node's
7868 // length."
7869 if (0 <= offset - 1
7870 && offset - 1 < node.childNodes.length
7871 && node.childNodes[offset - 1].nodeType == Node.TEXT_NODE) {
7872 node = node.childNodes[offset - 1];
7873 offset = getNodeLength(node);
7874 }
7875
7876 // "If node has a child whose index is offset, and that child is a Text
7877 // node, set node to that child, then set offset to zero."
7878 if (0 <= offset
7879 && offset < node.childNodes.length
7880 && node.childNodes[offset].nodeType == Node.TEXT_NODE) {
7881 node = node.childNodes[offset];
7882 offset = 0;
7883 }
7884
7885 // "Record current overrides, and let overrides be the result."
7886 var overrides = recordCurrentOverrides();
7887
7888 // "Call collapse(node, offset) on the context object's Selection."
7889 getSelection().collapse(node, offset);
7890 getActiveRange().setStart(node, offset);
7891 getActiveRange().setEnd(node, offset);
7892
7893 // "Canonicalize whitespace at (node, offset)."
7894 canonicalizeWhitespace(node, offset);
7895
7896 // "Let (node, offset) be the active range's start."
7897 node = getActiveRange().startContainer;
7898 offset = getActiveRange().startOffset;
7899
7900 // "If node is a Text node:"
7901 if (node.nodeType == Node.TEXT_NODE) {
7902 // "Call insertData(offset, value) on node."
7903 node.insertData(offset, value);
7904
7905 // "Call collapse(node, offset) on the context object's Selection."
7906 getSelection().collapse(node, offset);
7907 getActiveRange().setStart(node, offset);
7908
7909 // "Call extend(node, offset + 1) on the context object's
7910 // Selection."
7911 //
7912 // Work around WebKit bug: the extend() can throw if the text we're
7913 // adding is trailing whitespace.
7914 try { getSelection().extend(node, offset + 1); } catch(e) {}
7915 getActiveRange().setEnd(node, offset + 1);
7916
7917 // "Otherwise:"
7918 } else {
7919 // "If node has only one child, which is a collapsed line break,
7920 // remove its child from it."
7921 //
7922 // FIXME: IE incorrectly returns false here instead of true
7923 // sometimes?
7924 if (node.childNodes.length == 1
7925 && isCollapsedLineBreak(node.firstChild)) {
7926 node.removeChild(node.firstChild);
7927 }
7928
7929 // "Let text be the result of calling createTextNode(value) on the
7930 // context object."
7931 var text = document.createTextNode(value);
7932
7933 // "Call insertNode(text) on the active range."
7934 getActiveRange().insertNode(text);
7935
7936 // "Call collapse(text, 0) on the context object's Selection."
7937 getSelection().collapse(text, 0);
7938 getActiveRange().setStart(text, 0);
7939
7940 // "Call extend(text, 1) on the context object's Selection."
7941 getSelection().extend(text, 1);
7942 getActiveRange().setEnd(text, 1);
7943 }
7944
7945 // "Restore states and values from overrides."
7946 restoreStatesAndValues(overrides);
7947
7948 // "Canonicalize whitespace at the active range's start, with fix
7949 // collapsed space false."
7950 canonicalizeWhitespace(getActiveRange().startContainer, getActiveRange().startOffset, false);
7951
7952 // "Canonicalize whitespace at the active range's end, with fix
7953 // collapsed space false."
7954 canonicalizeWhitespace(getActiveRange().endContainer, getActiveRange().endOffset, false);
7955
7956 // "If value is a space character, autolink the active range's start."
7957 if (/^[ \t\n\f\r]$/.test(value)) {
7958 autolink(getActiveRange().startContainer, getActiveRange().startOffset);
7959 }
7960
7961 // "Call collapseToEnd() on the context object's Selection."
7962 //
7963 // Work around WebKit bug: sometimes it blows up the selection and
7964 // throws, which we don't want.
7965 try { getSelection().collapseToEnd(); } catch(e) {}
7966 getActiveRange().collapse(false);
7967
7968 // "Return true."
7969 return true;
7970 }
7971 };
7972
7973 //@}
7974 ///// The insertUnorderedList command /////
7975 //@{
7976 commands.insertunorderedlist = {
7977 preservesOverrides: true,
7978 // "Toggle lists with tag name "ul", then return true."
7979 action: function() { toggleLists("ul"); return true },
7980 // "True if the selection's list state is "mixed" or "mixed ul", false
7981 // otherwise."
7982 indeterm: function() { return /^mixed( ul)?$/.test(getSelectionListState()) },
7983 // "True if the selection's list state is "ul", false otherwise."
7984 state: function() { return getSelectionListState() == "ul" },
7985 };
7986
7987 //@}
7988 ///// The justifyCenter command /////
7989 //@{
7990 commands.justifycenter = {
7991 preservesOverrides: true,
7992 // "Justify the selection with alignment "center", then return true."
7993 action: function() { justifySelection("center"); return true },
7994 indeterm: function() {
7995 // "Return false if the active range is null. Otherwise, block-extend
7996 // the active range. Return true if among visible editable nodes that
7997 // are contained in the result and have no children, at least one has
7998 // alignment value "center" and at least one does not. Otherwise return
7999 // false."
8000 if (!getActiveRange()) {
8001 return false;
8002 }
8003 var nodes = getAllContainedNodes(blockExtend(getActiveRange()), function(node) {
8004 return isEditable(node) && isVisible(node) && !node.hasChildNodes();
8005 });
8006 return nodes.some(function(node) { return getAlignmentValue(node) == "center" })
8007 && nodes.some(function(node) { return getAlignmentValue(node) != "center" });
8008 }, state: function() {
8009 // "Return false if the active range is null. Otherwise, block-extend
8010 // the active range. Return true if there is at least one visible
8011 // editable node that is contained in the result and has no children,
8012 // and all such nodes have alignment value "center". Otherwise return
8013 // false."
8014 if (!getActiveRange()) {
8015 return false;
8016 }
8017 var nodes = getAllContainedNodes(blockExtend(getActiveRange()), function(node) {
8018 return isEditable(node) && isVisible(node) && !node.hasChildNodes();
8019 });
8020 return nodes.length
8021 && nodes.every(function(node) { return getAlignmentValue(node) == "center" });
8022 }, value: function() {
8023 // "Return the empty string if the active range is null. Otherwise,
8024 // block-extend the active range, and return the alignment value of the
8025 // first visible editable node that is contained in the result and has
8026 // no children. If there is no such node, return "left"."
8027 if (!getActiveRange()) {
8028 return "";
8029 }
8030 var nodes = getAllContainedNodes(blockExtend(getActiveRange()), function(node) {
8031 return isEditable(node) && isVisible(node) && !node.hasChildNodes();
8032 });
8033 if (nodes.length) {
8034 return getAlignmentValue(nodes[0]);
8035 } else {
8036 return "left";
8037 }
8038 },
8039 };
8040
8041 //@}
8042 ///// The justifyFull command /////
8043 //@{
8044 commands.justifyfull = {
8045 preservesOverrides: true,
8046 // "Justify the selection with alignment "justify", then return true."
8047 action: function() { justifySelection("justify"); return true },
8048 indeterm: function() {
8049 // "Return false if the active range is null. Otherwise, block-extend
8050 // the active range. Return true if among visible editable nodes that
8051 // are contained in the result and have no children, at least one has
8052 // alignment value "justify" and at least one does not. Otherwise
8053 // return false."
8054 if (!getActiveRange()) {
8055 return false;
8056 }
8057 var nodes = getAllContainedNodes(blockExtend(getActiveRange()), function(node) {
8058 return isEditable(node) && isVisible(node) && !node.hasChildNodes();
8059 });
8060 return nodes.some(function(node) { return getAlignmentValue(node) == "justify" })
8061 && nodes.some(function(node) { return getAlignmentValue(node) != "justify" });
8062 }, state: function() {
8063 // "Return false if the active range is null. Otherwise, block-extend
8064 // the active range. Return true if there is at least one visible
8065 // editable node that is contained in the result and has no children,
8066 // and all such nodes have alignment value "justify". Otherwise return
8067 // false."
8068 if (!getActiveRange()) {
8069 return false;
8070 }
8071 var nodes = getAllContainedNodes(blockExtend(getActiveRange()), function(node) {
8072 return isEditable(node) && isVisible(node) && !node.hasChildNodes();
8073 });
8074 return nodes.length
8075 && nodes.every(function(node) { return getAlignmentValue(node) == "justify" });
8076 }, value: function() {
8077 // "Return the empty string if the active range is null. Otherwise,
8078 // block-extend the active range, and return the alignment value of the
8079 // first visible editable node that is contained in the result and has
8080 // no children. If there is no such node, return "left"."
8081 if (!getActiveRange()) {
8082 return "";
8083 }
8084 var nodes = getAllContainedNodes(blockExtend(getActiveRange()), function(node) {
8085 return isEditable(node) && isVisible(node) && !node.hasChildNodes();
8086 });
8087 if (nodes.length) {
8088 return getAlignmentValue(nodes[0]);
8089 } else {
8090 return "left";
8091 }
8092 },
8093 };
8094
8095 //@}
8096 ///// The justifyLeft command /////
8097 //@{
8098 commands.justifyleft = {
8099 preservesOverrides: true,
8100 // "Justify the selection with alignment "left", then return true."
8101 action: function() { justifySelection("left"); return true },
8102 indeterm: function() {
8103 // "Return false if the active range is null. Otherwise, block-extend
8104 // the active range. Return true if among visible editable nodes that
8105 // are contained in the result and have no children, at least one has
8106 // alignment value "left" and at least one does not. Otherwise return
8107 // false."
8108 if (!getActiveRange()) {
8109 return false;
8110 }
8111 var nodes = getAllContainedNodes(blockExtend(getActiveRange()), function(node) {
8112 return isEditable(node) && isVisible(node) && !node.hasChildNodes();
8113 });
8114 return nodes.some(function(node) { return getAlignmentValue(node) == "left" })
8115 && nodes.some(function(node) { return getAlignmentValue(node) != "left" });
8116 }, state: function() {
8117 // "Return false if the active range is null. Otherwise, block-extend
8118 // the active range. Return true if there is at least one visible
8119 // editable node that is contained in the result and has no children,
8120 // and all such nodes have alignment value "left". Otherwise return
8121 // false."
8122 if (!getActiveRange()) {
8123 return false;
8124 }
8125 var nodes = getAllContainedNodes(blockExtend(getActiveRange()), function(node) {
8126 return isEditable(node) && isVisible(node) && !node.hasChildNodes();
8127 });
8128 return nodes.length
8129 && nodes.every(function(node) { return getAlignmentValue(node) == "left" });
8130 }, value: function() {
8131 // "Return the empty string if the active range is null. Otherwise,
8132 // block-extend the active range, and return the alignment value of the
8133 // first visible editable node that is contained in the result and has
8134 // no children. If there is no such node, return "left"."
8135 if (!getActiveRange()) {
8136 return "";
8137 }
8138 var nodes = getAllContainedNodes(blockExtend(getActiveRange()), function(node) {
8139 return isEditable(node) && isVisible(node) && !node.hasChildNodes();
8140 });
8141 if (nodes.length) {
8142 return getAlignmentValue(nodes[0]);
8143 } else {
8144 return "left";
8145 }
8146 },
8147 };
8148
8149 //@}
8150 ///// The justifyRight command /////
8151 //@{
8152 commands.justifyright = {
8153 preservesOverrides: true,
8154 // "Justify the selection with alignment "right", then return true."
8155 action: function() { justifySelection("right"); return true },
8156 indeterm: function() {
8157 // "Return false if the active range is null. Otherwise, block-extend
8158 // the active range. Return true if among visible editable nodes that
8159 // are contained in the result and have no children, at least one has
8160 // alignment value "right" and at least one does not. Otherwise return
8161 // false."
8162 if (!getActiveRange()) {
8163 return false;
8164 }
8165 var nodes = getAllContainedNodes(blockExtend(getActiveRange()), function(node) {
8166 return isEditable(node) && isVisible(node) && !node.hasChildNodes();
8167 });
8168 return nodes.some(function(node) { return getAlignmentValue(node) == "right" })
8169 && nodes.some(function(node) { return getAlignmentValue(node) != "right" });
8170 }, state: function() {
8171 // "Return false if the active range is null. Otherwise, block-extend
8172 // the active range. Return true if there is at least one visible
8173 // editable node that is contained in the result and has no children,
8174 // and all such nodes have alignment value "right". Otherwise return
8175 // false."
8176 if (!getActiveRange()) {
8177 return false;
8178 }
8179 var nodes = getAllContainedNodes(blockExtend(getActiveRange()), function(node) {
8180 return isEditable(node) && isVisible(node) && !node.hasChildNodes();
8181 });
8182 return nodes.length
8183 && nodes.every(function(node) { return getAlignmentValue(node) == "right" });
8184 }, value: function() {
8185 // "Return the empty string if the active range is null. Otherwise,
8186 // block-extend the active range, and return the alignment value of the
8187 // first visible editable node that is contained in the result and has
8188 // no children. If there is no such node, return "left"."
8189 if (!getActiveRange()) {
8190 return "";
8191 }
8192 var nodes = getAllContainedNodes(blockExtend(getActiveRange()), function(node) {
8193 return isEditable(node) && isVisible(node) && !node.hasChildNodes();
8194 });
8195 if (nodes.length) {
8196 return getAlignmentValue(nodes[0]);
8197 } else {
8198 return "left";
8199 }
8200 },
8201 };
8202
8203 //@}
8204 ///// The outdent command /////
8205 //@{
8206 commands.outdent = {
8207 preservesOverrides: true,
8208 action: function() {
8209 // "Let items be a list of all lis that are ancestor containers of the
8210 // range's start and/or end node."
8211 //
8212 // It's annoying to get this in tree order using functional stuff
8213 // without doing getDescendants(document), which is slow, so I do it
8214 // imperatively.
8215 var items = [];
8216 (function(){
8217 for (
8218 var ancestorContainer = getActiveRange().endContainer;
8219 ancestorContainer != getActiveRange().commonAncestorContainer;
8220 ancestorContainer = ancestorContainer.parentNode
8221 ) {
8222 if (isHtmlElement(ancestorContainer, "li")) {
8223 items.unshift(ancestorContainer);
8224 }
8225 }
8226 for (
8227 var ancestorContainer = getActiveRange().startContainer;
8228 ancestorContainer;
8229 ancestorContainer = ancestorContainer.parentNode
8230 ) {
8231 if (isHtmlElement(ancestorContainer, "li")) {
8232 items.unshift(ancestorContainer);
8233 }
8234 }
8235 })();
8236
8237 // "For each item in items, normalize sublists of item."
8238 items.forEach(normalizeSublists);
8239
8240 // "Block-extend the active range, and let new range be the result."
8241 var newRange = blockExtend(getActiveRange());
8242
8243 // "Let node list be a list of nodes, initially empty."
8244 //
8245 // "For each node node contained in new range, append node to node list
8246 // if the last member of node list (if any) is not an ancestor of node;
8247 // node is editable; and either node has no editable descendants, or is
8248 // an ol or ul, or is an li whose parent is an ol or ul."
8249 var nodeList = getContainedNodes(newRange, function(node) {
8250 return isEditable(node)
8251 && (!getDescendants(node).some(isEditable)
8252 || isHtmlElement(node, ["ol", "ul"])
8253 || (isHtmlElement(node, "li") && isHtmlElement(node.parentNode, ["ol", "ul"])));
8254 });
8255
8256 // "While node list is not empty:"
8257 while (nodeList.length) {
8258 // "While the first member of node list is an ol or ul or is not
8259 // the child of an ol or ul, outdent it and remove it from node
8260 // list."
8261 while (nodeList.length
8262 && (isHtmlElement(nodeList[0], ["OL", "UL"])
8263 || !isHtmlElement(nodeList[0].parentNode, ["OL", "UL"]))) {
8264 outdentNode(nodeList.shift());
8265 }
8266
8267 // "If node list is empty, break from these substeps."
8268 if (!nodeList.length) {
8269 break;
8270 }
8271
8272 // "Let sublist be a list of nodes, initially empty."
8273 var sublist = [];
8274
8275 // "Remove the first member of node list and append it to sublist."
8276 sublist.push(nodeList.shift());
8277
8278 // "While the first member of node list is the nextSibling of the
8279 // last member of sublist, and the first member of node list is not
8280 // an ol or ul, remove the first member of node list and append it
8281 // to sublist."
8282 while (nodeList.length
8283 && nodeList[0] == sublist[sublist.length - 1].nextSibling
8284 && !isHtmlElement(nodeList[0], ["OL", "UL"])) {
8285 sublist.push(nodeList.shift());
8286 }
8287
8288 // "Record the values of sublist, and let values be the result."
8289 var values = recordValues(sublist);
8290
8291 // "Split the parent of sublist, with new parent null."
8292 splitParent(sublist);
8293
8294 // "Fix disallowed ancestors of each member of sublist."
8295 sublist.forEach(fixDisallowedAncestors);
8296
8297 // "Restore the values from values."
8298 restoreValues(values);
8299 }
8300
8301 // "Return true."
8302 return true;
8303 }
8304 };
8305
8306 //@}
8307
8308 //////////////////////////////////
8309 ///// Miscellaneous commands /////
8310 //////////////////////////////////
8311
8312 ///// The defaultParagraphSeparator command /////
8313 //@{
8314 commands.defaultparagraphseparator = {
8315 action: function(value) {
8316 // "Let value be converted to ASCII lowercase. If value is then equal
8317 // to "p" or "div", set the context object's default single-line
8318 // container name to value and return true. Otherwise, return false."
8319 value = value.toLowerCase();
8320 if (value == "p" || value == "div") {
8321 defaultSingleLineContainerName = value;
8322 return true;
8323 }
8324 return false;
8325 }, value: function() {
8326 // "Return the context object's default single-line container name."
8327 return defaultSingleLineContainerName;
8328 },
8329 };
8330
8331 //@}
8332 ///// The selectAll command /////
8333 //@{
8334 commands.selectall = {
8335 // Note, this ignores the whole globalRange/getActiveRange() thing and
8336 // works with actual selections. Not suitable for autoimplementation.html.
8337 action: function() {
8338 // "Let target be the body element of the context object."
8339 var target = document.body;
8340
8341 // "If target is null, let target be the context object's
8342 // documentElement."
8343 if (!target) {
8344 target = document.documentElement;
8345 }
8346
8347 // "If target is null, call getSelection() on the context object, and
8348 // call removeAllRanges() on the result."
8349 if (!target) {
8350 getSelection().removeAllRanges();
8351
8352 // "Otherwise, call getSelection() on the context object, and call
8353 // selectAllChildren(target) on the result."
8354 } else {
8355 getSelection().selectAllChildren(target);
8356 }
8357
8358 // "Return true."
8359 return true;
8360 }
8361 };
8362
8363 //@}
8364 ///// The styleWithCSS command /////
8365 //@{
8366 commands.stylewithcss = {
8367 action: function(value) {
8368 // "If value is an ASCII case-insensitive match for the string
8369 // "false", set the CSS styling flag to false. Otherwise, set the
8370 // CSS styling flag to true. Either way, return true."
8371 cssStylingFlag = String(value).toLowerCase() != "false";
8372 return true;
8373 }, state: function() { return cssStylingFlag }
8374 };
8375
8376 //@}
8377 ///// The useCSS command /////
8378 //@{
8379 commands.usecss = {
8380 action: function(value) {
8381 // "If value is an ASCII case-insensitive match for the string "false",
8382 // set the CSS styling flag to true. Otherwise, set the CSS styling
8383 // flag to false. Either way, return true."
8384 cssStylingFlag = String(value).toLowerCase() == "false";
8385 return true;
8386 }
8387 };
8388 //@}
8389
8390 // Some final setup
8391 //@{
8392 (function() {
8393 // Opera 11.50 doesn't implement Object.keys, so I have to make an explicit
8394 // temporary, which means I need an extra closure to not leak the temporaries
8395 // into the global namespace. >:(
8396 var commandNames = [];
8397 for (var command in commands) {
8398 commandNames.push(command);
8399 }
8400 commandNames.forEach(function(command) {
8401 // "If a command does not have a relevant CSS property specified, it
8402 // defaults to null."
8403 if (!("relevantCssProperty" in commands[command])) {
8404 commands[command].relevantCssProperty = null;
8405 }
8406
8407 // "If a command has inline command activated values defined but nothing
8408 // else defines when it is indeterminate, it is indeterminate if among
8409 // formattable nodes effectively contained in the active range, there is at
8410 // least one whose effective command value is one of the given values and
8411 // at least one whose effective command value is not one of the given
8412 // values."
8413 if ("inlineCommandActivatedValues" in commands[command]
8414 && !("indeterm" in commands[command])) {
8415 commands[command].indeterm = function() {
8416 if (!getActiveRange()) {
8417 return false;
8418 }
8419
8420 var values = getAllEffectivelyContainedNodes(getActiveRange(), isFormattableNode)
8421 .map(function(node) { return getEffectiveCommandValue(node, command) });
8422
8423 var matchingValues = values.filter(function(value) {
8424 return commands[command].inlineCommandActivatedValues.indexOf(value) != -1;
8425 });
8426
8427 return matchingValues.length >= 1
8428 && values.length - matchingValues.length >= 1;
8429 };
8430 }
8431
8432 // "If a command has inline command activated values defined, its state is
8433 // true if either no formattable node is effectively contained in the
8434 // active range, and the active range's start node's effective command
8435 // value is one of the given values; or if there is at least one
8436 // formattable node effectively contained in the active range, and all of
8437 // them have an effective command value equal to one of the given values."
8438 if ("inlineCommandActivatedValues" in commands[command]) {
8439 commands[command].state = function() {
8440 if (!getActiveRange()) {
8441 return false;
8442 }
8443
8444 var nodes = getAllEffectivelyContainedNodes(getActiveRange(), isFormattableNode);
8445
8446 if (nodes.length == 0) {
8447 return commands[command].inlineCommandActivatedValues
8448 .indexOf(getEffectiveCommandValue(getActiveRange().startContainer, command)) != -1;
8449 } else {
8450 return nodes.every(function(node) {
8451 return commands[command].inlineCommandActivatedValues
8452 .indexOf(getEffectiveCommandValue(node, command)) != -1;
8453 });
8454 }
8455 };
8456 }
8457
8458 // "If a command is a standard inline value command, it is indeterminate if
8459 // among formattable nodes that are effectively contained in the active
8460 // range, there are two that have distinct effective command values. Its
8461 // value is the effective command value of the first formattable node that
8462 // is effectively contained in the active range; or if there is no such
8463 // node, the effective command value of the active range's start node; or
8464 // if that is null, the empty string."
8465 if ("standardInlineValueCommand" in commands[command]) {
8466 commands[command].indeterm = function() {
8467 if (!getActiveRange()) {
8468 return false;
8469 }
8470
8471 var values = getAllEffectivelyContainedNodes(getActiveRange())
8472 .filter(isFormattableNode)
8473 .map(function(node) { return getEffectiveCommandValue(node, command) });
8474 for (var i = 1; i < values.length; i++) {
8475 if (values[i] != values[i - 1]) {
8476 return true;
8477 }
8478 }
8479 return false;
8480 };
8481
8482 commands[command].value = function() {
8483 if (!getActiveRange()) {
8484 return "";
8485 }
8486
8487 var refNode = getAllEffectivelyContainedNodes(getActiveRange(), isFormattableNode)[0];
8488
8489 if (typeof refNode == "undefined") {
8490 refNode = getActiveRange().startContainer;
8491 }
8492
8493 var ret = getEffectiveCommandValue(refNode, command);
8494 if (ret === null) {
8495 return "";
8496 }
8497 return ret;
8498 };
8499 }
8500
8501 // "If a command preserves overrides, then before taking its action, the
8502 // user agent must record current overrides. After taking the action, if
8503 // the active range is collapsed, it must restore states and values from
8504 // the recorded list."
8505 if ("preservesOverrides" in commands[command]) {
8506 var oldAction = commands[command].action;
8507
8508 commands[command].action = function(value) {
8509 var overrides = recordCurrentOverrides();
8510 var ret = oldAction(value);
8511 if (getActiveRange().collapsed) {
8512 restoreStatesAndValues(overrides);
8513 }
8514 return ret;
8515 };
8516 }
8517 });
8518 })();
8519 //@}
8520
8521 // vim: foldmarker=@{,@} foldmethod=marker

mercurial