User:PBS/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 // User:PBS, 31 Jan 2020 added code to parse "family tree" and "tree chart" // // To install, add:  importScript("User:Daduxing/familytree.js"); // to your common.js page. This adds an option [Templates → Art] // to the toolbox menu when editing familytrees. // // It is a two pass operation. // 1. Press [Templates → Art] -- the template code is changed into an intermediate //                              format and the menu option changes to [Art → Templates] // 2. Press Art → Templates] -- converts the "Art" back in to cleaned up temlate code // // // 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;           // family *tree, chart or tree 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;

//--- // New code 2020-01-31 // This function returns the possible template names // It is in the form of a regular expression match. // The parenethasis represents "select" // The bar character ("|") represents "or" // so (Foo|Bar) means true if the string contains either Foo or Bar // and (Foo|Bar|Chart) means true if the string contains either Foo or Bar or Chart. // In this case there is a fourth option represented by a " *" which means 0 or more // spaces. In this case it covers "familytree" and "family tree". // The case of the first letter (is covered by another function so there is no need // to add cases to these options. //--- function template_names {   return "(family *tree|chart|tree chart)"; }

//--- // New code 2020-01-31 // This function returns a pattern for the whole of a family tree. // Because the string is passed into another function for processing it is necessary to double // escape the escape character "\", The first one is stripped in this creation leaving // the second one for the patter match ie "\\{\\{" becomes "\{\{" //--- function tree_pattern {   return "\\{\\{" + template_names + "\\/start[\\S\\s]*?\\{\\{" + template_names + "\\/end\\}\\}"; }

// 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 = new RegExp(tree_pattern, "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.match(/^(chart|tree chart)/i) ? 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 {{family *tree/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 = /([[\]{}])\1|\||| [\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?

var pattn = new RegExp("^\\s*" + template_names + "\\s*$", "i");

if (row[0].search(pattn) == -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 = new RegExp(tree_pattern, "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.match(/^(chart|tree chart)/i) && /^[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.match(/^(chart|tree chart)/i))   // 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  or  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.match(/^(chart|tree chart)/i) ? -5 : 0;

for (var sym in symbols) { var nesw = symbols[sym].slice(start,start+4).join; if (! (nesw in new_symbol) || Template.match(/^(chart|tree chart)/i) ) 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 pattn = new RegExp(tree_pattern, "ig"); var res = textbox.value.match(pattn); if (res) { Template = res[1]; pattn = new RegExp("^\\s*\\{\\{" + template_names + "\\s*\\|", "mi"); if (res[0].search(pattn) > 0) update_menu ("wiki2art"); else update_menu ("art2wiki"); } }

} );   // end of script and addOnloadHook wrapper //