Github Reply Comments

Easy reply to Github comments

目前为 2018-09-27 提交的版本。查看 最新版本

  1. // ==UserScript==
  2. // @name Github Reply Comments
  3. // @namespace https://github.com/jerone/UserScripts
  4. // @description Easy reply to Github comments
  5. // @author jerone
  6. // @copyright 2016+, jerone (http://jeroenvanwarmerdam.nl)
  7. // @license CC-BY-NC-SA-4.0; https://creativecommons.org/licenses/by-nc-sa/4.0/legalcode
  8. // @license GPL-3.0-or-later; http://www.gnu.org/licenses/gpl-3.0.txt
  9. // @homepage https://github.com/jerone/UserScripts/tree/master/Github_Reply_Comments
  10. // @homepageURL https://github.com/jerone/UserScripts/tree/master/Github_Reply_Comments
  11. // @supportURL https://github.com/jerone/UserScripts/issues
  12. // @contributionURL https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=VCYMHWQ7ZMBKW
  13. // @version 0.1.2
  14. // @icon https://assets-cdn.github.com/pinned-octocat.svg
  15. // @grant none
  16. // @include https://github.com/*
  17. // @include https://gist.github.com/*
  18. // ==/UserScript==
  19.  
  20. (function() {
  21.  
  22. String.format = function(string) {
  23. var args = Array.prototype.slice.call(arguments, 1, arguments.length);
  24. return string.replace(/{(\d+)}/g, function(match, number) {
  25. return typeof args[number] !== "undefined" ? args[number] : match;
  26. });
  27. };
  28.  
  29. /*
  30. * to-markdown - an HTML to Markdown converter
  31. * Copyright 2011, Dom Christie
  32. * Licenced under the MIT licence
  33. * Source: https://github.com/domchristie/to-markdown
  34. *
  35. * Code is altered:
  36. * - Added task list support: https://github.com/domchristie/to-markdown/pull/62
  37. * - He dependecy is removed
  38. */
  39. var toMarkdown = function(string) {
  40.  
  41. var ELEMENTS = [{
  42. patterns: 'p',
  43. replacement: function(str, attrs, innerHTML) {
  44. return innerHTML ? '\n\n' + innerHTML + '\n' : '';
  45. }
  46. }, {
  47. patterns: 'br',
  48. type: 'void',
  49. replacement: ' \n'
  50. }, {
  51. patterns: 'h([1-6])',
  52. replacement: function(str, hLevel, attrs, innerHTML) {
  53. var hPrefix = '';
  54. for (var i = 0; i < hLevel; i++) {
  55. hPrefix += '#';
  56. }
  57. return '\n\n' + hPrefix + ' ' + innerHTML + '\n';
  58. }
  59. }, {
  60. patterns: 'hr',
  61. type: 'void',
  62. replacement: '\n\n* * *\n'
  63. }, {
  64. patterns: 'a',
  65. replacement: function(str, attrs, innerHTML) {
  66. var href = attrs.match(attrRegExp('href')),
  67. title = attrs.match(attrRegExp('title'));
  68. return href ? '[' + innerHTML + ']' + '(' + href[1] + (title && title[1] ? ' "' + title[1] + '"' : '') + ')' : str;
  69. }
  70. }, {
  71. patterns: ['b', 'strong'],
  72. replacement: function(str, attrs, innerHTML) {
  73. return innerHTML ? '**' + innerHTML + '**' : '';
  74. }
  75. }, {
  76. patterns: ['i', 'em'],
  77. replacement: function(str, attrs, innerHTML) {
  78. return innerHTML ? '_' + innerHTML + '_' : '';
  79. }
  80. }, {
  81. patterns: 'code',
  82. replacement: function(str, attrs, innerHTML) {
  83. return innerHTML ? '`' + innerHTML + '`' : '';
  84. }
  85. }, {
  86. patterns: 'img',
  87. type: 'void',
  88. replacement: function(str, attrs) {
  89. var src = attrs.match(attrRegExp('src')),
  90. alt = attrs.match(attrRegExp('alt')),
  91. title = attrs.match(attrRegExp('title'));
  92. return src ? '![' + (alt && alt[1] ? alt[1] : '') + ']' + '(' + src[1] + (title && title[1] ? ' "' + title[1] + '"' : '') + ')' : '';
  93. }
  94. }];
  95.  
  96. for (var i = 0, len = ELEMENTS.length; i < len; i++) {
  97. if (typeof ELEMENTS[i].patterns === 'string') {
  98. string = replaceEls(string, {
  99. tag: ELEMENTS[i].patterns,
  100. replacement: ELEMENTS[i].replacement,
  101. type: ELEMENTS[i].type
  102. });
  103. } else {
  104. for (var j = 0, pLen = ELEMENTS[i].patterns.length; j < pLen; j++) {
  105. string = replaceEls(string, {
  106. tag: ELEMENTS[i].patterns[j],
  107. replacement: ELEMENTS[i].replacement,
  108. type: ELEMENTS[i].type
  109. });
  110. }
  111. }
  112. }
  113.  
  114. function replaceEls(html, elProperties) {
  115. var pattern = elProperties.type === 'void' ? '<' + elProperties.tag + '\\b([^>]*)\\/?>' : '<' + elProperties.tag + '\\b([^>]*)>([\\s\\S]*?)<\\/' + elProperties.tag + '>',
  116. regex = new RegExp(pattern, 'gi'),
  117. markdown = '';
  118. if (typeof elProperties.replacement === 'string') {
  119. markdown = html.replace(regex, elProperties.replacement);
  120. } else {
  121. markdown = html.replace(regex, function(str, p1, p2, p3) {
  122. return elProperties.replacement.call(this, str, p1, p2, p3);
  123. });
  124. }
  125. return markdown;
  126. }
  127.  
  128. function attrRegExp(attr) {
  129. return new RegExp(attr + '\\s*=\\s*["\']?([^"\']*)["\']?', 'i');
  130. }
  131.  
  132. // Pre code blocks
  133.  
  134. string = string.replace(/<pre\b[^>]*>`([\s\S]*?)`<\/pre>/gi, function(str, innerHTML) {
  135. var text = innerHTML;
  136. text = text.replace(/^\t+/g, ' '); // convert tabs to spaces (you know it makes sense)
  137. text = text.replace(/\n/g, '\n ');
  138. return '\n\n ' + text + '\n';
  139. });
  140.  
  141. // Lists
  142.  
  143. // Escape numbers that could trigger an ol
  144. // If there are more than three spaces before the code, it would be in a pre tag
  145. // Make sure we are escaping the period not matching any character
  146. string = string.replace(/^(\s{0,3}\d+)\. /g, '$1\\. ');
  147.  
  148. // Converts lists that have no child lists (of same type) first, then works its way up
  149. var noChildrenRegex = /<(ul|ol)\b[^>]*>(?:(?!<ul|<ol)[\s\S])*?<\/\1>/gi;
  150. while (string.match(noChildrenRegex)) {
  151. string = string.replace(noChildrenRegex, replaceLists);
  152. }
  153.  
  154. function replaceLists(html) {
  155.  
  156. html = html.replace(/<(ul|ol)\b[^>]*>([\s\S]*?)<\/\1>/gi, function(str, listType, innerHTML) {
  157. var lis = innerHTML.split('</li>');
  158. lis.splice(lis.length - 1, 1);
  159.  
  160. for (i = 0, len = lis.length; i < len; i++) {
  161. if (lis[i]) {
  162. var prefix = (listType === 'ol') ? (i + 1) + ". " : "* ";
  163. lis[i] = lis[i].replace(/\s*<li[^>]*>([\s\S]*)/i, function(str, innerHTML) {
  164. innerHTML = innerHTML.replace(/\s*<input[^>]*?(checked[^>]*)?type=['"]?checkbox['"]?[^>]>/, function(inputStr, checked) {
  165. return checked ? '[X]' : '[ ]';
  166. });
  167. innerHTML = innerHTML.replace(/^\s+/, '');
  168. innerHTML = innerHTML.replace(/\n\n/g, '\n\n ');
  169. // indent nested lists
  170. innerHTML = innerHTML.replace(/\n([ ]*)+(\*|\d+\.) /g, '\n$1 $2 ');
  171. return prefix + innerHTML;
  172. });
  173. }
  174. lis[i] = lis[i].replace(/(.) +$/m, '$1');
  175. }
  176. return lis.join('\n');
  177. });
  178.  
  179. return '\n\n' + html.replace(/[ \t]+\n|\s+$/g, '');
  180. }
  181.  
  182. // Blockquotes
  183. var deepest = /<blockquote\b[^>]*>((?:(?!<blockquote)[\s\S])*?)<\/blockquote>/gi;
  184. while (string.match(deepest)) {
  185. string = string.replace(deepest, replaceBlockquotes);
  186. }
  187.  
  188. function replaceBlockquotes(html) {
  189. html = html.replace(/<blockquote\b[^>]*>([\s\S]*?)<\/blockquote>/gi, function(str, inner) {
  190. inner = inner.replace(/^\s+|\s+$/g, '');
  191. inner = cleanUp(inner);
  192. inner = inner.replace(/^/gm, '> ');
  193. inner = inner.replace(/^(>([ \t]{2,}>)+)/gm, '> >');
  194. return inner;
  195. });
  196. return html;
  197. }
  198.  
  199. function cleanUp(string) {
  200. string = string.replace(/^[\t\r\n]+|[\t\r\n]+$/g, ''); // trim leading/trailing whitespace
  201. string = string.replace(/\n\s+\n/g, '\n\n');
  202. string = string.replace(/\n{3,}/g, '\n\n'); // limit consecutive linebreaks to 2
  203. return string;
  204. }
  205.  
  206. return cleanUp(string);
  207. };
  208.  
  209. function getCommentTextarea(replyBtn) {
  210. var newComment = replyBtn;
  211. while (newComment && !newComment.classList.contains('js-quote-selection-container')) {
  212. newComment = newComment.parentNode;
  213. }
  214.  
  215. var inlineComment = newComment.querySelector(".js-inline-comment-form-container")
  216. if (inlineComment) {
  217. inlineComment.classList.add('open');
  218. }
  219.  
  220. var textareas = newComment.querySelectorAll(":scope > :not(.last-review-thread) .comment-form-textarea");
  221. return textareas[textareas.length - 1];
  222. }
  223.  
  224. function addReplyButtons() {
  225. Array.prototype.forEach.call(document.querySelectorAll(".comment, .review-comment"), function(comment) {
  226. var oldReply = comment.querySelector(".GithubReplyComments, .GithubCommentEnhancerReply");
  227. if (oldReply) {
  228. oldReply.parentNode.removeChild(oldReply);
  229. }
  230.  
  231. var header = comment.querySelector(":scope > :not(.minimized-comment) .timeline-comment-header"),
  232. actions = comment.querySelector(":scope > :not(.minimized-comment) .timeline-comment-actions");
  233.  
  234. if (!header) {
  235. header = actions;
  236. }
  237.  
  238. if (!actions) {
  239. if (!header) {
  240. return;
  241. }
  242. actions = document.createElement("div");
  243. actions.classList.add("timeline-comment-actions");
  244. header.insertBefore(actions, header.firstElementChild);
  245. }
  246.  
  247. var reply = document.createElement("button");
  248. reply.setAttribute("type", "button");
  249. reply.setAttribute("title", "Reply to this comment");
  250. reply.setAttribute("aria-label", "Reply to this comment");
  251. reply.classList.add("GithubReplyComments", "btn-link", "timeline-comment-action", "tooltipped", "tooltipped-ne");
  252. reply.addEventListener("click", function(e) {
  253. e.preventDefault();
  254.  
  255. var newComment = getCommentTextarea(this);
  256.  
  257. var timestamp = comment.querySelector(".timestamp");
  258.  
  259. var commentText = comment.querySelector(".comment-form-textarea");
  260. if (commentText) {
  261. commentText = commentText.value;
  262. } else {
  263. commentText = toMarkdown(comment.querySelector(".comment-body").innerHTML);
  264. }
  265. commentText = commentText.trim().split("\n").map(function(line) {
  266. return "> " + line;
  267. }).join("\n");
  268.  
  269. var text = newComment.value.length > 0 ? "\n" : "";
  270. text += String.format('[**@{0}**]({1}/{0}) commented on [{2}]({3} "{4} - Replied by Github Reply Comments"):\n{5}\n\n',
  271. comment.querySelector(".author").textContent,
  272. location.origin,
  273. timestamp.firstElementChild.getAttribute("title"),
  274. timestamp.href,
  275. timestamp.firstElementChild.getAttribute("datetime"),
  276. commentText);
  277.  
  278. newComment.value += text;
  279. newComment.setSelectionRange(newComment.value.length, newComment.value.length);
  280. newComment.focus();
  281. });
  282.  
  283. var svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
  284. svg.classList.add("octicon", "octicon-mail-reply");
  285. svg.setAttribute("height", "16");
  286. svg.setAttribute("width", "16");
  287. reply.appendChild(svg);
  288. var path = document.createElementNS("http://www.w3.org/2000/svg", "path");
  289. path.setAttribute("d", "M6 2.5l-6 4.5 6 4.5v-3c1.73 0 5.14 0.95 6 4.38 0-4.55-3.06-7.05-6-7.38v-3z");
  290. svg.appendChild(path);
  291.  
  292. actions.appendChild(reply);
  293. });
  294. }
  295.  
  296. // init;
  297. addReplyButtons();
  298.  
  299. // on pjax;
  300. document.addEventListener('pjax:end', addReplyButtons);
  301.  
  302. })();