corinthia-commits mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From j...@apache.org
Subject [19/28] incubator-corinthia git commit: included MOC compiler for Qt implementation
Date Mon, 17 Aug 2015 08:50:11 GMT
http://git-wip-us.apache.org/repos/asf/incubator-corinthia/blob/9bf02bb2/experiments/editorFramework/src/Javascript_Layer_0/Tables.js
----------------------------------------------------------------------
diff --git a/experiments/editorFramework/src/Javascript_Layer_0/Tables.js b/experiments/editorFramework/src/Javascript_Layer_0/Tables.js
new file mode 100644
index 0000000..f1f27be
--- /dev/null
+++ b/experiments/editorFramework/src/Javascript_Layer_0/Tables.js
@@ -0,0 +1,1362 @@
+// 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 Tables_insertTable;
+var Tables_addAdjacentRow;
+var Tables_addAdjacentColumn;
+var Tables_removeAdjacentRow;
+var Tables_removeAdjacentColumn;
+var Tables_deleteRegion;
+var Tables_clearCells;
+var Tables_mergeCells;
+var Tables_splitSelection;
+var Tables_cloneRegion;
+var Tables_analyseStructure;
+var Tables_findContainingCell;
+var Tables_findContainingTable;
+var Tables_regionFromRange;
+var Tables_getSelectedTableId;
+var Tables_getProperties;
+var Tables_setProperties;
+var Tables_getColWidths;
+var Tables_setColWidths;
+var Tables_getGeometry;
+
+var Table_get;
+var Table_set;
+var Table_setRegion;
+var Table_fix;
+var Table_fixColumnWidths;
+var TableRegion_splitCells;
+
+(function() {
+
+    function Cell(element,row,col)
+    {
+        this.element = element;
+        this.row = row;
+        this.col = col;
+
+        if (element.hasAttribute("colspan"))
+            this.colspan = parseInt(element.getAttribute("colspan"));
+        else
+            this.colspan = 1;
+        if (element.hasAttribute("rowspan"))
+            this.rowspan = parseInt(element.getAttribute("rowspan"));
+        else
+            this.rowspan = 1;
+
+        if (this.colspan < 1)
+            this.colspan = 1;
+        if (this.rowspan < 1)
+            this.rowspan = 1;
+
+        this.top = this.row;
+        this.bottom = this.top + this.rowspan - 1;
+        this.left = this.col;
+        this.right = this.left + this.colspan - 1;
+    }
+
+    function Cell_setRowspan(cell,rowspan)
+    {
+        if (rowspan < 1)
+            rowspan = 1;
+        cell.rowspan = rowspan;
+        cell.bottom = cell.top + cell.rowspan - 1;
+        if (rowspan == 1)
+            DOM_removeAttribute(cell.element,"rowspan");
+        else
+            DOM_setAttribute(cell.element,"rowspan",rowspan);
+    }
+
+    function Cell_setColspan(cell,colspan)
+    {
+        if (colspan < 1)
+            colspan = 1;
+        cell.colspan = colspan;
+        cell.right = cell.left + cell.colspan - 1;
+        if (colspan == 1)
+            DOM_removeAttribute(cell.element,"colspan");
+        else
+            DOM_setAttribute(cell.element,"colspan",colspan);
+    }
+
+    function Table(element)
+    {
+        this.element = element;
+        this.row = 0;
+        this.col = 0;
+        this.cells = new Array();
+        this.numRows = 0;
+        this.numCols = 0;
+        this.translated = false;
+        this.cellsByElement = new NodeMap();
+        Table_processTable(this,element);
+    }
+
+    // public
+    Table_get = function(table,row,col)
+    {
+        if (table.cells[row] == null)
+            return null;
+        return table.cells[row][col];
+    }
+
+    // public
+    Table_set = function(table,row,col,cell)
+    {
+        if (table.numRows < row+1)
+            table.numRows = row+1;
+        if (table.numCols < col+1)
+            table.numCols = col+1;
+        if (table.cells[row] == null)
+            table.cells[row] = new Array();
+        table.cells[row][col] = cell;
+    }
+
+    // public
+    Table_setRegion = function(table,top,left,bottom,right,cell)
+    {
+        for (var row = top; row <= bottom; row++) {
+            for (var col = left; col <= right; col++) {
+                var destCell = Table_get(table,row,col);
+                DOM_deleteNode(destCell.element);
+                Table_set(table,row,col,cell);
+            }
+        }
+    }
+
+    function Table_processTable(table,node)
+    {
+        var type = node._type;
+        switch (node._type) {
+        case HTML_TD:
+        case HTML_TH: {
+            while (Table_get(table,table.row,table.col) != null)
+                table.col++;
+
+            var cell = new Cell(node,table.row,table.col);
+            table.cellsByElement.put(node,cell);
+
+            for (var r = 0; r < cell.rowspan; r++) {
+                for (var c = 0; c < cell.colspan; c++) {
+                    Table_set(table,table.row+r,table.col+c,cell);
+                }
+            }
+            table.col += cell.colspan;
+            break;
+        }
+        case HTML_TR:
+            for (var child = node.firstChild; child != null; child = child.nextSibling)
+                Table_processTable(table,child);
+            table.row++;
+            table.col = 0;
+            break;
+        default:
+            for (var child = node.firstChild; child != null; child = child.nextSibling)
+                Table_processTable(table,child);
+            break;
+        }
+    }
+
+    // public
+    Tables_insertTable = function(rows,cols,width,numbered,caption,className)
+    {
+        UndoManager_newGroup("Insert table");
+
+        if (rows < 1)
+            rows = 1;
+        if (cols < 1)
+            cols = 1;
+
+        var haveCaption = (caption != null) && (caption != "");
+        var table = DOM_createElement(document,"TABLE");
+
+        if (width != null)
+            DOM_setStyleProperties(table,{"width": width});
+
+        if (className != null)
+            DOM_setAttribute(table,"class",className);
+
+        // Caption comes first
+        if (haveCaption) {
+            var tableCaption = DOM_createElement(document,"CAPTION");
+            DOM_appendChild(tableCaption,DOM_createTextNode(document,caption));
+            DOM_appendChild(table,tableCaption);
+        }
+
+        // Set equal column widths
+        var colWidth = Math.round(100/cols)+"%";
+        for (var c = 0; c < cols; c++) {
+            var col = DOM_createElement(document,"COL");
+            DOM_setAttribute(col,"width",colWidth);
+            DOM_appendChild(table,col);
+        }
+
+        var firstTD = null;
+
+        // Then the rows and columns
+        var tbody = DOM_createElement(document,"TBODY");
+        DOM_appendChild(table,tbody);
+        for (var r = 0; r < rows; r++) {
+            var tr = DOM_createElement(document,"TR");
+            DOM_appendChild(tbody,tr);
+            for (var c = 0; c < cols; c++) {
+                var td = DOM_createElement(document,"TD");
+                var p = DOM_createElement(document,"P");
+                var br = DOM_createElement(document,"BR");
+                DOM_appendChild(tr,td);
+                DOM_appendChild(td,p);
+                DOM_appendChild(p,br);
+
+                if (firstTD == null)
+                    firstTD = td;
+            }
+        }
+
+        Clipboard_pasteNodes([table]);
+
+        // Now that the table has been inserted into the DOM tree, the outline code will
+        // have noticed it and added an id attribute, as well as a caption giving the
+        // table number.
+        Outline_setNumbered(table.getAttribute("id"),numbered);
+
+        // Place the cursor at the start of the first cell on the first row
+        var pos = new Position(firstTD,0);
+        pos = Position_closestMatchForwards(pos,Position_okForMovement);
+        Selection_set(pos.node,pos.offset,pos.node,pos.offset);
+
+        PostponedActions_add(UndoManager_newGroup);
+    }
+
+    // private
+    function createEmptyTableCell(elementName)
+    {
+        var br = DOM_createElement(document,"BR");
+        var p = DOM_createElement(document,"P");
+        var td = DOM_createElement(document,elementName);
+        DOM_appendChild(p,br);
+        DOM_appendChild(td,p);
+        return td;
+    }
+
+    // private
+    function addEmptyTableCell(newTR,elementName)
+    {
+        var td = createEmptyTableCell(elementName);
+        DOM_appendChild(newTR,td);
+        return td;
+    }
+
+    // private
+    function populateNewRow(structure,newTR,newRow,oldRow)
+    {
+        var col = 0;
+        while (col < structure.numCols) {
+            var existingCell = Table_get(structure,oldRow,col);
+            if (((newRow > oldRow) && (newRow < existingCell.row + existingCell.rowspan)) ||
+                ((newRow < oldRow) && (newRow >= existingCell.row))) {
+                Cell_setRowspan(existingCell,existingCell.rowspan+1);
+            }
+            else {
+                var td = addEmptyTableCell(newTR,existingCell.element.nodeName); // check-ok
+                if (existingCell.colspan != 1)
+                    DOM_setAttribute(td,"colspan",existingCell.colspan);
+            }
+            col += existingCell.colspan;
+        }
+    }
+
+    function tableAtRightOfRange(range)
+    {
+        if (!Range_isEmpty(range))
+            return null;
+
+        var pos = Position_preferElementPosition(range.start);
+        if ((pos.node.nodeType == Node.ELEMENT_NODE) &&
+            (pos.offset < pos.node.childNodes.length) &&
+            (pos.node.childNodes[pos.offset]._type == HTML_TABLE)) {
+            var element = pos.node.childNodes[pos.offset];
+            var table = Tables_analyseStructure(element);
+            return table;
+        }
+        return null;
+    }
+
+    function tableAtLeftOfRange(range)
+    {
+        if (!Range_isEmpty(range))
+            return null;
+
+        var pos = Position_preferElementPosition(range.start);
+        if ((pos.node.nodeType == Node.ELEMENT_NODE) &&
+            (pos.offset > 0) &&
+            (pos.node.childNodes[pos.offset-1]._type == HTML_TABLE)) {
+            var element = pos.node.childNodes[pos.offset-1];
+            var table = Tables_analyseStructure(element);
+            return table;
+        }
+        return null;
+    }
+
+    function insertRowAbove(table,row)
+    {
+        var cell = Table_get(table,row,0);
+        var oldTR = cell.element.parentNode;
+        var newTR = DOM_createElement(document,"TR");
+        DOM_insertBefore(oldTR.parentNode,newTR,oldTR);
+        populateNewRow(table,newTR,row-1,row);
+    }
+
+    function insertRowBelow(table,row)
+    {
+        var cell = Table_get(table,row,0);
+        var oldTR = cell.element.parentNode;
+        var newTR = DOM_createElement(document,"TR");
+        DOM_insertBefore(oldTR.parentNode,newTR,oldTR.nextSibling);
+        populateNewRow(table,newTR,row+1,row);
+    }
+
+    function insertRowAdjacentToRange(range)
+    {
+        var table;
+
+        table = tableAtLeftOfRange(range);
+        if (table != null) {
+            insertRowBelow(table,table.numRows-1);
+            return;
+        }
+
+        table = tableAtRightOfRange(range);
+        if (table != null) {
+            insertRowAbove(table,0);
+            return;
+        }
+    }
+
+    // public
+    Tables_addAdjacentRow = function()
+    {
+        UndoManager_newGroup("Insert row below");
+        Selection_preserveWhileExecuting(function() {
+            var range = Selection_get();
+            var region = Tables_regionFromRange(range,true);
+            if (region != null)
+                insertRowBelow(region.structure,region.bottom);
+            else
+                insertRowAdjacentToRange(range);
+        });
+        UndoManager_newGroup();
+    }
+
+    // private
+    function getColElements(table)
+    {
+        var cols = new Array();
+        for (child = table.firstChild; child != null; child = child.nextSibling) {
+            switch (child._type) {
+            case HTML_COLGROUP:
+                for (var gc = child.firstChild; gc != null; gc = gc.nextSibling) {
+                    if (gc._type == HTML_COL)
+                        cols.push(gc);
+                }
+                break;
+            case HTML_COL:
+                cols.push(child);
+                break;
+            }
+        }
+        return cols;
+    }
+
+    // private
+    function getColWidths(colElements,expectedCount)
+    {
+        // FIXME: also handle the case where the width has been set as a CSS property in the
+        // style attribute. There's probably not much we can do if the width comes from a style
+        // rule elsewhere in the document though.
+        var colWidths = new Array();
+        for (var i = 0; i < colElements.length; i++) {
+            if (colElements[i].hasAttribute("width"))
+                colWidths.push(colElements[i].getAttribute("width"));
+            else
+                colWidths.push("");
+        }
+        return colWidths;
+    }
+
+    // private
+    function addMissingColElements(structure,colElements)
+    {
+        // If there are fewer COL elements than there are colums, add extra ones, copying the
+        // width value from the last one
+        // FIXME: handle col elements with colspan > 1, as well as colgroups with width set
+        // FIXME: What if there are 0 col elements?
+        while (colElements.length < structure.numCols) {
+            var newColElement = DOM_createElement(document,"COL");
+            var lastColElement = colElements[colElements.length-1];
+            DOM_insertBefore(lastColElement.parentNode,newColElement,lastColElement.nextSibling);
+            colElements.push(newColElement);
+            DOM_setAttribute(newColElement,"width",lastColElement.getAttribute("width"));
+        }
+    }
+
+    // private
+    function fixColPercentages(structure,colElements)
+    {
+        var colWidths = getColWidths(colElements,structure.numCols);
+
+        var percentages = colWidths.map(getPercentage);
+        if (percentages.every(notNull)) {
+            var colWidthTotal = 0;
+            for (var i = 0; i < percentages.length; i++)
+                colWidthTotal += percentages[i];
+
+            for (var i = 0; i < colElements.length; i++) {
+                var pct = 100*percentages[i]/colWidthTotal;
+                // Store value using at most two decimal places
+                pct = Math.round(100*pct)/100;
+                DOM_setAttribute(colElements[i],"width",pct+"%");
+            }
+        }
+
+        function notNull(arg)
+        {
+            return (arg != null);
+        }
+
+        function getPercentage(str)
+        {
+            if (str.match(/^\s*\d+(\.\d+)?\s*%\s*$/))
+                return parseInt(str.replace(/\s*%\s*$/,""));
+            else
+                return null;
+        }
+    }
+
+    // private
+    function addColElement(structure,oldIndex,right)
+    {
+        var table = structure.element;
+
+        var colElements = getColElements(table);
+        if (colElements.length == 0) {
+            // The table doesn't have any COL elements; don't add any
+            return;
+        }
+
+        addMissingColElements(structure,colElements);
+
+        var prevColElement = colElements[oldIndex];
+        var newColElement = DOM_createElement(document,"COL");
+        DOM_setAttribute(newColElement,"width",prevColElement.getAttribute("width"));
+        if (right)
+            DOM_insertBefore(prevColElement.parentNode,newColElement,prevColElement.nextSibling);
+        else
+            DOM_insertBefore(prevColElement.parentNode,newColElement,prevColElement);
+
+        if (right) {
+            colElements.splice(oldIndex+1,0,newColElement);
+        }
+        else {
+            colElements.splice(oldIndex+1,0,newColElement);
+        }
+
+        fixColPercentages(structure,colElements);
+    }
+
+    // private
+    function deleteColElements(structure,left,right)
+    {
+        var table = structure.element;
+
+        var colElements = getColElements(table);
+        if (colElements.length == 0) {
+            // The table doesn't have any COL elements
+            return;
+        }
+
+        addMissingColElements(structure,colElements);
+
+        for (var col = left; col <= right; col++)
+            DOM_deleteNode(colElements[col]);
+        colElements.splice(left,right-left+1);
+
+        fixColPercentages(structure,colElements);
+    }
+
+    // private
+    function addColumnCells(structure,oldIndex,right)
+    {
+        for (var row = 0; row < structure.numRows; row++) {
+            var cell = Table_get(structure,row,oldIndex);
+            var oldTD = cell.element;
+            if (cell.row == row) {
+
+                if (((right && (oldIndex+1 < cell.col + cell.colspan)) ||
+                    (!right && (oldIndex-1 >= cell.col))) &&
+                    (cell.colspan > 1)) {
+                    Cell_setColspan(cell,cell.colspan+1);
+                }
+                else {
+                    var newTD = createEmptyTableCell(oldTD.nodeName); // check-ok
+                    if (right)
+                        DOM_insertBefore(cell.element.parentNode,newTD,oldTD.nextSibling);
+                    else
+                        DOM_insertBefore(cell.element.parentNode,newTD,oldTD);
+                    if (cell.rowspan != 1)
+                        DOM_setAttribute(newTD,"rowspan",cell.rowspan);
+                }
+            }
+        }
+    }
+
+    function insertColumnAdjacentToRange(range)
+    {
+        var table;
+
+        table = tableAtLeftOfRange(range);
+        if (table != null) {
+            var right = table.numCols-1;
+            addColElement(table,right,right+1);
+            addColumnCells(table,right,true);
+            return;
+        }
+
+        table = tableAtRightOfRange(range);
+        if (table != null) {
+            var left = 0;
+            addColElement(table,left,left-1);
+            addColumnCells(table,left,false);
+            return;
+        }
+    }
+
+    // public
+    Tables_addAdjacentColumn = function()
+    {
+        UndoManager_newGroup("Insert column at right");
+        Selection_preserveWhileExecuting(function() {
+            var range = Selection_get();
+            var region = Tables_regionFromRange(range,true);
+            if (region != null) {
+                addColElement(region.structure,region.right,region.right+1);
+                addColumnCells(region.structure,region.right,true);
+            }
+            else {
+                insertColumnAdjacentToRange(range);
+            }
+        });
+        UndoManager_newGroup();
+    }
+
+    function columnHasContent(table,col)
+    {
+        for (var row = 0; row < table.numRows; row++) {
+            var cell = Table_get(table,row,col);
+            if ((cell != null) && (cell.col == col) && nodeHasContent(cell.element))
+                return true;
+        }
+        return false;
+    }
+
+    function rowHasContent(table,row)
+    {
+        for (var col = 0; col < table.numCols; col++) {
+            var cell = Table_get(table,row,col);
+            if ((cell != null) && (cell.row == row) && nodeHasContent(cell.element))
+                return true;
+        }
+        return false;
+    }
+
+    function selectRegion(table,top,bottom,left,right)
+    {
+        left = clampCol(table,left);
+        right = clampCol(table,right);
+        top = clampRow(table,top);
+        bottom = clampRow(table,bottom);
+
+        var tlCell = Table_get(table,top,left);
+        var brCell = Table_get(table,bottom,right);
+        if ((tlCell != null) && (brCell != null)) {
+            var tlPos = new Position(tlCell.element,0);
+            tlPos = Position_closestMatchForwards(tlPos,Position_okForMovement);
+
+            var brPos = new Position(brCell.element,brCell.element.childNodes.length);
+            brPos = Position_closestMatchBackwards(brPos,Position_okForMovement);
+
+            Selection_set(tlPos.node,tlPos.offset,brPos.node,brPos.offset);
+        }
+    }
+
+    function clampCol(table,col)
+    {
+        if (col > table.numCols-1)
+            col = table.numCols-1;
+        if (col < 0)
+            col = 0;
+        return col;
+    }
+
+    function clampRow(table,row)
+    {
+        if (row > table.numRows-1)
+            row = table.numRows-1;
+        if (row < 0)
+            row = 0;
+        return row;
+    }
+
+    function removeRowAdjacentToRange(range)
+    {
+        var table;
+
+        table = tableAtLeftOfRange(range);
+        if ((table != null) && (table.numRows >= 2)) {
+            UndoManager_newGroup("Delete one row");
+            var row = table.numRows-1;
+            Tables_deleteRegion(new TableRegion(table,row,row,0,table.numCols-1));
+            UndoManager_newGroup();
+            return;
+        }
+
+        table = tableAtRightOfRange(range);
+        if ((table != null) && (table.numRows >= 2)) {
+            UndoManager_newGroup("Delete one row");
+            Tables_deleteRegion(new TableRegion(table,0,0,0,table.numCols-1));
+            UndoManager_newGroup();
+            return;
+        }
+    }
+
+    Tables_removeAdjacentRow = function()
+    {
+        var range = Selection_get();
+        var region = Tables_regionFromRange(range,true);
+
+        if (region == null) {
+            removeRowAdjacentToRange(range);
+            return;
+        }
+
+        if (region.structure.numRows <= 1)
+            return;
+
+        UndoManager_newGroup("Delete one row");
+
+        var table = region.structure;
+        var left = region.left;
+        var right = region.right;
+        var top = region.top;
+        var bottom = region.bottom;
+
+        // Is there an empty row below the selection? If so, delete it
+        if ((bottom+1 < table.numRows) && !rowHasContent(table,bottom+1)) {
+            Selection_preserveWhileExecuting(function() {
+                Tables_deleteRegion(new TableRegion(table,bottom+1,bottom+1,0,table.numCols-1));
+            });
+        }
+
+        // Is there an empty row above the selection? If so, delete it
+        else if ((top-1 >= 0) && !rowHasContent(table,top-1)) {
+            Selection_preserveWhileExecuting(function() {
+                Tables_deleteRegion(new TableRegion(table,top-1,top-1,0,table.numCols-1));
+            });
+        }
+
+
+        // There are no empty rows adjacent to the selection. Delete the right-most row
+        // of the selection (which may be the only one)
+        else {
+            Selection_preserveWhileExecuting(function() {
+                Tables_deleteRegion(new TableRegion(table,bottom,bottom,0,table.numCols-1));
+            });
+
+            table = Tables_analyseStructure(table.element);
+            var multiple = (top != bottom);
+
+            if (multiple) {
+                selectRegion(table,top,bottom-1,left,right);
+            }
+            else {
+                var newRow = clampRow(table,bottom);
+                var newCell = Table_get(table,newRow,left);
+                if (newCell != null) {
+                    var pos = new Position(newCell.element,0);
+                    pos = Position_closestMatchForwards(pos,Position_okForMovement);
+                    Selection_set(pos.node,pos.offset,pos.node,pos.offset);
+                }
+            }
+        }
+
+        UndoManager_newGroup();
+    }
+
+    function removeColumnAdjacentToRange(range)
+    {
+        var table;
+
+        table = tableAtLeftOfRange(range);
+        if ((table != null) && (table.numCols >= 2)) {
+            UndoManager_newGroup("Delete one column");
+            var col = table.numCols-1;
+            Tables_deleteRegion(new TableRegion(table,0,table.numRows-1,col,col));
+            UndoManager_newGroup();
+            return;
+        }
+
+        table = tableAtRightOfRange(range);
+        if ((table != null) && (table.numCols >= 2)) {
+            UndoManager_newGroup("Delete one column");
+            Tables_deleteRegion(new TableRegion(table,0,table.numRows-1,0,0));
+            UndoManager_newGroup();
+            return;
+        }
+    }
+
+    Tables_removeAdjacentColumn = function()
+    {
+        var range = Selection_get();
+        var region = Tables_regionFromRange(range,true);
+
+        if (region == null) {
+            removeColumnAdjacentToRange(range);
+            return;
+        }
+
+        if (region.structure.numCols <= 1)
+            return;
+
+        UndoManager_newGroup("Delete one column");
+
+        var table = region.structure;
+        var left = region.left;
+        var right = region.right;
+        var top = region.top;
+        var bottom = region.bottom;
+
+        // Is there an empty column to the right of the selection? If so, delete it
+        if ((right+1 < table.numCols) && !columnHasContent(table,right+1)) {
+            Selection_preserveWhileExecuting(function() {
+                Tables_deleteRegion(new TableRegion(table,0,table.numRows-1,right+1,right+1));
+            });
+        }
+
+        // Is there an empty column to the left of the selection? If so, delete it
+        else if ((left-1 >= 0) && !columnHasContent(table,left-1)) {
+            Selection_preserveWhileExecuting(function() {
+                Tables_deleteRegion(new TableRegion(table,0,table.numRows-1,left-1,left-1));
+            });
+        }
+
+        // There are no empty columns adjacent to the selection. Delete the right-most column
+        // of the selection (which may be the only one)
+        else {
+            Selection_preserveWhileExecuting(function() {
+                Tables_deleteRegion(new TableRegion(table,0,table.numRows-1,right,right));
+            });
+
+            table = Tables_analyseStructure(table.element);
+            var multiple = (left != right);
+
+            if (multiple) {
+                selectRegion(table,top,bottom,left,right-1);
+            }
+            else {
+                var newCol = clampCol(table,right);
+                var newCell = Table_get(table,top,newCol);
+                if (newCell != null) {
+                    var pos = new Position(newCell.element,0);
+                    pos = Position_closestMatchForwards(pos,Position_okForMovement);
+                    Selection_set(pos.node,pos.offset,pos.node,pos.offset);
+                }
+            }
+        }
+
+        UndoManager_newGroup();
+    }
+
+    // private
+    function deleteTable(structure)
+    {
+        DOM_deleteNode(structure.element);
+    }
+
+    // private
+    function deleteRows(structure,top,bottom)
+    {
+        var trElements = new Array();
+        getTRs(structure.element,trElements);
+
+        for (var row = top; row <= bottom; row++)
+            DOM_deleteNode(trElements[row]);
+    }
+
+    // private
+    function getTRs(node,result)
+    {
+        if (node._type == HTML_TR) {
+            result.push(node);
+        }
+        else {
+            for (var child = node.firstChild; child != null; child = child.nextSibling)
+                getTRs(child,result);
+        }
+    }
+
+    // private
+    function deleteColumns(structure,left,right)
+    {
+        var nodesToDelete = new NodeSet();
+        for (var row = 0; row < structure.numRows; row++) {
+            for (var col = left; col <= right; col++) {
+                var cell = Table_get(structure,row,col);
+                nodesToDelete.add(cell.element);
+            }
+        }
+        nodesToDelete.forEach(DOM_deleteNode);
+        deleteColElements(structure,left,right);
+    }
+
+    // private
+    function deleteCellContents(region)
+    {
+        var structure = region.structure;
+        for (var row = region.top; row <= region.bottom; row++) {
+            for (var col = region.left; col <= region.right; col++) {
+                var cell = Table_get(structure,row,col);
+                DOM_deleteAllChildren(cell.element);
+            }
+        }
+    }
+
+    // public
+    Tables_deleteRegion = function(region)
+    {
+        var structure = region.structure;
+
+        var coversEntireWidth = (region.left == 0) && (region.right == structure.numCols-1);
+        var coversEntireHeight = (region.top == 0) && (region.bottom == structure.numRows-1);
+
+        if (coversEntireWidth && coversEntireHeight)
+            deleteTable(region.structure);
+        else if (coversEntireWidth)
+            deleteRows(structure,region.top,region.bottom);
+        else if (coversEntireHeight)
+            deleteColumns(structure,region.left,region.right);
+        else
+            deleteCellContents(region);
+    }
+
+    // public
+    Tables_clearCells = function()
+    {
+    }
+
+    // public
+    Tables_mergeCells = function()
+    {
+        Selection_preserveWhileExecuting(function() {
+            var region = Tables_regionFromRange(Selection_get());
+            if (region == null)
+                return;
+
+            var structure = region.structure;
+
+            // FIXME: handle the case of missing cells
+            // (or even better, add cells where there are some missing)
+
+            for (var row = region.top; row <= region.bottom; row++) {
+                for (var col = region.left; col <= region.right; col++) {
+                    var cell = Table_get(structure,row,col);
+                    var cellFirstRow = cell.row;
+                    var cellLastRow = cell.row + cell.rowspan - 1;
+                    var cellFirstCol = cell.col;
+                    var cellLastCol = cell.col + cell.colspan - 1;
+
+                    if ((cellFirstRow < region.top) || (cellLastRow > region.bottom) ||
+                        (cellFirstCol < region.left) || (cellLastCol > region.right)) {
+                        debug("Can't merge this table: cell at "+row+","+col+
+                              " goes outside bounds of selection");
+                        return;
+                    }
+                }
+            }
+
+            var mergedCell = Table_get(structure,region.top,region.left);
+
+            for (var row = region.top; row <= region.bottom; row++) {
+                for (var col = region.left; col <= region.right; col++) {
+                    var cell = Table_get(structure,row,col);
+                    // parentNode will be null if we've already done this cell
+                    if ((cell != mergedCell) && (cell.element.parentNode != null)) {
+                        while (cell.element.firstChild != null)
+                            DOM_appendChild(mergedCell.element,cell.element.firstChild);
+                        DOM_deleteNode(cell.element);
+                    }
+                }
+            }
+
+            var totalRows = region.bottom - region.top + 1;
+            var totalCols = region.right - region.left + 1;
+            if (totalRows == 1)
+                DOM_removeAttribute(mergedCell.element,"rowspan");
+            else
+                DOM_setAttribute(mergedCell.element,"rowspan",totalRows);
+            if (totalCols == 1)
+                DOM_removeAttribute(mergedCell.element,"colspan");
+            else
+                DOM_setAttribute(mergedCell.element,"colspan",totalCols);
+        });
+    }
+
+    // public
+    Tables_splitSelection = function()
+    {
+        Selection_preserveWhileExecuting(function() {
+            var range = Selection_get();
+            Range_trackWhileExecuting(range,function() {
+                var region = Tables_regionFromRange(range,true);
+                if (region != null)
+                    TableRegion_splitCells(region);
+            });
+        });
+    }
+
+    // public
+    TableRegion_splitCells = function(region)
+    {
+        var structure = region.structure;
+        var trElements = new Array();
+        getTRs(structure.element,trElements);
+
+        for (var row = region.top; row <= region.bottom; row++) {
+            for (var col = region.left; col <= region.right; col++) {
+                var cell = Table_get(structure,row,col);
+                if ((cell.rowspan > 1) || (cell.colspan > 1)) {
+
+                    var original = cell.element;
+
+                    for (var r = cell.top; r <= cell.bottom; r++) {
+                        for (var c = cell.left; c <= cell.right; c++) {
+                            if ((r == cell.top) && (c == cell.left))
+                                continue;
+                            var newTD = createEmptyTableCell(original.nodeName); // check-ok
+                            var nextElement = null;
+
+                            var nextCol = cell.right+1;
+                            while (nextCol < structure.numCols) {
+                                var nextCell = Table_get(structure,r,nextCol);
+                                if ((nextCell != null) && (nextCell.row == r)) {
+                                    nextElement = nextCell.element;
+                                    break;
+                                }
+                                nextCol++;
+                            }
+
+                            DOM_insertBefore(trElements[r],newTD,nextElement);
+                            Table_set(structure,r,c,new Cell(newTD,r,c));
+                        }
+                    }
+                    DOM_removeAttribute(original,"rowspan");
+                    DOM_removeAttribute(original,"colspan");
+                }
+            }
+        }
+    }
+
+    // public
+    Tables_cloneRegion = function(region)
+    {
+        var cellNodesDone = new NodeSet();
+        var table = DOM_shallowCopyElement(region.structure.element);
+        for (var row = region.top; row <= region.bottom; row++) {
+            var tr = DOM_createElement(document,"TR");
+            DOM_appendChild(table,tr);
+            for (var col = region.left; col <= region.right; col++) {
+                var cell = Table_get(region.structure,row,col);
+                if (!cellNodesDone.contains(cell.element)) {
+                    DOM_appendChild(tr,DOM_cloneNode(cell.element,true));
+                    cellNodesDone.add(cell.element);
+                }
+            }
+        }
+        return table;
+    }
+
+    // private
+    function pasteCells(fromTableElement,toRegion)
+    {
+        // FIXME
+        var fromStructure = Tables_analyseStructure(fromTableElement);
+    }
+
+    // public
+    Table_fix = function(table)
+    {
+        var changed = false;
+
+        var tbody = null;
+        for (var child = table.element.firstChild; child != null; child = child.nextSibling) {
+            if (child._type == HTML_TBODY)
+                tbody = child;
+        }
+
+        if (tbody == null)
+            return table; // FIXME: handle presence of THEAD and TFOOT, and also a missing TBODY
+
+        var trs = new Array();
+        for (var child = tbody.firstChild; child != null; child = child.nextSibling) {
+            if (child._type == HTML_TR)
+                trs.push(child);
+        }
+
+        while (trs.length < table.numRows) {
+            var tr = DOM_createElement(document,"TR");
+            DOM_appendChild(tbody,tr);
+            trs.push(tr);
+        }
+
+        for (var row = 0; row < table.numRows; row++) {
+            for (var col = 0; col < table.numCols; col++) {
+                var cell = Table_get(table,row,col);
+                if (cell == null) {
+                    var td = createEmptyTableCell("TD");
+                    DOM_appendChild(trs[row],td);
+                    changed = true;
+                }
+            }
+        }
+
+        if (changed)
+            return new Table(table.element);
+        else
+            return table;
+    }
+
+    // public
+    Table_fixColumnWidths = function(structure)
+    {
+        var colElements = getColElements(structure.element);
+        if (colElements.length == 0)
+            return;
+        addMissingColElements(structure,colElements);
+
+        var widths = Tables_getColWidths(structure);
+        fixWidths(widths,structure.numCols);
+        colElements = getColElements(structure.element);
+        for (var i = 0; i < widths.length; i++)
+            DOM_setAttribute(colElements[i],"width",widths[i]+"%");
+    }
+
+    // public
+    Tables_analyseStructure = function(element)
+    {
+        // FIXME: we should probably be preserving the selection here, since we are modifying
+        // the DOM (though I think it's unlikely it would cause problems, becausing the fixup
+        // logic only adds elements). However this method is called (indirectly) from within
+        // Selection_update(), which causes unbounded recursion due to the subsequent Selecton_set()
+        // that occurs.
+        var initial = new Table(element);
+        var fixed = Table_fix(initial);
+        return fixed;
+    }
+
+    // public
+    Tables_findContainingCell = function(node)
+    {
+        for (var ancestor = node; ancestor != null; ancestor = ancestor.parentNode) {
+            if (isTableCell(ancestor))
+                return ancestor;
+        }
+        return null;
+    }
+
+    // public
+    Tables_findContainingTable = function(node)
+    {
+        for (var ancestor = node; ancestor != null; ancestor = ancestor.parentNode) {
+            if (ancestor._type == HTML_TABLE)
+                return ancestor;
+        }
+        return null;
+    }
+
+    function TableRegion(structure,top,bottom,left,right)
+    {
+        this.structure = structure;
+        this.top = top;
+        this.bottom = bottom;
+        this.left = left;
+        this.right = right;
+    }
+
+    TableRegion.prototype.toString = function()
+    {
+        return "("+this.top+","+this.left+") - ("+this.bottom+","+this.right+")";
+    }
+
+    // public
+    Tables_regionFromRange = function(range,allowSameCell)
+    {
+        var region = null;
+
+        if (range == null)
+            return null;
+
+        var start = Position_closestActualNode(range.start,true);
+        var end = Position_closestActualNode(range.end,true);
+
+        var startTD = Tables_findContainingCell(start);
+        var endTD = Tables_findContainingCell(end);
+
+        if (!isTableCell(start) || !isTableCell(end)) {
+            if (!allowSameCell) {
+                if (startTD == endTD) // not in cell, or both in same cell
+                    return null;
+            }
+        }
+
+        if ((startTD == null) || (endTD == null))
+            return null;
+
+        var startTable = Tables_findContainingTable(startTD);
+        var endTable = Tables_findContainingTable(endTD);
+
+        if (startTable != endTable)
+            return null;
+
+        var structure = Tables_analyseStructure(startTable);
+
+        var startInfo = structure.cellsByElement.get(startTD);
+        var endInfo = structure.cellsByElement.get(endTD);
+
+        var startTopRow = startInfo.row;
+        var startBottomRow = startInfo.row + startInfo.rowspan - 1;
+        var startLeftCol = startInfo.col;
+        var startRightCol = startInfo.col + startInfo.colspan - 1;
+
+        var endTopRow = endInfo.row;
+        var endBottomRow = endInfo.row + endInfo.rowspan - 1;
+        var endLeftCol = endInfo.col;
+        var endRightCol = endInfo.col + endInfo.colspan - 1;
+
+        var top = (startTopRow < endTopRow) ? startTopRow : endTopRow;
+        var bottom = (startBottomRow > endBottomRow) ? startBottomRow : endBottomRow;
+        var left = (startLeftCol < endLeftCol) ? startLeftCol : endLeftCol;
+        var right = (startRightCol > endRightCol) ? startRightCol : endRightCol;
+
+        var region = new TableRegion(structure,top,bottom,left,right);
+        adjustRegionForSpannedCells(region);
+        return region;
+    }
+
+    // private
+    function adjustRegionForSpannedCells(region)
+    {
+        var structure = region.structure;
+        var boundariesOk;
+        var columnsOk;
+        do {
+            boundariesOk = true;
+            for (var row = region.top; row <= region.bottom; row++) {
+                var cell = Table_get(structure,row,region.left);
+                if (region.left > cell.left) {
+                    region.left = cell.left;
+                    boundariesOk = false;
+                }
+                cell = Table_get(structure,row,region.right);
+                if (region.right < cell.right) {
+                    region.right = cell.right;
+                    boundariesOk = false;
+                }
+            }
+
+            for (var col = region.left; col <= region.right; col++) {
+                var cell = Table_get(structure,region.top,col);
+                if (region.top > cell.top) {
+                    region.top = cell.top;
+                    boundariesOk = false;
+                }
+                cell = Table_get(structure,region.bottom,col);
+                if (region.bottom < cell.bottom) {
+                    region.bottom = cell.bottom;
+                    boundariesOk = false;
+                }
+            }
+        } while (!boundariesOk);
+    }
+
+    Tables_getSelectedTableId = function()
+    {
+        var element = Cursor_getAdjacentNodeWithType(HTML_TABLE);
+        return element ? element.getAttribute("id") : null;
+    }
+
+    Tables_getProperties = function(itemId)
+    {
+        var element = document.getElementById(itemId);
+        if ((element == null) || (element._type != HTML_TABLE))
+            return null;
+        var structure = Tables_analyseStructure(element);
+        var width = element.style.width;
+        return { width: width, rows: structure.numRows, cols: structure.numCols };
+    }
+
+    Tables_setProperties = function(itemId,width)
+    {
+        var table = document.getElementById(itemId);
+        if (table == null)
+            return null;
+        DOM_setStyleProperties(table,{ width: width });
+        Selection_update(); // ensure cursor/selection drawn in correct pos
+    }
+
+    // Returns an array of numbers representing the percentage widths (0 - 100) of each
+    // column. This works on the assumption that all tables are supposed to have all of
+    // their column widths specified, and in all cases as percentages. Any which do not
+    // are considered invalid, and have any non-percentage values filled in based on the
+    // average values of all valid percentage-based columns.
+    Tables_getColWidths = function(structure)
+    {
+        var colElements = getColElements(structure.element);
+        var colWidths = new Array();
+
+        for (var i = 0; i < structure.numCols; i++) {
+            var value = null;
+
+            if (i < colElements.length) {
+                var widthStr = DOM_getAttribute(colElements[i],"width");
+                if (widthStr != null) {
+                    value = parsePercentage(widthStr);
+                }
+            }
+
+            if ((value != null) && (value >= 1.0)) {
+                colWidths[i] = value;
+            }
+            else {
+                colWidths[i] = null;
+            }
+        }
+
+        fixWidths(colWidths,structure.numCols);
+
+        return colWidths;
+
+        function parsePercentage(str)
+        {
+            if (str.match(/^\s*\d+(\.\d+)?\s*%\s*$/))
+                return parseFloat(str.replace(/\s*%\s*$/,""));
+            else
+                return null;
+        }
+    }
+
+    function fixWidths(colWidths,numCols)
+    {
+        var totalWidth = 0;
+        var numValidCols = 0;
+        for (var i = 0; i < numCols; i++) {
+            if (colWidths[i] != null) {
+                totalWidth += colWidths[i];
+                numValidCols++;
+            }
+        }
+
+        var averageWidth = (numValidCols > 0) ? totalWidth/numValidCols : 1.0;
+        for (var i = 0; i < numCols; i++) {
+            if (colWidths[i] == null) {
+                colWidths[i] = averageWidth;
+                totalWidth += averageWidth;
+            }
+        }
+
+        // To cater for the case where the column widths do not all add up to 100%,
+        // recalculate all of them based on their value relative to the total width
+        // of all columns. For example, if there are three columns of 33%, 33%, and 33%,
+        // these will get rounded up to 33.33333.....%.
+        // If there are no column widths defined, each will have 100/numCols%.
+        if (totalWidth > 0) {
+            for (var i = 0; i < numCols; i++) {
+                colWidths[i] = 100.0*colWidths[i]/totalWidth;
+            }
+        }
+    }
+
+    // public
+    Tables_setColWidths = function(itemId,widths)
+    {
+        var element = document.getElementById(itemId);
+        if (element == null)
+            return null;
+
+        var structure = Tables_analyseStructure(element);
+
+        fixWidths(widths,structure.numCols);
+
+        var colElements = getColElements(element);
+        for (var i = 0; i < widths.length; i++)
+            DOM_setAttribute(colElements[i],"width",widths[i]+"%");
+
+        Selection_update();
+    }
+
+    // public
+    Tables_getGeometry = function(itemId)
+    {
+        var element = document.getElementById(itemId);
+        if ((element == null) || (element.parentNode == null))
+            return null;
+
+        var structure = Tables_analyseStructure(element);
+
+        var result = new Object();
+
+        // Calculate the rect based on the cells, not the whole table element;
+        // we want to ignore the caption
+        var topLeftCell = Table_get(structure,0,0);
+        var bottomRightCell = Table_get(structure,structure.numRows-1,structure.numCols-1);
+
+        if (topLeftCell == null)
+            throw new Error("No top left cell");
+        if (bottomRightCell == null)
+            throw new Error("No bottom right cell");
+
+        var topLeftRect = topLeftCell.element.getBoundingClientRect();
+        var bottomRightRect = bottomRightCell.element.getBoundingClientRect();
+
+        var left = topLeftRect.left + window.scrollX;
+        var right = bottomRightRect.right + window.scrollX;
+        var top = topLeftRect.top + window.scrollY;
+        var bottom = bottomRightRect.bottom + window.scrollY;
+
+        result.contentRect = { x: left, y: top, width: right - left, height: bottom - top };
+        result.fullRect = xywhAbsElementRect(element);
+        result.parentRect = xywhAbsElementRect(element.parentNode);
+
+        result.columnWidths = Tables_getColWidths(structure);
+
+        var caption = firstChildOfType(element,HTML_CAPTION);
+        result.hasCaption = (caption != null);
+
+        return result;
+
+    }
+
+})();

http://git-wip-us.apache.org/repos/asf/incubator-corinthia/blob/9bf02bb2/experiments/editorFramework/src/Javascript_Layer_0/Text.js
----------------------------------------------------------------------
diff --git a/experiments/editorFramework/src/Javascript_Layer_0/Text.js b/experiments/editorFramework/src/Javascript_Layer_0/Text.js
new file mode 100644
index 0000000..5a69feb
--- /dev/null
+++ b/experiments/editorFramework/src/Javascript_Layer_0/Text.js
@@ -0,0 +1,543 @@
+// 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 Text_findParagraphBoundaries;
+var Text_analyseParagraph;
+var Text_posAbove;
+var Text_posBelow;
+var Text_closestPosBackwards;
+var Text_closestPosForwards;
+var Text_closestPosInDirection;
+
+var Paragraph_runFromOffset;
+var Paragraph_runFromNode;
+var Paragraph_positionAtOffset;
+var Paragraph_offsetAtPosition;
+var Paragraph_getRunRects;
+var Paragraph_getRunOrFallbackRects;
+
+var Text_toStartOfBoundary;
+var Text_toEndOfBoundary;
+
+(function() {
+
+    function Paragraph(node,startOffset,endOffset,runs,text)
+    {
+        this.node = node;
+        this.startOffset = startOffset;
+        this.endOffset = endOffset;
+        this.runs = runs;
+        this.text = text;
+
+        Object.defineProperty(this,"first",{
+            get: function() { throw new Error("Attempt to access first property of Position") },
+            set: function() {},
+            enumerable: true });
+        Object.defineProperty(this,"last",{
+            get: function() { throw new Error("Attempt to access last property of Position") },
+            set: function() {},
+            enumerable: true });
+    }
+
+    function Run(node,start,end)
+    {
+        this.node = node;
+        this.start = start;
+        this.end = end;
+    }
+
+    // In this code, we represent a paragraph by its first and last node. Normally, this will be
+    // the first and last child of a paragraph-level element (e.g. p or h1), but this scheme also
+    // represent a sequence of inline nodes between two paragraph or container nodes, e.g.
+    //
+    // <p>...</p> Some <i>inline</i> nodes <p>...</p>
+
+    Text_findParagraphBoundaries = function(pos)
+    {
+        Position_assertValid(pos);
+        var startOffset = pos.offset;
+        var endOffset = pos.offset;
+        var node = pos.node;
+
+        while (isInlineNode(node)) {
+            startOffset = DOM_nodeOffset(node);
+            endOffset = DOM_nodeOffset(node)+1;
+            node = node.parentNode;
+        }
+
+        if (node.nodeType != Node.ELEMENT_NODE)
+            throw new Error("Not an element node: "+nodeString(node));
+
+        while ((startOffset > 0) && isInlineNode(node.childNodes[startOffset-1]))
+            startOffset--;
+        while ((endOffset < node.childNodes.length) && isInlineNode(node.childNodes[endOffset]))
+            endOffset++;
+
+        return { node: node, startOffset: startOffset, endOffset: endOffset };
+    }
+
+    Text_analyseParagraph = function(pos)
+    {
+        var initial = pos.node;
+        var strings = new Array();
+        var runs = new Array();
+        var offset = 0;
+
+        var boundaries = Text_findParagraphBoundaries(pos);
+        if (boundaries == null)
+            return null;
+
+        for (var off = boundaries.startOffset; off < boundaries.endOffset; off++)
+            recurse(boundaries.node.childNodes[off]);
+
+        var text = strings.join("");
+
+        return new Paragraph(boundaries.node,boundaries.startOffset,boundaries.endOffset,runs,text);
+
+        function recurse(node)
+        {
+            if (node.nodeType == Node.TEXT_NODE) {
+                strings.push(node.nodeValue);
+                var start = offset;
+                var end = offset + node.nodeValue.length;
+                runs.push(new Run(node,start,end));
+                offset += node.nodeValue.length;
+            }
+            for (var child = node.firstChild; child != null; child = child.nextSibling)
+                recurse(child);
+        }
+    }
+
+    Text_posAbove = function(pos,cursorRect,cursorX)
+    {
+        if (cursorX == null)
+            cursorX = pos.targetX;
+        pos = Position_closestMatchBackwards(pos,Position_okForMovement);
+        if (cursorRect == null) {
+            cursorRect = Position_rectAtPos(pos);
+            if (cursorRect == null)
+                return null;
+        }
+
+        if (cursorX == null) {
+            cursorX = cursorRect.left;
+        }
+
+        while (true) {
+            pos = Position_closestMatchBackwards(pos,Position_okForMovement);
+            if (pos == null)
+                return null;
+
+            var paragraph = Text_analyseParagraph(pos);
+            if (paragraph == null)
+                return null;
+
+            var rects = Paragraph_getRunOrFallbackRects(paragraph,pos);
+
+            rects = rects.filter(function (rect) {
+                return (rect.bottom <= cursorRect.top);
+            });
+
+
+
+            var bottom = findLowestBottom(rects);
+
+            rects = rects.filter(function (rect) { return (rect.bottom == bottom); });
+
+            // Scroll previous line into view, if necessary
+            var top = findHighestTop(rects);
+            if (top < 0) {
+                var offset = -top;
+                window.scrollBy(0,-offset);
+                rects = offsetRects(rects,0,offset);
+            }
+
+            for (var i = 0; i < rects.length; i++) {
+                if ((cursorX >= rects[i].left) && (cursorX <= rects[i].right)) {
+                    var newPos = Position_atPoint(cursorX,rects[i].top + rects[i].height/2);
+                    if (newPos != null) {
+                        newPos = Position_closestMatchBackwards(newPos,Position_okForInsertion);
+                        newPos.targetX = cursorX;
+                        return newPos;
+                    }
+                }
+            }
+
+            var rightMost = findRightMostRect(rects);
+            if (rightMost != null) {
+                var newPos = Position_atPoint(rightMost.right,rightMost.top + rightMost.height/2);
+                if (newPos != null) {
+                    newPos = Position_closestMatchBackwards(newPos,Position_okForInsertion);
+                    newPos.targetX = cursorX;
+                    return newPos;
+                }
+            }
+
+
+            pos = new Position(paragraph.node,paragraph.startOffset);
+            pos = Position_prevMatch(pos,Position_okForMovement);
+        }
+    }
+
+    var findHighestTop = function(rects)
+    {
+        var top = null;
+        for (var i = 0; i < rects.length; i++) {
+            if ((top == null) || (top > rects[i].top))
+                top = rects[i].top;
+        }
+        return top;
+    }
+
+    var findLowestBottom = function(rects)
+    {
+        var bottom = null;
+        for (var i = 0; i < rects.length; i++) {
+            if ((bottom == null) || (bottom < rects[i].bottom))
+                bottom = rects[i].bottom;
+        }
+        return bottom;
+    }
+
+    var findRightMostRect = function(rects)
+    {
+        var rightMost = null;
+        for (var i = 0; i < rects.length; i++) {
+            if ((rightMost == null) || (rightMost.right < rects[i].right))
+                rightMost = rects[i];
+        }
+        return rightMost;
+    }
+
+    var offsetRects = function(rects,offsetX,offsetY)
+    {
+        var result = new Array();
+        for (var i = 0; i < rects.length; i++) {
+            result.push({ top: rects[i].top + offsetY,
+                          bottom: rects[i].bottom + offsetY,
+                          left: rects[i].left + offsetX,
+                          right: rects[i].right + offsetX,
+                          width: rects[i].width,
+                          height: rects[i].height });
+        }
+        return result;
+    }
+
+    Text_posBelow = function(pos,cursorRect,cursorX)
+    {
+        if (cursorX == null)
+            cursorX = pos.targetX;
+        pos = Position_closestMatchForwards(pos,Position_okForMovement);
+        if (cursorRect == null) {
+            cursorRect = Position_rectAtPos(pos);
+            if (cursorRect == null)
+                return null;
+        }
+
+        if (cursorX == null) {
+            cursorX = cursorRect.left;
+        }
+
+
+        while (true) {
+            pos = Position_closestMatchForwards(pos,Position_okForMovement);
+            if (pos == null)
+                return null;
+
+            var paragraph = Text_analyseParagraph(pos);
+            if (paragraph == null)
+                return null;
+
+            var rects = Paragraph_getRunOrFallbackRects(paragraph,pos);
+
+            rects = rects.filter(function (rect) {
+                return (rect.top >= cursorRect.bottom);
+            });
+
+            var top = findHighestTop(rects);
+
+            rects = rects.filter(function (rect) { return (rect.top == top); });
+
+            // Scroll next line into view, if necessary
+            var bottom = findLowestBottom(rects);
+            if (bottom > window.innerHeight) {
+                var offset = window.innerHeight - bottom;
+                window.scrollBy(0,-offset);
+                rects = offsetRects(rects,0,offset);
+            }
+
+            for (var i = 0; i < rects.length; i++) {
+                if ((cursorX >= rects[i].left) && (cursorX <= rects[i].right)) {
+                    var newPos = Position_atPoint(cursorX,rects[i].top + rects[i].height/2);
+                    if (newPos != null) {
+                        newPos = Position_closestMatchForwards(newPos,Position_okForInsertion);
+                        newPos.targetX = cursorX;
+                        return newPos;
+                    }
+                }
+            }
+
+            var rightMost = findRightMostRect(rects);
+            if (rightMost != null) {
+                var newPos = Position_atPoint(rightMost.right,rightMost.top + rightMost.height/2);
+                if (newPos != null) {
+                    newPos = Position_closestMatchForwards(newPos,Position_okForInsertion);
+                    newPos.targetX = cursorX;
+                    return newPos;
+                }
+            }
+
+            pos = new Position(paragraph.node,paragraph.endOffset);
+            pos = Position_nextMatch(pos,Position_okForMovement);
+        }
+    }
+
+    Text_closestPosBackwards = function(pos)
+    {
+        if (isNonWhitespaceTextNode(pos.node))
+            return pos;
+        var node;
+        if ((pos.node.nodeType == Node.ELEMENT_NODE) && (pos.offset > 0)) {
+            node = pos.node.childNodes[pos.offset-1];
+            while (node.lastChild != null)
+                node = node.lastChild;
+        }
+        else {
+            node = pos.node;
+        }
+        while ((node != null) && (node != document.body) && !isNonWhitespaceTextNode(node))
+            node = prevNode(node);
+
+        if ((node == null) || (node == document.body))
+            return null;
+        else
+            return new Position(node,node.nodeValue.length);
+    }
+
+    Text_closestPosForwards = function(pos)
+    {
+        if (isNonWhitespaceTextNode(pos.node))
+            return pos;
+        var node;
+        if ((pos.node.nodeType == Node.ELEMENT_NODE) && (pos.offset < pos.node.childNodes.length)) {
+            node = pos.node.childNodes[pos.offset];
+            while (node.firstChild != null)
+                node = node.firstChild;
+        }
+        else {
+            node = nextNodeAfter(pos.node);
+        }
+        while ((node != null) && !isNonWhitespaceTextNode(node)) {
+            var old = nodeString(node);
+            node = nextNode(node);
+        }
+
+        if (node == null)
+            return null;
+        else
+            return new Position(node,0);
+    }
+
+    Text_closestPosInDirection = function(pos,direction)
+    {
+        if ((direction == "forward") ||
+            (direction == "right") ||
+            (direction == "down")) {
+            return Text_closestPosForwards(pos);
+        }
+        else {
+            return Text_closestPosBackwards(pos);
+        }
+    }
+
+    Paragraph_runFromOffset = function(paragraph,offset,end)
+    {
+        if (paragraph.runs.length == 0)
+            throw new Error("Paragraph has no runs");
+        if (!end) {
+
+            for (var i = 0; i < paragraph.runs.length; i++) {
+                var run = paragraph.runs[i];
+                if ((offset >= run.start) && (offset < run.end))
+                    return run;
+                if ((i == paragraph.runs.length-1) && (offset == run.end))
+                    return run;
+            }
+
+        }
+        else {
+
+            for (var i = 0; i < paragraph.runs.length; i++) {
+                var run = paragraph.runs[i];
+                if ((offset > run.start) && (offset <= run.end))
+                    return run;
+                if ((i == 0) && (offset == 0))
+                    return run;
+            }
+
+        }
+    }
+
+    Paragraph_runFromNode = function(paragraph,node)
+    {
+        for (var i = 0; i < paragraph.runs.length; i++) {
+            if (paragraph.runs[i].node == node)
+                return paragraph.runs[i];
+        }
+        throw new Error("Run for text node not found");
+    }
+
+    Paragraph_positionAtOffset = function(paragraph,offset,end)
+    {
+        var run = Paragraph_runFromOffset(paragraph,offset,end);
+        if (run == null)
+            throw new Error("Run at offset "+offset+" not found");
+        return new Position(run.node,offset-run.start);
+    }
+
+    Paragraph_offsetAtPosition = function(paragraph,pos)
+    {
+        var run = Paragraph_runFromNode(paragraph,pos.node);
+        return run.start + pos.offset;
+    }
+
+    Paragraph_getRunRects = function(paragraph)
+    {
+        var rects = new Array();
+        for (var i = 0; i < paragraph.runs.length; i++) {
+            var run = paragraph.runs[i];
+            var runRange = new Range(run.node,0,run.node,run.node.nodeValue.length);
+            var runRects = Range_getClientRects(runRange);
+            Array.prototype.push.apply(rects,runRects);
+        }
+        return rects;
+    }
+
+    Paragraph_getRunOrFallbackRects = function(paragraph,pos)
+    {
+        var rects = Paragraph_getRunRects(paragraph);
+        if ((rects.length == 0) && (paragraph.node.nodeType == Node.ELEMENT_NODE)) {
+            if (isBlockNode(paragraph.node) &&
+                (paragraph.startOffset == 0) &&
+                (paragraph.endOffset == paragraph.node.childNodes.length)) {
+                rects = [paragraph.node.getBoundingClientRect()];
+            }
+            else {
+                var beforeNode = paragraph.node.childNodes[paragraph.startOffset-1];
+                var afterNode = paragraph.node.childNodes[paragraph.endOffset];
+                if ((afterNode != null) && isBlockNode(afterNode)) {
+                    rects = [afterNode.getBoundingClientRect()];
+                }
+                else if ((beforeNode != null) && isBlockNode(beforeNode)) {
+                    rects = [beforeNode.getBoundingClientRect()];
+                }
+            }
+        }
+        return rects;
+    }
+
+    function toStartOfParagraph(pos)
+    {
+        pos = Position_closestMatchBackwards(pos,Position_okForMovement);
+        if (pos == null)
+            return null;
+        var paragraph = Text_analyseParagraph(pos);
+        if (paragraph == null)
+            return null;
+
+        var newPos = new Position(paragraph.node,paragraph.startOffset);
+        return Position_closestMatchForwards(newPos,Position_okForMovement);
+    }
+
+    function toEndOfParagraph(pos)
+    {
+        pos = Position_closestMatchForwards(pos,Position_okForMovement);
+        if (pos == null)
+            return null;
+        var paragraph = Text_analyseParagraph(pos);
+        if (paragraph == null)
+            return null;
+
+        var newPos = new Position(paragraph.node,paragraph.endOffset);
+        return Position_closestMatchBackwards(newPos,Position_okForMovement);
+    }
+
+    function toStartOfLine(pos)
+    {
+        var posRect = Position_rectAtPos(pos);
+        if (posRect == null) {
+            pos = Text_closestPosBackwards(pos);
+            posRect = Position_rectAtPos(pos);
+            if (posRect == null) {
+                return null;
+            }
+        }
+
+        while (true) {
+            var check = Position_prevMatch(pos,Position_okForMovement);
+            var checkRect = Position_rectAtPos(check); // handles check == null case
+            if (checkRect == null)
+                return pos;
+            if ((checkRect.bottom <= posRect.top) || (checkRect.top >= posRect.bottom))
+                return pos;
+            pos = check;
+        }
+    }
+
+    function toEndOfLine(pos)
+    {
+        var posRect = Position_rectAtPos(pos);
+        if (posRect == null) {
+            pos = Text_closestPosForwards(pos);
+            posRect = Position_rectAtPos(pos);
+            if (posRect == null) {
+                return null;
+            }
+        }
+
+        while (true) {
+            var check = Position_nextMatch(pos,Position_okForMovement);
+            var checkRect = Position_rectAtPos(check); // handles check == null case
+            if (checkRect == null)
+                return pos;
+            if ((checkRect.bottom <= posRect.top) || (checkRect.top >= posRect.bottom))
+                return pos;
+            pos = check;
+        }
+    }
+
+    Text_toStartOfBoundary = function(pos,boundary)
+    {
+        if (boundary == "paragraph")
+            return toStartOfParagraph(pos);
+        else if (boundary == "line")
+            return toStartOfLine(pos);
+        else
+            throw new Error("Unsupported boundary: "+boundary);
+    }
+
+    Text_toEndOfBoundary = function(pos,boundary)
+    {
+        if (boundary == "paragraph")
+            return toEndOfParagraph(pos);
+        else if (boundary == "line")
+            return toEndOfLine(pos);
+        else
+            throw new Error("Unsupported boundary: "+boundary);
+    }
+
+})();

http://git-wip-us.apache.org/repos/asf/incubator-corinthia/blob/9bf02bb2/experiments/editorFramework/src/Javascript_Layer_0/UndoManager.js
----------------------------------------------------------------------
diff --git a/experiments/editorFramework/src/Javascript_Layer_0/UndoManager.js b/experiments/editorFramework/src/Javascript_Layer_0/UndoManager.js
new file mode 100644
index 0000000..9f63c43
--- /dev/null
+++ b/experiments/editorFramework/src/Javascript_Layer_0/UndoManager.js
@@ -0,0 +1,270 @@
+// 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.
+
+// FIXME: place a limit on the number of undo steps recorded - say, 30-50?
+
+var UndoManager_getLength;
+var UndoManager_getIndex;
+var UndoManager_setIndex;
+var UndoManager_print;
+var UndoManager_undo;
+var UndoManager_redo;
+var UndoManager_addAction;
+var UndoManager_newGroup;
+var UndoManager_groupType;
+var UndoManager_disableWhileExecuting;
+var UndoManager_isActive;
+var UndoManager_isDisabled;
+var UndoManager_clear;
+var UndoManager_setProperty;
+var UndoManager_deleteProperty;
+
+(function() {
+
+    var UNDO_LIMIT = 50;
+
+    function UndoGroup(type,onClose)
+    {
+        this.type = type;
+        this.onClose = onClose;
+        this.actions = new Array();
+    }
+
+    function UndoAction(fun,args)
+    {
+        this.fun = fun;
+        this.args = args;
+    }
+
+    UndoAction.prototype.toString = function()
+    {
+        var name;
+        if (this.fun.wrappedName != null)
+            name = this.fun.wrappedName;
+        else
+            name = this.fun.name;
+
+        var argStrings = new Array();
+        for (var i = 0; i < this.args.length; i++) {
+            if (this.args[i] instanceof Node)
+                argStrings.push(nodeString(this.args[i]));
+            else if (this.args[i] == null)
+                argStrings.push("null");
+            else
+                argStrings.push(this.args[i].toString());
+        }
+
+        return name + "(" + argStrings.join(",") + ")";
+    }
+
+    var undoStack = new Array();
+    var redoStack = new Array();
+    var inUndo = false;
+    var inRedo = false;
+    var currentGroup = null;
+    var disabled = 0;
+
+    // public
+    UndoManager_getLength = function()
+    {
+        return undoStack.length + redoStack.length;
+    }
+
+    // public
+    UndoManager_getIndex = function()
+    {
+        return undoStack.length;
+    }
+
+    // public
+    UndoManager_setIndex = function(index)
+    {
+        while (undoStack.length > index)
+            UndoManager_undo();
+        while (undoStack.length < index)
+            UndoManager_redo();
+    }
+
+    // public
+    UndoManager_print = function()
+    {
+        debug("");
+        debug("--------------------------------------------------------------------");
+        debug("Undo stack:");
+        for (var groupIndex = 0; groupIndex < undoStack.length; groupIndex++) {
+            var group = undoStack[groupIndex];
+            debug("    "+group.type);
+            for (var actionIndex = 0; actionIndex < group.actions.length; actionIndex++) {
+                var action = group.actions[actionIndex];
+                debug("        "+action);
+            }
+        }
+        debug("Redo stack:");
+        for (var groupIndex = 0; groupIndex < redoStack.length; groupIndex++) {
+            var group = redoStack[groupIndex];
+            debug("    "+group.type);
+            for (var actionIndex = 0; actionIndex < group.actions.length; actionIndex++) {
+                var action = group.actions[actionIndex];
+                debug("        "+action);
+            }
+        }
+        debug("Current group = "+currentGroup);
+        debug("--------------------------------------------------------------------");
+        debug("");
+    }
+
+    function closeCurrentGroup()
+    {
+        if ((currentGroup != null) && (currentGroup.onClose != null))
+            currentGroup.onClose();
+        currentGroup = null;
+    }
+
+    // public
+    UndoManager_undo = function()
+    {
+        closeCurrentGroup();
+        if (undoStack.length > 0) {
+            var group = undoStack.pop();
+            inUndo = true;
+            for (var i = group.actions.length-1; i >= 0; i--)
+                group.actions[i].fun.apply(null,group.actions[i].args);
+            inUndo = false;
+        }
+        closeCurrentGroup();
+    }
+
+    // public
+    UndoManager_redo = function()
+    {
+        closeCurrentGroup();
+        if (redoStack.length > 0) {
+            var group = redoStack.pop();
+            inRedo = true;
+            for (var i = group.actions.length-1; i >= 0; i--)
+                group.actions[i].fun.apply(null,group.actions[i].args);
+            inRedo = false;
+        }
+        closeCurrentGroup();
+    }
+
+    // public
+    UndoManager_addAction = function(fun)
+    {
+        if (disabled > 0)
+            return;
+
+        // remaining parameters after fun are arguments to be supplied to fun
+        var args = new Array();
+        for (var i = 1; i < arguments.length; i++)
+            args.push(arguments[i]);
+
+        if (!inUndo && !inRedo && (redoStack.length > 0))
+            redoStack.length = 0;
+
+        var stack = inUndo ? redoStack : undoStack;
+        if (currentGroup == null)
+            UndoManager_newGroup(null);
+
+        // Only add a group to the undo stack one it has at least one action, to avoid having
+        // empty groups present.
+        if (currentGroup.actions.length == 0) {
+            if (!inUndo && !inRedo && (stack.length == UNDO_LIMIT))
+                stack.shift();
+            stack.push(currentGroup);
+        }
+
+        currentGroup.actions.push(new UndoAction(fun,args));
+    }
+
+    // public
+    UndoManager_newGroup = function(type,onClose)
+    {
+        if (disabled > 0)
+            return;
+
+        closeCurrentGroup();
+
+        // We don't actually add the group to the undo stack until the first request to add an
+        // action to it. This way we don't end up with empty groups in the undo stack, which
+        // simplifies logic for moving back and forward through the undo history.
+
+        if ((type == null) || (type == ""))
+            type = "Anonymous";
+        currentGroup = new UndoGroup(type,onClose);
+    }
+
+    // public
+    UndoManager_groupType = function()
+    {
+        if (undoStack.length > 0)
+            return undoStack[undoStack.length-1].type;
+        else
+            return null;
+    }
+
+    UndoManager_disableWhileExecuting = function(fun) {
+        disabled++;
+        try {
+            return fun();
+        }
+        finally {
+            disabled--;
+        }
+    }
+
+    UndoManager_isActive = function()
+    {
+        return (inUndo || inRedo);
+    }
+
+    UndoManager_isDisabled = function() {
+        return (disabled > 0);
+    }
+
+    UndoManager_clear = function() {
+        undoStack.length = 0;
+        redoStack.length = 0;
+    }
+
+    function saveProperty(obj,name)
+    {
+        if (obj.hasOwnProperty(name))
+            UndoManager_addAction(UndoManager_setProperty,obj,name,obj[name]);
+        else
+            UndoManager_addAction(UndoManager_deleteProperty,obj,name);
+    }
+
+    UndoManager_setProperty = function(obj,name,value)
+    {
+        if (obj.hasOwnProperty(name) && (obj[name] == value))
+            return; // no point in adding an undo action
+        saveProperty(obj,name);
+        obj[name] = value;
+    }
+
+    UndoManager_deleteProperty = function(obj,name)
+    {
+        if (!obj.hasOwnProperty(name))
+            return; // no point in adding an undo action
+        saveProperty(obj,name);
+        delete obj[name];
+    }
+
+})();
+
+window.undoSupported = true;

http://git-wip-us.apache.org/repos/asf/incubator-corinthia/blob/9bf02bb2/experiments/editorFramework/src/Javascript_Layer_0/Viewport.js
----------------------------------------------------------------------
diff --git a/experiments/editorFramework/src/Javascript_Layer_0/Viewport.js b/experiments/editorFramework/src/Javascript_Layer_0/Viewport.js
new file mode 100644
index 0000000..47fbdfd
--- /dev/null
+++ b/experiments/editorFramework/src/Javascript_Layer_0/Viewport.js
@@ -0,0 +1,80 @@
+// 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 Viewport_init;
+var Viewport_setViewportWidth;
+var Viewport_setTextScale;
+
+(function() {
+
+    var viewportMetaElement = null;
+
+    // public
+    Viewport_init = function(width,textScale)
+    {
+        var head = DOM_documentHead(document);
+        for (var child = head.firstChild; child != null; child = child.nextSibling) {
+            if ((child._type == HTML_META) && (child.getAttribute("name") == "viewport")) {
+                viewportMetaElement = child;
+                break;
+            }
+        }
+
+        if (viewportMetaElement == null) {
+            viewportMetaElement = DOM_createElement(document,"META");
+            DOM_setAttribute(viewportMetaElement,"name","viewport");
+            DOM_appendChild(head,viewportMetaElement);
+        }
+
+        if (width != 0) {
+            // Only set the width and text scale if they are not already set, to avoid triggering
+            // an extra layout at load time
+            var contentValue = "width = "+width+", user-scalable = no";
+            if (viewportMetaElement.getAttribute("content") != contentValue)
+                DOM_setAttribute(viewportMetaElement,"content",contentValue);
+        }
+
+        if (textScale != 0) {
+            var pct = textScale+"%";
+            if (document.documentElement.style.getPropertyValue("-webkit-text-size-adjust") != pct)
+                DOM_setStyleProperties(document.documentElement,{"-webkit-text-size-adjust": pct});
+        }
+    }
+
+    // public
+    Viewport_setViewportWidth = function(width)
+    {
+        var contentValue = "width = "+width+", user-scalable = no";
+        if (viewportMetaElement.getAttribute("content") != contentValue)
+            DOM_setAttribute(viewportMetaElement,"content",contentValue);
+
+        Selection_update();
+        Cursor_ensureCursorVisible();
+    }
+
+    // public
+    Viewport_setTextScale = function(textScale)
+    {
+        var pct = textScale+"%";
+        if (document.documentElement.style.getPropertyValue("-webkit-text-size-adjust") != pct)
+            DOM_setStyleProperties(document.documentElement,{"-webkit-text-size-adjust": pct});
+
+        Selection_update();
+        Cursor_ensureCursorVisible();
+    }
+
+})();

http://git-wip-us.apache.org/repos/asf/incubator-corinthia/blob/9bf02bb2/experiments/editorFramework/src/Javascript_Layer_0/check-dom-methods.sh
----------------------------------------------------------------------
diff --git a/experiments/editorFramework/src/Javascript_Layer_0/check-dom-methods.sh b/experiments/editorFramework/src/Javascript_Layer_0/check-dom-methods.sh
new file mode 100644
index 0000000..6e17a4e
--- /dev/null
+++ b/experiments/editorFramework/src/Javascript_Layer_0/check-dom-methods.sh
@@ -0,0 +1,15 @@
+#!/bin/bash
+jsgrep -F '.createElement' | grep -vF '// check-ok'
+jsgrep -F '.createTextNode' | grep -vF '// check-ok'
+jsgrep -F '.createComment' | grep -vF '// check-ok'
+jsgrep -F '.appendChild' | grep -vF '// check-ok'
+jsgrep -F '.insertBefore' | grep -vF '// check-ok'
+jsgrep -F '.removeChild' | grep -vF '// check-ok'
+jsgrep -F '.cloneNode' | grep -vF '// check-ok'
+jsgrep -F '.nodeName' | grep -vE '(dtdsource/|tests/|treevis/)' | grep -vF '// check-ok'
+jsgrep -F '.setAttribute' | grep -vE '(dtdsource/|treevis/|docx/)' | grep -vF '// check-ok'
+jsgrep -F '.removeAttribute' | grep -vE '(dtdsource/|treevis/|docx/)' | grep -vF '// check-ok'
+jsgrep -F '.setProperty' | grep -vE '(dtdsource/|treevis/)' | grep -vF '// check-ok'
+jsgrep -F '.removeProperty' | grep -vE '(dtdsource/|treevis/)' | grep -vF '// check-ok'
+jsgrep -E '\.style\[.* = ' | grep -vE '(treevis/|docx/)' | grep -vF '// check-ok'
+jsgrep -E '\.style\..* = ' | grep -vE '(treevis/|docx/)' | grep -vF '// check-ok'


Mime
View raw message