GitHub RTL Comment Blocks

A userscript that adds a button to insert RTL text blocks in comments

As of 2017-03-25. See the latest version.

  1. // ==UserScript==
  2. // @name GitHub RTL Comment Blocks
  3. // @version 1.2.4
  4. // @description A userscript that adds a button to insert RTL text blocks in comments
  5. // @license https://creativecommons.org/licenses/by-sa/4.0/
  6. // @namespace https://github.com/Mottie
  7. // @include https://github.com/*
  8. // @include https://gist.github.com/*
  9. // @require https://greatest.deepsurf.us/scripts/28239-rangy-inputs-mod-js/code/rangy-inputs-modjs.js?version=181769
  10. // @run-at document-idle
  11. // @grant GM_addStyle
  12. // @connect github.com
  13. // @author Rob Garrison
  14. // ==/UserScript==
  15. (() => {
  16. "use strict";
  17.  
  18. const icon = `
  19. <svg class="octicon" xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 14 14">
  20. <path d="M14 3v8l-4-4m-7 7V6C1 6 0 5 0 3s1-3 3-3h7v2H9v12H7V2H5v12H3z"/>
  21. </svg>`,
  22.  
  23. // maybe using &#x2067; RTL text &#x2066; (isolates) is a better combo?
  24. openRTL = "&rlm;", // https://en.wikipedia.org/wiki/Right-to-left_mark
  25. closeRTL = "&lrm;", // https://en.wikipedia.org/wiki/Left-to-right_mark
  26.  
  27. regexOpen = /\u200f/ig,
  28. regexClose = /\u200e/ig,
  29. regexSplit = /(\u200f|\u200e)/ig;
  30.  
  31. GM_addStyle(`
  32. .ghu-rtl-css { direction:rtl; text-align:right; }
  33. /* delegated binding; ignore clicks on svg & path */
  34. .ghu-rtl > * { pointer-events:none; }
  35. /* override RTL on code blocks */
  36. .js-preview-body pre, .markdown-body pre,
  37. .js-preview-body code, .markdown-body code {
  38. direction:ltr;
  39. text-align:left;
  40. unicode-bidi:normal;
  41. }
  42. `);
  43.  
  44. // Add RTL button
  45. function addRtlButton() {
  46. let el, button,
  47. toolbars = $$(".toolbar-commenting"),
  48. indx = toolbars.length;
  49. if (indx) {
  50. button = document.createElement("button");
  51. button.type = "button";
  52. button.className = "ghu-rtl toolbar-item tooltipped tooltipped-n";
  53. button.setAttribute("aria-label", "RTL");
  54. button.setAttribute("tabindex", "-1");
  55. button.innerHTML = icon;
  56. while (indx--) {
  57. el = toolbars[indx];
  58. if (!$(".ghu-rtl", el)) {
  59. el.insertBefore(button.cloneNode(true), el.childNodes[0]);
  60. }
  61. }
  62. }
  63. checkRTL();
  64. }
  65.  
  66. function checkContent(el) {
  67. // check the contents, and wrap in either a span or div
  68. let indx, // useDiv,
  69. html = el.innerHTML,
  70. parts = html.split(regexSplit),
  71. len = parts.length;
  72. for (indx = 0; indx < len; indx++) {
  73. if (regexOpen.test(parts[indx])) {
  74. // check if the content contains HTML
  75. // useDiv = regexTestHTML.test(parts[indx + 1]);
  76. // parts[indx] = (useDiv ? "<div" : "<span") + " class='ghu-rtl-css'>";
  77. parts[indx] = "<div class='ghu-rtl-css'>";
  78. } else if (regexClose.test(parts[indx])) {
  79. // parts[indx] = useDiv ? "</div>" : "</span>";
  80. parts[indx] = "</div>";
  81. }
  82. }
  83. el.innerHTML = parts.join("");
  84. // remove empty paragraph wrappers (may have previously contained the mark)
  85. return el.innerHTML.replace(/<p><\/p>/g, "");
  86. }
  87.  
  88. function checkRTL() {
  89. let clone,
  90. indx = 0,
  91. div = document.createElement("div"),
  92. containers = $$(".js-preview-body, .markdown-body"),
  93. len = containers.length,
  94. // main loop
  95. loop = () => {
  96. let el, tmp,
  97. max = 0;
  98. while (max < 10 && indx < len) {
  99. if (indx > len) {
  100. return;
  101. }
  102. el = containers[indx];
  103. tmp = el.innerHTML;
  104. if (regexOpen.test(tmp) || regexClose.test(tmp)) {
  105. clone = div.cloneNode();
  106. clone.innerHTML = tmp;
  107. // now we can replace all instances
  108. el.innerHTML = checkContent(clone);
  109. max++;
  110. }
  111. indx++;
  112. }
  113. if (indx < len) {
  114. setTimeout(() => {
  115. loop();
  116. }, 200);
  117. }
  118. };
  119. loop();
  120. }
  121.  
  122. function addBindings() {
  123. window.rangyInput.init();
  124. $("body").addEventListener("click", event => {
  125. let textarea,
  126. target = event.target;
  127. if (target && target.classList.contains("ghu-rtl")) {
  128. textarea = closest(".previewable-comment-form", target);
  129. textarea = $(".comment-form-textarea", textarea);
  130. textarea.focus();
  131. // add extra white space around the tags
  132. window.rangyInput.surroundSelectedText(
  133. textarea,
  134. " " + openRTL + " ",
  135. " " + closeRTL + " "
  136. );
  137. return false;
  138. }
  139. });
  140. }
  141.  
  142. function $(selector, el) {
  143. return (el || document).querySelector(selector);
  144. }
  145.  
  146. function $$(selector, el) {
  147. return Array.from((el || document).querySelectorAll(selector));
  148. }
  149.  
  150. function closest(selector, el) {
  151. while (el && el.nodeType === 1) {
  152. if (el.matches(selector)) {
  153. return el;
  154. }
  155. el = el.parentNode;
  156. }
  157. return null;
  158. }
  159.  
  160. document.addEventListener("pjax:end", addRtlButton);
  161. // "preview:render" only fires when using the hotkey :(
  162. // "preview:setup" fires on hover & click
  163. document.addEventListener("preview:setup", event => {
  164. if (event.target && event.target.classList.contains("preview-selected")) {
  165. // must include some rendering time...
  166. // 200 ms seems to be enough for a 1100+ line markdown file
  167. setTimeout(() => {
  168. checkRTL();
  169. }, 500);
  170. }
  171. });
  172. addBindings();
  173. addRtlButton();
  174. })();