Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(340)

Side by Side Diff: third_party/WebKit/LayoutTests/imported/wpt/editing/include/implementation.js

Issue 2630243004: Manually import wpt/editing and wpt/selection/ (Closed)
Patch Set: Add expectation Created 3 years, 11 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch
OLDNEW
(Empty)
1 "use strict";
2
3 var htmlNamespace = "http://www.w3.org/1999/xhtml";
4
5 var cssStylingFlag = false;
6
7 var defaultSingleLineContainerName = "div";
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 functio n() {
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(g etActiveRange().endContainer))
588 && (getInclusiveAncestors(getActiveRange().commonAncestorContain er).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 functio n() {
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 functio n() {
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", "i nline-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://w ww.w3.org/2000/svg" && node.localName == "svg")
754 || (node.nodeType == Node.ELEMENT_NODE && node.namespaceURI == "http://w ww.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).wh iteSpace) != -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 && !isWhitespaceN ode(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 ind ex
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 t han
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, el ement.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, replacement Element.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.startOf fset + 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", "h 6"]],
1709 [["li"], ["li"]],
1710 [["nobr"], ["nobr"]],
1711 [namesOfElementsWithInlineContents, prohibitedParagraphChildNames],
1712 [["td", "th"], ["caption", "col", "colgroup", "tbody", "td", "tfoot", "t h", "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 isEffectivelyCont ained(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"].ind exOf(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", "S UP", "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 || (node.style.length == 4
1980 && "MozTextBlink" in node.style
1981 && "textDecorationColor" in node.style
1982 && "textDecorationLine" in node.style
1983 && "textDecorationStyle" in node.style)
1984 )
1985 && (node.style.textDecoration == "line-through"
1986 || node.style.textDecoration == "underline"
1987 || node.style.textDecoration == "overline"
1988 || node.style.textDecoration == "none")) {
1989 return true;
1990 }
1991
1992 return false;
1993 }
1994
1995 // "A formattable node is an editable visible node that is either a Text node,
1996 // an img, or a br."
1997 function isFormattableNode(node) {
1998 return isEditable(node)
1999 && isVisible(node)
2000 && (node.nodeType == Node.TEXT_NODE
2001 || isHtmlElement(node, ["img", "br"]));
2002 }
2003
2004 // "Two quantities are equivalent values for a command if either both are null,
2005 // or both are strings and they're equal and the command does not define any
2006 // equivalent values, or both are strings and the command defines equivalent
2007 // values and they match the definition."
2008 function areEquivalentValues(command, val1, val2) {
2009 if (val1 === null && val2 === null) {
2010 return true;
2011 }
2012
2013 if (typeof val1 == "string"
2014 && typeof val2 == "string"
2015 && val1 == val2
2016 && !("equivalentValues" in commands[command])) {
2017 return true;
2018 }
2019
2020 if (typeof val1 == "string"
2021 && typeof val2 == "string"
2022 && "equivalentValues" in commands[command]
2023 && commands[command].equivalentValues(val1, val2)) {
2024 return true;
2025 }
2026
2027 return false;
2028 }
2029
2030 // "Two quantities are loosely equivalent values for a command if either they
2031 // are equivalent values for the command, or if the command is the fontSize
2032 // command; one of the quantities is one of "x-small", "small", "medium",
2033 // "large", "x-large", "xx-large", or "xxx-large"; and the other quantity is
2034 // the resolved value of "font-size" on a font element whose size attribute has
2035 // the corresponding value set ("1" through "7" respectively)."
2036 function areLooselyEquivalentValues(command, val1, val2) {
2037 if (areEquivalentValues(command, val1, val2)) {
2038 return true;
2039 }
2040
2041 if (command != "fontsize"
2042 || typeof val1 != "string"
2043 || typeof val2 != "string") {
2044 return false;
2045 }
2046
2047 // Static variables in JavaScript?
2048 var callee = areLooselyEquivalentValues;
2049 if (callee.sizeMap === undefined) {
2050 callee.sizeMap = {};
2051 var font = document.createElement("font");
2052 document.body.appendChild(font);
2053 ["x-small", "small", "medium", "large", "x-large", "xx-large",
2054 "xxx-large"].forEach(function(keyword) {
2055 font.size = cssSizeToLegacy(keyword);
2056 callee.sizeMap[keyword] = getComputedStyle(font).fontSize;
2057 });
2058 document.body.removeChild(font);
2059 }
2060
2061 return val1 === callee.sizeMap[val2]
2062 || val2 === callee.sizeMap[val1];
2063 }
2064
2065 //@}
2066 ///// Assorted inline formatting command algorithms /////
2067 //@{
2068
2069 function getEffectiveCommandValue(node, command) {
2070 // "If neither node nor its parent is an Element, return null."
2071 if (node.nodeType != Node.ELEMENT_NODE
2072 && (!node.parentNode || node.parentNode.nodeType != Node.ELEMENT_NODE)) {
2073 return null;
2074 }
2075
2076 // "If node is not an Element, return the effective command value of its
2077 // parent for command."
2078 if (node.nodeType != Node.ELEMENT_NODE) {
2079 return getEffectiveCommandValue(node.parentNode, command);
2080 }
2081
2082 // "If command is "createLink" or "unlink":"
2083 if (command == "createlink" || command == "unlink") {
2084 // "While node is not null, and is not an a element that has an href
2085 // attribute, set node to its parent."
2086 while (node
2087 && (!isHtmlElement(node)
2088 || node.tagName != "A"
2089 || !node.hasAttribute("href"))) {
2090 node = node.parentNode;
2091 }
2092
2093 // "If node is null, return null."
2094 if (!node) {
2095 return null;
2096 }
2097
2098 // "Return the value of node's href attribute."
2099 return node.getAttribute("href");
2100 }
2101
2102 // "If command is "backColor" or "hiliteColor":"
2103 if (command == "backcolor"
2104 || command == "hilitecolor") {
2105 // "While the resolved value of "background-color" on node is any
2106 // fully transparent value, and node's parent is an Element, set
2107 // node to its parent."
2108 //
2109 // Another lame hack to avoid flawed APIs.
2110 while ((getComputedStyle(node).backgroundColor == "rgba(0, 0, 0, 0)"
2111 || getComputedStyle(node).backgroundColor === ""
2112 || getComputedStyle(node).backgroundColor == "transparent")
2113 && node.parentNode
2114 && node.parentNode.nodeType == Node.ELEMENT_NODE) {
2115 node = node.parentNode;
2116 }
2117
2118 // "Return the resolved value of "background-color" for node."
2119 return getComputedStyle(node).backgroundColor;
2120 }
2121
2122 // "If command is "subscript" or "superscript":"
2123 if (command == "subscript" || command == "superscript") {
2124 // "Let affected by subscript and affected by superscript be two
2125 // boolean variables, both initially false."
2126 var affectedBySubscript = false;
2127 var affectedBySuperscript = false;
2128
2129 // "While node is an inline node:"
2130 while (isInlineNode(node)) {
2131 var verticalAlign = getComputedStyle(node).verticalAlign;
2132
2133 // "If node is a sub, set affected by subscript to true."
2134 if (isHtmlElement(node, "sub")) {
2135 affectedBySubscript = true;
2136 // "Otherwise, if node is a sup, set affected by superscript to
2137 // true."
2138 } else if (isHtmlElement(node, "sup")) {
2139 affectedBySuperscript = true;
2140 }
2141
2142 // "Set node to its parent."
2143 node = node.parentNode;
2144 }
2145
2146 // "If affected by subscript and affected by superscript are both true,
2147 // return the string "mixed"."
2148 if (affectedBySubscript && affectedBySuperscript) {
2149 return "mixed";
2150 }
2151
2152 // "If affected by subscript is true, return "subscript"."
2153 if (affectedBySubscript) {
2154 return "subscript";
2155 }
2156
2157 // "If affected by superscript is true, return "superscript"."
2158 if (affectedBySuperscript) {
2159 return "superscript";
2160 }
2161
2162 // "Return null."
2163 return null;
2164 }
2165
2166 // "If command is "strikethrough", and the "text-decoration" property of
2167 // node or any of its ancestors has resolved value containing
2168 // "line-through", return "line-through". Otherwise, return null."
2169 if (command == "strikethrough") {
2170 do {
2171 if (getComputedStyle(node).textDecoration.indexOf("line-through") != -1) {
2172 return "line-through";
2173 }
2174 node = node.parentNode;
2175 } while (node && node.nodeType == Node.ELEMENT_NODE);
2176 return null;
2177 }
2178
2179 // "If command is "underline", and the "text-decoration" property of node
2180 // or any of its ancestors has resolved value containing "underline",
2181 // return "underline". Otherwise, return null."
2182 if (command == "underline") {
2183 do {
2184 if (getComputedStyle(node).textDecoration.indexOf("underline") != -1 ) {
2185 return "underline";
2186 }
2187 node = node.parentNode;
2188 } while (node && node.nodeType == Node.ELEMENT_NODE);
2189 return null;
2190 }
2191
2192 if (!("relevantCssProperty" in commands[command])) {
2193 throw "Bug: no relevantCssProperty for " + command + " in getEffectiveCo mmandValue";
2194 }
2195
2196 // "Return the resolved value for node of the relevant CSS property for
2197 // command."
2198 return getComputedStyle(node)[commands[command].relevantCssProperty];
2199 }
2200
2201 function getSpecifiedCommandValue(element, command) {
2202 // "If command is "backColor" or "hiliteColor" and element's display
2203 // property does not have resolved value "inline", return null."
2204 if ((command == "backcolor" || command == "hilitecolor")
2205 && getComputedStyle(element).display != "inline") {
2206 return null;
2207 }
2208
2209 // "If command is "createLink" or "unlink":"
2210 if (command == "createlink" || command == "unlink") {
2211 // "If element is an a element and has an href attribute, return the
2212 // value of that attribute."
2213 if (isHtmlElement(element)
2214 && element.tagName == "A"
2215 && element.hasAttribute("href")) {
2216 return element.getAttribute("href");
2217 }
2218
2219 // "Return null."
2220 return null;
2221 }
2222
2223 // "If command is "subscript" or "superscript":"
2224 if (command == "subscript" || command == "superscript") {
2225 // "If element is a sup, return "superscript"."
2226 if (isHtmlElement(element, "sup")) {
2227 return "superscript";
2228 }
2229
2230 // "If element is a sub, return "subscript"."
2231 if (isHtmlElement(element, "sub")) {
2232 return "subscript";
2233 }
2234
2235 // "Return null."
2236 return null;
2237 }
2238
2239 // "If command is "strikethrough", and element has a style attribute set,
2240 // and that attribute sets "text-decoration":"
2241 if (command == "strikethrough"
2242 && element.style.textDecoration != "") {
2243 // "If element's style attribute sets "text-decoration" to a value
2244 // containing "line-through", return "line-through"."
2245 if (element.style.textDecoration.indexOf("line-through") != -1) {
2246 return "line-through";
2247 }
2248
2249 // "Return null."
2250 return null;
2251 }
2252
2253 // "If command is "strikethrough" and element is a s or strike element,
2254 // return "line-through"."
2255 if (command == "strikethrough"
2256 && isHtmlElement(element, ["S", "STRIKE"])) {
2257 return "line-through";
2258 }
2259
2260 // "If command is "underline", and element has a style attribute set, and
2261 // that attribute sets "text-decoration":"
2262 if (command == "underline"
2263 && element.style.textDecoration != "") {
2264 // "If element's style attribute sets "text-decoration" to a value
2265 // containing "underline", return "underline"."
2266 if (element.style.textDecoration.indexOf("underline") != -1) {
2267 return "underline";
2268 }
2269
2270 // "Return null."
2271 return null;
2272 }
2273
2274 // "If command is "underline" and element is a u element, return
2275 // "underline"."
2276 if (command == "underline"
2277 && isHtmlElement(element, "U")) {
2278 return "underline";
2279 }
2280
2281 // "Let property be the relevant CSS property for command."
2282 var property = commands[command].relevantCssProperty;
2283
2284 // "If property is null, return null."
2285 if (property === null) {
2286 return null;
2287 }
2288
2289 // "If element has a style attribute set, and that attribute has the
2290 // effect of setting property, return the value that it sets property to."
2291 if (element.style[property] != "") {
2292 return element.style[property];
2293 }
2294
2295 // "If element is a font element that has an attribute whose effect is
2296 // to create a presentational hint for property, return the value that the
2297 // hint sets property to. (For a size of 7, this will be the non-CSS value
2298 // "xxx-large".)"
2299 if (isHtmlNamespace(element.namespaceURI)
2300 && element.tagName == "FONT") {
2301 if (property == "color" && element.hasAttribute("color")) {
2302 return element.color;
2303 }
2304 if (property == "fontFamily" && element.hasAttribute("face")) {
2305 return element.face;
2306 }
2307 if (property == "fontSize" && element.hasAttribute("size")) {
2308 // This is not even close to correct in general.
2309 var size = parseInt(element.size);
2310 if (size < 1) {
2311 size = 1;
2312 }
2313 if (size > 7) {
2314 size = 7;
2315 }
2316 return {
2317 1: "x-small",
2318 2: "small",
2319 3: "medium",
2320 4: "large",
2321 5: "x-large",
2322 6: "xx-large",
2323 7: "xxx-large"
2324 }[size];
2325 }
2326 }
2327
2328 // "If element is in the following list, and property is equal to the
2329 // CSS property name listed for it, return the string listed for it."
2330 //
2331 // A list follows, whose meaning is copied here.
2332 if (property == "fontWeight"
2333 && (element.tagName == "B" || element.tagName == "STRONG")) {
2334 return "bold";
2335 }
2336 if (property == "fontStyle"
2337 && (element.tagName == "I" || element.tagName == "EM")) {
2338 return "italic";
2339 }
2340
2341 // "Return null."
2342 return null;
2343 }
2344
2345 function reorderModifiableDescendants(node, command, newValue) {
2346 // "Let candidate equal node."
2347 var candidate = node;
2348
2349 // "While candidate is a modifiable element, and candidate has exactly one
2350 // child, and that child is also a modifiable element, and candidate is not
2351 // a simple modifiable element or candidate's specified command value for
2352 // command is not equivalent to new value, set candidate to its child."
2353 while (isModifiableElement(candidate)
2354 && candidate.childNodes.length == 1
2355 && isModifiableElement(candidate.firstChild)
2356 && (!isSimpleModifiableElement(candidate)
2357 || !areEquivalentValues(command, getSpecifiedCommandValue(candidate, command ), newValue))) {
2358 candidate = candidate.firstChild;
2359 }
2360
2361 // "If candidate is node, or is not a simple modifiable element, or its
2362 // specified command value is not equivalent to new value, or its effective
2363 // command value is not loosely equivalent to new value, abort these
2364 // steps."
2365 if (candidate == node
2366 || !isSimpleModifiableElement(candidate)
2367 || !areEquivalentValues(command, getSpecifiedCommandValue(candidate, command ), newValue)
2368 || !areLooselyEquivalentValues(command, getEffectiveCommandValue(candidate, command), newValue)) {
2369 return;
2370 }
2371
2372 // "While candidate has children, insert the first child of candidate into
2373 // candidate's parent immediately before candidate, preserving ranges."
2374 while (candidate.hasChildNodes()) {
2375 movePreservingRanges(candidate.firstChild, candidate.parentNode, getNode Index(candidate));
2376 }
2377
2378 // "Insert candidate into node's parent immediately after node."
2379 node.parentNode.insertBefore(candidate, node.nextSibling);
2380
2381 // "Append the node as the last child of candidate, preserving ranges."
2382 movePreservingRanges(node, candidate, -1);
2383 }
2384
2385 function recordValues(nodeList) {
2386 // "Let values be a list of (node, command, specified command value)
2387 // triples, initially empty."
2388 var values = [];
2389
2390 // "For each node in node list, for each command in the list "subscript",
2391 // "bold", "fontName", "fontSize", "foreColor", "hiliteColor", "italic",
2392 // "strikethrough", and "underline" in that order:"
2393 nodeList.forEach(function(node) {
2394 ["subscript", "bold", "fontname", "fontsize", "forecolor",
2395 "hilitecolor", "italic", "strikethrough", "underline"].forEach(function( command) {
2396 // "Let ancestor equal node."
2397 var ancestor = node;
2398
2399 // "If ancestor is not an Element, set it to its parent."
2400 if (ancestor.nodeType != Node.ELEMENT_NODE) {
2401 ancestor = ancestor.parentNode;
2402 }
2403
2404 // "While ancestor is an Element and its specified command value
2405 // for command is null, set it to its parent."
2406 while (ancestor
2407 && ancestor.nodeType == Node.ELEMENT_NODE
2408 && getSpecifiedCommandValue(ancestor, command) === null) {
2409 ancestor = ancestor.parentNode;
2410 }
2411
2412 // "If ancestor is an Element, add (node, command, ancestor's
2413 // specified command value for command) to values. Otherwise add
2414 // (node, command, null) to values."
2415 if (ancestor && ancestor.nodeType == Node.ELEMENT_NODE) {
2416 values.push([node, command, getSpecifiedCommandValue(ancestor, c ommand)]);
2417 } else {
2418 values.push([node, command, null]);
2419 }
2420 });
2421 });
2422
2423 // "Return values."
2424 return values;
2425 }
2426
2427 function restoreValues(values) {
2428 // "For each (node, command, value) triple in values:"
2429 values.forEach(function(triple) {
2430 var node = triple[0];
2431 var command = triple[1];
2432 var value = triple[2];
2433
2434 // "Let ancestor equal node."
2435 var ancestor = node;
2436
2437 // "If ancestor is not an Element, set it to its parent."
2438 if (!ancestor || ancestor.nodeType != Node.ELEMENT_NODE) {
2439 ancestor = ancestor.parentNode;
2440 }
2441
2442 // "While ancestor is an Element and its specified command value for
2443 // command is null, set it to its parent."
2444 while (ancestor
2445 && ancestor.nodeType == Node.ELEMENT_NODE
2446 && getSpecifiedCommandValue(ancestor, command) === null) {
2447 ancestor = ancestor.parentNode;
2448 }
2449
2450 // "If value is null and ancestor is an Element, push down values on
2451 // node for command, with new value null."
2452 if (value === null
2453 && ancestor
2454 && ancestor.nodeType == Node.ELEMENT_NODE) {
2455 pushDownValues(node, command, null);
2456
2457 // "Otherwise, if ancestor is an Element and its specified command
2458 // value for command is not equivalent to value, or if ancestor is not
2459 // an Element and value is not null, force the value of command to
2460 // value on node."
2461 } else if ((ancestor
2462 && ancestor.nodeType == Node.ELEMENT_NODE
2463 && !areEquivalentValues(command, getSpecifiedCommandValue(ancestor, comm and), value))
2464 || ((!ancestor || ancestor.nodeType != Node.ELEMENT_NODE)
2465 && value !== null)) {
2466 forceValue(node, command, value);
2467 }
2468 });
2469 }
2470
2471
2472 //@}
2473 ///// Clearing an element's value /////
2474 //@{
2475
2476 function clearValue(element, command) {
2477 // "If element is not editable, return the empty list."
2478 if (!isEditable(element)) {
2479 return [];
2480 }
2481
2482 // "If element's specified command value for command is null, return the
2483 // empty list."
2484 if (getSpecifiedCommandValue(element, command) === null) {
2485 return [];
2486 }
2487
2488 // "If element is a simple modifiable element:"
2489 if (isSimpleModifiableElement(element)) {
2490 // "Let children be the children of element."
2491 var children = Array.prototype.slice.call(element.childNodes);
2492
2493 // "For each child in children, insert child into element's parent
2494 // immediately before element, preserving ranges."
2495 for (var i = 0; i < children.length; i++) {
2496 movePreservingRanges(children[i], element.parentNode, getNodeIndex(e lement));
2497 }
2498
2499 // "Remove element from its parent."
2500 element.parentNode.removeChild(element);
2501
2502 // "Return children."
2503 return children;
2504 }
2505
2506 // "If command is "strikethrough", and element has a style attribute that
2507 // sets "text-decoration" to some value containing "line-through", delete
2508 // "line-through" from the value."
2509 if (command == "strikethrough"
2510 && element.style.textDecoration.indexOf("line-through") != -1) {
2511 if (element.style.textDecoration == "line-through") {
2512 element.style.textDecoration = "";
2513 } else {
2514 element.style.textDecoration = element.style.textDecoration.replace( "line-through", "");
2515 }
2516 if (element.getAttribute("style") == "") {
2517 element.removeAttribute("style");
2518 }
2519 }
2520
2521 // "If command is "underline", and element has a style attribute that sets
2522 // "text-decoration" to some value containing "underline", delete
2523 // "underline" from the value."
2524 if (command == "underline"
2525 && element.style.textDecoration.indexOf("underline") != -1) {
2526 if (element.style.textDecoration == "underline") {
2527 element.style.textDecoration = "";
2528 } else {
2529 element.style.textDecoration = element.style.textDecoration.replace( "underline", "");
2530 }
2531 if (element.getAttribute("style") == "") {
2532 element.removeAttribute("style");
2533 }
2534 }
2535
2536 // "If the relevant CSS property for command is not null, unset the CSS
2537 // property property of element."
2538 if (commands[command].relevantCssProperty !== null) {
2539 element.style[commands[command].relevantCssProperty] = '';
2540 if (element.getAttribute("style") == "") {
2541 element.removeAttribute("style");
2542 }
2543 }
2544
2545 // "If element is a font element:"
2546 if (isHtmlNamespace(element.namespaceURI) && element.tagName == "FONT") {
2547 // "If command is "foreColor", unset element's color attribute, if set."
2548 if (command == "forecolor") {
2549 element.removeAttribute("color");
2550 }
2551
2552 // "If command is "fontName", unset element's face attribute, if set."
2553 if (command == "fontname") {
2554 element.removeAttribute("face");
2555 }
2556
2557 // "If command is "fontSize", unset element's size attribute, if set."
2558 if (command == "fontsize") {
2559 element.removeAttribute("size");
2560 }
2561 }
2562
2563 // "If element is an a element and command is "createLink" or "unlink",
2564 // unset the href property of element."
2565 if (isHtmlElement(element, "A")
2566 && (command == "createlink" || command == "unlink")) {
2567 element.removeAttribute("href");
2568 }
2569
2570 // "If element's specified command value for command is null, return the
2571 // empty list."
2572 if (getSpecifiedCommandValue(element, command) === null) {
2573 return [];
2574 }
2575
2576 // "Set the tag name of element to "span", and return the one-node list
2577 // consisting of the result."
2578 return [setTagName(element, "span")];
2579 }
2580
2581
2582 //@}
2583 ///// Pushing down values /////
2584 //@{
2585
2586 function pushDownValues(node, command, newValue) {
2587 // "If node's parent is not an Element, abort this algorithm."
2588 if (!node.parentNode
2589 || node.parentNode.nodeType != Node.ELEMENT_NODE) {
2590 return;
2591 }
2592
2593 // "If the effective command value of command is loosely equivalent to new
2594 // value on node, abort this algorithm."
2595 if (areLooselyEquivalentValues(command, getEffectiveCommandValue(node, comma nd), newValue)) {
2596 return;
2597 }
2598
2599 // "Let current ancestor be node's parent."
2600 var currentAncestor = node.parentNode;
2601
2602 // "Let ancestor list be a list of Nodes, initially empty."
2603 var ancestorList = [];
2604
2605 // "While current ancestor is an editable Element and the effective command
2606 // value of command is not loosely equivalent to new value on it, append
2607 // current ancestor to ancestor list, then set current ancestor to its
2608 // parent."
2609 while (isEditable(currentAncestor)
2610 && currentAncestor.nodeType == Node.ELEMENT_NODE
2611 && !areLooselyEquivalentValues(command, getEffectiveCommandValue(currentAnce stor, command), newValue)) {
2612 ancestorList.push(currentAncestor);
2613 currentAncestor = currentAncestor.parentNode;
2614 }
2615
2616 // "If ancestor list is empty, abort this algorithm."
2617 if (!ancestorList.length) {
2618 return;
2619 }
2620
2621 // "Let propagated value be the specified command value of command on the
2622 // last member of ancestor list."
2623 var propagatedValue = getSpecifiedCommandValue(ancestorList[ancestorList.len gth - 1], command);
2624
2625 // "If propagated value is null and is not equal to new value, abort this
2626 // algorithm."
2627 if (propagatedValue === null && propagatedValue != newValue) {
2628 return;
2629 }
2630
2631 // "If the effective command value for the parent of the last member of
2632 // ancestor list is not loosely equivalent to new value, and new value is
2633 // not null, abort this algorithm."
2634 if (newValue !== null
2635 && !areLooselyEquivalentValues(command, getEffectiveCommandValue(ancestorLis t[ancestorList.length - 1].parentNode, command), newValue)) {
2636 return;
2637 }
2638
2639 // "While ancestor list is not empty:"
2640 while (ancestorList.length) {
2641 // "Let current ancestor be the last member of ancestor list."
2642 // "Remove the last member from ancestor list."
2643 var currentAncestor = ancestorList.pop();
2644
2645 // "If the specified command value of current ancestor for command is
2646 // not null, set propagated value to that value."
2647 if (getSpecifiedCommandValue(currentAncestor, command) !== null) {
2648 propagatedValue = getSpecifiedCommandValue(currentAncestor, command) ;
2649 }
2650
2651 // "Let children be the children of current ancestor."
2652 var children = Array.prototype.slice.call(currentAncestor.childNodes);
2653
2654 // "If the specified command value of current ancestor for command is
2655 // not null, clear the value of current ancestor."
2656 if (getSpecifiedCommandValue(currentAncestor, command) !== null) {
2657 clearValue(currentAncestor, command);
2658 }
2659
2660 // "For every child in children:"
2661 for (var i = 0; i < children.length; i++) {
2662 var child = children[i];
2663
2664 // "If child is node, continue with the next child."
2665 if (child == node) {
2666 continue;
2667 }
2668
2669 // "If child is an Element whose specified command value for
2670 // command is neither null nor equivalent to propagated value,
2671 // continue with the next child."
2672 if (child.nodeType == Node.ELEMENT_NODE
2673 && getSpecifiedCommandValue(child, command) !== null
2674 && !areEquivalentValues(command, propagatedValue, getSpecifiedComman dValue(child, command))) {
2675 continue;
2676 }
2677
2678 // "If child is the last member of ancestor list, continue with the
2679 // next child."
2680 if (child == ancestorList[ancestorList.length - 1]) {
2681 continue;
2682 }
2683
2684 // "Force the value of child, with command as in this algorithm
2685 // and new value equal to propagated value."
2686 forceValue(child, command, propagatedValue);
2687 }
2688 }
2689 }
2690
2691
2692 //@}
2693 ///// Forcing the value of a node /////
2694 //@{
2695
2696 function forceValue(node, command, newValue) {
2697 // "If node's parent is null, abort this algorithm."
2698 if (!node.parentNode) {
2699 return;
2700 }
2701
2702 // "If new value is null, abort this algorithm."
2703 if (newValue === null) {
2704 return;
2705 }
2706
2707 // "If node is an allowed child of "span":"
2708 if (isAllowedChild(node, "span")) {
2709 // "Reorder modifiable descendants of node's previousSibling."
2710 reorderModifiableDescendants(node.previousSibling, command, newValue);
2711
2712 // "Reorder modifiable descendants of node's nextSibling."
2713 reorderModifiableDescendants(node.nextSibling, command, newValue);
2714
2715 // "Wrap the one-node list consisting of node, with sibling criteria
2716 // returning true for a simple modifiable element whose specified
2717 // command value is equivalent to new value and whose effective command
2718 // value is loosely equivalent to new value and false otherwise, and
2719 // with new parent instructions returning null."
2720 wrap([node],
2721 function(node) {
2722 return isSimpleModifiableElement(node)
2723 && areEquivalentValues(command, getSpecifiedCommandValue(nod e, command), newValue)
2724 && areLooselyEquivalentValues(command, getEffectiveCommandVa lue(node, command), newValue);
2725 },
2726 function() { return null }
2727 );
2728 }
2729
2730 // "If node is invisible, abort this algorithm."
2731 if (isInvisible(node)) {
2732 return;
2733 }
2734
2735 // "If the effective command value of command is loosely equivalent to new
2736 // value on node, abort this algorithm."
2737 if (areLooselyEquivalentValues(command, getEffectiveCommandValue(node, comma nd), newValue)) {
2738 return;
2739 }
2740
2741 // "If node is not an allowed child of "span":"
2742 if (!isAllowedChild(node, "span")) {
2743 // "Let children be all children of node, omitting any that are
2744 // Elements whose specified command value for command is neither null
2745 // nor equivalent to new value."
2746 var children = [];
2747 for (var i = 0; i < node.childNodes.length; i++) {
2748 if (node.childNodes[i].nodeType == Node.ELEMENT_NODE) {
2749 var specifiedValue = getSpecifiedCommandValue(node.childNodes[i] , command);
2750
2751 if (specifiedValue !== null
2752 && !areEquivalentValues(command, newValue, specifiedValue)) {
2753 continue;
2754 }
2755 }
2756 children.push(node.childNodes[i]);
2757 }
2758
2759 // "Force the value of each Node in children, with command and new
2760 // value as in this invocation of the algorithm."
2761 for (var i = 0; i < children.length; i++) {
2762 forceValue(children[i], command, newValue);
2763 }
2764
2765 // "Abort this algorithm."
2766 return;
2767 }
2768
2769 // "If the effective command value of command is loosely equivalent to new
2770 // value on node, abort this algorithm."
2771 if (areLooselyEquivalentValues(command, getEffectiveCommandValue(node, comma nd), newValue)) {
2772 return;
2773 }
2774
2775 // "Let new parent be null."
2776 var newParent = null;
2777
2778 // "If the CSS styling flag is false:"
2779 if (!cssStylingFlag) {
2780 // "If command is "bold" and new value is "bold", let new parent be the
2781 // result of calling createElement("b") on the ownerDocument of node."
2782 if (command == "bold" && (newValue == "bold" || newValue == "700")) {
2783 newParent = node.ownerDocument.createElement("b");
2784 }
2785
2786 // "If command is "italic" and new value is "italic", let new parent be
2787 // the result of calling createElement("i") on the ownerDocument of
2788 // node."
2789 if (command == "italic" && newValue == "italic") {
2790 newParent = node.ownerDocument.createElement("i");
2791 }
2792
2793 // "If command is "strikethrough" and new value is "line-through", let
2794 // new parent be the result of calling createElement("s") on the
2795 // ownerDocument of node."
2796 if (command == "strikethrough" && newValue == "line-through") {
2797 newParent = node.ownerDocument.createElement("s");
2798 }
2799
2800 // "If command is "underline" and new value is "underline", let new
2801 // parent be the result of calling createElement("u") on the
2802 // ownerDocument of node."
2803 if (command == "underline" && newValue == "underline") {
2804 newParent = node.ownerDocument.createElement("u");
2805 }
2806
2807 // "If command is "foreColor", and new value is fully opaque with red,
2808 // green, and blue components in the range 0 to 255:"
2809 if (command == "forecolor" && parseSimpleColor(newValue)) {
2810 // "Let new parent be the result of calling createElement("font")
2811 // on the ownerDocument of node."
2812 newParent = node.ownerDocument.createElement("font");
2813
2814 // "Set the color attribute of new parent to the result of applying
2815 // the rules for serializing simple color values to new value
2816 // (interpreted as a simple color)."
2817 newParent.setAttribute("color", parseSimpleColor(newValue));
2818 }
2819
2820 // "If command is "fontName", let new parent be the result of calling
2821 // createElement("font") on the ownerDocument of node, then set the
2822 // face attribute of new parent to new value."
2823 if (command == "fontname") {
2824 newParent = node.ownerDocument.createElement("font");
2825 newParent.face = newValue;
2826 }
2827 }
2828
2829 // "If command is "createLink" or "unlink":"
2830 if (command == "createlink" || command == "unlink") {
2831 // "Let new parent be the result of calling createElement("a") on the
2832 // ownerDocument of node."
2833 newParent = node.ownerDocument.createElement("a");
2834
2835 // "Set the href attribute of new parent to new value."
2836 newParent.setAttribute("href", newValue);
2837
2838 // "Let ancestor be node's parent."
2839 var ancestor = node.parentNode;
2840
2841 // "While ancestor is not null:"
2842 while (ancestor) {
2843 // "If ancestor is an a, set the tag name of ancestor to "span",
2844 // and let ancestor be the result."
2845 if (isHtmlElement(ancestor, "A")) {
2846 ancestor = setTagName(ancestor, "span");
2847 }
2848
2849 // "Set ancestor to its parent."
2850 ancestor = ancestor.parentNode;
2851 }
2852 }
2853
2854 // "If command is "fontSize"; and new value is one of "x-small", "small",
2855 // "medium", "large", "x-large", "xx-large", or "xxx-large"; and either the
2856 // CSS styling flag is false, or new value is "xxx-large": let new parent
2857 // be the result of calling createElement("font") on the ownerDocument of
2858 // node, then set the size attribute of new parent to the number from the
2859 // following table based on new value: [table omitted]"
2860 if (command == "fontsize"
2861 && ["x-small", "small", "medium", "large", "x-large", "xx-large", "xxx-large "].indexOf(newValue) != -1
2862 && (!cssStylingFlag || newValue == "xxx-large")) {
2863 newParent = node.ownerDocument.createElement("font");
2864 newParent.size = cssSizeToLegacy(newValue);
2865 }
2866
2867 // "If command is "subscript" or "superscript" and new value is
2868 // "subscript", let new parent be the result of calling
2869 // createElement("sub") on the ownerDocument of node."
2870 if ((command == "subscript" || command == "superscript")
2871 && newValue == "subscript") {
2872 newParent = node.ownerDocument.createElement("sub");
2873 }
2874
2875 // "If command is "subscript" or "superscript" and new value is
2876 // "superscript", let new parent be the result of calling
2877 // createElement("sup") on the ownerDocument of node."
2878 if ((command == "subscript" || command == "superscript")
2879 && newValue == "superscript") {
2880 newParent = node.ownerDocument.createElement("sup");
2881 }
2882
2883 // "If new parent is null, let new parent be the result of calling
2884 // createElement("span") on the ownerDocument of node."
2885 if (!newParent) {
2886 newParent = node.ownerDocument.createElement("span");
2887 }
2888
2889 // "Insert new parent in node's parent before node."
2890 node.parentNode.insertBefore(newParent, node);
2891
2892 // "If the effective command value of command for new parent is not loosely
2893 // equivalent to new value, and the relevant CSS property for command is
2894 // not null, set that CSS property of new parent to new value (if the new
2895 // value would be valid)."
2896 var property = commands[command].relevantCssProperty;
2897 if (property !== null
2898 && !areLooselyEquivalentValues(command, getEffectiveCommandValue(newParent, command), newValue)) {
2899 newParent.style[property] = newValue;
2900 }
2901
2902 // "If command is "strikethrough", and new value is "line-through", and the
2903 // effective command value of "strikethrough" for new parent is not
2904 // "line-through", set the "text-decoration" property of new parent to
2905 // "line-through"."
2906 if (command == "strikethrough"
2907 && newValue == "line-through"
2908 && getEffectiveCommandValue(newParent, "strikethrough") != "line-through") {
2909 newParent.style.textDecoration = "line-through";
2910 }
2911
2912 // "If command is "underline", and new value is "underline", and the
2913 // effective command value of "underline" for new parent is not
2914 // "underline", set the "text-decoration" property of new parent to
2915 // "underline"."
2916 if (command == "underline"
2917 && newValue == "underline"
2918 && getEffectiveCommandValue(newParent, "underline") != "underline") {
2919 newParent.style.textDecoration = "underline";
2920 }
2921
2922 // "Append node to new parent as its last child, preserving ranges."
2923 movePreservingRanges(node, newParent, newParent.childNodes.length);
2924
2925 // "If node is an Element and the effective command value of command for
2926 // node is not loosely equivalent to new value:"
2927 if (node.nodeType == Node.ELEMENT_NODE
2928 && !areEquivalentValues(command, getEffectiveCommandValue(node, command), ne wValue)) {
2929 // "Insert node into the parent of new parent before new parent,
2930 // preserving ranges."
2931 movePreservingRanges(node, newParent.parentNode, getNodeIndex(newParent) );
2932
2933 // "Remove new parent from its parent."
2934 newParent.parentNode.removeChild(newParent);
2935
2936 // "Let children be all children of node, omitting any that are
2937 // Elements whose specified command value for command is neither null
2938 // nor equivalent to new value."
2939 var children = [];
2940 for (var i = 0; i < node.childNodes.length; i++) {
2941 if (node.childNodes[i].nodeType == Node.ELEMENT_NODE) {
2942 var specifiedValue = getSpecifiedCommandValue(node.childNodes[i] , command);
2943
2944 if (specifiedValue !== null
2945 && !areEquivalentValues(command, newValue, specifiedValue)) {
2946 continue;
2947 }
2948 }
2949 children.push(node.childNodes[i]);
2950 }
2951
2952 // "Force the value of each Node in children, with command and new
2953 // value as in this invocation of the algorithm."
2954 for (var i = 0; i < children.length; i++) {
2955 forceValue(children[i], command, newValue);
2956 }
2957 }
2958 }
2959
2960
2961 //@}
2962 ///// Setting the selection's value /////
2963 //@{
2964
2965 function setSelectionValue(command, newValue) {
2966 // "If there is no formattable node effectively contained in the active
2967 // range:"
2968 if (!getAllEffectivelyContainedNodes(getActiveRange())
2969 .some(isFormattableNode)) {
2970 // "If command has inline command activated values, set the state
2971 // override to true if new value is among them and false if it's not."
2972 if ("inlineCommandActivatedValues" in commands[command]) {
2973 setStateOverride(command, commands[command].inlineCommandActivatedVa lues
2974 .indexOf(newValue) != -1);
2975 }
2976
2977 // "If command is "subscript", unset the state override for
2978 // "superscript"."
2979 if (command == "subscript") {
2980 unsetStateOverride("superscript");
2981 }
2982
2983 // "If command is "superscript", unset the state override for
2984 // "subscript"."
2985 if (command == "superscript") {
2986 unsetStateOverride("subscript");
2987 }
2988
2989 // "If new value is null, unset the value override (if any)."
2990 if (newValue === null) {
2991 unsetValueOverride(command);
2992
2993 // "Otherwise, if command is "createLink" or it has a value specified,
2994 // set the value override to new value."
2995 } else if (command == "createlink" || "value" in commands[command]) {
2996 setValueOverride(command, newValue);
2997 }
2998
2999 // "Abort these steps."
3000 return;
3001 }
3002
3003 // "If the active range's start node is an editable Text node, and its
3004 // start offset is neither zero nor its start node's length, call
3005 // splitText() on the active range's start node, with argument equal to the
3006 // active range's start offset. Then set the active range's start node to
3007 // the result, and its start offset to zero."
3008 if (isEditable(getActiveRange().startContainer)
3009 && getActiveRange().startContainer.nodeType == Node.TEXT_NODE
3010 && getActiveRange().startOffset != 0
3011 && getActiveRange().startOffset != getNodeLength(getActiveRange().startConta iner)) {
3012 // Account for browsers not following range mutation rules
3013 var newActiveRange = document.createRange();
3014 var newNode;
3015 if (getActiveRange().startContainer == getActiveRange().endContainer) {
3016 var newEndOffset = getActiveRange().endOffset - getActiveRange().sta rtOffset;
3017 newNode = getActiveRange().startContainer.splitText(getActiveRange() .startOffset);
3018 newActiveRange.setEnd(newNode, newEndOffset);
3019 getActiveRange().setEnd(newNode, newEndOffset);
3020 } else {
3021 newNode = getActiveRange().startContainer.splitText(getActiveRange() .startOffset);
3022 }
3023 newActiveRange.setStart(newNode, 0);
3024 getSelection().removeAllRanges();
3025 getSelection().addRange(newActiveRange);
3026
3027 getActiveRange().setStart(newNode, 0);
3028 }
3029
3030 // "If the active range's end node is an editable Text node, and its end
3031 // offset is neither zero nor its end node's length, call splitText() on
3032 // the active range's end node, with argument equal to the active range's
3033 // end offset."
3034 if (isEditable(getActiveRange().endContainer)
3035 && getActiveRange().endContainer.nodeType == Node.TEXT_NODE
3036 && getActiveRange().endOffset != 0
3037 && getActiveRange().endOffset != getNodeLength(getActiveRange().endContainer )) {
3038 // IE seems to mutate the range incorrectly here, so we need correction
3039 // here as well. The active range will be temporarily in orphaned
3040 // nodes, so calling getActiveRange() after splitText() but before
3041 // fixing the range will throw an exception.
3042 var activeRange = getActiveRange();
3043 var newStart = [activeRange.startContainer, activeRange.startOffset];
3044 var newEnd = [activeRange.endContainer, activeRange.endOffset];
3045 activeRange.endContainer.splitText(activeRange.endOffset);
3046 activeRange.setStart(newStart[0], newStart[1]);
3047 activeRange.setEnd(newEnd[0], newEnd[1]);
3048
3049 getSelection().removeAllRanges();
3050 getSelection().addRange(activeRange);
3051 }
3052
3053 // "Let element list be all editable Elements effectively contained in the
3054 // active range.
3055 //
3056 // "For each element in element list, clear the value of element."
3057 getAllEffectivelyContainedNodes(getActiveRange(), function(node) {
3058 return isEditable(node) && node.nodeType == Node.ELEMENT_NODE;
3059 }).forEach(function(element) {
3060 clearValue(element, command);
3061 });
3062
3063 // "Let node list be all editable nodes effectively contained in the active
3064 // range.
3065 //
3066 // "For each node in node list:"
3067 getAllEffectivelyContainedNodes(getActiveRange(), isEditable).forEach(functi on(node) {
3068 // "Push down values on node."
3069 pushDownValues(node, command, newValue);
3070
3071 // "If node is an allowed child of span, force the value of node."
3072 if (isAllowedChild(node, "span")) {
3073 forceValue(node, command, newValue);
3074 }
3075 });
3076 }
3077
3078
3079 //@}
3080 ///// The backColor command /////
3081 //@{
3082 commands.backcolor = {
3083 // Copy-pasted, same as hiliteColor
3084 action: function(value) {
3085 // Action is further copy-pasted, same as foreColor
3086
3087 // "If value is not a valid CSS color, prepend "#" to it."
3088 //
3089 // "If value is still not a valid CSS color, or if it is currentColor,
3090 // return false."
3091 //
3092 // Cheap hack for testing, no attempt to be comprehensive.
3093 if (/^([0-9a-fA-F]{3}){1,2}$/.test(value)) {
3094 value = "#" + value;
3095 }
3096 if (!/^(rgba?|hsla?)\(.*\)$/.test(value)
3097 && !parseSimpleColor(value)
3098 && value.toLowerCase() != "transparent") {
3099 return false;
3100 }
3101
3102 // "Set the selection's value to value."
3103 setSelectionValue("backcolor", value);
3104
3105 // "Return true."
3106 return true;
3107 }, standardInlineValueCommand: true, relevantCssProperty: "backgroundColor",
3108 equivalentValues: function(val1, val2) {
3109 // "Either both strings are valid CSS colors and have the same red,
3110 // green, blue, and alpha components, or neither string is a valid CSS
3111 // color."
3112 return normalizeColor(val1) === normalizeColor(val2);
3113 },
3114 };
3115
3116 //@}
3117 ///// The bold command /////
3118 //@{
3119 commands.bold = {
3120 action: function() {
3121 // "If queryCommandState("bold") returns true, set the selection's
3122 // value to "normal". Otherwise set the selection's value to "bold".
3123 // Either way, return true."
3124 if (myQueryCommandState("bold")) {
3125 setSelectionValue("bold", "normal");
3126 } else {
3127 setSelectionValue("bold", "bold");
3128 }
3129 return true;
3130 }, inlineCommandActivatedValues: ["bold", "600", "700", "800", "900"],
3131 relevantCssProperty: "fontWeight",
3132 equivalentValues: function(val1, val2) {
3133 // "Either the two strings are equal, or one is "bold" and the other is
3134 // "700", or one is "normal" and the other is "400"."
3135 return val1 == val2
3136 || (val1 == "bold" && val2 == "700")
3137 || (val1 == "700" && val2 == "bold")
3138 || (val1 == "normal" && val2 == "400")
3139 || (val1 == "400" && val2 == "normal");
3140 },
3141 };
3142
3143 //@}
3144 ///// The createLink command /////
3145 //@{
3146 commands.createlink = {
3147 action: function(value) {
3148 // "If value is the empty string, return false."
3149 if (value === "") {
3150 return false;
3151 }
3152
3153 // "For each editable a element that has an href attribute and is an
3154 // ancestor of some node effectively contained in the active range, set
3155 // that a element's href attribute to value."
3156 //
3157 // TODO: We don't actually do this in tree order, not that it matters
3158 // unless you're spying with mutation events.
3159 getAllEffectivelyContainedNodes(getActiveRange()).forEach(function(node) {
3160 getAncestors(node).forEach(function(ancestor) {
3161 if (isEditable(ancestor)
3162 && isHtmlElement(ancestor, "a")
3163 && ancestor.hasAttribute("href")) {
3164 ancestor.setAttribute("href", value);
3165 }
3166 });
3167 });
3168
3169 // "Set the selection's value to value."
3170 setSelectionValue("createlink", value);
3171
3172 // "Return true."
3173 return true;
3174 }
3175 };
3176
3177 //@}
3178 ///// The fontName command /////
3179 //@{
3180 commands.fontname = {
3181 action: function(value) {
3182 // "Set the selection's value to value, then return true."
3183 setSelectionValue("fontname", value);
3184 return true;
3185 }, standardInlineValueCommand: true, relevantCssProperty: "fontFamily"
3186 };
3187
3188 //@}
3189 ///// The fontSize command /////
3190 //@{
3191
3192 // Helper function for fontSize's action plus queryOutputHelper. It's just the
3193 // middle of fontSize's action, ripped out into its own function. Returns null
3194 // if the size is invalid.
3195 function normalizeFontSize(value) {
3196 // "Strip leading and trailing whitespace from value."
3197 //
3198 // Cheap hack, not following the actual algorithm.
3199 value = value.trim();
3200
3201 // "If value is not a valid floating point number, and would not be a valid
3202 // floating point number if a single leading "+" character were stripped,
3203 // return false."
3204 if (!/^[-+]?[0-9]+(\.[0-9]+)?([eE][-+]?[0-9]+)?$/.test(value)) {
3205 return null;
3206 }
3207
3208 var mode;
3209
3210 // "If the first character of value is "+", delete the character and let
3211 // mode be "relative-plus"."
3212 if (value[0] == "+") {
3213 value = value.slice(1);
3214 mode = "relative-plus";
3215 // "Otherwise, if the first character of value is "-", delete the character
3216 // and let mode be "relative-minus"."
3217 } else if (value[0] == "-") {
3218 value = value.slice(1);
3219 mode = "relative-minus";
3220 // "Otherwise, let mode be "absolute"."
3221 } else {
3222 mode = "absolute";
3223 }
3224
3225 // "Apply the rules for parsing non-negative integers to value, and let
3226 // number be the result."
3227 //
3228 // Another cheap hack.
3229 var num = parseInt(value);
3230
3231 // "If mode is "relative-plus", add three to number."
3232 if (mode == "relative-plus") {
3233 num += 3;
3234 }
3235
3236 // "If mode is "relative-minus", negate number, then add three to it."
3237 if (mode == "relative-minus") {
3238 num = 3 - num;
3239 }
3240
3241 // "If number is less than one, let number equal 1."
3242 if (num < 1) {
3243 num = 1;
3244 }
3245
3246 // "If number is greater than seven, let number equal 7."
3247 if (num > 7) {
3248 num = 7;
3249 }
3250
3251 // "Set value to the string here corresponding to number:" [table omitted]
3252 value = {
3253 1: "x-small",
3254 2: "small",
3255 3: "medium",
3256 4: "large",
3257 5: "x-large",
3258 6: "xx-large",
3259 7: "xxx-large"
3260 }[num];
3261
3262 return value;
3263 }
3264
3265 commands.fontsize = {
3266 action: function(value) {
3267 value = normalizeFontSize(value);
3268 if (value === null) {
3269 return false;
3270 }
3271
3272 // "Set the selection's value to value."
3273 setSelectionValue("fontsize", value);
3274
3275 // "Return true."
3276 return true;
3277 }, indeterm: function() {
3278 // "True if among formattable nodes that are effectively contained in
3279 // the active range, there are two that have distinct effective command
3280 // values. Otherwise false."
3281 return getAllEffectivelyContainedNodes(getActiveRange(), isFormattableNo de)
3282 .map(function(node) {
3283 return getEffectiveCommandValue(node, "fontsize");
3284 }).filter(function(value, i, arr) {
3285 return arr.slice(0, i).indexOf(value) == -1;
3286 }).length >= 2;
3287 }, value: function() {
3288 // "If the active range is null, return the empty string."
3289 if (!getActiveRange()) {
3290 return "";
3291 }
3292
3293 // "Let pixel size be the effective command value of the first
3294 // formattable node that is effectively contained in the active range,
3295 // or if there is no such node, the effective command value of the
3296 // active range's start node, in either case interpreted as a number of
3297 // pixels."
3298 var node = getAllEffectivelyContainedNodes(getActiveRange(), isFormattab leNode)[0];
3299 if (node === undefined) {
3300 node = getActiveRange().startContainer;
3301 }
3302 var pixelSize = getEffectiveCommandValue(node, "fontsize");
3303
3304 // "Return the legacy font size for pixel size."
3305 return getLegacyFontSize(pixelSize);
3306 }, relevantCssProperty: "fontSize"
3307 };
3308
3309 function getLegacyFontSize(size) {
3310 if (getLegacyFontSize.resultCache === undefined) {
3311 getLegacyFontSize.resultCache = {};
3312 }
3313
3314 if (getLegacyFontSize.resultCache[size] !== undefined) {
3315 return getLegacyFontSize.resultCache[size];
3316 }
3317
3318 // For convenience in other places in my code, I handle all sizes, not just
3319 // pixel sizes as the spec says. This means pixel sizes have to be passed
3320 // in suffixed with "px", not as plain numbers.
3321 if (normalizeFontSize(size) !== null) {
3322 return getLegacyFontSize.resultCache[size] = cssSizeToLegacy(normalizeFo ntSize(size));
3323 }
3324
3325 if (["x-small", "x-small", "small", "medium", "large", "x-large", "xx-large" , "xxx-large"].indexOf(size) == -1
3326 && !/^[0-9]+(\.[0-9]+)?(cm|mm|in|pt|pc|px)$/.test(size)) {
3327 // There is no sensible legacy size for things like "2em".
3328 return getLegacyFontSize.resultCache[size] = null;
3329 }
3330
3331 var font = document.createElement("font");
3332 document.body.appendChild(font);
3333 if (size == "xxx-large") {
3334 font.size = 7;
3335 } else {
3336 font.style.fontSize = size;
3337 }
3338 var pixelSize = parseInt(getComputedStyle(font).fontSize);
3339 document.body.removeChild(font);
3340
3341 // "Let returned size be 1."
3342 var returnedSize = 1;
3343
3344 // "While returned size is less than 7:"
3345 while (returnedSize < 7) {
3346 // "Let lower bound be the resolved value of "font-size" in pixels
3347 // of a font element whose size attribute is set to returned size."
3348 var font = document.createElement("font");
3349 font.size = returnedSize;
3350 document.body.appendChild(font);
3351 var lowerBound = parseInt(getComputedStyle(font).fontSize);
3352
3353 // "Let upper bound be the resolved value of "font-size" in pixels
3354 // of a font element whose size attribute is set to one plus
3355 // returned size."
3356 font.size = 1 + returnedSize;
3357 var upperBound = parseInt(getComputedStyle(font).fontSize);
3358 document.body.removeChild(font);
3359
3360 // "Let average be the average of upper bound and lower bound."
3361 var average = (upperBound + lowerBound)/2;
3362
3363 // "If pixel size is less than average, return the one-element
3364 // string consisting of the digit returned size."
3365 if (pixelSize < average) {
3366 return getLegacyFontSize.resultCache[size] = String(returnedSize);
3367 }
3368
3369 // "Add one to returned size."
3370 returnedSize++;
3371 }
3372
3373 // "Return "7"."
3374 return getLegacyFontSize.resultCache[size] = "7";
3375 }
3376
3377 //@}
3378 ///// The foreColor command /////
3379 //@{
3380 commands.forecolor = {
3381 action: function(value) {
3382 // Copy-pasted, same as backColor and hiliteColor
3383
3384 // "If value is not a valid CSS color, prepend "#" to it."
3385 //
3386 // "If value is still not a valid CSS color, or if it is currentColor,
3387 // return false."
3388 //
3389 // Cheap hack for testing, no attempt to be comprehensive.
3390 if (/^([0-9a-fA-F]{3}){1,2}$/.test(value)) {
3391 value = "#" + value;
3392 }
3393 if (!/^(rgba?|hsla?)\(.*\)$/.test(value)
3394 && !parseSimpleColor(value)
3395 && value.toLowerCase() != "transparent") {
3396 return false;
3397 }
3398
3399 // "Set the selection's value to value."
3400 setSelectionValue("forecolor", value);
3401
3402 // "Return true."
3403 return true;
3404 }, standardInlineValueCommand: true, relevantCssProperty: "color",
3405 equivalentValues: function(val1, val2) {
3406 // "Either both strings are valid CSS colors and have the same red,
3407 // green, blue, and alpha components, or neither string is a valid CSS
3408 // color."
3409 return normalizeColor(val1) === normalizeColor(val2);
3410 },
3411 };
3412
3413 //@}
3414 ///// The hiliteColor command /////
3415 //@{
3416 commands.hilitecolor = {
3417 // Copy-pasted, same as backColor
3418 action: function(value) {
3419 // Action is further copy-pasted, same as foreColor
3420
3421 // "If value is not a valid CSS color, prepend "#" to it."
3422 //
3423 // "If value is still not a valid CSS color, or if it is currentColor,
3424 // return false."
3425 //
3426 // Cheap hack for testing, no attempt to be comprehensive.
3427 if (/^([0-9a-fA-F]{3}){1,2}$/.test(value)) {
3428 value = "#" + value;
3429 }
3430 if (!/^(rgba?|hsla?)\(.*\)$/.test(value)
3431 && !parseSimpleColor(value)
3432 && value.toLowerCase() != "transparent") {
3433 return false;
3434 }
3435
3436 // "Set the selection's value to value."
3437 setSelectionValue("hilitecolor", value);
3438
3439 // "Return true."
3440 return true;
3441 }, indeterm: function() {
3442 // "True if among editable Text nodes that are effectively contained in
3443 // the active range, there are two that have distinct effective command
3444 // values. Otherwise false."
3445 return getAllEffectivelyContainedNodes(getActiveRange(), function(node) {
3446 return isEditable(node) && node.nodeType == Node.TEXT_NODE;
3447 }).map(function(node) {
3448 return getEffectiveCommandValue(node, "hilitecolor");
3449 }).filter(function(value, i, arr) {
3450 return arr.slice(0, i).indexOf(value) == -1;
3451 }).length >= 2;
3452 }, standardInlineValueCommand: true, relevantCssProperty: "backgroundColor",
3453 equivalentValues: function(val1, val2) {
3454 // "Either both strings are valid CSS colors and have the same red,
3455 // green, blue, and alpha components, or neither string is a valid CSS
3456 // color."
3457 return normalizeColor(val1) === normalizeColor(val2);
3458 },
3459 };
3460
3461 //@}
3462 ///// The italic command /////
3463 //@{
3464 commands.italic = {
3465 action: function() {
3466 // "If queryCommandState("italic") returns true, set the selection's
3467 // value to "normal". Otherwise set the selection's value to "italic".
3468 // Either way, return true."
3469 if (myQueryCommandState("italic")) {
3470 setSelectionValue("italic", "normal");
3471 } else {
3472 setSelectionValue("italic", "italic");
3473 }
3474 return true;
3475 }, inlineCommandActivatedValues: ["italic", "oblique"],
3476 relevantCssProperty: "fontStyle"
3477 };
3478
3479 //@}
3480 ///// The removeFormat command /////
3481 //@{
3482 commands.removeformat = {
3483 action: function() {
3484 // "A removeFormat candidate is an editable HTML element with local
3485 // name "abbr", "acronym", "b", "bdi", "bdo", "big", "blink", "cite",
3486 // "code", "dfn", "em", "font", "i", "ins", "kbd", "mark", "nobr", "q",
3487 // "s", "samp", "small", "span", "strike", "strong", "sub", "sup",
3488 // "tt", "u", or "var"."
3489 function isRemoveFormatCandidate(node) {
3490 return isEditable(node)
3491 && isHtmlElement(node, ["abbr", "acronym", "b", "bdi", "bdo",
3492 "big", "blink", "cite", "code", "dfn", "em", "font", "i",
3493 "ins", "kbd", "mark", "nobr", "q", "s", "samp", "small",
3494 "span", "strike", "strong", "sub", "sup", "tt", "u", "var"]);
3495 }
3496
3497 // "Let elements to remove be a list of every removeFormat candidate
3498 // effectively contained in the active range."
3499 var elementsToRemove = getAllEffectivelyContainedNodes(getActiveRange(), isRemoveFormatCandidate);
3500
3501 // "For each element in elements to remove:"
3502 elementsToRemove.forEach(function(element) {
3503 // "While element has children, insert the first child of element
3504 // into the parent of element immediately before element,
3505 // preserving ranges."
3506 while (element.hasChildNodes()) {
3507 movePreservingRanges(element.firstChild, element.parentNode, get NodeIndex(element));
3508 }
3509
3510 // "Remove element from its parent."
3511 element.parentNode.removeChild(element);
3512 });
3513
3514 // "If the active range's start node is an editable Text node, and its
3515 // start offset is neither zero nor its start node's length, call
3516 // splitText() on the active range's start node, with argument equal to
3517 // the active range's start offset. Then set the active range's start
3518 // node to the result, and its start offset to zero."
3519 if (isEditable(getActiveRange().startContainer)
3520 && getActiveRange().startContainer.nodeType == Node.TEXT_NODE
3521 && getActiveRange().startOffset != 0
3522 && getActiveRange().startOffset != getNodeLength(getActiveRange().startC ontainer)) {
3523 // Account for browsers not following range mutation rules
3524 if (getActiveRange().startContainer == getActiveRange().endContainer ) {
3525 var newEnd = getActiveRange().endOffset - getActiveRange().start Offset;
3526 var newNode = getActiveRange().startContainer.splitText(getActiv eRange().startOffset);
3527 getActiveRange().setStart(newNode, 0);
3528 getActiveRange().setEnd(newNode, newEnd);
3529 } else {
3530 getActiveRange().setStart(getActiveRange().startContainer.splitT ext(getActiveRange().startOffset), 0);
3531 }
3532 }
3533
3534 // "If the active range's end node is an editable Text node, and its
3535 // end offset is neither zero nor its end node's length, call
3536 // splitText() on the active range's end node, with argument equal to
3537 // the active range's end offset."
3538 if (isEditable(getActiveRange().endContainer)
3539 && getActiveRange().endContainer.nodeType == Node.TEXT_NODE
3540 && getActiveRange().endOffset != 0
3541 && getActiveRange().endOffset != getNodeLength(getActiveRange().endConta iner)) {
3542 // IE seems to mutate the range incorrectly here, so we need
3543 // correction here as well. Have to be careful to set the range to
3544 // something not including the text node so that getActiveRange()
3545 // doesn't throw an exception due to a temporarily detached
3546 // endpoint.
3547 var newStart = [getActiveRange().startContainer, getActiveRange().st artOffset];
3548 var newEnd = [getActiveRange().endContainer, getActiveRange().endOff set];
3549 getActiveRange().setEnd(document.documentElement, 0);
3550 newEnd[0].splitText(newEnd[1]);
3551 getActiveRange().setStart(newStart[0], newStart[1]);
3552 getActiveRange().setEnd(newEnd[0], newEnd[1]);
3553 }
3554
3555 // "Let node list consist of all editable nodes effectively contained
3556 // in the active range."
3557 //
3558 // "For each node in node list, while node's parent is a removeFormat
3559 // candidate in the same editing host as node, split the parent of the
3560 // one-node list consisting of node."
3561 getAllEffectivelyContainedNodes(getActiveRange(), isEditable).forEach(fu nction(node) {
3562 while (isRemoveFormatCandidate(node.parentNode)
3563 && inSameEditingHost(node.parentNode, node)) {
3564 splitParent([node]);
3565 }
3566 });
3567
3568 // "For each of the entries in the following list, in the given order,
3569 // set the selection's value to null, with command as given."
3570 [
3571 "subscript",
3572 "bold",
3573 "fontname",
3574 "fontsize",
3575 "forecolor",
3576 "hilitecolor",
3577 "italic",
3578 "strikethrough",
3579 "underline",
3580 ].forEach(function(command) {
3581 setSelectionValue(command, null);
3582 });
3583
3584 // "Return true."
3585 return true;
3586 }
3587 };
3588
3589 //@}
3590 ///// The strikethrough command /////
3591 //@{
3592 commands.strikethrough = {
3593 action: function() {
3594 // "If queryCommandState("strikethrough") returns true, set the
3595 // selection's value to null. Otherwise set the selection's value to
3596 // "line-through". Either way, return true."
3597 if (myQueryCommandState("strikethrough")) {
3598 setSelectionValue("strikethrough", null);
3599 } else {
3600 setSelectionValue("strikethrough", "line-through");
3601 }
3602 return true;
3603 }, inlineCommandActivatedValues: ["line-through"]
3604 };
3605
3606 //@}
3607 ///// The subscript command /////
3608 //@{
3609 commands.subscript = {
3610 action: function() {
3611 // "Call queryCommandState("subscript"), and let state be the result."
3612 var state = myQueryCommandState("subscript");
3613
3614 // "Set the selection's value to null."
3615 setSelectionValue("subscript", null);
3616
3617 // "If state is false, set the selection's value to "subscript"."
3618 if (!state) {
3619 setSelectionValue("subscript", "subscript");
3620 }
3621
3622 // "Return true."
3623 return true;
3624 }, indeterm: function() {
3625 // "True if either among formattable nodes that are effectively
3626 // contained in the active range, there is at least one with effective
3627 // command value "subscript" and at least one with some other effective
3628 // command value; or if there is some formattable node effectively
3629 // contained in the active range with effective command value "mixed".
3630 // Otherwise false."
3631 var nodes = getAllEffectivelyContainedNodes(getActiveRange(), isFormatta bleNode);
3632 return (nodes.some(function(node) { return getEffectiveCommandValue(node , "subscript") == "subscript" })
3633 && nodes.some(function(node) { return getEffectiveCommandValue(node, "subscript") != "subscript" }))
3634 || nodes.some(function(node) { return getEffectiveCommandValue(node, "subscript") == "mixed" });
3635 }, inlineCommandActivatedValues: ["subscript"],
3636 };
3637
3638 //@}
3639 ///// The superscript command /////
3640 //@{
3641 commands.superscript = {
3642 action: function() {
3643 // "Call queryCommandState("superscript"), and let state be the
3644 // result."
3645 var state = myQueryCommandState("superscript");
3646
3647 // "Set the selection's value to null."
3648 setSelectionValue("superscript", null);
3649
3650 // "If state is false, set the selection's value to "superscript"."
3651 if (!state) {
3652 setSelectionValue("superscript", "superscript");
3653 }
3654
3655 // "Return true."
3656 return true;
3657 }, indeterm: function() {
3658 // "True if either among formattable nodes that are effectively
3659 // contained in the active range, there is at least one with effective
3660 // command value "superscript" and at least one with some other
3661 // effective command value; or if there is some formattable node
3662 // effectively contained in the active range with effective command
3663 // value "mixed". Otherwise false."
3664 var nodes = getAllEffectivelyContainedNodes(getActiveRange(), isFormatta bleNode);
3665 return (nodes.some(function(node) { return getEffectiveCommandValue(node , "superscript") == "superscript" })
3666 && nodes.some(function(node) { return getEffectiveCommandValue(node, "superscript") != "superscript" }))
3667 || nodes.some(function(node) { return getEffectiveCommandValue(node, "superscript") == "mixed" });
3668 }, inlineCommandActivatedValues: ["superscript"],
3669 };
3670
3671 //@}
3672 ///// The underline command /////
3673 //@{
3674 commands.underline = {
3675 action: function() {
3676 // "If queryCommandState("underline") returns true, set the selection's
3677 // value to null. Otherwise set the selection's value to "underline".
3678 // Either way, return true."
3679 if (myQueryCommandState("underline")) {
3680 setSelectionValue("underline", null);
3681 } else {
3682 setSelectionValue("underline", "underline");
3683 }
3684 return true;
3685 }, inlineCommandActivatedValues: ["underline"]
3686 };
3687
3688 //@}
3689 ///// The unlink command /////
3690 //@{
3691 commands.unlink = {
3692 action: function() {
3693 // "Let hyperlinks be a list of every a element that has an href
3694 // attribute and is contained in the active range or is an ancestor of
3695 // one of its boundary points."
3696 //
3697 // As usual, take care to ensure it's tree order. The correctness of
3698 // the following is left as an exercise for the reader.
3699 var range = getActiveRange();
3700 var hyperlinks = [];
3701 for (
3702 var node = range.startContainer;
3703 node;
3704 node = node.parentNode
3705 ) {
3706 if (isHtmlElement(node, "A")
3707 && node.hasAttribute("href")) {
3708 hyperlinks.unshift(node);
3709 }
3710 }
3711 for (
3712 var node = range.startContainer;
3713 node != nextNodeDescendants(range.endContainer);
3714 node = nextNode(node)
3715 ) {
3716 if (isHtmlElement(node, "A")
3717 && node.hasAttribute("href")
3718 && (isContained(node, range)
3719 || isAncestor(node, range.endContainer)
3720 || node == range.endContainer)) {
3721 hyperlinks.push(node);
3722 }
3723 }
3724
3725 // "Clear the value of each member of hyperlinks."
3726 for (var i = 0; i < hyperlinks.length; i++) {
3727 clearValue(hyperlinks[i], "unlink");
3728 }
3729
3730 // "Return true."
3731 return true;
3732 }
3733 };
3734
3735 //@}
3736
3737 /////////////////////////////////////
3738 ///// Block formatting commands /////
3739 /////////////////////////////////////
3740
3741 ///// Block formatting command definitions /////
3742 //@{
3743
3744 // "An indentation element is either a blockquote, or a div that has a style
3745 // attribute that sets "margin" or some subproperty of it."
3746 function isIndentationElement(node) {
3747 if (!isHtmlElement(node)) {
3748 return false;
3749 }
3750
3751 if (node.tagName == "BLOCKQUOTE") {
3752 return true;
3753 }
3754
3755 if (node.tagName != "DIV") {
3756 return false;
3757 }
3758
3759 for (var i = 0; i < node.style.length; i++) {
3760 // Approximate check
3761 if (/^(-[a-z]+-)?margin/.test(node.style[i])) {
3762 return true;
3763 }
3764 }
3765
3766 return false;
3767 }
3768
3769 // "A simple indentation element is an indentation element that has no
3770 // attributes except possibly
3771 //
3772 // * "a style attribute that sets no properties other than "margin",
3773 // "border", "padding", or subproperties of those; and/or
3774 // * "a dir attribute."
3775 function isSimpleIndentationElement(node) {
3776 if (!isIndentationElement(node)) {
3777 return false;
3778 }
3779
3780 for (var i = 0; i < node.attributes.length; i++) {
3781 if (!isHtmlNamespace(node.attributes[i].namespaceURI)
3782 || ["style", "dir"].indexOf(node.attributes[i].name) == -1) {
3783 return false;
3784 }
3785 }
3786
3787 for (var i = 0; i < node.style.length; i++) {
3788 // This is approximate, but it works well enough for my purposes.
3789 if (!/^(-[a-z]+-)?(margin|border|padding)/.test(node.style[i])) {
3790 return false;
3791 }
3792 }
3793
3794 return true;
3795 }
3796
3797 // "A non-list single-line container is an HTML element with local name
3798 // "address", "div", "h1", "h2", "h3", "h4", "h5", "h6", "listing", "p", "pre",
3799 // or "xmp"."
3800 function isNonListSingleLineContainer(node) {
3801 return isHtmlElement(node, ["address", "div", "h1", "h2", "h3", "h4", "h5",
3802 "h6", "listing", "p", "pre", "xmp"]);
3803 }
3804
3805 // "A single-line container is either a non-list single-line container, or an
3806 // HTML element with local name "li", "dt", or "dd"."
3807 function isSingleLineContainer(node) {
3808 return isNonListSingleLineContainer(node)
3809 || isHtmlElement(node, ["li", "dt", "dd"]);
3810 }
3811
3812 function getBlockNodeOf(node) {
3813 // "While node is an inline node, set node to its parent."
3814 while (isInlineNode(node)) {
3815 node = node.parentNode;
3816 }
3817
3818 // "Return node."
3819 return node;
3820 }
3821
3822 //@}
3823 ///// Assorted block formatting command algorithms /////
3824 //@{
3825
3826 function fixDisallowedAncestors(node) {
3827 // "If node is not editable, abort these steps."
3828 if (!isEditable(node)) {
3829 return;
3830 }
3831
3832 // "If node is not an allowed child of any of its ancestors in the same
3833 // editing host:"
3834 if (getAncestors(node).every(function(ancestor) {
3835 return !inSameEditingHost(node, ancestor)
3836 || !isAllowedChild(node, ancestor)
3837 })) {
3838 // "If node is a dd or dt, wrap the one-node list consisting of node,
3839 // with sibling criteria returning true for any dl with no attributes
3840 // and false otherwise, and new parent instructions returning the
3841 // result of calling createElement("dl") on the context object. Then
3842 // abort these steps."
3843 if (isHtmlElement(node, ["dd", "dt"])) {
3844 wrap([node],
3845 function(sibling) { return isHtmlElement(sibling, "dl") && !sibl ing.attributes.length },
3846 function() { return document.createElement("dl") });
3847 return;
3848 }
3849
3850 // "If "p" is not an allowed child of the editing host of node, abort
3851 // these steps."
3852 if (!isAllowedChild("p", getEditingHostOf(node))) {
3853 return;
3854 }
3855
3856 // "If node is not a prohibited paragraph child, abort these steps."
3857 if (!isProhibitedParagraphChild(node)) {
3858 return;
3859 }
3860
3861 // "Set the tag name of node to the default single-line container name,
3862 // and let node be the result."
3863 node = setTagName(node, defaultSingleLineContainerName);
3864
3865 // "Fix disallowed ancestors of node."
3866 fixDisallowedAncestors(node);
3867
3868 // "Let children be node's children."
3869 var children = [].slice.call(node.childNodes);
3870
3871 // "For each child in children, if child is a prohibited paragraph
3872 // child:"
3873 children.filter(isProhibitedParagraphChild)
3874 .forEach(function(child) {
3875 // "Record the values of the one-node list consisting of child, and
3876 // let values be the result."
3877 var values = recordValues([child]);
3878
3879 // "Split the parent of the one-node list consisting of child."
3880 splitParent([child]);
3881
3882 // "Restore the values from values."
3883 restoreValues(values);
3884 });
3885
3886 // "Abort these steps."
3887 return;
3888 }
3889
3890 // "Record the values of the one-node list consisting of node, and let
3891 // values be the result."
3892 var values = recordValues([node]);
3893
3894 // "While node is not an allowed child of its parent, split the parent of
3895 // the one-node list consisting of node."
3896 while (!isAllowedChild(node, node.parentNode)) {
3897 splitParent([node]);
3898 }
3899
3900 // "Restore the values from values."
3901 restoreValues(values);
3902 }
3903
3904 function normalizeSublists(item) {
3905 // "If item is not an li or it is not editable or its parent is not
3906 // editable, abort these steps."
3907 if (!isHtmlElement(item, "LI")
3908 || !isEditable(item)
3909 || !isEditable(item.parentNode)) {
3910 return;
3911 }
3912
3913 // "Let new item be null."
3914 var newItem = null;
3915
3916 // "While item has an ol or ul child:"
3917 while ([].some.call(item.childNodes, function (node) { return isHtmlElement( node, ["OL", "UL"]) })) {
3918 // "Let child be the last child of item."
3919 var child = item.lastChild;
3920
3921 // "If child is an ol or ul, or new item is null and child is a Text
3922 // node whose data consists of zero of more space characters:"
3923 if (isHtmlElement(child, ["OL", "UL"])
3924 || (!newItem && child.nodeType == Node.TEXT_NODE && /^[ \t\n\f\r]*$/.tes t(child.data))) {
3925 // "Set new item to null."
3926 newItem = null;
3927
3928 // "Insert child into the parent of item immediately following
3929 // item, preserving ranges."
3930 movePreservingRanges(child, item.parentNode, 1 + getNodeIndex(item)) ;
3931
3932 // "Otherwise:"
3933 } else {
3934 // "If new item is null, let new item be the result of calling
3935 // createElement("li") on the ownerDocument of item, then insert
3936 // new item into the parent of item immediately after item."
3937 if (!newItem) {
3938 newItem = item.ownerDocument.createElement("li");
3939 item.parentNode.insertBefore(newItem, item.nextSibling);
3940 }
3941
3942 // "Insert child into new item as its first child, preserving
3943 // ranges."
3944 movePreservingRanges(child, newItem, 0);
3945 }
3946 }
3947 }
3948
3949 function getSelectionListState() {
3950 // "If the active range is null, return "none"."
3951 if (!getActiveRange()) {
3952 return "none";
3953 }
3954
3955 // "Block-extend the active range, and let new range be the result."
3956 var newRange = blockExtend(getActiveRange());
3957
3958 // "Let node list be a list of nodes, initially empty."
3959 //
3960 // "For each node contained in new range, append node to node list if the
3961 // last member of node list (if any) is not an ancestor of node; node is
3962 // editable; node is not an indentation element; and node is either an ol
3963 // or ul, or the child of an ol or ul, or an allowed child of "li"."
3964 var nodeList = getContainedNodes(newRange, function(node) {
3965 return isEditable(node)
3966 && !isIndentationElement(node)
3967 && (isHtmlElement(node, ["ol", "ul"])
3968 || isHtmlElement(node.parentNode, ["ol", "ul"])
3969 || isAllowedChild(node, "li"));
3970 });
3971
3972 // "If node list is empty, return "none"."
3973 if (!nodeList.length) {
3974 return "none";
3975 }
3976
3977 // "If every member of node list is either an ol or the child of an ol or
3978 // the child of an li child of an ol, and none is a ul or an ancestor of a
3979 // ul, return "ol"."
3980 if (nodeList.every(function(node) {
3981 return isHtmlElement(node, "ol")
3982 || isHtmlElement(node.parentNode, "ol")
3983 || (isHtmlElement(node.parentNode, "li") && isHtmlElement(node.paren tNode.parentNode, "ol"));
3984 })
3985 && !nodeList.some(function(node) { return isHtmlElement(node, "ul") || ("que rySelector" in node && node.querySelector("ul")) })) {
3986 return "ol";
3987 }
3988
3989 // "If every member of node list is either a ul or the child of a ul or the
3990 // child of an li child of a ul, and none is an ol or an ancestor of an ol,
3991 // return "ul"."
3992 if (nodeList.every(function(node) {
3993 return isHtmlElement(node, "ul")
3994 || isHtmlElement(node.parentNode, "ul")
3995 || (isHtmlElement(node.parentNode, "li") && isHtmlElement(node.paren tNode.parentNode, "ul"));
3996 })
3997 && !nodeList.some(function(node) { return isHtmlElement(node, "ol") || ("que rySelector" in node && node.querySelector("ol")) })) {
3998 return "ul";
3999 }
4000
4001 var hasOl = nodeList.some(function(node) {
4002 return isHtmlElement(node, "ol")
4003 || isHtmlElement(node.parentNode, "ol")
4004 || ("querySelector" in node && node.querySelector("ol"))
4005 || (isHtmlElement(node.parentNode, "li") && isHtmlElement(node.paren tNode.parentNode, "ol"));
4006 });
4007 var hasUl = nodeList.some(function(node) {
4008 return isHtmlElement(node, "ul")
4009 || isHtmlElement(node.parentNode, "ul")
4010 || ("querySelector" in node && node.querySelector("ul"))
4011 || (isHtmlElement(node.parentNode, "li") && isHtmlElement(node.paren tNode.parentNode, "ul"));
4012 });
4013 // "If some member of node list is either an ol or the child or ancestor of
4014 // an ol or the child of an li child of an ol, and some member of node list
4015 // is either a ul or the child or ancestor of a ul or the child of an li
4016 // child of a ul, return "mixed"."
4017 if (hasOl && hasUl) {
4018 return "mixed";
4019 }
4020
4021 // "If some member of node list is either an ol or the child or ancestor of
4022 // an ol or the child of an li child of an ol, return "mixed ol"."
4023 if (hasOl) {
4024 return "mixed ol";
4025 }
4026
4027 // "If some member of node list is either a ul or the child or ancestor of
4028 // a ul or the child of an li child of a ul, return "mixed ul"."
4029 if (hasUl) {
4030 return "mixed ul";
4031 }
4032
4033 // "Return "none"."
4034 return "none";
4035 }
4036
4037 function getAlignmentValue(node) {
4038 // "While node is neither null nor an Element, or it is an Element but its
4039 // "display" property has resolved value "inline" or "none", set node to
4040 // its parent."
4041 while ((node && node.nodeType != Node.ELEMENT_NODE)
4042 || (node.nodeType == Node.ELEMENT_NODE
4043 && ["inline", "none"].indexOf(getComputedStyle(node).display) != -1)) {
4044 node = node.parentNode;
4045 }
4046
4047 // "If node is not an Element, return "left"."
4048 if (!node || node.nodeType != Node.ELEMENT_NODE) {
4049 return "left";
4050 }
4051
4052 var resolvedValue = getComputedStyle(node).textAlign
4053 // Hack around browser non-standardness
4054 .replace(/^-(moz|webkit)-/, "")
4055 .replace(/^auto$/, "start");
4056
4057 // "If node's "text-align" property has resolved value "start", return
4058 // "left" if the directionality of node is "ltr", "right" if it is "rtl"."
4059 if (resolvedValue == "start") {
4060 return getDirectionality(node) == "ltr" ? "left" : "right";
4061 }
4062
4063 // "If node's "text-align" property has resolved value "end", return
4064 // "right" if the directionality of node is "ltr", "left" if it is "rtl"."
4065 if (resolvedValue == "end") {
4066 return getDirectionality(node) == "ltr" ? "right" : "left";
4067 }
4068
4069 // "If node's "text-align" property has resolved value "center", "justify",
4070 // "left", or "right", return that value."
4071 if (["center", "justify", "left", "right"].indexOf(resolvedValue) != -1) {
4072 return resolvedValue;
4073 }
4074
4075 // "Return "left"."
4076 return "left";
4077 }
4078
4079 function getNextEquivalentPoint(node, offset) {
4080 // "If node's length is zero, return null."
4081 if (getNodeLength(node) == 0) {
4082 return null;
4083 }
4084
4085 // "If offset is node's length, and node's parent is not null, and node is
4086 // an inline node, return (node's parent, 1 + node's index)."
4087 if (offset == getNodeLength(node)
4088 && node.parentNode
4089 && isInlineNode(node)) {
4090 return [node.parentNode, 1 + getNodeIndex(node)];
4091 }
4092
4093 // "If node has a child with index offset, and that child's length is not
4094 // zero, and that child is an inline node, return (that child, 0)."
4095 if (0 <= offset
4096 && offset < node.childNodes.length
4097 && getNodeLength(node.childNodes[offset]) != 0
4098 && isInlineNode(node.childNodes[offset])) {
4099 return [node.childNodes[offset], 0];
4100 }
4101
4102 // "Return null."
4103 return null;
4104 }
4105
4106 function getPreviousEquivalentPoint(node, offset) {
4107 // "If node's length is zero, return null."
4108 if (getNodeLength(node) == 0) {
4109 return null;
4110 }
4111
4112 // "If offset is 0, and node's parent is not null, and node is an inline
4113 // node, return (node's parent, node's index)."
4114 if (offset == 0
4115 && node.parentNode
4116 && isInlineNode(node)) {
4117 return [node.parentNode, getNodeIndex(node)];
4118 }
4119
4120 // "If node has a child with index offset − 1, and that child's length is
4121 // not zero, and that child is an inline node, return (that child, that
4122 // child's length)."
4123 if (0 <= offset - 1
4124 && offset - 1 < node.childNodes.length
4125 && getNodeLength(node.childNodes[offset - 1]) != 0
4126 && isInlineNode(node.childNodes[offset - 1])) {
4127 return [node.childNodes[offset - 1], getNodeLength(node.childNodes[offse t - 1])];
4128 }
4129
4130 // "Return null."
4131 return null;
4132 }
4133
4134 function getFirstEquivalentPoint(node, offset) {
4135 // "While (node, offset)'s previous equivalent point is not null, set
4136 // (node, offset) to its previous equivalent point."
4137 var prev;
4138 while (prev = getPreviousEquivalentPoint(node, offset)) {
4139 node = prev[0];
4140 offset = prev[1];
4141 }
4142
4143 // "Return (node, offset)."
4144 return [node, offset];
4145 }
4146
4147 function getLastEquivalentPoint(node, offset) {
4148 // "While (node, offset)'s next equivalent point is not null, set (node,
4149 // offset) to its next equivalent point."
4150 var next;
4151 while (next = getNextEquivalentPoint(node, offset)) {
4152 node = next[0];
4153 offset = next[1];
4154 }
4155
4156 // "Return (node, offset)."
4157 return [node, offset];
4158 }
4159
4160 //@}
4161 ///// Block-extending a range /////
4162 //@{
4163
4164 // "A boundary point (node, offset) is a block start point if either node's
4165 // parent is null and offset is zero; or node has a child with index offset −
4166 // 1, and that child is either a visible block node or a visible br."
4167 function isBlockStartPoint(node, offset) {
4168 return (!node.parentNode && offset == 0)
4169 || (0 <= offset - 1
4170 && offset - 1 < node.childNodes.length
4171 && isVisible(node.childNodes[offset - 1])
4172 && (isBlockNode(node.childNodes[offset - 1])
4173 || isHtmlElement(node.childNodes[offset - 1], "br")));
4174 }
4175
4176 // "A boundary point (node, offset) is a block end point if either node's
4177 // parent is null and offset is node's length; or node has a child with index
4178 // offset, and that child is a visible block node."
4179 function isBlockEndPoint(node, offset) {
4180 return (!node.parentNode && offset == getNodeLength(node))
4181 || (offset < node.childNodes.length
4182 && isVisible(node.childNodes[offset])
4183 && isBlockNode(node.childNodes[offset]));
4184 }
4185
4186 // "A boundary point is a block boundary point if it is either a block start
4187 // point or a block end point."
4188 function isBlockBoundaryPoint(node, offset) {
4189 return isBlockStartPoint(node, offset)
4190 || isBlockEndPoint(node, offset);
4191 }
4192
4193 function blockExtend(range) {
4194 // "Let start node, start offset, end node, and end offset be the start
4195 // and end nodes and offsets of the range."
4196 var startNode = range.startContainer;
4197 var startOffset = range.startOffset;
4198 var endNode = range.endContainer;
4199 var endOffset = range.endOffset;
4200
4201 // "If some ancestor container of start node is an li, set start offset to
4202 // the index of the last such li in tree order, and set start node to that
4203 // li's parent."
4204 var liAncestors = getAncestors(startNode).concat(startNode)
4205 .filter(function(ancestor) { return isHtmlElement(ancestor, "li") })
4206 .slice(-1);
4207 if (liAncestors.length) {
4208 startOffset = getNodeIndex(liAncestors[0]);
4209 startNode = liAncestors[0].parentNode;
4210 }
4211
4212 // "If (start node, start offset) is not a block start point, repeat the
4213 // following steps:"
4214 if (!isBlockStartPoint(startNode, startOffset)) do {
4215 // "If start offset is zero, set it to start node's index, then set
4216 // start node to its parent."
4217 if (startOffset == 0) {
4218 startOffset = getNodeIndex(startNode);
4219 startNode = startNode.parentNode;
4220
4221 // "Otherwise, subtract one from start offset."
4222 } else {
4223 startOffset--;
4224 }
4225
4226 // "If (start node, start offset) is a block boundary point, break from
4227 // this loop."
4228 } while (!isBlockBoundaryPoint(startNode, startOffset));
4229
4230 // "While start offset is zero and start node's parent is not null, set
4231 // start offset to start node's index, then set start node to its parent."
4232 while (startOffset == 0
4233 && startNode.parentNode) {
4234 startOffset = getNodeIndex(startNode);
4235 startNode = startNode.parentNode;
4236 }
4237
4238 // "If some ancestor container of end node is an li, set end offset to one
4239 // plus the index of the last such li in tree order, and set end node to
4240 // that li's parent."
4241 var liAncestors = getAncestors(endNode).concat(endNode)
4242 .filter(function(ancestor) { return isHtmlElement(ancestor, "li") })
4243 .slice(-1);
4244 if (liAncestors.length) {
4245 endOffset = 1 + getNodeIndex(liAncestors[0]);
4246 endNode = liAncestors[0].parentNode;
4247 }
4248
4249 // "If (end node, end offset) is not a block end point, repeat the
4250 // following steps:"
4251 if (!isBlockEndPoint(endNode, endOffset)) do {
4252 // "If end offset is end node's length, set it to one plus end node's
4253 // index, then set end node to its parent."
4254 if (endOffset == getNodeLength(endNode)) {
4255 endOffset = 1 + getNodeIndex(endNode);
4256 endNode = endNode.parentNode;
4257
4258 // "Otherwise, add one to end offset.
4259 } else {
4260 endOffset++;
4261 }
4262
4263 // "If (end node, end offset) is a block boundary point, break from
4264 // this loop."
4265 } while (!isBlockBoundaryPoint(endNode, endOffset));
4266
4267 // "While end offset is end node's length and end node's parent is not
4268 // null, set end offset to one plus end node's index, then set end node to
4269 // its parent."
4270 while (endOffset == getNodeLength(endNode)
4271 && endNode.parentNode) {
4272 endOffset = 1 + getNodeIndex(endNode);
4273 endNode = endNode.parentNode;
4274 }
4275
4276 // "Let new range be a new range whose start and end nodes and offsets
4277 // are start node, start offset, end node, and end offset."
4278 var newRange = startNode.ownerDocument.createRange();
4279 newRange.setStart(startNode, startOffset);
4280 newRange.setEnd(endNode, endOffset);
4281
4282 // "Return new range."
4283 return newRange;
4284 }
4285
4286 function followsLineBreak(node) {
4287 // "Let offset be zero."
4288 var offset = 0;
4289
4290 // "While (node, offset) is not a block boundary point:"
4291 while (!isBlockBoundaryPoint(node, offset)) {
4292 // "If node has a visible child with index offset minus one, return
4293 // false."
4294 if (0 <= offset - 1
4295 && offset - 1 < node.childNodes.length
4296 && isVisible(node.childNodes[offset - 1])) {
4297 return false;
4298 }
4299
4300 // "If offset is zero or node has no children, set offset to node's
4301 // index, then set node to its parent."
4302 if (offset == 0
4303 || !node.hasChildNodes()) {
4304 offset = getNodeIndex(node);
4305 node = node.parentNode;
4306
4307 // "Otherwise, set node to its child with index offset minus one, then
4308 // set offset to node's length."
4309 } else {
4310 node = node.childNodes[offset - 1];
4311 offset = getNodeLength(node);
4312 }
4313 }
4314
4315 // "Return true."
4316 return true;
4317 }
4318
4319 function precedesLineBreak(node) {
4320 // "Let offset be node's length."
4321 var offset = getNodeLength(node);
4322
4323 // "While (node, offset) is not a block boundary point:"
4324 while (!isBlockBoundaryPoint(node, offset)) {
4325 // "If node has a visible child with index offset, return false."
4326 if (offset < node.childNodes.length
4327 && isVisible(node.childNodes[offset])) {
4328 return false;
4329 }
4330
4331 // "If offset is node's length or node has no children, set offset to
4332 // one plus node's index, then set node to its parent."
4333 if (offset == getNodeLength(node)
4334 || !node.hasChildNodes()) {
4335 offset = 1 + getNodeIndex(node);
4336 node = node.parentNode;
4337
4338 // "Otherwise, set node to its child with index offset and set offset
4339 // to zero."
4340 } else {
4341 node = node.childNodes[offset];
4342 offset = 0;
4343 }
4344 }
4345
4346 // "Return true."
4347 return true;
4348 }
4349
4350 //@}
4351 ///// Recording and restoring overrides /////
4352 //@{
4353
4354 function recordCurrentOverrides() {
4355 // "Let overrides be a list of (string, string or boolean) ordered pairs,
4356 // initially empty."
4357 var overrides = [];
4358
4359 // "If there is a value override for "createLink", add ("createLink", value
4360 // override for "createLink") to overrides."
4361 if (getValueOverride("createlink") !== undefined) {
4362 overrides.push(["createlink", getValueOverride("createlink")]);
4363 }
4364
4365 // "For each command in the list "bold", "italic", "strikethrough",
4366 // "subscript", "superscript", "underline", in order: if there is a state
4367 // override for command, add (command, command's state override) to
4368 // overrides."
4369 ["bold", "italic", "strikethrough", "subscript", "superscript",
4370 "underline"].forEach(function(command) {
4371 if (getStateOverride(command) !== undefined) {
4372 overrides.push([command, getStateOverride(command)]);
4373 }
4374 });
4375
4376 // "For each command in the list "fontName", "fontSize", "foreColor",
4377 // "hiliteColor", in order: if there is a value override for command, add
4378 // (command, command's value override) to overrides."
4379 ["fontname", "fontsize", "forecolor",
4380 "hilitecolor"].forEach(function(command) {
4381 if (getValueOverride(command) !== undefined) {
4382 overrides.push([command, getValueOverride(command)]);
4383 }
4384 });
4385
4386 // "Return overrides."
4387 return overrides;
4388 }
4389
4390 function recordCurrentStatesAndValues() {
4391 // "Let overrides be a list of (string, string or boolean) ordered pairs,
4392 // initially empty."
4393 var overrides = [];
4394
4395 // "Let node be the first formattable node effectively contained in the
4396 // active range, or null if there is none."
4397 var node = getAllEffectivelyContainedNodes(getActiveRange())
4398 .filter(isFormattableNode)[0];
4399
4400 // "If node is null, return overrides."
4401 if (!node) {
4402 return overrides;
4403 }
4404
4405 // "Add ("createLink", node's effective command value for "createLink") to
4406 // overrides."
4407 overrides.push(["createlink", getEffectiveCommandValue(node, "createlink")]) ;
4408
4409 // "For each command in the list "bold", "italic", "strikethrough",
4410 // "subscript", "superscript", "underline", in order: if node's effective
4411 // command value for command is one of its inline command activated values,
4412 // add (command, true) to overrides, and otherwise add (command, false) to
4413 // overrides."
4414 ["bold", "italic", "strikethrough", "subscript", "superscript",
4415 "underline"].forEach(function(command) {
4416 if (commands[command].inlineCommandActivatedValues
4417 .indexOf(getEffectiveCommandValue(node, command)) != -1) {
4418 overrides.push([command, true]);
4419 } else {
4420 overrides.push([command, false]);
4421 }
4422 });
4423
4424 // "For each command in the list "fontName", "foreColor", "hiliteColor", in
4425 // order: add (command, command's value) to overrides."
4426 ["fontname", "fontsize", "forecolor", "hilitecolor"].forEach(function(comman d) {
4427 overrides.push([command, commands[command].value()]);
4428 });
4429
4430 // "Add ("fontSize", node's effective command value for "fontSize") to
4431 // overrides."
4432 overrides.push(["fontsize", getEffectiveCommandValue(node, "fontsize")]);
4433
4434 // "Return overrides."
4435 return overrides;
4436 }
4437
4438 function restoreStatesAndValues(overrides) {
4439 // "Let node be the first formattable node effectively contained in the
4440 // active range, or null if there is none."
4441 var node = getAllEffectivelyContainedNodes(getActiveRange())
4442 .filter(isFormattableNode)[0];
4443
4444 // "If node is not null, then for each (command, override) pair in
4445 // overrides, in order:"
4446 if (node) {
4447 for (var i = 0; i < overrides.length; i++) {
4448 var command = overrides[i][0];
4449 var override = overrides[i][1];
4450
4451 // "If override is a boolean, and queryCommandState(command)
4452 // returns something different from override, take the action for
4453 // command, with value equal to the empty string."
4454 if (typeof override == "boolean"
4455 && myQueryCommandState(command) != override) {
4456 commands[command].action("");
4457
4458 // "Otherwise, if override is a string, and command is neither
4459 // "createLink" nor "fontSize", and queryCommandValue(command)
4460 // returns something not equivalent to override, take the action
4461 // for command, with value equal to override."
4462 } else if (typeof override == "string"
4463 && command != "createlink"
4464 && command != "fontsize"
4465 && !areEquivalentValues(command, myQueryCommandValue(command), overr ide)) {
4466 commands[command].action(override);
4467
4468 // "Otherwise, if override is a string; and command is
4469 // "createLink"; and either there is a value override for
4470 // "createLink" that is not equal to override, or there is no value
4471 // override for "createLink" and node's effective command value for
4472 // "createLink" is not equal to override: take the action for
4473 // "createLink", with value equal to override."
4474 } else if (typeof override == "string"
4475 && command == "createlink"
4476 && (
4477 (
4478 getValueOverride("createlink") !== undefined
4479 && getValueOverride("createlink") !== override
4480 ) || (
4481 getValueOverride("createlink") === undefined
4482 && getEffectiveCommandValue(node, "createlink") !== override
4483 )
4484 )) {
4485 commands.createlink.action(override);
4486
4487 // "Otherwise, if override is a string; and command is "fontSize";
4488 // and either there is a value override for "fontSize" that is not
4489 // equal to override, or there is no value override for "fontSize"
4490 // and node's effective command value for "fontSize" is not loosely
4491 // equivalent to override:"
4492 } else if (typeof override == "string"
4493 && command == "fontsize"
4494 && (
4495 (
4496 getValueOverride("fontsize") !== undefined
4497 && getValueOverride("fontsize") !== override
4498 ) || (
4499 getValueOverride("fontsize") === undefined
4500 && !areLooselyEquivalentValues(command, getEffectiveCommandV alue(node, "fontsize"), override)
4501 )
4502 )) {
4503 // "Convert override to an integer number of pixels, and set
4504 // override to the legacy font size for the result."
4505 override = getLegacyFontSize(override);
4506
4507 // "Take the action for "fontSize", with value equal to
4508 // override."
4509 commands.fontsize.action(override);
4510
4511 // "Otherwise, continue this loop from the beginning."
4512 } else {
4513 continue;
4514 }
4515
4516 // "Set node to the first formattable node effectively contained in
4517 // the active range, if there is one."
4518 node = getAllEffectivelyContainedNodes(getActiveRange())
4519 .filter(isFormattableNode)[0]
4520 || node;
4521 }
4522
4523 // "Otherwise, for each (command, override) pair in overrides, in order:"
4524 } else {
4525 for (var i = 0; i < overrides.length; i++) {
4526 var command = overrides[i][0];
4527 var override = overrides[i][1];
4528
4529 // "If override is a boolean, set the state override for command to
4530 // override."
4531 if (typeof override == "boolean") {
4532 setStateOverride(command, override);
4533 }
4534
4535 // "If override is a string, set the value override for command to
4536 // override."
4537 if (typeof override == "string") {
4538 setValueOverride(command, override);
4539 }
4540 }
4541 }
4542 }
4543
4544 //@}
4545 ///// Deleting the selection /////
4546 //@{
4547
4548 // The flags argument is a dictionary that can have blockMerging,
4549 // stripWrappers, and/or direction as keys.
4550 function deleteSelection(flags) {
4551 if (flags === undefined) {
4552 flags = {};
4553 }
4554
4555 var blockMerging = "blockMerging" in flags ? Boolean(flags.blockMerging) : t rue;
4556 var stripWrappers = "stripWrappers" in flags ? Boolean(flags.stripWrappers) : true;
4557 var direction = "direction" in flags ? flags.direction : "forward";
4558
4559 // "If the active range is null, abort these steps and do nothing."
4560 if (!getActiveRange()) {
4561 return;
4562 }
4563
4564 // "Canonicalize whitespace at the active range's start."
4565 canonicalizeWhitespace(getActiveRange().startContainer, getActiveRange().sta rtOffset);
4566
4567 // "Canonicalize whitespace at the active range's end."
4568 canonicalizeWhitespace(getActiveRange().endContainer, getActiveRange().endOf fset);
4569
4570 // "Let (start node, start offset) be the last equivalent point for the
4571 // active range's start."
4572 var start = getLastEquivalentPoint(getActiveRange().startContainer, getActiv eRange().startOffset);
4573 var startNode = start[0];
4574 var startOffset = start[1];
4575
4576 // "Let (end node, end offset) be the first equivalent point for the active
4577 // range's end."
4578 var end = getFirstEquivalentPoint(getActiveRange().endContainer, getActiveRa nge().endOffset);
4579 var endNode = end[0];
4580 var endOffset = end[1];
4581
4582 // "If (end node, end offset) is not after (start node, start offset):"
4583 if (getPosition(endNode, endOffset, startNode, startOffset) !== "after") {
4584 // "If direction is "forward", call collapseToStart() on the context
4585 // object's Selection."
4586 //
4587 // Here and in a few other places, we check rangeCount to work around a
4588 // WebKit bug: it will sometimes incorrectly remove ranges from the
4589 // selection if nodes are removed, so collapseToStart() will throw.
4590 // This will break everything if we're using an actual selection, but
4591 // if getActiveRange() is really just returning globalRange and that's
4592 // all we care about, it will work fine. I only add the extra check
4593 // for errors I actually hit in testing.
4594 if (direction == "forward") {
4595 if (getSelection().rangeCount) {
4596 getSelection().collapseToStart();
4597 }
4598 getActiveRange().collapse(true);
4599
4600 // "Otherwise, call collapseToEnd() on the context object's Selection."
4601 } else {
4602 getSelection().collapseToEnd();
4603 getActiveRange().collapse(false);
4604 }
4605
4606 // "Abort these steps."
4607 return;
4608 }
4609
4610 // "If start node is a Text node and start offset is 0, set start offset to
4611 // the index of start node, then set start node to its parent."
4612 if (startNode.nodeType == Node.TEXT_NODE
4613 && startOffset == 0) {
4614 startOffset = getNodeIndex(startNode);
4615 startNode = startNode.parentNode;
4616 }
4617
4618 // "If end node is a Text node and end offset is its length, set end offset
4619 // to one plus the index of end node, then set end node to its parent."
4620 if (endNode.nodeType == Node.TEXT_NODE
4621 && endOffset == getNodeLength(endNode)) {
4622 endOffset = 1 + getNodeIndex(endNode);
4623 endNode = endNode.parentNode;
4624 }
4625
4626 // "Call collapse(start node, start offset) on the context object's
4627 // Selection."
4628 getSelection().collapse(startNode, startOffset);
4629 getActiveRange().setStart(startNode, startOffset);
4630
4631 // "Call extend(end node, end offset) on the context object's Selection."
4632 getSelection().extend(endNode, endOffset);
4633 getActiveRange().setEnd(endNode, endOffset);
4634
4635 // "Let start block be the active range's start node."
4636 var startBlock = getActiveRange().startContainer;
4637
4638 // "While start block's parent is in the same editing host and start block
4639 // is an inline node, set start block to its parent."
4640 while (inSameEditingHost(startBlock, startBlock.parentNode)
4641 && isInlineNode(startBlock)) {
4642 startBlock = startBlock.parentNode;
4643 }
4644
4645 // "If start block is neither a block node nor an editing host, or "span"
4646 // is not an allowed child of start block, or start block is a td or th,
4647 // set start block to null."
4648 if ((!isBlockNode(startBlock) && !isEditingHost(startBlock))
4649 || !isAllowedChild("span", startBlock)
4650 || isHtmlElement(startBlock, ["td", "th"])) {
4651 startBlock = null;
4652 }
4653
4654 // "Let end block be the active range's end node."
4655 var endBlock = getActiveRange().endContainer;
4656
4657 // "While end block's parent is in the same editing host and end block is
4658 // an inline node, set end block to its parent."
4659 while (inSameEditingHost(endBlock, endBlock.parentNode)
4660 && isInlineNode(endBlock)) {
4661 endBlock = endBlock.parentNode;
4662 }
4663
4664 // "If end block is neither a block node nor an editing host, or "span" is
4665 // not an allowed child of end block, or end block is a td or th, set end
4666 // block to null."
4667 if ((!isBlockNode(endBlock) && !isEditingHost(endBlock))
4668 || !isAllowedChild("span", endBlock)
4669 || isHtmlElement(endBlock, ["td", "th"])) {
4670 endBlock = null;
4671 }
4672
4673 // "Record current states and values, and let overrides be the result."
4674 var overrides = recordCurrentStatesAndValues();
4675
4676 // "If start node and end node are the same, and start node is an editable
4677 // Text node:"
4678 if (startNode == endNode
4679 && isEditable(startNode)
4680 && startNode.nodeType == Node.TEXT_NODE) {
4681 // "Call deleteData(start offset, end offset − start offset) on start
4682 // node."
4683 startNode.deleteData(startOffset, endOffset - startOffset);
4684
4685 // "Canonicalize whitespace at (start node, start offset), with fix
4686 // collapsed space false."
4687 canonicalizeWhitespace(startNode, startOffset, false);
4688
4689 // "If direction is "forward", call collapseToStart() on the context
4690 // object's Selection."
4691 if (direction == "forward") {
4692 if (getSelection().rangeCount) {
4693 getSelection().collapseToStart();
4694 }
4695 getActiveRange().collapse(true);
4696
4697 // "Otherwise, call collapseToEnd() on the context object's Selection."
4698 } else {
4699 getSelection().collapseToEnd();
4700 getActiveRange().collapse(false);
4701 }
4702
4703 // "Restore states and values from overrides."
4704 restoreStatesAndValues(overrides);
4705
4706 // "Abort these steps."
4707 return;
4708 }
4709
4710 // "If start node is an editable Text node, call deleteData() on it, with
4711 // start offset as the first argument and (length of start node − start
4712 // offset) as the second argument."
4713 if (isEditable(startNode)
4714 && startNode.nodeType == Node.TEXT_NODE) {
4715 startNode.deleteData(startOffset, getNodeLength(startNode) - startOffset );
4716 }
4717
4718 // "Let node list be a list of nodes, initially empty."
4719 //
4720 // "For each node contained in the active range, append node to node list
4721 // if the last member of node list (if any) is not an ancestor of node;
4722 // node is editable; and node is not a thead, tbody, tfoot, tr, th, or td."
4723 var nodeList = getContainedNodes(getActiveRange(),
4724 function(node) {
4725 return isEditable(node)
4726 && !isHtmlElement(node, ["thead", "tbody", "tfoot", "tr", "th", "td"]);
4727 }
4728 );
4729
4730 // "For each node in node list:"
4731 for (var i = 0; i < nodeList.length; i++) {
4732 var node = nodeList[i];
4733
4734 // "Let parent be the parent of node."
4735 var parent_ = node.parentNode;
4736
4737 // "Remove node from parent."
4738 parent_.removeChild(node);
4739
4740 // "If the block node of parent has no visible children, and parent is
4741 // editable or an editing host, call createElement("br") on the context
4742 // object and append the result as the last child of parent."
4743 if (![].some.call(getBlockNodeOf(parent_).childNodes, isVisible)
4744 && (isEditable(parent_) || isEditingHost(parent_))) {
4745 parent_.appendChild(document.createElement("br"));
4746 }
4747
4748 // "If strip wrappers is true or parent is not an ancestor container of
4749 // start node, while parent is an editable inline node with length 0,
4750 // let grandparent be the parent of parent, then remove parent from
4751 // grandparent, then set parent to grandparent."
4752 if (stripWrappers
4753 || (!isAncestor(parent_, startNode) && parent_ != startNode)) {
4754 while (isEditable(parent_)
4755 && isInlineNode(parent_)
4756 && getNodeLength(parent_) == 0) {
4757 var grandparent = parent_.parentNode;
4758 grandparent.removeChild(parent_);
4759 parent_ = grandparent;
4760 }
4761 }
4762 }
4763
4764 // "If end node is an editable Text node, call deleteData(0, end offset) on
4765 // it."
4766 if (isEditable(endNode)
4767 && endNode.nodeType == Node.TEXT_NODE) {
4768 endNode.deleteData(0, endOffset);
4769 }
4770
4771 // "Canonicalize whitespace at the active range's start, with fix collapsed
4772 // space false."
4773 canonicalizeWhitespace(getActiveRange().startContainer, getActiveRange().sta rtOffset, false);
4774
4775 // "Canonicalize whitespace at the active range's end, with fix collapsed
4776 // space false."
4777 canonicalizeWhitespace(getActiveRange().endContainer, getActiveRange().endOf fset, false);
4778
4779 // "If block merging is false, or start block or end block is null, or
4780 // start block is not in the same editing host as end block, or start block
4781 // and end block are the same:"
4782 if (!blockMerging
4783 || !startBlock
4784 || !endBlock
4785 || !inSameEditingHost(startBlock, endBlock)
4786 || startBlock == endBlock) {
4787 // "If direction is "forward", call collapseToStart() on the context
4788 // object's Selection."
4789 if (direction == "forward") {
4790 if (getSelection().rangeCount) {
4791 getSelection().collapseToStart();
4792 }
4793 getActiveRange().collapse(true);
4794
4795 // "Otherwise, call collapseToEnd() on the context object's Selection."
4796 } else {
4797 if (getSelection().rangeCount) {
4798 getSelection().collapseToEnd();
4799 }
4800 getActiveRange().collapse(false);
4801 }
4802
4803 // "Restore states and values from overrides."
4804 restoreStatesAndValues(overrides);
4805
4806 // "Abort these steps."
4807 return;
4808 }
4809
4810 // "If start block has one child, which is a collapsed block prop, remove
4811 // its child from it."
4812 if (startBlock.children.length == 1
4813 && isCollapsedBlockProp(startBlock.firstChild)) {
4814 startBlock.removeChild(startBlock.firstChild);
4815 }
4816
4817 // "If start block is an ancestor of end block:"
4818 if (isAncestor(startBlock, endBlock)) {
4819 // "Let reference node be end block."
4820 var referenceNode = endBlock;
4821
4822 // "While reference node is not a child of start block, set reference
4823 // node to its parent."
4824 while (referenceNode.parentNode != startBlock) {
4825 referenceNode = referenceNode.parentNode;
4826 }
4827
4828 // "Call collapse() on the context object's Selection, with first
4829 // argument start block and second argument the index of reference
4830 // node."
4831 getSelection().collapse(startBlock, getNodeIndex(referenceNode));
4832 getActiveRange().setStart(startBlock, getNodeIndex(referenceNode));
4833 getActiveRange().collapse(true);
4834
4835 // "If end block has no children:"
4836 if (!endBlock.hasChildNodes()) {
4837 // "While end block is editable and is the only child of its parent
4838 // and is not a child of start block, let parent equal end block,
4839 // then remove end block from parent, then set end block to
4840 // parent."
4841 while (isEditable(endBlock)
4842 && endBlock.parentNode.childNodes.length == 1
4843 && endBlock.parentNode != startBlock) {
4844 var parent_ = endBlock;
4845 parent_.removeChild(endBlock);
4846 endBlock = parent_;
4847 }
4848
4849 // "If end block is editable and is not an inline node, and its
4850 // previousSibling and nextSibling are both inline nodes, call
4851 // createElement("br") on the context object and insert it into end
4852 // block's parent immediately after end block."
4853 if (isEditable(endBlock)
4854 && !isInlineNode(endBlock)
4855 && isInlineNode(endBlock.previousSibling)
4856 && isInlineNode(endBlock.nextSibling)) {
4857 endBlock.parentNode.insertBefore(document.createElement("br"), e ndBlock.nextSibling);
4858 }
4859
4860 // "If end block is editable, remove it from its parent."
4861 if (isEditable(endBlock)) {
4862 endBlock.parentNode.removeChild(endBlock);
4863 }
4864
4865 // "Restore states and values from overrides."
4866 restoreStatesAndValues(overrides);
4867
4868 // "Abort these steps."
4869 return;
4870 }
4871
4872 // "If end block's firstChild is not an inline node, restore states and
4873 // values from overrides, then abort these steps."
4874 if (!isInlineNode(endBlock.firstChild)) {
4875 restoreStatesAndValues(overrides);
4876 return;
4877 }
4878
4879 // "Let children be a list of nodes, initially empty."
4880 var children = [];
4881
4882 // "Append the first child of end block to children."
4883 children.push(endBlock.firstChild);
4884
4885 // "While children's last member is not a br, and children's last
4886 // member's nextSibling is an inline node, append children's last
4887 // member's nextSibling to children."
4888 while (!isHtmlElement(children[children.length - 1], "br")
4889 && isInlineNode(children[children.length - 1].nextSibling)) {
4890 children.push(children[children.length - 1].nextSibling);
4891 }
4892
4893 // "Record the values of children, and let values be the result."
4894 var values = recordValues(children);
4895
4896 // "While children's first member's parent is not start block, split
4897 // the parent of children."
4898 while (children[0].parentNode != startBlock) {
4899 splitParent(children);
4900 }
4901
4902 // "If children's first member's previousSibling is an editable br,
4903 // remove that br from its parent."
4904 if (isEditable(children[0].previousSibling)
4905 && isHtmlElement(children[0].previousSibling, "br")) {
4906 children[0].parentNode.removeChild(children[0].previousSibling);
4907 }
4908
4909 // "Otherwise, if start block is a descendant of end block:"
4910 } else if (isDescendant(startBlock, endBlock)) {
4911 // "Call collapse() on the context object's Selection, with first
4912 // argument start block and second argument start block's length."
4913 getSelection().collapse(startBlock, getNodeLength(startBlock));
4914 getActiveRange().setStart(startBlock, getNodeLength(startBlock));
4915 getActiveRange().collapse(true);
4916
4917 // "Let reference node be start block."
4918 var referenceNode = startBlock;
4919
4920 // "While reference node is not a child of end block, set reference
4921 // node to its parent."
4922 while (referenceNode.parentNode != endBlock) {
4923 referenceNode = referenceNode.parentNode;
4924 }
4925
4926 // "If reference node's nextSibling is an inline node and start block's
4927 // lastChild is a br, remove start block's lastChild from it."
4928 if (isInlineNode(referenceNode.nextSibling)
4929 && isHtmlElement(startBlock.lastChild, "br")) {
4930 startBlock.removeChild(startBlock.lastChild);
4931 }
4932
4933 // "Let nodes to move be a list of nodes, initially empty."
4934 var nodesToMove = [];
4935
4936 // "If reference node's nextSibling is neither null nor a block node,
4937 // append it to nodes to move."
4938 if (referenceNode.nextSibling
4939 && !isBlockNode(referenceNode.nextSibling)) {
4940 nodesToMove.push(referenceNode.nextSibling);
4941 }
4942
4943 // "While nodes to move is nonempty and its last member isn't a br and
4944 // its last member's nextSibling is neither null nor a block node,
4945 // append its last member's nextSibling to nodes to move."
4946 if (nodesToMove.length
4947 && !isHtmlElement(nodesToMove[nodesToMove.length - 1], "br")
4948 && nodesToMove[nodesToMove.length - 1].nextSibling
4949 && !isBlockNode(nodesToMove[nodesToMove.length - 1].nextSibling)) {
4950 nodesToMove.push(nodesToMove[nodesToMove.length - 1].nextSibling);
4951 }
4952
4953 // "Record the values of nodes to move, and let values be the result."
4954 var values = recordValues(nodesToMove);
4955
4956 // "For each node in nodes to move, append node as the last child of
4957 // start block, preserving ranges."
4958 nodesToMove.forEach(function(node) {
4959 movePreservingRanges(node, startBlock, -1);
4960 });
4961
4962 // "Otherwise:"
4963 } else {
4964 // "Call collapse() on the context object's Selection, with first
4965 // argument start block and second argument start block's length."
4966 getSelection().collapse(startBlock, getNodeLength(startBlock));
4967 getActiveRange().setStart(startBlock, getNodeLength(startBlock));
4968 getActiveRange().collapse(true);
4969
4970 // "If end block's firstChild is an inline node and start block's
4971 // lastChild is a br, remove start block's lastChild from it."
4972 if (isInlineNode(endBlock.firstChild)
4973 && isHtmlElement(startBlock.lastChild, "br")) {
4974 startBlock.removeChild(startBlock.lastChild);
4975 }
4976
4977 // "Record the values of end block's children, and let values be the
4978 // result."
4979 var values = recordValues([].slice.call(endBlock.childNodes));
4980
4981 // "While end block has children, append the first child of end block
4982 // to start block, preserving ranges."
4983 while (endBlock.hasChildNodes()) {
4984 movePreservingRanges(endBlock.firstChild, startBlock, -1);
4985 }
4986
4987 // "While end block has no children, let parent be the parent of end
4988 // block, then remove end block from parent, then set end block to
4989 // parent."
4990 while (!endBlock.hasChildNodes()) {
4991 var parent_ = endBlock.parentNode;
4992 parent_.removeChild(endBlock);
4993 endBlock = parent_;
4994 }
4995 }
4996
4997 // "Let ancestor be start block."
4998 var ancestor = startBlock;
4999
5000 // "While ancestor has an inclusive ancestor ol in the same editing host
5001 // whose nextSibling is also an ol in the same editing host, or an
5002 // inclusive ancestor ul in the same editing host whose nextSibling is also
5003 // a ul in the same editing host:"
5004 while (getInclusiveAncestors(ancestor).some(function(node) {
5005 return inSameEditingHost(ancestor, node)
5006 && (
5007 (isHtmlElement(node, "ol") && isHtmlElement(node.nextSibling, "o l"))
5008 || (isHtmlElement(node, "ul") && isHtmlElement(node.nextSibling, "ul"))
5009 ) && inSameEditingHost(ancestor, node.nextSibling);
5010 })) {
5011 // "While ancestor and its nextSibling are not both ols in the same
5012 // editing host, and are also not both uls in the same editing host,
5013 // set ancestor to its parent."
5014 while (!(
5015 isHtmlElement(ancestor, "ol")
5016 && isHtmlElement(ancestor.nextSibling, "ol")
5017 && inSameEditingHost(ancestor, ancestor.nextSibling)
5018 ) && !(
5019 isHtmlElement(ancestor, "ul")
5020 && isHtmlElement(ancestor.nextSibling, "ul")
5021 && inSameEditingHost(ancestor, ancestor.nextSibling)
5022 )) {
5023 ancestor = ancestor.parentNode;
5024 }
5025
5026 // "While ancestor's nextSibling has children, append ancestor's
5027 // nextSibling's firstChild as the last child of ancestor, preserving
5028 // ranges."
5029 while (ancestor.nextSibling.hasChildNodes()) {
5030 movePreservingRanges(ancestor.nextSibling.firstChild, ancestor, -1);
5031 }
5032
5033 // "Remove ancestor's nextSibling from its parent."
5034 ancestor.parentNode.removeChild(ancestor.nextSibling);
5035 }
5036
5037 // "Restore the values from values."
5038 restoreValues(values);
5039
5040 // "If start block has no children, call createElement("br") on the context
5041 // object and append the result as the last child of start block."
5042 if (!startBlock.hasChildNodes()) {
5043 startBlock.appendChild(document.createElement("br"));
5044 }
5045
5046 // "Remove extraneous line breaks at the end of start block."
5047 removeExtraneousLineBreaksAtTheEndOf(startBlock);
5048
5049 // "Restore states and values from overrides."
5050 restoreStatesAndValues(overrides);
5051 }
5052
5053
5054 //@}
5055 ///// Splitting a node list's parent /////
5056 //@{
5057
5058 function splitParent(nodeList) {
5059 // "Let original parent be the parent of the first member of node list."
5060 var originalParent = nodeList[0].parentNode;
5061
5062 // "If original parent is not editable or its parent is null, do nothing
5063 // and abort these steps."
5064 if (!isEditable(originalParent)
5065 || !originalParent.parentNode) {
5066 return;
5067 }
5068
5069 // "If the first child of original parent is in node list, remove
5070 // extraneous line breaks before original parent."
5071 if (nodeList.indexOf(originalParent.firstChild) != -1) {
5072 removeExtraneousLineBreaksBefore(originalParent);
5073 }
5074
5075 // "If the first child of original parent is in node list, and original
5076 // parent follows a line break, set follows line break to true. Otherwise,
5077 // set follows line break to false."
5078 var followsLineBreak_ = nodeList.indexOf(originalParent.firstChild) != -1
5079 && followsLineBreak(originalParent);
5080
5081 // "If the last child of original parent is in node list, and original
5082 // parent precedes a line break, set precedes line break to true.
5083 // Otherwise, set precedes line break to false."
5084 var precedesLineBreak_ = nodeList.indexOf(originalParent.lastChild) != -1
5085 && precedesLineBreak(originalParent);
5086
5087 // "If the first child of original parent is not in node list, but its last
5088 // child is:"
5089 if (nodeList.indexOf(originalParent.firstChild) == -1
5090 && nodeList.indexOf(originalParent.lastChild) != -1) {
5091 // "For each node in node list, in reverse order, insert node into the
5092 // parent of original parent immediately after original parent,
5093 // preserving ranges."
5094 for (var i = nodeList.length - 1; i >= 0; i--) {
5095 movePreservingRanges(nodeList[i], originalParent.parentNode, 1 + get NodeIndex(originalParent));
5096 }
5097
5098 // "If precedes line break is true, and the last member of node list
5099 // does not precede a line break, call createElement("br") on the
5100 // context object and insert the result immediately after the last
5101 // member of node list."
5102 if (precedesLineBreak_
5103 && !precedesLineBreak(nodeList[nodeList.length - 1])) {
5104 nodeList[nodeList.length - 1].parentNode.insertBefore(document.creat eElement("br"), nodeList[nodeList.length - 1].nextSibling);
5105 }
5106
5107 // "Remove extraneous line breaks at the end of original parent."
5108 removeExtraneousLineBreaksAtTheEndOf(originalParent);
5109
5110 // "Abort these steps."
5111 return;
5112 }
5113
5114 // "If the first child of original parent is not in node list:"
5115 if (nodeList.indexOf(originalParent.firstChild) == -1) {
5116 // "Let cloned parent be the result of calling cloneNode(false) on
5117 // original parent."
5118 var clonedParent = originalParent.cloneNode(false);
5119
5120 // "If original parent has an id attribute, unset it."
5121 originalParent.removeAttribute("id");
5122
5123 // "Insert cloned parent into the parent of original parent immediately
5124 // before original parent."
5125 originalParent.parentNode.insertBefore(clonedParent, originalParent);
5126
5127 // "While the previousSibling of the first member of node list is not
5128 // null, append the first child of original parent as the last child of
5129 // cloned parent, preserving ranges."
5130 while (nodeList[0].previousSibling) {
5131 movePreservingRanges(originalParent.firstChild, clonedParent, cloned Parent.childNodes.length);
5132 }
5133 }
5134
5135 // "For each node in node list, insert node into the parent of original
5136 // parent immediately before original parent, preserving ranges."
5137 for (var i = 0; i < nodeList.length; i++) {
5138 movePreservingRanges(nodeList[i], originalParent.parentNode, getNodeInde x(originalParent));
5139 }
5140
5141 // "If follows line break is true, and the first member of node list does
5142 // not follow a line break, call createElement("br") on the context object
5143 // and insert the result immediately before the first member of node list."
5144 if (followsLineBreak_
5145 && !followsLineBreak(nodeList[0])) {
5146 nodeList[0].parentNode.insertBefore(document.createElement("br"), nodeLi st[0]);
5147 }
5148
5149 // "If the last member of node list is an inline node other than a br, and
5150 // the first child of original parent is a br, and original parent is not
5151 // an inline node, remove the first child of original parent from original
5152 // parent."
5153 if (isInlineNode(nodeList[nodeList.length - 1])
5154 && !isHtmlElement(nodeList[nodeList.length - 1], "br")
5155 && isHtmlElement(originalParent.firstChild, "br")
5156 && !isInlineNode(originalParent)) {
5157 originalParent.removeChild(originalParent.firstChild);
5158 }
5159
5160 // "If original parent has no children:"
5161 if (!originalParent.hasChildNodes()) {
5162 // "Remove original parent from its parent."
5163 originalParent.parentNode.removeChild(originalParent);
5164
5165 // "If precedes line break is true, and the last member of node list
5166 // does not precede a line break, call createElement("br") on the
5167 // context object and insert the result immediately after the last
5168 // member of node list."
5169 if (precedesLineBreak_
5170 && !precedesLineBreak(nodeList[nodeList.length - 1])) {
5171 nodeList[nodeList.length - 1].parentNode.insertBefore(document.creat eElement("br"), nodeList[nodeList.length - 1].nextSibling);
5172 }
5173
5174 // "Otherwise, remove extraneous line breaks before original parent."
5175 } else {
5176 removeExtraneousLineBreaksBefore(originalParent);
5177 }
5178
5179 // "If node list's last member's nextSibling is null, but its parent is not
5180 // null, remove extraneous line breaks at the end of node list's last
5181 // member's parent."
5182 if (!nodeList[nodeList.length - 1].nextSibling
5183 && nodeList[nodeList.length - 1].parentNode) {
5184 removeExtraneousLineBreaksAtTheEndOf(nodeList[nodeList.length - 1].paren tNode);
5185 }
5186 }
5187
5188 // "To remove a node node while preserving its descendants, split the parent of
5189 // node's children if it has any. If it has no children, instead remove it from
5190 // its parent."
5191 function removePreservingDescendants(node) {
5192 if (node.hasChildNodes()) {
5193 splitParent([].slice.call(node.childNodes));
5194 } else {
5195 node.parentNode.removeChild(node);
5196 }
5197 }
5198
5199
5200 //@}
5201 ///// Canonical space sequences /////
5202 //@{
5203
5204 function canonicalSpaceSequence(n, nonBreakingStart, nonBreakingEnd) {
5205 // "If n is zero, return the empty string."
5206 if (n == 0) {
5207 return "";
5208 }
5209
5210 // "If n is one and both non-breaking start and non-breaking end are false,
5211 // return a single space (U+0020)."
5212 if (n == 1 && !nonBreakingStart && !nonBreakingEnd) {
5213 return " ";
5214 }
5215
5216 // "If n is one, return a single non-breaking space (U+00A0)."
5217 if (n == 1) {
5218 return "\xa0";
5219 }
5220
5221 // "Let buffer be the empty string."
5222 var buffer = "";
5223
5224 // "If non-breaking start is true, let repeated pair be U+00A0 U+0020.
5225 // Otherwise, let it be U+0020 U+00A0."
5226 var repeatedPair;
5227 if (nonBreakingStart) {
5228 repeatedPair = "\xa0 ";
5229 } else {
5230 repeatedPair = " \xa0";
5231 }
5232
5233 // "While n is greater than three, append repeated pair to buffer and
5234 // subtract two from n."
5235 while (n > 3) {
5236 buffer += repeatedPair;
5237 n -= 2;
5238 }
5239
5240 // "If n is three, append a three-element string to buffer depending on
5241 // non-breaking start and non-breaking end:"
5242 if (n == 3) {
5243 buffer +=
5244 !nonBreakingStart && !nonBreakingEnd ? " \xa0 "
5245 : nonBreakingStart && !nonBreakingEnd ? "\xa0\xa0 "
5246 : !nonBreakingStart && nonBreakingEnd ? " \xa0\xa0"
5247 : nonBreakingStart && nonBreakingEnd ? "\xa0 \xa0"
5248 : "impossible";
5249
5250 // "Otherwise, append a two-element string to buffer depending on
5251 // non-breaking start and non-breaking end:"
5252 } else {
5253 buffer +=
5254 !nonBreakingStart && !nonBreakingEnd ? "\xa0 "
5255 : nonBreakingStart && !nonBreakingEnd ? "\xa0 "
5256 : !nonBreakingStart && nonBreakingEnd ? " \xa0"
5257 : nonBreakingStart && nonBreakingEnd ? "\xa0\xa0"
5258 : "impossible";
5259 }
5260
5261 // "Return buffer."
5262 return buffer;
5263 }
5264
5265 function canonicalizeWhitespace(node, offset, fixCollapsedSpace) {
5266 if (fixCollapsedSpace === undefined) {
5267 // "an optional boolean argument fix collapsed space that defaults to
5268 // true"
5269 fixCollapsedSpace = true;
5270 }
5271
5272 // "If node is neither editable nor an editing host, abort these steps."
5273 if (!isEditable(node) && !isEditingHost(node)) {
5274 return;
5275 }
5276
5277 // "Let start node equal node and let start offset equal offset."
5278 var startNode = node;
5279 var startOffset = offset;
5280
5281 // "Repeat the following steps:"
5282 while (true) {
5283 // "If start node has a child in the same editing host with index start
5284 // offset minus one, set start node to that child, then set start
5285 // offset to start node's length."
5286 if (0 <= startOffset - 1
5287 && inSameEditingHost(startNode, startNode.childNodes[startOffset - 1])) {
5288 startNode = startNode.childNodes[startOffset - 1];
5289 startOffset = getNodeLength(startNode);
5290
5291 // "Otherwise, if start offset is zero and start node does not follow a
5292 // line break and start node's parent is in the same editing host, set
5293 // start offset to start node's index, then set start node to its
5294 // parent."
5295 } else if (startOffset == 0
5296 && !followsLineBreak(startNode)
5297 && inSameEditingHost(startNode, startNode.parentNode)) {
5298 startOffset = getNodeIndex(startNode);
5299 startNode = startNode.parentNode;
5300
5301 // "Otherwise, if start node is a Text node and its parent's resolved
5302 // value for "white-space" is neither "pre" nor "pre-wrap" and start
5303 // offset is not zero and the (start offset − 1)st element of start
5304 // node's data is a space (0x0020) or non-breaking space (0x00A0),
5305 // subtract one from start offset."
5306 } else if (startNode.nodeType == Node.TEXT_NODE
5307 && ["pre", "pre-wrap"].indexOf(getComputedStyle(startNode.parentNode).wh iteSpace) == -1
5308 && startOffset != 0
5309 && /[ \xa0]/.test(startNode.data[startOffset - 1])) {
5310 startOffset--;
5311
5312 // "Otherwise, break from this loop."
5313 } else {
5314 break;
5315 }
5316 }
5317
5318 // "Let end node equal start node and end offset equal start offset."
5319 var endNode = startNode;
5320 var endOffset = startOffset;
5321
5322 // "Let length equal zero."
5323 var length = 0;
5324
5325 // "Let collapse spaces be true if start offset is zero and start node
5326 // follows a line break, otherwise false."
5327 var collapseSpaces = startOffset == 0 && followsLineBreak(startNode);
5328
5329 // "Repeat the following steps:"
5330 while (true) {
5331 // "If end node has a child in the same editing host with index end
5332 // offset, set end node to that child, then set end offset to zero."
5333 if (endOffset < endNode.childNodes.length
5334 && inSameEditingHost(endNode, endNode.childNodes[endOffset])) {
5335 endNode = endNode.childNodes[endOffset];
5336 endOffset = 0;
5337
5338 // "Otherwise, if end offset is end node's length and end node does not
5339 // precede a line break and end node's parent is in the same editing
5340 // host, set end offset to one plus end node's index, then set end node
5341 // to its parent."
5342 } else if (endOffset == getNodeLength(endNode)
5343 && !precedesLineBreak(endNode)
5344 && inSameEditingHost(endNode, endNode.parentNode)) {
5345 endOffset = 1 + getNodeIndex(endNode);
5346 endNode = endNode.parentNode;
5347
5348 // "Otherwise, if end node is a Text node and its parent's resolved
5349 // value for "white-space" is neither "pre" nor "pre-wrap" and end
5350 // offset is not end node's length and the end offsetth element of
5351 // end node's data is a space (0x0020) or non-breaking space (0x00A0):"
5352 } else if (endNode.nodeType == Node.TEXT_NODE
5353 && ["pre", "pre-wrap"].indexOf(getComputedStyle(endNode.parentNode).whit eSpace) == -1
5354 && endOffset != getNodeLength(endNode)
5355 && /[ \xa0]/.test(endNode.data[endOffset])) {
5356 // "If fix collapsed space is true, and collapse spaces is true,
5357 // and the end offsetth code unit of end node's data is a space
5358 // (0x0020): call deleteData(end offset, 1) on end node, then
5359 // continue this loop from the beginning."
5360 if (fixCollapsedSpace
5361 && collapseSpaces
5362 && " " == endNode.data[endOffset]) {
5363 endNode.deleteData(endOffset, 1);
5364 continue;
5365 }
5366
5367 // "Set collapse spaces to true if the end offsetth element of end
5368 // node's data is a space (0x0020), false otherwise."
5369 collapseSpaces = " " == endNode.data[endOffset];
5370
5371 // "Add one to end offset."
5372 endOffset++;
5373
5374 // "Add one to length."
5375 length++;
5376
5377 // "Otherwise, break from this loop."
5378 } else {
5379 break;
5380 }
5381 }
5382
5383 // "If fix collapsed space is true, then while (start node, start offset)
5384 // is before (end node, end offset):"
5385 if (fixCollapsedSpace) {
5386 while (getPosition(startNode, startOffset, endNode, endOffset) == "befor e") {
5387 // "If end node has a child in the same editing host with index end
5388 // offset − 1, set end node to that child, then set end offset to en d
5389 // node's length."
5390 if (0 <= endOffset - 1
5391 && endOffset - 1 < endNode.childNodes.length
5392 && inSameEditingHost(endNode, endNode.childNodes[endOffset - 1])) {
5393 endNode = endNode.childNodes[endOffset - 1];
5394 endOffset = getNodeLength(endNode);
5395
5396 // "Otherwise, if end offset is zero and end node's parent is in the
5397 // same editing host, set end offset to end node's index, then set e nd
5398 // node to its parent."
5399 } else if (endOffset == 0
5400 && inSameEditingHost(endNode, endNode.parentNode)) {
5401 endOffset = getNodeIndex(endNode);
5402 endNode = endNode.parentNode;
5403
5404 // "Otherwise, if end node is a Text node and its parent's resolved
5405 // value for "white-space" is neither "pre" nor "pre-wrap" and end
5406 // offset is end node's length and the last code unit of end node's
5407 // data is a space (0x0020) and end node precedes a line break:"
5408 } else if (endNode.nodeType == Node.TEXT_NODE
5409 && ["pre", "pre-wrap"].indexOf(getComputedStyle(endNode.parentNode). whiteSpace) == -1
5410 && endOffset == getNodeLength(endNode)
5411 && endNode.data[endNode.data.length - 1] == " "
5412 && precedesLineBreak(endNode)) {
5413 // "Subtract one from end offset."
5414 endOffset--;
5415
5416 // "Subtract one from length."
5417 length--;
5418
5419 // "Call deleteData(end offset, 1) on end node."
5420 endNode.deleteData(endOffset, 1);
5421
5422 // "Otherwise, break from this loop."
5423 } else {
5424 break;
5425 }
5426 }
5427 }
5428
5429 // "Let replacement whitespace be the canonical space sequence of length
5430 // length. non-breaking start is true if start offset is zero and start
5431 // node follows a line break, and false otherwise. non-breaking end is true
5432 // if end offset is end node's length and end node precedes a line break,
5433 // and false otherwise."
5434 var replacementWhitespace = canonicalSpaceSequence(length,
5435 startOffset == 0 && followsLineBreak(startNode),
5436 endOffset == getNodeLength(endNode) && precedesLineBreak(endNode));
5437
5438 // "While (start node, start offset) is before (end node, end offset):"
5439 while (getPosition(startNode, startOffset, endNode, endOffset) == "before") {
5440 // "If start node has a child with index start offset, set start node
5441 // to that child, then set start offset to zero."
5442 if (startOffset < startNode.childNodes.length) {
5443 startNode = startNode.childNodes[startOffset];
5444 startOffset = 0;
5445
5446 // "Otherwise, if start node is not a Text node or if start offset is
5447 // start node's length, set start offset to one plus start node's
5448 // index, then set start node to its parent."
5449 } else if (startNode.nodeType != Node.TEXT_NODE
5450 || startOffset == getNodeLength(startNode)) {
5451 startOffset = 1 + getNodeIndex(startNode);
5452 startNode = startNode.parentNode;
5453
5454 // "Otherwise:"
5455 } else {
5456 // "Remove the first element from replacement whitespace, and let
5457 // element be that element."
5458 var element = replacementWhitespace[0];
5459 replacementWhitespace = replacementWhitespace.slice(1);
5460
5461 // "If element is not the same as the start offsetth element of
5462 // start node's data:"
5463 if (element != startNode.data[startOffset]) {
5464 // "Call insertData(start offset, element) on start node."
5465 startNode.insertData(startOffset, element);
5466
5467 // "Call deleteData(start offset + 1, 1) on start node."
5468 startNode.deleteData(startOffset + 1, 1);
5469 }
5470
5471 // "Add one to start offset."
5472 startOffset++;
5473 }
5474 }
5475 }
5476
5477
5478 //@}
5479 ///// Indenting and outdenting /////
5480 //@{
5481
5482 function indentNodes(nodeList) {
5483 // "If node list is empty, do nothing and abort these steps."
5484 if (!nodeList.length) {
5485 return;
5486 }
5487
5488 // "Let first node be the first member of node list."
5489 var firstNode = nodeList[0];
5490
5491 // "If first node's parent is an ol or ul:"
5492 if (isHtmlElement(firstNode.parentNode, ["OL", "UL"])) {
5493 // "Let tag be the local name of the parent of first node."
5494 var tag = firstNode.parentNode.tagName;
5495
5496 // "Wrap node list, with sibling criteria returning true for an HTML
5497 // element with local name tag and false otherwise, and new parent
5498 // instructions returning the result of calling createElement(tag) on
5499 // the ownerDocument of first node."
5500 wrap(nodeList,
5501 function(node) { return isHtmlElement(node, tag) },
5502 function() { return firstNode.ownerDocument.createElement(tag) });
5503
5504 // "Abort these steps."
5505 return;
5506 }
5507
5508 // "Wrap node list, with sibling criteria returning true for a simple
5509 // indentation element and false otherwise, and new parent instructions
5510 // returning the result of calling createElement("blockquote") on the
5511 // ownerDocument of first node. Let new parent be the result."
5512 var newParent = wrap(nodeList,
5513 function(node) { return isSimpleIndentationElement(node) },
5514 function() { return firstNode.ownerDocument.createElement("blockquote") });
5515
5516 // "Fix disallowed ancestors of new parent."
5517 fixDisallowedAncestors(newParent);
5518 }
5519
5520 function outdentNode(node) {
5521 // "If node is not editable, abort these steps."
5522 if (!isEditable(node)) {
5523 return;
5524 }
5525
5526 // "If node is a simple indentation element, remove node, preserving its
5527 // descendants. Then abort these steps."
5528 if (isSimpleIndentationElement(node)) {
5529 removePreservingDescendants(node);
5530 return;
5531 }
5532
5533 // "If node is an indentation element:"
5534 if (isIndentationElement(node)) {
5535 // "Unset the dir attribute of node, if any."
5536 node.removeAttribute("dir");
5537
5538 // "Unset the margin, padding, and border CSS properties of node."
5539 node.style.margin = "";
5540 node.style.padding = "";
5541 node.style.border = "";
5542 if (node.getAttribute("style") == ""
5543 // Crazy WebKit bug: https://bugs.webkit.org/show_bug.cgi?id=68551
5544 || node.getAttribute("style") == "border-width: initial; border-color: i nitial; ") {
5545 node.removeAttribute("style");
5546 }
5547
5548 // "Set the tag name of node to "div"."
5549 setTagName(node, "div");
5550
5551 // "Abort these steps."
5552 return;
5553 }
5554
5555 // "Let current ancestor be node's parent."
5556 var currentAncestor = node.parentNode;
5557
5558 // "Let ancestor list be a list of nodes, initially empty."
5559 var ancestorList = [];
5560
5561 // "While current ancestor is an editable Element that is neither a simple
5562 // indentation element nor an ol nor a ul, append current ancestor to
5563 // ancestor list and then set current ancestor to its parent."
5564 while (isEditable(currentAncestor)
5565 && currentAncestor.nodeType == Node.ELEMENT_NODE
5566 && !isSimpleIndentationElement(currentAncestor)
5567 && !isHtmlElement(currentAncestor, ["ol", "ul"])) {
5568 ancestorList.push(currentAncestor);
5569 currentAncestor = currentAncestor.parentNode;
5570 }
5571
5572 // "If current ancestor is not an editable simple indentation element:"
5573 if (!isEditable(currentAncestor)
5574 || !isSimpleIndentationElement(currentAncestor)) {
5575 // "Let current ancestor be node's parent."
5576 currentAncestor = node.parentNode;
5577
5578 // "Let ancestor list be the empty list."
5579 ancestorList = [];
5580
5581 // "While current ancestor is an editable Element that is neither an
5582 // indentation element nor an ol nor a ul, append current ancestor to
5583 // ancestor list and then set current ancestor to its parent."
5584 while (isEditable(currentAncestor)
5585 && currentAncestor.nodeType == Node.ELEMENT_NODE
5586 && !isIndentationElement(currentAncestor)
5587 && !isHtmlElement(currentAncestor, ["ol", "ul"])) {
5588 ancestorList.push(currentAncestor);
5589 currentAncestor = currentAncestor.parentNode;
5590 }
5591 }
5592
5593 // "If node is an ol or ul and current ancestor is not an editable
5594 // indentation element:"
5595 if (isHtmlElement(node, ["OL", "UL"])
5596 && (!isEditable(currentAncestor)
5597 || !isIndentationElement(currentAncestor))) {
5598 // "Unset the reversed, start, and type attributes of node, if any are
5599 // set."
5600 node.removeAttribute("reversed");
5601 node.removeAttribute("start");
5602 node.removeAttribute("type");
5603
5604 // "Let children be the children of node."
5605 var children = [].slice.call(node.childNodes);
5606
5607 // "If node has attributes, and its parent is not an ol or ul, set the
5608 // tag name of node to "div"."
5609 if (node.attributes.length
5610 && !isHtmlElement(node.parentNode, ["OL", "UL"])) {
5611 setTagName(node, "div");
5612
5613 // "Otherwise:"
5614 } else {
5615 // "Record the values of node's children, and let values be the
5616 // result."
5617 var values = recordValues([].slice.call(node.childNodes));
5618
5619 // "Remove node, preserving its descendants."
5620 removePreservingDescendants(node);
5621
5622 // "Restore the values from values."
5623 restoreValues(values);
5624 }
5625
5626 // "Fix disallowed ancestors of each member of children."
5627 for (var i = 0; i < children.length; i++) {
5628 fixDisallowedAncestors(children[i]);
5629 }
5630
5631 // "Abort these steps."
5632 return;
5633 }
5634
5635 // "If current ancestor is not an editable indentation element, abort these
5636 // steps."
5637 if (!isEditable(currentAncestor)
5638 || !isIndentationElement(currentAncestor)) {
5639 return;
5640 }
5641
5642 // "Append current ancestor to ancestor list."
5643 ancestorList.push(currentAncestor);
5644
5645 // "Let original ancestor be current ancestor."
5646 var originalAncestor = currentAncestor;
5647
5648 // "While ancestor list is not empty:"
5649 while (ancestorList.length) {
5650 // "Let current ancestor be the last member of ancestor list."
5651 //
5652 // "Remove the last member of ancestor list."
5653 currentAncestor = ancestorList.pop();
5654
5655 // "Let target be the child of current ancestor that is equal to either
5656 // node or the last member of ancestor list."
5657 var target = node.parentNode == currentAncestor
5658 ? node
5659 : ancestorList[ancestorList.length - 1];
5660
5661 // "If target is an inline node that is not a br, and its nextSibling
5662 // is a br, remove target's nextSibling from its parent."
5663 if (isInlineNode(target)
5664 && !isHtmlElement(target, "BR")
5665 && isHtmlElement(target.nextSibling, "BR")) {
5666 target.parentNode.removeChild(target.nextSibling);
5667 }
5668
5669 // "Let preceding siblings be the preceding siblings of target, and let
5670 // following siblings be the following siblings of target."
5671 var precedingSiblings = [].slice.call(currentAncestor.childNodes, 0, get NodeIndex(target));
5672 var followingSiblings = [].slice.call(currentAncestor.childNodes, 1 + ge tNodeIndex(target));
5673
5674 // "Indent preceding siblings."
5675 indentNodes(precedingSiblings);
5676
5677 // "Indent following siblings."
5678 indentNodes(followingSiblings);
5679 }
5680
5681 // "Outdent original ancestor."
5682 outdentNode(originalAncestor);
5683 }
5684
5685
5686 //@}
5687 ///// Toggling lists /////
5688 //@{
5689
5690 function toggleLists(tagName) {
5691 // "Let mode be "disable" if the selection's list state is tag name, and
5692 // "enable" otherwise."
5693 var mode = getSelectionListState() == tagName ? "disable" : "enable";
5694
5695 var range = getActiveRange();
5696 tagName = tagName.toUpperCase();
5697
5698 // "Let other tag name be "ol" if tag name is "ul", and "ul" if tag name is
5699 // "ol"."
5700 var otherTagName = tagName == "OL" ? "UL" : "OL";
5701
5702 // "Let items be a list of all lis that are ancestor containers of the
5703 // range's start and/or end node."
5704 //
5705 // It's annoying to get this in tree order using functional stuff without
5706 // doing getDescendants(document), which is slow, so I do it imperatively.
5707 var items = [];
5708 (function(){
5709 for (
5710 var ancestorContainer = range.endContainer;
5711 ancestorContainer != range.commonAncestorContainer;
5712 ancestorContainer = ancestorContainer.parentNode
5713 ) {
5714 if (isHtmlElement(ancestorContainer, "li")) {
5715 items.unshift(ancestorContainer);
5716 }
5717 }
5718 for (
5719 var ancestorContainer = range.startContainer;
5720 ancestorContainer;
5721 ancestorContainer = ancestorContainer.parentNode
5722 ) {
5723 if (isHtmlElement(ancestorContainer, "li")) {
5724 items.unshift(ancestorContainer);
5725 }
5726 }
5727 })();
5728
5729 // "For each item in items, normalize sublists of item."
5730 items.forEach(normalizeSublists);
5731
5732 // "Block-extend the range, and let new range be the result."
5733 var newRange = blockExtend(range);
5734
5735 // "If mode is "enable", then let lists to convert consist of every
5736 // editable HTML element with local name other tag name that is contained
5737 // in new range, and for every list in lists to convert:"
5738 if (mode == "enable") {
5739 getAllContainedNodes(newRange, function(node) {
5740 return isEditable(node)
5741 && isHtmlElement(node, otherTagName);
5742 }).forEach(function(list) {
5743 // "If list's previousSibling or nextSibling is an editable HTML
5744 // element with local name tag name:"
5745 if ((isEditable(list.previousSibling) && isHtmlElement(list.previous Sibling, tagName))
5746 || (isEditable(list.nextSibling) && isHtmlElement(list.nextSibling, tagName))) {
5747 // "Let children be list's children."
5748 var children = [].slice.call(list.childNodes);
5749
5750 // "Record the values of children, and let values be the
5751 // result."
5752 var values = recordValues(children);
5753
5754 // "Split the parent of children."
5755 splitParent(children);
5756
5757 // "Wrap children, with sibling criteria returning true for an
5758 // HTML element with local name tag name and false otherwise."
5759 wrap(children, function(node) { return isHtmlElement(node, tagNa me) });
5760
5761 // "Restore the values from values."
5762 restoreValues(values);
5763
5764 // "Otherwise, set the tag name of list to tag name."
5765 } else {
5766 setTagName(list, tagName);
5767 }
5768 });
5769 }
5770
5771 // "Let node list be a list of nodes, initially empty."
5772 //
5773 // "For each node node contained in new range, if node is editable; the
5774 // last member of node list (if any) is not an ancestor of node; node
5775 // is not an indentation element; and either node is an ol or ul, or its
5776 // parent is an ol or ul, or it is an allowed child of "li"; then append
5777 // node to node list."
5778 var nodeList = getContainedNodes(newRange, function(node) {
5779 return isEditable(node)
5780 && !isIndentationElement(node)
5781 && (isHtmlElement(node, ["OL", "UL"])
5782 || isHtmlElement(node.parentNode, ["OL", "UL"])
5783 || isAllowedChild(node, "li"));
5784 });
5785
5786 // "If mode is "enable", remove from node list any ol or ul whose parent is
5787 // not also an ol or ul."
5788 if (mode == "enable") {
5789 nodeList = nodeList.filter(function(node) {
5790 return !isHtmlElement(node, ["ol", "ul"])
5791 || isHtmlElement(node.parentNode, ["ol", "ul"]);
5792 });
5793 }
5794
5795 // "If mode is "disable", then while node list is not empty:"
5796 if (mode == "disable") {
5797 while (nodeList.length) {
5798 // "Let sublist be an empty list of nodes."
5799 var sublist = [];
5800
5801 // "Remove the first member from node list and append it to
5802 // sublist."
5803 sublist.push(nodeList.shift());
5804
5805 // "If the first member of sublist is an HTML element with local
5806 // name tag name, outdent it and continue this loop from the
5807 // beginning."
5808 if (isHtmlElement(sublist[0], tagName)) {
5809 outdentNode(sublist[0]);
5810 continue;
5811 }
5812
5813 // "While node list is not empty, and the first member of node list
5814 // is the nextSibling of the last member of sublist and is not an
5815 // HTML element with local name tag name, remove the first member
5816 // from node list and append it to sublist."
5817 while (nodeList.length
5818 && nodeList[0] == sublist[sublist.length - 1].nextSibling
5819 && !isHtmlElement(nodeList[0], tagName)) {
5820 sublist.push(nodeList.shift());
5821 }
5822
5823 // "Record the values of sublist, and let values be the result."
5824 var values = recordValues(sublist);
5825
5826 // "Split the parent of sublist."
5827 splitParent(sublist);
5828
5829 // "Fix disallowed ancestors of each member of sublist."
5830 for (var i = 0; i < sublist.length; i++) {
5831 fixDisallowedAncestors(sublist[i]);
5832 }
5833
5834 // "Restore the values from values."
5835 restoreValues(values);
5836 }
5837
5838 // "Otherwise, while node list is not empty:"
5839 } else {
5840 while (nodeList.length) {
5841 // "Let sublist be an empty list of nodes."
5842 var sublist = [];
5843
5844 // "While either sublist is empty, or node list is not empty and
5845 // its first member is the nextSibling of sublist's last member:"
5846 while (!sublist.length
5847 || (nodeList.length
5848 && nodeList[0] == sublist[sublist.length - 1].nextSibling)) {
5849 // "If node list's first member is a p or div, set the tag name
5850 // of node list's first member to "li", and append the result
5851 // to sublist. Remove the first member from node list."
5852 if (isHtmlElement(nodeList[0], ["p", "div"])) {
5853 sublist.push(setTagName(nodeList[0], "li"));
5854 nodeList.shift();
5855
5856 // "Otherwise, if the first member of node list is an li or ol
5857 // or ul, remove it from node list and append it to sublist."
5858 } else if (isHtmlElement(nodeList[0], ["li", "ol", "ul"])) {
5859 sublist.push(nodeList.shift());
5860
5861 // "Otherwise:"
5862 } else {
5863 // "Let nodes to wrap be a list of nodes, initially empty."
5864 var nodesToWrap = [];
5865
5866 // "While nodes to wrap is empty, or node list is not empty
5867 // and its first member is the nextSibling of nodes to
5868 // wrap's last member and the first member of node list is
5869 // an inline node and the last member of nodes to wrap is
5870 // an inline node other than a br, remove the first member
5871 // from node list and append it to nodes to wrap."
5872 while (!nodesToWrap.length
5873 || (nodeList.length
5874 && nodeList[0] == nodesToWrap[nodesToWrap.length - 1].nextSi bling
5875 && isInlineNode(nodeList[0])
5876 && isInlineNode(nodesToWrap[nodesToWrap.length - 1])
5877 && !isHtmlElement(nodesToWrap[nodesToWrap.length - 1], "br") )) {
5878 nodesToWrap.push(nodeList.shift());
5879 }
5880
5881 // "Wrap nodes to wrap, with new parent instructions
5882 // returning the result of calling createElement("li") on
5883 // the context object. Append the result to sublist."
5884 sublist.push(wrap(nodesToWrap,
5885 undefined,
5886 function() { return document.createElement("li") }));
5887 }
5888 }
5889
5890 // "If sublist's first member's parent is an HTML element with
5891 // local name tag name, or if every member of sublist is an ol or
5892 // ul, continue this loop from the beginning."
5893 if (isHtmlElement(sublist[0].parentNode, tagName)
5894 || sublist.every(function(node) { return isHtmlElement(node, ["ol", "ul"]) })) {
5895 continue;
5896 }
5897
5898 // "If sublist's first member's parent is an HTML element with
5899 // local name other tag name:"
5900 if (isHtmlElement(sublist[0].parentNode, otherTagName)) {
5901 // "Record the values of sublist, and let values be the
5902 // result."
5903 var values = recordValues(sublist);
5904
5905 // "Split the parent of sublist."
5906 splitParent(sublist);
5907
5908 // "Wrap sublist, with sibling criteria returning true for an
5909 // HTML element with local name tag name and false otherwise,
5910 // and new parent instructions returning the result of calling
5911 // createElement(tag name) on the context object."
5912 wrap(sublist,
5913 function(node) { return isHtmlElement(node, tagName) },
5914 function() { return document.createElement(tagName) });
5915
5916 // "Restore the values from values."
5917 restoreValues(values);
5918
5919 // "Continue this loop from the beginning."
5920 continue;
5921 }
5922
5923 // "Wrap sublist, with sibling criteria returning true for an HTML
5924 // element with local name tag name and false otherwise, and new
5925 // parent instructions being the following:"
5926 // . . .
5927 // "Fix disallowed ancestors of the previous step's result."
5928 fixDisallowedAncestors(wrap(sublist,
5929 function(node) { return isHtmlElement(node, tagName) },
5930 function() {
5931 // "If sublist's first member's parent is not an editable
5932 // simple indentation element, or sublist's first member's
5933 // parent's previousSibling is not an editable HTML element
5934 // with local name tag name, call createElement(tag name)
5935 // on the context object and return the result."
5936 if (!isEditable(sublist[0].parentNode)
5937 || !isSimpleIndentationElement(sublist[0].parentNode)
5938 || !isEditable(sublist[0].parentNode.previousSibling)
5939 || !isHtmlElement(sublist[0].parentNode.previousSibling, tag Name)) {
5940 return document.createElement(tagName);
5941 }
5942
5943 // "Let list be sublist's first member's parent's
5944 // previousSibling."
5945 var list = sublist[0].parentNode.previousSibling;
5946
5947 // "Normalize sublists of list's lastChild."
5948 normalizeSublists(list.lastChild);
5949
5950 // "If list's lastChild is not an editable HTML element
5951 // with local name tag name, call createElement(tag name)
5952 // on the context object, and append the result as the last
5953 // child of list."
5954 if (!isEditable(list.lastChild)
5955 || !isHtmlElement(list.lastChild, tagName)) {
5956 list.appendChild(document.createElement(tagName));
5957 }
5958
5959 // "Return the last child of list."
5960 return list.lastChild;
5961 }
5962 ));
5963 }
5964 }
5965 }
5966
5967
5968 //@}
5969 ///// Justifying the selection /////
5970 //@{
5971
5972 function justifySelection(alignment) {
5973 // "Block-extend the active range, and let new range be the result."
5974 var newRange = blockExtend(globalRange);
5975
5976 // "Let element list be a list of all editable Elements contained in new
5977 // range that either has an attribute in the HTML namespace whose local
5978 // name is "align", or has a style attribute that sets "text-align", or is
5979 // a center."
5980 var elementList = getAllContainedNodes(newRange, function(node) {
5981 return node.nodeType == Node.ELEMENT_NODE
5982 && isEditable(node)
5983 // Ignoring namespaces here
5984 && (
5985 node.hasAttribute("align")
5986 || node.style.textAlign != ""
5987 || isHtmlElement(node, "center")
5988 );
5989 });
5990
5991 // "For each element in element list:"
5992 for (var i = 0; i < elementList.length; i++) {
5993 var element = elementList[i];
5994
5995 // "If element has an attribute in the HTML namespace whose local name
5996 // is "align", remove that attribute."
5997 element.removeAttribute("align");
5998
5999 // "Unset the CSS property "text-align" on element, if it's set by a
6000 // style attribute."
6001 element.style.textAlign = "";
6002 if (element.getAttribute("style") == "") {
6003 element.removeAttribute("style");
6004 }
6005
6006 // "If element is a div or span or center with no attributes, remove
6007 // it, preserving its descendants."
6008 if (isHtmlElement(element, ["div", "span", "center"])
6009 && !element.attributes.length) {
6010 removePreservingDescendants(element);
6011 }
6012
6013 // "If element is a center with one or more attributes, set the tag
6014 // name of element to "div"."
6015 if (isHtmlElement(element, "center")
6016 && element.attributes.length) {
6017 setTagName(element, "div");
6018 }
6019 }
6020
6021 // "Block-extend the active range, and let new range be the result."
6022 newRange = blockExtend(globalRange);
6023
6024 // "Let node list be a list of nodes, initially empty."
6025 var nodeList = [];
6026
6027 // "For each node node contained in new range, append node to node list if
6028 // the last member of node list (if any) is not an ancestor of node; node
6029 // is editable; node is an allowed child of "div"; and node's alignment
6030 // value is not alignment."
6031 nodeList = getContainedNodes(newRange, function(node) {
6032 return isEditable(node)
6033 && isAllowedChild(node, "div")
6034 && getAlignmentValue(node) != alignment;
6035 });
6036
6037 // "While node list is not empty:"
6038 while (nodeList.length) {
6039 // "Let sublist be a list of nodes, initially empty."
6040 var sublist = [];
6041
6042 // "Remove the first member of node list and append it to sublist."
6043 sublist.push(nodeList.shift());
6044
6045 // "While node list is not empty, and the first member of node list is
6046 // the nextSibling of the last member of sublist, remove the first
6047 // member of node list and append it to sublist."
6048 while (nodeList.length
6049 && nodeList[0] == sublist[sublist.length - 1].nextSibling) {
6050 sublist.push(nodeList.shift());
6051 }
6052
6053 // "Wrap sublist. Sibling criteria returns true for any div that has
6054 // one or both of the following two attributes and no other attributes,
6055 // and false otherwise:"
6056 //
6057 // * "An align attribute whose value is an ASCII case-insensitive
6058 // match for alignment.
6059 // * "A style attribute which sets exactly one CSS property
6060 // (including unrecognized or invalid attributes), which is
6061 // "text-align", which is set to alignment.
6062 //
6063 // "New parent instructions are to call createElement("div") on the
6064 // context object, then set its CSS property "text-align" to alignment
6065 // and return the result."
6066 wrap(sublist,
6067 function(node) {
6068 return isHtmlElement(node, "div")
6069 && [].every.call(node.attributes, function(attr) {
6070 return (attr.name == "align" && attr.value.toLowerCase() == alignment)
6071 || (attr.name == "style" && node.style.length == 1 & & node.style.textAlign == alignment);
6072 });
6073 },
6074 function() {
6075 var newParent = document.createElement("div");
6076 newParent.setAttribute("style", "text-align: " + alignment);
6077 return newParent;
6078 }
6079 );
6080 }
6081 }
6082
6083
6084 //@}
6085 ///// Automatic linking /////
6086 //@{
6087 // "An autolinkable URL is a string of the following form:"
6088 var autolinkableUrlRegexp =
6089 // "Either a string matching the scheme pattern from RFC 3986 section 3.1
6090 // followed by the literal string ://, or the literal string mailto:;
6091 // followed by"
6092 //
6093 // From the RFC: scheme = ALPHA *( ALPHA / DIGIT / "+" / "-" / "." )
6094 "([a-zA-Z][a-zA-Z0-9+.-]*://|mailto:)"
6095 // "Zero or more characters other than space characters; followed by"
6096 + "[^ \t\n\f\r]*"
6097 // "A character that is not one of the ASCII characters !"'(),-.:;<>[]`{}."
6098 + "[^!\"'(),\\-.:;<>[\\]`{}]";
6099
6100 // "A valid e-mail address is a string that matches the ABNF production 1*(
6101 // atext / "." ) "@" ldh-str *( "." ldh-str ) where atext is defined in RFC
6102 // 5322 section 3.2.3, and ldh-str is defined in RFC 1034 section 3.5."
6103 //
6104 // atext: ALPHA / DIGIT / "!" / "#" / "$" / "%" / "&" / "'" / "*" / "+" / "-" /
6105 // "/" / "=" / "?" / "^" / "_" / "`" / "{" / "|" / "}" / "~"
6106 //
6107 //<ldh-str> ::= <let-dig-hyp> | <let-dig-hyp> <ldh-str>
6108 //<let-dig-hyp> ::= <let-dig> | "-"
6109 //<let-dig> ::= <letter> | <digit>
6110 var validEmailRegexp =
6111 "[a-zA-Z0-9!#$%&'*+\\-/=?^_`{|}~.]+@[a-zA-Z0-9-]+(\.[a-zA-Z0-9-]+)*";
6112
6113 function autolink(node, endOffset) {
6114 // "While (node, end offset)'s previous equivalent point is not null, set
6115 // it to its previous equivalent point."
6116 while (getPreviousEquivalentPoint(node, endOffset)) {
6117 var prev = getPreviousEquivalentPoint(node, endOffset);
6118 node = prev[0];
6119 endOffset = prev[1];
6120 }
6121
6122 // "If node is not a Text node, or has an a ancestor, do nothing and abort
6123 // these steps."
6124 if (node.nodeType != Node.TEXT_NODE
6125 || getAncestors(node).some(function(ancestor) { return isHtmlElement(ancesto r, "a") })) {
6126 return;
6127 }
6128
6129 // "Let search be the largest substring of node's data whose end is end
6130 // offset and that contains no space characters."
6131 var search = /[^ \t\n\f\r]*$/.exec(node.substringData(0, endOffset))[0];
6132
6133 // "If some substring of search is an autolinkable URL:"
6134 if (new RegExp(autolinkableUrlRegexp).test(search)) {
6135 // "While there is no substring of node's data ending at end offset
6136 // that is an autolinkable URL, decrement end offset."
6137 while (!(new RegExp(autolinkableUrlRegexp + "$").test(node.substringData (0, endOffset)))) {
6138 endOffset--;
6139 }
6140
6141 // "Let start offset be the start index of the longest substring of
6142 // node's data that is an autolinkable URL ending at end offset."
6143 var startOffset = new RegExp(autolinkableUrlRegexp + "$").exec(node.subs tringData(0, endOffset)).index;
6144
6145 // "Let href be the substring of node's data starting at start offset
6146 // and ending at end offset."
6147 var href = node.substringData(startOffset, endOffset - startOffset);
6148
6149 // "Otherwise, if some substring of search is a valid e-mail address:"
6150 } else if (new RegExp(validEmailRegexp).test(search)) {
6151 // "While there is no substring of node's data ending at end offset
6152 // that is a valid e-mail address, decrement end offset."
6153 while (!(new RegExp(validEmailRegexp + "$").test(node.substringData(0, e ndOffset)))) {
6154 endOffset--;
6155 }
6156
6157 // "Let start offset be the start index of the longest substring of
6158 // node's data that is a valid e-mail address ending at end offset."
6159 var startOffset = new RegExp(validEmailRegexp + "$").exec(node.substring Data(0, endOffset)).index;
6160
6161 // "Let href be "mailto:" concatenated with the substring of node's
6162 // data starting at start offset and ending at end offset."
6163 var href = "mailto:" + node.substringData(startOffset, endOffset - start Offset);
6164
6165 // "Otherwise, do nothing and abort these steps."
6166 } else {
6167 return;
6168 }
6169
6170 // "Let original range be the active range."
6171 var originalRange = getActiveRange();
6172
6173 // "Create a new range with start (node, start offset) and end (node, end
6174 // offset), and set the context object's selection's range to it."
6175 var newRange = document.createRange();
6176 newRange.setStart(node, startOffset);
6177 newRange.setEnd(node, endOffset);
6178 getSelection().removeAllRanges();
6179 getSelection().addRange(newRange);
6180 globalRange = newRange;
6181
6182 // "Take the action for "createLink", with value equal to href."
6183 commands.createlink.action(href);
6184
6185 // "Set the context object's selection's range to original range."
6186 getSelection().removeAllRanges();
6187 getSelection().addRange(originalRange);
6188 globalRange = originalRange;
6189 }
6190 //@}
6191 ///// The delete command /////
6192 //@{
6193 commands["delete"] = {
6194 preservesOverrides: true,
6195 action: function() {
6196 // "If the active range is not collapsed, delete the selection and
6197 // return true."
6198 if (!getActiveRange().collapsed) {
6199 deleteSelection();
6200 return true;
6201 }
6202
6203 // "Canonicalize whitespace at the active range's start."
6204 canonicalizeWhitespace(getActiveRange().startContainer, getActiveRange() .startOffset);
6205
6206 // "Let node and offset be the active range's start node and offset."
6207 var node = getActiveRange().startContainer;
6208 var offset = getActiveRange().startOffset;
6209
6210 // "Repeat the following steps:"
6211 while (true) {
6212 // "If offset is zero and node's previousSibling is an editable
6213 // invisible node, remove node's previousSibling from its parent."
6214 if (offset == 0
6215 && isEditable(node.previousSibling)
6216 && isInvisible(node.previousSibling)) {
6217 node.parentNode.removeChild(node.previousSibling);
6218
6219 // "Otherwise, if node has a child with index offset − 1 and that
6220 // child is an editable invisible node, remove that child from
6221 // node, then subtract one from offset."
6222 } else if (0 <= offset - 1
6223 && offset - 1 < node.childNodes.length
6224 && isEditable(node.childNodes[offset - 1])
6225 && isInvisible(node.childNodes[offset - 1])) {
6226 node.removeChild(node.childNodes[offset - 1]);
6227 offset--;
6228
6229 // "Otherwise, if offset is zero and node is an inline node, or if
6230 // node is an invisible node, set offset to the index of node, then
6231 // set node to its parent."
6232 } else if ((offset == 0
6233 && isInlineNode(node))
6234 || isInvisible(node)) {
6235 offset = getNodeIndex(node);
6236 node = node.parentNode;
6237
6238 // "Otherwise, if node has a child with index offset − 1 and that
6239 // child is an editable a, remove that child from node, preserving
6240 // its descendants. Then return true."
6241 } else if (0 <= offset - 1
6242 && offset - 1 < node.childNodes.length
6243 && isEditable(node.childNodes[offset - 1])
6244 && isHtmlElement(node.childNodes[offset - 1], "a")) {
6245 removePreservingDescendants(node.childNodes[offset - 1]);
6246 return true;
6247
6248 // "Otherwise, if node has a child with index offset − 1 and that
6249 // child is not a block node or a br or an img, set node to that
6250 // child, then set offset to the length of node."
6251 } else if (0 <= offset - 1
6252 && offset - 1 < node.childNodes.length
6253 && !isBlockNode(node.childNodes[offset - 1])
6254 && !isHtmlElement(node.childNodes[offset - 1], ["br", "img"])) {
6255 node = node.childNodes[offset - 1];
6256 offset = getNodeLength(node);
6257
6258 // "Otherwise, break from this loop."
6259 } else {
6260 break;
6261 }
6262 }
6263
6264 // "If node is a Text node and offset is not zero, or if node is a
6265 // block node that has a child with index offset − 1 and that child is
6266 // a br or hr or img:"
6267 if ((node.nodeType == Node.TEXT_NODE
6268 && offset != 0)
6269 || (isBlockNode(node)
6270 && 0 <= offset - 1
6271 && offset - 1 < node.childNodes.length
6272 && isHtmlElement(node.childNodes[offset - 1], ["br", "hr", "img"]))) {
6273 // "Call collapse(node, offset) on the context object's Selection."
6274 getSelection().collapse(node, offset);
6275 getActiveRange().setEnd(node, offset);
6276
6277 // "Call extend(node, offset − 1) on the context object's
6278 // Selection."
6279 getSelection().extend(node, offset - 1);
6280 getActiveRange().setStart(node, offset - 1);
6281
6282 // "Delete the selection."
6283 deleteSelection();
6284
6285 // "Return true."
6286 return true;
6287 }
6288
6289 // "If node is an inline node, return true."
6290 if (isInlineNode(node)) {
6291 return true;
6292 }
6293
6294 // "If node is an li or dt or dd and is the first child of its parent,
6295 // and offset is zero:"
6296 if (isHtmlElement(node, ["li", "dt", "dd"])
6297 && node == node.parentNode.firstChild
6298 && offset == 0) {
6299 // "Let items be a list of all lis that are ancestors of node."
6300 //
6301 // Remember, must be in tree order.
6302 var items = [];
6303 for (var ancestor = node.parentNode; ancestor; ancestor = ancestor.p arentNode) {
6304 if (isHtmlElement(ancestor, "li")) {
6305 items.unshift(ancestor);
6306 }
6307 }
6308
6309 // "Normalize sublists of each item in items."
6310 for (var i = 0; i < items.length; i++) {
6311 normalizeSublists(items[i]);
6312 }
6313
6314 // "Record the values of the one-node list consisting of node, and
6315 // let values be the result."
6316 var values = recordValues([node]);
6317
6318 // "Split the parent of the one-node list consisting of node."
6319 splitParent([node]);
6320
6321 // "Restore the values from values."
6322 restoreValues(values);
6323
6324 // "If node is a dd or dt, and it is not an allowed child of any of
6325 // its ancestors in the same editing host, set the tag name of node
6326 // to the default single-line container name and let node be the
6327 // result."
6328 if (isHtmlElement(node, ["dd", "dt"])
6329 && getAncestors(node).every(function(ancestor) {
6330 return !inSameEditingHost(node, ancestor)
6331 || !isAllowedChild(node, ancestor)
6332 })) {
6333 node = setTagName(node, defaultSingleLineContainerName);
6334 }
6335
6336 // "Fix disallowed ancestors of node."
6337 fixDisallowedAncestors(node);
6338
6339 // "Return true."
6340 return true;
6341 }
6342
6343 // "Let start node equal node and let start offset equal offset."
6344 var startNode = node;
6345 var startOffset = offset;
6346
6347 // "Repeat the following steps:"
6348 while (true) {
6349 // "If start offset is zero, set start offset to the index of start
6350 // node and then set start node to its parent."
6351 if (startOffset == 0) {
6352 startOffset = getNodeIndex(startNode);
6353 startNode = startNode.parentNode;
6354
6355 // "Otherwise, if start node has an editable invisible child with
6356 // index start offset minus one, remove it from start node and
6357 // subtract one from start offset."
6358 } else if (0 <= startOffset - 1
6359 && startOffset - 1 < startNode.childNodes.length
6360 && isEditable(startNode.childNodes[startOffset - 1])
6361 && isInvisible(startNode.childNodes[startOffset - 1])) {
6362 startNode.removeChild(startNode.childNodes[startOffset - 1]);
6363 startOffset--;
6364
6365 // "Otherwise, break from this loop."
6366 } else {
6367 break;
6368 }
6369 }
6370
6371 // "If offset is zero, and node has an editable ancestor container in
6372 // the same editing host that's an indentation element:"
6373 if (offset == 0
6374 && getAncestors(node).concat(node).filter(function(ancestor) {
6375 return isEditable(ancestor)
6376 && inSameEditingHost(ancestor, node)
6377 && isIndentationElement(ancestor);
6378 }).length) {
6379 // "Block-extend the range whose start and end are both (node, 0),
6380 // and let new range be the result."
6381 var newRange = document.createRange();
6382 newRange.setStart(node, 0);
6383 newRange = blockExtend(newRange);
6384
6385 // "Let node list be a list of nodes, initially empty."
6386 //
6387 // "For each node current node contained in new range, append
6388 // current node to node list if the last member of node list (if
6389 // any) is not an ancestor of current node, and current node is
6390 // editable but has no editable descendants."
6391 var nodeList = getContainedNodes(newRange, function(currentNode) {
6392 return isEditable(currentNode)
6393 && !hasEditableDescendants(currentNode);
6394 });
6395
6396 // "Outdent each node in node list."
6397 for (var i = 0; i < nodeList.length; i++) {
6398 outdentNode(nodeList[i]);
6399 }
6400
6401 // "Return true."
6402 return true;
6403 }
6404
6405 // "If the child of start node with index start offset is a table,
6406 // return true."
6407 if (isHtmlElement(startNode.childNodes[startOffset], "table")) {
6408 return true;
6409 }
6410
6411 // "If start node has a child with index start offset − 1, and that
6412 // child is a table:"
6413 if (0 <= startOffset - 1
6414 && startOffset - 1 < startNode.childNodes.length
6415 && isHtmlElement(startNode.childNodes[startOffset - 1], "table")) {
6416 // "Call collapse(start node, start offset − 1) on the context
6417 // object's Selection."
6418 getSelection().collapse(startNode, startOffset - 1);
6419 getActiveRange().setStart(startNode, startOffset - 1);
6420
6421 // "Call extend(start node, start offset) on the context object's
6422 // Selection."
6423 getSelection().extend(startNode, startOffset);
6424 getActiveRange().setEnd(startNode, startOffset);
6425
6426 // "Return true."
6427 return true;
6428 }
6429
6430 // "If offset is zero; and either the child of start node with index
6431 // start offset minus one is an hr, or the child is a br whose
6432 // previousSibling is either a br or not an inline node:"
6433 if (offset == 0
6434 && (isHtmlElement(startNode.childNodes[startOffset - 1], "hr")
6435 || (
6436 isHtmlElement(startNode.childNodes[startOffset - 1], "br")
6437 && (
6438 isHtmlElement(startNode.childNodes[startOffset - 1].previous Sibling, "br")
6439 || !isInlineNode(startNode.childNodes[startOffset - 1].previ ousSibling)
6440 )
6441 )
6442 )) {
6443 // "Call collapse(start node, start offset − 1) on the context
6444 // object's Selection."
6445 getSelection().collapse(startNode, startOffset - 1);
6446 getActiveRange().setStart(startNode, startOffset - 1);
6447
6448 // "Call extend(start node, start offset) on the context object's
6449 // Selection."
6450 getSelection().extend(startNode, startOffset);
6451 getActiveRange().setEnd(startNode, startOffset);
6452
6453 // "Delete the selection."
6454 deleteSelection();
6455
6456 // "Call collapse(node, offset) on the Selection."
6457 getSelection().collapse(node, offset);
6458 getActiveRange().setStart(node, offset);
6459 getActiveRange().collapse(true);
6460
6461 // "Return true."
6462 return true;
6463 }
6464
6465 // "If the child of start node with index start offset is an li or dt
6466 // or dd, and that child's firstChild is an inline node, and start
6467 // offset is not zero:"
6468 if (isHtmlElement(startNode.childNodes[startOffset], ["li", "dt", "dd"])
6469 && isInlineNode(startNode.childNodes[startOffset].firstChild)
6470 && startOffset != 0) {
6471 // "Let previous item be the child of start node with index start
6472 // offset minus one."
6473 var previousItem = startNode.childNodes[startOffset - 1];
6474
6475 // "If previous item's lastChild is an inline node other than a br,
6476 // call createElement("br") on the context object and append the
6477 // result as the last child of previous item."
6478 if (isInlineNode(previousItem.lastChild)
6479 && !isHtmlElement(previousItem.lastChild, "br")) {
6480 previousItem.appendChild(document.createElement("br"));
6481 }
6482
6483 // "If previous item's lastChild is an inline node, call
6484 // createElement("br") on the context object and append the result
6485 // as the last child of previous item."
6486 if (isInlineNode(previousItem.lastChild)) {
6487 previousItem.appendChild(document.createElement("br"));
6488 }
6489 }
6490
6491 // "If start node's child with index start offset is an li or dt or dd,
6492 // and that child's previousSibling is also an li or dt or dd:"
6493 if (isHtmlElement(startNode.childNodes[startOffset], ["li", "dt", "dd"])
6494 && isHtmlElement(startNode.childNodes[startOffset].previousSibling, ["li ", "dt", "dd"])) {
6495 // "Call cloneRange() on the active range, and let original range
6496 // be the result."
6497 //
6498 // We need to add it to extraRanges so it will actually get updated
6499 // when moving preserving ranges.
6500 var originalRange = getActiveRange().cloneRange();
6501 extraRanges.push(originalRange);
6502
6503 // "Set start node to its child with index start offset − 1."
6504 startNode = startNode.childNodes[startOffset - 1];
6505
6506 // "Set start offset to start node's length."
6507 startOffset = getNodeLength(startNode);
6508
6509 // "Set node to start node's nextSibling."
6510 node = startNode.nextSibling;
6511
6512 // "Call collapse(start node, start offset) on the context object's
6513 // Selection."
6514 getSelection().collapse(startNode, startOffset);
6515 getActiveRange().setStart(startNode, startOffset);
6516
6517 // "Call extend(node, 0) on the context object's Selection."
6518 getSelection().extend(node, 0);
6519 getActiveRange().setEnd(node, 0);
6520
6521 // "Delete the selection."
6522 deleteSelection();
6523
6524 // "Call removeAllRanges() on the context object's Selection."
6525 getSelection().removeAllRanges();
6526
6527 // "Call addRange(original range) on the context object's
6528 // Selection."
6529 getSelection().addRange(originalRange);
6530 getActiveRange().setStart(originalRange.startContainer, originalRang e.startOffset);
6531 getActiveRange().setEnd(originalRange.endContainer, originalRange.en dOffset);
6532
6533 // "Return true."
6534 extraRanges.pop();
6535 return true;
6536 }
6537
6538 // "While start node has a child with index start offset minus one:"
6539 while (0 <= startOffset - 1
6540 && startOffset - 1 < startNode.childNodes.length) {
6541 // "If start node's child with index start offset minus one is
6542 // editable and invisible, remove it from start node, then subtract
6543 // one from start offset."
6544 if (isEditable(startNode.childNodes[startOffset - 1])
6545 && isInvisible(startNode.childNodes[startOffset - 1])) {
6546 startNode.removeChild(startNode.childNodes[startOffset - 1]);
6547 startOffset--;
6548
6549 // "Otherwise, set start node to its child with index start offset
6550 // minus one, then set start offset to the length of start node."
6551 } else {
6552 startNode = startNode.childNodes[startOffset - 1];
6553 startOffset = getNodeLength(startNode);
6554 }
6555 }
6556
6557 // "Call collapse(start node, start offset) on the context object's
6558 // Selection."
6559 getSelection().collapse(startNode, startOffset);
6560 getActiveRange().setStart(startNode, startOffset);
6561
6562 // "Call extend(node, offset) on the context object's Selection."
6563 getSelection().extend(node, offset);
6564 getActiveRange().setEnd(node, offset);
6565
6566 // "Delete the selection, with direction "backward"."
6567 deleteSelection({direction: "backward"});
6568
6569 // "Return true."
6570 return true;
6571 }
6572 };
6573
6574 //@}
6575 ///// The formatBlock command /////
6576 //@{
6577 // "A formattable block name is "address", "dd", "div", "dt", "h1", "h2", "h3",
6578 // "h4", "h5", "h6", "p", or "pre"."
6579 var formattableBlockNames = ["address", "dd", "div", "dt", "h1", "h2", "h3",
6580 "h4", "h5", "h6", "p", "pre"];
6581
6582 commands.formatblock = {
6583 preservesOverrides: true,
6584 action: function(value) {
6585 // "If value begins with a "<" character and ends with a ">" character,
6586 // remove the first and last characters from it."
6587 if (/^<.*>$/.test(value)) {
6588 value = value.slice(1, -1);
6589 }
6590
6591 // "Let value be converted to ASCII lowercase."
6592 value = value.toLowerCase();
6593
6594 // "If value is not a formattable block name, return false."
6595 if (formattableBlockNames.indexOf(value) == -1) {
6596 return false;
6597 }
6598
6599 // "Block-extend the active range, and let new range be the result."
6600 var newRange = blockExtend(getActiveRange());
6601
6602 // "Let node list be an empty list of nodes."
6603 //
6604 // "For each node node contained in new range, append node to node list
6605 // if it is editable, the last member of original node list (if any) is
6606 // not an ancestor of node, node is either a non-list single-line
6607 // container or an allowed child of "p" or a dd or dt, and node is not
6608 // the ancestor of a prohibited paragraph child."
6609 var nodeList = getContainedNodes(newRange, function(node) {
6610 return isEditable(node)
6611 && (isNonListSingleLineContainer(node)
6612 || isAllowedChild(node, "p")
6613 || isHtmlElement(node, ["dd", "dt"]))
6614 && !getDescendants(node).some(isProhibitedParagraphChild);
6615 });
6616
6617 // "Record the values of node list, and let values be the result."
6618 var values = recordValues(nodeList);
6619
6620 // "For each node in node list, while node is the descendant of an
6621 // editable HTML element in the same editing host, whose local name is
6622 // a formattable block name, and which is not the ancestor of a
6623 // prohibited paragraph child, split the parent of the one-node list
6624 // consisting of node."
6625 for (var i = 0; i < nodeList.length; i++) {
6626 var node = nodeList[i];
6627 while (getAncestors(node).some(function(ancestor) {
6628 return isEditable(ancestor)
6629 && inSameEditingHost(ancestor, node)
6630 && isHtmlElement(ancestor, formattableBlockNames)
6631 && !getDescendants(ancestor).some(isProhibitedParagraphChild );
6632 })) {
6633 splitParent([node]);
6634 }
6635 }
6636
6637 // "Restore the values from values."
6638 restoreValues(values);
6639
6640 // "While node list is not empty:"
6641 while (nodeList.length) {
6642 var sublist;
6643
6644 // "If the first member of node list is a single-line
6645 // container:"
6646 if (isSingleLineContainer(nodeList[0])) {
6647 // "Let sublist be the children of the first member of node
6648 // list."
6649 sublist = [].slice.call(nodeList[0].childNodes);
6650
6651 // "Record the values of sublist, and let values be the
6652 // result."
6653 var values = recordValues(sublist);
6654
6655 // "Remove the first member of node list from its parent,
6656 // preserving its descendants."
6657 removePreservingDescendants(nodeList[0]);
6658
6659 // "Restore the values from values."
6660 restoreValues(values);
6661
6662 // "Remove the first member from node list."
6663 nodeList.shift();
6664
6665 // "Otherwise:"
6666 } else {
6667 // "Let sublist be an empty list of nodes."
6668 sublist = [];
6669
6670 // "Remove the first member of node list and append it to
6671 // sublist."
6672 sublist.push(nodeList.shift());
6673
6674 // "While node list is not empty, and the first member of
6675 // node list is the nextSibling of the last member of
6676 // sublist, and the first member of node list is not a
6677 // single-line container, and the last member of sublist is
6678 // not a br, remove the first member of node list and
6679 // append it to sublist."
6680 while (nodeList.length
6681 && nodeList[0] == sublist[sublist.length - 1].nextSibling
6682 && !isSingleLineContainer(nodeList[0])
6683 && !isHtmlElement(sublist[sublist.length - 1], "BR")) {
6684 sublist.push(nodeList.shift());
6685 }
6686 }
6687
6688 // "Wrap sublist. If value is "div" or "p", sibling criteria
6689 // returns false; otherwise it returns true for an HTML element
6690 // with local name value and no attributes, and false otherwise.
6691 // New parent instructions return the result of running
6692 // createElement(value) on the context object. Then fix disallowed
6693 // ancestors of the result."
6694 fixDisallowedAncestors(wrap(sublist,
6695 ["div", "p"].indexOf(value) == - 1
6696 ? function(node) { return isHtmlElement(node, value) && !nod e.attributes.length }
6697 : function() { return false },
6698 function() { return document.createElement(value) }));
6699 }
6700
6701 // "Return true."
6702 return true;
6703 }, indeterm: function() {
6704 // "If the active range is null, return false."
6705 if (!getActiveRange()) {
6706 return false;
6707 }
6708
6709 // "Block-extend the active range, and let new range be the result."
6710 var newRange = blockExtend(getActiveRange());
6711
6712 // "Let node list be all visible editable nodes that are contained in
6713 // new range and have no children."
6714 var nodeList = getAllContainedNodes(newRange, function(node) {
6715 return isVisible(node)
6716 && isEditable(node)
6717 && !node.hasChildNodes();
6718 });
6719
6720 // "If node list is empty, return false."
6721 if (!nodeList.length) {
6722 return false;
6723 }
6724
6725 // "Let type be null."
6726 var type = null;
6727
6728 // "For each node in node list:"
6729 for (var i = 0; i < nodeList.length; i++) {
6730 var node = nodeList[i];
6731
6732 // "While node's parent is editable and in the same editing host as
6733 // node, and node is not an HTML element whose local name is a
6734 // formattable block name, set node to its parent."
6735 while (isEditable(node.parentNode)
6736 && inSameEditingHost(node, node.parentNode)
6737 && !isHtmlElement(node, formattableBlockNames)) {
6738 node = node.parentNode;
6739 }
6740
6741 // "Let current type be the empty string."
6742 var currentType = "";
6743
6744 // "If node is an editable HTML element whose local name is a
6745 // formattable block name, and node is not the ancestor of a
6746 // prohibited paragraph child, set current type to node's local
6747 // name."
6748 if (isEditable(node)
6749 && isHtmlElement(node, formattableBlockNames)
6750 && !getDescendants(node).some(isProhibitedParagraphChild)) {
6751 currentType = node.tagName;
6752 }
6753
6754 // "If type is null, set type to current type."
6755 if (type === null) {
6756 type = currentType;
6757
6758 // "Otherwise, if type does not equal current type, return true."
6759 } else if (type != currentType) {
6760 return true;
6761 }
6762 }
6763
6764 // "Return false."
6765 return false;
6766 }, value: function() {
6767 // "If the active range is null, return the empty string."
6768 if (!getActiveRange()) {
6769 return "";
6770 }
6771
6772 // "Block-extend the active range, and let new range be the result."
6773 var newRange = blockExtend(getActiveRange());
6774
6775 // "Let node be the first visible editable node that is contained in
6776 // new range and has no children. If there is no such node, return the
6777 // empty string."
6778 var nodes = getAllContainedNodes(newRange, function(node) {
6779 return isVisible(node)
6780 && isEditable(node)
6781 && !node.hasChildNodes();
6782 });
6783 if (!nodes.length) {
6784 return "";
6785 }
6786 var node = nodes[0];
6787
6788 // "While node's parent is editable and in the same editing host as
6789 // node, and node is not an HTML element whose local name is a
6790 // formattable block name, set node to its parent."
6791 while (isEditable(node.parentNode)
6792 && inSameEditingHost(node, node.parentNode)
6793 && !isHtmlElement(node, formattableBlockNames)) {
6794 node = node.parentNode;
6795 }
6796
6797 // "If node is an editable HTML element whose local name is a
6798 // formattable block name, and node is not the ancestor of a prohibited
6799 // paragraph child, return node's local name, converted to ASCII
6800 // lowercase."
6801 if (isEditable(node)
6802 && isHtmlElement(node, formattableBlockNames)
6803 && !getDescendants(node).some(isProhibitedParagraphChild)) {
6804 return node.tagName.toLowerCase();
6805 }
6806
6807 // "Return the empty string."
6808 return "";
6809 }
6810 };
6811
6812 //@}
6813 ///// The forwardDelete command /////
6814 //@{
6815 commands.forwarddelete = {
6816 preservesOverrides: true,
6817 action: function() {
6818 // "If the active range is not collapsed, delete the selection and
6819 // return true."
6820 if (!getActiveRange().collapsed) {
6821 deleteSelection();
6822 return true;
6823 }
6824
6825 // "Canonicalize whitespace at the active range's start."
6826 canonicalizeWhitespace(getActiveRange().startContainer, getActiveRange() .startOffset);
6827
6828 // "Let node and offset be the active range's start node and offset."
6829 var node = getActiveRange().startContainer;
6830 var offset = getActiveRange().startOffset;
6831
6832 // "Repeat the following steps:"
6833 while (true) {
6834 // "If offset is the length of node and node's nextSibling is an
6835 // editable invisible node, remove node's nextSibling from its
6836 // parent."
6837 if (offset == getNodeLength(node)
6838 && isEditable(node.nextSibling)
6839 && isInvisible(node.nextSibling)) {
6840 node.parentNode.removeChild(node.nextSibling);
6841
6842 // "Otherwise, if node has a child with index offset and that child
6843 // is an editable invisible node, remove that child from node."
6844 } else if (offset < node.childNodes.length
6845 && isEditable(node.childNodes[offset])
6846 && isInvisible(node.childNodes[offset])) {
6847 node.removeChild(node.childNodes[offset]);
6848
6849 // "Otherwise, if offset is the length of node and node is an
6850 // inline node, or if node is invisible, set offset to one plus the
6851 // index of node, then set node to its parent."
6852 } else if ((offset == getNodeLength(node)
6853 && isInlineNode(node))
6854 || isInvisible(node)) {
6855 offset = 1 + getNodeIndex(node);
6856 node = node.parentNode;
6857
6858 // "Otherwise, if node has a child with index offset and that child
6859 // is neither a block node nor a br nor an img nor a collapsed
6860 // block prop, set node to that child, then set offset to zero."
6861 } else if (offset < node.childNodes.length
6862 && !isBlockNode(node.childNodes[offset])
6863 && !isHtmlElement(node.childNodes[offset], ["br", "img"])
6864 && !isCollapsedBlockProp(node.childNodes[offset])) {
6865 node = node.childNodes[offset];
6866 offset = 0;
6867
6868 // "Otherwise, break from this loop."
6869 } else {
6870 break;
6871 }
6872 }
6873
6874 // "If node is a Text node and offset is not node's length:"
6875 if (node.nodeType == Node.TEXT_NODE
6876 && offset != getNodeLength(node)) {
6877 // "Let end offset be offset plus one."
6878 var endOffset = offset + 1;
6879
6880 // "While end offset is not node's length and the end offsetth
6881 // element of node's data has general category M when interpreted
6882 // as a Unicode code point, add one to end offset."
6883 //
6884 // TODO: Not even going to try handling anything beyond the most
6885 // basic combining marks, since I couldn't find a good list. I
6886 // special-case a few Hebrew diacritics too to test basic coverage
6887 // of non-Latin stuff.
6888 while (endOffset != node.length
6889 && /^[\u0300-\u036f\u0591-\u05bd\u05c1\u05c2]$/.test(node.data[endOf fset])) {
6890 endOffset++;
6891 }
6892
6893 // "Call collapse(node, offset) on the context object's Selection."
6894 getSelection().collapse(node, offset);
6895 getActiveRange().setStart(node, offset);
6896
6897 // "Call extend(node, end offset) on the context object's
6898 // Selection."
6899 getSelection().extend(node, endOffset);
6900 getActiveRange().setEnd(node, endOffset);
6901
6902 // "Delete the selection."
6903 deleteSelection();
6904
6905 // "Return true."
6906 return true;
6907 }
6908
6909 // "If node is an inline node, return true."
6910 if (isInlineNode(node)) {
6911 return true;
6912 }
6913
6914 // "If node has a child with index offset and that child is a br or hr
6915 // or img, but is not a collapsed block prop:"
6916 if (offset < node.childNodes.length
6917 && isHtmlElement(node.childNodes[offset], ["br", "hr", "img"])
6918 && !isCollapsedBlockProp(node.childNodes[offset])) {
6919 // "Call collapse(node, offset) on the context object's Selection."
6920 getSelection().collapse(node, offset);
6921 getActiveRange().setStart(node, offset);
6922
6923 // "Call extend(node, offset + 1) on the context object's
6924 // Selection."
6925 getSelection().extend(node, offset + 1);
6926 getActiveRange().setEnd(node, offset + 1);
6927
6928 // "Delete the selection."
6929 deleteSelection();
6930
6931 // "Return true."
6932 return true;
6933 }
6934
6935 // "Let end node equal node and let end offset equal offset."
6936 var endNode = node;
6937 var endOffset = offset;
6938
6939 // "If end node has a child with index end offset, and that child is a
6940 // collapsed block prop, add one to end offset."
6941 if (endOffset < endNode.childNodes.length
6942 && isCollapsedBlockProp(endNode.childNodes[endOffset])) {
6943 endOffset++;
6944 }
6945
6946 // "Repeat the following steps:"
6947 while (true) {
6948 // "If end offset is the length of end node, set end offset to one
6949 // plus the index of end node and then set end node to its parent."
6950 if (endOffset == getNodeLength(endNode)) {
6951 endOffset = 1 + getNodeIndex(endNode);
6952 endNode = endNode.parentNode;
6953
6954 // "Otherwise, if end node has a an editable invisible child with
6955 // index end offset, remove it from end node."
6956 } else if (endOffset < endNode.childNodes.length
6957 && isEditable(endNode.childNodes[endOffset])
6958 && isInvisible(endNode.childNodes[endOffset])) {
6959 endNode.removeChild(endNode.childNodes[endOffset]);
6960
6961 // "Otherwise, break from this loop."
6962 } else {
6963 break;
6964 }
6965 }
6966
6967 // "If the child of end node with index end offset minus one is a
6968 // table, return true."
6969 if (isHtmlElement(endNode.childNodes[endOffset - 1], "table")) {
6970 return true;
6971 }
6972
6973 // "If the child of end node with index end offset is a table:"
6974 if (isHtmlElement(endNode.childNodes[endOffset], "table")) {
6975 // "Call collapse(end node, end offset) on the context object's
6976 // Selection."
6977 getSelection().collapse(endNode, endOffset);
6978 getActiveRange().setStart(endNode, endOffset);
6979
6980 // "Call extend(end node, end offset + 1) on the context object's
6981 // Selection."
6982 getSelection().extend(endNode, endOffset + 1);
6983 getActiveRange().setEnd(endNode, endOffset + 1);
6984
6985 // "Return true."
6986 return true;
6987 }
6988
6989 // "If offset is the length of node, and the child of end node with
6990 // index end offset is an hr or br:"
6991 if (offset == getNodeLength(node)
6992 && isHtmlElement(endNode.childNodes[endOffset], ["br", "hr"])) {
6993 // "Call collapse(end node, end offset) on the context object's
6994 // Selection."
6995 getSelection().collapse(endNode, endOffset);
6996 getActiveRange().setStart(endNode, endOffset);
6997
6998 // "Call extend(end node, end offset + 1) on the context object's
6999 // Selection."
7000 getSelection().extend(endNode, endOffset + 1);
7001 getActiveRange().setEnd(endNode, endOffset + 1);
7002
7003 // "Delete the selection."
7004 deleteSelection();
7005
7006 // "Call collapse(node, offset) on the Selection."
7007 getSelection().collapse(node, offset);
7008 getActiveRange().setStart(node, offset);
7009 getActiveRange().collapse(true);
7010
7011 // "Return true."
7012 return true;
7013 }
7014
7015 // "While end node has a child with index end offset:"
7016 while (endOffset < endNode.childNodes.length) {
7017 // "If end node's child with index end offset is editable and
7018 // invisible, remove it from end node."
7019 if (isEditable(endNode.childNodes[endOffset])
7020 && isInvisible(endNode.childNodes[endOffset])) {
7021 endNode.removeChild(endNode.childNodes[endOffset]);
7022
7023 // "Otherwise, set end node to its child with index end offset and
7024 // set end offset to zero."
7025 } else {
7026 endNode = endNode.childNodes[endOffset];
7027 endOffset = 0;
7028 }
7029 }
7030
7031 // "Call collapse(node, offset) on the context object's Selection."
7032 getSelection().collapse(node, offset);
7033 getActiveRange().setStart(node, offset);
7034
7035 // "Call extend(end node, end offset) on the context object's
7036 // Selection."
7037 getSelection().extend(endNode, endOffset);
7038 getActiveRange().setEnd(endNode, endOffset);
7039
7040 // "Delete the selection."
7041 deleteSelection();
7042
7043 // "Return true."
7044 return true;
7045 }
7046 };
7047
7048 //@}
7049 ///// The indent command /////
7050 //@{
7051 commands.indent = {
7052 preservesOverrides: true,
7053 action: function() {
7054 // "Let items be a list of all lis that are ancestor containers of the
7055 // active range's start and/or end node."
7056 //
7057 // Has to be in tree order, remember!
7058 var items = [];
7059 for (var node = getActiveRange().endContainer; node != getActiveRange(). commonAncestorContainer; node = node.parentNode) {
7060 if (isHtmlElement(node, "LI")) {
7061 items.unshift(node);
7062 }
7063 }
7064 for (var node = getActiveRange().startContainer; node != getActiveRange( ).commonAncestorContainer; node = node.parentNode) {
7065 if (isHtmlElement(node, "LI")) {
7066 items.unshift(node);
7067 }
7068 }
7069 for (var node = getActiveRange().commonAncestorContainer; node; node = n ode.parentNode) {
7070 if (isHtmlElement(node, "LI")) {
7071 items.unshift(node);
7072 }
7073 }
7074
7075 // "For each item in items, normalize sublists of item."
7076 for (var i = 0; i < items.length; i++) {
7077 normalizeSublists(items[i]);
7078 }
7079
7080 // "Block-extend the active range, and let new range be the result."
7081 var newRange = blockExtend(getActiveRange());
7082
7083 // "Let node list be a list of nodes, initially empty."
7084 var nodeList = [];
7085
7086 // "For each node node contained in new range, if node is editable and
7087 // is an allowed child of "div" or "ol" and if the last member of node
7088 // list (if any) is not an ancestor of node, append node to node list."
7089 nodeList = getContainedNodes(newRange, function(node) {
7090 return isEditable(node)
7091 && (isAllowedChild(node, "div")
7092 || isAllowedChild(node, "ol"));
7093 });
7094
7095 // "If the first visible member of node list is an li whose parent is
7096 // an ol or ul:"
7097 if (isHtmlElement(nodeList.filter(isVisible)[0], "li")
7098 && isHtmlElement(nodeList.filter(isVisible)[0].parentNode, ["ol", "ul"]) ) {
7099 // "Let sibling be node list's first visible member's
7100 // previousSibling."
7101 var sibling = nodeList.filter(isVisible)[0].previousSibling;
7102
7103 // "While sibling is invisible, set sibling to its
7104 // previousSibling."
7105 while (isInvisible(sibling)) {
7106 sibling = sibling.previousSibling;
7107 }
7108
7109 // "If sibling is an li, normalize sublists of sibling."
7110 if (isHtmlElement(sibling, "li")) {
7111 normalizeSublists(sibling);
7112 }
7113 }
7114
7115 // "While node list is not empty:"
7116 while (nodeList.length) {
7117 // "Let sublist be a list of nodes, initially empty."
7118 var sublist = [];
7119
7120 // "Remove the first member of node list and append it to sublist."
7121 sublist.push(nodeList.shift());
7122
7123 // "While the first member of node list is the nextSibling of the
7124 // last member of sublist, remove the first member of node list and
7125 // append it to sublist."
7126 while (nodeList.length
7127 && nodeList[0] == sublist[sublist.length - 1].nextSibling) {
7128 sublist.push(nodeList.shift());
7129 }
7130
7131 // "Indent sublist."
7132 indentNodes(sublist);
7133 }
7134
7135 // "Return true."
7136 return true;
7137 }
7138 };
7139
7140 //@}
7141 ///// The insertHorizontalRule command /////
7142 //@{
7143 commands.inserthorizontalrule = {
7144 preservesOverrides: true,
7145 action: function() {
7146 // "Let start node, start offset, end node, and end offset be the
7147 // active range's start and end nodes and offsets."
7148 var startNode = getActiveRange().startContainer;
7149 var startOffset = getActiveRange().startOffset;
7150 var endNode = getActiveRange().endContainer;
7151 var endOffset = getActiveRange().endOffset;
7152
7153 // "While start offset is 0 and start node's parent is not null, set
7154 // start offset to start node's index, then set start node to its
7155 // parent."
7156 while (startOffset == 0
7157 && startNode.parentNode) {
7158 startOffset = getNodeIndex(startNode);
7159 startNode = startNode.parentNode;
7160 }
7161
7162 // "While end offset is end node's length, and end node's parent is not
7163 // null, set end offset to one plus end node's index, then set end node
7164 // to its parent."
7165 while (endOffset == getNodeLength(endNode)
7166 && endNode.parentNode) {
7167 endOffset = 1 + getNodeIndex(endNode);
7168 endNode = endNode.parentNode;
7169 }
7170
7171 // "Call collapse(start node, start offset) on the context object's
7172 // Selection."
7173 getSelection().collapse(startNode, startOffset);
7174 getActiveRange().setStart(startNode, startOffset);
7175
7176 // "Call extend(end node, end offset) on the context object's
7177 // Selection."
7178 getSelection().extend(endNode, endOffset);
7179 getActiveRange().setEnd(endNode, endOffset);
7180
7181 // "Delete the selection, with block merging false."
7182 deleteSelection({blockMerging: false});
7183
7184 // "If the active range's start node is neither editable nor an editing
7185 // host, return true."
7186 if (!isEditable(getActiveRange().startContainer)
7187 && !isEditingHost(getActiveRange().startContainer)) {
7188 return true;
7189 }
7190
7191 // "If the active range's start node is a Text node and its start
7192 // offset is zero, call collapse() on the context object's Selection,
7193 // with first argument the active range's start node's parent and
7194 // second argument the active range's start node's index."
7195 if (getActiveRange().startContainer.nodeType == Node.TEXT_NODE
7196 && getActiveRange().startOffset == 0) {
7197 var newNode = getActiveRange().startContainer.parentNode;
7198 var newOffset = getNodeIndex(getActiveRange().startContainer);
7199 getSelection().collapse(newNode, newOffset);
7200 getActiveRange().setStart(newNode, newOffset);
7201 getActiveRange().collapse(true);
7202 }
7203
7204 // "If the active range's start node is a Text node and its start
7205 // offset is the length of its start node, call collapse() on the
7206 // context object's Selection, with first argument the active range's
7207 // start node's parent, and the second argument one plus the active
7208 // range's start node's index."
7209 if (getActiveRange().startContainer.nodeType == Node.TEXT_NODE
7210 && getActiveRange().startOffset == getNodeLength(getActiveRange().startC ontainer)) {
7211 var newNode = getActiveRange().startContainer.parentNode;
7212 var newOffset = 1 + getNodeIndex(getActiveRange().startContainer);
7213 getSelection().collapse(newNode, newOffset);
7214 getActiveRange().setStart(newNode, newOffset);
7215 getActiveRange().collapse(true);
7216 }
7217
7218 // "Let hr be the result of calling createElement("hr") on the
7219 // context object."
7220 var hr = document.createElement("hr");
7221
7222 // "Run insertNode(hr) on the active range."
7223 getActiveRange().insertNode(hr);
7224
7225 // "Fix disallowed ancestors of hr."
7226 fixDisallowedAncestors(hr);
7227
7228 // "Run collapse() on the context object's Selection, with first
7229 // argument hr's parent and the second argument equal to one plus hr's
7230 // index."
7231 getSelection().collapse(hr.parentNode, 1 + getNodeIndex(hr));
7232 getActiveRange().setStart(hr.parentNode, 1 + getNodeIndex(hr));
7233 getActiveRange().collapse(true);
7234
7235 // "Return true."
7236 return true;
7237 }
7238 };
7239
7240 //@}
7241 ///// The insertHTML command /////
7242 //@{
7243 commands.inserthtml = {
7244 preservesOverrides: true,
7245 action: function(value) {
7246 // "Delete the selection."
7247 deleteSelection();
7248
7249 // "If the active range's start node is neither editable nor an editing
7250 // host, return true."
7251 if (!isEditable(getActiveRange().startContainer)
7252 && !isEditingHost(getActiveRange().startContainer)) {
7253 return true;
7254 }
7255
7256 // "Let frag be the result of calling createContextualFragment(value)
7257 // on the active range."
7258 var frag = getActiveRange().createContextualFragment(value);
7259
7260 // "Let last child be the lastChild of frag."
7261 var lastChild = frag.lastChild;
7262
7263 // "If last child is null, return true."
7264 if (!lastChild) {
7265 return true;
7266 }
7267
7268 // "Let descendants be all descendants of frag."
7269 var descendants = getDescendants(frag);
7270
7271 // "If the active range's start node is a block node:"
7272 if (isBlockNode(getActiveRange().startContainer)) {
7273 // "Let collapsed block props be all editable collapsed block prop
7274 // children of the active range's start node that have index
7275 // greater than or equal to the active range's start offset."
7276 //
7277 // "For each node in collapsed block props, remove node from its
7278 // parent."
7279 [].filter.call(getActiveRange().startContainer.childNodes, function( node) {
7280 return isEditable(node)
7281 && isCollapsedBlockProp(node)
7282 && getNodeIndex(node) >= getActiveRange().startOffset;
7283 }).forEach(function(node) {
7284 node.parentNode.removeChild(node);
7285 });
7286 }
7287
7288 // "Call insertNode(frag) on the active range."
7289 getActiveRange().insertNode(frag);
7290
7291 // "If the active range's start node is a block node with no visible
7292 // children, call createElement("br") on the context object and append
7293 // the result as the last child of the active range's start node."
7294 if (isBlockNode(getActiveRange().startContainer)
7295 && ![].some.call(getActiveRange().startContainer.childNodes, isVisible)) {
7296 getActiveRange().startContainer.appendChild(document.createElement(" br"));
7297 }
7298
7299 // "Call collapse() on the context object's Selection, with last
7300 // child's parent as the first argument and one plus its index as the
7301 // second."
7302 getActiveRange().setStart(lastChild.parentNode, 1 + getNodeIndex(lastChi ld));
7303 getActiveRange().setEnd(lastChild.parentNode, 1 + getNodeIndex(lastChild ));
7304
7305 // "Fix disallowed ancestors of each member of descendants."
7306 for (var i = 0; i < descendants.length; i++) {
7307 fixDisallowedAncestors(descendants[i]);
7308 }
7309
7310 // "Return true."
7311 return true;
7312 }
7313 };
7314
7315 //@}
7316 ///// The insertImage command /////
7317 //@{
7318 commands.insertimage = {
7319 preservesOverrides: true,
7320 action: function(value) {
7321 // "If value is the empty string, return false."
7322 if (value === "") {
7323 return false;
7324 }
7325
7326 // "Delete the selection, with strip wrappers false."
7327 deleteSelection({stripWrappers: false});
7328
7329 // "Let range be the active range."
7330 var range = getActiveRange();
7331
7332 // "If the active range's start node is neither editable nor an editing
7333 // host, return true."
7334 if (!isEditable(getActiveRange().startContainer)
7335 && !isEditingHost(getActiveRange().startContainer)) {
7336 return true;
7337 }
7338
7339 // "If range's start node is a block node whose sole child is a br, and
7340 // its start offset is 0, remove its start node's child from it."
7341 if (isBlockNode(range.startContainer)
7342 && range.startContainer.childNodes.length == 1
7343 && isHtmlElement(range.startContainer.firstChild, "br")
7344 && range.startOffset == 0) {
7345 range.startContainer.removeChild(range.startContainer.firstChild);
7346 }
7347
7348 // "Let img be the result of calling createElement("img") on the
7349 // context object."
7350 var img = document.createElement("img");
7351
7352 // "Run setAttribute("src", value) on img."
7353 img.setAttribute("src", value);
7354
7355 // "Run insertNode(img) on the range."
7356 range.insertNode(img);
7357
7358 // "Run collapse() on the Selection, with first argument equal to the
7359 // parent of img and the second argument equal to one plus the index of
7360 // img."
7361 //
7362 // Not everyone actually supports collapse(), so we do it manually
7363 // instead. Also, we need to modify the actual range we're given as
7364 // well, for the sake of autoimplementation.html's range-filling-in.
7365 range.setStart(img.parentNode, 1 + getNodeIndex(img));
7366 range.setEnd(img.parentNode, 1 + getNodeIndex(img));
7367 getSelection().removeAllRanges();
7368 getSelection().addRange(range);
7369
7370 // IE adds width and height attributes for some reason, so remove those
7371 // to actually do what the spec says.
7372 img.removeAttribute("width");
7373 img.removeAttribute("height");
7374
7375 // "Return true."
7376 return true;
7377 }
7378 };
7379
7380 //@}
7381 ///// The insertLineBreak command /////
7382 //@{
7383 commands.insertlinebreak = {
7384 preservesOverrides: true,
7385 action: function(value) {
7386 // "Delete the selection, with strip wrappers false."
7387 deleteSelection({stripWrappers: false});
7388
7389 // "If the active range's start node is neither editable nor an editing
7390 // host, return true."
7391 if (!isEditable(getActiveRange().startContainer)
7392 && !isEditingHost(getActiveRange().startContainer)) {
7393 return true;
7394 }
7395
7396 // "If the active range's start node is an Element, and "br" is not an
7397 // allowed child of it, return true."
7398 if (getActiveRange().startContainer.nodeType == Node.ELEMENT_NODE
7399 && !isAllowedChild("br", getActiveRange().startContainer)) {
7400 return true;
7401 }
7402
7403 // "If the active range's start node is not an Element, and "br" is not
7404 // an allowed child of the active range's start node's parent, return
7405 // true."
7406 if (getActiveRange().startContainer.nodeType != Node.ELEMENT_NODE
7407 && !isAllowedChild("br", getActiveRange().startContainer.parentNode)) {
7408 return true;
7409 }
7410
7411 // "If the active range's start node is a Text node and its start
7412 // offset is zero, call collapse() on the context object's Selection,
7413 // with first argument equal to the active range's start node's parent
7414 // and second argument equal to the active range's start node's index."
7415 if (getActiveRange().startContainer.nodeType == Node.TEXT_NODE
7416 && getActiveRange().startOffset == 0) {
7417 var newNode = getActiveRange().startContainer.parentNode;
7418 var newOffset = getNodeIndex(getActiveRange().startContainer);
7419 getSelection().collapse(newNode, newOffset);
7420 getActiveRange().setStart(newNode, newOffset);
7421 getActiveRange().setEnd(newNode, newOffset);
7422 }
7423
7424 // "If the active range's start node is a Text node and its start
7425 // offset is the length of its start node, call collapse() on the
7426 // context object's Selection, with first argument equal to the active
7427 // range's start node's parent and second argument equal to one plus
7428 // the active range's start node's index."
7429 if (getActiveRange().startContainer.nodeType == Node.TEXT_NODE
7430 && getActiveRange().startOffset == getNodeLength(getActiveRange().startC ontainer)) {
7431 var newNode = getActiveRange().startContainer.parentNode;
7432 var newOffset = 1 + getNodeIndex(getActiveRange().startContainer);
7433 getSelection().collapse(newNode, newOffset);
7434 getActiveRange().setStart(newNode, newOffset);
7435 getActiveRange().setEnd(newNode, newOffset);
7436 }
7437
7438 // "Let br be the result of calling createElement("br") on the context
7439 // object."
7440 var br = document.createElement("br");
7441
7442 // "Call insertNode(br) on the active range."
7443 getActiveRange().insertNode(br);
7444
7445 // "Call collapse() on the context object's Selection, with br's parent
7446 // as the first argument and one plus br's index as the second
7447 // argument."
7448 getSelection().collapse(br.parentNode, 1 + getNodeIndex(br));
7449 getActiveRange().setStart(br.parentNode, 1 + getNodeIndex(br));
7450 getActiveRange().setEnd(br.parentNode, 1 + getNodeIndex(br));
7451
7452 // "If br is a collapsed line break, call createElement("br") on the
7453 // context object and let extra br be the result, then call
7454 // insertNode(extra br) on the active range."
7455 if (isCollapsedLineBreak(br)) {
7456 getActiveRange().insertNode(document.createElement("br"));
7457
7458 // Compensate for nonstandard implementations of insertNode
7459 getSelection().collapse(br.parentNode, 1 + getNodeIndex(br));
7460 getActiveRange().setStart(br.parentNode, 1 + getNodeIndex(br));
7461 getActiveRange().setEnd(br.parentNode, 1 + getNodeIndex(br));
7462 }
7463
7464 // "Return true."
7465 return true;
7466 }
7467 };
7468
7469 //@}
7470 ///// The insertOrderedList command /////
7471 //@{
7472 commands.insertorderedlist = {
7473 preservesOverrides: true,
7474 // "Toggle lists with tag name "ol", then return true."
7475 action: function() { toggleLists("ol"); return true },
7476 // "True if the selection's list state is "mixed" or "mixed ol", false
7477 // otherwise."
7478 indeterm: function() { return /^mixed( ol)?$/.test(getSelectionListState()) },
7479 // "True if the selection's list state is "ol", false otherwise."
7480 state: function() { return getSelectionListState() == "ol" },
7481 };
7482
7483 //@}
7484 ///// The insertParagraph command /////
7485 //@{
7486 commands.insertparagraph = {
7487 preservesOverrides: true,
7488 action: function() {
7489 // "Delete the selection."
7490 deleteSelection();
7491
7492 // "If the active range's start node is neither editable nor an editing
7493 // host, return true."
7494 if (!isEditable(getActiveRange().startContainer)
7495 && !isEditingHost(getActiveRange().startContainer)) {
7496 return true;
7497 }
7498
7499 // "Let node and offset be the active range's start node and offset."
7500 var node = getActiveRange().startContainer;
7501 var offset = getActiveRange().startOffset;
7502
7503 // "If node is a Text node, and offset is neither 0 nor the length of
7504 // node, call splitText(offset) on node."
7505 if (node.nodeType == Node.TEXT_NODE
7506 && offset != 0
7507 && offset != getNodeLength(node)) {
7508 node.splitText(offset);
7509 }
7510
7511 // "If node is a Text node and offset is its length, set offset to one
7512 // plus the index of node, then set node to its parent."
7513 if (node.nodeType == Node.TEXT_NODE
7514 && offset == getNodeLength(node)) {
7515 offset = 1 + getNodeIndex(node);
7516 node = node.parentNode;
7517 }
7518
7519 // "If node is a Text or Comment node, set offset to the index of node,
7520 // then set node to its parent."
7521 if (node.nodeType == Node.TEXT_NODE
7522 || node.nodeType == Node.COMMENT_NODE) {
7523 offset = getNodeIndex(node);
7524 node = node.parentNode;
7525 }
7526
7527 // "Call collapse(node, offset) on the context object's Selection."
7528 getSelection().collapse(node, offset);
7529 getActiveRange().setStart(node, offset);
7530 getActiveRange().setEnd(node, offset);
7531
7532 // "Let container equal node."
7533 var container = node;
7534
7535 // "While container is not a single-line container, and container's
7536 // parent is editable and in the same editing host as node, set
7537 // container to its parent."
7538 while (!isSingleLineContainer(container)
7539 && isEditable(container.parentNode)
7540 && inSameEditingHost(node, container.parentNode)) {
7541 container = container.parentNode;
7542 }
7543
7544 // "If container is an editable single-line container in the same
7545 // editing host as node, and its local name is "p" or "div":"
7546 if (isEditable(container)
7547 && isSingleLineContainer(container)
7548 && inSameEditingHost(node, container.parentNode)
7549 && (container.tagName == "P" || container.tagName == "DIV")) {
7550 // "Let outer container equal container."
7551 var outerContainer = container;
7552
7553 // "While outer container is not a dd or dt or li, and outer
7554 // container's parent is editable, set outer container to its
7555 // parent."
7556 while (!isHtmlElement(outerContainer, ["dd", "dt", "li"])
7557 && isEditable(outerContainer.parentNode)) {
7558 outerContainer = outerContainer.parentNode;
7559 }
7560
7561 // "If outer container is a dd or dt or li, set container to outer
7562 // container."
7563 if (isHtmlElement(outerContainer, ["dd", "dt", "li"])) {
7564 container = outerContainer;
7565 }
7566 }
7567
7568 // "If container is not editable or not in the same editing host as
7569 // node or is not a single-line container:"
7570 if (!isEditable(container)
7571 || !inSameEditingHost(container, node)
7572 || !isSingleLineContainer(container)) {
7573 // "Let tag be the default single-line container name."
7574 var tag = defaultSingleLineContainerName;
7575
7576 // "Block-extend the active range, and let new range be the
7577 // result."
7578 var newRange = blockExtend(getActiveRange());
7579
7580 // "Let node list be a list of nodes, initially empty."
7581 //
7582 // "Append to node list the first node in tree order that is
7583 // contained in new range and is an allowed child of "p", if any."
7584 var nodeList = getContainedNodes(newRange, function(node) { return i sAllowedChild(node, "p") })
7585 .slice(0, 1);
7586
7587 // "If node list is empty:"
7588 if (!nodeList.length) {
7589 // "If tag is not an allowed child of the active range's start
7590 // node, return true."
7591 if (!isAllowedChild(tag, getActiveRange().startContainer)) {
7592 return true;
7593 }
7594
7595 // "Set container to the result of calling createElement(tag)
7596 // on the context object."
7597 container = document.createElement(tag);
7598
7599 // "Call insertNode(container) on the active range."
7600 getActiveRange().insertNode(container);
7601
7602 // "Call createElement("br") on the context object, and append
7603 // the result as the last child of container."
7604 container.appendChild(document.createElement("br"));
7605
7606 // "Call collapse(container, 0) on the context object's
7607 // Selection."
7608 getSelection().collapse(container, 0);
7609 getActiveRange().setStart(container, 0);
7610 getActiveRange().setEnd(container, 0);
7611
7612 // "Return true."
7613 return true;
7614 }
7615
7616 // "While the nextSibling of the last member of node list is not
7617 // null and is an allowed child of "p", append it to node list."
7618 while (nodeList[nodeList.length - 1].nextSibling
7619 && isAllowedChild(nodeList[nodeList.length - 1].nextSibling, "p")) {
7620 nodeList.push(nodeList[nodeList.length - 1].nextSibling);
7621 }
7622
7623 // "Wrap node list, with sibling criteria returning false and new
7624 // parent instructions returning the result of calling
7625 // createElement(tag) on the context object. Set container to the
7626 // result."
7627 container = wrap(nodeList,
7628 function() { return false },
7629 function() { return document.createElement(tag) }
7630 );
7631 }
7632
7633 // "If container's local name is "address", "listing", or "pre":"
7634 if (container.tagName == "ADDRESS"
7635 || container.tagName == "LISTING"
7636 || container.tagName == "PRE") {
7637 // "Let br be the result of calling createElement("br") on the
7638 // context object."
7639 var br = document.createElement("br");
7640
7641 // "Call insertNode(br) on the active range."
7642 getActiveRange().insertNode(br);
7643
7644 // "Call collapse(node, offset + 1) on the context object's
7645 // Selection."
7646 getSelection().collapse(node, offset + 1);
7647 getActiveRange().setStart(node, offset + 1);
7648 getActiveRange().setEnd(node, offset + 1);
7649
7650 // "If br is the last descendant of container, let br be the result
7651 // of calling createElement("br") on the context object, then call
7652 // insertNode(br) on the active range."
7653 //
7654 // Work around browser bugs: some browsers select the
7655 // newly-inserted node, not per spec.
7656 if (!isDescendant(nextNode(br), container)) {
7657 getActiveRange().insertNode(document.createElement("br"));
7658 getSelection().collapse(node, offset + 1);
7659 getActiveRange().setEnd(node, offset + 1);
7660 }
7661
7662 // "Return true."
7663 return true;
7664 }
7665
7666 // "If container's local name is "li", "dt", or "dd"; and either it has
7667 // no children or it has a single child and that child is a br:"
7668 if (["LI", "DT", "DD"].indexOf(container.tagName) != -1
7669 && (!container.hasChildNodes()
7670 || (container.childNodes.length == 1
7671 && isHtmlElement(container.firstChild, "br")))) {
7672 // "Split the parent of the one-node list consisting of container."
7673 splitParent([container]);
7674
7675 // "If container has no children, call createElement("br") on the
7676 // context object and append the result as the last child of
7677 // container."
7678 if (!container.hasChildNodes()) {
7679 container.appendChild(document.createElement("br"));
7680 }
7681
7682 // "If container is a dd or dt, and it is not an allowed child of
7683 // any of its ancestors in the same editing host, set the tag name
7684 // of container to the default single-line container name and let
7685 // container be the result."
7686 if (isHtmlElement(container, ["dd", "dt"])
7687 && getAncestors(container).every(function(ancestor) {
7688 return !inSameEditingHost(container, ancestor)
7689 || !isAllowedChild(container, ancestor)
7690 })) {
7691 container = setTagName(container, defaultSingleLineContainerName );
7692 }
7693
7694 // "Fix disallowed ancestors of container."
7695 fixDisallowedAncestors(container);
7696
7697 // "Return true."
7698 return true;
7699 }
7700
7701 // "Let new line range be a new range whose start is the same as
7702 // the active range's, and whose end is (container, length of
7703 // container)."
7704 var newLineRange = document.createRange();
7705 newLineRange.setStart(getActiveRange().startContainer, getActiveRange(). startOffset);
7706 newLineRange.setEnd(container, getNodeLength(container));
7707
7708 // "While new line range's start offset is zero and its start node is
7709 // not a prohibited paragraph child, set its start to (parent of start
7710 // node, index of start node)."
7711 while (newLineRange.startOffset == 0
7712 && !isProhibitedParagraphChild(newLineRange.startContainer)) {
7713 newLineRange.setStart(newLineRange.startContainer.parentNode, getNod eIndex(newLineRange.startContainer));
7714 }
7715
7716 // "While new line range's start offset is the length of its start node
7717 // and its start node is not a prohibited paragraph child, set its
7718 // start to (parent of start node, 1 + index of start node)."
7719 while (newLineRange.startOffset == getNodeLength(newLineRange.startConta iner)
7720 && !isProhibitedParagraphChild(newLineRange.startContainer)) {
7721 newLineRange.setStart(newLineRange.startContainer.parentNode, 1 + ge tNodeIndex(newLineRange.startContainer));
7722 }
7723
7724 // "Let end of line be true if new line range contains either nothing
7725 // or a single br, and false otherwise."
7726 var containedInNewLineRange = getContainedNodes(newLineRange);
7727 var endOfLine = !containedInNewLineRange.length
7728 || (containedInNewLineRange.length == 1
7729 && isHtmlElement(containedInNewLineRange[0], "br"));
7730
7731 // "If the local name of container is "h1", "h2", "h3", "h4", "h5", or
7732 // "h6", and end of line is true, let new container name be the default
7733 // single-line container name."
7734 var newContainerName;
7735 if (/^H[1-6]$/.test(container.tagName)
7736 && endOfLine) {
7737 newContainerName = defaultSingleLineContainerName;
7738
7739 // "Otherwise, if the local name of container is "dt" and end of line
7740 // is true, let new container name be "dd"."
7741 } else if (container.tagName == "DT"
7742 && endOfLine) {
7743 newContainerName = "dd";
7744
7745 // "Otherwise, if the local name of container is "dd" and end of line
7746 // is true, let new container name be "dt"."
7747 } else if (container.tagName == "DD"
7748 && endOfLine) {
7749 newContainerName = "dt";
7750
7751 // "Otherwise, let new container name be the local name of container."
7752 } else {
7753 newContainerName = container.tagName.toLowerCase();
7754 }
7755
7756 // "Let new container be the result of calling createElement(new
7757 // container name) on the context object."
7758 var newContainer = document.createElement(newContainerName);
7759
7760 // "Copy all attributes of container to new container."
7761 for (var i = 0; i < container.attributes.length; i++) {
7762 newContainer.setAttributeNS(container.attributes[i].namespaceURI, co ntainer.attributes[i].name, container.attributes[i].value);
7763 }
7764
7765 // "If new container has an id attribute, unset it."
7766 newContainer.removeAttribute("id");
7767
7768 // "Insert new container into the parent of container immediately after
7769 // container."
7770 container.parentNode.insertBefore(newContainer, container.nextSibling);
7771
7772 // "Let contained nodes be all nodes contained in new line range."
7773 var containedNodes = getAllContainedNodes(newLineRange);
7774
7775 // "Let frag be the result of calling extractContents() on new line
7776 // range."
7777 var frag = newLineRange.extractContents();
7778
7779 // "Unset the id attribute (if any) of each Element descendant of frag
7780 // that is not in contained nodes."
7781 var descendants = getDescendants(frag);
7782 for (var i = 0; i < descendants.length; i++) {
7783 if (descendants[i].nodeType == Node.ELEMENT_NODE
7784 && containedNodes.indexOf(descendants[i]) == -1) {
7785 descendants[i].removeAttribute("id");
7786 }
7787 }
7788
7789 // "Call appendChild(frag) on new container."
7790 newContainer.appendChild(frag);
7791
7792 // "While container's lastChild is a prohibited paragraph child, set
7793 // container to its lastChild."
7794 while (isProhibitedParagraphChild(container.lastChild)) {
7795 container = container.lastChild;
7796 }
7797
7798 // "While new container's lastChild is a prohibited paragraph child,
7799 // set new container to its lastChild."
7800 while (isProhibitedParagraphChild(newContainer.lastChild)) {
7801 newContainer = newContainer.lastChild;
7802 }
7803
7804 // "If container has no visible children, call createElement("br") on
7805 // the context object, and append the result as the last child of
7806 // container."
7807 if (![].some.call(container.childNodes, isVisible)) {
7808 container.appendChild(document.createElement("br"));
7809 }
7810
7811 // "If new container has no visible children, call createElement("br")
7812 // on the context object, and append the result as the last child of
7813 // new container."
7814 if (![].some.call(newContainer.childNodes, isVisible)) {
7815 newContainer.appendChild(document.createElement("br"));
7816 }
7817
7818 // "Call collapse(new container, 0) on the context object's Selection."
7819 getSelection().collapse(newContainer, 0);
7820 getActiveRange().setStart(newContainer, 0);
7821 getActiveRange().setEnd(newContainer, 0);
7822
7823 // "Return true."
7824 return true;
7825 }
7826 };
7827
7828 //@}
7829 ///// The insertText command /////
7830 //@{
7831 commands.inserttext = {
7832 action: function(value) {
7833 // "Delete the selection, with strip wrappers false."
7834 deleteSelection({stripWrappers: false});
7835
7836 // "If the active range's start node is neither editable nor an editing
7837 // host, return true."
7838 if (!isEditable(getActiveRange().startContainer)
7839 && !isEditingHost(getActiveRange().startContainer)) {
7840 return true;
7841 }
7842
7843 // "If value's length is greater than one:"
7844 if (value.length > 1) {
7845 // "For each element el in value, take the action for the
7846 // insertText command, with value equal to el."
7847 for (var i = 0; i < value.length; i++) {
7848 commands.inserttext.action(value[i]);
7849 }
7850
7851 // "Return true."
7852 return true;
7853 }
7854
7855 // "If value is the empty string, return true."
7856 if (value == "") {
7857 return true;
7858 }
7859
7860 // "If value is a newline (U+00A0), take the action for the
7861 // insertParagraph command and return true."
7862 if (value == "\n") {
7863 commands.insertparagraph.action();
7864 return true;
7865 }
7866
7867 // "Let node and offset be the active range's start node and offset."
7868 var node = getActiveRange().startContainer;
7869 var offset = getActiveRange().startOffset;
7870
7871 // "If node has a child whose index is offset − 1, and that child is a
7872 // Text node, set node to that child, then set offset to node's
7873 // length."
7874 if (0 <= offset - 1
7875 && offset - 1 < node.childNodes.length
7876 && node.childNodes[offset - 1].nodeType == Node.TEXT_NODE) {
7877 node = node.childNodes[offset - 1];
7878 offset = getNodeLength(node);
7879 }
7880
7881 // "If node has a child whose index is offset, and that child is a Text
7882 // node, set node to that child, then set offset to zero."
7883 if (0 <= offset
7884 && offset < node.childNodes.length
7885 && node.childNodes[offset].nodeType == Node.TEXT_NODE) {
7886 node = node.childNodes[offset];
7887 offset = 0;
7888 }
7889
7890 // "Record current overrides, and let overrides be the result."
7891 var overrides = recordCurrentOverrides();
7892
7893 // "Call collapse(node, offset) on the context object's Selection."
7894 getSelection().collapse(node, offset);
7895 getActiveRange().setStart(node, offset);
7896 getActiveRange().setEnd(node, offset);
7897
7898 // "Canonicalize whitespace at (node, offset)."
7899 canonicalizeWhitespace(node, offset);
7900
7901 // "Let (node, offset) be the active range's start."
7902 node = getActiveRange().startContainer;
7903 offset = getActiveRange().startOffset;
7904
7905 // "If node is a Text node:"
7906 if (node.nodeType == Node.TEXT_NODE) {
7907 // "Call insertData(offset, value) on node."
7908 node.insertData(offset, value);
7909
7910 // "Call collapse(node, offset) on the context object's Selection."
7911 getSelection().collapse(node, offset);
7912 getActiveRange().setStart(node, offset);
7913
7914 // "Call extend(node, offset + 1) on the context object's
7915 // Selection."
7916 //
7917 // Work around WebKit bug: the extend() can throw if the text we're
7918 // adding is trailing whitespace.
7919 try { getSelection().extend(node, offset + 1); } catch(e) {}
7920 getActiveRange().setEnd(node, offset + 1);
7921
7922 // "Otherwise:"
7923 } else {
7924 // "If node has only one child, which is a collapsed line break,
7925 // remove its child from it."
7926 //
7927 // FIXME: IE incorrectly returns false here instead of true
7928 // sometimes?
7929 if (node.childNodes.length == 1
7930 && isCollapsedLineBreak(node.firstChild)) {
7931 node.removeChild(node.firstChild);
7932 }
7933
7934 // "Let text be the result of calling createTextNode(value) on the
7935 // context object."
7936 var text = document.createTextNode(value);
7937
7938 // "Call insertNode(text) on the active range."
7939 getActiveRange().insertNode(text);
7940
7941 // "Call collapse(text, 0) on the context object's Selection."
7942 getSelection().collapse(text, 0);
7943 getActiveRange().setStart(text, 0);
7944
7945 // "Call extend(text, 1) on the context object's Selection."
7946 getSelection().extend(text, 1);
7947 getActiveRange().setEnd(text, 1);
7948 }
7949
7950 // "Restore states and values from overrides."
7951 restoreStatesAndValues(overrides);
7952
7953 // "Canonicalize whitespace at the active range's start, with fix
7954 // collapsed space false."
7955 canonicalizeWhitespace(getActiveRange().startContainer, getActiveRange() .startOffset, false);
7956
7957 // "Canonicalize whitespace at the active range's end, with fix
7958 // collapsed space false."
7959 canonicalizeWhitespace(getActiveRange().endContainer, getActiveRange().e ndOffset, false);
7960
7961 // "If value is a space character, autolink the active range's start."
7962 if (/^[ \t\n\f\r]$/.test(value)) {
7963 autolink(getActiveRange().startContainer, getActiveRange().startOffs et);
7964 }
7965
7966 // "Call collapseToEnd() on the context object's Selection."
7967 //
7968 // Work around WebKit bug: sometimes it blows up the selection and
7969 // throws, which we don't want.
7970 try { getSelection().collapseToEnd(); } catch(e) {}
7971 getActiveRange().collapse(false);
7972
7973 // "Return true."
7974 return true;
7975 }
7976 };
7977
7978 //@}
7979 ///// The insertUnorderedList command /////
7980 //@{
7981 commands.insertunorderedlist = {
7982 preservesOverrides: true,
7983 // "Toggle lists with tag name "ul", then return true."
7984 action: function() { toggleLists("ul"); return true },
7985 // "True if the selection's list state is "mixed" or "mixed ul", false
7986 // otherwise."
7987 indeterm: function() { return /^mixed( ul)?$/.test(getSelectionListState()) },
7988 // "True if the selection's list state is "ul", false otherwise."
7989 state: function() { return getSelectionListState() == "ul" },
7990 };
7991
7992 //@}
7993 ///// The justifyCenter command /////
7994 //@{
7995 commands.justifycenter = {
7996 preservesOverrides: true,
7997 // "Justify the selection with alignment "center", then return true."
7998 action: function() { justifySelection("center"); return true },
7999 indeterm: function() {
8000 // "Return false if the active range is null. Otherwise, block-extend
8001 // the active range. Return true if among visible editable nodes that
8002 // are contained in the result and have no children, at least one has
8003 // alignment value "center" and at least one does not. Otherwise return
8004 // false."
8005 if (!getActiveRange()) {
8006 return false;
8007 }
8008 var nodes = getAllContainedNodes(blockExtend(getActiveRange()), function (node) {
8009 return isEditable(node) && isVisible(node) && !node.hasChildNodes();
8010 });
8011 return nodes.some(function(node) { return getAlignmentValue(node) == "ce nter" })
8012 && nodes.some(function(node) { return getAlignmentValue(node) != "ce nter" });
8013 }, state: function() {
8014 // "Return false if the active range is null. Otherwise, block-extend
8015 // the active range. Return true if there is at least one visible
8016 // editable node that is contained in the result and has no children,
8017 // and all such nodes have alignment value "center". Otherwise return
8018 // false."
8019 if (!getActiveRange()) {
8020 return false;
8021 }
8022 var nodes = getAllContainedNodes(blockExtend(getActiveRange()), function (node) {
8023 return isEditable(node) && isVisible(node) && !node.hasChildNodes();
8024 });
8025 return nodes.length
8026 && nodes.every(function(node) { return getAlignmentValue(node) == "c enter" });
8027 }, value: function() {
8028 // "Return the empty string if the active range is null. Otherwise,
8029 // block-extend the active range, and return the alignment value of the
8030 // first visible editable node that is contained in the result and has
8031 // no children. If there is no such node, return "left"."
8032 if (!getActiveRange()) {
8033 return "";
8034 }
8035 var nodes = getAllContainedNodes(blockExtend(getActiveRange()), function (node) {
8036 return isEditable(node) && isVisible(node) && !node.hasChildNodes();
8037 });
8038 if (nodes.length) {
8039 return getAlignmentValue(nodes[0]);
8040 } else {
8041 return "left";
8042 }
8043 },
8044 };
8045
8046 //@}
8047 ///// The justifyFull command /////
8048 //@{
8049 commands.justifyfull = {
8050 preservesOverrides: true,
8051 // "Justify the selection with alignment "justify", then return true."
8052 action: function() { justifySelection("justify"); return true },
8053 indeterm: function() {
8054 // "Return false if the active range is null. Otherwise, block-extend
8055 // the active range. Return true if among visible editable nodes that
8056 // are contained in the result and have no children, at least one has
8057 // alignment value "justify" and at least one does not. Otherwise
8058 // return false."
8059 if (!getActiveRange()) {
8060 return false;
8061 }
8062 var nodes = getAllContainedNodes(blockExtend(getActiveRange()), function (node) {
8063 return isEditable(node) && isVisible(node) && !node.hasChildNodes();
8064 });
8065 return nodes.some(function(node) { return getAlignmentValue(node) == "ju stify" })
8066 && nodes.some(function(node) { return getAlignmentValue(node) != "ju stify" });
8067 }, state: function() {
8068 // "Return false if the active range is null. Otherwise, block-extend
8069 // the active range. Return true if there is at least one visible
8070 // editable node that is contained in the result and has no children,
8071 // and all such nodes have alignment value "justify". Otherwise return
8072 // false."
8073 if (!getActiveRange()) {
8074 return false;
8075 }
8076 var nodes = getAllContainedNodes(blockExtend(getActiveRange()), function (node) {
8077 return isEditable(node) && isVisible(node) && !node.hasChildNodes();
8078 });
8079 return nodes.length
8080 && nodes.every(function(node) { return getAlignmentValue(node) == "j ustify" });
8081 }, value: function() {
8082 // "Return the empty string if the active range is null. Otherwise,
8083 // block-extend the active range, and return the alignment value of the
8084 // first visible editable node that is contained in the result and has
8085 // no children. If there is no such node, return "left"."
8086 if (!getActiveRange()) {
8087 return "";
8088 }
8089 var nodes = getAllContainedNodes(blockExtend(getActiveRange()), function (node) {
8090 return isEditable(node) && isVisible(node) && !node.hasChildNodes();
8091 });
8092 if (nodes.length) {
8093 return getAlignmentValue(nodes[0]);
8094 } else {
8095 return "left";
8096 }
8097 },
8098 };
8099
8100 //@}
8101 ///// The justifyLeft command /////
8102 //@{
8103 commands.justifyleft = {
8104 preservesOverrides: true,
8105 // "Justify the selection with alignment "left", then return true."
8106 action: function() { justifySelection("left"); return true },
8107 indeterm: function() {
8108 // "Return false if the active range is null. Otherwise, block-extend
8109 // the active range. Return true if among visible editable nodes that
8110 // are contained in the result and have no children, at least one has
8111 // alignment value "left" and at least one does not. Otherwise return
8112 // false."
8113 if (!getActiveRange()) {
8114 return false;
8115 }
8116 var nodes = getAllContainedNodes(blockExtend(getActiveRange()), function (node) {
8117 return isEditable(node) && isVisible(node) && !node.hasChildNodes();
8118 });
8119 return nodes.some(function(node) { return getAlignmentValue(node) == "le ft" })
8120 && nodes.some(function(node) { return getAlignmentValue(node) != "le ft" });
8121 }, state: function() {
8122 // "Return false if the active range is null. Otherwise, block-extend
8123 // the active range. Return true if there is at least one visible
8124 // editable node that is contained in the result and has no children,
8125 // and all such nodes have alignment value "left". Otherwise return
8126 // false."
8127 if (!getActiveRange()) {
8128 return false;
8129 }
8130 var nodes = getAllContainedNodes(blockExtend(getActiveRange()), function (node) {
8131 return isEditable(node) && isVisible(node) && !node.hasChildNodes();
8132 });
8133 return nodes.length
8134 && nodes.every(function(node) { return getAlignmentValue(node) == "l eft" });
8135 }, value: function() {
8136 // "Return the empty string if the active range is null. Otherwise,
8137 // block-extend the active range, and return the alignment value of the
8138 // first visible editable node that is contained in the result and has
8139 // no children. If there is no such node, return "left"."
8140 if (!getActiveRange()) {
8141 return "";
8142 }
8143 var nodes = getAllContainedNodes(blockExtend(getActiveRange()), function (node) {
8144 return isEditable(node) && isVisible(node) && !node.hasChildNodes();
8145 });
8146 if (nodes.length) {
8147 return getAlignmentValue(nodes[0]);
8148 } else {
8149 return "left";
8150 }
8151 },
8152 };
8153
8154 //@}
8155 ///// The justifyRight command /////
8156 //@{
8157 commands.justifyright = {
8158 preservesOverrides: true,
8159 // "Justify the selection with alignment "right", then return true."
8160 action: function() { justifySelection("right"); return true },
8161 indeterm: function() {
8162 // "Return false if the active range is null. Otherwise, block-extend
8163 // the active range. Return true if among visible editable nodes that
8164 // are contained in the result and have no children, at least one has
8165 // alignment value "right" and at least one does not. Otherwise return
8166 // false."
8167 if (!getActiveRange()) {
8168 return false;
8169 }
8170 var nodes = getAllContainedNodes(blockExtend(getActiveRange()), function (node) {
8171 return isEditable(node) && isVisible(node) && !node.hasChildNodes();
8172 });
8173 return nodes.some(function(node) { return getAlignmentValue(node) == "ri ght" })
8174 && nodes.some(function(node) { return getAlignmentValue(node) != "ri ght" });
8175 }, state: function() {
8176 // "Return false if the active range is null. Otherwise, block-extend
8177 // the active range. Return true if there is at least one visible
8178 // editable node that is contained in the result and has no children,
8179 // and all such nodes have alignment value "right". Otherwise return
8180 // false."
8181 if (!getActiveRange()) {
8182 return false;
8183 }
8184 var nodes = getAllContainedNodes(blockExtend(getActiveRange()), function (node) {
8185 return isEditable(node) && isVisible(node) && !node.hasChildNodes();
8186 });
8187 return nodes.length
8188 && nodes.every(function(node) { return getAlignmentValue(node) == "r ight" });
8189 }, value: function() {
8190 // "Return the empty string if the active range is null. Otherwise,
8191 // block-extend the active range, and return the alignment value of the
8192 // first visible editable node that is contained in the result and has
8193 // no children. If there is no such node, return "left"."
8194 if (!getActiveRange()) {
8195 return "";
8196 }
8197 var nodes = getAllContainedNodes(blockExtend(getActiveRange()), function (node) {
8198 return isEditable(node) && isVisible(node) && !node.hasChildNodes();
8199 });
8200 if (nodes.length) {
8201 return getAlignmentValue(nodes[0]);
8202 } else {
8203 return "left";
8204 }
8205 },
8206 };
8207
8208 //@}
8209 ///// The outdent command /////
8210 //@{
8211 commands.outdent = {
8212 preservesOverrides: true,
8213 action: function() {
8214 // "Let items be a list of all lis that are ancestor containers of the
8215 // range's start and/or end node."
8216 //
8217 // It's annoying to get this in tree order using functional stuff
8218 // without doing getDescendants(document), which is slow, so I do it
8219 // imperatively.
8220 var items = [];
8221 (function(){
8222 for (
8223 var ancestorContainer = getActiveRange().endContainer;
8224 ancestorContainer != getActiveRange().commonAncestorContainer;
8225 ancestorContainer = ancestorContainer.parentNode
8226 ) {
8227 if (isHtmlElement(ancestorContainer, "li")) {
8228 items.unshift(ancestorContainer);
8229 }
8230 }
8231 for (
8232 var ancestorContainer = getActiveRange().startContainer;
8233 ancestorContainer;
8234 ancestorContainer = ancestorContainer.parentNode
8235 ) {
8236 if (isHtmlElement(ancestorContainer, "li")) {
8237 items.unshift(ancestorContainer);
8238 }
8239 }
8240 })();
8241
8242 // "For each item in items, normalize sublists of item."
8243 items.forEach(normalizeSublists);
8244
8245 // "Block-extend the active range, and let new range be the result."
8246 var newRange = blockExtend(getActiveRange());
8247
8248 // "Let node list be a list of nodes, initially empty."
8249 //
8250 // "For each node node contained in new range, append node to node list
8251 // if the last member of node list (if any) is not an ancestor of node;
8252 // node is editable; and either node has no editable descendants, or is
8253 // an ol or ul, or is an li whose parent is an ol or ul."
8254 var nodeList = getContainedNodes(newRange, function(node) {
8255 return isEditable(node)
8256 && (!getDescendants(node).some(isEditable)
8257 || isHtmlElement(node, ["ol", "ul"])
8258 || (isHtmlElement(node, "li") && isHtmlElement(node.parentNode, ["ol", "ul"])));
8259 });
8260
8261 // "While node list is not empty:"
8262 while (nodeList.length) {
8263 // "While the first member of node list is an ol or ul or is not
8264 // the child of an ol or ul, outdent it and remove it from node
8265 // list."
8266 while (nodeList.length
8267 && (isHtmlElement(nodeList[0], ["OL", "UL"])
8268 || !isHtmlElement(nodeList[0].parentNode, ["OL", "UL"]))) {
8269 outdentNode(nodeList.shift());
8270 }
8271
8272 // "If node list is empty, break from these substeps."
8273 if (!nodeList.length) {
8274 break;
8275 }
8276
8277 // "Let sublist be a list of nodes, initially empty."
8278 var sublist = [];
8279
8280 // "Remove the first member of node list and append it to sublist."
8281 sublist.push(nodeList.shift());
8282
8283 // "While the first member of node list is the nextSibling of the
8284 // last member of sublist, and the first member of node list is not
8285 // an ol or ul, remove the first member of node list and append it
8286 // to sublist."
8287 while (nodeList.length
8288 && nodeList[0] == sublist[sublist.length - 1].nextSibling
8289 && !isHtmlElement(nodeList[0], ["OL", "UL"])) {
8290 sublist.push(nodeList.shift());
8291 }
8292
8293 // "Record the values of sublist, and let values be the result."
8294 var values = recordValues(sublist);
8295
8296 // "Split the parent of sublist, with new parent null."
8297 splitParent(sublist);
8298
8299 // "Fix disallowed ancestors of each member of sublist."
8300 sublist.forEach(fixDisallowedAncestors);
8301
8302 // "Restore the values from values."
8303 restoreValues(values);
8304 }
8305
8306 // "Return true."
8307 return true;
8308 }
8309 };
8310
8311 //@}
8312
8313 //////////////////////////////////
8314 ///// Miscellaneous commands /////
8315 //////////////////////////////////
8316
8317 ///// The defaultParagraphSeparator command /////
8318 //@{
8319 commands.defaultparagraphseparator = {
8320 action: function(value) {
8321 // "Let value be converted to ASCII lowercase. If value is then equal
8322 // to "p" or "div", set the context object's default single-line
8323 // container name to value and return true. Otherwise, return false."
8324 value = value.toLowerCase();
8325 if (value == "p" || value == "div") {
8326 defaultSingleLineContainerName = value;
8327 return true;
8328 }
8329 return false;
8330 }, value: function() {
8331 // "Return the context object's default single-line container name."
8332 return defaultSingleLineContainerName;
8333 },
8334 };
8335
8336 //@}
8337 ///// The selectAll command /////
8338 //@{
8339 commands.selectall = {
8340 // Note, this ignores the whole globalRange/getActiveRange() thing and
8341 // works with actual selections. Not suitable for autoimplementation.html.
8342 action: function() {
8343 // "Let target be the body element of the context object."
8344 var target = document.body;
8345
8346 // "If target is null, let target be the context object's
8347 // documentElement."
8348 if (!target) {
8349 target = document.documentElement;
8350 }
8351
8352 // "If target is null, call getSelection() on the context object, and
8353 // call removeAllRanges() on the result."
8354 if (!target) {
8355 getSelection().removeAllRanges();
8356
8357 // "Otherwise, call getSelection() on the context object, and call
8358 // selectAllChildren(target) on the result."
8359 } else {
8360 getSelection().selectAllChildren(target);
8361 }
8362
8363 // "Return true."
8364 return true;
8365 }
8366 };
8367
8368 //@}
8369 ///// The styleWithCSS command /////
8370 //@{
8371 commands.stylewithcss = {
8372 action: function(value) {
8373 // "If value is an ASCII case-insensitive match for the string
8374 // "false", set the CSS styling flag to false. Otherwise, set the
8375 // CSS styling flag to true. Either way, return true."
8376 cssStylingFlag = String(value).toLowerCase() != "false";
8377 return true;
8378 }, state: function() { return cssStylingFlag }
8379 };
8380
8381 //@}
8382 ///// The useCSS command /////
8383 //@{
8384 commands.usecss = {
8385 action: function(value) {
8386 // "If value is an ASCII case-insensitive match for the string "false",
8387 // set the CSS styling flag to true. Otherwise, set the CSS styling
8388 // flag to false. Either way, return true."
8389 cssStylingFlag = String(value).toLowerCase() == "false";
8390 return true;
8391 }
8392 };
8393 //@}
8394
8395 // Some final setup
8396 //@{
8397 (function() {
8398 // Opera 11.50 doesn't implement Object.keys, so I have to make an explicit
8399 // temporary, which means I need an extra closure to not leak the temporaries
8400 // into the global namespace. >:(
8401 var commandNames = [];
8402 for (var command in commands) {
8403 commandNames.push(command);
8404 }
8405 commandNames.forEach(function(command) {
8406 // "If a command does not have a relevant CSS property specified, it
8407 // defaults to null."
8408 if (!("relevantCssProperty" in commands[command])) {
8409 commands[command].relevantCssProperty = null;
8410 }
8411
8412 // "If a command has inline command activated values defined but nothing
8413 // else defines when it is indeterminate, it is indeterminate if among
8414 // formattable nodes effectively contained in the active range, there is at
8415 // least one whose effective command value is one of the given values and
8416 // at least one whose effective command value is not one of the given
8417 // values."
8418 if ("inlineCommandActivatedValues" in commands[command]
8419 && !("indeterm" in commands[command])) {
8420 commands[command].indeterm = function() {
8421 if (!getActiveRange()) {
8422 return false;
8423 }
8424
8425 var values = getAllEffectivelyContainedNodes(getActiveRange(), isFor mattableNode)
8426 .map(function(node) { return getEffectiveCommandValue(node, comm and) });
8427
8428 var matchingValues = values.filter(function(value) {
8429 return commands[command].inlineCommandActivatedValues.indexOf(va lue) != -1;
8430 });
8431
8432 return matchingValues.length >= 1
8433 && values.length - matchingValues.length >= 1;
8434 };
8435 }
8436
8437 // "If a command has inline command activated values defined, its state is
8438 // true if either no formattable node is effectively contained in the
8439 // active range, and the active range's start node's effective command
8440 // value is one of the given values; or if there is at least one
8441 // formattable node effectively contained in the active range, and all of
8442 // them have an effective command value equal to one of the given values."
8443 if ("inlineCommandActivatedValues" in commands[command]) {
8444 commands[command].state = function() {
8445 if (!getActiveRange()) {
8446 return false;
8447 }
8448
8449 var nodes = getAllEffectivelyContainedNodes(getActiveRange(), isForm attableNode);
8450
8451 if (nodes.length == 0) {
8452 return commands[command].inlineCommandActivatedValues
8453 .indexOf(getEffectiveCommandValue(getActiveRange().startCont ainer, command)) != -1;
8454 } else {
8455 return nodes.every(function(node) {
8456 return commands[command].inlineCommandActivatedValues
8457 .indexOf(getEffectiveCommandValue(node, command)) != -1;
8458 });
8459 }
8460 };
8461 }
8462
8463 // "If a command is a standard inline value command, it is indeterminate if
8464 // among formattable nodes that are effectively contained in the active
8465 // range, there are two that have distinct effective command values. Its
8466 // value is the effective command value of the first formattable node that
8467 // is effectively contained in the active range; or if there is no such
8468 // node, the effective command value of the active range's start node; or
8469 // if that is null, the empty string."
8470 if ("standardInlineValueCommand" in commands[command]) {
8471 commands[command].indeterm = function() {
8472 if (!getActiveRange()) {
8473 return false;
8474 }
8475
8476 var values = getAllEffectivelyContainedNodes(getActiveRange())
8477 .filter(isFormattableNode)
8478 .map(function(node) { return getEffectiveCommandValue(node, comm and) });
8479 for (var i = 1; i < values.length; i++) {
8480 if (values[i] != values[i - 1]) {
8481 return true;
8482 }
8483 }
8484 return false;
8485 };
8486
8487 commands[command].value = function() {
8488 if (!getActiveRange()) {
8489 return "";
8490 }
8491
8492 var refNode = getAllEffectivelyContainedNodes(getActiveRange(), isFo rmattableNode)[0];
8493
8494 if (typeof refNode == "undefined") {
8495 refNode = getActiveRange().startContainer;
8496 }
8497
8498 var ret = getEffectiveCommandValue(refNode, command);
8499 if (ret === null) {
8500 return "";
8501 }
8502 return ret;
8503 };
8504 }
8505
8506 // "If a command preserves overrides, then before taking its action, the
8507 // user agent must record current overrides. After taking the action, if
8508 // the active range is collapsed, it must restore states and values from
8509 // the recorded list."
8510 if ("preservesOverrides" in commands[command]) {
8511 var oldAction = commands[command].action;
8512
8513 commands[command].action = function(value) {
8514 var overrides = recordCurrentOverrides();
8515 var ret = oldAction(value);
8516 if (getActiveRange().collapsed) {
8517 restoreStatesAndValues(overrides);
8518 }
8519 return ret;
8520 };
8521 }
8522 });
8523 })();
8524 //@}
8525
8526 // vim: foldmarker=@{,@} foldmethod=marker
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698