Bitbucket Collapse In Comment

A userscript that adds a header that can toggle long code and quote blocks in comments

  1. // ==UserScript==
  2. // @name Bitbucket Collapse In Comment
  3. // @version 0.1.1
  4. // @description A userscript that adds a header that can toggle long code and quote blocks in comments
  5. // @license MIT
  6. // @author Rob Garrison
  7. // @namespace https://github.com/Mottie
  8. // @include https://bitbucket.org/*
  9. // @run-at document-idle
  10. // @grant GM_addStyle
  11. // @grant GM_getValue
  12. // @grant GM_setValue
  13. // @grant GM_registerMenuCommand
  14. // @icon https://bitbucket.org/mottie/bitbucket-userscripts/raw/HEAD/images/bitbucket.svg
  15. // ==/UserScript==
  16. (() => {
  17. "use strict";
  18. // hide code/quotes longer than this number of lines
  19. let minLines = GM_getValue("bbic-max-lines", 10),
  20. startCollapsed = GM_getValue("bbic-start-collapsed", true);
  21.  
  22. // syntax highlight class name lookup table
  23. const syntaxClass = {
  24. basic: "HTML",
  25. cs: "C#",
  26. fsharp: "F#",
  27. gfm: "Markdown",
  28. jq: "JSONiq",
  29. shell: "Bash (shell)",
  30. tcl: "Glyph",
  31. tex: "LaTex"
  32. };
  33.  
  34. GM_addStyle(`
  35. .bbic-block.aui-button {
  36. border:#eee 1px solid;
  37. padding:2px 8px 2px 10px;
  38. border-radius:5px 5px 0 0;
  39. position:relative;
  40. top:1px;
  41. cursor:pointer;
  42. font-weight:bold;
  43. display:block;
  44. width:100%;
  45. margin:5px 0 0 0;
  46. }
  47. .bbic-block + .codehilite {
  48. border-top:none;
  49. margin-top:0;
  50. }
  51. .bbic-block:after {
  52. content:"\u25bc ";
  53. float:right;
  54. }
  55. .bbic-block-closed.aui-button {
  56. border-radius:5px;
  57. margin-bottom:10px;
  58. }
  59. .bbic-block-closed:after {
  60. transform: rotate(90deg);
  61. }
  62. .bbic-block-closed + .codehilite, .bbic-block-closed + pre {
  63. display:none;
  64. }
  65. `);
  66.  
  67. function makeToggle(name, lines) {
  68. /* full list of class names from (look at "tm_scope" value)
  69. https://github.com/github/linguist/blob/master/lib/linguist/languages.yml
  70. here are some example syntax highlighted class names:
  71. language-text-html-markdown-source-gfm-apib
  72. language-text-html-basic
  73. language-source-fortran-modern
  74. language-text-tex
  75. */
  76. let n = (name || "").replace(
  77. /(language[-\s]|(source-)|(text-)|(html-)|(markdown-)|(-modern))/g, ""
  78. );
  79. n = (syntaxClass[n] || n).toUpperCase().trim();
  80. return `${n || "Block"} (${lines} lines)`;
  81. }
  82.  
  83. function addToggles(event) {
  84. // issue comments
  85. if ($("#issue-main-content")) {
  86. let indx = 0;
  87. const block = document.createElement("a"),
  88. els = $$(
  89. event && event.type === "ajaxComplete" ?
  90. // only target preview containers on ajaxComplete event
  91. ".preview-container pre" :
  92. ".wiki-content pre"
  93. ),
  94. len = els.length;
  95.  
  96. if (len) {
  97. // "aui-button-primary" = blue button styling
  98. block.className = `bbic-block aui-button aui-button-primary${
  99. startCollapsed ? " bbic-block-closed" : ""
  100. }`;
  101. block.href = "#";
  102.  
  103. // loop with delay to allow user interaction
  104. const loop = () => {
  105. let el, wrap, node, syntaxClass, numberOfLines,
  106. // max number of DOM insertions per loop
  107. max = 0;
  108. while (max < 20 && indx < len) {
  109. if (indx >= len) {
  110. return;
  111. }
  112. el = els[indx];
  113. if (el && !el.classList.contains("bbic-has-toggle")) {
  114. numberOfLines = el.innerHTML.split("\n").length;
  115. if (numberOfLines > minLines) {
  116. syntaxClass = "";
  117. wrap = closest(".codehilite", el);
  118. if (wrap && wrap.classList.contains("codehilite")) {
  119. syntaxClass = (wrap.className || "").replace("codehilite", "");
  120. } else {
  121. // no syntax highlighter defined (not wrapped)
  122. wrap = el;
  123. }
  124. node = block.cloneNode();
  125. node.innerHTML = makeToggle(syntaxClass, numberOfLines);
  126. wrap.parentNode.insertBefore(node, wrap);
  127. el.classList.add("bbic-has-toggle");
  128. if (startCollapsed) {
  129. el.display = "none";
  130. }
  131. max++;
  132. }
  133. }
  134. indx++;
  135. }
  136. if (indx < len) {
  137. setTimeout(() => {
  138. loop();
  139. }, 200);
  140. }
  141. };
  142. loop();
  143. }
  144. }
  145. }
  146.  
  147. function addBindings() {
  148. document.addEventListener("click", event => {
  149. let els, indx, flag;
  150. const el = event.target;
  151. if (el && el.classList.contains("bbic-block")) {
  152. event.preventDefault();
  153. // shift + click = toggle all blocks in a single comment
  154. // shift + ctrl + click = toggle all blocks on page
  155. if (event.shiftKey) {
  156. els = $$(
  157. ".bbic-block",
  158. event.ctrlKey ? "" : closest(".wiki-content", el)
  159. );
  160. indx = els.length;
  161. flag = el.classList.contains("bbic-block-closed");
  162. while (indx--) {
  163. els[indx].classList.toggle("bbic-block-closed", !flag);
  164. }
  165. } else {
  166. el.classList.toggle("bbic-block-closed");
  167. }
  168. removeSelection();
  169. }
  170. });
  171. }
  172.  
  173. function update() {
  174. let toggles = $$(".bbic-block"),
  175. indx = toggles.length;
  176. while (indx--) {
  177. toggles[indx].parentNode.removeChild(toggles[indx]);
  178. }
  179. toggles = $$(".bbic-has-toggle");
  180. indx = toggles.length;
  181. while (indx--) {
  182. toggles[indx].classList.remove("bbic-has-toggle");
  183. }
  184. addToggles();
  185. }
  186.  
  187. function $(selector, el) {
  188. return (el || document).querySelector(selector);
  189. }
  190.  
  191. function $$(selector, el) {
  192. return Array.from((el || document).querySelectorAll(selector));
  193. }
  194.  
  195. function closest(selector, el) {
  196. while (el && el.nodeType === 1) {
  197. if (el.matches(selector)) {
  198. return el;
  199. }
  200. el = el.parentNode;
  201. }
  202. return null;
  203. }
  204.  
  205. function removeSelection() {
  206. // remove text selection - https://stackoverflow.com/a/3171348/145346
  207. const sel = window.getSelection ?
  208. window.getSelection() :
  209. document.selection;
  210. if (sel) {
  211. if (sel.removeAllRanges) {
  212. sel.removeAllRanges();
  213. } else if (sel.empty) {
  214. sel.empty();
  215. }
  216. }
  217. }
  218.  
  219. GM_registerMenuCommand("Set Bitbucket Collapse In Comment Max Lines", () => {
  220. let val = prompt("Minimum number of lines before adding a toggle:",
  221. minLines);
  222. val = parseInt(val, 10);
  223. if (val) {
  224. minLines = val;
  225. GM_setValue("bbic-max-lines", val);
  226. update();
  227. }
  228. });
  229.  
  230. GM_registerMenuCommand("Set Bitbucket Collapse In Comment Initial State", () => {
  231. let val = prompt(
  232. "Start with blocks (c)ollapsed or (e)xpanded (first letter necessary):",
  233. startCollapsed ? "collapsed" : "expanded"
  234. );
  235. if (val) {
  236. val = /^c/.test(val || "");
  237. startCollapsed = val;
  238. GM_setValue("bbic-start-collapsed", val);
  239. update();
  240. }
  241. });
  242.  
  243. document.addEventListener("pjax:end", addToggles);
  244. // listen for ajax on preview (jQuery ajaxComplete)
  245. jQuery(document).on("ajaxComplete", addToggles);
  246.  
  247. addBindings();
  248. addToggles();
  249.  
  250. })();