Github Reply Comments

Easy reply to Github comments

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。

您需要先安装用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name             Github Reply Comments
// @namespace        https://github.com/jerone/UserScripts
// @description      Easy reply to Github comments
// @author           jerone
// @copyright        2016+, jerone (https://github.com/jerone)
// @license          CC-BY-NC-SA-4.0; https://creativecommons.org/licenses/by-nc-sa/4.0/legalcode
// @license          GPL-3.0-or-later; http://www.gnu.org/licenses/gpl-3.0.txt
// @homepage         https://github.com/jerone/UserScripts/tree/master/Github_Reply_Comments
// @homepageURL      https://github.com/jerone/UserScripts/tree/master/Github_Reply_Comments
// @supportURL       https://github.com/jerone/UserScripts/issues
// @contributionURL  https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=VCYMHWQ7ZMBKW
// @version          1.0.6
// @icon             https://github.githubassets.com/pinned-octocat.svg
// @grant            none
// @include          https://github.com/*
// @include          https://gist.github.com/*
// @require          https://unpkg.com/[email protected]/dist/turndown.js
// @require          https://unpkg.com/[email protected]/dist/turndown-plugin-gfm.js
// @require          https://unpkg.com/[email protected]/turndown-plugin-github-code-snippet.js
// ==/UserScript==

// cSpell:ignore textareas, previewable, tooltipped
/* eslint security/detect-object-injection: "off" */
/* global TurndownService,turndownPluginGfm,turndownPluginGithubCodeSnippet */

(function () {
	String.format = function (string) {
		var args = Array.prototype.slice.call(arguments, 1, arguments.length);
		return string.replace(/{(\d+)}/g, function (match, number) {
			return typeof args[number] !== "undefined" ? args[number] : match;
		});
	};

	function turndownPluginGitHubAlert(turndownService) {
		turndownService.addRule("gfm-alert", {
			filter: function (node, _options) {
				return (
					node.nodeName === "DIV" &&
					node.classList.contains("markdown-alert")
				);
			},
			replacement: function (content, node, options) {
				const variant = node
					.querySelector(".markdown-alert-title")
					.innerText.trim();
				content = content.replace(/^\n+|\n+$/g, "");
				content = content.replace(
					// eslint-disable-next-line security/detect-non-literal-regexp
					new RegExp("^" + variant),
					"[!" + variant.toUpperCase() + "]",
				);
				return options.rules.blockquote.replacement(content);
			},
		});
	}

	var turndownService = new TurndownService({
		headingStyle: "atx",
		codeBlockStyle: "fenced",
		hr: "***",
	});
	turndownService.use(turndownPluginGfm.gfm);
	turndownService.use(turndownPluginGithubCodeSnippet);
	turndownService.use(turndownPluginGitHubAlert);

	function getCommentTextarea(replyBtn) {
		var newComment = replyBtn;
		while (
			newComment &&
			!newComment.classList.contains("js-quote-selection-container")
		) {
			newComment = newComment.parentNode;
		}

		var inlineComment = newComment.querySelector(
			".js-inline-comment-form-container",
		);
		if (inlineComment) {
			inlineComment.classList.add("open");
		}

		var textareas = newComment.querySelectorAll(
			":scope > :not(.last-review-thread) .js-comment-field:not(.github-writer-ckeditor)",
		);
		return textareas[textareas.length - 1];
	}

	function getCommentMarkdown(comment) {
		var commentText = "";

		// Use raw comment when available.
		// Extra scope is needed to get the correct comment field, which is not an "Reference new issue" modal (with org rights).
		var commentForm = comment.querySelector(
			":scope > .js-comment-update .js-comment-field",
		);
		if (commentForm) {
			commentText = commentForm.value;
		}

		// Convert comment HTML to markdown.
		if (!commentText) {
			// Clone it, so we can alter the HTML a bit, without modifying the page.
			var commentBody = comment
				.querySelector(".comment-body")
				.cloneNode(true);

			// Skip empty PR description.
			if (
				commentBody
					.querySelector("em")
					?.innerText.includes("No description provided.")
			) {
				return "";
			}

			// Remove 'Toggle code wrap' buttons from https://greasyfork.org/en/scripts/18789-github-toggle-code-wrap
			Array.prototype.forEach.call(
				commentBody.querySelectorAll(".ghd-wrap-toggle"),
				function (ghd) {
					ghd.remove();
				},
			);

			// 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
			Array.prototype.forEach.call(
				commentBody.querySelectorAll(".rgh-small-user-avatars"),
				function (rgh) {
					rgh.remove();
				},
			);

			// GitHub add an extra new line, which is converted by Turndown.
			Array.prototype.forEach.call(
				commentBody.querySelectorAll("pre code"),
				function (pre) {
					pre.innerHTML = pre.innerHTML.replace(/\n$/g, "");
				},
			);

			commentText = turndownService.turndown(commentBody.innerHTML);
		}

		return commentText;
	}

	function addReplyButtons() {
		Array.prototype.forEach.call(
			document.querySelectorAll(".comment, .review-comment"),
			function (comment) {
				var oldReply = comment.querySelector(
					".GithubReplyComments, .GithubCommentEnhancerReply",
				);
				if (oldReply) {
					oldReply.parentNode.removeChild(oldReply);
				}

				var header = comment.querySelector(
						":scope > :not(.minimized-comment) .timeline-comment-header",
					),
					actions = comment.querySelector(
						":scope > :not(.minimized-comment) .timeline-comment-actions",
					);

				if (!header) {
					header = actions;
				}

				if (!actions) {
					if (!header) {
						return;
					}
					actions = document.createElement("div");
					actions.classList.add("timeline-comment-actions");
					header.insertBefore(actions, header.firstElementChild);
				}

				var reply = document.createElement("button");
				reply.setAttribute("type", "button");
				reply.setAttribute("title", "Reply to this comment");
				reply.setAttribute("aria-label", "Reply to this comment");
				reply.classList.add(
					"GithubReplyComments",
					"btn-link",
					"timeline-comment-action",
					"tooltipped",
					"tooltipped-ne",
				);
				reply.addEventListener("click", function (e) {
					e.preventDefault();

					var timestamp = comment.querySelector(
						".js-timestamp, .timestamp",
					);

					var commentText = getCommentMarkdown(comment);
					commentText = commentText
						.trim()
						.split("\n")
						.map(function (line) {
							return "> " + line;
						})
						.join("\n");

					var newComment = getCommentTextarea(this);

					var author = comment.querySelector(".author");
					var authorLink =
						location.origin +
						(author.getAttribute("href") ||
							"/" + author.textContent);

					var text = newComment.value.length > 0 ? "\n" : "";
					text += String.format(
						'[**@{0}**]({1}) commented on [{2}]({3} "{4} - Replied by Github Reply Comments"):\n{5}\n\n',
						author.textContent,
						authorLink,
						timestamp.firstElementChild.getAttribute("title"),
						timestamp.href,
						timestamp.firstElementChild.getAttribute("datetime"),
						commentText,
					);

					newComment.value += text;
					newComment.setSelectionRange(
						newComment.value.length,
						newComment.value.length,
					);
					//newComment.closest('.previewable-comment-form').querySelector('.js-write-tab').click();
					newComment.focus();

					// This will enable the "Comment" button, when there was no comment text yet.
					newComment.dispatchEvent(
						new CustomEvent("change", {
							bubbles: true,
							cancelable: false,
						}),
					);

					// This will render GitHub Writer - https://github.com/ckeditor/github-writer
					// https://github.com/ckeditor/github-writer/blob/8dbc12cb01b7903d0d6c90202078214a8637de6d/src/app/plugins/quoteselection.js#L116-L127
					const githubWriter = newComment.closest(
						[
							"form.js-new-comment-form[data-github-writer-id]",
							"form.js-inline-comment-form[data-github-writer-id]",
						].join(),
					);
					if (githubWriter) {
						window.postMessage(
							{
								type: "GitHub-Writer-Quote-Selection",
								id: Number(
									githubWriter.getAttribute(
										"data-github-writer-id",
									),
								),
								text: text,
							},
							"*",
						);
					}
				});

				var svg = document.createElementNS(
					"http://www.w3.org/2000/svg",
					"svg",
				);
				svg.classList.add("octicon", "octicon-mail-reply");
				svg.setAttribute("height", "16");
				svg.setAttribute("width", "16");
				reply.appendChild(svg);
				var path = document.createElementNS(
					"http://www.w3.org/2000/svg",
					"path",
				);
				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",
				);
				svg.appendChild(path);

				actions.appendChild(reply);
			},
		);
	}

	// init;
	addReplyButtons();

	// on pjax;
	document.addEventListener("pjax:end", addReplyButtons);
})();