summaryrefslogtreecommitdiff
path: root/src/client/textanalysis.js
diff options
context:
space:
mode:
authorAlon Levy <alon@pobox.com>2014-12-16 17:37:42 +0200
committerAlon Levy <alon@pobox.com>2014-12-16 17:37:42 +0200
commitade19de410e0a789a921ec1666b278b2a64176d6 (patch)
treec4c54b106de93609ca7bb70a20fd4be696715dbe /src/client/textanalysis.js
parentc7d026b1d504323a8c61d51b726e5e8d2337fc4b (diff)
moving files around after repository merger
Diffstat (limited to 'src/client/textanalysis.js')
-rw-r--r--src/client/textanalysis.js501
1 files changed, 501 insertions, 0 deletions
diff --git a/src/client/textanalysis.js b/src/client/textanalysis.js
new file mode 100644
index 00000000..830c16c1
--- /dev/null
+++ b/src/client/textanalysis.js
@@ -0,0 +1,501 @@
+"use strict";
+
+define(['rz_core', 'model/core', 'model/util', 'model/diff', 'rz_bus', 'consts'],
+function(rz_core, model_core, model_util, model_diff, rz_bus, consts) {
+
+var typeindex = 0;
+var nodetypes = consts.nodetypes;
+var typeStack = [];
+
+var lastnode;
+
+var sugg = {}, // suggestions for autocompletion of node names
+ suggestions_options = new Bacon.Bus(); // TODO: Property: same as bus, but with initial value
+
+var ANALYSIS_NODE_START = 'ANALYSIS_NODE_START';
+var ANALYSIS_NODE = 'ANALYSIS_NODE'
+var ANALYSIS_LINK = 'ANALYSIS_LINK';
+
+function selectedType()
+{
+ return nodetypes[typeindex];
+}
+
+function autoSuggestAddName(name)
+{
+ /* note that name can contain spaces - this is ok. We might want to limit this though? */
+ sugg[name] = 1;
+ suggestions_options.push(sugg);
+}
+
+/* up_to_two_renames:
+ *
+ * allow one letter or 'new node' to anything changes */
+function up_to_two_renames(graph, old_name, new_name)
+{
+ var not_one_letter = false;
+ var k;
+ /* Allowed renames:
+ * no change
+ * s1 is substring of s2
+ * older (s1) node being 'new node'
+ */
+ function allowed_rename(s1, s2)
+ {
+ return (s1 == s2 ||
+ s1 == 'new node' ||
+ s1.substr(0, s2.length) == s2 ||
+ s2.substr(0, s1.length) == s1);
+ }
+
+ if (old_name.length != new_name.length) {
+ console.log('bug: up_to_two_renames: not equal inputs');
+ return;
+ }
+ if (old_name.length > 2) {
+ console.log('bug: up_to_two_renames: input length 2 < ' + old_name.length);
+ return;
+ }
+ if (old_name.length == 2) {
+ if (allowed_rename(old_name[0], new_name[1]) &&
+ allowed_rename(old_name[1], new_name[0])) {
+ old_name = [old_name[1], old_name[0]];
+ } else {
+ if (!allowed_rename(old_name[0], new_name[0]) ||
+ !allowed_rename(old_name[1], new_name[1])) {
+ not_one_letter = true;
+ }
+ }
+ }
+ if (not_one_letter) {
+ console.log('bug: up_to_two_renames: not one letter changes');
+ console.log(old_name);
+ console.log(new_name);
+ return;
+ }
+ for (k = 0 ; k < old_name.length ; ++k) {
+ graph.editNameByName(old_name[k], new_name[k]);
+ }
+}
+
+// TODO: add escape char, i.e. r"bla\"bla" -> ['bla"bla']
+function tokenize(text, node_token, quote)
+{
+ var segment = [],
+ subsegment = [],
+ sentence = [],
+ quoteword;
+ var j;
+
+ segment = text.split(node_token);
+ for (j = 0; j < segment.length; j++) {
+ if (j !== 0) sentence.push(node_token);
+ subsegment = segment[j].split(" ");
+ if (subsegment.length === 0) {
+ sentence.push(" ");
+ }
+ for (var k = 0; k < subsegment.length; k++) {
+ if (subsegment[k] !== " " && subsegment[k] !== "") {
+ if (subsegment[k].charAt(0) === quote) {
+ quoteword = "";
+ do {
+ quoteword += subsegment[k] + ' ';
+ if(subsegment[k].charAt(subsegment[k].length-1) !== quote)k++;
+ } while (k < subsegment.length && subsegment[k].charAt(subsegment[k].length - 1) !== quote);
+ if (subsegment[k] && subsegment[k]!==quoteword.replace(/ /g, "")) {
+ quoteword += subsegment[k];
+ }
+ sentence.push(quoteword.replace(new RegExp(quote, 'g'), ""));
+ } else {
+ sentence.push(subsegment[k]);
+ }
+ }
+ }
+ }
+ return sentence;
+}
+
+/*
+ * textAnalyser
+ *
+ * Input:
+ * @newtext - new sentence
+ * @finalize - is this an intermediate editing state or are we editing the graph
+ *
+ * Output:
+ * none
+ *
+ * Side effect:
+ * updating graph (global)
+ *
+ * Implementation notes:
+ * There is no well defined grammer. The translation goes from obvious to not
+ * so much for more complex sentences involving more than two nodes (two '#'
+ * marks).
+ *
+ */
+var textAnalyser = function (newtext, finalize) {
+ var sentence,
+ token_set_new_node_names = [], // token set representing new node names
+ token_set_new_link_names = [], // token set representing new link names
+ linkindex = 0,
+ nodeindex = 0,
+ orderStack = [],
+ and_count = 0,
+ prefix = "",
+ m,
+ word,
+ completeSentence,
+ typesetter, starGraph,
+ n,
+ link_hash = {},
+ yell_bug = false, // TODO: fix both issues
+ NODE = "NODE",
+ LINK = "LINK",
+ START = "START",
+ ret = model_diff.new_topo_diff();
+
+ function addNode(name, type, state) {
+ if (type === undefined) {
+ console.log('bug: textanalyser.addNode of type undefined');
+ }
+ var node = model_core.create_node_from_spec(
+ {'name':name,
+ 'type':type,
+ 'state':state});
+
+ ret.node_set_add.push(node);
+ }
+
+ function __addLink(src, dst, name, state) {
+ if (!src || !dst) {
+ if (yell_bug) {
+ console.log('bug - adding link (' + src + ', ' + dst + ')');
+ }
+ return;
+ }
+ if (link_hash[src] && link_hash[src][dst]) {
+ if (yell_bug) {
+ console.log('bug - adding link twice (' + src + ', ' + dst + ')');
+ }
+ return;
+ }
+ if (!link_hash[src]) {
+ link_hash[src] = {};
+ }
+ link_hash[src][dst] = 1;
+
+ var link = {'__src': src, '__dst':dst, 'name':name, 'state':state}; // can't use model_core.create_link_from_spec as src,dst are only names
+ ret.link_set_add.push(link);
+ }
+
+ function apply_conjugator_link_logic(link, drop_conjugator_links){
+ if (drop_conjugator_links && link.name && (link.name.replace(/ /g,"") === "and")) {
+ link.state = "temp";
+ }
+ }
+
+ if (newtext.indexOf('#') == -1 || finalize) {
+ lastnode = null;
+ }
+
+ //Sentence Sequencing
+ //Build the words and cuts the main elements
+ sentence = tokenize(newtext, '#', '"');
+
+ // build new node,link arrays in order of appearance
+ for (m = 0; m < sentence.length; m++) {
+ switch (sentence[m]) {
+ case "#":
+ orderStack.push(START);
+ break;
+ case "and":
+ case "+":
+ case ",":
+ case "&":
+ sentence[m] = "and";
+ and_count++;
+ //orderStack.push("AND");
+ default:
+ if (orderStack[orderStack.length - 1] === START) {
+ orderStack.push(NODE);
+ token_set_new_node_names.push(sentence[m]);
+ linkindex++;
+ } else if (orderStack[orderStack.length - 1] === NODE) {
+ orderStack.push(LINK);
+ if (!token_set_new_link_names[linkindex]) {
+ token_set_new_link_names[linkindex] = sentence[m] + " ";
+ } else {
+ token_set_new_link_names[linkindex] += sentence[m] + " ";
+ }
+ } else {
+ if (!token_set_new_link_names[linkindex]) {
+ token_set_new_link_names[linkindex] = sentence[m] + " ";
+ } else {
+ token_set_new_link_names[linkindex] += sentence[m] + " ";
+ }
+ }
+ if (token_set_new_node_names.length === 0) {
+ prefix += (prefix.length > 0 ? ' ' : '') + sentence[m];
+ }
+ break;
+ }
+ }
+
+ starGraph = (token_set_new_link_names.length - and_count) >= 3 ||
+ ((token_set_new_link_names.length - and_count >= 1) &&
+ token_set_new_link_names.length > 2 &&
+ orderStack.length > 1 &&
+ orderStack[orderStack.length - 1] != NODE);
+
+ //PREFIX not null case - put complete sentence in first link.
+ if (prefix && !starGraph) {
+ token_set_new_link_names[1] = prefix + " " + token_set_new_node_names[0] +
+ (token_set_new_link_names[1] !== undefined || token_set_new_node_names[1] !== undefined ?
+ " " : "")
+ + (token_set_new_link_names[1] !== undefined ? token_set_new_link_names[1] : "")
+ + (token_set_new_node_names[1] !== undefined ? token_set_new_node_names[1] : "");
+ }
+
+ //WRITE COMPLETE SENTENCE
+ linkindex = 0;
+ nodeindex = 0;
+ word = "";
+ completeSentence = prefix.length > 0 ? String(prefix) + " " : "";
+ for (m = 0; m < orderStack.length; m++) {
+ if (orderStack[m] === NODE) {
+ word += " (" + token_set_new_node_names[nodeindex] + ") ";
+ completeSentence += token_set_new_node_names[nodeindex] + " ";
+ nodeindex++;
+ } else if (orderStack[m] === LINK) {
+ word += " -->" + token_set_new_link_names[nodeindex] + " --> ";
+ completeSentence += token_set_new_link_names[nodeindex];
+ }
+ }
+ completeSentence = completeSentence.trim();
+
+ //REBUILD GRAPH
+ linkindex = 0;
+ nodeindex = 0;
+
+ //CHANGE TO PERMANENT STATE AND UPDATE SUGGESTIONLIST
+ typesetter = "";
+ if (finalize === true) {
+ typesetter = "perm";
+ for (n = 0; n < token_set_new_node_names.length; n++) {
+ autoSuggestAddName(token_set_new_node_names[n]);
+ }
+ } else {
+ typesetter = "temp";
+ }
+
+ //ADD SURROUNDING BUBBLE
+ if (orderStack.length > 0) {
+ addNode("", "bubble","temp");
+ }
+
+ //0-N ORDER STACK
+ for (m = 0; m < orderStack.length - 1; m++) {
+ switch (orderStack[m]) {
+ case START:
+ if (!typeStack[nodeindex]) {
+ typeStack[nodeindex] = selectedType();
+ }
+ break;
+ case NODE:
+ addNode(token_set_new_node_names[nodeindex], typeStack[nodeindex], typesetter);
+ if (!starGraph && nodeindex > 0) {
+ __addLink(token_set_new_node_names[nodeindex - 1],
+ token_set_new_node_names[nodeindex],
+ token_set_new_link_names[linkindex], typesetter);
+ }
+ nodeindex++;
+ break;
+ case LINK:
+ linkindex++;
+ break;
+ }
+ }
+
+ //FINAL N ORDER
+ switch (orderStack[orderStack.length - 1]) {
+ case START:
+ typeStack[nodeindex] = selectedType();
+ addNode("new node", typeStack[nodeindex], "temp");
+ if (!starGraph && nodeindex > 0) {
+ __addLink(token_set_new_node_names[nodeindex - 1], "new node",
+ token_set_new_link_names[linkindex], "temp");
+ and_connect("new node");
+ }
+ ret.state = ANALYSIS_NODE_START;
+ break;
+ case NODE:
+ typeStack[nodeindex] = selectedType();
+ addNode(token_set_new_node_names[nodeindex], typeStack[nodeindex], typesetter);
+ if (!starGraph && nodeindex > 0) {
+ __addLink(token_set_new_node_names[nodeindex - 1],
+ token_set_new_node_names[nodeindex],
+ token_set_new_link_names[linkindex], typesetter);
+ and_connect(token_set_new_node_names[nodeindex]);
+ }
+ ret.state = ANALYSIS_NODE_START;
+ break;
+ case LINK:
+ linkindex++;
+ addNode("new node", selectedType(), "temp");
+ if (!starGraph) {
+ __addLink(token_set_new_node_names[nodeindex - 1], "new node", token_set_new_link_names[linkindex], "temp");
+ and_connect("new node");
+ }
+ ret.state = ANALYSIS_LINK;
+ break;
+ }
+
+ //EXTERNAL AND CONNECTION CHECKING
+ function and_connect(node) {
+ var verb;
+ for(var x=0;x<token_set_new_link_names.length;x++){
+ if(token_set_new_link_names[x])if(token_set_new_link_names[x].replace(/ /g,"")!=="and"){
+ verb = token_set_new_link_names[x];
+ for(var y=0; y<x ;y++){
+ __addLink(token_set_new_node_names[y], node, verb, typesetter);
+ for(var z=x; z<token_set_new_node_names.length ;z++){
+ __addLink(token_set_new_node_names[y], token_set_new_node_names[z], verb, typesetter);
+ }
+ }
+ }
+ }
+ }
+
+ /*console.log(sentence);
+ console.log(completeSentence);
+ console.log(orderStack);*/
+
+ //STAR CASE
+ if (starGraph) {
+ addNode(completeSentence, "chainlink", typesetter);
+ for (n = 0; n < token_set_new_node_names.length; n++) {
+ __addLink(token_set_new_node_names[n], completeSentence, "", typesetter);
+ }
+ }
+
+ ret.drop_conjugator_links = and_count < linkindex;
+
+ ret.applyToGraph = function(graph, backend_commit) {
+ window.ret = ret;
+
+ /*
+ * generate fitered node set who:
+ * - are not name-present in graph
+ * - are not of type 'bubble'
+ */
+ var n_set = ret.node_set_add.filter(function(node) {
+ return false == graph.hasNodeByNameAndNotState(node.name, "temp")
+ && node.type !== 'bubble';
+ }),
+ link_set = ret.link_set_add.map(
+ function (link) {
+ return [link.__src, link.__dst];
+ }),
+ comp = graph.compareSubset('temp', n_set, link_set);
+
+ if (false == finalize && comp.graph_same) {
+ if (comp.old_name && comp.new_name) {
+ up_to_two_renames(graph, comp.old_name, comp.new_name);
+ }
+ } else {
+ // REINITIALISE GRAPH (DUMB BUT IT WORKS)
+ graph.removeNodes(function(n){ return "temp" == n.state; });
+ graph.removeLinks("temp");
+
+ ret.for_each_node_add(function (node) {
+ if (true == finalize && node.state == 'temp') {
+ console.log('bug: temp node creation on finalize');
+ } else {
+ if (!finalize) {
+ lastnode = graph.addNode(node);
+ } else {
+ graph.addNode(node);
+ }
+ }
+ });
+ }
+ ret.for_each_link_add(function (link) {
+ if (false == finalize || link.name !== 'and') {
+ apply_conjugator_link_logic(link, ret.drop_conjugator_links);
+ graph.addLinkByName(link.__src,
+ link.__dst,
+ link.name,
+ link.state);
+ }
+ });
+
+ if (!finalize) {
+ graph.markRelated(token_set_new_node_names);
+ } else {
+ graph.removeRelated();
+ }
+
+ if (finalize && backend_commit) {
+ // broadcast diff:
+ // - finalize?
+ // - broadcast_diff requested by caller
+ var topo_diff = model_util.adapt_format_write_topo_diff(ret.nodes, ret.links);
+ var diff_set = model_diff.new_diff_set();
+ diff_set.add_diff_obj(topo_diff);
+ graph.commit_diff_set(diff_set);
+ }
+
+ // UPDATE GRAPH ONCE
+ rz_core.update_view__graph(!finalize && comp.graph_same);
+ };
+
+ if (finalize) {
+ typeStack = [];
+ }
+
+ return ret;
+};
+
+function init(graph)
+{
+ function onNodeAdded(diff) {
+ if (!diff || !diff.nodes || !diff.nodes.added) {
+ return;
+ }
+ for (var k in diff.nodes.added) {
+ var node = diff.nodes[k];
+ autoSuggestAddName(node.name.toLowerCase());
+ }
+ }
+ function toLowerCase(n) {
+ return n.toLowerCase();
+ }
+ function onSuggestedNameAdd(names) {
+ names.map(toLowerCase).forEach(autoSuggestAddName);
+ }
+ graph.diffBus.onValue(onNodeAdded);
+ rz_bus.names.onValue(onSuggestedNameAdd);
+}
+
+return {
+ init:init,
+ textAnalyser:textAnalyser,
+ suggestions_options: suggestions_options,
+ ANALYSIS_NODE_START:ANALYSIS_NODE_START,
+ ANALYSIS_NODE: ANALYSIS_NODE,
+ ANALYSIS_LINK:ANALYSIS_LINK,
+
+ //for the external arrow-type changer
+ lastnode: function() { return lastnode; },
+
+ selected_type_next: function() {
+ typeindex = (typeindex + 1) % 5;
+ return selectedType();
+ },
+ selected_type_prev: function() {
+ typeindex = (typeindex + 4) % 5;
+ return selectedType();
+ }
+};
+});