User:GregU/familytree.js

// Wiki user script to help maintain or // boxes-and-lines diagrams, by allowing you to edit the diagram // in a simpler and more standard ASCII art format. // Greg Ubben, 1 Dec 2008 // // To install, add:  importScript("User:GregU/familytree.js"); // to your vector.js (or monobook.js) page. This adds an option // to the toolbox menu when editing familytrees. // // IE may work better than Firefox since it supports typeover mode. // // TODO: // - Anything we can do to improve WP:ACCESSIBILITY // - Some smarts with border/boxstyle // // Advanced ideas: // - Draw line between start and end of selection // - Cut/copy/paste rectangular selections (no existing library??) //  - include overwrite/typeover mode emulation for Firefox // - Java GUI version where you drag boxes and lines on a grid addOnloadHook (function {     // wraps entire script var Summary  = "Edited  using familytree.js"; var Special  = [ "border", "boxstyle", "colspan", "rowspan" ]; var Template;            // familytree or chart ? var Style    = null; var Center   = 40;       // center small diagrams on this column var Maxwidth = 80; var Picky    = 0;        // complain instead of self-correct? var rows; var boxes; //  Add/replace convert option at top of toolbox menu on sidebar. // function update_menu (item) {    var node = document.getElementById("t-diagram");    if (node)        node.parentNode.removeChild(node);    node = document.getElementById("t-whatlinkshere");    if (item == "wiki2art")        addPortletLink ("p-tb", "javascript:wiki2art", "Templates → Art", "t-diagram", "Convert ... to ASCII art", "", node);   if (item == "art2wiki")        addPortletLink ("p-tb", "javascript:art2wiki", "Art → Templates", "t-diagram", "Convert ASCII art back to ...", "", node); } function wiki2art {   try {        Style = null;        var textarea = document.editform.wpTextbox1;        var scroll_pos = textarea.scrollTop;        var pattern = /\{\{(familytree|chart)\/start[\S\s]*?\{\{\w+\/end}}/ig;        textarea.value = textarea.value.replace(pattern, wiki2art_replace);        textarea.setAttribute("wrap", "off");        // work around problem with Firefox ignoring wrap (bug 302710)        textarea.style.display = "block";        textarea.scrollTop = scroll_pos;      // Mozilla only?        update_menu ("art2wiki");        document.editform.wpSave.disabled = true;    }    catch (e) {        alert ("Could not convert to ASCII art because:\n\n" + e);    } } function wiki2art_replace (text, tmpl) {    var rows  = [];    var parts = {};    if (text.indexOf("\n") == -1)        return text;                // don't convert a 1-line legend // Sanity check, if non-empty but no lines begin with {{. //   if (text.search(/\n\s*\{\{.*\n/)   == -1 &&        text.search(/\n\s*[^\s<].*\n/) != -1) { toss ("Out of sync; looks like this already is art."); return text; }   Template = tmpl.toLowerCase; Maxwidth = (Template == "chart" ? 50 : 80); Style   = Style || new MarkupStyle(text); parse_templates (text, rows); var start = "{{" + rows.shift.join("|") + "}}\n"; var end  = "{{" + rows.pop.join("|")   + "}}"; layout_tiles (rows, parts); var art = pad_text( touchup( parts.art )); var width = art.indexOf("\n") / 2; width = (width > 50 && Maxwidth > 50) ? Maxwidth : 50; var ruler = Array(11).join("0-1-2-3-4-5-6-7-8-9-") .slice(0, width*2-1); return start + "\n" + ruler + "\n" + art + "\n" + parts.list + "\n" + end; } // Remember markup spacing styles based on first occurrences. // So to change the markup style, just change the first one // then toggle twice to "refresh". // function MarkupStyle (text) {   this.initial = ""; this.lead   = " "; this.equal  = "="; var res; text = text || ""; text = text.replace(/^.*\n/, "");  // strip {{familytree/start}} res = text.match(/\w( *)\|/); if (res) { this.initial = res[1];  // space after template name? }   res = text.match(/\|(\s*)\w{2,5}(\s*=\s*)[^\s=|}]/); if (res) { this.lead = res[1];     // params indented on new lines? this.equal = res[2]; }   this.trail = (/\n/.test(this.lead) ? " " : ""); this.trim = (text.search(/\| \| (\|?}}|\|\s*\w+\s*=)/) == -1); this.param = function(name,value) { return this.lead + name + this.equal + value + this.trail; } } // Parse textual series of  templates // into a list of parameter lists. The parameters can contain // arbitrarily complex nested wiki syntax like bar and //  but this simple strategy of just // counting double brackets and braces should be good enough. // function parse_templates (text, rows) {   var pattern = /(\||| [\S\s]*?<\/nowiki>/ig;    var level = 0;    var row, start, res;    while ((res = pattern.exec(text)) != null) {        if (res[1]) {            (res[1]=="[" || res[1]=="{") ? level++ : level--;        }        if (res[0] == "" && level == 0) {            row.push(text.slice(start, res.index));            rows.push(row);        }    }    if (level != 0)        throw "Mismatched  or [[..."; } function layout_tiles (rows, parts) {    var art     = "";    var params  = {};    var order   = [];    var specpat = new RegExp("^((" + Special.join("|") + ")_)\\s*(\\S.*)" );    //  Tweak name so it is valid (matches namepat from map_boxes    //  and is 2 to 5 characters long) and so it is unique if the    //  same name is used on several templates with different values.    //  Then store it in params{} and order[].    //    //  Could remember mappings in another hash, and change    //  back to original name on output (if original name not    //  already used on line).  Probably best not to though.    //    function goodname (name, value)    {        var res, prefix="", nn;        if (res = name.match(specpat)) {            prefix = res[1];            name   = res[3];        }        nn = alias[name];        if (!nn) {             // first encounter on this template            nn = name;            if (nn.search(/\w.*\w/) == -1 && value.search(/\w.*\w/) > -1)                nn = value.toUpperCase;            nn = nn.replace( /[^\w.\/&]/g,               "_");            nn = nn.replace( /_*([\W_])[\W_]*/g,        "$1");            nn = nn.replace( /^[\W_]*(.{0,4}[^\W_]).*/, "$1");            nn = nn.replace( /^.?$/,                    "A0001");            var base = nn;            var num  = 1;            while (nn in params && (params[nn] != value || prefix)) {                num++;                nn = base.slice(0, 5 - String(num).length) + num;            }            alias[name] = nn;        }        nn = prefix + nn;        if (! (nn in params)) {            order.push(nn);            params[nn] = value;        }        return nn;    }    //   FRANKLIN = Benjamin Franklin    FRANK    //   FRANKLIN = Frank N. Furter      FRAN2    boxstyle_FRANKLIN = red    //   FRANKLIN = Franklin Richards    FRAN3    //   FRANKLIN = Frank N. Furter               boxstyle_FRANKLIN = blue    for (var r=0; r < rows.length; r++) {        var row   = rows[r];        var seen  = {};        var alias = {};     // mapped to different name on this row?        if (row[0].search(/^\s*(familytree|chart)\s*$/i) == -1)            throw "Unrecognized template ";        for (var i=0; i < Special.length; i++)            alias[Special[i]] = Special[i];    // don't truncate boxstyle // Pass 1:  Do only the assignments first, because if the // same parameter name is used on a previous row with a        //  different value, then we need to rename this parameter // and its boxes before they are output. //       for (var c=1; c < row.length; c++) {           var cell = row[c]; var i   = cell.indexOf("="); if (i < 0 || cell == "=") continue; var name = trim(cell.slice(0,i)); var value = trim(cell.slice(i+1)); if (value.indexOf("\n") >= 0) toss ('Parameter "' + name + '" spans multiple lines.'); value = value.replace(/\n\s*/g, " "); if (seen[name] && value != seen[name]) throw 'Parameter "' + name + '" has multiple values on template ' + (r+1); seen[name] = value; goodname(name, value); }       //  Pass 2:  Now layout the tiles and boxes. //       for (var c=1; c < row.length; c++) {           var cell = trim(row[c]); if (istile(cell) && ! (cell in seen)) {               art += pad(cell, 2); }           else if (cell.indexOf("=") == -1)        // it's a BOX {               cell = goodname(cell, cell.replace(/_/g, " ")).slice(0,5); // Don't adjoin a wide cell if can avoid if (cell.length == 4 && /\w$/.test(art)) cell = " " + cell; art += (" "+cell+"   ").substr(cell.length/2, 6); }       }        art += "\n"; }   // list the parameter values, one per line // TODO: Styles referenced via [1], [2], etc var param_width = 5; for (var name in params) if (name.length > 8) param_width = 14;      // any boxstyle_FOO ? var param_list = ""; while (name = order.shift) { param_list += pad(name, param_width) + " = " + (params[name] || "") + "\n"; }   parts.art  = art; parts.list = param_list; } // Make the art more readable by converting some symbols. // Mainly just fills in --- and  horizontal lines for now. // 1.  Fill in a ~ tile followed by a ~ tile or a box // 2.  Fill in a box    followed by a ~ tile // TOM  - v -  SUE    becomes    TOM ---v--- SUE // function touchup (art) {   art = art.replace( /!/g, "|"); art = art.replace( /([,`^)}*+-]|\b[Xadijqrv]) (?=[.'^({*+-]|[acijlqrv]| ?\w\w)/g, "$1-"); art = art.replace( /([~%#\]]|\b[ADFLVfhy]) (?=[~%#[]|[7ACJKVXehy]| ?\w\w)/g,     "$1~"); art = art.replace( /(\w\w ? ?) (?=[.'^({*+-]|[acijlqrv]\b)/g, "$1-");   art = art.replace( /(\w\w ? ?) (?=[~%#[]|[7ACJKVXehy]\b)/g,   "$1~");    art = art.replace( /(\w\w ) (-|~)/g, "$1$2$2");    return art; } //  Trim and pad a multi-line diagram with spaces to its maximum //  width, adding a margin on both sides and a 1-line padded //  margin above and below.  Also tweaks the alignment if most //  of the alignment indicators are mis-aligned on odd. //  If margin is not given (wiki2art), it depends on the width. // function pad_text (text, margin) {    // trim trailing spaces and leading and trailing lines    text = text.replace(/\t/g, "        ");    // just in case    text = text.replace(/ *\r*$/mg, "");    text = text.replace(/^\n*/, "\n");    text = text.replace(/\n*$/, "\n");    // trim indentation if not empty    while (text.search(/(^|\n).?\S|^\s*$/) == -1) {        text = text.replace(/^  /mg, "");    }    var rows  = text.split("\n"); var width = 0; var align = 0; var alignpat = /[^\w\s=~&\/\[\].-]|[A-Z0-9]+([\/&._]?[A-Z0-9])+/ig; var res; for (var i=0; i < rows.length; i++) { width = Math.max(width, rows[i].length); // Are majority of alignment indicators on odd or even? //       while ((res = alignpat.exec(rows[i])) != null) { var len = res[0].length; if (len % 2)             // even boxes are ambiguous ((res.index + len/2) & 1) ? align-- : align++; }   }    //  If formatting for display, center diagram on column 40, but // at least a 4-cell left margin unless close to max width. // The margin gives room to draw another box on the left, and // you can then toggle view twice to indent another 4 cells. //   if (margin == null) { margin = Center - width / 2; margin = Math.max(margin & ~1, 8); if (width/2 + margin > Maxwidth) margin = 0; }   else if (align < 0) margin++; margin = pad("", margin); text  = ""; for (var i=0; i < rows.length; i++) { text += margin + pad(rows[i], width) + margin + "\n"; }   return text; } // Pad str with spaces on right to width len, but don't truncate. // function pad (str, len) {   if (str.length < len) str += Array(len - str.length + 1).join(" "); return str; } function trim (str) {   return str.replace(/^\s+|\s+$/g, ""); } function art2wiki {   try { var textarea = document.editform.wpTextbox1; var scroll_pos = textarea.scrollTop; var pattern = /\{\{(familytree|chart)\/start[\S\s]*?\{\{\w+\/end}}/ig; textarea.value = textarea.value.replace(pattern, art2wiki_replace); textarea.removeAttribute("wrap"); textarea.style.display = "inline";   // Firefox work-around textarea.scrollTop = scroll_pos;     // Firefox only? document.editform.wpSave.disabled = false; update_menu ("wiki2art"); if (document.editform.wpSummary.value.search(/^(\/\* .* \*\/)? *$/) == 0)           document.editform.wpSummary.value += Summary.replace("%s", Template); }   catch (e) { alert ("Could not convert ASCII art because:\n\n" + e); } } function art2wiki_replace (text, tmpl) {   var label      = {}; var param_rows = []; Template = tmpl.toLowerCase; rows    = []; boxes   = []; if (text.indexOf("\n") == -1) return text;               // don't convert a 1-line legend // Sanity check, if any lines begin with ");    else        tag = tag.replace(/\d+(?= (boxes|nodes|individuals))/, count);    return tag; } function istile (sym) {    return sym.length <= 1 ||           Template == "chart" && /^[a-z]2$/.test(sym); } function Tile(r,c) {    var a = get_tile(r,c);    this.orig_sym = a[0];    this.sides    = a[1].slice(0,4);   // copy vs ref    this.weight   = a[1][4];    // If edge is a line but next tile not same with > weight, change it    // If edge is blank  but next tile is line with >= weight, change it    //    this.tweak = function (r,c,dir)    {        var neighbor = get_tile(r,c);        var specs    = neighbor[1];        var ne_line  = specs[dir ^ 2];        var us_line  = this.sides[dir];        if (us_line > 0  && ne_line != us_line && specs[4] > this.weight ||            us_line == 0 && ne_line > 0        && specs[4] >= this.weight)                this.sides[dir] = ne_line; }   this.symbol = function {       var ch = new_symbol[this.sides]; if (ch == null || /[ :~!-]/.test(ch)) ch = this.orig_sym; return ch; }   function get_tile(r,c) {       if (boxes[r][c]) return ["BOX", [0, 0, 0, 0, 20]]; var ch = rows[r].charAt(c); var ch2 = rows[r].charAt(c+1); if (/[ P_=~-]/.test(ch) && /[^ [\]P_=~-]/.test(ch2))   // mis-aligned? ch = ch2; if (/\w/.test(ch) && ch2 == '2')             //  long symbol? ch += '2'; if (ch == '|' || ch == '1') ch = '!'; if (ch == '_' || ch == '=') ch = '-'; var specs = symbols[ch] || [0, 0, 0, 0, 20]; if (specs.length > 5 && Template == "chart")   // t, T, k, G            specs = specs.slice(5); return [ch, specs]; } } // Build reverse lookup table needed by Tile objects. // There is some conflict between the  and  symbols. // A few recently-added symbols map to different specs, and some specs // map back to different symbols. Hence the extra logic here depending // on the current Template family. // Tile.invert_symbols = function {   new_symbol = {}; var start = (Template == "chart") ? -5 : 0;   for (var sym in symbols) { var nesw = symbols[sym].slice(start,start+4).join; if (! (nesw in new_symbol) || Template == "chart") new_symbol[nesw] = sym; } } function toss (msg)           // Soft throw. {   if (Picky) throw msg; } // I haven't tuned many of these weights yet. // Hopefully we won't need to go to per-edge weights. // //       Doubt: //       0   space //       1   ^ v //        2   - ! ~ : //       3   + ., ' ` / \ BOX var new_symbol = {}; var symbols = { //             N, E, S, W, Weight " " : [ 0, 0, 0, 0, 90 ],       "-" : [ 0, 1, 0, 1, 50 ],        "!" : [ 1, 0, 1, 0, 50 ],        "+" : [ 1, 1, 1, 1, 20 ],        "," : [ 0, 1, 1, 0, 20 ],        "." : [ 0, 0, 1, 1, 20 ],        "`" : [ 1, 1, 0, 0, 20 ],        "'" : [ 1, 0, 0, 1, 20 ],        "^" : [ 1, 1, 0, 1, 70 ],        "v" : [ 0, 1, 1, 1, 70 ], "(" : [ 1, 0, 1, 1, 70 ],       ")" : [ 1, 1, 1, 0, 70 ],        "~" : [ 0, 2, 0, 2, 50 ],        ":" : [ 2, 0, 2, 0, 50 ],        "%" : [ 2, 2, 2, 2, 20 ],        "F" : [ 0, 2, 2, 0, 20 ], "7" : [ 0, 0, 2, 2, 20 ],       "L" : [ 2, 2, 0, 0, 20 ], "J" : [ 2, 0, 0, 2, 20 ], "A" : [ 2, 2, 0, 2, 70 ], "V" : [ 0, 2, 2, 2, 70 ], "C" : [ 2, 0, 2, 2, 70 ], "D" : [ 2, 2, 2, 0, 70 ], "*" : [ 2, 1, 2, 1, 51 ],       "#" : [ 1, 2, 1, 2, 51 ],   // don't tweak ---#--- "h" : [ 1, 2, 0, 2, 33 ], "y" : [ 0, 2, 1, 2, 33 ], "{" : [ 2, 0, 2, 1, 33 ],       "}" : [ 2, 1, 2, 0, 33 ],        "t" : [ 2, 1, 0, 1, 33,   1, 2, 1, 2, 51 ], "[" : [ 1, 0, 1, 2, 33 ],       "]" : [ 1, 2, 1, 0, 33 ],        "X" : [ 2, 1, 2, 2, 33 ], "T" : [ 0, 1, 2, 2, 33,  0, 0, 3, 3, 20 ], "K" : [ 2, 0, 1, 2, 33 ], "k" : [ 1, 0, 2, 2, 33,  3, 1, 3, 0, 33 ], "G" : [ 2, 2, 1, 0, 33,  3, 0, 3, 3, 70 ], // chart "P" : [ 0, 3, 0, 3, 50 ], "Q" : [ 3, 0, 3, 0, 50 ], "R" : [ 3, 3, 3, 3, 20 ], "S" : [ 0, 3, 3, 0, 20 ], "Y" : [ 3, 3, 0, 0, 20 ], "Z" : [ 3, 0, 0, 3, 20 ], "W" : [ 3, 3, 0, 3, 70 ], "M" : [ 0, 3, 3, 3, 70 ], "H" : [ 3, 3, 3, 0, 70 ], "c" : [ 2, 0, 2, 1, 33 ], "d" : [ 2, 1, 2, 0, 33 ], "i" : [ 2, 1, 0, 1, 33 ], "j" : [ 0, 1, 2, 1, 33 ], "e" : [ 1, 0, 1, 2, 33 ], "f" : [ 1, 2, 1, 0, 33 ], "a" : [ 3, 1, 3, 1, 51 ], "b" : [ 1, 3, 1, 3, 51 ],  // don't tweak ---b--- "l" : [ 3, 0, 3, 1, 33 ], "m" : [ 0, 3, 1, 3, 33 ], "n" : [ 1, 3, 0, 3, 33 ], "o" : [ 1, 3, 1, 0, 33 ], "p" : [ 1, 0, 1, 3, 33 ], "q" : [ 3, 1, 0, 1, 33 ], "r" : [ 0, 1, 3, 1, 33 ], "a2" : [ 3, 2, 3, 2, 54 ], "b2" : [ 2, 3, 2, 3, 54 ], "k2" : [ 3, 2, 3, 0, 44 ], "l2" : [ 3, 0, 3, 2, 44 ], "m2" : [ 0, 3, 2, 3, 44 ], "n2" : [ 2, 3, 0, 3, 44 ], "o2" : [ 2, 3, 2, 0, 44 ], "p2" : [ 2, 0, 2, 3, 44 ], "q2" : [ 3, 2, 0, 2, 44 ], "r2" : [ 0, 2, 3, 2, 44 ] }; window.wiki2art = wiki2art;    // expose to HTML link window.art2wiki = art2wiki; if (document.editform) { var textbox = document.editform.wpTextbox1; var res = textbox.value.match(/\{\{(familytree|chart)\/start[\S\s]*\{\{\w+\/end/i); if (res) { Template = res[1]; if (res[0].search(/^\s*\{\{(familytree|chart)\s*\|/mi) > 0) update_menu ("wiki2art"); else update_menu ("art2wiki"); } } } );   // end of script and addOnloadHook wrapper