Medium: Editor For Programmers

Use `code` for inline code. Automatically fix quotes in code tags. Link to a section of text easily.

  1. // ==UserScript==
  2. // @name Medium: Editor For Programmers
  3. // @namespace https://github.com/Zren/
  4. // @version 5
  5. // @description Use `code` for inline code. Automatically fix quotes in code tags. Link to a section of text easily.
  6. // @author Zren
  7. // @icon https://medium.com/favicon.ico
  8. // @match https://medium.com/p/*/edit
  9. // @grant none
  10. // ==/UserScript==
  11.  
  12. (function(){
  13. function wrapInlineCode() {
  14. var sel = window.getSelection();
  15. if (sel.type === 'Caret' && sel.focusNode.nodeName === "#text") {
  16. // Note: Does not trigger if first character in text node, since the focus isn't a Text node.
  17. //var tagBefore = '<code class="markup--code' + (sel.focusNode.parentNode.tagName === 'P' ? 'markup--p-code' : '') + '"><strong class="markup--strong markup--code-strong">';
  18. //var tagAfter = '</strong></code>';
  19. //var tagBefore = '<code class="markup--code' + (sel.focusNode.parentNode.tagName === 'P' ? 'markup--p-code' : '') + '">';
  20. var tagBefore = '<code>';
  21. var tagAfter = '</code>';
  22. var str = sel.focusNode.nodeValue;
  23. var before = str.substr(0, sel.focusOffset - 1);
  24. var after = str.substr(sel.focusOffset);
  25. console.log("Before: ", before);
  26. console.log("After: ", after);
  27. var index = before.lastIndexOf('`');
  28. if (index >= 0) {
  29. // we just typed the right quote
  30. if (index === before.length-1) {
  31. // ``
  32. // Ignore
  33. } else {
  34. // `...`
  35. var range = document.createRange();
  36. range.setStart(sel.focusNode, index);
  37. range.setEnd(sel.focusNode, sel.focusOffset);
  38. sel.removeAllRanges();
  39. sel.addRange(range);
  40. var html = range.toString();
  41. html = html.substr(1, html.length-2); // trim ``
  42. html = tagBefore + html + tagAfter + ' ';
  43. document.execCommand('insertHTML', false, html);
  44. range.collapse(false); // move cursor to end
  45. return;
  46. }
  47. }
  48. var index = after.indexOf('`');
  49. if (index >= 0) {
  50. // we just typed the left quote
  51. if (index == 0) {
  52. // ``
  53. // Ignore
  54. } else {
  55. var range = document.createRange();
  56. range.setStart(sel.focusNode, sel.focusOffset - 1);
  57. range.setEnd(sel.focusNode, sel.focusOffset + index + 1);
  58. sel.removeAllRanges();
  59. sel.addRange(range);
  60. var html = range.toString();
  61. html = html.substr(1, html.length-2); // trim ``
  62. html = tagBefore + html + tagAfter + ' ';
  63. document.execCommand('insertHTML', false, html);
  64. range.collapse(false); // move cursor to end
  65. return;
  66. }
  67. }
  68. }
  69. }
  70. function alwaysBrInPre(e) {
  71. var sel = window.getSelection();
  72. if (sel.type === 'Caret' && sel.focusNode.nodeName === "#text") {
  73. console.log(sel.focusNode.nodeValue.length, sel.focusOffset, sel.focusNode.nodeValue.substr(sel.focusOffset), sel.focusNode);
  74. if (sel.focusOffset !== 0) return; // End of line = selecting start of next line.
  75.  
  76. // Focused on start of line.
  77. var el = sel.focusNode;
  78. while (el) {
  79. if (!el.parentNode) break; // Don't run on #document element since it doesn't have el.hasAttribute
  80. if (el.classList && el.classList.contains('section-inner')) break; // Ignore everything outside the post itself.
  81. if (el != el.parentNode.firstChild) break; // Only match end of line = start of next line.
  82. if (el.parentNode.tagName == 'PRE') {
  83. // Insert linebreak <br>
  84. var secondPre = el.parentNode;
  85. var firstPre = secondPre.previousSibling;
  86. firstPre.appendChild(document.createElement('br')); // <br> removed during split
  87. firstPre.appendChild(document.createTextNode(''));
  88. var newFocusLine = document.createElement('br');
  89. firstPre.appendChild(newFocusLine); // The actual <br> we wanted to enter.
  90. // Move all elements back into the first pre.
  91. while (secondPre.firstChild) {
  92. firstPre.appendChild(secondPre.firstChild);
  93. }
  94. // Delete the second <pre>
  95. // We can't remove it since it will break the entire editor...
  96. secondPre.appendChild(document.createElement('br'));
  97. //secondPre.remove();
  98. //document.execCommand('delete');
  99. // Move cursor to new line.
  100. var range = document.createRange();
  101. range.setStart(newFocusLine, 0);
  102. range.collapse(true);
  103. sel.removeAllRanges();
  104. sel.addRange(range);
  105. e.preventDefault();
  106. }
  107. el = el.parentNode;
  108. }
  109. }
  110. }
  111.  
  112. function onKeyDown(e) {
  113. if (e.key === '`') {
  114. setTimeout(wrapInlineCode, 100); // Wait for ` to be written so we can replace it
  115. } else if (e.keyCode == 9) { // Tab
  116. e.preventDefault();
  117. } else if (e.keyCode == 13) { // Enter
  118. alwaysBrInPre(e);
  119. } else if (e.key == '6' && e.ctrlKey && e.altKey) {
  120. console.log('CTRL+ALT+6', e);
  121. } else {
  122. console.log('Key:', e.key, e.ctrlKey, e.altKey);
  123. }
  124. }
  125. function fixQuotes() {
  126. // Fix quotes in <pre> and <code> tags.
  127. setInterval(function(){
  128. var codeTags = document.querySelectorAll('pre, code');
  129. for (var tag of codeTags) {
  130. if (tag.innerHTML.indexOf('“') >= 0 || tag.innerHTML.indexOf('”') >= 0 || tag.innerHTML.indexOf('‘') >= 0 || tag.innerHTML.indexOf('’') >= 0) {
  131. tag.innerHTML = tag.innerHTML.replace('“', '"').replace('”', '"').replace('‘', '\'').replace('’', '\'');
  132. }
  133. }
  134. }, 1000);
  135. }
  136. function showPermalink() {
  137. // Setup (temporary) permalink to line.
  138. var tag = document.createElement('a');
  139. tag.style.position = 'absolute';
  140. tag.style.display = 'block';
  141. tag.style.top = '-9999px';
  142. tag.style.left = 0;
  143. tag.style.color = '#888';
  144. tag.innerHTML = '¶'; //'[link]';
  145. document.body.appendChild(tag);
  146.  
  147. function onMouseOver(e) {
  148. var el = e.relatedTarget || e.target;
  149. while (el) {
  150. if (!el.parentNode) break; // Don't run on #document element since it doesn't have el.hasAttribute
  151. if (el.classList.contains('section-inner')) break; // Ignore everything outside the post itself.
  152. if (el.hasAttribute('name')) {
  153. showTag(el)
  154. break;
  155. }
  156. el = el.parentNode;
  157. }
  158. }
  159. function showTag(el) {
  160. var rect = el.getBoundingClientRect();
  161. var url = document.querySelector('link[rel="canonical"]').href;
  162. url = url.substr(0, url.length - '/edit'.length);
  163. url += '#' + el.getAttribute('name');
  164. tag.href = 'javascript:window.history.replaceState({}, "", "' + url + '")';
  165. var tagGuide = document.querySelector('.section-inner');
  166. var tagGuideRect = tagGuide.getBoundingClientRect();
  167. var tagRect = tag.getBoundingClientRect();
  168. tag.style.left = '' + (tagGuideRect.left + window.scrollX - tagRect.width - 60) + 'px';
  169. tag.style.top = '' + (rect.top + window.scrollY) + 'px';
  170. }
  171.  
  172. var taggedElements = document.querySelectorAll('.postArticle-content');
  173. for (var el of taggedElements) {
  174. //el.addEventListener('mouseover', onMouseOver, true);
  175. el.addEventListener('click', onMouseOver, true);
  176. }
  177. }
  178. function onLoad() {
  179. // Bind keys
  180. var main = document.querySelector('main[contenteditable="true"]');
  181. main.addEventListener('keydown', onKeyDown, true);
  182. fixQuotes();
  183. showPermalink();
  184. }
  185.  
  186. function waitForLoad() {
  187. var main = document.querySelector('main[contenteditable="true"]');
  188. if (main) {
  189. onLoad();
  190. console.log('[Medium: Markdown] Loaded');
  191. } else {
  192. setTimeout(waitForLoad, 100);
  193. }
  194. }
  195. setTimeout(waitForLoad, 100);
  196. })();
  197.