Easy Markdown for StackExchange

Adds "Markdown" and "Copy" buttons to display original Markdown content in StackExchange sites.

  1. // ==UserScript==
  2. // @name Easy Markdown for StackExchange
  3. // @name:zh-CN 在StackExchange中显示Markdown
  4. // @version 2.4
  5. // @author Murphy Lo (http://github.com/MurphyLo)
  6. // @description Adds "Markdown" and "Copy" buttons to display original Markdown content in StackExchange sites.
  7. // @description:zh-CN 在StackExchange网站上添加“Markdown”和“Copy”按钮,以显示原始Markdown内容
  8. // @license GNU GPL v3 (http://www.gnu.org/copyleft/gpl.html)
  9. // @match https://*.stackoverflow.com/questions/*
  10. // @match https://*.superuser.com/questions/*
  11. // @match https://*.serverfault.com/questions/*
  12. // @match https://*.stackexchange.com/questions/*
  13. // @match https://*.askubuntu.com/questions/*
  14. // @match https://*.math.stackexchange.com/questions/*
  15. // @match https://*.tex.stackexchange.com/questions/*
  16. // @match https://*.english.stackexchange.com/questions/*
  17. // @match https://*.gaming.stackexchange.com/questions/*
  18. // @match https://*.physics.stackexchange.com/questions/*
  19. // @match https://*.chemistry.stackexchange.com/questions/*
  20. // @match https://*.biology.stackexchange.com/questions/*
  21. // @match https://*.programmers.stackexchange.com/questions/*
  22. // @match https://*.electronics.stackexchange.com/questions/*
  23. // @grant none
  24. // @namespace https://gf.zukizuki.org/users/1224148
  25. // ==/UserScript==
  26.  
  27. // This script is a modified version of an original script by Manish Goregaokar (http://stackapps.com/users/10098/manishearth)
  28. // Original script: https://github.com/Manishearth/Manish-Codes/raw/master/StackExchange/PrintPost.user.js
  29. // The original script is licensed under the GNU GPL v3 (http://www.gnu.org/copyleft/gpl.html)
  30. // Modifications made by Murphy Lo
  31.  
  32. (async function() {
  33. 'use strict';
  34.  
  35. // Function to fetch content of a post
  36. async function fetchMarkdown(postId) {
  37. const response = await fetch(`/posts/${postId}/edit-inline`);
  38. // Check user login status.
  39. if (response.status === 404) {
  40. return "--- Please LOG IN StackExchange/Stackoverflow to view and copy the Markdown content. ---";
  41. }
  42.  
  43. const data = await response.text();
  44. let markdown = data.match(/<textarea[^>]*>([\s\S]*?)<\/textarea>/)[1];
  45. // Decode HTML entities
  46. return decodeHtmlEntities(markdown);
  47. }
  48.  
  49. // Function to decode HTML entities from the content into Markdown plain text
  50. function decodeHtmlEntities(str) {
  51. const tempElement = document.createElement('div');
  52. tempElement.innerHTML = str;
  53. return tempElement.textContent;
  54. }
  55.  
  56. // Function to show markdown content in a modal
  57. function showMarkdown(event) {
  58. // Prevent the default action
  59. event.preventDefault();
  60.  
  61. // Disable scrolling on the main page
  62. const scrollbarWidth = window.innerWidth - document.documentElement.clientWidth;
  63. document.body.style.overflow = 'hidden';
  64. document.body.style.paddingRight = `${scrollbarWidth}px`;
  65.  
  66. // Add paddingRight to fixed positioned elements
  67. const fixedElements = document.querySelectorAll('header.s-topbar');
  68. fixedElements.forEach(el => el.style.paddingRight = `${scrollbarWidth}px`);
  69.  
  70. // Create a modal to display the markdown (updated style)
  71. const modal = document.createElement('div');
  72. modal.style.cssText = `
  73. position: fixed;
  74. z-index: 5051;
  75. left: 0;
  76. top: 0;
  77. width: 100%;
  78. height: 100%;
  79. overflow: none;
  80. background-color: rgba(0,0,0,0.5);`;
  81.  
  82. const modalContent = document.createElement('div');
  83. modalContent.style.cssText = `
  84. background-color: #FFF;
  85. margin: 5% auto;
  86. padding: 20px;
  87. border: 1px solid #888;
  88. width: 70%;
  89. max-height: 90%;
  90. overflow-y: auto;
  91. box-sizing: border-box;
  92. border-radius: 5px;
  93. box-shadow: 0 4px 8px 0 rgba(0,0,0,0.3);`;
  94.  
  95. // Updated close button style
  96. const closeButton = document.createElement('span');
  97. closeButton.style.cssText = `
  98. color: #aaa;
  99. float: right;
  100. font-size: 28px;
  101. font-weight: bold;
  102. cursor: pointer;
  103. margin-left: 10px;`;
  104. closeButton.textContent = '✕';
  105. closeButton.onmouseover = () => closeButton.style.color = 'black';
  106. closeButton.onmouseout = () => closeButton.style.color = '#aaa';
  107.  
  108. const markdownElement = document.createElement('pre');
  109. markdownElement.style.cssText = `
  110. margin: 0;
  111. white-space: pre-wrap;
  112. word-wrap: break-word;
  113. overflow-y: auto;`;
  114. markdownElement.textContent = this.markdownContent;
  115.  
  116. modalContent.appendChild(closeButton);
  117. modalContent.appendChild(markdownElement);
  118. modal.appendChild(modalContent);
  119. document.body.appendChild(modal);
  120.  
  121. // Close modal behaviors
  122. function closeModal() {
  123. // Removing the modal from the DOM
  124. document.body.removeChild(modal);
  125. document.removeEventListener('keydown', handleKeyDown);
  126.  
  127. // Re-enable scrolling on the main page and remove the paddingRight
  128. document.body.style.overflow = '';
  129. document.body.style.paddingRight = '';
  130.  
  131. // Remove paddingRight from fixed positioned elements
  132. const fixedElements = document.querySelectorAll('header.s-topbar');
  133. fixedElements.forEach(el => el.style.paddingRight = '');
  134. }
  135.  
  136. // Click `x` to close modal
  137. closeButton.onclick = closeModal;
  138. // Press `Esc` to close modal
  139. const handleKeyDown = (event) => event.keyCode === 27 && closeModal(); // 27 is the keyCode for the Esc key
  140. document.addEventListener('keydown', handleKeyDown);
  141. // Click modal background to close modal
  142. modal.onclick = (e) => e.target === modal && closeModal();
  143. }
  144.  
  145. // Function to copy text to clipboard
  146. async function copyToClipboard(text) {
  147. try {
  148. await navigator.clipboard.writeText(text);
  149. // console.log('Markdown content copied to clipboard');
  150. } catch (err) {
  151. console.error('Failed to copy Markdown content: ', err);
  152. }
  153. }
  154.  
  155. // Add "Markdown" and "Copy" buttons to each post
  156. const posts = document.querySelectorAll('.question, .answer');
  157. for (const post of posts) {
  158. const postMenu = post.querySelector('.d-flex.gs8.s-anchors.s-anchors__muted.fw-wrap');
  159. const separator = document.createElement('span');
  160. separator.className = 'lsep';
  161. separator.textContent = '|';
  162. postMenu.appendChild(separator);
  163.  
  164. // Add Markdown button
  165. const printButton = document.createElement('a');
  166. printButton.href = '#';
  167. printButton.textContent = 'Markdown';
  168. printButton.title = 'View this post\'s Markdown content';
  169. printButton.onclick = showMarkdown;
  170. printButton.style.cssText = `
  171. background-color: #f5f5f5;
  172. padding: 5px 10px;
  173. border-radius: 5px;
  174. cursor: pointer;
  175. transition: background-color 0.3s;`;
  176. printButton.onmouseover = () => printButton.style.backgroundColor = '#e0e0e0';
  177. printButton.onmouseout = () => printButton.style.backgroundColor = '#f5f5f5';
  178.  
  179. const printButtonWrapper = document.createElement('div');
  180. printButtonWrapper.className = 'flex--item';
  181. printButtonWrapper.appendChild(printButton);
  182.  
  183. postMenu.appendChild(printButtonWrapper);
  184.  
  185. // Add Copy button
  186. const copyButton = document.createElement('a');
  187. copyButton.href = '#';
  188. copyButton.textContent = 'Copy';
  189. copyButton.title = 'Copy this post\'s Markdown content to clipboard';
  190. copyButton.onclick = (event) => {
  191. event.preventDefault();
  192. copyToClipboard(printButton.markdownContent);
  193. copyButton.textContent = 'Copied!';
  194. setTimeout(() => copyButton.textContent = 'Copy', 2000);
  195. };
  196. copyButton.style.cssText = `
  197. background-color: #f5f5f5;
  198. padding: 5px 10px;
  199. border-radius: 5px;
  200. cursor: pointer;
  201. margin-left: 5px;
  202. transition: background-color 0.3s;`;
  203. copyButton.onmouseover = () => copyButton.style.backgroundColor = '#e0e0e0';
  204. copyButton.onmouseout = () => copyButton.style.backgroundColor = '#f5f5f5';
  205.  
  206. const copyButtonWrapper = document.createElement('div');
  207. copyButtonWrapper.className = 'flex--item';
  208. copyButtonWrapper.appendChild(copyButton);
  209.  
  210. postMenu.appendChild(copyButtonWrapper);
  211.  
  212. // Fetch and store markdown content
  213. const postId = post.dataset.questionid || post.dataset.answerid;
  214. printButton.markdownContent = await fetchMarkdown(postId);
  215. }
  216. })();