GitHub Toggle Code Wrap

A userscript that adds a code wrap toggle button

  1. // ==UserScript==
  2. // @name GitHub Toggle Code Wrap
  3. // @version 1.1.11
  4. // @description A userscript that adds a code wrap toggle button
  5. // @license MIT
  6. // @author StylishThemes
  7. // @namespace https://github.com/StylishThemes
  8. // @include https://github.com/*
  9. // @include https://gist.github.com/*
  10. // @run-at document-idle
  11. // @grant GM_registerMenuCommand
  12. // @grant GM_getValue
  13. // @grant GM_setValue
  14. // @grant GM_addStyle
  15. // @require https://greasyfork.org/scripts/28721-mutations/code/mutations.js?version=634242
  16. // @icon https://avatars3.githubusercontent.com/u/6145677?v=3&s=200
  17. // @homepageURL https://github.com/StylishThemes/GitHub-Dark-Script
  18. // ==/UserScript==
  19. (() => {
  20. "use strict";
  21. // This code is also part of the GitHub-Dark Script
  22. // (https://github.com/StylishThemes/GitHub-Dark-Script)
  23. // Extracted out into a separate userscript in case users only want
  24. // to add this functionality
  25. // set by GM popup menu
  26. let globalWrap = GM_getValue("github-global-code-wrap", true),
  27. busy = false;
  28.  
  29. const wrapIcon = `
  30. <svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 768 768">
  31. <path d="M544.5 352.5q52.5 0 90 37.5t37.5 90-37.5 90-90 37.5H480V672l-96-96 96-96v64.5h72q25.5 0 45-19.5t19.5-45-19.5-45-45-19.5H127.5v-63h417zm96-192v63h-513v-63h513zm-513 447v-63h192v63h-192z"/>
  32. </svg>`,
  33.  
  34. // inline code wrap css
  35. wrapCss = {
  36. "wrapped": "white-space: pre-wrap !important; word-break: break-all !important; overflow-wrap: break-word !important; display: block !important;",
  37. "unwrap": "white-space: pre !important; word-break: normal !important; display: block !important;"
  38. };
  39.  
  40. function findWrap(event) {
  41. const target = event.target;
  42. if (target.classList.contains("ghd-wrap-toggle")) {
  43. toggleClasses(target);
  44. }
  45. }
  46.  
  47. function findSibling(node, selector) {
  48. node = node.parentNode.firstElementChild;
  49. while ((node = node.nextElementSibling)) {
  50. if (node.matches(selector)) {
  51. return node;
  52. }
  53. }
  54. return null;
  55. }
  56.  
  57. function toggleClasses(button) {
  58. let css;
  59. const target = findSibling(button, "code, pre, .highlight, .diff-table");
  60.  
  61. if (!target) {
  62. console.error("Code wrap icon associated code not found", button);
  63. return;
  64. }
  65. // code with line numbers
  66. if (target.nodeName === "TABLE") {
  67. if (!target.className.includes("wrap-table")) {
  68. css = !globalWrap;
  69. } else {
  70. css = target.classList.contains("ghd-unwrap-table");
  71. }
  72. target.classList.toggle("ghd-wrap-table", css);
  73. target.classList.toggle("ghd-unwrap-table", !css);
  74. button.classList.toggle("wrapped", css);
  75. button.classList.toggle("unwrap", !css);
  76. } else {
  77. css = target.getAttribute("style") || "";
  78. if (css === "") {
  79. css = wrapCss[globalWrap ? "unwrap" : "wrapped"];
  80. } else {
  81. css = wrapCss[css === wrapCss.wrapped ? "unwrap" : "wrapped"];
  82. }
  83. target.setAttribute("style", css);
  84. button.classList.toggle("wrapped", css === wrapCss.wrapped);
  85. button.classList.toggle("unwrap", css === wrapCss.wrapped);
  86. }
  87. }
  88.  
  89. function addCodeWrapButton(button, target) {
  90. target.insertBefore(button.cloneNode(true), target.childNodes[0]);
  91. target.classList.add("ghd-code-wrapper");
  92. }
  93.  
  94. function moveMenu(codeWrap) {
  95. const menu = $("details", codeWrap);
  96. if (menu) {
  97. menu.classList.add("ghd-menu");
  98. codeWrap.parentNode.appendChild(menu);
  99. }
  100. }
  101.  
  102. // Add code wrap toggle
  103. function buildCodeWrap() {
  104. if (busy) {
  105. return;
  106. }
  107. busy = true;
  108.  
  109. // add wrap code buttons
  110. let wrapper = $$(".blob-wrapper"),
  111. indx = wrapper ? wrapper.length : 0;
  112. const button = document.createElement("button");
  113. button.className = `ghd-wrap-toggle tooltipped tooltipped-sw btn btn-sm${
  114. globalWrap ? "" : " unwrap"}`;
  115. button.setAttribute("aria-label", "Toggle code wrap");
  116. button.innerHTML = wrapIcon;
  117.  
  118. // Code in table with line numbers
  119. while (indx--) {
  120. if (!$(".ghd-wrap-toggle", wrapper[indx])) {
  121. addCodeWrapButton(button, wrapper[indx]);
  122. moveMenu(wrapper[indx]); // Fixes #66
  123. }
  124. }
  125.  
  126. // Code in markdown comments & wiki pages
  127. wrapper = $$(`
  128. .markdown-body pre:not(.ghd-code-wrapper),
  129. .markdown-format pre:not(.ghd-code-wrapper)`
  130. );
  131. indx = wrapper ? wrapper.length : 0;
  132. while (indx--) {
  133. const pre = wrapper[indx];
  134. const code = $("code", pre);
  135. const wrap = pre.parentNode;
  136. if (code) {
  137. addCodeWrapButton(button, pre);
  138. } else if (wrap.classList.contains("highlight")) {
  139. addCodeWrapButton(button, wrap);
  140. }
  141. }
  142. busy = false;
  143. }
  144.  
  145. function init() {
  146. document.addEventListener("click", findWrap);
  147. $("body").classList.toggle("nowrap", !globalWrap);
  148. buildCodeWrap();
  149. }
  150.  
  151. function $(str, el) {
  152. return (el || document).querySelector(str);
  153. }
  154.  
  155. function $$(str, el) {
  156. return [...(el || document).querySelectorAll(str)];
  157. }
  158.  
  159. // don't initialize if GitHub Dark Script is active
  160. if (!$("#ghd-menu")) {
  161. GM_addStyle(`
  162. /* icons next to a pre */
  163. .ghd-wrap-toggle {
  164. padding: 3px 5px;
  165. position: absolute;
  166. right: 3px;
  167. top: 3px;
  168. -moz-user-select: none;
  169. -webkit-user-select: none;
  170. cursor: pointer;
  171. z-index: 20;
  172. }
  173. .ghd-code-wrapper:not(:hover) .ghd-wrap-toggle {
  174. border-color: transparent !important;
  175. background: transparent !important;
  176. }
  177. .ghd-menu {
  178. margin-top: 45px;
  179. }
  180. /* file & diff code tables */
  181. body .ghd-wrap-table td.blob-code-inner:not(.blob-code-hunk) {
  182. white-space: pre-wrap !important;
  183. word-break: break-all !important;
  184. }
  185. body .ghd-unwrap-table td.blob-code-inner:not(.blob-code-hunk) {
  186. white-space: pre !important;
  187. word-break: normal !important;
  188. }
  189. /* icons for non-syntax highlighted code blocks;
  190. * see https://github.com/gjtorikian/html-proofer/blob/master/README.md
  191. */
  192. .markdown-body:not(.comment-body) .ghd-wrap-toggle:not(:first-child) {
  193. right: 3.4em;
  194. }
  195. .ghd-wrap-toggle svg {
  196. height: 14px;
  197. width: 14px;
  198. fill: rgba(110, 110, 110, .4);
  199. pointer-events: none;
  200. vertical-align: text-bottom;
  201. }
  202. .ghd-code-wrapper:hover .ghd-wrap-toggle.unwrap svg,
  203. .ghd-code-wrapper:hover .ghd-wrap-toggle svg {
  204. fill: #8b0000; /* wrap disabled (red) */
  205. }
  206. body:not(.nowrap) .ghd-code-wrapper:hover .ghd-wrap-toggle:not(.unwrap) svg,
  207. .ghd-code-wrapper:hover .ghd-wrap-toggle.wrapped svg {
  208. fill: #006400; /* wrap enabled (green) */
  209. }
  210. .blob-wrapper, .markdown-body pre, .markdown-body .highlight,
  211. .ghd-code-wrapper {
  212. position: relative;
  213. }
  214. /* global code wrap */
  215. body:not(.nowrap) .blob-code-inner:not(.blob-code-hunk),
  216. body:not(.nowrap) .markdown-body pre > code,
  217. body:not(.nowrap) .markdown-body .highlight > pre {
  218. white-space: pre-wrap !important;
  219. word-break: break-all !important;
  220. overflow-wrap: break-word !important;
  221. display: block !important;
  222. }
  223. td.blob-code-inner {
  224. display: table-cell !important;
  225. }
  226. `);
  227.  
  228. document.addEventListener("ghmo:container", buildCodeWrap);
  229. document.addEventListener("ghmo:preview", buildCodeWrap);
  230.  
  231. // Add GM options
  232. GM_registerMenuCommand("Set Global Code Wrap Option", () => {
  233. const body = $("body");
  234. const val = prompt("Global Code Wrap (true/false):", `${globalWrap}`);
  235. globalWrap = val.startsWith("t");
  236. GM_setValue("github-global-code-wrap", globalWrap);
  237. body.classList.toggle("nowrap", !globalWrap);
  238. });
  239.  
  240. init();
  241. }
  242. })();