// ==UserScript==
// @name Linkify bug comments (rt.perl.org)
// @namespace [mauke]/rt.perl.org
// @description turn commit references into clickable links
// @match http://rt.perl.org/*
// @match https://rt.perl.org/*
// @grant GM_xmlhttpRequest
// @version 1.0.2
// ==/UserScript==
'use strict';
const RT_TICKET = 'http://rt.perl.org/rt3/Public/Bug/Display.html?id=';
const GIT_BASE = 'http://perl5.git.perl.org';
const GIT_REPO = GIT_BASE + '/perl.git';
const GIT_COMMITDIFF = GIT_REPO + '/commitdiff/';
function search_git_for(s) {
return GIT_REPO + '?a=search&h=HEAD&st=commit&s=' + encodeURIComponent(s);
}
function process_ranges_under(root, predicate, body, kont) {
if (predicate(root)) {
let range = document.createRange();
range.selectNode(root);
return body(range, kont);
}
let queue = [root];
let loop_tree = function loop_tree() {
while (queue.length) {
let node = queue.shift();
if (node.nodeType !== node.ELEMENT_NODE) {
continue;
}
let loop_children = function loop_children(p) {
while (p) {
if (!predicate(p)) {
queue.push(p);
p = p.nextSibling;
continue;
}
let range = document.createRange();
range.setStartBefore(p);
while (p.nextSibling && predicate(p.nextSibling)) {
p = p.nextSibling;
}
range.setEndAfter(p);
p = p.nextSibling;
return body(range, () => loop_children(p));
}
return loop_tree();
};
return loop_children(node.firstChild);
}
return kont();
};
return loop_tree();
}
function is_kinda_text(node) {
return (
node.nodeType === node.TEXT_NODE ||
node.nodeType === node.ELEMENT_NODE && node.nodeName === 'BR'
);
}
function replace_text_under(root, body, kont) {
return process_ranges_under(
root,
is_kinda_text,
function (range, kont_inner) {
let synth = '';
let frag = range.extractContents();
for (let p = frag.firstChild; p; p = p.nextSibling) {
synth += p.nodeType === p.TEXT_NODE ? p.nodeValue : '\0';
}
return body(synth, (x) => {
range.insertNode(x);
return kont_inner();
});
},
kont
);
}
function xpath(expr, doc) {
doc = doc || document;
return doc.evaluate(expr, doc, () => 'http://www.w3.org/1999/xhtml', XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null);
}
function autolink(text, kont_outer) {
let re = function () {
let bug_re = (
'(?:' +
'\\b' +
'(?:' +
'bug' +
'|' +
'fix\\w*' +
'|' +
'perl' +
')' +
')?' +
'[\\0\\s]+' +
'#' +
// $1
'(' +
'\\d{2,}' +
')' +
'\\b'
);
let commit_re = (
// $2
'(' +
'\\b' +
'(?:' +
'as' +
'|' +
'by' +
'|' +
'commit' +
'|' +
'in' +
'|' +
'of' +
'|' +
'with' +
')' +
'[\\0\\s]+' +
'|' +
'[(:\\0]' +
'[\\0\\s]*' +
')' +
// $3
'(' +
'[\\da-f]{4,}' + '\\b' +
'(?:' +
'[\\0\\s]*' +
'(?:' +
',' +
'|' +
'(?:' +
',' +
'[\\0\\s]*' +
')?' +
'(?:' +
'and' +
'|' +
'or' +
')' +
'[\\0\\s]' +
')' +
'[\\0\\s]*' +
'[\\da-f]{4,}' + '\\b' +
')*' +
')'
);
let p4id_re = (
// $4
'(' +
'applied' +
'[\\0\\s]+' +
'as' +
'[\\0\\s]+' +
'|' +
'change' +
'[\\0\\s]*' +
')' +
// $5
'(' +
'#' + '\\d{2,}' + '\\b' +
')'
);
return new RegExp([bug_re, commit_re, p4id_re].join('|'), 'ig');
}();
let prev = 0;
let frag = document.createDocumentFragment();
function autotext_from(t, a, z) {
let chunk = t.slice(a, z);
let pieces = chunk.match(/[^\0]+|\0/g) || [];
for (let p of pieces) {
let x = p === '\0'
? document.createElement('br')
: document.createTextNode(p)
;
frag.appendChild(x);
}
}
function autotext(to) {
autotext_from(text, prev, to);
}
function step(kont) {
let m = re.exec(text);
if (!m) {
return kont();
}
autotext(m.index + (m[2] || m[4] || '').length);
let link_url, link_text;
let kont_local = function () {
let a = document.createElement('a');
a.href = link_url;
a.appendChild(document.createTextNode(link_text));
frag.appendChild(a);
prev = re.lastIndex;
return step(kont);
};
if (m[1]) {
link_url = RT_TICKET + m[1];
link_text = m[0];
} else if (m[3]) {
if (/^[\da-fA-F]+$/.test(m[3])) {
link_url = GIT_COMMITDIFF + m[3];
link_text = m[3];
} else {
let t = m[3];
let p = 0;
let re2 = /\b[\da-fA-F]{4,}\b/g;
let m2;
while ((m2 = re2.exec(t))) {
autotext_from(t, p, m2.index);
let a = document.createElement('a');
a.href = GIT_COMMITDIFF + m2[0];
a.appendChild(document.createTextNode(m2[0]));
frag.appendChild(a);
p = re2.lastIndex;
}
autotext_from(t, p, t.length);
prev = re.lastIndex;
return step(kont);
}
} else {
let srch = search_git_for('@' + m[5].substr(1));
return GM_xmlhttpRequest({
method: 'GET',
synchronous: false,
url: srch,
responseType: 'document',
onreadystatechange: function (r) {
if (r.readyState !== 4) return;
link_text = m[5];
link_url = srch;
if (r.status === 200 && typeof r.response === 'object') {
let results = xpath(
'//h:table[@class="commit_search"]' +
'//h:tr' +
'[h:td/h:span[@class="match"][not(following-sibling::text())]]' +
'/h:td[@class="link"]' +
'/h:a[text()="commitdiff"][last()]',
r.response
);
if (results && results.snapshotLength === 1) {
let base = (/^\w+:\/\/[^\/]+/.exec(r.finalUrl) || [GIT_BASE])[0];
link_url = results.snapshotItem(0).href.replace(/^(?=\/)/, () => base);
}
}
return kont_local();
},
});
}
return kont_local();
}
step(function () {
autotext(text.length);
return kont_outer(frag);
});
}
let roots = document.querySelectorAll('div.messagebody');
for (let root of roots) {
replace_text_under(root, autolink, () => {});
}