corinthia-commits mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From pmke...@apache.org
Subject [48/92] [abbrv] [partial] incubator-corinthia git commit: Add editing code from UX Write
Date Wed, 17 Dec 2014 13:28:58 GMT
http://git-wip-us.apache.org/repos/asf/incubator-corinthia/blob/03bd5af0/Editor/src/Formatting.js
----------------------------------------------------------------------
diff --git a/Editor/src/Formatting.js b/Editor/src/Formatting.js
new file mode 100644
index 0000000..625b928
--- /dev/null
+++ b/Editor/src/Formatting.js
@@ -0,0 +1,1275 @@
+// Copyright 2011-2014 UX Productivity Pty Ltd
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+var Formatting_splitTextBefore;
+var Formatting_splitTextAfter;
+var Formatting_movePreceding;
+var Formatting_moveFollowing;
+var Formatting_splitAroundSelection;
+var Formatting_mergeUpwards;
+var Formatting_mergeWithNeighbours;
+var Formatting_paragraphTextUpToPosition;
+var Formatting_getAllNodeProperties;
+var Formatting_getFormatting;
+var Formatting_pushDownInlineProperties;
+var Formatting_applyFormattingChanges;
+var Formatting_formatInlineNode;
+
+var Formatting_MERGEABLE_INLINE;
+var Formatting_MERGEABLE_BLOCK;
+var Formatting_MERGEABLE_BLOCK_AND_INLINE;
+
+(function() {
+
+    // Some properties in CSS, such as 'margin', 'border', and 'padding', are shorthands which
+    // set multiple, more fine-grained properties. The CSS spec outlines what these are - e.g.
+    // an assignment to the 'margin' property is considered a simultaneous assignment to
+    // 'margin-left', 'margin-right', 'margin-top', and 'margin-bottom' properties.
+
+    // However, Firefox contains a bug (https://bugzilla.mozilla.org/show_bug.cgi?id=241234),
+    // which has gone unfixed for more than six years, whereby it actually sets different
+    // properties for *-left and *-right, which are reflected when examining the style property
+    // of an element. Additionally, it also gives an error if you try to set these, so if you simply
+    // get all the style properties and try to set them again it won't work.
+
+    // To get around this problem, we record the following set of replacements. When getting the
+    // style properties of an element, we replace any properties with the names given below with
+    // their corresponding spec name. A null entry means that property should be ignored altogether.
+
+    // You should always use getStyleProperties() instead of accessing element.style directly.
+
+    var CSS_PROPERTY_REPLACEMENTS = {
+        "margin-left-value": "margin-left",
+        "margin-left-ltr-source": null,
+        "margin-left-rtl-source": null,
+        "margin-right-value": "margin-right",
+        "margin-right-ltr-source": null,
+        "margin-right-rtl-source": null,
+        "padding-left-value": "padding-left",
+        "padding-left-ltr-source": null,
+        "padding-left-rtl-source": null,
+        "padding-right-value": "padding-right",
+        "padding-right-ltr-source": null,
+        "padding-right-rtl-source": null,
+        "border-right-width-value": "border-right-width",
+        "border-right-width-ltr-source": null,
+        "border-right-width-rtl-source": null,
+        "border-left-width-value": "border-left-width",
+        "border-left-width-ltr-source": null,
+        "border-left-width-rtl-source": null,
+        "border-right-color-value": "border-right-color",
+        "border-right-color-ltr-source": null,
+        "border-right-color-rtl-source": null,
+        "border-left-color-value": "border-left-color",
+        "border-left-color-ltr-source": null,
+        "border-left-color-rtl-source": null,
+        "border-right-style-value": "border-right-style",
+        "border-right-style-ltr-source": null,
+        "border-right-style-rtl-source": null,
+        "border-left-style-value": "border-left-style",
+        "border-left-style-ltr-source": null,
+        "border-left-style-rtl-source": null,
+    };
+
+    // private
+    function getStyleProperties(element,dontReplace)
+    {
+        var properties = new Object();
+
+        for (var i = 0; i < element.style.length; i++) {
+            var name = element.style[i];
+            var value = element.style.getPropertyValue(name);
+
+            var replacement;
+            if (dontReplace) {
+                replacement = name;
+            }
+            else {
+                replacement = CSS_PROPERTY_REPLACEMENTS[name];
+                if (typeof(replacement) == "undefined")
+                    replacement = name;
+            }
+
+            if (replacement != null)
+                properties[replacement] = value;
+        }
+        return properties;
+    }
+
+    // public (for testing purposes only)
+    Formatting_splitAroundSelection = function(range,allowDirectInline)
+    {
+        Range_trackWhileExecuting(range,function() {
+            if (!allowDirectInline)
+                Range_ensureInlineNodesInParagraph(range);
+            Range_ensureValidHierarchy(range);
+
+            if ((range.start.node.nodeType == Node.TEXT_NODE) &&
+                (range.start.offset > 0)) {
+                Formatting_splitTextBefore(range.start);
+                if (range.end.node == range.start.node)
+                    range.end.offset -= range.start.offset;
+                range.start.offset = 0;
+            }
+            else if (range.start.node.nodeType == Node.ELEMENT_NODE) {
+                Formatting_movePreceding(range.start,isBlockNode);
+            }
+            else {
+                Formatting_movePreceding(new Position(range.start.node.parentNode,
+                                                      DOM_nodeOffset(range.start.node)),
+                                         isBlockNode);
+            }
+
+            // Save the start and end position of the range. The mutation listeners will move it
+            // when the following node is moved, which we don't actually want in this case.
+            var startNode = range.start.node;
+            var startOffset = range.start.offset;
+            var endNode = range.end.node;
+            var endOffset = range.end.offset;
+
+            if ((range.end.node.nodeType == Node.TEXT_NODE) &&
+                (range.end.offset < range.end.node.nodeValue.length)) {
+                Formatting_splitTextAfter(range.end);
+            }
+            else if (range.end.node.nodeType == Node.ELEMENT_NODE) {
+                Formatting_moveFollowing(range.end,isBlockNode);
+            }
+            else {
+                Formatting_moveFollowing(new Position(range.end.node.parentNode,
+                                                      DOM_nodeOffset(range.end.node)+1),
+                                         isBlockNode);
+            }
+
+            range.start.node = startNode;
+            range.start.offset = startOffset;
+            range.end.node = endNode;
+            range.end.offset = endOffset;
+        });
+    }
+
+    // public
+    Formatting_mergeUpwards = function(node,whiteList)
+    {
+        while ((node != null) && whiteList[node._type]) {
+            var parent = node.parentNode;
+            Formatting_mergeWithNeighbours(node,whiteList,true);
+            node = parent;
+        }
+    }
+
+    function isDiscardable(node)
+    {
+        if (node.nodeType != Node.ELEMENT_NODE)
+            return false;
+
+        if (!isInlineNode(node))
+            return false;
+
+        if (isOpaqueNode(node))
+            return false;
+
+        for (var child = node.firstChild; child != null; child = child.nextSibling) {
+            if (!isDiscardable(child))
+                return false;
+        }
+
+        return true;
+    }
+
+    // public (for use by tests)
+    Formatting_mergeWithNeighbours = function(node,whiteList,trim)
+    {
+        var parent = node.parentNode;
+        if (parent == null)
+            return;
+
+        var start = node;
+        var end = node;
+
+        while ((start.previousSibling != null) &&
+               DOM_nodesMergeable(start.previousSibling,start,whiteList))
+            start = start.previousSibling;
+
+        while ((end.nextSibling != null) &&
+               DOM_nodesMergeable(end,end.nextSibling,whiteList))
+            end = end.nextSibling;
+
+        if (trim) {
+            while ((start.previousSibling != null) && isDiscardable(start.previousSibling))
+                DOM_deleteNode(start.previousSibling);
+            while ((end.nextSibling != null) && isDiscardable(end.nextSibling))
+                DOM_deleteNode(end.nextSibling);
+        }
+
+        if (start != end) {
+            var lastMerge;
+            do {
+                lastMerge = (start.nextSibling == end);
+
+                var lastChild = null;
+                if (start.nodeType == Node.ELEMENT_NODE)
+                    lastChild = start.lastChild;
+
+                DOM_mergeWithNextSibling(start,whiteList);
+
+                if (lastChild != null)
+                    Formatting_mergeWithNeighbours(lastChild,whiteList);
+            } while (!lastMerge);
+        }
+    }
+
+    // private
+    function mergeRange(range,whiteList)
+    {
+        var nodes = Range_getAllNodes(range);
+        for (var i = 0; i < nodes.length; i++) {
+            var next;
+            for (var p = nodes[i]; p != null; p = next) {
+                next = p.parentNode;
+                Formatting_mergeWithNeighbours(p,whiteList);
+            }
+        }
+    }
+
+    // public (called from cursor.js)
+    Formatting_splitTextBefore = function(pos,parentCheckFn,force)
+    {
+        var node = pos.node;
+        var offset = pos.offset;
+        if (parentCheckFn == null)
+            parentCheckFn = isBlockNode;
+
+        if (force || (offset > 0)) {
+            var before = DOM_createTextNode(document,"");
+            DOM_insertBefore(node.parentNode,before,node);
+            DOM_moveCharacters(node,0,offset,before,0,false,true);
+            Formatting_movePreceding(new Position(node.parentNode,DOM_nodeOffset(node)),
+                                     parentCheckFn,force);
+            return new Position(before,before.nodeValue.length);
+        }
+        else {
+            Formatting_movePreceding(new Position(node.parentNode,DOM_nodeOffset(node)),
+                                     parentCheckFn,force);
+            return pos;
+        }
+    }
+
+    // public
+    Formatting_splitTextAfter = function(pos,parentCheckFn,force)
+    {
+        var node = pos.node;
+        var offset = pos.offset;
+        if (parentCheckFn == null)
+            parentCheckFn = isBlockNode;
+
+        if (force || (offset < pos.node.nodeValue.length)) {
+            var after = DOM_createTextNode(document,"");
+            DOM_insertBefore(node.parentNode,after,node.nextSibling);
+            DOM_moveCharacters(node,offset,node.nodeValue.length,after,0,true,false);
+            Formatting_moveFollowing(new Position(node.parentNode,DOM_nodeOffset(node)+1),
+                                     parentCheckFn,force);
+            return new Position(after,0);
+        }
+        else {
+            Formatting_moveFollowing(new Position(node.parentNode,DOM_nodeOffset(node)+1),
+                                     parentCheckFn,force);
+            return pos;
+        }
+    }
+
+    // FIXME: movePreceding and moveNext could possibly be optimised by passing in a (parent,child)
+    // pair instead of (node,offset), i.e. parent is the same as node, but rather than passing the
+    // index of a child, we pass the child itself (or null if the offset is equal to
+    // childNodes.length)
+    // public
+    Formatting_movePreceding = function(pos,parentCheckFn,force)
+    {
+        var node = pos.node;
+        var offset = pos.offset;
+        if (parentCheckFn(node) || (node == document.body))
+            return new Position(node,offset);
+
+        var toMove = new Array();
+        var justWhitespace = true;
+        var result = new Position(node,offset);
+        for (var i = 0; i < offset; i++) {
+            if (!isWhitespaceTextNode(node.childNodes[i]))
+                justWhitespace = false;
+            toMove.push(node.childNodes[i]);
+        }
+
+        if ((toMove.length > 0) || force) {
+            if (justWhitespace && !force) {
+                for (var i = 0; i < toMove.length; i++)
+                    DOM_insertBefore(node.parentNode,toMove[i],node);
+            }
+            else {
+                var copy = DOM_shallowCopyElement(node);
+                DOM_insertBefore(node.parentNode,copy,node);
+
+                for (var i = 0; i < toMove.length; i++)
+                    DOM_insertBefore(copy,toMove[i],null);
+                result = new Position(copy,copy.childNodes.length);
+            }
+        }
+
+        Formatting_movePreceding(new Position(node.parentNode,DOM_nodeOffset(node)),
+                                 parentCheckFn,force);
+        return result;
+    }
+
+    // public
+    Formatting_moveFollowing = function(pos,parentCheckFn,force)
+    {
+        var node = pos.node;
+        var offset = pos.offset;
+        if (parentCheckFn(node) || (node == document.body))
+            return new Position(node,offset);
+
+        var toMove = new Array();
+        var justWhitespace = true;
+        var result =  new Position(node,offset);
+        for (var i = offset; i < node.childNodes.length; i++) {
+            if (!isWhitespaceTextNode(node.childNodes[i]))
+                justWhitespace = false;
+            toMove.push(node.childNodes[i]);
+        }
+
+        if ((toMove.length > 0) || force) {
+            if (justWhitespace && !force) {
+                for (var i = 0; i < toMove.length; i++)
+                    DOM_insertBefore(node.parentNode,toMove[i],node.nextSibling);
+            }
+            else {
+                var copy = DOM_shallowCopyElement(node);
+                DOM_insertBefore(node.parentNode,copy,node.nextSibling);
+
+                for (var i = 0; i < toMove.length; i++)
+                    DOM_insertBefore(copy,toMove[i],null);
+                result = new Position(copy,0);
+            }
+        }
+
+        Formatting_moveFollowing(new Position(node.parentNode,DOM_nodeOffset(node)+1),
+                                 parentCheckFn,force);
+        return result;
+    }
+
+    // public
+    Formatting_paragraphTextUpToPosition = function(pos)
+    {
+        if (pos.node.nodeType == Node.TEXT_NODE) {
+            return stringToStartOfParagraph(pos.node,pos.offset);
+        }
+        else {
+            return stringToStartOfParagraph(Position_closestActualNode(pos),0);
+        }
+
+        function stringToStartOfParagraph(node,offset)
+        {
+            var start = node;
+            var components = new Array();
+            while (isInlineNode(node)) {
+                if (node.nodeType == Node.TEXT_NODE) {
+                    if (node == start)
+                        components.push(node.nodeValue.slice(0,offset));
+                    else
+                        components.push(node.nodeValue);
+                }
+
+                if (node.previousSibling != null) {
+                    node = node.previousSibling;
+                    while (isInlineNode(node) && (node.lastChild != null))
+                        node = node.lastChild;
+                }
+                else {
+                    node = node.parentNode;
+                }
+            }
+            return components.reverse().join("");
+        }
+    }
+
+    // public
+    Formatting_getFormatting = function()
+    {
+        // FIXME: implement a more efficient version of this algorithm which avoids duplicate checks
+
+        var range = Selection_get();
+        if (range == null)
+            return {};
+
+        Range_assertValid(range,"Selection");
+
+        var outermost = Range_getOutermostNodes(range,true);
+
+        var leafNodes = new Array();
+        for (var i = 0; i < outermost.length; i++) {
+            findLeafNodes(outermost[i],leafNodes);
+        }
+        var empty = Range_isEmpty(range);
+
+        var commonProperties = null;
+        for (var i = 0; i < leafNodes.length; i++) {
+            if (!isWhitespaceTextNode(leafNodes[i]) || empty) {
+                var leafNodeProperties = Formatting_getAllNodeProperties(leafNodes[i]);
+                if (leafNodeProperties["-uxwrite-paragraph-style"] == null)
+                    leafNodeProperties["-uxwrite-paragraph-style"] = Keys.NONE_STYLE;
+                if (commonProperties == null)
+                    commonProperties = leafNodeProperties;
+                else
+                    commonProperties = intersection(commonProperties,leafNodeProperties);
+            }
+        }
+
+        if (commonProperties == null)
+            commonProperties = {"-uxwrite-paragraph-style": Keys.NONE_STYLE};
+
+        for (var i = 0; i < leafNodes.length; i++) {
+            var leaf = leafNodes[i];
+            if (leaf._type == HTML_LI) {
+                switch (leaf.parentNode._type) {
+                case HTML_UL:
+                    commonProperties["-uxwrite-in-ul"] = "true";
+                    break;
+                case HTML_OL:
+                    commonProperties["-uxwrite-in-ol"] = "true";
+                    break;
+                }
+            }
+            else {
+                for (var ancestor = leaf;
+                     ancestor.parentNode != null;
+                     ancestor = ancestor.parentNode) {
+
+                    if (ancestor.parentNode._type == HTML_LI) {
+                        var havePrev = false;
+                        for (var c = ancestor.previousSibling; c != null; c = c.previousSibling) {
+                            if (!isWhitespaceTextNode(c)) {
+                                havePrev = true;
+                                break;
+                            }
+                        }
+                        if (!havePrev) {
+                            var listNode = ancestor.parentNode.parentNode;
+                            switch (listNode._type) {
+                            case HTML_UL:
+                                commonProperties["-uxwrite-in-ul"] = "true";
+                                break;
+                            case HTML_OL:
+                                commonProperties["-uxwrite-in-ol"] = "true";
+                                break;
+                            }
+                        }
+                    }
+                }
+            }
+        }
+
+        getFlags(range.start,commonProperties);
+
+        return commonProperties;
+
+        function getFlags(pos,commonProperties)
+        {
+            var strBeforeCursor = Formatting_paragraphTextUpToPosition(pos);
+
+            if (isWhitespaceString(strBeforeCursor)) {
+                var firstInParagraph = true;
+                for (var p = pos.node; isInlineNode(p); p = p.parentNode) {
+                    if (p.previousSibling != null)
+                        firstInParagraph = false;
+                }
+                if (firstInParagraph)
+                    commonProperties["-uxwrite-shift"] = "true";
+            }
+            if (strBeforeCursor.match(/\.\s*$/))
+                commonProperties["-uxwrite-shift"] = "true";
+            if (strBeforeCursor.match(/\([^\)]*$/))
+                commonProperties["-uxwrite-in-brackets"] = "true";
+            if (strBeforeCursor.match(/\u201c[^\u201d]*$/))
+                commonProperties["-uxwrite-in-quotes"] = "true";
+        }
+
+        function intersection(a,b)
+        {
+            var result = new Object();
+            for (var name in a) {
+                if (a[name] == b[name])
+                    result[name] = a[name];
+            }
+            return result;
+        }
+
+        function findLeafNodes(node,result)
+        {
+            if (node.firstChild == null) {
+                result.push(node);
+            }
+            else {
+                for (var child = node.firstChild; child != null; child = child.nextSibling)
+                    findLeafNodes(child,result);
+            }
+        }
+    }
+
+    // public
+    Formatting_getAllNodeProperties = function(node)
+    {
+        if (node == null)
+            throw new Error("Node is not in tree");
+
+        if (node == node.ownerDocument.body)
+            return new Object();
+
+        var properties = Formatting_getAllNodeProperties(node.parentNode);
+
+        if (node.nodeType == Node.ELEMENT_NODE) {
+            // Note: Style names corresponding to element names must be in lowercase, because
+            // canonicaliseSelector() in Styles.js always converts selectors to lowercase.
+            if (node.hasAttribute("STYLE")) {
+                var nodeProperties = getStyleProperties(node);
+                for (var name in nodeProperties)
+                    properties[name] = nodeProperties[name];
+            }
+
+            var type = node._type;
+            switch (type) {
+            case HTML_B:
+                properties["font-weight"] = "bold";
+                break;
+            case HTML_I:
+                properties["font-style"] = "italic";
+                break;
+            case HTML_U: {
+                var components = [];
+                if (properties["text-decoration"] != null) {
+                    var components = properties["text-decoration"].toLowerCase().split(/\s+/);
+                    if (components.indexOf("underline") == -1)
+                        properties["text-decoration"] += " underline";
+                }
+                else {
+                    properties["text-decoration"] = "underline";
+                }
+                break;
+            }
+//            case HTML_TT:
+//                properties["-uxwrite-in-tt"] = "true";
+//                break;
+            case HTML_IMG:
+                properties["-uxwrite-in-image"] = "true";
+                break;
+            case HTML_FIGURE:
+                properties["-uxwrite-in-figure"] = "true";
+                break;
+            case HTML_TABLE:
+                properties["-uxwrite-in-table"] = "true";
+                break;
+            case HTML_A:
+                if (node.hasAttribute("href")) {
+                    var href = node.getAttribute("href");
+                    if (href.charAt(0) == "#")
+                        properties["-uxwrite-in-reference"] = "true";
+                    else
+                        properties["-uxwrite-in-link"] = "true";
+                }
+                break;
+            case HTML_NAV: {
+                var className = DOM_getAttribute(node,"class");
+                if ((className == Keys.SECTION_TOC) ||
+                    (className == Keys.FIGURE_TOC) ||
+                    (className == Keys.TABLE_TOC))
+                    properties["-uxwrite-in-toc"] = "true";
+                break;
+            }
+            default:
+                if (PARAGRAPH_ELEMENTS[type]) {
+                    var name = node.nodeName.toLowerCase();
+                    var selector;
+                    if (node.hasAttribute("class"))
+                        selector = name + "." + node.getAttribute("class");
+                    else
+                        selector = name;
+                    properties["-uxwrite-paragraph-style"] = selector;
+                }
+                break;
+            }
+
+            if (OUTLINE_TITLE_ELEMENTS[type] && node.hasAttribute("id"))
+                properties["-uxwrite-in-item-title"] = node.getAttribute("id");
+        }
+
+        return properties;
+    }
+
+    var PARAGRAPH_PROPERTIES = {
+        "margin-left": true,
+        "margin-right": true,
+        "margin-top": true,
+        "margin-bottom": true,
+
+        "padding-left": true,
+        "padding-right": true,
+        "padding-top": true,
+        "padding-bottom": true,
+
+        "border-left-width": true,
+        "border-right-width": true,
+        "border-top-width": true,
+        "border-bottom-width": true,
+
+        "border-left-style": true,
+        "border-right-style": true,
+        "border-top-style": true,
+        "border-bottom-style": true,
+
+        "border-left-color": true,
+        "border-right-color": true,
+        "border-top-color": true,
+        "border-bottom-color": true,
+
+        "border-top-left-radius": true,
+        "border-top-right-radius": true,
+        "border-bottom-left-radius": true,
+        "border-bottom-right-radius": true,
+
+        "text-align": true,
+        "text-indent": true,
+        "line-height": true,
+        "display": true,
+
+        "width": true,
+        "height": true,
+    };
+
+    var SPECIAL_PROPERTIES = {
+        "-webkit-text-size-adjust": true, // set on HTML element for text scaling purposes
+    };
+
+    function isParagraphProperty(name)
+    {
+        return PARAGRAPH_PROPERTIES[name];
+    }
+
+    function isInlineProperty(name)
+    {
+        return !PARAGRAPH_PROPERTIES[name] && !SPECIAL_PROPERTIES[name];
+    }
+
+    // private
+    function putDirectInlineChildrenInParagraphs(parent)
+    {
+        var inlineChildren = new Array();
+        for (var child = parent.firstChild; child != null; child = child.nextSibling)
+            if (isInlineNode(child))
+                inlineChildren.push(child);
+        for (var i = 0; i < inlineChildren.length; i++) {
+            if (inlineChildren[i].parentNode == parent) { // may already have been moved
+                if (!isWhitespaceTextNode(inlineChildren[i]))
+                    Hierarchy_wrapInlineNodesInParagraph(inlineChildren[i]);
+            }
+        }
+    }
+
+    // private
+    function getParagraphs(nodes)
+    {
+        var array = new Array();
+        var set = new NodeSet();
+        for (var i = 0; i < nodes.length; i++) {
+            for (var anc = nodes[i].parentNode; anc != null; anc = anc.parentNode) {
+                if (anc._type == HTML_LI)
+                    putDirectInlineChildrenInParagraphs(anc);
+            }
+            recurse(nodes[i]);
+        }
+
+        var remove = new NodeSet();
+        for (var i = 0; i < array.length; i++) {
+            for (var anc = array[i].parentNode; anc != null; anc = anc.parentNode)
+                remove.add(anc);
+        }
+
+        var modified = new Array();
+        for (var i = 0; i < array.length; i++) {
+            if (!remove.contains(array[i]))
+                modified.push(array[i]);
+        }
+
+        return modified;
+
+        function recurse(node)
+        {
+            if (node._type == HTML_LI)
+                putDirectInlineChildrenInParagraphs(node);
+            if (node.firstChild == null) {
+                // Leaf node
+                for (var anc = node; anc != null; anc = anc.parentNode)
+                    if (isParagraphNode(anc)) {
+                        add(anc);
+                    }
+            }
+            else {
+                for (var child = node.firstChild; child != null; child = child.nextSibling)
+                    recurse(child);
+            }
+        }
+
+        function add(node)
+        {
+            if (!set.contains(node)) {
+                array.push(node);
+                set.add(node);
+            }
+        }
+    }
+
+    // private
+    function setParagraphStyle(paragraph,selector)
+    {
+        var wasHeading = isHeadingNode(paragraph);
+        DOM_removeAttribute(paragraph,"class");
+        if (selector == "") {
+            if (paragraph._type != HTML_P)
+                paragraph = DOM_replaceElement(paragraph,"P");
+        }
+        else {
+            var elementClassRegex = /^([a-zA-Z0-9]+)?(\.(.+))?$/;
+            var result = elementClassRegex.exec(selector);
+            if ((result != null) && (result.length == 4)) {
+                var elementName = result[1];
+                var className = result[3];
+
+                if (elementName == null)
+                    elementName = "P";
+                else
+                    elementName = elementName.toUpperCase();
+
+                var elementType = ElementTypes[elementName];
+
+                if (!PARAGRAPH_ELEMENTS[elementType])
+                    return; // better than throwing an exception
+
+                if (paragraph._type != elementType)
+                    paragraph = DOM_replaceElement(paragraph,elementName);
+
+                if (className != null)
+                    DOM_setAttribute(paragraph,"class",className);
+                else
+                    DOM_removeAttribute(paragraph,"class");
+            }
+        }
+
+        // FIXME: this will need to change when we add Word/ODF support, because the ids serve
+        // a purpose other than simply being targets for references
+        var isHeading = isHeadingNode(paragraph);
+        if (wasHeading && !isHeading)
+            DOM_removeAttribute(paragraph,"id");
+    }
+
+    // public
+    Formatting_pushDownInlineProperties = function(outermost)
+    {
+        for (var i = 0; i < outermost.length; i++)
+            outermost[i] = pushDownInlinePropertiesSingle(outermost[i]);
+    }
+
+    // private
+    function pushDownInlinePropertiesSingle(target)
+    {
+        recurse(target.parentNode);
+        return target;
+
+        function recurse(node)
+        {
+            if (node.nodeType == Node.DOCUMENT_NODE)
+                return;
+
+            if (node.parentNode != null)
+                recurse(node.parentNode);
+
+            var inlineProperties = new Object();
+            var nodeProperties = getStyleProperties(node);
+            for (var name in nodeProperties) {
+                if (isInlineProperty(name)) {
+                    inlineProperties[name] = nodeProperties[name];
+                }
+            }
+
+            var remove = new Object();
+            for (var name in inlineProperties)
+                remove[name] = null;
+            DOM_setStyleProperties(node,remove);
+
+            var type = node._type;
+            switch (type) {
+            case HTML_B:
+                inlineProperties["font-weight"] = "bold";
+                break;
+            case HTML_I:
+                inlineProperties["font-style"] = "italic";
+                break;
+            case HTML_U:
+                if (inlineProperties["text-decoration"] != null)
+                    inlineProperties["text-decoration"] += " underline";
+                else
+                    inlineProperties["text-decoration"] = "underline";
+                break;
+            }
+
+            var special = extractSpecial(inlineProperties);
+            var count = Object.getOwnPropertyNames(inlineProperties).length;
+
+            if ((count > 0) || special.bold || special.italic || special.underline) {
+
+                var next;
+                for (var child = node.firstChild; child != null; child = next) {
+                    next = child.nextSibling;
+
+                    if (isWhitespaceTextNode(child))
+                        continue;
+
+                    var replacement = applyInlineFormatting(child,inlineProperties,special);
+                    if (target == child)
+                        target = replacement;
+                }
+            }
+
+            if (node.hasAttribute("style") && (node.style.length == 0))
+                DOM_removeAttribute(node,"style");
+
+            switch (type) {
+            case HTML_B:
+            case HTML_I:
+            case HTML_U:
+                DOM_removeNodeButKeepChildren(node);
+                break;
+            }
+        }
+    }
+
+    // private
+    function wrapInline(node,elementName)
+    {
+        if (!isInlineNode(node) || isAbstractSpan(node)) {
+            var next;
+            for (var child = node.firstChild; child != null; child = next) {
+                next = child.nextSibling;
+                wrapInline(child,elementName);
+            }
+            return node;
+        }
+        else {
+            return DOM_wrapNode(node,elementName);
+        }
+    }
+
+    // private
+    function applyInlineFormatting(target,inlineProperties,special,applyToWhitespace)
+    {
+        if (!applyToWhitespace && isWhitespaceTextNode(target))
+            return;
+
+        if (special.underline)
+            target = wrapInline(target,"U");
+        if (special.italic)
+            target = wrapInline(target,"I");
+        if (special.bold)
+            target = wrapInline(target,"B");
+
+        var isbiu = false;
+        switch (target._type) {
+        case HTML_B:
+        case HTML_I:
+        case HTML_U:
+            isbiu = true;
+            break;
+        }
+
+        if ((Object.getOwnPropertyNames(inlineProperties).length > 0) &&
+            ((target.nodeType != Node.ELEMENT_NODE) ||
+             isbiu || isSpecialSpan(target))) {
+            target = wrapInline(target,"SPAN");
+        }
+
+
+        var propertiesToSet = new Object();
+        for (var name in inlineProperties) {
+            var existing = target.style.getPropertyValue(name);
+            if ((existing == null) || (existing == ""))
+                propertiesToSet[name] = inlineProperties[name];
+        }
+        DOM_setStyleProperties(target,propertiesToSet);
+
+        return target;
+    }
+
+    // private
+    function extractSpecial(properties)
+    {
+        var special = { bold: null, italic: null, underline: null };
+        var fontWeight = properties["font-weight"];
+        var fontStyle = properties["font-style"];
+        var textDecoration = properties["text-decoration"];
+
+        if (typeof(fontWeight) != "undefined") {
+            special.bold = false;
+            if ((fontWeight != null) &&
+                (fontWeight.toLowerCase() == "bold")) {
+                special.bold = true;
+                delete properties["font-weight"];
+            }
+        }
+
+        if (typeof(fontStyle) != "undefined") {
+            special.italic = false;
+            if ((fontStyle != null) &&
+                (fontStyle.toLowerCase() == "italic")) {
+                special.italic = true;
+                delete properties["font-style"];
+            }
+        }
+
+        if (typeof(textDecoration) != "undefined") {
+            special.underline = false;
+            if (textDecoration != null) {
+                var values = textDecoration.toLowerCase().split(/\s+/);
+                var index;
+                while ((index = values.indexOf("underline")) >= 0) {
+                    values.splice(index,1);
+                    special.underline = true;
+                }
+                if (values.length == 0)
+                    delete properties["text-decoration"];
+                else
+                    properties["text-decoration"] = values.join(" ");
+            }
+        }
+        return special;
+    }
+
+    // private
+    function removeProperties(outermost,properties)
+    {
+        properties = clone(properties);
+        var special = extractSpecial(properties);
+        var remaining = new Array();
+        for (var i = 0; i < outermost.length; i++) {
+            removePropertiesSingle(outermost[i],properties,special,remaining);
+        }
+        return remaining;
+    }
+
+    // private
+    function getOutermostParagraphs(paragraphs)
+    {
+        var all = new NodeSet();
+        for (var i = 0; i < paragraphs.length; i++)
+            all.add(paragraphs[i]);
+
+        var result = new Array();
+        for (var i = 0; i < paragraphs.length; i++) {
+            var haveAncestor = false;
+            for (var p = paragraphs[i].parentNode; p != null; p = p.parentNode) {
+                if (all.contains(p)) {
+                    haveAncestor = true;
+                    break;
+                }
+            }
+            if (!haveAncestor)
+                result.push(paragraphs[i]);
+        }
+        return result;
+    }
+
+    // private
+    function removePropertiesSingle(node,properties,special,remaining)
+    {
+        if ((node.nodeType == Node.ELEMENT_NODE) && (node.hasAttribute("style"))) {
+            var remove = new Object();
+            for (var name in properties)
+                remove[name] = null;
+            DOM_setStyleProperties(node,remove);
+        }
+
+        var willRemove = false;
+        switch (node._type) {
+        case HTML_B:
+            willRemove = (special.bold != null);
+            break;
+        case HTML_I:
+            willRemove = (special.italic != null);
+            break;
+        case HTML_U:
+            willRemove = (special.underline != null);
+            break;
+        case HTML_SPAN:
+            willRemove = (!node.hasAttribute("style") && !isSpecialSpan(node));
+            break;
+        }
+
+        var childRemaining = willRemove ? remaining : null;
+
+        var next;
+        for (var child = node.firstChild; child != null; child = next) {
+            next = child.nextSibling;
+            removePropertiesSingle(child,properties,special,childRemaining);
+        }
+
+        if (willRemove)
+            DOM_removeNodeButKeepChildren(node);
+        else if (remaining != null)
+            remaining.push(node);
+    }
+
+    function isSpecialSpan(span)
+    {
+        if (span._type == HTML_SPAN) {
+            if (span.hasAttribute(Keys.ABSTRACT_ELEMENT))
+                return true;
+            if (DOM_getStringAttribute(span,"class").indexOf(Keys.UXWRITE_PREFIX) == 0)
+                return true;
+        }
+        return false;
+    }
+
+    // private
+    function containsOnlyWhitespace(ancestor)
+    {
+        for (child = ancestor.firstChild; child != null; child = child.nextSibling) {
+            if (!isWhitespaceTextNode(child))
+                return false;
+        }
+        return true;
+    }
+
+    // public
+    Formatting_applyFormattingChanges = function(style,properties)
+    {
+        debug("JS: applyFormattingChanges: style = "+JSON.stringify(style));
+        if (properties != null) {
+            var names = Object.getOwnPropertyNames(properties).sort();
+            for (var i = 0; i < names.length; i++) {
+                debug("    "+names[i]+" = "+properties[names[i]]);
+            }
+        }
+        UndoManager_newGroup("Apply formatting changes");
+
+        if (properties == null)
+            properties = new Object();
+
+        if (style == Keys.NONE_STYLE)
+            style = null;
+
+        var paragraphProperties = new Object();
+        var inlineProperties = new Object();
+
+        for (var name in properties) {
+            if (isParagraphProperty(name))
+                paragraphProperties[name] = properties[name];
+            else if (isInlineProperty(name))
+                inlineProperties[name] = properties[name];
+        }
+
+        var selectionRange = Selection_get();
+        if (selectionRange == null)
+            return;
+
+        // If we're applying formatting properties to an empty selection, and the node of the
+        // selection start & end is an element, add an empty text node so that we have something
+        // to apply the formatting to.
+        if (Range_isEmpty(selectionRange) &&
+            (selectionRange.start.node.nodeType == Node.ELEMENT_NODE)) {
+            var node = selectionRange.start.node;
+            var offset = selectionRange.start.offset;
+            var text = DOM_createTextNode(document,"");
+            DOM_insertBefore(node,text,node.childNodes[offset]);
+            Selection_set(text,0,text,0);
+            selectionRange = Selection_get();
+        }
+
+        // If the cursor is in a container (such as BODY OR FIGCAPTION), and not inside a paragraph,
+        // put it in one so we can set a paragraph style
+
+        if ((style != null) && Range_isEmpty(selectionRange)) {
+            var node = Range_singleNode(selectionRange);
+            while (isInlineNode(node))
+                node = node.parentNode;
+            if (isContainerNode(node) && containsOnlyInlineChildren(node)) {
+                var p = DOM_createElement(document,"P");
+                DOM_appendChild(node,p);
+                while (node.firstChild != p)
+                    DOM_appendChild(p,node.firstChild);
+                Cursor_updateBRAtEndOfParagraph(p);
+            }
+        }
+
+
+        var range = new Range(selectionRange.start.node,selectionRange.start.offset,
+                              selectionRange.end.node,selectionRange.end.offset);
+        var positions = [selectionRange.start,selectionRange.end,
+                         range.start,range.end];
+
+        var allowDirectInline = (style == null);
+        Position_trackWhileExecuting(positions,function() {
+            Formatting_splitAroundSelection(range,allowDirectInline);
+            Range_expand(range);
+            if (!allowDirectInline)
+                Range_ensureInlineNodesInParagraph(range);
+            Range_ensureValidHierarchy(range);
+            Range_expand(range);
+            var outermost = Range_getOutermostNodes(range);
+            var target = null;
+
+            var paragraphs;
+            if (outermost.length > 0)
+                paragraphs = getParagraphs(outermost);
+            else
+                paragraphs = getParagraphs([Range_singleNode(range)]);
+
+            // Push down inline properties
+            Formatting_pushDownInlineProperties(outermost);
+
+            outermost = removeProperties(outermost,inlineProperties);
+
+            // Set properties on inline nodes
+            for (var i = 0; i < outermost.length; i++) {
+                var existing = Formatting_getAllNodeProperties(outermost[i]);
+                var toSet = new Object();
+                for (var name in inlineProperties) {
+                    if ((inlineProperties[name] != null) &&
+                        (existing[name] != inlineProperties[name])) {
+                        toSet[name] = inlineProperties[name];
+                    }
+                }
+
+                var special = extractSpecial(toSet);
+                var applyToWhitespace = (outermost.length == 1);
+                applyInlineFormatting(outermost[i],toSet,special,applyToWhitespace);
+            }
+
+            // Remove properties from paragraph nodes
+            paragraphs = removeProperties(paragraphs,paragraphProperties,{});
+
+            // Set properties on paragraph nodes
+            var paragraphPropertiesToSet = new Object();
+            for (var name in paragraphProperties) {
+                if (paragraphProperties[name] != null)
+                    paragraphPropertiesToSet[name] = paragraphProperties[name];
+            }
+
+            var outermostParagraphs = getOutermostParagraphs(paragraphs);
+            for (var i = 0; i < outermostParagraphs.length; i++)
+                DOM_setStyleProperties(outermostParagraphs[i],paragraphPropertiesToSet);
+
+            // Set style on paragraph nodes
+            if (style != null) {
+                for (var i = 0; i < paragraphs.length; i++) {
+                    setParagraphStyle(paragraphs[i],style);
+                }
+            }
+
+            mergeRange(range,Formatting_MERGEABLE_INLINE);
+
+            if (target != null) {
+                for (var p = target; p != null; p = next) {
+                    next = p.parentNode;
+                    Formatting_mergeWithNeighbours(p,Formatting_MERGEABLE_INLINE);
+                }
+            }
+        });
+
+        // The current cursor position may no longer be valid, e.g. if a heading span was inserted
+        // and the cursor is at a position that is now immediately before the span.
+        var start = Position_closestMatchForwards(selectionRange.start,Position_okForInsertion);
+        var end = Position_closestMatchBackwards(selectionRange.end,Position_okForInsertion);
+        var tempRange = new Range(start.node,start.offset,end.node,end.offset);
+        tempRange = Range_forwards(tempRange);
+        Range_ensureValidHierarchy(tempRange);
+        start = tempRange.start;
+        end = tempRange.end;
+        Selection_set(start.node,start.offset,end.node,end.offset);
+
+        function containsOnlyInlineChildren(node)
+        {
+            for (var child = node.firstChild; child != null; child = child.nextSibling) {
+                if (!isInlineNode(child))
+                    return false;
+            }
+            return true;
+        }
+    }
+
+    Formatting_formatInlineNode = function(node,properties)
+    {
+        properties = clone(properties);
+        var special = extractSpecial(properties);
+        return applyInlineFormatting(node,properties,special,true);
+    }
+
+    Formatting_MERGEABLE_INLINE = new Array(HTML_COUNT);
+
+    Formatting_MERGEABLE_INLINE[HTML_TEXT] = true;
+
+    Formatting_MERGEABLE_INLINE[HTML_SPAN] = true;
+    Formatting_MERGEABLE_INLINE[HTML_A] = true;
+    Formatting_MERGEABLE_INLINE[HTML_Q] = true;
+
+    // HTML 4.01 Section 9.2.1: Phrase elements
+    Formatting_MERGEABLE_INLINE[HTML_EM] = true;
+    Formatting_MERGEABLE_INLINE[HTML_STRONG] = true;
+    Formatting_MERGEABLE_INLINE[HTML_DFN] = true;
+    Formatting_MERGEABLE_INLINE[HTML_CODE] = true;
+    Formatting_MERGEABLE_INLINE[HTML_SAMP] = true;
+    Formatting_MERGEABLE_INLINE[HTML_KBD] = true;
+    Formatting_MERGEABLE_INLINE[HTML_VAR] = true;
+    Formatting_MERGEABLE_INLINE[HTML_CITE] = true;
+    Formatting_MERGEABLE_INLINE[HTML_ABBR] = true;
+
+    // HTML 4.01 Section 9.2.3: Subscripts and superscripts
+    Formatting_MERGEABLE_INLINE[HTML_SUB] = true;
+    Formatting_MERGEABLE_INLINE[HTML_SUP] = true;
+
+    // HTML 4.01 Section 15.2.1: Font style elements
+    Formatting_MERGEABLE_INLINE[HTML_I] = true;
+    Formatting_MERGEABLE_INLINE[HTML_B] = true;
+    Formatting_MERGEABLE_INLINE[HTML_SMALL] = true;
+    Formatting_MERGEABLE_INLINE[HTML_S] = true;
+    Formatting_MERGEABLE_INLINE[HTML_U] = true;
+
+    Formatting_MERGEABLE_BLOCK = new Array(HTML_COUNT);
+
+    Formatting_MERGEABLE_BLOCK[HTML_P] = true;
+    Formatting_MERGEABLE_BLOCK[HTML_H1] = true;
+    Formatting_MERGEABLE_BLOCK[HTML_H2] = true;
+    Formatting_MERGEABLE_BLOCK[HTML_H3] = true;
+    Formatting_MERGEABLE_BLOCK[HTML_H4] = true;
+    Formatting_MERGEABLE_BLOCK[HTML_H5] = true;
+    Formatting_MERGEABLE_BLOCK[HTML_H6] = true;
+    Formatting_MERGEABLE_BLOCK[HTML_DIV] = true;
+    Formatting_MERGEABLE_BLOCK[HTML_PRE] = true;
+    Formatting_MERGEABLE_BLOCK[HTML_BLOCKQUOTE] = true;
+
+    Formatting_MERGEABLE_BLOCK[HTML_UL] = true;
+    Formatting_MERGEABLE_BLOCK[HTML_OL] = true;
+    Formatting_MERGEABLE_BLOCK[HTML_LI] = true;
+
+    Formatting_MERGEABLE_BLOCK_AND_INLINE = new Array(HTML_COUNT);
+    for (var i = 0; i < HTML_COUNT; i++) {
+        if (Formatting_MERGEABLE_INLINE[i] || Formatting_MERGEABLE_BLOCK[i])
+            Formatting_MERGEABLE_BLOCK_AND_INLINE[i] = true;
+        Formatting_MERGEABLE_BLOCK_AND_INLINE["force"] = true;
+    }
+
+})();

http://git-wip-us.apache.org/repos/asf/incubator-corinthia/blob/03bd5af0/Editor/src/Hierarchy.js
----------------------------------------------------------------------
diff --git a/Editor/src/Hierarchy.js b/Editor/src/Hierarchy.js
new file mode 100644
index 0000000..3151082
--- /dev/null
+++ b/Editor/src/Hierarchy.js
@@ -0,0 +1,281 @@
+// Copyright 2011-2014 UX Productivity Pty Ltd
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+var Hierarchy_ensureValidHierarchy;
+var Hierarchy_ensureInlineNodesInParagraph;
+var Hierarchy_wrapInlineNodesInParagraph;
+var Hierarchy_avoidInlineChildren;
+
+(function() {
+
+    // private
+    function wrapInlineChildren(first,last,ancestors)
+    {
+        var haveNonWhitespace = false;
+        for (var node = first; node != last.nextSibling; node = node.nextSibling) {
+            if (!isWhitespaceTextNode(node))
+                haveNonWhitespace = true;
+        }
+        if (!haveNonWhitespace)
+            return false;
+
+        var parentNode = first.parentNode;
+        var nextSibling = first;
+        for (var i = ancestors.length-1; i >= 0; i--) {
+            var ancestorCopy = DOM_shallowCopyElement(ancestors[i]);
+            DOM_insertBefore(parentNode,ancestorCopy,nextSibling);
+            parentNode = ancestorCopy;
+            nextSibling = null;
+
+            var node = first;
+            while (true) {
+                var next = node.nextSibling;
+                DOM_insertBefore(parentNode,node,null);
+                if (node == last)
+                    break;
+                node = next;
+            }
+        }
+    }
+
+    // private
+    function wrapInlineChildrenInAncestors(node,ancestors)
+    {
+        var firstInline = null;
+        var lastInline = null;
+
+        var child = node.firstChild;
+        while (true) {
+            var next = (child != null) ? child.nextSibling : null;
+            if ((child == null) || !isInlineNode(child)) {
+
+                if ((firstInline != null) && (lastInline != null)) {
+                    wrapInlineChildren(firstInline,lastInline,ancestors);
+                }
+                firstInline = null;
+                lastInline = null;
+                if (child != null)
+                    wrapInlineChildrenInAncestors(child,ancestors);
+            }
+            else {
+                if (firstInline == null)
+                    firstInline = child;
+                lastInline = child;
+            }
+            if (child == null)
+                break;
+            child = next;
+        }
+    }
+
+    function checkInvalidNesting(node)
+    {
+        var parent = node.parentNode;
+        if ((parent._type == HTML_DIV) &&
+            (DOM_getAttribute(parent,"class") == Keys.SELECTION_CLASS)) {
+            parent = parent.parentNode;
+        }
+
+        var invalidNesting = !isContainerNode(parent);
+        switch (parent._type) {
+        case HTML_DIV:
+            if (isParagraphNode(node) || isListNode(node))
+                invalidNesting = false; // this case is ok
+            break;
+        case HTML_CAPTION:
+        case HTML_FIGCAPTION:
+        case HTML_TABLE:
+        case HTML_FIGURE:
+            switch (node._type) {
+            case HTML_FIGURE:
+            case HTML_TABLE:
+            case HTML_H1:
+            case HTML_H2:
+            case HTML_H3:
+            case HTML_H4:
+            case HTML_H5:
+            case HTML_H6:
+                return true;
+            }
+            break;
+        }
+
+        return invalidNesting;
+    }
+
+    function checkInvalidHeadingNesting(node)
+    {
+        switch (node._type) {
+        case HTML_H1:
+        case HTML_H2:
+        case HTML_H3:
+        case HTML_H4:
+        case HTML_H5:
+        case HTML_H6:
+            switch (node.parentNode._type) {
+            case HTML_BODY:
+            case HTML_NAV:
+            case HTML_DIV:
+                return false;
+            default:
+                return true;
+            }
+            break;
+        default:
+            return false;
+        }
+    }
+
+    function nodeHasSignificantChildren(node)
+    {
+        for (var child = node.firstChild; child != null; child = child.nextSibling) {
+            if (!isWhitespaceTextNode(child))
+                return true;
+        }
+        return false;
+    }
+
+    // Enforce the restriction that any path from the root to a given node must be of the form
+    //    container+ paragraph inline
+    // or container+ paragraph
+    // or container+
+    // public
+    Hierarchy_ensureValidHierarchy = function(node,recursive,allowDirectInline)
+    {
+        var count = 0;
+        while ((node != null) && (node.parentNode != null) && (node != document.body)) {
+            count++;
+            if (count > 200)
+                throw new Error("too many iterations");
+
+            if (checkInvalidHeadingNesting(node)) {
+                var offset = DOM_nodeOffset(node);
+                var parent = node.parentNode;
+                Formatting_moveFollowing(new Position(node.parentNode,offset+1),
+                                         function() { return false; });
+                DOM_insertBefore(node.parentNode.parentNode,
+                                 node,
+                                 node.parentNode.nextSibling);
+
+                while ((parent != document.body) && !nodeHasSignificantChildren(parent)) {
+                    var grandParent = parent.parentNode;
+                    DOM_deleteNode(parent);
+                    parent = grandParent;
+                }
+
+                continue;
+            }
+            else if (isContainerNode(node) || isParagraphNode(node)) {
+                var invalidNesting = checkInvalidNesting(node);
+                if (invalidNesting) {
+                    var ancestors = new Array();
+                    var child = node;
+                    while (!isContainerNode(child.parentNode)) {
+                        if (isInlineNode(child.parentNode)) {
+                            var keep = false;
+                            if (child.parentNode._type == HTML_SPAN) {
+                                for (var i = 0; i < child.attributes.length; i++) {
+                                    var attr = child.attributes[i];
+                                    if (attr.nodeName.toUpperCase() != "ID")
+                                        keep = true;
+                                }
+                                if (keep)
+                                    ancestors.push(child.parentNode);
+                            }
+                            else {
+                                ancestors.push(child.parentNode);
+                            }
+                        }
+                        child = child.parentNode;
+                    }
+
+                    while (checkInvalidNesting(node)) {
+                        var offset = DOM_nodeOffset(node);
+                        var parent = node.parentNode;
+                        Formatting_moveFollowing(new Position(node.parentNode,offset+1),
+                                                 isContainerNode);
+                        DOM_insertBefore(node.parentNode.parentNode,
+                                         node,
+                                         node.parentNode.nextSibling);
+                        if (!nodeHasSignificantChildren(parent))
+                            DOM_deleteNode(parent);
+
+                    }
+                    wrapInlineChildrenInAncestors(node,ancestors);
+                }
+            }
+
+            node = node.parentNode;
+        }
+    }
+
+    Hierarchy_ensureInlineNodesInParagraph = function(node,weak)
+    {
+        var count = 0;
+        while ((node != null) && (node.parentNode != null) && (node != document.body)) {
+            count++;
+            if (count > 200)
+                throw new Error("too many iterations");
+            if (isInlineNode(node) &&
+                isContainerNode(node.parentNode) && (node.parentNode._type != HTML_LI) &&
+                (!weak || !isTableCell(node.parentNode)) &&
+                !isWhitespaceTextNode(node)) {
+                Hierarchy_wrapInlineNodesInParagraph(node);
+                return;
+            }
+            node = node.parentNode;
+        }
+    }
+
+    // public
+    Hierarchy_wrapInlineNodesInParagraph = function(node)
+    {
+        var start = node;
+        var end = node;
+
+        while ((start.previousSibling != null) && isInlineNode(start.previousSibling))
+            start = start.previousSibling;
+        while ((end.nextSibling != null) && isInlineNode(end.nextSibling))
+            end = end.nextSibling;
+
+        return DOM_wrapSiblings(start,end,"P");
+    }
+
+    Hierarchy_avoidInlineChildren = function(parent)
+    {
+        var child = parent.firstChild;
+
+        while (child != null) {
+            if (isInlineNode(child)) {
+                var start = child;
+                var end = child;
+                var haveContent = nodeHasContent(end);
+                while ((end.nextSibling != null) && isInlineNode(end.nextSibling)) {
+                    end = end.nextSibling;
+                    if (nodeHasContent(end))
+                        haveContent = true;
+                }
+                child = DOM_wrapSiblings(start,end,"P");
+                var next = child.nextSibling;
+                if (!nodeHasContent(child))
+                    DOM_deleteNode(child);
+                child = next;
+            }
+            else {
+                child = child.nextSibling;
+            }
+        }
+    }
+
+})();

http://git-wip-us.apache.org/repos/asf/incubator-corinthia/blob/03bd5af0/Editor/src/Input.js
----------------------------------------------------------------------
diff --git a/Editor/src/Input.js b/Editor/src/Input.js
new file mode 100644
index 0000000..0be1294
--- /dev/null
+++ b/Editor/src/Input.js
@@ -0,0 +1,743 @@
+// Copyright 2011-2014 UX Productivity Pty Ltd
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+var Input_removePosition;
+var Input_addPosition;
+var Input_getPosition;
+var Input_textInRange;
+var Input_replaceRange;
+var Input_selectedTextRange;
+var Input_setSelectedTextRange;
+var Input_markedTextRange;
+var Input_setMarkedText;
+var Input_unmarkText;
+var Input_forwardSelectionAffinity;
+var Input_setForwardSelectionAffinity;
+var Input_positionFromPositionOffset;
+var Input_positionFromPositionInDirectionOffset;
+var Input_comparePositionToPosition;
+var Input_offsetFromPositionToPosition;
+var Input_positionWithinRangeFarthestInDirection;
+var Input_characterRangeByExtendingPositionInDirection;
+var Input_firstRectForRange;
+var Input_caretRectForPosition;
+var Input_closestPositionToPoint;
+var Input_closestPositionToPointWithinRange;
+var Input_characterRangeAtPoint;
+var Input_positionWithinRangeAtCharacterOffset;
+var Input_characterOffsetOfPositionWithinRange;
+
+var Input_isAtWordBoundary;
+var Input_isAtParagraphBoundary;
+var Input_isPositionAtBoundaryGranularityInDirection;
+var Input_isPositionWithinTextUnitInDirection;
+var Input_toWordBoundary;
+var Input_toParagraphBoundary;
+var Input_toLineBoundary;
+var Input_positionFromPositionToBoundaryInDirection;
+var Input_rangeEnclosingPositionWithGranularityInDirection;
+
+// FIXME: ensure updateFormatting() is called after any cursor/selection changes
+// FIXME: test capitalisation of on-screen keyboard at start of sentence
+
+(function() {
+
+    //function idebug(str)
+    //{
+    //    debug(str);
+    //}
+
+    var forwardSelection = true;
+    var positions = new Object();
+    var BaseIdNull = 0;
+    var BaseIdDocumentStart = 1;
+    var BaseIdDocumentEnd = 2;
+    var BaseIdSelectionStart = 3;
+    var BaseIdSelectionEnd = 4;
+    var firstDynamicPosId = 5;
+    var nextPosId = firstDynamicPosId;
+
+    function addPosition(pos)
+    {
+        if (pos == null)
+            return 0;
+        var copy = new Position(pos.node,pos.offset);
+        copy.targetX = pos.targetX;
+        pos = copy;
+        pos.posId = nextPosId++;
+        positions[pos.posId] = pos;
+        Position_track(pos);
+        return pos.posId;
+    }
+
+    Input_addPosition = addPosition;
+
+    function getPosition(posId)
+    {
+        if (posId instanceof Position) // for tests
+            return posId;
+        if (posId < firstDynamicPosId) {
+            switch (posId) {
+            case BaseIdNull: {
+                return null;
+            }
+            case BaseIdDocumentStart: {
+                var pos = new Position(document.body,0);
+                pos = Position_closestMatchForwards(pos,Position_okForMovement);
+                return pos;
+            }
+            case BaseIdDocumentEnd: {
+                var pos = new Position(document.body,document.body.childNodes.length);
+                pos = Position_closestMatchBackwards(pos,Position_okForMovement);
+                return pos;
+            }
+            case BaseIdSelectionStart: {
+                var range = Selection_get();
+                return (range != null) ? range.start : null;
+            }
+            case BaseIdSelectionEnd: {
+                var range = Selection_get();
+                return (range != null) ? range.end : null;
+            }
+            default:
+                return null;
+            }
+        }
+        if (positions[posId] == null)
+            throw new Error("No position for pos id "+posId);
+        return positions[posId];
+    }
+
+    Input_getPosition = getPosition;
+
+    // void
+    Input_removePosition = function(posId)
+    {
+        //idebug("Input_removePosition("+posId+")");
+        var pos = positions[posId];
+        if (pos == null) {
+            throw new Error("no position for id "+posId);
+        }
+        Position_untrack(pos);
+        delete positions[posId];
+    }
+
+    // string
+    Input_textInRange = function(startId,startAdjust,endId,endAdjust)
+    {
+        var start = getPosition(startId);
+        var end = getPosition(endId);
+        start = positionRight(start,startAdjust);
+        end = positionRight(end,endAdjust);
+        if ((start == null) || (end == null))
+            return "";
+
+        var range = new Range(start.node,start.offset,end.node,end.offset);
+        var result = Range_getText(range);
+        //idebug("Input_textInRange("+startId+","+startAdjust+","+endId+","+endAdjust+") = "+
+        //       JSON.stringify(result));
+        return result;
+    }
+
+    // void
+    Input_replaceRange = function(startId,endId,text)
+    {
+        //idebug("Input_replaceRange("+startId+","+endId+","+JSON.stringify(text)+")");
+        var start = getPosition(startId);
+        var end = getPosition(endId);
+        if (start == null)
+            throw new Error("start is null");
+        if (end == null)
+            throw new Error("end is null");
+
+        var range = new Range(start.node,start.offset,end.node,end.offset);
+        Range_trackWhileExecuting(range,function() {
+            Selection_deleteRangeContents(range,true);
+        });
+        range.start = Position_preferTextPosition(range.start);
+        var node = range.start.node;
+        var offset = range.start.offset;
+
+        if (node.nodeType == Node.TEXT_NODE) {
+            DOM_insertCharacters(node,offset,text);
+            Cursor_set(node,offset+text.length);
+        }
+        else if (node.nodeType == Node.ELEMENT_NODE) {
+            var textNode = DOM_createTextNode(document,text);
+            DOM_insertBefore(node,textNode,node.childNodes[offset]);
+            Cursor_set(node,offset+1);
+        }
+    }
+
+    // { startId, endId }
+    Input_selectedTextRange = function()
+    {
+        var range = Selection_get();
+        if (range == null) {
+            //idebug("Input_selectedTextRange = null");
+            return null;
+        }
+        else {
+            var startId = addPosition(range.start);
+            var endId = addPosition(range.end);
+            //idebug("Input_selectedTextRange = "+startId+", "+endId);
+            return { startId: startId,
+                     endId: endId };
+        }
+    }
+
+    // void
+    Input_setSelectedTextRange = function(startId,endId)
+    {
+        //idebug("Input_setSelectedTextRange("+startId+","+endId+")");
+        var start = getPosition(startId);
+        var end = getPosition(endId);
+
+        var oldSelection = Selection_get();
+        var oldStart = (oldSelection != null) ? oldSelection.start : null;
+        var oldEnd = (oldSelection != null) ? oldSelection.end : null;
+
+        Selection_set(start.node,start.offset,end.node,end.offset);
+
+        // The positions may have changed as a result of spans being added/removed
+        var newRange = Selection_get();
+        start = newRange.start;
+        end = newRange.end;
+
+        if (Position_equal(start,end))
+            Cursor_ensurePositionVisible(end);
+        else if (Position_equal(oldStart,start) && !Position_equal(oldEnd,end))
+            Cursor_ensurePositionVisible(end);
+        else if (Position_equal(oldEnd,end) && !Position_equal(oldStart,start))
+            Cursor_ensurePositionVisible(start);
+    }
+
+    // { startId, endId }
+    Input_markedTextRange = function()
+    {
+        //idebug("Input_markedTextRange");
+        return null;
+    }
+
+    // void
+    Input_setMarkedText = function(text,startOffset,endOffset)
+    {
+        Selection_deleteContents(true);
+        var oldSel = Selection_get();
+        Range_trackWhileExecuting(oldSel,function() {
+            Cursor_insertCharacter(text,false,false,true);
+        });
+        var newSel = Selection_get();
+
+        Selection_set(oldSel.start.node,oldSel.start.offset,
+                      newSel.end.node,newSel.end.offset,false,true);
+    }
+
+    // void
+    Input_unmarkText = function()
+    {
+        var range = Selection_get();
+        Cursor_set(range.end.node,range.end.offset);
+        //idebug("Input_unmarkText");
+    }
+
+    // boolean
+    Input_forwardSelectionAffinity = function()
+    {
+        //idebug("Input_forwardSelectionAffinity");
+        return forwardSelection;
+    }
+
+    // void
+    Input_setForwardSelectionAffinity = function(value)
+    {
+        //idebug("Input_setForwardSelectionAffinity");
+        forwardSelection = value;
+    }
+
+    function positionRight(pos,offset)
+    {
+        if (offset > 0) {
+            for (; offset > 0; offset--) {
+                var next = Position_nextMatch(pos,Position_okForMovement);
+                if (next == null)
+                    return pos;
+                pos = next;
+            }
+        }
+        else {
+            for (; offset < 0; offset++) {
+                var prev = Position_prevMatch(pos,Position_okForMovement);
+                if (prev == null)
+                    return pos;
+                pos = prev;
+            }
+        }
+        return pos;
+    }
+
+    function positionDown(pos,offset)
+    {
+        if (offset > 0) {
+            for (; offset > 0; offset--) {
+                var below = Text_posBelow(pos);
+                if (below == null)
+                    return pos;
+                pos = below;
+            }
+        }
+        else {
+            for (; offset < 0; offset++) {
+                var above = Text_posAbove(pos);
+                if (above == null)
+                    return pos;
+                pos = above;
+            }
+        }
+        return pos;
+    }
+
+    // posId
+    Input_positionFromPositionOffset = function(posId,offset)
+    {
+        var pos = getPosition(posId);
+        var res = addPosition(positionRight(pos,offset));
+        //idebug("Input_positionFromPositionOffset("+posId+","+offset+") = "+res);
+        return res;
+    }
+
+    // posId
+    Input_positionFromPositionInDirectionOffset = function(posId,direction,offset)
+    {
+        //idebug("Input_positionFromPositionInDirectionOffset("+posId+","+direction+","+offset+")");
+        var pos = getPosition(posId);
+        if (direction == "left")
+            return addPosition(positionRight(pos,-offset));
+        else if (direction == "right")
+            return addPosition(positionRight(pos,offset));
+        else if (direction == "up")
+            return addPosition(positionDown(pos,-offset));
+        else if (direction == "down")
+            return addPosition(positionDown(pos,offset));
+        else
+            throw new Error("unknown direction: "+direction);
+    }
+
+    // int
+    Input_comparePositionToPosition = function(posId1,posId2)
+    {
+        //idebug("Input_comparePositionToPosition("+posId1+","+posId2+")");
+        var pos1 = getPosition(posId1);
+        var pos2 = getPosition(posId2);
+        if (pos1 == null)
+            throw new Error("pos1 is null");
+        if (pos2 == null)
+            throw new Error("pos2 is null");
+        return Position_compare(pos1,pos2);
+    }
+
+    // int
+    Input_offsetFromPositionToPosition = function(fromId,toId)
+    {
+        //idebug("Input_offsetFromPositionToPosition("+fromId+","+toId+")");
+        throw new Error("offsetFromPositionToPosition: not implemented");
+    }
+
+    Input_positionWithinRangeFarthestInDirection = function(startId,endId,direction)
+    {
+        //idebug("Input_positionWithinRangeFarthestInDirection("+startId+","+endId+","+direction);
+        throw new Error("positionWithinRangeFarthestInDirection: not implemented");
+    }
+
+    // { startId, endId }
+    Input_characterRangeByExtendingPositionInDirection = function(posId,direction)
+    {
+        //idebug("Input_characterRangeByExtendingPositionInDirection("+posId+","+direction);
+        throw new Error("characterRangeByExtendingPositionInDirection: not implemented");
+    }
+
+    Input_firstRectForRange = function(startId,endId)
+    {
+        //idebug("Input_firstRectForRange("+startId+","+endId+")");
+        var start = getPosition(startId);
+        var end = getPosition(endId);
+        var range = new Range(start.node,start.offset,end.node,end.offset);
+        var rects = Range_getClientRects(range);
+        if (rects.length == 0)
+            return { x: 0, y: 0, width: 0, height: 0 };
+        else
+            return { x: rects[0].left, y: rects[0].top,
+                     width: rects[0].width, height: rects[0].height };
+    }
+
+    Input_caretRectForPosition = function(posId)
+    {
+        //idebug("Input_caretRectForPosition("+posId+")");
+        var pos = getPosition(posId);
+        var rect = Position_rectAtPos(pos);
+        if (rect == null)
+            return { x: 0, y: 0, width: 0, height: 0 };
+        else
+            return { x: rect.left, y: rect.top, width: rect.width, height: rect.height };
+    }
+
+    // posId
+    Input_closestPositionToPoint = function(x,y)
+    {
+        //idebug("Input_closestPositionToPoint("+x+","+y+")");
+        throw new Error("closestPositionToPoint: not implemented");
+    }
+
+    // posId
+    Input_closestPositionToPointWithinRange = function(x,y,startId,endId)
+    {
+        //idebug("Input_closestPositionToPointWithinRange("+x+","+y+")");
+        throw new Error("closestPositionToPointWithinRange: not implemented");
+    }
+
+    // { startId, endId }
+    Input_characterRangeAtPoint = function(x,y)
+    {
+        //idebug("Input_characterRangeAtPoint("+x+","+y+")");
+        throw new Error("characterRangeAtPoint: not implemented");
+    }
+
+    // posId
+    Input_positionWithinRangeAtCharacterOffset = function(startId,endId,offset)
+    {
+        //idebug("Input_positionWithinRangeAtCharacterOffset("+startId+","+endId+","+offset+")");
+        throw new Error("positionWithinRangeAtCharacterOffset: not implemented");
+    }
+
+    // int
+    Input_characterOffsetOfPositionWithinRange = function(posId,startId,endId)
+    {
+        //idebug("Input_characterOffsetOfPositionWithinRange("+posId+","+startId+","+endId+")");
+        throw new Error("characterOffsetOfPositionWithinRange: not implemented");
+    }
+
+    // UITextInputTokenizer methods
+
+    var punctuation = "!\"#%&',-/:;<=>@`~\\^\\$\\\\\\.\\*\\+\\?\\(\\)\\[\\]\\{\\}\\|";
+    var letterRE = new RegExp("[^\\s"+punctuation+"]");
+    var wordAtStartRE = new RegExp("^[^\\s"+punctuation+"]+");
+    var nonWordAtStartRE = new RegExp("^[\\s"+punctuation+"]+");
+    var wordAtEndRE = new RegExp("[^\\s"+punctuation+"]+$");
+    var nonWordAtEndRE = new RegExp("[\\s"+punctuation+"]+$");
+
+    function isForward(direction)
+    {
+        return ((direction == "forward") ||
+                (direction == "right") ||
+                (direction == "down"));
+    }
+
+    Input_isAtWordBoundary = function(pos,direction)
+    {
+        if (pos.node.nodeType != Node.TEXT_NODE)
+            return false;
+        var paragraph = Text_analyseParagraph(pos);
+        if (paragraph == null)
+            return false;
+        var offset = Paragraph_offsetAtPosition(paragraph,pos);
+        var before = paragraph.text.substring(0,offset);
+        var after = paragraph.text.substring(offset);
+        var text = paragraph.text;
+
+        var afterMatch = (offset < text.length) && (text.charAt(offset).match(letterRE));
+        var beforeMatch = (offset > 0) && (text.charAt(offset-1).match(letterRE));
+
+        // coerce to boolean
+        afterMatch = !!afterMatch;
+        beforeMatch = !!beforeMatch;
+
+        if (isForward(direction))
+            return beforeMatch && !afterMatch;
+        else
+            return !beforeMatch;
+    }
+
+    Input_isAtParagraphBoundary = function(pos,direction)
+    {
+    }
+
+    Input_isPositionAtBoundaryGranularityInDirection = function(posId,granularity,direction)
+    {
+        //idebug("Input_isPositionAtBoundaryGranularityInDirection("+
+        //       posId+","+granularity+","+direction+")");
+        var pos = getPosition(posId);
+        if (pos == null)
+            return false;
+
+        // FIXME: Temporary hack to avoid exceptions when running under iOS 8
+        if ((granularity == "sentence") || (granularity == "document"))
+            return false;
+
+        if (granularity == "character") {
+            return true;
+        }
+        else if (granularity == "word") {
+            return Input_isAtWordBoundary(pos,direction);
+        }
+        else if ((granularity == "paragraph") || (granularity == "line")) {
+            if (isForward(direction))
+                return Position_equal(pos,Text_toEndOfBoundary(pos,granularity));
+            else
+                return Position_equal(pos,Text_toStartOfBoundary(pos,granularity));
+        }
+        else if (granularity == "sentence") {
+        }
+        else if (granularity == "document") {
+        }
+        throw new Error("unsupported granularity: "+granularity);
+    }
+
+    Input_isPositionWithinTextUnitInDirection = function(posId,granularity,direction)
+    {
+        //idebug("Input_isPositionWithinTextUnitInDirection("+
+        //       posId+","+granularity+","+direction+")");
+        var pos = getPosition(posId);
+        if (pos == null)
+            return false;
+
+        // FIXME: Temporary hack to avoid exceptions when running under iOS 8
+        if ((granularity == "sentence") || (granularity == "document"))
+            return true;
+
+        if (granularity == "character") {
+            return true;
+        }
+        else if (granularity == "word") {
+            pos = Text_closestPosInDirection(pos,direction);
+            if (pos == null)
+                return false;
+            var paragraph = Text_analyseParagraph(pos);
+            if (paragraph == null)
+                return false;
+            if ((pos != null) && (pos.node.nodeType == Node.TEXT_NODE)) {
+                var offset = Paragraph_offsetAtPosition(paragraph,pos);
+                var text = paragraph.text;
+                if (isForward(direction))
+                    return !!((offset < text.length) && (text.charAt(offset).match(letterRE)));
+                else
+                    return !!((offset > 0) && (text.charAt(offset-1).match(letterRE)));
+            }
+            else {
+                return false;
+            }
+        }
+        else if (granularity == "sentence") {
+        }
+        else if ((granularity == "paragraph") || (granularity == "line")) {
+            var start = Text_toStartOfBoundary(pos,granularity);
+            var end = Text_toEndOfBoundary(pos,granularity);
+            start = start ? start : pos;
+            end = end ? end : pos;
+            if (isForward(direction)) {
+                return ((Position_compare(start,pos) <= 0) &&
+                        (Position_compare(pos,end) < 0));
+            }
+            else {
+                return ((Position_compare(start,pos) < 0) &&
+                        (Position_compare(pos,end) <= 0));
+            }
+        }
+        else if (granularity == "document") {
+        }
+        throw new Error("unsupported granularity: "+granularity);
+    }
+
+    Input_toWordBoundary = function(pos,direction)
+    {
+        pos = Text_closestPosInDirection(pos,direction);
+        if (pos == null)
+            return null;
+        var paragraph = Text_analyseParagraph(pos);
+        if (paragraph == null)
+            return null;
+        var run = Paragraph_runFromNode(paragraph,pos.node);
+        var offset = pos.offset + run.start;
+
+        if (isForward(direction)) {
+            var remaining = paragraph.text.substring(offset);
+            var afterWord = remaining.replace(wordAtStartRE,"");
+            var afterNonWord = remaining.replace(nonWordAtStartRE,"");
+
+            if (remaining.length == 0) {
+                return pos;
+            }
+            else if (afterWord.length < remaining.length) {
+                var newOffset = offset + (remaining.length - afterWord.length);
+                return Paragraph_positionAtOffset(paragraph,newOffset);
+            }
+            else {
+                var newOffset = offset + (remaining.length - afterNonWord.length);
+                return Paragraph_positionAtOffset(paragraph,newOffset);
+            }
+        }
+        else {
+            var remaining = paragraph.text.substring(0,offset);
+            var beforeWord = remaining.replace(wordAtEndRE,"");
+            var beforeNonWord = remaining.replace(nonWordAtEndRE,"");
+
+            if (remaining.length == 0) {
+                return pos;
+            }
+            else if (beforeWord.length < remaining.length) {
+                var newOffset = offset - (remaining.length - beforeWord.length);
+                return Paragraph_positionAtOffset(paragraph,newOffset);
+            }
+            else {
+                var newOffset = offset - (remaining.length - beforeNonWord.length);
+                return Paragraph_positionAtOffset(paragraph,newOffset);
+            }
+        }
+    }
+
+    Input_toParagraphBoundary = function(pos,direction)
+    {
+        if (isForward(direction)) {
+            var end = Text_toEndOfBoundary(pos,"paragraph");
+            if (Position_equal(pos,end)) {
+                end = Position_nextMatch(end,Position_okForMovement);
+                end = Text_toEndOfBoundary(end,"paragraph");
+                end = Text_toStartOfBoundary(end,"paragraph");
+            }
+            return end ? end : pos;
+        }
+        else {
+            var start = Text_toStartOfBoundary(pos,"paragraph");
+            if (Position_equal(pos,start)) {
+                start = Position_prevMatch(start,Position_okForMovement);
+                start = Text_toStartOfBoundary(start,"paragraph");
+                start = Text_toEndOfBoundary(start,"paragraph");
+            }
+            return start ? start : pos;
+        }
+    }
+
+    Input_toLineBoundary = function(pos,direction)
+    {
+        if (isForward(direction)) {
+            var end = Text_toEndOfBoundary(pos,"line");
+            return end ? end : pos;
+        }
+        else {
+            var start = Text_toStartOfBoundary(pos,"line");
+            return start ? start : pos;
+        }
+    }
+
+    Input_positionFromPositionToBoundaryInDirection = function(posId,granularity,direction)
+    {
+        //idebug("Input_positionFromPositionToBoundaryInDirection("+
+        //       posId+","+granularity+","+direction+")");
+        var pos = getPosition(posId);
+        if (pos == null)
+            return null;
+
+        // FIXME: Temporary hack to avoid exceptions when running under iOS 8
+        if (granularity == "sentence")
+            granularity = "paragraph";
+
+        if (granularity == "word")
+            return addPosition(Input_toWordBoundary(pos,direction));
+        else if (granularity == "paragraph")
+            return addPosition(Input_toParagraphBoundary(pos,direction));
+        else if (granularity == "line")
+            return addPosition(Input_toLineBoundary(pos,direction));
+        else if (granularity == "character")
+            return Input_positionFromPositionInDirectionOffset(posId,direction,1);
+        else if (granularity == "document")
+            return isForward(direction) ? BaseIdDocumentEnd : BaseIdDocumentStart;
+        else
+            throw new Error("unsupported granularity: "+granularity);
+    }
+
+    Input_rangeEnclosingPositionWithGranularityInDirection = function(posId,granularity,direction)
+    {
+        //idebug("Input_rangeEnclosingPositionWithGranularityInDirection("+
+        //       posId+","+granularity+","+direction);
+        var pos = getPosition(posId);
+        if (pos == null)
+            return null;
+
+        // FIXME: Temporary hack to avoid exceptions when running under iOS 8
+        if (granularity == "sentence")
+            granularity = "paragraph";
+
+        if (granularity == "word") {
+            pos = Text_closestPosInDirection(pos,direction);
+            if (pos == null)
+                return null;
+            var paragraph = Text_analyseParagraph(pos);
+            if (pos == null)
+                return addPosition(null);
+            if (paragraph == null)
+                return addPosition(null);
+            var run = Paragraph_runFromNode(paragraph,pos.node);
+            var offset = pos.offset + run.start;
+
+            var before = paragraph.text.substring(0,offset);
+            var after = paragraph.text.substring(offset);
+            var beforeWord = before.replace(wordAtEndRE,"");
+            var afterWord = after.replace(wordAtStartRE,"");
+
+            var ok;
+
+            if (isForward(direction))
+                ok = (afterWord.length < after.length);
+            else
+                ok = (beforeWord.length < before.length);
+
+            if (ok) {
+                var charsBefore = (before.length - beforeWord.length);
+                var charsAfter = (after.length - afterWord.length);
+                var startOffset = offset - charsBefore;
+                var endOffset = offset + charsAfter;
+
+                var startPos = Paragraph_positionAtOffset(paragraph,startOffset);
+                var endPos = Paragraph_positionAtOffset(paragraph,endOffset);
+                return { startId: addPosition(startPos),
+                         endId: addPosition(endPos) };
+            }
+            else {
+                return null;
+            }
+        }
+        else if ((granularity == "paragraph") || (granularity == "line")) {
+            var start = Text_toStartOfBoundary(pos,granularity);
+            var end = Text_toEndOfBoundary(pos,granularity);
+            start = start ? start : pos;
+            end = end ? end : pos;
+
+            if ((granularity == "paragraph") || !isForward(direction)) {
+                if (isForward(direction)) {
+                    if (Position_equal(pos,Text_toEndOfBoundary(pos,granularity)))
+                        return null;
+                }
+                else {
+                    if (Position_equal(pos,Text_toStartOfBoundary(pos,granularity)))
+                        return null;
+                }
+            }
+            return { startId: addPosition(start),
+                     endId: addPosition(end) };
+        }
+        else {
+            throw new Error("unsupported granularity: "+granularity);
+        }
+    }
+
+})();


Mime
View raw message