You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@corinthia.apache.org by ja...@apache.org on 2015/08/17 10:50:18 UTC

[26/28] incubator-corinthia git commit: included MOC compiler for Qt implementation

http://git-wip-us.apache.org/repos/asf/incubator-corinthia/blob/9bf02bb2/experiments/editorFramework/src/Javascript_Layer_0/Clipboard.js
----------------------------------------------------------------------
diff --git a/experiments/editorFramework/src/Javascript_Layer_0/Clipboard.js b/experiments/editorFramework/src/Javascript_Layer_0/Clipboard.js
new file mode 100644
index 0000000..03efdcc
--- /dev/null
+++ b/experiments/editorFramework/src/Javascript_Layer_0/Clipboard.js
@@ -0,0 +1,757 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you 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 Markdown_htmlToMarkdown;
+var Clipboard_htmlToText;
+var Clipboard_cut;
+var Clipboard_copy;
+var Clipboard_pasteText;
+var Clipboard_pasteHTML;
+var Clipboard_pasteNodes;
+
+(function() {
+
+    // private
+    function blockToText(md,node,indent,nextIndent,listType,listNo)
+    {
+        var linesBetweenChildren = 1;
+        var childIndent = indent;
+        switch (node._type) {
+        case HTML_LI:
+            if (listType == "OL") {
+                var listMarker;
+                if (listNo.value < 10)
+                    listMarker = listNo.value+".  ";
+                else
+                    listMarker = listNo.value+". ";
+                beginParagraph(md,0,indent,nextIndent,listMarker);
+                nextIndent += "    ";
+            }
+            else {
+                beginParagraph(md,0,indent,nextIndent,"  - ");
+                nextIndent += "    ";
+            }
+            listNo.value++;
+            break;
+        case HTML_UL:
+            listType = "UL";
+            listNo = { value: 1 };
+            beginParagraph(md,1,indent,nextIndent);
+            linesBetweenChildren = 0;
+            break;
+        case HTML_OL:
+            listType = "OL";
+            listNo = { value: 1 };
+            beginParagraph(md,1,indent,nextIndent);
+            linesBetweenChildren = 0;
+            break;
+        case HTML_H1:
+            beginParagraph(md,1,indent,nextIndent,"# "," #");
+            break;
+        case HTML_H2:
+            beginParagraph(md,1,indent,nextIndent,"## "," ##");
+            break;
+        case HTML_H3:
+            beginParagraph(md,1,indent,nextIndent,"### "," ###");
+            break;
+        case HTML_H4:
+            beginParagraph(md,1,indent,nextIndent,"#### "," ####");
+            break;
+        case HTML_H5:
+            beginParagraph(md,1,indent,nextIndent,"##### "," #####");
+            break;
+        case HTML_H6:
+            beginParagraph(md,1,indent,nextIndent,"###### "," ######");
+            break;
+        case HTML_BLOCKQUOTE:
+            beginParagraph(md,1,indent,nextIndent,"> ");
+            nextIndent += "> ";
+            break;
+        case HTML_PRE:
+            md.preDepth++;
+            break;
+        }
+
+        var foundNonWhitespaceChild = false;
+        for (var child = node.firstChild; child != null; child = child.nextSibling) {
+            if (isContainerNode(child) || isParagraphNode(child)) {
+                beginParagraph(md,linesBetweenChildren,indent,nextIndent);
+                blockToText(md,child,indent,nextIndent,listType,listNo);
+                beginParagraph(md,linesBetweenChildren);
+                indent = nextIndent;
+                foundNonWhitespaceChild = false;
+            }
+            else {
+                if (!foundNonWhitespaceChild) {
+                    if (isWhitespaceTextNode(child))
+                        continue;
+                    beginParagraph(md,0,indent,nextIndent);
+                    indent = nextIndent;
+                    foundNonWhitespaceChild = true;
+                }
+
+                inlineToText(md,child);
+            }
+        }
+
+        if (node._type == HTML_PRE)
+            md.preDepth--;
+    }
+
+    // private
+    function shipOutParagraph(md)
+    {
+        var text = md.buildParagraph.join("");
+        if (md.buildPre) {
+            text = text.replace(/\n$/,"");
+            text = "    "+text.replace(/\n/g,"\n"+md.nextIndent+"    ");
+        }
+        else {
+            text = normalizeWhitespace(text);
+        }
+        if (md.allText.length > 0) {
+            for (var i = 0; i < md.buildLines; i++)
+                md.allText.push("\n");
+        }
+        md.allText.push(md.indent+md.buildPrefix+text+md.buildSuffix+"\n");
+        resetBuild(md);
+    }
+
+    // private
+    function beginParagraph(md,blankLines,indent,nextIndent,paraPrefix,paraSuffix)
+    {
+        if (blankLines == null)
+            blankLines = 1;
+        if (indent == null)
+            indent = "";
+        if (nextIndent == null)
+            nextIndent = "";
+        if (paraPrefix == null)
+            paraPrefix = "";
+        if (paraSuffix == null)
+            paraSuffix = "";
+
+        if (md == null)
+            throw new Error("beginParagraph: md is null");
+        if (md.buildParagraph == null)
+            throw new Error("beginParagraph: md.buildParagraph is null");
+
+        if (md.buildParagraph.length > 0) {
+            shipOutParagraph(md);
+        }
+
+        if (md.buildLines < blankLines)
+            md.buildLines = blankLines;
+        if (md.indent.length < indent.length)
+            md.indent = indent;
+        if (md.nextIndent.length < nextIndent.length)
+            md.nextIndent = nextIndent;
+        md.buildPrefix += paraPrefix;
+        md.buildSuffix = paraSuffix + md.buildSuffix;
+        if (md.preDepth > 0)
+            md.buildPre = true;
+    }
+
+    // private
+    function inlineToText(md,node)
+    {
+        switch (node._type) {
+        case HTML_TEXT: {
+            var text = node.nodeValue;
+            if (md.preDepth == 0) {
+                text = text.replace(/\\/g,"\\\\");
+                text = text.replace(/\*/g,"\\*");
+                text = text.replace(/\[/g,"\\[");
+                text = text.replace(/\]/g,"\\]");
+            }
+            md.buildParagraph.push(text);
+            break;
+        }
+        case HTML_I:
+        case HTML_EM:
+            md.buildParagraph.push("*");
+            processChildren();
+            md.buildParagraph.push("*");
+            break;
+        case HTML_B:
+        case HTML_STRONG:
+            md.buildParagraph.push("**");
+            processChildren();
+            md.buildParagraph.push("**");
+            break;
+        case HTML_A:
+            if (node.hasAttribute("href")) {
+                md.buildParagraph.push("[");
+                processChildren();
+                md.buildParagraph.push("]("+node.getAttribute("href")+")");
+            }
+            break;
+        default:
+            processChildren();
+            break;
+        }
+
+        function processChildren()
+        {
+            for (var child = node.firstChild; child != null; child = child.nextSibling) {
+                inlineToText(md,child);
+            }
+        }
+    }
+
+    // private
+    function resetBuild(md)
+    {
+        md.buildParagraph = new Array();
+        md.buildLines = 0;
+        md.buildPrefix = "";
+        md.buildSuffix = "";
+        md.buildPre = false;
+        md.indent = "";
+        md.nextIndent = "";
+    }
+
+    // private
+    function MarkdownBuilder()
+    {
+    }
+
+    // public
+    Markdown_htmlToMarkdown = function(node)
+    {
+        var md = new MarkdownBuilder();
+        md.allText = new Array();
+        md.preDepth = 0;
+        resetBuild(md);
+
+        if (isContainerNode(node) || isParagraphNode(node)) {
+            blockToText(md,node,"","","UL",{value: 1});
+            beginParagraph(md);
+            return md.allText.join("");
+        }
+        else {
+            inlineToText(md,node);
+            return normalizeWhitespace(md.buildParagraph.join(""));
+        }
+    }
+
+})();
+
+(function() {
+
+    function expandRangeForCopy(range)
+    {
+        if (range == null)
+            return range;
+
+        var startInLI = null;
+        for (var node = range.start.node; node != null; node = node.parentNode) {
+            if (node._type == HTML_LI)
+                startInLI = node;
+        }
+
+        var endInLI = null;
+        for (var node = range.end.node; node != null; node = node.parentNode) {
+            if (node._type == HTML_LI)
+                endInLI = node;
+        }
+
+        if ((startInLI != null) && (startInLI == endInLI)) {
+            var beforeRange = new Range(startInLI,0,
+                                        range.start.node,range.start.offset);
+            var afterRange = new Range(range.end.node,range.end.offset,
+                                       endInLI,DOM_maxChildOffset(endInLI));
+            var contentBefore = Range_hasContent(beforeRange);
+            var contentAfter = Range_hasContent(afterRange);
+
+            if (!contentBefore && !contentAfter) {
+                var li = startInLI;
+                var offset = DOM_nodeOffset(li);
+                range = new Range(li.parentNode,offset,li.parentNode,offset+1);
+            }
+        }
+        return range;
+    }
+
+    function copyRange(range)
+    {
+        var html = "";
+        var text = "";
+
+        if (range != null) {
+            var nodes;
+            var region = Tables_regionFromRange(range);
+            if (region != null) {
+                nodes = [Tables_cloneRegion(region)];
+            }
+            else {
+                nodes = Range_cloneContents(range);
+            };
+
+            var div = DOM_createElement(document,"DIV");
+            for (var i = 0; i < nodes.length; i++)
+                DOM_appendChild(div,nodes[i]);
+            Main_removeSpecial(div);
+
+            html = div.innerHTML;
+            text = Clipboard_htmlToText(div);
+        }
+
+        return { "text/html": html,
+                 "text/plain": text };
+    }
+
+    // public (FIXME: temp: for testing)
+    Clipboard_htmlToText = function(node)
+    {
+        return Markdown_htmlToMarkdown(node);
+    }
+
+    // public
+    Clipboard_cut = function()
+    {
+        UndoManager_newGroup("Cut");
+        var content;
+
+        var range = Selection_get();
+        range = expandRangeForCopy(range);
+        content = copyRange(range);
+
+        Selection_set(range.start.node,range.start.offset,range.end.node,range.end.offset);
+        Selection_deleteContents(false);
+        var selRange = Selection_get();
+        if (selRange != null) {
+            Range_trackWhileExecuting(selRange,function() {
+                var node = Position_closestActualNode(selRange.start);
+                while (node != null) {
+                    var parent = node.parentNode;
+                    switch (node._type) {
+                    case HTML_LI:
+                        if (!nodeHasContent(node))
+                            DOM_deleteNode(node);
+                        break;
+                    case HTML_UL:
+                    case HTML_OL: {
+                        var haveLI = false;
+                        for (var c = node.firstChild; c != null; c = c.nextSibling) {
+                            if (c._type == HTML_LI) {
+                                haveLI = true;
+                                break;
+                            }
+                        }
+                        if (!haveLI)
+                            DOM_deleteNode(node);
+                        break;
+                    }
+                    }
+                    node = parent;
+                }
+            });
+
+            var pos = Position_closestMatchForwards(selRange.start,Position_okForMovement);
+            Selection_set(pos.node,pos.offset,pos.node,pos.offset);
+        }
+
+        Cursor_ensureCursorVisible();
+
+        PostponedActions_perform(UndoManager_newGroup);
+        return content;
+    }
+
+    // public
+    Clipboard_copy = function()
+    {
+        var range = Selection_get();
+        range = expandRangeForCopy(range);
+        return copyRange(range);
+    }
+
+    // public
+    Clipboard_pasteText = function(text)
+    {
+        var converter = new Showdown.converter();
+        var html = converter.makeHtml(text);
+        UndoManager_newGroup("Paste");
+        Clipboard_pasteHTML(html);
+        UndoManager_newGroup();
+    }
+
+    // public
+    Clipboard_pasteHTML = function(html)
+    {
+        if (html.match(/^\s*<thead/i))
+            html = "<table>" + html + "</table>";
+        else if (html.match(/^\s*<tbody/i))
+            html = "<table>" + html + "</table>";
+        else if (html.match(/^\s*<tfoot/i))
+            html = "<table>" + html + "</table>";
+        else if (html.match(/^\s*<tr/i))
+            html = "<table>" + html + "</table>";
+        else if (html.match(/^\s*<td/i))
+            html = "<table><tr>" + html + "</tr></table>";
+        else if (html.match(/^\s*<th/i))
+            html = "<table><tr>" + html + "</tr></table>";
+        else if (html.match(/^\s*<li/i))
+            html = "<ul>" + html + "</ul>";
+
+        var div = DOM_createElement(document,"DIV");
+        div.innerHTML = html;
+        for (var child = div.firstChild; child != null; child = child.nextSibling)
+            DOM_assignNodeIds(child);
+
+        var nodes = new Array();
+        for (var child = div.firstChild; child != null; child = child.nextSibling)
+            nodes.push(child);
+
+        UndoManager_newGroup("Paste");
+        var region = Tables_regionFromRange(Selection_get(),true);
+        if ((region != null) && (nodes.length == 1) && (nodes[0]._type == HTML_TABLE))
+            pasteTable(nodes[0],region);
+        else
+            Clipboard_pasteNodes(nodes);
+        UndoManager_newGroup();
+    }
+
+    function pasteTable(srcTable,dest)
+    {
+        var src = Tables_analyseStructure(srcTable);
+
+        // In the destination table, the region into which we will paste the cells will the
+        // the same size as that of the source table, regardless of how many rows and columns
+        // were selected - i.e. we only pay attention to the top-left most cell, ignoring
+        // whatever the bottom-right is set to
+        dest.bottom = dest.top + src.numRows - 1;
+        dest.right = dest.left + src.numCols - 1;
+
+        // Make sure the destination table is big enough to hold all the cells we want to paste.
+        // This will add rows and columns as appropriate, with empty cells that only contain a
+        // <p><br></p> (to ensure they have non-zero height)
+        if (dest.structure.numRows < dest.bottom + 1)
+            dest.structure.numRows = dest.bottom + 1;
+        if (dest.structure.numCols < dest.right + 1)
+            dest.structure.numCols = dest.right + 1;
+        dest.structure = Table_fix(dest.structure);
+
+        // To simplify the paste, split any merged cells that are in the region of the destination
+        // table we're pasting into. We have to re-analyse the table structure after this to
+        // get the correct cell array.
+        TableRegion_splitCells(dest);
+        dest.structure = Tables_analyseStructure(dest.structure.element);
+
+        // Do the actual paste
+        Selection_preserveWhileExecuting(function() {
+            replaceCells(src,dest.structure,dest.top,dest.left);
+        });
+
+        // If any new columns were added, calculate a width for them
+        Table_fixColumnWidths(dest.structure);
+
+        // Remove duplicate ids
+        var found = new Object();
+        removeDuplicateIds(dest.structure.element,found);
+
+        // Place the cursor in the bottom-right cell that was pasted
+        var bottomRightCell = Table_get(dest.structure,dest.bottom,dest.right);
+        var node = bottomRightCell.element;
+        Selection_set(node,node.childNodes.length,node,node.childNodes.length);
+    }
+
+    function replaceCells(src,dest,destRow,destCol)
+    {
+        // By this point, all of the cells have been split. So it is guaranteed that every cell
+        // in dest will have rowspan = 1 and colspan = 1.
+        for (var srcRow = 0; srcRow < src.numRows; srcRow++) {
+            for (var srcCol = 0; srcCol < src.numCols; srcCol++) {
+                var srcCell = Table_get(src,srcRow,srcCol);
+                var destCell = Table_get(dest,srcRow+destRow,srcCol+destCol);
+
+                if ((srcRow != srcCell.row) || (srcCol != srcCell.col))
+                    continue;
+
+                if (destCell.rowspan != 1)
+                    throw new Error("unexpected rowspan: "+destCell.rowspan);
+                if (destCell.colspan != 1)
+                    throw new Error("unexpected colspan: "+destCell.colspan);
+
+                DOM_insertBefore(destCell.element.parentNode,srcCell.element,destCell.element);
+
+                var destTop = destRow + srcRow;
+                var destLeft = destCol + srcCol;
+                var destBottom = destTop + srcCell.rowspan - 1;
+                var destRight = destLeft + srcCell.colspan - 1;
+                Table_setRegion(dest,destTop,destLeft,destBottom,destRight,srcCell);
+            }
+        }
+    }
+
+    function insertChildrenBefore(parent,child,nextSibling,pastedNodes)
+    {
+        var next;
+        for (var grandChild = child.firstChild; grandChild != null; grandChild = next) {
+            next = grandChild.nextSibling;
+            pastedNodes.push(grandChild);
+            DOM_insertBefore(parent,grandChild,nextSibling);
+        }
+    }
+
+    function fixParagraphStyles(node,paragraphClass)
+    {
+        if (isParagraphNode(node)) {
+            if (node._type == HTML_P) {
+                var className = DOM_getAttribute(node,"class");
+                if ((className == null) || (className == "")) {
+                    debug("Setting paragraph class to "+paragraphClass);
+                    DOM_setAttribute(node,"class",paragraphClass);
+                }
+            }
+        }
+        else {
+            for (var child = node.firstChild; child != null; child = child.nextSibling) {
+                fixParagraphStyles(child,paragraphClass);
+            }
+        }
+    }
+
+    // public
+    Clipboard_pasteNodes = function(nodes)
+    {
+        if (nodes.length == 0)
+            return;
+
+        var paragraphClass = Styles_getParagraphClass();
+        if (paragraphClass != null) {
+            for (var i = 0; i < nodes.length; i++) {
+                fixParagraphStyles(nodes[i],paragraphClass);
+            }
+        }
+
+        // Remove any elements which don't belong in the document body (in case an entire
+        // HTML document is being pasted in)
+        var i = 0;
+        while (i < nodes.length) {
+            switch (nodes[i]._type) {
+            case HTML_HTML:
+            case HTML_BODY:
+            case HTML_META:
+            case HTML_TITLE:
+            case HTML_SCRIPT:
+            case HTML_STYLE:
+                nodes.splice(i,1);
+                break;
+            default:
+                i++;
+            }
+        }
+
+        var found = new Object();
+        for (var i = 0; i < nodes.length; i++)
+            removeDuplicateIds(nodes[i],found);
+
+//        if ((nodes.length == 0) && (nodes[0]._type == HTML_TABLE)) {
+//            // FIXME: this won't work; selectionRange is not defined
+//            var fromRegion = Tables_getTableRegionFromTable(nodes[0]);
+//            var toRegion = Tables_regionFromRange(selectionRange);
+//            if (toRegion != null) {
+//                return;
+//            }
+//        }
+
+        Selection_deleteContents(true);
+        var range = Selection_get();
+        if (range == null)
+            throw new Error("No current selection");
+
+        var parent;
+        var previousSibling;
+        var nextSibling;
+
+        var start = range.start;
+        start = Position_preferElementPosition(start);
+        if (start.node.nodeType == Node.ELEMENT_NODE) {
+            parent = start.node;
+            nextSibling = start.node.childNodes[start.offset];
+            previousSibling = start.node.childNodes[start.offset-1];
+        }
+        else {
+            Formatting_splitTextAfter(start);
+            parent = start.node.parentNode;
+            nextSibling = start.node.nextSibling;
+            previousSibling = start.node;
+        }
+
+        var prevLI = null;
+        var inItem = null;
+        var inList = null;
+        var containerParent = null;
+
+        for (var temp = parent; temp != null; temp = temp.parentNode) {
+            if (isContainerNode(temp)) {
+                switch (temp._type) {
+                case HTML_LI:
+                    inItem = temp;
+                    break;
+                case HTML_UL:
+                case HTML_OL:
+                    inList = temp;
+                    break;
+                }
+                containerParent = temp.parentNode;
+                break;
+            }
+        }
+
+        var pastedNodes;
+        if (inItem) {
+            pastedNodes = new Array();
+            for (var i = 0; i < nodes.length; i++) {
+                var child = nodes[i];
+
+                var offset = DOM_nodeOffset(nextSibling,parent);
+
+                switch (child._type) {
+                case HTML_UL:
+                case HTML_OL:
+                    Formatting_movePreceding(new Position(parent,offset),
+                                             function(x) { return (x == containerParent); });
+                    insertChildrenBefore(inItem.parentNode,child,inItem,pastedNodes);
+                    break;
+                case HTML_LI:
+                    Formatting_movePreceding(new Position(parent,offset),
+                                             function(x) { return (x == containerParent); });
+                    DOM_insertBefore(inItem.parentNode,child,inItem);
+                    pastedNodes.push(child);
+                    break;
+                default:
+                    DOM_insertBefore(parent,child,nextSibling);
+                    pastedNodes.push(child);
+                    break;
+                }
+            }
+        }
+        else if (inList) {
+            pastedNodes = new Array();
+            for (var i = 0; i < nodes.length; i++) {
+                var child = nodes[i];
+
+                var offset = DOM_nodeOffset(nextSibling,parent);
+
+                switch (child._type) {
+                case HTML_UL:
+                case HTML_OL:
+                    insertChildrenBefore(parent,child,nextSibling,pastedNodes);
+                    prevLI = null;
+                    break;
+                case HTML_LI:
+                    DOM_insertBefore(parent,child,nextSibling);
+                    pastedNodes.push(child);
+                    prevLI = null;
+                    break;
+                default:
+                    if (!isWhitespaceTextNode(child)) {
+                        if (prevLI == null)
+                            prevLI = DOM_createElement(document,"LI");
+                        DOM_appendChild(prevLI,child);
+                        DOM_insertBefore(parent,prevLI,nextSibling);
+                        pastedNodes.push(child);
+                    }
+                }
+            }
+        }
+        else {
+            pastedNodes = nodes;
+            for (var i = 0; i < nodes.length; i++) {
+                var child = nodes[i];
+                DOM_insertBefore(parent,child,nextSibling);
+            }
+        }
+
+        var prevOffset;
+        if (previousSibling == null)
+            prevOffset = 0;
+        else
+            prevOffset = DOM_nodeOffset(previousSibling);
+        var nextOffset = DOM_nodeOffset(nextSibling,parent);
+
+        var origRange = new Range(parent,prevOffset,parent,nextOffset);
+
+        var firstPasted = pastedNodes[0];
+        var lastPasted = pastedNodes[pastedNodes.length-1];
+        var pastedRange = new Range(firstPasted,0,lastPasted,DOM_maxChildOffset(lastPasted));
+        Range_trackWhileExecuting(origRange,function() {
+        Range_trackWhileExecuting(pastedRange,function() {
+            if (previousSibling != null)
+                Formatting_mergeWithNeighbours(previousSibling,Formatting_MERGEABLE_INLINE);
+            if (nextSibling != null)
+                Formatting_mergeWithNeighbours(nextSibling,Formatting_MERGEABLE_INLINE);
+
+            Cursor_updateBRAtEndOfParagraph(parent);
+
+            Range_ensureValidHierarchy(pastedRange,true);
+        })});
+
+        var pos = new Position(origRange.end.node,origRange.end.offset);
+        Range_trackWhileExecuting(pastedRange,function() {
+        Position_trackWhileExecuting(pos,function() {
+            while (true) {
+                if (pos.node == document.body)
+                    break;
+                if (isContainerNode(pos.node) && (pos.node._type != HTML_LI))
+                    break;
+                if (!nodeHasContent(pos.node)) {
+                    var oldNode = pos.node;
+                    pos = new Position(pos.node.parentNode,DOM_nodeOffset(pos.node));
+                    DOM_deleteNode(oldNode);
+                }
+                else
+                    break;
+            }
+        });
+        });
+
+        pos = new Position(pastedRange.end.node,pastedRange.end.offset);
+        while (isOpaqueNode(pos.node))
+            pos = new Position(pos.node.parentNode,DOM_nodeOffset(pos.node)+1);
+        pos = Position_closestMatchBackwards(pos,Position_okForInsertion);
+
+        Selection_set(pos.node,pos.offset,pos.node,pos.offset);
+        Cursor_ensureCursorVisible();
+    }
+
+    function removeDuplicateIds(node,found)
+    {
+        if ((node.nodeType == Node.ELEMENT_NODE) && node.hasAttribute("id")) {
+            var id = node.getAttribute("id");
+
+            var existing = document.getElementById(id);
+            if (existing == null)
+                existing = found[id];
+
+            if ((existing != null) && (existing != node))
+                DOM_removeAttribute(node,"id");
+            else
+                found[id] = node;
+        }
+        for (var child = node.firstChild; child != null; child = child.nextSibling)
+            removeDuplicateIds(child,found);
+    }
+
+    function pasteImage(href)
+    {
+        // FIXME
+    }
+
+})();

http://git-wip-us.apache.org/repos/asf/incubator-corinthia/blob/9bf02bb2/experiments/editorFramework/src/Javascript_Layer_0/Cursor.js
----------------------------------------------------------------------
diff --git a/experiments/editorFramework/src/Javascript_Layer_0/Cursor.js b/experiments/editorFramework/src/Javascript_Layer_0/Cursor.js
new file mode 100644
index 0000000..7362a9e
--- /dev/null
+++ b/experiments/editorFramework/src/Javascript_Layer_0/Cursor.js
@@ -0,0 +1,1050 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you 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 Cursor_ensurePositionVisible;
+var Cursor_ensureCursorVisible;
+var Cursor_scrollDocumentForY;
+var Cursor_positionCursor;
+var Cursor_getCursorPosition;
+var Cursor_moveLeft;
+var Cursor_moveRight;
+var Cursor_moveToStartOfDocument;
+var Cursor_moveToEndOfDocument;
+var Cursor_updateBRAtEndOfParagraph;
+var Cursor_insertReference;
+var Cursor_insertLink;
+var Cursor_insertCharacter;
+var Cursor_deleteCharacter;
+var Cursor_enterPressed;
+var Cursor_getPrecedingWord;
+var Cursor_getAdjacentNodeWithType;
+var Cursor_getLinkProperties;
+var Cursor_setLinkProperties;
+var Cursor_setReferenceTarget;
+var Cursor_makeContainerInsertionPoint;
+var Cursor_set;
+var Cursor_insertFootnote;
+var Cursor_insertEndnote;
+
+(function() {
+
+    var cursorX = null;
+
+    Cursor_ensurePositionVisible = function(pos,center)
+    {
+        // If we can't find the cursor rect for some reason, just don't do anything.
+        // This is better than using an incorrect position or throwing an exception.
+        var rect = Position_displayRectAtPos(pos)
+        if (rect != null) {
+            var extraSpace = 4;
+
+            var cursorTop = rect.top + window.scrollY - extraSpace;
+            var cursorBottom = rect.top + rect.height + window.scrollY + extraSpace;
+
+            var windowTop = window.scrollY;
+            var windowBottom = window.scrollY + window.innerHeight;
+
+            if (center) {
+                var newY = Math.floor(cursorTop + rect.height/2 - window.innerHeight/2);
+                window.scrollTo(window.scrollX,newY);
+            }
+            else if (cursorTop < windowTop) {
+                window.scrollTo(window.scrollX,cursorTop);
+            }
+            else if (cursorBottom > windowBottom) {
+                window.scrollTo(window.scrollX,cursorBottom - window.innerHeight);
+            }
+        }
+    }
+
+    // public
+    Cursor_ensureCursorVisible = function(center)
+    {
+        var selRange = Selection_get();
+        if (selRange != null)
+            Cursor_ensurePositionVisible(selRange.end,center);
+    }
+
+    Cursor_scrollDocumentForY = function(y)
+    {
+        var absY = window.scrollY + y;
+        if (absY-44 < window.scrollY) {
+            window.scrollTo(window.scrollX,absY-44);
+            y = absY - window.scrollY;
+        }
+        else if (absY+44 >= window.scrollY + window.innerHeight) {
+            window.scrollTo(window.scrollX,absY+44 - window.innerHeight);
+            y = absY - window.scrollY;
+        }
+        return y;
+    }
+
+    // public
+    Cursor_positionCursor = function(x,y,wordBoundary)
+    {
+        if (UndoManager_groupType() != "Cursor movement")
+            UndoManager_newGroup("Cursor movement");
+
+        y = Cursor_scrollDocumentForY(y);
+
+        var result = null;
+        var position = Position_atPoint(x,y);
+        if (position == null)
+            return null;
+
+        var node = Position_closestActualNode(position);
+        for (; node != null; node = node.parentNode) {
+            var type = node._type;
+            if ((type == HTML_A) &&
+                (node.hasAttribute("href")) &&
+                (result == null)) {
+
+                var arange = new Range(node,0,node,node.childNodes.length);
+                var rects = Range_getClientRects(arange);
+                var insideLink = false;
+                for (var i = 0; i < rects.length; i++) {
+                    if (rectContainsPoint(rects[i],x,y))
+                        insideLink = true;
+                }
+
+                if (insideLink) {
+                    var href = node.getAttribute("href");
+                    if ((href != null) && (href.charAt(0) == "#")) {
+                        if (isInTOC(node))
+                            result = "intocreference-"+href.substring(1);
+                        else
+                            result = "inreference";
+                    }
+                    else {
+                        result = "inlink";
+                    }
+                }
+            }
+            else if ((type == HTML_IMG) && (result == null)) {
+                for (var anc = node; anc != null; anc = anc.parentNode) {
+                    if (anc._type == HTML_FIGURE) {
+                        result = "infigure";
+                        break;
+                    }
+                }
+            }
+            else if (isAutoCorrectNode(node) && (result == null)) {
+                result = "incorrection";
+            }
+            else if (isTOCNode(node)) {
+                var rect = node.getBoundingClientRect();
+                if (x >= rect.left + rect.width/2)
+                    position = new Position(node.parentNode,DOM_nodeOffset(node)+1);
+                else
+                    position = new Position(node.parentNode,DOM_nodeOffset(node));
+                break;
+            }
+        }
+
+        var position = Position_closestMatchForwards(position,Position_okForMovement);
+        if ((position != null) && isOpaqueNode(position.node))
+            position = Position_nextMatch(position,Position_okForMovement);
+        if (position == null)
+            return false;
+
+        var selectionRange = Selection_get();
+        var samePosition = ((selectionRange != null) && Range_isEmpty(selectionRange) &&
+                            (position.node == selectionRange.start.node) &&
+                            (position.offset == selectionRange.start.offset));
+        if (samePosition && (result == null))
+            result = "same";
+
+        if (wordBoundary) {
+            var startOfWord = Selection_posAtStartOfWord(position);
+            var endOfWord = Selection_posAtEndOfWord(position);
+            if ((startOfWord.node != position.node) || (startOfWord.node != position.node))
+                throw new Error("Word boundary in different node");
+            var distanceBefore = position.offset - startOfWord.offset;
+            var distanceAfter = endOfWord.offset - position.offset;
+            if (distanceBefore <= distanceAfter)
+                position = startOfWord;
+            else
+                position = endOfWord;
+        }
+
+        Cursor_set(position.node,position.offset);
+        return result;
+    }
+
+    // public
+    Cursor_getCursorPosition = function()
+    {
+        var selRange = Selection_get();
+        if (selRange == null)
+            return null;
+
+        // FIXME: in the cases where this is called from Objective C, test what happens if we
+        // return a null rect
+        var rect = Position_displayRectAtPos(selRange.end);
+        if (rect == null)
+            return null;
+
+        var left = rect.left + window.scrollX;
+        var top = rect.top + window.scrollY;
+        var height = rect.height;
+        return { x: left, y: top, width: 0, height: height };
+    }
+
+    // public
+    Cursor_moveLeft = function()
+    {
+        var range = Selection_get();
+        if (range == null)
+            return;
+
+        var pos = Position_prevMatch(range.start,Position_okForMovement);
+        if (pos != null)
+            Cursor_set(pos.node,pos.offset);
+        Cursor_ensureCursorVisible();
+    }
+
+    // public
+    Cursor_moveRight = function()
+    {
+        var range = Selection_get();
+        if (range == null)
+            return;
+
+        var pos = Position_nextMatch(range.start,Position_okForMovement);
+        if (pos != null)
+            Cursor_set(pos.node,pos.offset);
+        Cursor_ensureCursorVisible();
+    }
+
+    // public
+    Cursor_moveToStartOfDocument = function()
+    {
+        var pos = new Position(document.body,0);
+        pos = Position_closestMatchBackwards(pos,Position_okForMovement);
+        Cursor_set(pos.node,pos.offset);
+        Cursor_ensureCursorVisible();
+    }
+
+    // public
+    Cursor_moveToEndOfDocument = function()
+    {
+        var pos = new Position(document.body,document.body.childNodes.length);
+        pos = Position_closestMatchForwards(pos,Position_okForMovement);
+        Cursor_set(pos.node,pos.offset);
+        Cursor_ensureCursorVisible();
+    }
+
+    // An empty paragraph does not get shown and cannot be edited. We can fix this by adding
+    // a BR element as a child
+    // public
+    Cursor_updateBRAtEndOfParagraph = function(node)
+    {
+        var paragraph = node;
+        while ((paragraph != null) && !isParagraphNode(paragraph))
+            paragraph = paragraph.parentNode;
+        if (paragraph != null) {
+
+            var br = null;
+            var last = paragraph;
+            do {
+
+                var child = last;
+                while ((child != null) && isWhitespaceTextNode(child))
+                    child = child.previousSibling;
+
+                if ((child != null) && (child._type == HTML_BR))
+                    br = child;
+
+                last = last.lastChild;
+
+            } while ((last != null) && isInlineNode(last));
+
+            if (nodeHasContent(paragraph)) {
+                // Paragraph has content: don't want BR at end
+                if (br != null) {
+                    DOM_deleteNode(br);
+                }
+            }
+            else {
+                // Paragraph consists only of whitespace: must have BR at end
+                if (br == null) {
+                    br = DOM_createElement(document,"BR");
+                    DOM_appendChild(paragraph,br);
+                }
+            }
+        }
+    }
+
+    // public
+    Cursor_insertReference = function(itemId)
+    {
+        var a = DOM_createElement(document,"A");
+        DOM_setAttribute(a,"href","#"+itemId);
+        Clipboard_pasteNodes([a]);
+    }
+
+    // public
+    Cursor_insertLink = function(text,url)
+    {
+        var a = DOM_createElement(document,"A");
+        DOM_setAttribute(a,"href",url);
+        DOM_appendChild(a,DOM_createTextNode(document,text));
+        Clipboard_pasteNodes([a]);
+    }
+
+    var nbsp = String.fromCharCode(160);
+
+    function spaceToNbsp(pos)
+    {
+        var node = pos.node;
+        var offset = pos.offset;
+
+        if ((node.nodeType == Node.TEXT_NODE) && (offset > 0) &&
+            (isWhitespaceString(node.nodeValue.charAt(offset-1)))) {
+            // Insert first, to preserve any tracked positions
+            DOM_insertCharacters(node,offset-1,nbsp);
+            DOM_deleteCharacters(node,offset,offset+1);
+        }
+    }
+
+    function nbspToSpace(pos)
+    {
+        var node = pos.node;
+        var offset = pos.offset;
+
+        if ((node.nodeType == Node.TEXT_NODE) && (offset > 0) &&
+            (node.nodeValue.charAt(offset-1) == nbsp)) {
+            // Insert first, to preserve any tracked positions
+            DOM_insertCharacters(node,offset-1," ");
+            DOM_deleteCharacters(node,offset,offset+1);
+        }
+    }
+
+    function checkNbsp()
+    {
+        Selection_preserveWhileExecuting(function() {
+            var selRange = Selection_get();
+            if (selRange != null)
+                nbspToSpace(selRange.end);
+        });
+    }
+
+    function isPosAtStartOfParagraph(pos)
+    {
+        if ((pos.node.nodeType == Node.ELEMENT_NODE) && (pos.offset == 0) &&
+            !isInlineNode(pos.node)) {
+            return true;
+        }
+
+
+
+        while (pos != null) {
+            if (pos.node.nodeType == Node.ELEMENT_NODE) {
+                if ((pos.offset == 0) && !isInlineNode(pos.node))
+                    return true;
+                else
+                    pos = Position_prev(pos);
+            }
+            else if (pos.node.nodeType == Node.TEXT_NODE) {
+                if (pos.offset > 0)
+                    return false;
+                else
+                    pos = Position_prev(pos);
+            }
+            else {
+                return false;
+            }
+        }
+
+        return false;
+    }
+
+    // public
+    Cursor_insertCharacter = function(str,allowInvalidPos,allowNoParagraph)
+    {
+        var firstInsertion = (UndoManager_groupType() != "Insert text");
+
+        if (firstInsertion)
+            UndoManager_newGroup("Insert text",checkNbsp);
+
+        if (str == "-") {
+            var preceding = Cursor_getPrecedingWord();
+            if (preceding.match(/[0-9]\s*$/))
+                str = String.fromCharCode(0x2013); // en dash
+            else if (preceding.match(/\s+$/))
+                str = String.fromCharCode(0x2014); // em dash
+        }
+
+        var selRange = Selection_get();
+        if (selRange == null)
+            return;
+
+        if (!Range_isEmpty(selRange)) {
+            Selection_deleteContents(true);
+            selRange = Selection_get();
+        }
+        var pos = selRange.start;
+        pos = Position_preferTextPosition(pos);
+        if ((str == " ") && isPosAtStartOfParagraph(pos))
+            return;
+        if (!allowInvalidPos && !Position_okForInsertion(pos)) {
+            var elemPos = Position_preferElementPosition(pos);
+            if (Position_okForInsertion(elemPos)) {
+                pos = elemPos;
+            }
+            else {
+                var oldPos = pos;
+                pos = Position_closestMatchForwards(selRange.start,Position_okForInsertion);
+                var difference = new Range(oldPos.node,oldPos.offset,pos.node,pos.offset);
+                difference = Range_forwards(difference);
+                Position_trackWhileExecuting([pos],function() {
+                    if (!Range_hasContent(difference)) {
+                        Selection_deleteRangeContents(difference,true);
+                    }
+                });
+            }
+        }
+        var node = pos.node;
+        var offset = pos.offset;
+
+        if ((str == " ") &&
+            !firstInsertion &&
+            (node.nodeType == Node.TEXT_NODE) &&
+            (offset > 0) &&
+            (node.nodeValue.charAt(offset-1) == nbsp)) {
+
+            if (!node.nodeValue.substring(0,offset).match(/\.\s+$/)) {
+                DOM_deleteCharacters(node,offset-1,offset);
+                DOM_insertCharacters(node,offset-1,".");
+            }
+        }
+
+        if (isWhitespaceString(str) && (node.nodeType == Node.TEXT_NODE) && (offset > 0)) {
+            var prevChar = node.nodeValue.charAt(offset-1);
+            if (isWhitespaceString(prevChar) || (prevChar == nbsp)) {
+                Selection_update();
+                Cursor_ensureCursorVisible();
+                return;
+            }
+        }
+
+        nbspToSpace(pos);
+
+        // If the user enters two double quotes in succession (open and close), replace them with
+        // just one plain double quote character
+        if ((str == "”") && (node.nodeType == Node.TEXT_NODE) &&
+            (offset > 0) && (node.nodeValue.charAt(offset-1) == "“")) {
+            DOM_deleteCharacters(node,offset-1,offset);
+            offset--;
+            str = "\"";
+        }
+
+        if (node.nodeType == Node.ELEMENT_NODE) {
+            var emptyTextNode = DOM_createTextNode(document,"");
+            if (offset >= node.childNodes.length)
+                DOM_appendChild(node,emptyTextNode);
+            else
+                DOM_insertBefore(node,emptyTextNode,node.childNodes[offset]);
+            node = emptyTextNode;
+            offset = 0;
+        }
+
+        if (str == " ")
+            DOM_insertCharacters(node,offset,nbsp);
+        else
+            DOM_insertCharacters(node,offset,str);
+
+                // must be done *after* inserting the text
+        if (!allowNoParagraph) {
+            switch (node.parentNode._type) {
+            case HTML_CAPTION:
+            case HTML_FIGCAPTION:
+                // Do nothing
+                break;
+            default:
+                Hierarchy_ensureInlineNodesInParagraph(node,true);
+                break;
+            }
+        }
+
+        offset += str.length;
+
+        pos = new Position(node,offset);
+        Position_trackWhileExecuting([pos],function() {
+            Formatting_mergeWithNeighbours(pos.node,Formatting_MERGEABLE_INLINE);
+        });
+
+        Cursor_set(pos.node,pos.offset);
+        Range_trackWhileExecuting(Selection_get(),function() {
+            Cursor_updateBRAtEndOfParagraph(pos.node);
+        });
+
+        Selection_update();
+        Cursor_ensureCursorVisible();
+    }
+
+    function tryDeleteEmptyCaption(pos)
+    {
+        var caption = Position_captionAncestor(pos);
+        if ((caption == null) || nodeHasContent(caption))
+            return false;
+
+        var container = Position_figureOrTableAncestor(pos);
+        if (container == null)
+            return false;
+
+        Cursor_set(container.parentNode,DOM_nodeOffset(container)+1);
+        Selection_preserveWhileExecuting(function() {
+            DOM_deleteNode(caption);
+        });
+
+        return true;
+    }
+
+    function tryDeleteEmptyNote(pos)
+    {
+        var note = Position_noteAncestor(pos);
+        if ((note == null) || nodeHasContent(note))
+            return false;
+
+        var parent = note.parentNode;
+        Cursor_set(note.parentNode,DOM_nodeOffset(note)+1);
+        Selection_preserveWhileExecuting(function() {
+            DOM_deleteNode(note);
+        });
+
+        return true;
+    }
+
+    // public
+    Cursor_deleteCharacter = function()
+    {
+        if (UndoManager_groupType() != "Delete text")
+            UndoManager_newGroup("Delete text",checkNbsp);
+
+        Selection_preferElementPositions();
+        var selRange = Selection_get();
+        if (selRange == null)
+            return;
+
+        if (!Range_isEmpty(selRange)) {
+            Selection_deleteContents(true);
+        }
+        else {
+            var currentPos = selRange.start;
+
+            // Special cases of pressing backspace after a table, figure, TOC, hyperlink,
+            // footnote, or endnote. For each of these we delete the whole thing.
+            var back = Position_closestMatchBackwards(currentPos,Position_okForMovement);
+            if ((back != null) && (back.node.nodeType == Node.ELEMENT_NODE) && (back.offset > 0)) {
+                var prevNode = back.node.childNodes[back.offset-1];
+                if (isSpecialBlockNode(prevNode)) {
+                    var p = DOM_createElement(document,"P");
+                    DOM_insertBefore(prevNode.parentNode,p,prevNode);
+                    DOM_deleteNode(prevNode);
+                    Cursor_updateBRAtEndOfParagraph(p);
+                    Cursor_set(p,0);
+                    Cursor_ensureCursorVisible();
+                    return;
+                }
+                if ((prevNode._type == HTML_A) || isNoteNode(prevNode)) {
+                    Cursor_set(back.node,back.offset-1);
+                    Selection_preserveWhileExecuting(function() {
+                        DOM_deleteNode(prevNode);
+                    });
+                    return;
+                }
+            }
+
+            // Backspace inside an empty figure or table caption
+            if (tryDeleteEmptyCaption(currentPos))
+                return;
+
+            currentPos = Position_preferTextPosition(currentPos);
+            var prevPos = Position_prevMatch(currentPos,Position_okForMovement);
+
+            // Backspace inside or just after a footnote or endnote
+            if (tryDeleteEmptyNote(currentPos))
+                return;
+            if ((prevPos != null) && tryDeleteEmptyNote(prevPos))
+                return;
+
+            if (prevPos != null) {
+                var startBlock = firstBlockAncestor(Position_closestActualNode(prevPos));
+                var endBlock = firstBlockAncestor(Position_closestActualNode(selRange.end));
+                if ((startBlock != endBlock) &&
+                    isParagraphNode(startBlock) && !nodeHasContent(startBlock)) {
+                    DOM_deleteNode(startBlock);
+                    Cursor_set(selRange.end.node,selRange.end.offset)
+                }
+                else {
+                    var range = new Range(prevPos.node,prevPos.offset,
+                                          selRange.end.node,selRange.end.offset);
+                    Selection_deleteRangeContents(range,true);
+                }
+            }
+        }
+
+        selRange = Selection_get();
+        if (selRange != null)
+            spaceToNbsp(selRange.end);
+        Selection_update();
+        Cursor_ensureCursorVisible();
+
+        function firstBlockAncestor(node)
+        {
+            while (isInlineNode(node))
+                node = node.parentNode;
+            return node;
+        }
+    }
+
+    // public
+    Cursor_enterPressed = function()
+    {
+        UndoManager_newGroup("New paragraph");
+
+        Selection_preferElementPositions();
+        var selRange = Selection_get();
+        if (selRange == null)
+            return;
+
+        Range_trackWhileExecuting(selRange,function() {
+            if (!Range_isEmpty(selRange))
+                Selection_deleteContents(true);
+        });
+
+        // Are we inside a figure or table caption? If so, put an empty paragraph directly after it
+        var inCaption = false;
+        var inFigCaption = false;
+        var closestNode = Position_closestActualNode(selRange.start);
+        for (var ancestor = closestNode; ancestor != null; ancestor = ancestor.parentNode) {
+            switch (ancestor._type) {
+            case HTML_CAPTION:
+                inCaption = true;
+                break;
+            case HTML_FIGCAPTION:
+                inFigCaption = true;
+                break;
+            case HTML_TABLE:
+            case HTML_FIGURE:
+                if ((inCaption && (ancestor._type == HTML_TABLE)) ||
+                    (inFigCaption && (ancestor._type == HTML_FIGURE))) {
+                    var p = DOM_createElement(document,"P");
+                    DOM_insertBefore(ancestor.parentNode,p,ancestor.nextSibling);
+                    Cursor_updateBRAtEndOfParagraph(p);
+                    Selection_set(p,0,p,0);
+                    return;
+                }
+                break;
+            }
+        }
+
+        // Are we inside a footnote or endnote? If so, move the cursor immediately after it
+        var note = null;
+        if (selRange.start.node.nodeType == Node.TEXT_NODE) {
+            note = Position_noteAncestor(selRange.start);
+        }
+        else {
+            // We can't use Position_noteAncestor in this case, because we want to to break
+            // the paragraph *before* the note, not after
+            var checkNode = selRange.start.node;
+            for (var anc = checkNode; anc != null; anc = anc.parentNode) {
+                if (isNoteNode(anc)) {
+                    note = anc;
+                    break;
+                }
+            }
+        }
+        if (note != null) {
+            var noteOffset = DOM_nodeOffset(note);
+            selRange = new Range(note.parentNode,noteOffset+1,note.parentNode,noteOffset+1);
+        }
+
+        var check = Position_preferElementPosition(selRange.start);
+        if (check.node.nodeType == Node.ELEMENT_NODE) {
+            var before = check.node.childNodes[check.offset-1];
+            var after = check.node.childNodes[check.offset];
+            if (((before != null) && isSpecialBlockNode(before)) ||
+                ((after != null) && isSpecialBlockNode(after))) {
+                var p = DOM_createElement(document,"P");
+                DOM_insertBefore(check.node,p,check.node.childNodes[check.offset]);
+                Cursor_updateBRAtEndOfParagraph(p);
+                Cursor_set(p,0);
+                Cursor_ensureCursorVisible();
+                return;
+            }
+        }
+
+        Range_trackWhileExecuting(selRange,function() {
+            Range_ensureInlineNodesInParagraph(selRange);
+            Range_ensureValidHierarchy(selRange);
+        });
+
+        var pos = selRange.start;
+
+        var detail = Range_detail(selRange);
+        switch (detail.startParent._type) {
+        case HTML_OL:
+        case HTML_UL: {
+            var li = DOM_createElement(document,"LI");
+            DOM_insertBefore(detail.startParent,li,detail.startChild);
+
+            Cursor_set(li,0);
+            Cursor_ensureCursorVisible();
+            return;
+        }
+        }
+
+        if (isAutoCorrectNode(pos.node)) {
+            pos = Position_preferTextPosition(pos);
+            selRange.start = selRange.end = pos;
+        }
+
+        Range_trackWhileExecuting(selRange,function() {
+
+            // If we're directly in a container node, add a paragraph, so we have something to
+            // split.
+            if (isContainerNode(pos.node) && (pos.node._type != HTML_LI)) {
+                var p = DOM_createElement(document,"P");
+                DOM_insertBefore(pos.node,p,pos.node.childNodes[pos.offset]);
+                pos = new Position(p,0);
+            }
+
+            var blockToSplit = getBlockToSplit(pos);
+            var stopAt = blockToSplit.parentNode;
+
+            if (positionAtStartOfHeading(pos)) {
+                var container = getContainerOrParagraph(pos.node);
+                pos = new Position(container,0);
+                pos = Formatting_movePreceding(pos,function(n) { return (n == stopAt); },true);
+            }
+            else if (pos.node.nodeType == Node.TEXT_NODE) {
+                pos = Formatting_splitTextAfter(pos,function(n) { return (n == stopAt); },true);
+            }
+            else {
+                pos = Formatting_moveFollowing(pos,function(n) { return (n == stopAt); },true);
+            }
+        });
+
+        Cursor_set(pos.node,pos.offset);
+        selRange = Selection_get();
+
+        Range_trackWhileExecuting(selRange,function() {
+            if ((pos.node.nodeType == Node.TEXT_NODE) && (pos.node.nodeValue.length == 0)) {
+                DOM_deleteNode(pos.node);
+            }
+
+            var detail = Range_detail(selRange);
+
+            // If a preceding paragraph has become empty as a result of enter being pressed
+            // while the cursor was in it, then update the BR at the end of the paragraph
+            var start = detail.startChild ? detail.startChild : detail.startParent;
+            for (var ancestor = start; ancestor != null; ancestor = ancestor.parentNode) {
+                var prev = ancestor.previousSibling;
+                if ((prev != null) && isParagraphNode(prev) && !nodeHasContent(prev)) {
+                    DOM_deleteAllChildren(prev);
+                    Cursor_updateBRAtEndOfParagraph(prev);
+                    break;
+                }
+                else if ((prev != null) && (prev._type == HTML_LI) && !nodeHasContent(prev)) {
+                    var next;
+                    for (var child = prev.firstChild; child != null; child = next) {
+                        next = child.nextSibling;
+                        if (isWhitespaceTextNode(child))
+                            DOM_deleteNode(child);
+                        else
+                            Cursor_updateBRAtEndOfParagraph(child);
+                    }
+                    break;
+                }
+            }
+
+            for (var ancestor = start; ancestor != null; ancestor = ancestor.parentNode) {
+
+                if (isParagraphNode(ancestor)) {
+                    var nextSelector = Styles_nextSelectorAfter(ancestor);
+                    if (nextSelector != null) {
+                        var nextElementName = null;
+                        var nextClassName = null;
+
+
+                        var dotIndex = nextSelector.indexOf(".");
+                        if (dotIndex >= 0) {
+                            nextElementName = nextSelector.substring(0,dotIndex);
+                            nextClassName = nextSelector.substring(dotIndex+1);
+                        }
+                        else {
+                            nextElementName = nextSelector;
+                        }
+
+                        ancestor = DOM_replaceElement(ancestor,nextElementName);
+                        DOM_removeAttribute(ancestor,"id");
+                        DOM_setAttribute(ancestor,"class",nextClassName);
+                    }
+                }
+
+                if (isParagraphNode(ancestor) && !nodeHasContent(ancestor)) {
+                    Cursor_updateBRAtEndOfParagraph(prev);
+                    break;
+                }
+                else if ((ancestor._type == HTML_LI) && !nodeHasContent(ancestor)) {
+                    DOM_deleteAllChildren(ancestor);
+                    break;
+                }
+            }
+
+            Cursor_updateBRAtEndOfParagraph(Range_singleNode(selRange));
+        });
+
+        Selection_set(selRange.start.node,selRange.start.offset,
+                      selRange.end.node,selRange.end.offset);
+        cursorX = null;
+        Cursor_ensureCursorVisible();
+
+        function getBlockToSplit(pos)
+        {
+            var blockToSplit = null;
+            for (var n = pos.node; n != null; n = n.parentNode) {
+                if (n._type == HTML_LI) {
+                    blockToSplit = n;
+                    break;
+                }
+            }
+            if (blockToSplit == null) {
+                blockToSplit = pos.node;
+                while (isInlineNode(blockToSplit))
+                    blockToSplit = blockToSplit.parentNode;
+            }
+            return blockToSplit;
+        }
+
+        function getContainerOrParagraph(node)
+        {
+            while ((node != null) && isInlineNode(node))
+                node = node.parentNode;
+            return node;
+        }
+
+        function positionAtStartOfHeading(pos)
+        {
+            var container = getContainerOrParagraph(pos.node);
+            if (isHeadingNode(container)) {
+                var startOffset = 0;
+                if (isOpaqueNode(container.firstChild))
+                    startOffset = 1;
+                var range = new Range(container,startOffset,pos.node,pos.offset);
+                return !Range_hasContent(range);
+            }
+            else
+                return false;
+        }
+    }
+
+    Cursor_getPrecedingWord = function() {
+        var selRange = Selection_get();
+        if ((selRange == null) && !Range_isEmpty(selRange))
+            return "";
+
+        var node = selRange.start.node;
+        var offset = selRange.start.offset;
+        if (node.nodeType != Node.TEXT_NODE)
+            return "";
+
+        return node.nodeValue.substring(0,offset);
+    }
+
+    Cursor_getAdjacentNodeWithType = function(type)
+    {
+        var selRange = Selection_get();
+        var pos = Position_preferElementPosition(selRange.start);
+        var node = pos.node;
+        var offset = pos.offset;
+
+        while (true) {
+
+            if (node._type == type)
+                return node;
+
+            if (node.nodeType == Node.ELEMENT_NODE) {
+                var before = node.childNodes[offset-1];
+                if ((before != null) && (before._type == type))
+                    return before;
+
+                var after = node.childNodes[offset];
+                if ((after != null) && (after._type == type))
+                    return after;
+            }
+
+            if (node.parentNode == null)
+                return null;
+
+            offset = DOM_nodeOffset(node);
+            node = node.parentNode;
+        }
+    }
+
+    Cursor_getLinkProperties = function()
+    {
+        var a = Cursor_getAdjacentNodeWithType(HTML_A);
+        if (a == null)
+            return null;
+
+        return { href: a.getAttribute("href"),
+                 text: getNodeText(a) };
+    }
+
+    Cursor_setLinkProperties = function(properties)
+    {
+        var a = Cursor_getAdjacentNodeWithType(HTML_A);
+        if (a == null)
+            return null;
+
+        Selection_preserveWhileExecuting(function() {
+            DOM_setAttribute(a,"href",properties.href);
+            DOM_deleteAllChildren(a);
+            DOM_appendChild(a,DOM_createTextNode(document,properties.text));
+        });
+    }
+
+    Cursor_setReferenceTarget = function(itemId)
+    {
+        var a = Cursor_getAdjacentNodeWithType(HTML_A);
+        if (a != null)
+            Outline_setReferenceTarget(a,itemId);
+    }
+
+    // Deletes the current selection contents and ensures that the cursor is located directly
+    // inside the nearest container element, i.e. not inside a paragraph or inline node. This
+    // is intended for preventing things like inserting a table of contants inside a heading
+    Cursor_makeContainerInsertionPoint = function()
+    {
+        var selRange = Selection_get();
+        if (selRange == null)
+            return;
+
+        if (!Range_isEmpty(selRange)) {
+            Selection_deleteContents();
+            selRange = Selection_get();
+        }
+
+        var parent;
+        var previousSibling;
+        var nextSibling;
+
+        if (selRange.start.node.nodeType == Node.ELEMENT_NODE) {
+            parent = selRange.start.node;
+            nextSibling = selRange.start.node.childNodes[selRange.start.offset];
+        }
+        else {
+            if (selRange.start.offset > 0)
+                Formatting_splitTextBefore(selRange.start);
+            parent = selRange.start.node.parentNode;
+            nextSibling = selRange.start.node;
+        }
+
+        var offset = DOM_nodeOffset(nextSibling,parent);
+
+        if (isContainerNode(parent)) {
+            Cursor_set(parent,offset);
+            return;
+        }
+
+        if ((offset > 0) && isItemNumber(parent.childNodes[offset-1]))
+            offset--;
+
+        Formatting_moveFollowing(new Position(parent,offset),isContainerNode);
+        Formatting_movePreceding(new Position(parent,offset),isContainerNode);
+
+        offset = 0;
+        while (!isContainerNode(parent)) {
+            var old = parent;
+            offset = DOM_nodeOffset(parent);
+            parent = parent.parentNode;
+            DOM_deleteNode(old);
+        }
+
+        Cursor_set(parent,offset);
+        cursorX = null;
+    }
+
+    Cursor_set = function(node,offset,keepCursorX)
+    {
+        Selection_set(node,offset,node,offset);
+        if (!keepCursorX)
+            cursorX = null;
+    }
+
+    function moveRangeOutsideOfNote(range)
+    {
+        var node = range.start.node;
+        var offset = range.start.offset;
+
+        for (var anc = node; anc != null; anc = anc.parentNode) {
+            if (isNoteNode(anc) && (anc.parentNode != null)) {
+                node = anc.parentNode;
+                offset = DOM_nodeOffset(anc)+1;
+                return new Range(node,offset,node,offset);
+            }
+        }
+
+        return range;
+    }
+
+    function insertNote(className,content)
+    {
+        var footnote = DOM_createElement(document,"span");
+        DOM_setAttribute(footnote,"class",className);
+        DOM_appendChild(footnote,DOM_createTextNode(document,content));
+
+        var range = Selection_get();
+        range = moveRangeOutsideOfNote(range);
+        Formatting_splitAroundSelection(range,false);
+
+        // If we're part-way through a text node, splitAroundSelection will give us an
+        // empty text node between the before and after text. For formatting purposes that's
+        // fine (not sure if necessary), but when inserting a footnote or endnote we want
+        // to avoid this as it causes problems with cursor movement - specifically, the cursor
+        // is allowed to go inside the empty text node, and this doesn't show up in the correct
+        // position on screen.
+        var pos = range.start;
+        if ((pos.node._type == HTML_TEXT) &&
+            (pos.node.nodeValue.length == 0)) {
+            var empty = pos.node;
+            pos = new Position(empty.parentNode,DOM_nodeOffset(empty));
+            DOM_deleteNode(empty);
+        }
+        else {
+            pos = Position_preferElementPosition(pos);
+        }
+
+        DOM_insertBefore(pos.node,footnote,pos.node.childNodes[pos.offset]);
+        Selection_set(footnote,0,footnote,footnote.childNodes.length);
+        Cursor_updateBRAtEndOfParagraph(footnote);
+    }
+
+    Cursor_insertFootnote = function(content)
+    {
+        insertNote("footnote",content);
+    }
+
+    Cursor_insertEndnote = function(content)
+    {
+        insertNote("endnote",content);
+    }
+
+})();