diff --git a/src/main/java/com/metaweb/gridworks/expr/functions/strings/Partition.java b/src/main/java/com/metaweb/gridworks/expr/functions/strings/Partition.java index 0bf7996ac..d9cf93e47 100644 --- a/src/main/java/com/metaweb/gridworks/expr/functions/strings/Partition.java +++ b/src/main/java/com/metaweb/gridworks/expr/functions/strings/Partition.java @@ -1,6 +1,8 @@ package com.metaweb.gridworks.expr.functions.strings; import java.util.Properties; +import java.util.regex.Matcher; +import java.util.regex.Pattern; import org.json.JSONException; import org.json.JSONWriter; @@ -10,22 +12,53 @@ import com.metaweb.gridworks.gel.Function; public class Partition implements Function { public Object call(Properties bindings, Object[] args) { - if (args.length == 2) { + if (args.length >= 2 && args.length <= 3) { Object o1 = args[0]; Object o2 = args[1]; - if (o1 != null && o2 != null && o1 instanceof String && o2 instanceof String) { + + boolean omitFragment = false; + if (args.length == 3) { + Object o3 = args[2]; + if (o3 instanceof Boolean) { + omitFragment = ((Boolean) o3).booleanValue(); + } + } + + if (o1 != null && o2 != null && o1 instanceof String) { String s = (String) o1; - String frag = (String) o2; - int index = s.indexOf(frag); - String[] output = new String[3]; - if (index > -1) { - output[0] = s.substring(0, index); - output[1] = frag; - output[2] = s.substring(index + frag.length(), s.length()); + + int from = -1; + int to = -1; + + if (o2 instanceof String) { + String frag = (String) o2; + + from = s.indexOf(frag); + to = from + frag.length(); + } else if (o2 instanceof Pattern) { + Pattern pattern = (Pattern) o2; + Matcher matcher = pattern.matcher(s); + if (matcher.find()) { + from = matcher.start(); + to = matcher.end(); + } + } + + String[] output = omitFragment ? new String[2] : new String[3]; + if (from > -1) { + output[0] = s.substring(0, from); + if (omitFragment) { + output[1] = s.substring(to); + } else { + output[1] = s.substring(from, to); + output[2] = s.substring(to); + } } else { output[0] = s; output[1] = ""; - output[2] = ""; + if (!omitFragment) { + output[2] = ""; + } } return output; } @@ -37,8 +70,9 @@ public class Partition implements Function { throws JSONException { writer.object(); - writer.key("description"); writer.value("Returns an array of strings [a,frag,b] where a is the string part before the first occurrence of frag in s and b is what's left."); - writer.key("params"); writer.value("string s, string frag"); + writer.key("description"); writer.value( + "Returns an array of strings [a,frag,b] where a is the string part before the first occurrence of frag in s and b is what's left. If omitFragment is true, frag is not returned."); + writer.key("params"); writer.value("string s, string or regex frag, optional boolean omitFragment"); writer.key("returns"); writer.value("array"); writer.endObject(); } diff --git a/src/main/java/com/metaweb/gridworks/expr/functions/strings/RPartition.java b/src/main/java/com/metaweb/gridworks/expr/functions/strings/RPartition.java index 15543f887..3dfadaeff 100644 --- a/src/main/java/com/metaweb/gridworks/expr/functions/strings/RPartition.java +++ b/src/main/java/com/metaweb/gridworks/expr/functions/strings/RPartition.java @@ -1,6 +1,8 @@ package com.metaweb.gridworks.expr.functions.strings; import java.util.Properties; +import java.util.regex.Matcher; +import java.util.regex.Pattern; import org.json.JSONException; import org.json.JSONWriter; @@ -10,22 +12,54 @@ import com.metaweb.gridworks.gel.Function; public class RPartition implements Function { public Object call(Properties bindings, Object[] args) { - if (args.length == 2) { + if (args.length >= 2 && args.length <= 3) { Object o1 = args[0]; Object o2 = args[1]; - if (o1 != null && o2 != null && o1 instanceof String && o2 instanceof String) { + + boolean omitFragment = false; + if (args.length == 3) { + Object o3 = args[2]; + if (o3 instanceof Boolean) { + omitFragment = ((Boolean) o3).booleanValue(); + } + } + + if (o1 != null && o2 != null && o1 instanceof String) { String s = (String) o1; - String frag = (String) o2; - int index = s.lastIndexOf(frag); - String[] output = new String[3]; - if (index > -1) { - output[0] = s.substring(0, index); - output[1] = frag; - output[2] = s.substring(index + frag.length(), s.length()); + + int from = -1; + int to = -1; + + if (o2 instanceof String) { + String frag = (String) o2; + + from = s.lastIndexOf(frag); + to = from + frag.length(); + } else if (o2 instanceof Pattern) { + Pattern pattern = (Pattern) o2; + Matcher matcher = pattern.matcher(s); + + while (matcher.find()) { + from = matcher.start(); + to = matcher.end(); + } + } + + String[] output = omitFragment ? new String[2] : new String[3]; + if (from > -1) { + output[0] = s.substring(0, from); + if (omitFragment) { + output[1] = s.substring(to); + } else { + output[1] = s.substring(from, to); + output[2] = s.substring(to); + } } else { output[0] = s; output[1] = ""; - output[2] = ""; + if (!omitFragment) { + output[2] = ""; + } } return output; } @@ -37,8 +71,9 @@ public class RPartition implements Function { throws JSONException { writer.object(); - writer.key("description"); writer.value("Returns an array of strings [a,frag,b] where a is the string part before the last occurrence of frag in s and b is what's left."); - writer.key("params"); writer.value("string s, string frag"); + writer.key("description"); writer.value( + "Returns an array of strings [a,frag,b] where a is the string part before the last occurrence of frag in s and b is what's left. If omitFragment is true, frag is not returned."); + writer.key("params"); writer.value("string s, string or regex frag, optional boolean omitFragment"); writer.key("returns"); writer.value("array"); writer.endObject(); } diff --git a/src/main/java/com/metaweb/gridworks/expr/functions/strings/Replace.java b/src/main/java/com/metaweb/gridworks/expr/functions/strings/Replace.java index d3be5d38b..10ac2e2f1 100644 --- a/src/main/java/com/metaweb/gridworks/expr/functions/strings/Replace.java +++ b/src/main/java/com/metaweb/gridworks/expr/functions/strings/Replace.java @@ -1,6 +1,7 @@ package com.metaweb.gridworks.expr.functions.strings; import java.util.Properties; +import java.util.regex.Pattern; import org.json.JSONException; import org.json.JSONWriter; @@ -16,12 +17,18 @@ public class Replace implements Function { Object o1 = args[0]; Object o2 = args[1]; Object o3 = args[2]; - if (o1 != null && o2 != null && o3 != null && o2 instanceof String && o3 instanceof String) { + if (o1 != null && o2 != null && o3 != null && o3 instanceof String) { String str = (o1 instanceof String) ? (String) o1 : o1.toString(); - return str.replace((String) o2, (String) o3); + + if (o2 instanceof String) { + return str.replace((String) o2, (String) o3); + } else if (o2 instanceof Pattern) { + Pattern pattern = (Pattern) o2; + return pattern.matcher(str).replaceAll((String) o3); + } } } - return new EvalError(ControlFunctionRegistry.getFunctionName(this) + " expects 3 strings"); + return new EvalError(ControlFunctionRegistry.getFunctionName(this) + " expects 3 strings, or 1 string, 1 regex, and 1 string"); } @@ -30,7 +37,7 @@ public class Replace implements Function { writer.object(); writer.key("description"); writer.value("Returns the string obtained by replacing f with r in s"); - writer.key("params"); writer.value("string s, string f, string r"); + writer.key("params"); writer.value("string s, string or regex f, string r"); writer.key("returns"); writer.value("string"); writer.endObject(); } diff --git a/src/main/java/com/metaweb/gridworks/expr/functions/strings/ReplaceRegexp.java b/src/main/java/com/metaweb/gridworks/expr/functions/strings/ReplaceRegexp.java deleted file mode 100644 index e3a501c50..000000000 --- a/src/main/java/com/metaweb/gridworks/expr/functions/strings/ReplaceRegexp.java +++ /dev/null @@ -1,37 +0,0 @@ -package com.metaweb.gridworks.expr.functions.strings; - -import java.util.Properties; - -import org.json.JSONException; -import org.json.JSONWriter; - -import com.metaweb.gridworks.expr.EvalError; -import com.metaweb.gridworks.gel.ControlFunctionRegistry; -import com.metaweb.gridworks.gel.Function; - -public class ReplaceRegexp implements Function { - - public Object call(Properties bindings, Object[] args) { - if (args.length == 3) { - Object o1 = args[0]; - Object o2 = args[1]; - Object o3 = args[2]; - if (o1 != null && o2 != null && o3 != null && o2 instanceof String && o3 instanceof String) { - String str = (o1 instanceof String) ? (String) o1 : o1.toString(); - return str.replaceAll((String) o2, (String) o3); - } - } - return new EvalError(ControlFunctionRegistry.getFunctionName(this) + " expects 3 strings"); - } - - - public void write(JSONWriter writer, Properties options) - throws JSONException { - - writer.object(); - writer.key("description"); writer.value("Returns the string obtained by replacing f with r in s"); - writer.key("params"); writer.value("string s, string f, string r"); - writer.key("returns"); writer.value("string"); - writer.endObject(); - } -} diff --git a/src/main/java/com/metaweb/gridworks/expr/functions/strings/Split.java b/src/main/java/com/metaweb/gridworks/expr/functions/strings/Split.java index 12731e65a..0f0275e37 100644 --- a/src/main/java/com/metaweb/gridworks/expr/functions/strings/Split.java +++ b/src/main/java/com/metaweb/gridworks/expr/functions/strings/Split.java @@ -1,6 +1,7 @@ package com.metaweb.gridworks.expr.functions.strings; import java.util.Properties; +import java.util.regex.Pattern; import org.json.JSONException; import org.json.JSONWriter; @@ -15,11 +16,17 @@ public class Split implements Function { if (args.length == 2) { Object v = args[0]; Object split = args[1]; - if (v != null && split != null && split instanceof String) { - return (v instanceof String ? (String) v : v.toString()).split((String) split); + if (v != null && split != null) { + String str = (v instanceof String ? (String) v : v.toString()); + if (split instanceof String) { + return str.split((String) split); + } else if (split instanceof Pattern) { + Pattern pattern = (Pattern) split; + return pattern.split(str); + } } } - return new EvalError(ControlFunctionRegistry.getFunctionName(this) + " expects 2 strings"); + return new EvalError(ControlFunctionRegistry.getFunctionName(this) + " expects 2 strings, or 1 string and 1 regex"); } public void write(JSONWriter writer, Properties options) @@ -27,7 +34,7 @@ public class Split implements Function { writer.object(); writer.key("description"); writer.value("Returns the array of strings obtained by splitting s with separator sep"); - writer.key("params"); writer.value("string s, string sep"); + writer.key("params"); writer.value("string s, string or regex sep"); writer.key("returns"); writer.value("array"); writer.endObject(); } diff --git a/src/main/java/com/metaweb/gridworks/gel/ControlFunctionRegistry.java b/src/main/java/com/metaweb/gridworks/gel/ControlFunctionRegistry.java index 65d57cdda..2242a26e6 100644 --- a/src/main/java/com/metaweb/gridworks/gel/ControlFunctionRegistry.java +++ b/src/main/java/com/metaweb/gridworks/gel/ControlFunctionRegistry.java @@ -43,7 +43,6 @@ import com.metaweb.gridworks.expr.functions.strings.RPartition; import com.metaweb.gridworks.expr.functions.strings.Reinterpret; import com.metaweb.gridworks.expr.functions.strings.Replace; import com.metaweb.gridworks.expr.functions.strings.ReplaceChars; -import com.metaweb.gridworks.expr.functions.strings.ReplaceRegexp; import com.metaweb.gridworks.expr.functions.strings.SHA1; import com.metaweb.gridworks.expr.functions.strings.Split; import com.metaweb.gridworks.expr.functions.strings.SplitByCharType; @@ -117,7 +116,6 @@ public class ControlFunctionRegistry { registerFunction("slice", new Slice()); registerFunction("substring", new Slice()); registerFunction("replace", new Replace()); - registerFunction("replaceRegexp", new ReplaceRegexp()); registerFunction("replaceChars", new ReplaceChars()); registerFunction("split", new Split()); registerFunction("splitByCharType", new SplitByCharType()); diff --git a/src/main/java/com/metaweb/gridworks/gel/Parser.java b/src/main/java/com/metaweb/gridworks/gel/Parser.java index 387c20160..aa6dccdd9 100644 --- a/src/main/java/com/metaweb/gridworks/gel/Parser.java +++ b/src/main/java/com/metaweb/gridworks/gel/Parser.java @@ -2,10 +2,12 @@ package com.metaweb.gridworks.gel; import java.util.LinkedList; import java.util.List; +import java.util.regex.Pattern; import com.metaweb.gridworks.expr.Evaluable; import com.metaweb.gridworks.expr.ParsingException; import com.metaweb.gridworks.gel.Scanner.NumberToken; +import com.metaweb.gridworks.gel.Scanner.RegexToken; import com.metaweb.gridworks.gel.Scanner.Token; import com.metaweb.gridworks.gel.Scanner.TokenType; import com.metaweb.gridworks.gel.ast.ControlCallExpr; @@ -26,7 +28,7 @@ public class Parser { public Parser(String s, int from, int to) throws ParsingException { _scanner = new Scanner(s, from, to); - _token = _scanner.next(); + _token = _scanner.next(true); _root = parseExpression(); } @@ -35,8 +37,8 @@ public class Parser { return _root; } - protected void next() { - _token = _scanner.next(); + protected void next(boolean regexPossible) { + _token = _scanner.next(regexPossible); } protected ParsingException makeException(String desc) { @@ -54,7 +56,7 @@ public class Parser { String op = _token.text; - next(); + next(true); Evaluable sub2 = parseSubExpression(); @@ -73,7 +75,7 @@ public class Parser { String op = _token.text; - next(); + next(true); Evaluable sub2 = parseSubExpression(); @@ -92,7 +94,7 @@ public class Parser { String op = _token.text; - next(); + next(true); Evaluable factor2 = parseFactor(); @@ -111,22 +113,27 @@ public class Parser { if (_token.type == TokenType.String) { eval = new LiteralExpr(_token.text); - next(); + next(false); + } else if (_token.type == TokenType.Regex) { + RegexToken t = (RegexToken) _token; + + eval = new LiteralExpr(Pattern.compile(_token.text, t.caseInsensitive ? Pattern.CASE_INSENSITIVE : 0)); + next(false); } else if (_token.type == TokenType.Number) { eval = new LiteralExpr(((NumberToken)_token).value); - next(); + next(false); } else if (_token.type == TokenType.Operator && _token.text.equals("-")) { // unary minus? - next(); + next(true); if (_token != null && _token.type == TokenType.Number) { eval = new LiteralExpr(-((NumberToken)_token).value); - next(); + next(false); } else { throw makeException("Bad negative number"); } } else if (_token.type == TokenType.Identifier) { String text = _token.text; - next(); + next(false); if (_token == null || _token.type != TokenType.Delimiter || !_token.text.equals("(")) { eval = new VariableExpr(text); @@ -137,7 +144,7 @@ public class Parser { throw makeException("Unknown function or control named " + text); } - next(); // swallow ( + next(true); // swallow ( List args = parseExpressionList(")"); @@ -153,12 +160,12 @@ public class Parser { } } } else if (_token.type == TokenType.Delimiter && _token.text.equals("(")) { - next(); + next(true); eval = parseExpression(); if (_token != null && _token.type == TokenType.Delimiter && _token.text.equals(")")) { - next(); + next(false); } else { throw makeException("Missing )"); } @@ -168,17 +175,17 @@ public class Parser { while (_token != null) { if (_token.type == TokenType.Operator && _token.text.equals(".")) { - next(); // swallow . + next(false); // swallow . if (_token == null || _token.type != TokenType.Identifier) { throw makeException("Missing function name"); } String identifier = _token.text; - next(); + next(false); if (_token != null && _token.type == TokenType.Delimiter && _token.text.equals("(")) { - next(); // swallow ( + next(true); // swallow ( Function f = ControlFunctionRegistry.getFunction(identifier); if (f == null) { @@ -193,7 +200,7 @@ public class Parser { eval = new FieldAccessorExpr(eval, identifier); } } else if (_token.type == TokenType.Delimiter && _token.text.equals("[")) { - next(); // swallow [ + next(true); // swallow [ List args = parseExpressionList("]"); args.add(0, eval); @@ -219,7 +226,7 @@ public class Parser { l.add(eval); if (_token != null && _token.type == TokenType.Delimiter && _token.text.equals(",")) { - next(); // swallow comma, loop back for more + next(true); // swallow comma, loop back for more } else { break; } @@ -227,7 +234,7 @@ public class Parser { } if (_token != null && _token.type == TokenType.Delimiter && _token.text.equals(closingDelimiter)) { - next(); // swallow closing delimiter + next(false); // swallow closing delimiter } else { throw makeException("Missing " + closingDelimiter); } diff --git a/src/main/java/com/metaweb/gridworks/gel/Scanner.java b/src/main/java/com/metaweb/gridworks/gel/Scanner.java index 714a3504d..3df2ba1bc 100644 --- a/src/main/java/com/metaweb/gridworks/gel/Scanner.java +++ b/src/main/java/com/metaweb/gridworks/gel/Scanner.java @@ -7,7 +7,8 @@ public class Scanner { Operator, Identifier, Number, - String + String, + Regex } static public class Token { @@ -42,6 +43,15 @@ public class Scanner { } } + static public class RegexToken extends Token { + final public boolean caseInsensitive; + + public RegexToken(int start, int end, String text, boolean caseInsensitive) { + super(start, end, TokenType.Regex, text); + this.caseInsensitive = caseInsensitive; + } + } + protected String _text; protected int _index; protected int _limit; @@ -60,7 +70,7 @@ public class Scanner { return _index; } - public Token next() { + public Token next(boolean regexPossible) { // skip whitespace while (_index < _limit && Character.isWhitespace(_text.charAt(_index))) { _index++; @@ -148,6 +158,46 @@ public class Scanner { TokenType.Identifier, _text.substring(start, _index) ); + } else if (c == '/' && regexPossible) { + /* + * Regex literal + */ + StringBuffer sb = new StringBuffer(); + + _index++; // skip opening delimiter + + while (_index < _limit) { + c = _text.charAt(_index); + if (c == '/') { + _index++; // skip closing delimiter + + boolean caseInsensitive = false; + if (_index < _limit && _text.charAt(_index) == 'i') { + caseInsensitive = true; + _index++; + } + + return new RegexToken( + start, + _index, + sb.toString(), + caseInsensitive + ); + } else if (c == '\\') { + sb.append(c); + + _index++; // skip escaping marker + if (_index < _limit) { + sb.append(_text.charAt(_index)); + } + } else { + sb.append(c); + } + _index++; + } + + detail = "Regex not properly closed"; + // fall through } else if ("+-*/.".indexOf(c) >= 0) { // operator _index++; diff --git a/src/main/webapp/scripts/dialogs/expression-preview-dialog.js b/src/main/webapp/scripts/dialogs/expression-preview-dialog.js index 41d50ce4d..39fe3b542 100644 --- a/src/main/webapp/scripts/dialogs/expression-preview-dialog.js +++ b/src/main/webapp/scripts/dialogs/expression-preview-dialog.js @@ -322,7 +322,10 @@ ExpressionPreviewDialog.Widget.prototype._renderPreview = function(expression, d var renderValue = function(td, v) { if (v !== null && v !== undefined) { if ($.isArray(v)) { - td.text(JSON.stringify(v)); + var a = []; + $.each(v, function() { a.push(JSON.stringify(this)); }); + + td.text("[ " + a.join(", ") + " ]"); } else if ($.isPlainObject(v)) { $('').addClass("expression-preview-special-value").text("Error: " + v.message).appendTo(td); } else if (typeof v === "string" && v.length == 0) { diff --git a/src/main/webapp/scripts/views/data-table-column-header-ui.js b/src/main/webapp/scripts/views/data-table-column-header-ui.js index c8080b8ae..731fbf87d 100644 --- a/src/main/webapp/scripts/views/data-table-column-header-ui.js +++ b/src/main/webapp/scripts/views/data-table-column-header-ui.js @@ -54,55 +54,6 @@ DataTableColumnHeaderUI.prototype._render = function() { DataTableColumnHeaderUI.prototype._createMenuForColumnHeader = function(elmt) { self = this; MenuSystem.createAndShowStandardMenu([ - { - label: "Edit Cells", - submenu: [ - { - label: "To Titlecase", - click: function() { self._doTextTransform("toTitlecase(value)", "store-blank", false, ""); } - }, - { - label: "To Uppercase", - click: function() { self._doTextTransform("toUppercase(value)", "store-blank", false, ""); } - }, - { - label: "To Lowercase", - click: function() { self._doTextTransform("toLowercase(value)", "store-blank", false, ""); } - }, - { - label: "Custom Transform ...", - click: function() { self._doTextTransformPrompt(); } - }, - {}, - { - label: "Split Multi-Valued Cells ...", - click: function() { self._doSplitMultiValueCells(); } - }, - { - label: "Join Multi-Valued Cells ...", - click: function() { self._doJoinMultiValueCells(); } - }, - {}, - { - label: "Cluster & Edit ...", - click: function() { new FacetBasedEditDialog(self._column.name, "value"); } - } - ] - }, - { - label: "Edit Column", - submenu: [ - { - label: "Add Column Based on This Column ...", - click: function() { self._doAddColumn("value"); } - }, - { - label: "Remove This Column", - click: function() { self._doRemoveColumn(); } - }, - ] - }, - {}, { label: "Filter", tooltip: "Filter rows by this column's cell content or characteristics", @@ -124,6 +75,24 @@ DataTableColumnHeaderUI.prototype._createMenuForColumnHeader = function(elmt) { label: "Custom Text Facet ...", click: function() { self._doFilterByExpressionPrompt("value", "list"); } }, + { + label: "Common Text Facets", + submenu: [ + { + label: "Word Facet", + click: function() { + ui.browsingEngine.addFacet( + "list", + { + "name" : self._column.name + " value.split(' ')", + "columnName" : self._column.name, + "expression" : "value.split(' ')" + } + ); + } + } + ] + }, {}, { label: "Numeric Facet", @@ -134,11 +103,7 @@ DataTableColumnHeaderUI.prototype._createMenuForColumnHeader = function(elmt) { "name" : self._column.name, "columnName" : self._column.name, "expression" : "value", - "mode" : "range", - "min" : 0, - "max" : 1 - }, - { + "mode" : "range" } ); } @@ -147,6 +112,53 @@ DataTableColumnHeaderUI.prototype._createMenuForColumnHeader = function(elmt) { label: "Custom Numeric Facet ...", click: function() { self._doFilterByExpressionPrompt("value", "range"); } }, + { + label: "Common Numeric Facets", + submenu: [ + { + label: "Text Length Facet", + click: function() { + ui.browsingEngine.addFacet( + "range", + { + "name" : self._column.name + ": value.length()", + "columnName" : self._column.name, + "expression" : "value.length()", + "mode" : "range" + } + ); + } + }, + { + label: "Log of Text Length Facet", + click: function() { + ui.browsingEngine.addFacet( + "range", + { + "name" : self._column.name + ": value.length().log()", + "columnName" : self._column.name, + "expression" : "value.length().log()", + "mode" : "range" + } + ); + } + }, + { + label: "Unicode Char-code Facet", + click: function() { + ui.browsingEngine.addFacet( + "range", + { + "name" : self._column.name + ": value.unicode()", + "columnName" : self._column.name, + "expression" : "value.unicode()", + "mode" : "range" + } + ); + } + } + ] + }, {}, { label: "Text Search", @@ -205,6 +217,70 @@ DataTableColumnHeaderUI.prototype._createMenuForColumnHeader = function(elmt) { } ] }, + {}, + { + label: "Edit Cells", + submenu: [ + { + label: "Transform ...", + click: function() { self._doTextTransformPrompt(); } + }, + { + label: "Common Transforms", + submenu: [ + { + label: "Unescape HTML entities", + click: function() { self._doTextTransform("value.unescape('html')", "store-blank", true, 10); } + }, + { + label: "Collapse whitespace", + click: function() { self._doTextTransform("value.replaceRegexp('\\s+', ' ')", "store-blank", false, ""); } + }, + {}, + { + label: "To Titlecase", + click: function() { self._doTextTransform("toTitlecase(value)", "store-blank", false, ""); } + }, + { + label: "To Uppercase", + click: function() { self._doTextTransform("toUppercase(value)", "store-blank", false, ""); } + }, + { + label: "To Lowercase", + click: function() { self._doTextTransform("toLowercase(value)", "store-blank", false, ""); } + } + ] + }, + {}, + { + label: "Split Multi-Valued Cells ...", + click: function() { self._doSplitMultiValueCells(); } + }, + { + label: "Join Multi-Valued Cells ...", + click: function() { self._doJoinMultiValueCells(); } + }, + {}, + { + label: "Cluster & Edit ...", + click: function() { new FacetBasedEditDialog(self._column.name, "value"); } + } + ] + }, + { + label: "Edit Column", + submenu: [ + { + label: "Add Column Based on This Column ...", + click: function() { self._doAddColumn("value"); } + }, + { + label: "Remove This Column", + click: function() { self._doRemoveColumn(); } + }, + ] + }, + {}, { label: "View", tooltip: "Collapse/expand columns to make viewing the data more convenient",