GitHub RTL Comments

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

As of 2023-07-01. See the latest version.

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