Linkify bug comments (rt.perl.org)

turn commit references into clickable links

  1. // ==UserScript==
  2. // @name Linkify bug comments (rt.perl.org)
  3. // @namespace [mauke]/rt.perl.org
  4. // @description turn commit references into clickable links
  5. // @match http://rt.perl.org/*
  6. // @match https://rt.perl.org/*
  7. // @grant GM_xmlhttpRequest
  8. // @version 1.0.2
  9. // ==/UserScript==
  10.  
  11. 'use strict';
  12.  
  13. const RT_TICKET = 'http://rt.perl.org/rt3/Public/Bug/Display.html?id=';
  14.  
  15. const GIT_BASE = 'http://perl5.git.perl.org';
  16. const GIT_REPO = GIT_BASE + '/perl.git';
  17. const GIT_COMMITDIFF = GIT_REPO + '/commitdiff/';
  18.  
  19. function search_git_for(s) {
  20. return GIT_REPO + '?a=search&h=HEAD&st=commit&s=' + encodeURIComponent(s);
  21. }
  22.  
  23. function process_ranges_under(root, predicate, body, kont) {
  24. if (predicate(root)) {
  25. let range = document.createRange();
  26. range.selectNode(root);
  27. return body(range, kont);
  28. }
  29.  
  30. let queue = [root];
  31.  
  32. let loop_tree = function loop_tree() {
  33. while (queue.length) {
  34. let node = queue.shift();
  35. if (node.nodeType !== node.ELEMENT_NODE) {
  36. continue;
  37. }
  38.  
  39. let loop_children = function loop_children(p) {
  40. while (p) {
  41. if (!predicate(p)) {
  42. queue.push(p);
  43. p = p.nextSibling;
  44. continue;
  45. }
  46.  
  47. let range = document.createRange();
  48. range.setStartBefore(p);
  49. while (p.nextSibling && predicate(p.nextSibling)) {
  50. p = p.nextSibling;
  51. }
  52. range.setEndAfter(p);
  53. p = p.nextSibling;
  54. return body(range, () => loop_children(p));
  55. }
  56. return loop_tree();
  57. };
  58.  
  59. return loop_children(node.firstChild);
  60. }
  61. return kont();
  62. };
  63. return loop_tree();
  64. }
  65.  
  66. function is_kinda_text(node) {
  67. return (
  68. node.nodeType === node.TEXT_NODE ||
  69. node.nodeType === node.ELEMENT_NODE && node.nodeName === 'BR'
  70. );
  71. }
  72.  
  73. function replace_text_under(root, body, kont) {
  74. return process_ranges_under(
  75. root,
  76. is_kinda_text,
  77. function (range, kont_inner) {
  78. let synth = '';
  79. let frag = range.extractContents();
  80. for (let p = frag.firstChild; p; p = p.nextSibling) {
  81. synth += p.nodeType === p.TEXT_NODE ? p.nodeValue : '\0';
  82. }
  83. return body(synth, (x) => {
  84. range.insertNode(x);
  85. return kont_inner();
  86. });
  87. },
  88. kont
  89. );
  90. }
  91.  
  92. function xpath(expr, doc) {
  93. doc = doc || document;
  94. return doc.evaluate(expr, doc, () => 'http://www.w3.org/1999/xhtml', XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null);
  95. }
  96.  
  97. function autolink(text, kont_outer) {
  98. let re = function () {
  99.  
  100. let bug_re = (
  101. '(?:' +
  102. '\\b' +
  103. '(?:' +
  104. 'bug' +
  105. '|' +
  106. 'fix\\w*' +
  107. '|' +
  108. 'perl' +
  109. ')' +
  110. ')?' +
  111. '[\\0\\s]+' +
  112. '#' +
  113. // $1
  114. '(' +
  115. '\\d{2,}' +
  116. ')' +
  117. '\\b'
  118. );
  119.  
  120. let commit_re = (
  121. // $2
  122. '(' +
  123. '\\b' +
  124. '(?:' +
  125. 'as' +
  126. '|' +
  127. 'by' +
  128. '|' +
  129. 'commit' +
  130. '|' +
  131. 'in' +
  132. '|' +
  133. 'of' +
  134. '|' +
  135. 'with' +
  136. ')' +
  137. '[\\0\\s]+' +
  138. '|' +
  139. '[(:\\0]' +
  140. '[\\0\\s]*' +
  141. ')' +
  142. // $3
  143. '(' +
  144. '[\\da-f]{4,}' + '\\b' +
  145. '(?:' +
  146. '[\\0\\s]*' +
  147. '(?:' +
  148. ',' +
  149. '|' +
  150. '(?:' +
  151. ',' +
  152. '[\\0\\s]*' +
  153. ')?' +
  154. '(?:' +
  155. 'and' +
  156. '|' +
  157. 'or' +
  158. ')' +
  159. '[\\0\\s]' +
  160. ')' +
  161. '[\\0\\s]*' +
  162. '[\\da-f]{4,}' + '\\b' +
  163. ')*' +
  164. ')'
  165. );
  166.  
  167. let p4id_re = (
  168. // $4
  169. '(' +
  170. 'applied' +
  171. '[\\0\\s]+' +
  172. 'as' +
  173. '[\\0\\s]+' +
  174. '|' +
  175. 'change' +
  176. '[\\0\\s]*' +
  177. ')' +
  178. // $5
  179. '(' +
  180. '#' + '\\d{2,}' + '\\b' +
  181. ')'
  182. );
  183.  
  184. return new RegExp([bug_re, commit_re, p4id_re].join('|'), 'ig');
  185. }();
  186.  
  187. let prev = 0;
  188. let frag = document.createDocumentFragment();
  189.  
  190. function autotext_from(t, a, z) {
  191. let chunk = t.slice(a, z);
  192. let pieces = chunk.match(/[^\0]+|\0/g) || [];
  193. for (let p of pieces) {
  194. let x = p === '\0'
  195. ? document.createElement('br')
  196. : document.createTextNode(p)
  197. ;
  198. frag.appendChild(x);
  199. }
  200. }
  201.  
  202. function autotext(to) {
  203. autotext_from(text, prev, to);
  204. }
  205.  
  206. function step(kont) {
  207. let m = re.exec(text);
  208. if (!m) {
  209. return kont();
  210. }
  211. autotext(m.index + (m[2] || m[4] || '').length);
  212.  
  213. let link_url, link_text;
  214. let kont_local = function () {
  215. let a = document.createElement('a');
  216. a.href = link_url;
  217. a.appendChild(document.createTextNode(link_text));
  218. frag.appendChild(a);
  219.  
  220. prev = re.lastIndex;
  221. return step(kont);
  222. };
  223.  
  224. if (m[1]) {
  225. link_url = RT_TICKET + m[1];
  226. link_text = m[0];
  227. } else if (m[3]) {
  228. if (/^[\da-fA-F]+$/.test(m[3])) {
  229. link_url = GIT_COMMITDIFF + m[3];
  230. link_text = m[3];
  231. } else {
  232. let t = m[3];
  233. let p = 0;
  234. let re2 = /\b[\da-fA-F]{4,}\b/g;
  235. let m2;
  236. while ((m2 = re2.exec(t))) {
  237. autotext_from(t, p, m2.index);
  238. let a = document.createElement('a');
  239. a.href = GIT_COMMITDIFF + m2[0];
  240. a.appendChild(document.createTextNode(m2[0]));
  241. frag.appendChild(a);
  242. p = re2.lastIndex;
  243. }
  244. autotext_from(t, p, t.length);
  245. prev = re.lastIndex;
  246. return step(kont);
  247. }
  248. } else {
  249. let srch = search_git_for('@' + m[5].substr(1));
  250. return GM_xmlhttpRequest({
  251. method: 'GET',
  252. synchronous: false,
  253. url: srch,
  254. responseType: 'document',
  255. onreadystatechange: function (r) {
  256. if (r.readyState !== 4) return;
  257. link_text = m[5];
  258. link_url = srch;
  259. if (r.status === 200 && typeof r.response === 'object') {
  260. let results = xpath(
  261. '//h:table[@class="commit_search"]' +
  262. '//h:tr' +
  263. '[h:td/h:span[@class="match"][not(following-sibling::text())]]' +
  264. '/h:td[@class="link"]' +
  265. '/h:a[text()="commitdiff"][last()]',
  266. r.response
  267. );
  268. if (results && results.snapshotLength === 1) {
  269. let base = (/^\w+:\/\/[^\/]+/.exec(r.finalUrl) || [GIT_BASE])[0];
  270. link_url = results.snapshotItem(0).href.replace(/^(?=\/)/, () => base);
  271. }
  272. }
  273. return kont_local();
  274. },
  275. });
  276. }
  277.  
  278. return kont_local();
  279. }
  280.  
  281. step(function () {
  282. autotext(text.length);
  283. return kont_outer(frag);
  284. });
  285. }
  286.  
  287. let roots = document.querySelectorAll('div.messagebody');
  288. for (let root of roots) {
  289. replace_text_under(root, autolink, () => {});
  290. }