issues copy

add markdown copy to issues page

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Userscripts to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==UserScript==
// @name            issues copy
// @namespace       https://github.com/zhzLuke96/github-issues-copy-user-js
// @version         1.0.2
// @description:cn     为issues页面添加markdown复制按钮
// @description:en     add markdown copy to issues page
// @author          zhzluke96
// @match           https://*.github.com/*
// @icon            https://www.google.com/s2/favicons?sz=64&domain=github.com
// @grant           none
// @license         MIT
// @supportURL      https://github.com/zhzLuke96/github-issues-copy-user-js/issues
// @description add markdown copy to issues page
// ==/UserScript==

(function () {
  "use strict";

  const _historyWrap = function (type) {
    const orig = history[type];
    const e = new Event(type);
    return function () {
      const rv = orig.apply(this, arguments);
      e.arguments = arguments;
      window.dispatchEvent(e);
      return rv;
    };
  };
  history.pushState = _historyWrap("pushState");
  history.replaceState = _historyWrap("replaceState");

  const html_tpls = {
    btn: (callback) => {
      const btn = renderHtml(
        `<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:">
        <svg viewBox="0 0 16 16" width="16" height="16" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
  <text x="1" y="11" font-size="10" font-family="Arial, sans-serif" fill="currentColor">MD</text>
</svg>
</button>`
      );
      btn.addEventListener("click", callback);
      btn.title = "copy this page to markdown";
      btn.dataset.check_id = "issues_copy";
      return btn;
    },
  };

  function is_injected() {
    return (
      document.querySelectorAll(`[data-check_id="issues_copy"]`).length > 0
    );
  }

  function get_issues_markdown_content() {
    const issues_elements = [
      `[data-component="PH_Title"]`,
      `[data-testid="issue-viewer-issue-container"]`,
      `[data-testid="issue-timeline-container"]`,
    ];
    return issues_elements
      .map((selector) => document.querySelector(selector))
      .map((element) => htmlElementToMarkdown(element))
      .join("\n\n");
  }

  function is_issues_page() {
    return (
      document.querySelectorAll(`[data-testid="issue-viewer-issue-container"]`)
        .length > 0
    );
  }

  async function wait_for_page_render(timeout_ms = 1000) {
    return new Promise((resolve) => {
      let timer = null;
      const observer = new MutationObserver(() => {
        clearTimeout(timer);
        refresh();
      });
      function refresh() {
        timer = setTimeout(() => {
          observer.disconnect();
          resolve();
        }, timeout_ms);
      }

      observer.observe(document.documentElement, {
        childList: true,
        subtree: true,
        attributes: true,
        characterData: true,
      });
      refresh();
    });
  }

  async function do_inject() {
    await wait_for_page_render();
    if (is_injected()) return;
    if (!is_issues_page()) return;
    const anchor_span = document.querySelector(
      `[data-component="PH_Actions"] [class*="CopyToClipboardButton"]`
    );
    anchor_span.after(
      html_tpls.btn(() => {
        const markdown_content = get_issues_markdown_content();
        const textarea = document.createElement("textarea");
        textarea.value = markdown_content;
        document.body.appendChild(textarea);
        textarea.select();
        document.execCommand("copy");
        document.body.removeChild(textarea);
        alert("[issues_copy]copy success");
        console.log("[issues_copy]copy success");
        console.log(markdown_content);
      })
    );
  }

  do_inject();

  window.addEventListener("pushState", () => {
    do_inject();
  });
  window.addEventListener("replaceState", () => {
    do_inject();
  });

  // ----------------------------
  // 拓展区域
  // ----------------------------
  /**
   *
   * @param {HTMLElement} element
   * @returns {boolean}
   */
  function is_hide_element(element) {
    // 判断是否为隐藏
    return (
      element.hasAttribute("hidden") ||
      element.classList.contains("hidden") ||
      element.style.display === "none" ||
      getComputedStyle(element).display === "none" ||
      getComputedStyle(element).visibility === "hidden" ||
      getComputedStyle(element).opacity === "0"
    );
  }

  /**
   *
   * @param {HTMLElement} element
   * @returns {string}
   */
  function htmlElementToMarkdown(element) {
    /**
     *
     * @param {HTMLElement} node
     * @returns string
     */
    function process(node) {
      if (node.nodeType === Node.TEXT_NODE) {
        return node.textContent;
      }

      if (node.nodeType !== Node.ELEMENT_NODE) {
        return "";
      }
      if (is_hide_element(node)) {
        return "";
      }

      const tag = node.tagName.toLowerCase();
      let content = Array.from(node.childNodes).map(process).join("");

      if (content.trim() === "") {
        return "";
      }

      if (node.classList.contains("markdown-body")) {
        content = "\n" + content;
      }

      switch (tag) {
        case "h1":
          return `# ${content}\n\n`;
        case "h2":
          return `## ${content}\n\n`;
        case "h3":
          return `### ${content}\n\n`;
        case "h4":
          return `#### ${content}\n\n`;
        case "h5":
          return `##### ${content}\n\n`;
        case "h6":
          return `###### ${content}\n\n`;
        case "p":
          return `${content}\n\n`;
        case "strong":
        case "b":
          return `**${content}**`;
        case "em":
        case "i":
          return `*${content}*`;
        case "a":
          return `[${content}](${node.getAttribute("href")})`;
        case "code": {
          const code_content = node.innerText;
          return `\`${code_content}\``;
        }
        case "pre": {
          // 尝试查找代码块的语言 父元素上的 `highlight-source-xxx` 就是语言
          const lang =
            Array.from(node.parentElement.classList)
              .find((className) => className.startsWith("highlight-source-"))
              ?.replace("highlight-source-", "") ?? "";
          const code_content = node.innerText;
          return `\n\`\`\`${lang}\n${code_content}\n\`\`\`\n`;
        }
        case "ul":
          return (
            "\n" +
            Array.from(node.children)
              .map((li) => `- ${process(li)}`)
              .join("\n") +
            "\n\n"
          );
        case "ol":
          return (
            "\n" +
            Array.from(node.children)
              .map((li, i) => `${i + 1}. ${process(li)}`)
              .join("\n") +
            "\n\n"
          );
        case "br":
          return "  \n";
        case "blockquote":
          return "> " + content.replace(/\n/g, "\n> ") + "\n\n";
        case "img":
          const alt = node.getAttribute("alt") || "";
          const src = node.getAttribute("src") || "";
          return `![${alt}](${src})`;
        default:
          return content;
      }
    }

    return process(element).trim();
  }

  /**
   *
   * @param {string} html_content
   * @returns {HTMLElement}
   */
  function renderHtml(html_content) {
    const container = document.createElement("div");
    container.innerHTML = html_content;
    return container.children[0];
  }
})();