Github Reply Comments

Easy reply to Github comments

  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 (https://github.com/jerone)
  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 1.0.6
  14. // @icon https://github.githubassets.com/pinned-octocat.svg
  15. // @grant none
  16. // @include https://github.com/*
  17. // @include https://gist.github.com/*
  18. // @require https://unpkg.com/turndown@5.0.3/dist/turndown.js
  19. // @require https://unpkg.com/turndown-plugin-gfm@1.0.2/dist/turndown-plugin-gfm.js
  20. // @require https://unpkg.com/turndown-plugin-github-code-snippet@1.0.2/turndown-plugin-github-code-snippet.js
  21. // ==/UserScript==
  22.  
  23. // cSpell:ignore textareas, previewable, tooltipped
  24. /* eslint security/detect-object-injection: "off" */
  25. /* global TurndownService,turndownPluginGfm,turndownPluginGithubCodeSnippet */
  26.  
  27. (function () {
  28. String.format = function (string) {
  29. var args = Array.prototype.slice.call(arguments, 1, arguments.length);
  30. return string.replace(/{(\d+)}/g, function (match, number) {
  31. return typeof args[number] !== "undefined" ? args[number] : match;
  32. });
  33. };
  34.  
  35. function turndownPluginGitHubAlert(turndownService) {
  36. turndownService.addRule("gfm-alert", {
  37. filter: function (node, _options) {
  38. return (
  39. node.nodeName === "DIV" &&
  40. node.classList.contains("markdown-alert")
  41. );
  42. },
  43. replacement: function (content, node, options) {
  44. const variant = node
  45. .querySelector(".markdown-alert-title")
  46. .innerText.trim();
  47. content = content.replace(/^\n+|\n+$/g, "");
  48. content = content.replace(
  49. // eslint-disable-next-line security/detect-non-literal-regexp
  50. new RegExp("^" + variant),
  51. "[!" + variant.toUpperCase() + "]",
  52. );
  53. return options.rules.blockquote.replacement(content);
  54. },
  55. });
  56. }
  57.  
  58. var turndownService = new TurndownService({
  59. headingStyle: "atx",
  60. codeBlockStyle: "fenced",
  61. hr: "***",
  62. });
  63. turndownService.use(turndownPluginGfm.gfm);
  64. turndownService.use(turndownPluginGithubCodeSnippet);
  65. turndownService.use(turndownPluginGitHubAlert);
  66.  
  67. function getCommentTextarea(replyBtn) {
  68. var newComment = replyBtn;
  69. while (
  70. newComment &&
  71. !newComment.classList.contains("js-quote-selection-container")
  72. ) {
  73. newComment = newComment.parentNode;
  74. }
  75.  
  76. var inlineComment = newComment.querySelector(
  77. ".js-inline-comment-form-container",
  78. );
  79. if (inlineComment) {
  80. inlineComment.classList.add("open");
  81. }
  82.  
  83. var textareas = newComment.querySelectorAll(
  84. ":scope > :not(.last-review-thread) .js-comment-field:not(.github-writer-ckeditor)",
  85. );
  86. return textareas[textareas.length - 1];
  87. }
  88.  
  89. function getCommentMarkdown(comment) {
  90. var commentText = "";
  91.  
  92. // Use raw comment when available.
  93. // Extra scope is needed to get the correct comment field, which is not an "Reference new issue" modal (with org rights).
  94. var commentForm = comment.querySelector(
  95. ":scope > .js-comment-update .js-comment-field",
  96. );
  97. if (commentForm) {
  98. commentText = commentForm.value;
  99. }
  100.  
  101. // Convert comment HTML to markdown.
  102. if (!commentText) {
  103. // Clone it, so we can alter the HTML a bit, without modifying the page.
  104. var commentBody = comment
  105. .querySelector(".comment-body")
  106. .cloneNode(true);
  107.  
  108. // Skip empty PR description.
  109. if (
  110. commentBody
  111. .querySelector("em")
  112. ?.innerText.includes("No description provided.")
  113. ) {
  114. return "";
  115. }
  116.  
  117. // Remove 'Toggle code wrap' buttons from https://gf.qytechs.cn/en/scripts/18789-github-toggle-code-wrap
  118. Array.prototype.forEach.call(
  119. commentBody.querySelectorAll(".ghd-wrap-toggle"),
  120. function (ghd) {
  121. ghd.remove();
  122. },
  123. );
  124.  
  125. // Refined GitHub adds a small avatar to username mention. See https://github.com/refined-github/refined-github/blob/main/source/features/small-user-avatars.tsx
  126. Array.prototype.forEach.call(
  127. commentBody.querySelectorAll(".rgh-small-user-avatars"),
  128. function (rgh) {
  129. rgh.remove();
  130. },
  131. );
  132.  
  133. // GitHub add an extra new line, which is converted by Turndown.
  134. Array.prototype.forEach.call(
  135. commentBody.querySelectorAll("pre code"),
  136. function (pre) {
  137. pre.innerHTML = pre.innerHTML.replace(/\n$/g, "");
  138. },
  139. );
  140.  
  141. commentText = turndownService.turndown(commentBody.innerHTML);
  142. }
  143.  
  144. return commentText;
  145. }
  146.  
  147. function addReplyButtons() {
  148. Array.prototype.forEach.call(
  149. document.querySelectorAll(".comment, .review-comment"),
  150. function (comment) {
  151. var oldReply = comment.querySelector(
  152. ".GithubReplyComments, .GithubCommentEnhancerReply",
  153. );
  154. if (oldReply) {
  155. oldReply.parentNode.removeChild(oldReply);
  156. }
  157.  
  158. var header = comment.querySelector(
  159. ":scope > :not(.minimized-comment) .timeline-comment-header",
  160. ),
  161. actions = comment.querySelector(
  162. ":scope > :not(.minimized-comment) .timeline-comment-actions",
  163. );
  164.  
  165. if (!header) {
  166. header = actions;
  167. }
  168.  
  169. if (!actions) {
  170. if (!header) {
  171. return;
  172. }
  173. actions = document.createElement("div");
  174. actions.classList.add("timeline-comment-actions");
  175. header.insertBefore(actions, header.firstElementChild);
  176. }
  177.  
  178. var reply = document.createElement("button");
  179. reply.setAttribute("type", "button");
  180. reply.setAttribute("title", "Reply to this comment");
  181. reply.setAttribute("aria-label", "Reply to this comment");
  182. reply.classList.add(
  183. "GithubReplyComments",
  184. "btn-link",
  185. "timeline-comment-action",
  186. "tooltipped",
  187. "tooltipped-ne",
  188. );
  189. reply.addEventListener("click", function (e) {
  190. e.preventDefault();
  191.  
  192. var timestamp = comment.querySelector(
  193. ".js-timestamp, .timestamp",
  194. );
  195.  
  196. var commentText = getCommentMarkdown(comment);
  197. commentText = commentText
  198. .trim()
  199. .split("\n")
  200. .map(function (line) {
  201. return "> " + line;
  202. })
  203. .join("\n");
  204.  
  205. var newComment = getCommentTextarea(this);
  206.  
  207. var author = comment.querySelector(".author");
  208. var authorLink =
  209. location.origin +
  210. (author.getAttribute("href") ||
  211. "/" + author.textContent);
  212.  
  213. var text = newComment.value.length > 0 ? "\n" : "";
  214. text += String.format(
  215. '[**@{0}**]({1}) commented on [{2}]({3} "{4} - Replied by Github Reply Comments"):\n{5}\n\n',
  216. author.textContent,
  217. authorLink,
  218. timestamp.firstElementChild.getAttribute("title"),
  219. timestamp.href,
  220. timestamp.firstElementChild.getAttribute("datetime"),
  221. commentText,
  222. );
  223.  
  224. newComment.value += text;
  225. newComment.setSelectionRange(
  226. newComment.value.length,
  227. newComment.value.length,
  228. );
  229. //newComment.closest('.previewable-comment-form').querySelector('.js-write-tab').click();
  230. newComment.focus();
  231.  
  232. // This will enable the "Comment" button, when there was no comment text yet.
  233. newComment.dispatchEvent(
  234. new CustomEvent("change", {
  235. bubbles: true,
  236. cancelable: false,
  237. }),
  238. );
  239.  
  240. // This will render GitHub Writer - https://github.com/ckeditor/github-writer
  241. // https://github.com/ckeditor/github-writer/blob/8dbc12cb01b7903d0d6c90202078214a8637de6d/src/app/plugins/quoteselection.js#L116-L127
  242. const githubWriter = newComment.closest(
  243. [
  244. "form.js-new-comment-form[data-github-writer-id]",
  245. "form.js-inline-comment-form[data-github-writer-id]",
  246. ].join(),
  247. );
  248. if (githubWriter) {
  249. window.postMessage(
  250. {
  251. type: "GitHub-Writer-Quote-Selection",
  252. id: Number(
  253. githubWriter.getAttribute(
  254. "data-github-writer-id",
  255. ),
  256. ),
  257. text: text,
  258. },
  259. "*",
  260. );
  261. }
  262. });
  263.  
  264. var svg = document.createElementNS(
  265. "http://www.w3.org/2000/svg",
  266. "svg",
  267. );
  268. svg.classList.add("octicon", "octicon-mail-reply");
  269. svg.setAttribute("height", "16");
  270. svg.setAttribute("width", "16");
  271. reply.appendChild(svg);
  272. var path = document.createElementNS(
  273. "http://www.w3.org/2000/svg",
  274. "path",
  275. );
  276. path.setAttribute(
  277. "d",
  278. "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",
  279. );
  280. svg.appendChild(path);
  281.  
  282. actions.appendChild(reply);
  283. },
  284. );
  285. }
  286.  
  287. // init;
  288. addReplyButtons();
  289.  
  290. // on pjax;
  291. document.addEventListener("pjax:end", addReplyButtons);
  292. })();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址