User:Elliottmobile/monobook.js: Difference between revisions
No edit summary |
No edit summary |
||
Line 1: | Line 1: | ||
// | // Modded version of http://en.wikipedia.org/wiki/User:Interiot/Tool2/code.js by Elliott. | ||
// | // see http://paperlined.org/apps/wikipedia/Tool2/ for instructions on adding this to your monobook.js | ||
// | |||
// To run this tool on other servers: | |||
//To | // 1. copy this script to the target server (this is required because of javascript cross-site security restrictions) | ||
// | |||
// | // 2. update the following URL | ||
//for | // for example: "User:Interiot/Tool2/code.js" | ||
var tool2_url = "User:Interiot/Tool2/code.js"; | |||
// 3. update this namespace list, extracted from something like http://en.wikiquote.org/wiki/Special:Export// | |||
// These *should not* have colons after them. | |||
var namespaces = [ | |||
"Talk", | |||
"User", | |||
"User talk", | |||
"Wikiquote", | |||
"Wikiquote talk", | |||
"Image", | |||
"Image talk", | |||
"MediaWiki", | |||
"MediaWiki talk", | |||
"Template", | |||
"Template talk", | |||
"Help", | |||
"Help talk", | |||
"Category", | |||
"Category talk", | |||
// 3b. these two project project entries are not added by Special:Export, and might or might not need to be updated | |||
"Hydrogenaudio Knowledgebase", | |||
"Hydrogenaudio Knowledgebase talk" | |||
]; | |||
namespaces[100] = "Portal"; | |||
namespaces[101] = "Portal talk"; | |||
// 4. update this date-parser to match the format and language of your specific wiki. Feel free to contact Interiot regarding this, if you can't find another | |||
// copy of this script that uses the same language. | |||
// input: a text string from Special:Contributions. output: a javascript Date object | |||
// documentation: http://www.quirksmode.org/js/introdate.html#parse, http://www.elated.com/tutorials/programming/javascript/dates/ | |||
function date_parse(text) { | |||
var matches = text.match(/^([0-9:]+), +([0-9]+) +([a-z]+) +([0-9]+)$/i); | |||
if (!matches) { | |||
//dump_text("XXX"); // for debugging | |||
return matches; | |||
} | |||
parseme = matches[3] + ", " + matches[2] + " " + matches[4] + " " + matches[1] + ":00"; | |||
//dump_text(parseme); // for debugging | |||
var dt = new Date(); | |||
dt.setTime( Date.parse(parseme)); | |||
//dump_text(dt.toLocaleString()); // for debugging | |||
return dt; | |||
} | |||
// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ end of server-specific configuration ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ | |||
// TODO: | |||
// - the current document.location method doesn't work when the page is accessed sans-mod_rewrite | |||
// - test with non-ASCII characters | |||
// - non-ascii usernames | |||
// - ?? | |||
var prefix = ""; | |||
var params = parse_params(); | |||
addOnloadFunction(function() { | |||
var path_len = document.location.pathname.length; | |||
// trigger once we view the right page | |||
if (document.location.pathname.substring(path_len - tool2_url.length, path_len) == tool2_url) { | |||
// get the prefix (needs to be fixed to work sans-mod_rewrite | |||
prefix = document.location.protocol + "//" + document.location.host + "/" | |||
+ document.location.pathname.substring(1, path_len - tool2_url.length); | |||
// blank the inner contents of the page | |||
var bodyContent = document.getElementById("bodyContent"); | |||
while (bodyContent.childNodes.length > 0) bodyContent.removeChild(bodyContent.lastChild); | |||
if (document.location.search.length == 0) { | |||
generate_input_form(bodyContent); | |||
} else { | |||
generate_main_report(bodyContent); | |||
} | } | ||
} | |||
}); | |||
function generate_input_form(bodyContent) { | |||
if (navigator.userAgent.toLowerCase().indexOf('msie')+1) | |||
{ | |||
bodyContent.innerHTML = "This counter does not currently work in Internet Explorer. Please <a href='http://www.getfirefox.com'>get Firefox</a>."; | |||
} | |||
else | |||
{ | |||
bodyContent.innerHTML = | |||
"<form><table><tr><td>Username <td><input maxlength=128 name=username value='' id=username title='username'>" + | |||
" <tr><td> <td><input type=submit value='Submit'>" + | |||
"</table></form>"; | |||
var form = bodyContent.getElementsByTagName("form")[0]; | |||
form.method = "get"; | |||
form.action = document.location; | |||
document.getElementById("username").focus(); | |||
} | |||
} | |||
function generate_main_report() { | |||
fetch_data(params["username"].replace(/\+/g, " "), | |||
"", output_main_report, 0, []); | |||
} | } | ||
function | |||
function add_stats_row(left_col, right_col) { | |||
var row = document.createElement("tr"); | |||
var left = document.createElement("td"); | |||
var right = document.createElement("td"); | |||
document.getElementById("basic_stats").appendChild(row); | |||
row.appendChild(left); | |||
row.appendChild(right); | |||
//left.innerHTML = left_col; | |||
left.appendChild( document.createTextNode(left_col) ); | |||
right.appendChild( document.createTextNode(right_col) ); | |||
return row; | |||
} | |||
function output_main_report(history) { | |||
// -- generate summary statistics | |||
var unique_articles = new Array(); | |||
var namespace_numedits = new Array(); | |||
for (var i=0; i<namespaces.length; i++) { | |||
namespace_numedits[ namespaces[i] ] = 0; | |||
} | |||
namespace_numedits[""] = 0; | |||
for (var i=0; i<history.length; i++) { | |||
var h = history[i]; | |||
unique_articles[ h["title"] ]++; | |||
namespace_numedits[ h["namespace"] ]++; | |||
} | |||
var unique_articles_keys = keys(unique_articles); | |||
// -- output report | |||
var table = document.createElement("table"); | |||
table.id = "basic_stats"; | |||
document.getElementById("bodyContent").appendChild(table); | |||
add_stats_row("Username", params["username"].replace(/\+/g, " ")); | |||
add_stats_row("Total edits", history.length); | |||
add_stats_row("Distinct pages edited", unique_articles_keys.length); | |||
add_stats_row("Average edits/page", new Number(history.length / unique_articles_keys.length).toFixed(3)); | |||
add_stats_row("First edit", history[ history.length-1 ]["date_text"] ); | |||
// add a blank row | |||
add_stats_row("", "").childNodes[0].style.height = "1em"; | |||
add_stats_row("(main)", namespace_numedits[""]); | |||
for (var i=0; i<namespaces.length; i++) { | |||
var nmspc = namespaces[i]; | |||
if (namespace_numedits[nmspc]) { | |||
add_stats_row(nmspc, namespace_numedits[nmspc]); | |||
} | |||
} | |||
} | } | ||
// ===================================== HTML-scraping backend ========================================= | |||
function | function add_loading_notice() { | ||
{ | if (document.getElementById("loading_notice")) | ||
return; | |||
var loading = document.createElement("div"); | |||
loading.id = "loading_notice"; | |||
loading.innerHTML = "<br><br>Retrieving data<blink>...</blink>"; | |||
document.getElementById("bodyContent").appendChild(loading); | |||
} | |||
function remove_loading_notice() { | |||
var loading = document.getElementById("loading_notice"); | |||
if (!loading) return; | |||
loading.parentNode.removeChild(loading); | |||
} | } | ||
// | var offset_regexp = /href="[^"]+:Contributions[^"]+offset=(\d+)/gi; | ||
function fetch_data(username, end_date, handler, offset, page_list) { | |||
/ | add_loading_notice(); | ||
var url = prefix + "Special:Contributions/" + username + "?offset=" + offset + "&limit=5000"; | |||
function | loadXMLDoc(url, | ||
{ | function (request) { | ||
var next_offset = 0; | |||
if (request.readyState != 4) return; | |||
if (request.status == 200) { | |||
page_list.push(request.responseText); | |||
//dump_text(request.responseText); | |||
// see if there's another pageful to get | |||
var matches = map( function(p){ | |||
return p.match( /(\d+)$/ )[0]; | |||
}, request.responseText.match( offset_regexp ) ); | |||
for (var i=0; i<matches.length; i++) { | |||
var v = matches[i] * 1; | |||
if (v != 0 && (offset == 0 || v < offset)) { | |||
next_offset = v; | |||
break; | |||
} | |||
} | |||
} | |||
//next_offset = 0; // for testing only, retrieve just the first page of results | |||
if (next_offset == 0) { | |||
parse_data(page_list, handler); | |||
} else { | |||
// tail recurse | |||
fetch_data(username, end_date, handler, next_offset, page_list); | |||
} | |||
}); | |||
} | |||
// input: a list of strings, each string containing the HTML from a single page | |||
// output: a list, where each individual entry is a specific edit from history | |||
function parse_data(page_list, handler) { | |||
//var total_len = 0; | |||
//for (var i=0; i<page_list.length; i++) total_len += page_list[i].length; | |||
//alert("parsing " + page_list.length + " pages comprising " + total_len + " total bytes"); | |||
var last_history_ent = []; | |||
last_history_ent["title"] = ""; | |||
last_history_ent["oldid"] = ""; | |||
var edit_history = new Array(); | |||
for (var pagecnt=0; pagecnt<page_list.length; pagecnt++) { | |||
var matches = page_list[pagecnt].match( /^<li>[^(]+\(<a href="[^"]+action=history.*/gim ); | |||
//dump_lines(matches); | |||
for (var matchcnt=0; matchcnt<matches.length; matchcnt++) { | |||
var history_text = matches[matchcnt]; | |||
var history_entry = new Array(); | |||
history_entry["date_text"] = history_text.match( /^<li>([^(<]+)/i )[1] | |||
.replace( / +$/, ""); | |||
history_entry["date"] = date_parse( history_entry["date_text"] ); | |||
history_entry["title"] = history_text.match( /title="([^"]+)"/i )[1] | |||
.replace( /"/g, "\"") | |||
.replace( /&/g, "&"); | |||
var find_comment = history_text.replace(/<span class="autocomment">.*?<\/span> ?/, ""); | |||
history_entry["comment"] = ifmatch(find_comment.match( /<span class='comment'>(.*?)<\/span>/ )) | |||
.replace(/^\((.*)\)$/, "$1"); | |||
history_entry["minor"] = /<span class="minor"/.test(history_text); | |||
history_entry["oldid"] = ifmatch(history_text.match(/oldid=([0-9]+)/i)); | |||
history_entry["namespace"] = ""; | |||
for (var nmspc_ctr=0; nmspc_ctr<namespaces.length; nmspc_ctr++) { | |||
var nmspc = namespaces[nmspc_ctr] + ":"; | |||
if (history_entry["title"].substring(0, nmspc.length) == nmspc) { | |||
history_entry["namespace"] = namespaces[nmspc_ctr]; | |||
break; | |||
} | |||
} | |||
//dump_object(history_entry); | |||
if (history_entry["title"] != last_history_ent["title"] || history_entry["oldid"] != last_history_ent["oldid"]) | |||
edit_history.push(history_entry); | |||
last_history_ent = history_entry; | |||
} | |||
} | |||
remove_loading_notice(); | |||
handler(edit_history); | |||
} | |||
// ===================================== test/debug functions ========================================= | |||
function dump_text(text) { | |||
//alert("dump_text, with text of size " + text.length); | |||
// | |||
var pre = document.createElement("pre"); | |||
var div = document.createElement("div"); | |||
div.style.width = "60em"; | |||
div.style.maxHeight = "40em"; | |||
div.style.overflow = "auto"; | |||
pre.appendChild(document.createTextNode(text)); | |||
div.appendChild(pre); | |||
document.getElementById("bodyContent").appendChild(div); | |||
} | } | ||
function dump_lines(ary) { | |||
dump_text("--> " + ary.join("\n--> ")); | |||
} | |||
function dump_object(obj) { | |||
var toString = ""; | |||
for (var prop in obj) { | |||
toString += prop + ": " + obj[prop] + "\n"; | |||
} | |||
dump_text(toString); | |||
} | |||
// ===================================== utility functions ========================================= | |||
function addOnloadFunction(f) { | |||
if (window.addEventListener) window.addEventListener("load",f,false); | |||
else if (window.attachEvent) window.attachEvent("onload",f); | |||
else { | |||
var oldOnload='_old_onload_'+addOnloadFunction.uid; | |||
addOnloadFunction[oldOnload] = window.onload ? window.onload : function () {}; | |||
window.onload = function() { addOnloadFunction[oldOnload](); f(); } | |||
++addOnloadFunction.uid; | |||
} | } | ||
} | |||
function parse_params() { | |||
var pairs = document.location.search.substring(1).split("&"); | |||
var ret = []; | |||
for (var i=0; i < pairs.length; i++) { | |||
var values = pairs[i].split("="); | |||
ret[values[0]] = unescape(values[1]); | |||
} | } | ||
} | return ret; | ||
} | |||
function | function loadXMLDoc(url, handler) | ||
{ | { | ||
// branch for native XMLHttpRequest object | |||
if (window.XMLHttpRequest) { | |||
req = new XMLHttpRequest(); | |||
req.onreadystatechange = function () {handler(req)}; | |||
req.open("GET", url, true); | |||
req.send(null); | |||
// branch for IE/Windows ActiveX version | |||
} else if (window.ActiveXObject) { | |||
req = new ActiveXObject("Microsoft.XMLHTTP"); | |||
if (req) { | |||
req.onreadystatechange = function () {handler(req)}; | |||
req.open("GET", url, true); | |||
req.send(); | |||
} | |||
} | |||
} | } | ||
// see http://search.cpan.org/dist/perl/pod/perlfunc.pod#map | |||
function map (handler, list) { | |||
var ret = new Array(); | |||
for (var i=0; i<list.length; i++) { | |||
ret[i] = handler( list[i] ); | |||
var | // ret.push( handler( list[i] ) ); | ||
} | } | ||
return ret; | |||
} | |||
// see http://search.cpan.org/dist/perl/pod/perlfunc.pod#keys | |||
function keys (obj) { | |||
var ret = new Array(); | |||
for (var key in obj) { | |||
ret.push(key); | |||
} | |||
return ret; | |||
} | } | ||
function ifmatch(ary) { | |||
if (ary && ary.length >= 2) { | |||
return ary[1]; | |||
} else { | |||
return ""; | |||
} | |||
} |
Revision as of 21:04, 21 September 2006
// Modded version of http://en.wikipedia.org/wiki/User:Interiot/Tool2/code.js by Elliott. // see http://paperlined.org/apps/wikipedia/Tool2/ for instructions on adding this to your monobook.js // To run this tool on other servers: // 1. copy this script to the target server (this is required because of javascript cross-site security restrictions) // 2. update the following URL // for example: "User:Interiot/Tool2/code.js" var tool2_url = "User:Interiot/Tool2/code.js"; // 3. update this namespace list, extracted from something like http://en.wikiquote.org/wiki/Special:Export// // These *should not* have colons after them. var namespaces = [ "Talk", "User", "User talk", "Wikiquote", "Wikiquote talk", "Image", "Image talk", "MediaWiki", "MediaWiki talk", "Template", "Template talk", "Help", "Help talk", "Category", "Category talk", // 3b. these two project project entries are not added by Special:Export, and might or might not need to be updated "Hydrogenaudio Knowledgebase", "Hydrogenaudio Knowledgebase talk" ]; namespaces[100] = "Portal"; namespaces[101] = "Portal talk"; // 4. update this date-parser to match the format and language of your specific wiki. Feel free to contact Interiot regarding this, if you can't find another // copy of this script that uses the same language. // input: a text string from Special:Contributions. output: a javascript Date object // documentation: http://www.quirksmode.org/js/introdate.html#parse, http://www.elated.com/tutorials/programming/javascript/dates/ function date_parse(text) { var matches = text.match(/^([0-9:]+), +([0-9]+) +([a-z]+) +([0-9]+)$/i); if (!matches) { //dump_text("XXX"); // for debugging return matches; } parseme = matches[3] + ", " + matches[2] + " " + matches[4] + " " + matches[1] + ":00"; //dump_text(parseme); // for debugging var dt = new Date(); dt.setTime( Date.parse(parseme)); //dump_text(dt.toLocaleString()); // for debugging return dt; } // ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ end of server-specific configuration ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ // TODO: // - the current document.location method doesn't work when the page is accessed sans-mod_rewrite // - test with non-ASCII characters // - non-ascii usernames // - ?? var prefix = ""; var params = parse_params(); addOnloadFunction(function() { var path_len = document.location.pathname.length; // trigger once we view the right page if (document.location.pathname.substring(path_len - tool2_url.length, path_len) == tool2_url) { // get the prefix (needs to be fixed to work sans-mod_rewrite prefix = document.location.protocol + "//" + document.location.host + "/" + document.location.pathname.substring(1, path_len - tool2_url.length); // blank the inner contents of the page var bodyContent = document.getElementById("bodyContent"); while (bodyContent.childNodes.length > 0) bodyContent.removeChild(bodyContent.lastChild); if (document.location.search.length == 0) { generate_input_form(bodyContent); } else { generate_main_report(bodyContent); } } }); function generate_input_form(bodyContent) { if (navigator.userAgent.toLowerCase().indexOf('msie')+1) { bodyContent.innerHTML = "This counter does not currently work in Internet Explorer. Please <a href='http://www.getfirefox.com'>get Firefox</a>."; } else { bodyContent.innerHTML = "<form><table><tr><td>Username <td><input maxlength=128 name=username value='' id=username title='username'>" + " <tr><td> <td><input type=submit value='Submit'>" + "</table></form>"; var form = bodyContent.getElementsByTagName("form")[0]; form.method = "get"; form.action = document.location; document.getElementById("username").focus(); } } function generate_main_report() { fetch_data(params["username"].replace(/\+/g, " "), "", output_main_report, 0, []); } function add_stats_row(left_col, right_col) { var row = document.createElement("tr"); var left = document.createElement("td"); var right = document.createElement("td"); document.getElementById("basic_stats").appendChild(row); row.appendChild(left); row.appendChild(right); //left.innerHTML = left_col; left.appendChild( document.createTextNode(left_col) ); right.appendChild( document.createTextNode(right_col) ); return row; } function output_main_report(history) { // -- generate summary statistics var unique_articles = new Array(); var namespace_numedits = new Array(); for (var i=0; i<namespaces.length; i++) { namespace_numedits[ namespaces[i] ] = 0; } namespace_numedits[""] = 0; for (var i=0; i<history.length; i++) { var h = history[i]; unique_articles[ h["title"] ]++; namespace_numedits[ h["namespace"] ]++; } var unique_articles_keys = keys(unique_articles); // -- output report var table = document.createElement("table"); table.id = "basic_stats"; document.getElementById("bodyContent").appendChild(table); add_stats_row("Username", params["username"].replace(/\+/g, " ")); add_stats_row("Total edits", history.length); add_stats_row("Distinct pages edited", unique_articles_keys.length); add_stats_row("Average edits/page", new Number(history.length / unique_articles_keys.length).toFixed(3)); add_stats_row("First edit", history[ history.length-1 ]["date_text"] ); // add a blank row add_stats_row("", "").childNodes[0].style.height = "1em"; add_stats_row("(main)", namespace_numedits[""]); for (var i=0; i<namespaces.length; i++) { var nmspc = namespaces[i]; if (namespace_numedits[nmspc]) { add_stats_row(nmspc, namespace_numedits[nmspc]); } } } // ===================================== HTML-scraping backend ========================================= function add_loading_notice() { if (document.getElementById("loading_notice")) return; var loading = document.createElement("div"); loading.id = "loading_notice"; loading.innerHTML = "<br><br>Retrieving data<blink>...</blink>"; document.getElementById("bodyContent").appendChild(loading); } function remove_loading_notice() { var loading = document.getElementById("loading_notice"); if (!loading) return; loading.parentNode.removeChild(loading); } var offset_regexp = /href="[^"]+:Contributions[^"]+offset=(\d+)/gi; function fetch_data(username, end_date, handler, offset, page_list) { add_loading_notice(); var url = prefix + "Special:Contributions/" + username + "?offset=" + offset + "&limit=5000"; loadXMLDoc(url, function (request) { var next_offset = 0; if (request.readyState != 4) return; if (request.status == 200) { page_list.push(request.responseText); //dump_text(request.responseText); // see if there's another pageful to get var matches = map( function(p){ return p.match( /(\d+)$/ )[0]; }, request.responseText.match( offset_regexp ) ); for (var i=0; i<matches.length; i++) { var v = matches[i] * 1; if (v != 0 && (offset == 0 || v < offset)) { next_offset = v; break; } } } //next_offset = 0; // for testing only, retrieve just the first page of results if (next_offset == 0) { parse_data(page_list, handler); } else { // tail recurse fetch_data(username, end_date, handler, next_offset, page_list); } }); } // input: a list of strings, each string containing the HTML from a single page // output: a list, where each individual entry is a specific edit from history function parse_data(page_list, handler) { //var total_len = 0; //for (var i=0; i<page_list.length; i++) total_len += page_list[i].length; //alert("parsing " + page_list.length + " pages comprising " + total_len + " total bytes"); var last_history_ent = []; last_history_ent["title"] = ""; last_history_ent["oldid"] = ""; var edit_history = new Array(); for (var pagecnt=0; pagecnt<page_list.length; pagecnt++) { var matches = page_list[pagecnt].match( /^<li>[^(]+\(<a href="[^"]+action=history.*/gim ); //dump_lines(matches); for (var matchcnt=0; matchcnt<matches.length; matchcnt++) { var history_text = matches[matchcnt]; var history_entry = new Array(); history_entry["date_text"] = history_text.match( /^<li>([^(<]+)/i )[1] .replace( / +$/, ""); history_entry["date"] = date_parse( history_entry["date_text"] ); history_entry["title"] = history_text.match( /title="([^"]+)"/i )[1] .replace( /"/g, "\"") .replace( /&/g, "&"); var find_comment = history_text.replace(/<span class="autocomment">.*?<\/span> ?/, ""); history_entry["comment"] = ifmatch(find_comment.match( /<span class='comment'>(.*?)<\/span>/ )) .replace(/^\((.*)\)$/, "$1"); history_entry["minor"] = /<span class="minor"/.test(history_text); history_entry["oldid"] = ifmatch(history_text.match(/oldid=([0-9]+)/i)); history_entry["namespace"] = ""; for (var nmspc_ctr=0; nmspc_ctr<namespaces.length; nmspc_ctr++) { var nmspc = namespaces[nmspc_ctr] + ":"; if (history_entry["title"].substring(0, nmspc.length) == nmspc) { history_entry["namespace"] = namespaces[nmspc_ctr]; break; } } //dump_object(history_entry); if (history_entry["title"] != last_history_ent["title"] || history_entry["oldid"] != last_history_ent["oldid"]) edit_history.push(history_entry); last_history_ent = history_entry; } } remove_loading_notice(); handler(edit_history); } // ===================================== test/debug functions ========================================= function dump_text(text) { //alert("dump_text, with text of size " + text.length); var pre = document.createElement("pre"); var div = document.createElement("div"); div.style.width = "60em"; div.style.maxHeight = "40em"; div.style.overflow = "auto"; pre.appendChild(document.createTextNode(text)); div.appendChild(pre); document.getElementById("bodyContent").appendChild(div); } function dump_lines(ary) { dump_text("--> " + ary.join("\n--> ")); } function dump_object(obj) { var toString = ""; for (var prop in obj) { toString += prop + ": " + obj[prop] + "\n"; } dump_text(toString); } // ===================================== utility functions ========================================= function addOnloadFunction(f) { if (window.addEventListener) window.addEventListener("load",f,false); else if (window.attachEvent) window.attachEvent("onload",f); else { var oldOnload='_old_onload_'+addOnloadFunction.uid; addOnloadFunction[oldOnload] = window.onload ? window.onload : function () {}; window.onload = function() { addOnloadFunction[oldOnload](); f(); } ++addOnloadFunction.uid; } } function parse_params() { var pairs = document.location.search.substring(1).split("&"); var ret = []; for (var i=0; i < pairs.length; i++) { var values = pairs[i].split("="); ret[values[0]] = unescape(values[1]); } return ret; } function loadXMLDoc(url, handler) { // branch for native XMLHttpRequest object if (window.XMLHttpRequest) { req = new XMLHttpRequest(); req.onreadystatechange = function () {handler(req)}; req.open("GET", url, true); req.send(null); // branch for IE/Windows ActiveX version } else if (window.ActiveXObject) { req = new ActiveXObject("Microsoft.XMLHTTP"); if (req) { req.onreadystatechange = function () {handler(req)}; req.open("GET", url, true); req.send(); } } } // see http://search.cpan.org/dist/perl/pod/perlfunc.pod#map function map (handler, list) { var ret = new Array(); for (var i=0; i<list.length; i++) { ret[i] = handler( list[i] ); // ret.push( handler( list[i] ) ); } return ret; } // see http://search.cpan.org/dist/perl/pod/perlfunc.pod#keys function keys (obj) { var ret = new Array(); for (var key in obj) { ret.push(key); } return ret; } function ifmatch(ary) { if (ary && ary.length >= 2) { return ary[1]; } else { return ""; } }