MediaWiki:Familytree.js

// Wiki user script to help maintain boxes-and-lines // diagrams by allowing you to edit the diagram in a simpler and more // natural ASCII art format. // Greg Ubben, 1 Dec 2008

addOnloadHook (function {     // wraps entire script

var Special = [ "border", "boxstyle", "colspan", "rowspan" ]; var new_symbol = {}; var Template;           // familytree or chart ? 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; textarea.value = textarea.value.replace(/\{\{(familytree|chart)\/start[\S\s]*?\{\{\1\/end}}/ig, wiki2art_replace); textarea.setAttribute("wrap", "off"); // work around problem with Firefox ignoring this (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;

parse_templates (text, rows); var start = "\n"; var end  = ""; layout_tiles (rows, parts); var art = touchup(parts.art);

var ruler = "0-1-2-3-4-5-6-7-8-9-"; ruler += ruler + ruler + ruler + ruler.slice(0,-1);

return start + "\n" + ruler + "\n\n" + art + "\n\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.substring(start, res.index)); rows.push(row); }   }    if (level != 0) throw "Mismatched or ...";

// TODO: Read the unused params from last edit }

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+" ").substring(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 (name in params && value != params[name]) throw "TODO: uniquify names!"; if (! (name in params)) { params[name] = value; order.push(name); }                           }            else {         // it's a BOX cell = cell.replace(/^\s+|\s+$/g, "");

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

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

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+"             ").substring(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. // And pads all lines to 79 columns for easier editing. // function touchup (art) {   art = art.replace(/!/g, "|"); art = art.replace(/([,`^)}*+-]|\b[Xadijqrv]) (?=[.'^({*+-]|[acijlqrv]\b)/g, "$1-"); art = art.replace(/([~%#\]]|\b[ADFLVfhy]) (?=[~%#[]|[7ACJKVXehy]\b)/g, "$1~");

while (art.search(/^.{0,78}$/m) >= 0) art = art.replace(/^(.{0,78})$/mg, "$1 "); return art; }

function art2wiki {   invert_symbols; try { var textarea = document.editform.wpTextbox1; var scroll_pos = textarea.scrollTop; textarea.value = textarea.value.replace(/(\{\{(familytree|chart)\/start.*}})([\S\s]*?)(\{\{\2\/end}})/ig, 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 "";        });

if (text.search(/\S/) == -1) throw "No diagram";

// trim trailing spaces and leading and trailing lines text = text.replace(/ +$/mg, ""); text = text.replace(/^\s*\n/, "\n"); text = text.replace(/\s*$/,  "\n");

// trim indentation while (text.search(/^\S/m) == -1) { text = text.replace(/^ /mg, ""); }

var rows = text.split(/\r?\n/);

var width = 0; var align = -1; for (var i=0; i < rows.length; i++) { width = Math.max(width, rows[i].length); if (align == -1) align = rows[i].search(/[^\w\s=~&\/\[\].-]/); }

var padding = "   ";    // 1 box overlap + 1 neighbor if (align % 2) padding += " ";

for (var i=0; i < rows.length; i++) { var pad = width - rows[i].length; var row = padding + rows[i] + padding; while (pad--) row += " "; outrows.push(row); }

// At this point, outrows[] should contain the diagram padded // to the maximum width with two extra blank cells on each // side 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.substring(0,2)))     // a2b2h ? continue; if (name.length % 2 == 1 && res.index % 2 == 0) throw 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.substr(0, res.index) +     // blank out the name name.replace(/./g, " ") + row.substr(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[\]~=_-])/); if (res) throw res[2] + " is mis-aligned on row " + r;           // Probably a common/annoying problem. The ruler should help. // Can we self-correct?

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. // // (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 += last_part + "}}\n"; }

var unused = [];

for (i in label) { if (! (i in label_used)) unused.push(i); }   if (unused.length > 0) result += "\n"; return result; }

function istile (sym) {   return sym.length <= 1 || Template == "chart" && sym.search(/^[a-z]2$/) == 0; }

//       Doubt: //       0   space //       1   ^ v //        2   - ! ~ : //       3   + ., ' ` / \ BOX

function Tile(r,c) {   var a = get_tile(r,c); this.orig_sym = a[0]; this.specs   = a[1];

// 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.specs[dir]; var weight  = this.specs[4];

if (us_line > 0 && ne_line != us_line && specs[4] > weight ||            us_line == 0 && ne_line > 0        && specs[4] >= weight)                            this.specs[dir] = ne_line; }

this.symbol = function {       var a  = this.specs; var ch = new_symbol[String.concat(a[0], a[1], a[2], a[3])]; if (ch == undefined) 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); if (ch == '|' || ch == '1') ch = '!'; if (ch == '_' || ch == '=') ch = '-'; if (ch.search(/\w/) == 0 && rows[r].charAt(c+1) == '2') ch += '2'; var specs = symbols[ch]; if (specs == undefined) specs = symbols['?'];

return [ch, specs]; } }

// Build reverse lookup table needed by Tile objects. // function invert_symbols {   var a, nesw; for (var sym in symbols) { a   = symbols[sym]; nesw = String.concat(a[0], a[1], a[2], a[3]); if (! (nesw in new_symbol)) new_symbol[nesw] = sym; } }

// I haven't tuned many of these weights yet. // Hopefully we won't need to go to per-edge weights.

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 ], "?" : [ 0, 0, 0, 0, 20 ]       // unknown };

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