MediaWiki: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 // // Ideas for other features: // - Draw line between start and end of selection // - Cut/copy/paste rectangular selections (no existing library??)

addOnloadHook (function {     // wraps entire script

var Special = [ "border", "boxstyle", "colspan", "rowspan" ]; var new_symbol = {}; var Template;           // familytree or chart ? 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",                       "Markup → Diagram", "t-diagram",           "Convert ... to ASCII art", "", node);

if (item == "art2wiki") addPortletLink ("p-tb", "javascript:art2wiki",                       "Diagram → Markup", "t-diagram",           "Convert ASCII art back to ...", "", node); }

function wiki2art {   try { var textarea = document.editform.wpTextbox1; var scroll_pos = textarea.scrollTop; var pattern = /\{\{(familytree|chart)\/start[\S\s]*?\{\{\1\/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"); }   catch (e) { alert ("Could not convert to ASCII art because:\n\n" + e); } }

function wiki2art_replace (text, tmpl) {   var rows  = []; var parts = {};

Template = tmpl.toLowerCase; Maxwidth = (Template == "chart" ? 50 : 80);

parse_templates (text, rows); var start = "\n"; var end  = ""; 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; }

// 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 namepat = /^[A-Z0-9]+([\/&._]?[A-Z0-9])+$/i; var num     = 0; var params  = {}; var order   = []; var truncpat = new RegExp( "^((" + Special.join("|") + ")?_?.{0,5}).*" );

for (var r=0; r < rows.length; r++) { var row = rows[r]; var seen = {};

if (row[0].search(/^\s*(familytree|chart)\s*$/i) == -1) continue;    // maybe the /start or /end ? num++;

for (var c=1; c < row.length; c++) {           var cell = row[c];

if (cell == "") { continue; }           if (istile(cell)) { art += (cell+" ").slice(0,2); continue; }           var res = cell.match(/^\s*(.*?)\s*=\s*([\S\s]*?)\s*$/); if (res) { var name = res[1]; var value = res[2]; name = name.replace(truncpat, "$1");

if (! namepat.test(name)) throw '"' + name + '" is not an allowed parameter name.'; if (value.indexOf("\n") >= 0) throw "Parameter " + name + " spans multiple lines"; if (name in seen && value != seen[name]) throw "Parameter " + name + " has multiple values on template " + num; seen[name] = value;

if (params[name] != null && params[name] != value) throw "Parameter " + name + " is used on several rows with different values." // TODO: uniquify names

if (! (name in params)) order.push(name); params[name] = value; }           else            // it's a BOX {               cell = cell.replace(/^\s+|\s+$/g, "");

// TODO: Allow wide names instead of truncating cell = cell.slice(0,5);

if (! namepat.test(cell)) throw '"' + cell + '" is not an allowed parameter name.';

if (! (cell in params)) { order.push(cell); params[cell] = null; }               art += ("  "+cell+"   ").substr(cell.length/2, 6); }       }        art += "\n"; }

// list the parameter values, one per line

var param_list = "" while (name = order.shift) { param_list += (name+"             ").slice(0,14) + " = " + (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~");    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(/\s+$/mg, "");        // includes any \r 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 = Array(margin+1).join(" "); text = "";

for (var i=0; i < rows.length; i++) { var pad = Array(width - rows[i].length + 1).join(" "); text += margin + rows[i] + pad + margin + "\n"; }   return text; }

function art2wiki {   invert_symbols; try { var textarea = document.editform.wpTextbox1; var scroll_pos = textarea.scrollTop; var pattern = /(\{\{(familytree|chart)\/start.*}})([\S\s]*?)(\{\{\2\/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? update_menu ("wiki2art"); }   catch (e) { alert ("Could not convert ASCII art because:\n\n" + e); } }

function art2wiki_replace (all, start,tmpl,content,end) {   var label = {}; var param_rows = [];

Template = tmpl.toLowerCase; rows    = []; boxes   = [];

parse_art (content, label,rows); map_boxes (rows, boxes); map_tiles (boxes,rows, param_rows); crop_rows (param_rows); var wikitext = to_wikitext (label, param_rows);

return start + "\n" + wikitext + end; }

// Parse the simple ASCII art, storing the diagram in //  rows[] and the labels in label{} // function parse_art (text, label,outrows) {   // remove any rulers text = text.replace(/^.*0-1-2-3-4-5-6-7-8-9.*\n/mg, "");

// parse the name=value definitions into label{} // We're as flexible as possible, allowing settings // with no RHS, settings in multiple columns, etc. // However, a value cannot span lines. //   text = text.replace(/(\w[^\s=]*) *= *(.*?)(\t|\s\s\s| *$)/mg,         function (str,name,value) {            label[name] = value;            return "";        });

text = pad_text(text, 4);

var a = text.slice(0,-1).split("\n"); while (a.length) outrows.push(a.shift);

// At this point, outrows[] should contain the diagram padded // to the maximum width with two extra blank cells on each // side (1 box overlap + 1 neighbor) and with the vertical // lines aligned on the even characters (assuming diagram is   // consistent in this). }

// Find which cells are occupied by boxes, even if the box // names are real short (must be at least 2 characters) or //  real long. Doing this first makes processing the tiles // easier. Returns the 2D boxes array. // function map_boxes (rows, boxes) {   var namepat = /[A-Z0-9]+([\/&._]?[A-Z0-9])+/ig; var row, map, res, name, pos;

for (var i=0; i < rows.length; i++) { row = rows[i]; map = new Array(row.length);

while ((res = namepat.exec(row)) != null) { name = res[0]; if (istile(name.slice(0,2)))     // a2b2h ? continue; if (name.length % 2 == 1 && res.index % 2 == 0) toss (name + " is aligned ambiguously"); pos = (res.index + name.length / 2) & ~1; if (map[pos-2]) throw name + " overlaps " + map[pos-2]; map[pos-2] = name; map[pos]  = name; map[pos+2] = name;

row = row.slice(0, res.index) +     // blank out the name name.replace(/./g, " ") + row.slice(res.index + name.length); }       boxes.push(map); rows[i] = row; } }

function map_tiles (boxes,rows, param_rows) {   for (var r=1; r < rows.length-1; r++) {       var row    = rows[r]; var params = [];

var res = row.match(/^.(..)*?([^\s[\]P~=_-])/); if (res) toss (res[2] + " is mis-aligned on row " + r);

for (var c=2; c < row.length-2; c += 2) {           if (boxes[r][c]) { params.push( boxes[r][c] ); c += 4; }           else { var t = new Tile(r,c); t.tweak(r-1, c, 0); t.tweak(r+1, c, 2); t.tweak(r, c-2, 3); t.tweak(r, c+2, 1); params.push( t.symbol ); }       }        param_rows.push(params); } }

// Crop unneeded spaces from beginnings and ends of parameter // lists if entire columns are unused. The rows are assumed // to be the same virtual width. If a margin is desired, use //, not empty rows/columns. // // (In rare cases there could also be leading/trailing rows that // are empty, but don't crop them. Should only happen if these // lines were blank exept for character(s) in the odd cells. // Which shouldn't happen by accident.) // function crop_rows (rows) {   var min = 9999; var max = 0;

// Find first and last columns used //   for (var r=0; r < rows.length; r++) { var params = rows[r]; var col   = 0;         // virtual column / width var first = 9999;      // first used column var last  = 0;         // last used column

for (var i=0; i < params.length; i++) { var param = params[i]; if (param != ' ' && first > col) first = col; if (! istile(param)) col += 2;         // it's a 3-wide box if (param != ' ') last = col; col++; }       min = Math.min(min, first); max = Math.max(max, last); }

if (min > max) return;        // all blank var extra = col - max - 1;    // amount to trim on right

// Now crop leading and trailing params in blank columns. // Though the param list lengths vary, their virtual widths // should all be the same, and will continue to be consistent // after shaving the same amount off of each end. //   for (r=0; r < rows.length; r++) { rows[r].splice(0, min); rows[r].splice(rows[r].length - extra, extra); } }

function to_wikitext (label, rows) {   var result     = ""; var first_part = "{{" + Template; var label_used = {}; var i, attr;

for (i=0; i < Special.length; i++) { attr = Special[i]; if (attr in label) { first_part += "|" + attr + "=" + label[attr]; label_used[attr] = 1; }   }

for (var r=0; r < rows.length; r++) {       var params    = rows[r]; var seen     = {}; var last_part = ""; var param; result += first_part;

while (param = params.shift) { result += "|";

if (istile(param)) { result += param; continue; }

if (! (param in seen)) { seen[param] = 1;

if (param in label) { last_part += "| " + param + "=" + label[param]; label_used[param] = 1; }               for (i=0; i < Special.length; i++) { attr = Special[i] + "_" + param; if (attr in label) { last_part += "| " + attr + "=" + label[attr]; label_used[attr] = 1; seen[param] = 2; }               }            }

// If param.length < 5, center it so it looks better. // Unless it's used in any per-box attributes like boxstyle_FOO, // in which case it must be flush left to work correctly.

if (param.length >= 5) result += param; else if (seen[param] == 2) result += (param+"    ").substr(0,5); else result += (" "+param+"  ").substr(param.length/2,5); }

result = result.replace(/(\| )+$/g, "");  // omit empty cells on end result += last_part + "}}\n"; }

var unused = "";

for (i in label) { if (! (i in label_used) && label[i]) unused += "|" + i + "=" + label[i]; }   if (unused) result += "\n" + "\n"; return result; }

function istile (sym) {   return sym.length <= 1 && sym != "\n" || 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];

return [ch, specs]; } }

// Build reverse lookup table needed by Tile objects. // function invert_symbols {   for (var sym in symbols) { var nesw = symbols[sym].slice(0,4).join; new_symbol[nesw] = sym; } }

// Soft throw. // function toss (msg) {   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 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, 33 ],       "#" : [ 1, 2, 1, 2, 33 ],        "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, 0, 1, 2, 33 ],       "]" : [ 1, 2, 1, 0, 33 ],        "X" : [ 2, 1, 2, 2, 33 ], "T" : [ 0, 1, 2, 2, 33 ], "K" : [ 2, 0, 1, 2, 33 ], "k" : [ 1, 0, 2, 2, 33 ], "G" : [ 2, 2, 1, 0, 33 ] };

window.wiki2art = wiki2art;    // expose to HTML link window.art2wiki = art2wiki;

if (document.editform) { var textarea = document.editform.wpTextbox1; var res = textarea.value.match(/\{\{(familytree|chart)\/start[\S\s]*?\{\{\1\/end/i); if (res) { Template = res[1]; if (res[0].search(/\{\{(familytree|chart)\s*\|/i) == -1) update_menu ("art2wiki"); else update_menu ("wiki2art"); } }

} );   // end of script and addOnloadHook wrapper