issues copy

add markdown copy to issues page

  1. // ==UserScript==
  2. // @name issues copy
  3. // @namespace https://github.com/zhzLuke96/github-issues-copy-user-js
  4. // @version 1.0.2
  5. // @description:cn 为issues页面添加markdown复制按钮
  6. // @description:en add markdown copy to issues page
  7. // @author zhzluke96
  8. // @match https://*.github.com/*
  9. // @icon https://www.google.com/s2/favicons?sz=64&domain=github.com
  10. // @grant none
  11. // @license MIT
  12. // @supportURL https://github.com/zhzLuke96/github-issues-copy-user-js/issues
  13. // @description add markdown copy to issues page
  14. // ==/UserScript==
  15.  
  16. (function () {
  17. "use strict";
  18.  
  19. const _historyWrap = function (type) {
  20. const orig = history[type];
  21. const e = new Event(type);
  22. return function () {
  23. const rv = orig.apply(this, arguments);
  24. e.arguments = arguments;
  25. window.dispatchEvent(e);
  26. return rv;
  27. };
  28. };
  29. history.pushState = _historyWrap("pushState");
  30. history.replaceState = _historyWrap("replaceState");
  31.  
  32. const html_tpls = {
  33. btn: (callback) => {
  34. const btn = renderHtml(
  35. `<button id="issues_copy_btn" data-component="IconButton" type="button" class="prc-Button-ButtonBase-c50BI prc-Button-IconButton-szpyj" data-loading="false" data-no-visuals="true" data-size="medium" data-variant="invisible" aria-describedby=":r59:-loading-announcement" aria-labelledby=":r57:">
  36. <svg viewBox="0 0 16 16" width="16" height="16" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
  37. <text x="1" y="11" font-size="10" font-family="Arial, sans-serif" fill="currentColor">MD</text>
  38. </svg>
  39. </button>`
  40. );
  41. btn.addEventListener("click", callback);
  42. btn.title = "copy this page to markdown";
  43. btn.dataset.check_id = "issues_copy";
  44. return btn;
  45. },
  46. };
  47.  
  48. function is_injected() {
  49. return (
  50. document.querySelectorAll(`[data-check_id="issues_copy"]`).length > 0
  51. );
  52. }
  53.  
  54. function get_issues_markdown_content() {
  55. const issues_elements = [
  56. `[data-component="PH_Title"]`,
  57. `[data-testid="issue-viewer-issue-container"]`,
  58. `[data-testid="issue-timeline-container"]`,
  59. ];
  60. return issues_elements
  61. .map((selector) => document.querySelector(selector))
  62. .map((element) => htmlElementToMarkdown(element))
  63. .join("\n\n");
  64. }
  65.  
  66. function is_issues_page() {
  67. return (
  68. document.querySelectorAll(`[data-testid="issue-viewer-issue-container"]`)
  69. .length > 0
  70. );
  71. }
  72.  
  73. async function wait_for_page_render(timeout_ms = 1000) {
  74. return new Promise((resolve) => {
  75. let timer = null;
  76. const observer = new MutationObserver(() => {
  77. clearTimeout(timer);
  78. refresh();
  79. });
  80. function refresh() {
  81. timer = setTimeout(() => {
  82. observer.disconnect();
  83. resolve();
  84. }, timeout_ms);
  85. }
  86.  
  87. observer.observe(document.documentElement, {
  88. childList: true,
  89. subtree: true,
  90. attributes: true,
  91. characterData: true,
  92. });
  93. refresh();
  94. });
  95. }
  96.  
  97. async function do_inject() {
  98. await wait_for_page_render();
  99. if (is_injected()) return;
  100. if (!is_issues_page()) return;
  101. const anchor_span = document.querySelector(
  102. `[data-component="PH_Actions"] [class*="CopyToClipboardButton"]`
  103. );
  104. anchor_span.after(
  105. html_tpls.btn(() => {
  106. const markdown_content = get_issues_markdown_content();
  107. const textarea = document.createElement("textarea");
  108. textarea.value = markdown_content;
  109. document.body.appendChild(textarea);
  110. textarea.select();
  111. document.execCommand("copy");
  112. document.body.removeChild(textarea);
  113. alert("[issues_copy]copy success");
  114. console.log("[issues_copy]copy success");
  115. console.log(markdown_content);
  116. })
  117. );
  118. }
  119.  
  120. do_inject();
  121.  
  122. window.addEventListener("pushState", () => {
  123. do_inject();
  124. });
  125. window.addEventListener("replaceState", () => {
  126. do_inject();
  127. });
  128.  
  129. // ----------------------------
  130. // 拓展区域
  131. // ----------------------------
  132. /**
  133. *
  134. * @param {HTMLElement} element
  135. * @returns {boolean}
  136. */
  137. function is_hide_element(element) {
  138. // 判断是否为隐藏
  139. return (
  140. element.hasAttribute("hidden") ||
  141. element.classList.contains("hidden") ||
  142. element.style.display === "none" ||
  143. getComputedStyle(element).display === "none" ||
  144. getComputedStyle(element).visibility === "hidden" ||
  145. getComputedStyle(element).opacity === "0"
  146. );
  147. }
  148.  
  149. /**
  150. *
  151. * @param {HTMLElement} element
  152. * @returns {string}
  153. */
  154. function htmlElementToMarkdown(element) {
  155. /**
  156. *
  157. * @param {HTMLElement} node
  158. * @returns string
  159. */
  160. function process(node) {
  161. if (node.nodeType === Node.TEXT_NODE) {
  162. return node.textContent;
  163. }
  164.  
  165. if (node.nodeType !== Node.ELEMENT_NODE) {
  166. return "";
  167. }
  168. if (is_hide_element(node)) {
  169. return "";
  170. }
  171.  
  172. const tag = node.tagName.toLowerCase();
  173. let content = Array.from(node.childNodes).map(process).join("");
  174.  
  175. if (content.trim() === "") {
  176. return "";
  177. }
  178.  
  179. if (node.classList.contains("markdown-body")) {
  180. content = "\n" + content;
  181. }
  182.  
  183. switch (tag) {
  184. case "h1":
  185. return `# ${content}\n\n`;
  186. case "h2":
  187. return `## ${content}\n\n`;
  188. case "h3":
  189. return `### ${content}\n\n`;
  190. case "h4":
  191. return `#### ${content}\n\n`;
  192. case "h5":
  193. return `##### ${content}\n\n`;
  194. case "h6":
  195. return `###### ${content}\n\n`;
  196. case "p":
  197. return `${content}\n\n`;
  198. case "strong":
  199. case "b":
  200. return `**${content}**`;
  201. case "em":
  202. case "i":
  203. return `*${content}*`;
  204. case "a":
  205. return `[${content}](${node.getAttribute("href")})`;
  206. case "code": {
  207. const code_content = node.innerText;
  208. return `\`${code_content}\``;
  209. }
  210. case "pre": {
  211. // 尝试查找代码块的语言 父元素上的 `highlight-source-xxx` 就是语言
  212. const lang =
  213. Array.from(node.parentElement.classList)
  214. .find((className) => className.startsWith("highlight-source-"))
  215. ?.replace("highlight-source-", "") ?? "";
  216. const code_content = node.innerText;
  217. return `\n\`\`\`${lang}\n${code_content}\n\`\`\`\n`;
  218. }
  219. case "ul":
  220. return (
  221. "\n" +
  222. Array.from(node.children)
  223. .map((li) => `- ${process(li)}`)
  224. .join("\n") +
  225. "\n\n"
  226. );
  227. case "ol":
  228. return (
  229. "\n" +
  230. Array.from(node.children)
  231. .map((li, i) => `${i + 1}. ${process(li)}`)
  232. .join("\n") +
  233. "\n\n"
  234. );
  235. case "br":
  236. return " \n";
  237. case "blockquote":
  238. return "> " + content.replace(/\n/g, "\n> ") + "\n\n";
  239. case "img":
  240. const alt = node.getAttribute("alt") || "";
  241. const src = node.getAttribute("src") || "";
  242. return `![${alt}](${src})`;
  243. default:
  244. return content;
  245. }
  246. }
  247.  
  248. return process(element).trim();
  249. }
  250.  
  251. /**
  252. *
  253. * @param {string} html_content
  254. * @returns {HTMLElement}
  255. */
  256. function renderHtml(html_content) {
  257. const container = document.createElement("div");
  258. container.innerHTML = html_content;
  259. return container.children[0];
  260. }
  261. })();