Github Comment Enhancer

Enhances Github comments

目前为 2014-07-31 提交的版本。查看 最新版本

  1. // ==UserScript==
  2. // @id Github_Comment_Enhancer@https://github.com/jerone/UserScripts
  3. // @name Github Comment Enhancer
  4. // @namespace https://github.com/jerone/UserScripts
  5. // @description Enhances Github comments
  6. // @author jerone
  7. // @copyright 2014+, jerone (http://jeroenvanwarmerdam.nl)
  8. // @license GNU GPLv3
  9. // @homepage https://github.com/jerone/UserScripts/tree/master/Github_Comment_Enhancer
  10. // @homepageURL https://github.com/jerone/UserScripts/tree/master/Github_Comment_Enhancer
  11. // @version 1.6
  12. // @grant none
  13. // @run-at document-end
  14. // @include https://github.com/*/*/issues/*
  15. // @include https://github.com/*/*/pull/*
  16. // @include https://github.com/*/*/commit/*
  17. // @include https://github.com/*/*/compare/*
  18. // @include https://github.com/*/*/wiki/*
  19. // @include https://gist.github.com/*
  20. // ==/UserScript==
  21. /* global unsafeWindow */
  22.  
  23. (function() {
  24.  
  25. String.format = function(string) {
  26. var args = Array.prototype.slice.call(arguments, 1, arguments.length);
  27. return string.replace(/{(\d+)}/g, function(match, number) {
  28. return typeof args[number] !== "undefined" ? args[number] : match;
  29. });
  30. };
  31.  
  32. // Source: https://github.com/gollum/gollum/blob/9c714e768748db4560bc017cacef4afa0c751a63/lib/gollum/public/gollum/javascript/editor/langs/markdown.js
  33. var MarkDown = {
  34. "function-bold": {
  35. search: /^(\s*)([\s\S]*?)(\s*)$/g,
  36. replace: "$1**$2**$3"
  37. },
  38. "function-italic": {
  39. search: /^(\s*)([\s\S]*?)(\s*)$/g,
  40. replace: "$1_$2_$3"
  41. },
  42. "function-strikethrough": {
  43. search: /^(\s*)([\s\S]*?)(\s*)$/g,
  44. replace: "$1~~$2~~$3"
  45. },
  46.  
  47. "function-h1": {
  48. search: /(.+)([\n]?)/g,
  49. replace: "# $1$2",
  50. forceNewline: true
  51. },
  52. "function-h2": {
  53. search: /(.+)([\n]?)/g,
  54. replace: "## $1$2",
  55. forceNewline: true
  56. },
  57. "function-h3": {
  58. search: /(.+)([\n]?)/g,
  59. replace: "### $1$2",
  60. forceNewline: true
  61. },
  62. "function-h4": {
  63. search: /(.+)([\n]?)/g,
  64. replace: "#### $1$2",
  65. forceNewline: true
  66. },
  67. "function-h5": {
  68. search: /(.+)([\n]?)/g,
  69. replace: "##### $1$2",
  70. forceNewline: true
  71. },
  72. "function-h6": {
  73. search: /(.+)([\n]?)/g,
  74. replace: "###### $1$2",
  75. forceNewline: true
  76. },
  77.  
  78. "function-link": {
  79. exec: function(txt, selText, commentForm, next) {
  80. var selTxt = selText.trim(),
  81. isUrl = selTxt && /(?:https?:\/\/)|(?:www\.)/.test(selTxt),
  82. href = window.prompt("Link href:", isUrl ? selTxt : ""),
  83. text = window.prompt("Link text:", isUrl ? "" : selTxt);
  84. if (href) {
  85. next(String.format("[{0}]({1}){2}", text || href, href, (/\s+$/.test(selText) ? " " : "")));
  86. }
  87. }
  88. },
  89. "function-image": {
  90. exec: function(txt, selText, commentForm, next) {
  91. var selTxt = selText.trim(),
  92. isUrl = selTxt && /(?:https?:\/\/)|(?:www\.)/.test(selTxt),
  93. href = window.prompt("Image href:", isUrl ? selTxt : ""),
  94. text = window.prompt("Image text:", isUrl ? "" : selTxt);
  95. if (href) {
  96. next(String.format("![{0}]({1}){2}", text || href, href, (/\s+$/.test(selText) ? " " : "")));
  97. }
  98. }
  99. },
  100.  
  101. "function-ul": {
  102. search: /(.+)([\n]?)/g,
  103. replace: "* $1$2",
  104. forceNewline: true
  105. },
  106. "function-ol": {
  107. exec: function(txt, selText, commentForm, next) {
  108. var repText = "";
  109. if (!selText) {
  110. repText = "1. ";
  111. } else {
  112. var lines = selText.split("\n"),
  113. hasContent = /[\w]+/;
  114. for (var i = 0; i < lines.length; i++) {
  115. if (hasContent.test(lines[i])) {
  116. repText += String.format("$0. $1\n", i + 1, lines[i]);
  117. }
  118. }
  119. }
  120. next(repText);
  121. }
  122. },
  123. "function-checklist": {
  124. search: /(.+)([\n]?)/g,
  125. replace: "* [ ] $1$2",
  126. forceNewline: true
  127. },
  128.  
  129. "function-code": {
  130. exec: function(txt, selText, commentForm, next) {
  131. var rt = selText.indexOf("\n") > -1 ? "$1\n```\n$2\n```$3" : "$1`$2`$3";
  132. next(selText.replace(/^(\s*)([\s\S]*?)(\s*)$/g, rt));
  133. }
  134. },
  135. "function-blockquote": {
  136. search: /(.+)([\n]?)/g,
  137. replace: "> $1$2",
  138. forceNewline: true
  139. },
  140. "function-hr": {
  141. append: "\n***\n",
  142. forceNewline: true
  143. },
  144. "function-table": {
  145. append: "\n" +
  146. "| Head | Head | Head |\n" +
  147. "| :--- | :--: | ---: |\n" +
  148. "| Cell | Cell | Cell |\n" +
  149. "| Cell | Cell | Cell |\n",
  150. forceNewline: true
  151. },
  152.  
  153. "function-clear": {
  154. exec: function(txt, selText, commentForm, next) {
  155. commentForm.value = "";
  156. next("");
  157. }
  158. },
  159.  
  160. "function-snippets-useragent": {
  161. exec: function(txt, selText, commentForm, next) {
  162. next("`" + navigator.userAgent + "`");
  163. }
  164. },
  165. "function-snippets-contributing": {
  166. exec: function(txt, selText, commentForm, next) {
  167. next("Please, always consider reviewing the [guidelines for contributing](../blob/master/CONTRIBUTING.md) to this repository.");
  168. }
  169. }
  170. };
  171.  
  172. var editorHTML = (function editorHTML() {
  173. return '<div id="gollum-editor-function-buttons" style="float: left;">' +
  174. ' <div class="button-group">' +
  175. ' <a href="#" id="function-bold" class="minibutton function-button tooltipped tooltipped-ne" aria-label="Bold">' +
  176. ' <b style="font-weight: bolder;">B</b>' +
  177. ' </a>' +
  178. ' <a href="#" id="function-italic" class="minibutton function-button tooltipped tooltipped-ne" aria-label="Italic">' +
  179. ' <em>i</em>' +
  180. ' </a>' +
  181. ' <a href="#" id="function-strikethrough" class="minibutton function-button tooltipped tooltipped-ne" aria-label="Strikethrough">' +
  182. ' <s>S</s>' +
  183. ' </a>' +
  184. ' </div>' +
  185.  
  186. ' <div class="button-group">' +
  187. ' <div class="select-menu js-menu-container js-select-menu">' +
  188. ' <span class="minibutton select-menu-button icon-only js-menu-target" aria-label="Headers" style="padding:0 7px; width:auto; border-bottom-right-radius:3px; border-top-right-radius:3px;">' +
  189. ' <span class="js-select-button">h#</span>' +
  190. ' </span>' +
  191. ' <div class="select-menu-modal-holder js-menu-content js-navigation-container js-active-navigation-container" style="top: 26px;">' +
  192. ' <div class="select-menu-modal" style="width:auto; overflow:visible;">' +
  193. ' <div class="select-menu-header">' +
  194. ' <span class="select-menu-title">Choose header</span>' +
  195. ' <span class="octicon octicon-remove-close js-menu-close"></span>' +
  196. ' </div>' +
  197. ' <div class="button-group">' +
  198. ' <a href="#" id="function-h1" class="minibutton function-button js-navigation-item js-menu-close tooltipped tooltipped-s" aria-label="Header 1">' +
  199. ' <b class="select-menu-item-text js-select-button-text">h1</b>' +
  200. ' </a>' +
  201. ' <a href="#" id="function-h2" class="minibutton function-button js-navigation-item js-menu-close tooltipped tooltipped-s" aria-label="Header 2">' +
  202. ' <b class="select-menu-item-text js-select-button-text">h2</b>' +
  203. ' </a>' +
  204. ' <a href="#" id="function-h3" class="minibutton function-button js-navigation-item js-menu-close tooltipped tooltipped-s" aria-label="Header 3">' +
  205. ' <b class="select-menu-item-text js-select-button-text">h3</b>' +
  206. ' </a>' +
  207. ' <a href="#" id="function-h4" class="minibutton function-button js-navigation-item js-menu-close tooltipped tooltipped-s" aria-label="Header 4">' +
  208. ' <b class="select-menu-item-text js-select-button-text">h4</b>' +
  209. ' </a>' +
  210. ' <a href="#" id="function-h5" class="minibutton function-button js-navigation-item js-menu-close tooltipped tooltipped-s" aria-label="Header 5">' +
  211. ' <b class="select-menu-item-text js-select-button-text">h5</b>' +
  212. ' </a>' +
  213. ' <a href="#" id="function-h6" class="minibutton function-button js-navigation-item js-menu-close tooltipped tooltipped-s" aria-label="Header 6">' +
  214. ' <b class="select-menu-item-text js-select-button-text">h6</b>' +
  215. ' </a>' +
  216. ' </div>' +
  217. ' </div>' +
  218. ' </div>' +
  219. ' </div>' +
  220. ' </div>' +
  221.  
  222. ' <div class="button-group">' +
  223. ' <a href="#" id="function-link" class="minibutton function-button tooltipped tooltipped-ne" aria-label="Link">' +
  224. ' <span class="octicon octicon-link"></span>' +
  225. ' </a>' +
  226. ' <a href="#" id="function-image" class="minibutton function-button tooltipped tooltipped-ne" aria-label="Image">' +
  227. ' <span class="octicon octicon-file-media"></span>' +
  228. ' </a>' +
  229. ' </div>' +
  230. ' <div class="button-group">' +
  231. ' <a href="#" id="function-ul" class="minibutton function-button tooltipped tooltipped-ne" aria-label="Unordered List">' +
  232. ' <span class="octicon octicon-list-unordered"></span>' +
  233. ' </a>' +
  234. ' <a href="#" id="function-ol" class="minibutton function-button tooltipped tooltipped-ne" aria-label="Ordered List">' +
  235. ' <span class="octicon octicon-list-ordered"></span>' +
  236. ' </a>' +
  237. ' <a href="#" id="function-checklist" class="minibutton function-button tooltipped tooltipped-ne" aria-label="Task List">' +
  238. ' <span class="octicon octicon-checklist"></span>' +
  239. ' </a>' +
  240. ' </div>' +
  241.  
  242. ' <div class="button-group">' +
  243. ' <a href="#" id="function-code" class="minibutton function-button tooltipped tooltipped-ne" aria-label="Code">' +
  244. ' <span class="octicon octicon-code"></span>' +
  245. ' </a>' +
  246. ' <a href="#" id="function-blockquote" class="minibutton function-button tooltipped tooltipped-ne" aria-label="Blockquote">' +
  247. ' <span class="octicon octicon-quote"></span>' +
  248. ' </a>' +
  249. ' <a href="#" id="function-hr" class="minibutton function-button tooltipped tooltipped-ne" aria-label="Horizontal Rule">' +
  250. ' <span class="octicon octicon-horizontal-rule"></span>' +
  251. ' </a>' +
  252. ' <a href="#" id="function-table" class="minibutton function-button tooltipped tooltipped-ne" aria-label="Table">' +
  253. ' <span class="octicon octicon-three-bars"></span>' +
  254. ' </a>' +
  255. ' </div>' +
  256.  
  257. ' <div class="button-group">' +
  258. ' <div class="select-menu js-menu-container js-select-menu">' +
  259. ' <span class="minibutton select-menu-button js-menu-target" aria-label="Snippets" style="padding:0 7px; width:auto; border-bottom-right-radius:3px; border-top-right-radius:3px;">' +
  260. ' <span class="octicon octicon-pin"></span>' +
  261. ' </span>' +
  262. ' <div class="select-menu-modal-holder js-menu-content js-navigation-container js-active-navigation-container">' +
  263. ' <div class="select-menu-modal" style="overflow:visible;">' +
  264. ' <div class="select-menu-header">' +
  265. ' <span class="select-menu-title">Snippets</span>' +
  266. ' <span class="octicon octicon-remove-close js-menu-close"></span>' +
  267. ' </div>' +
  268. ' <div class="select-menu-filters">' +
  269. ' <div class="select-menu-text-filter">' +
  270. ' <input type="text" placeholder="Filter snippets..." class="js-filterable-field js-navigation-enable" id="context-snippets-filter-field">' +
  271. ' </div>' +
  272. ' </div>' +
  273. ' <div class="select-menu-list" style="overflow:visible;">' +
  274. ' <div data-filterable-for="context-snippets-filter-field">' +
  275. ' <a href="#" id="function-snippets-useragent" class="function-button select-menu-item js-navigation-item tooltipped tooltipped-w" aria-label="Add UserAgent" style="table-layout:initial;">' +
  276. ' <span class="select-menu-item-text js-select-button-text">Add UserAgent</span>' +
  277. ' </a>' +
  278. ' <a href="#" id="function-snippets-contributing" class="function-button select-menu-item js-navigation-item tooltipped tooltipped-w" aria-label="Add contributing message" style="table-layout:initial;">' +
  279. ' <span class="select-menu-item-text">' +
  280. ' <span class="js-select-button-text">Contributing</span>' +
  281. ' <span class="description">Add contributing message</span>' +
  282. ' </span>' +
  283. ' </a>' +
  284. ' </div>' +
  285. ' <div class="select-menu-no-results">Nothing to show</div>' +
  286. ' </div>' +
  287. ' </div>' +
  288. ' </div>' +
  289. ' </div>' +
  290. ' </div>' +
  291.  
  292. '</div>' +
  293.  
  294. '<div class="button-group" style="float:right;">' +
  295. ' <a href="#" id="function-clear" class="minibutton function-button tooltipped tooltipped-nw" aria-label="Clear">' +
  296. ' <span class="octicon octicon-circle-slash"></span>' +
  297. ' </a>' +
  298. '</div>';
  299. })();
  300.  
  301. // Source: https://github.com/gollum/gollum/blob/9c714e768748db4560bc017cacef4afa0c751a63/lib/gollum/public/gollum/javascript/editor/gollum.editor.js#L516
  302. function executeAction(definitionObject, commentForm) {
  303. var txt = commentForm.value,
  304. selPos = {
  305. start: commentForm.selectionStart,
  306. end: commentForm.selectionEnd
  307. },
  308. selText = txt.substring(selPos.start, selPos.end),
  309. repText = selText,
  310. reselect = true,
  311. cursor = null;
  312.  
  313. // execute replacement function;
  314. if (definitionObject.exec) {
  315. definitionObject.exec(txt, selText, commentForm, function(repText) {
  316. replaceFieldSelection(commentForm, repText);
  317. });
  318. return;
  319. }
  320.  
  321. // execute a search;
  322. var searchExp = new RegExp(definitionObject.search || /([^\n]+)/gi);
  323.  
  324. // replace text;
  325. if (definitionObject.replace) {
  326. var rt = definitionObject.replace;
  327. repText = repText.replace(searchExp, rt);
  328. repText = repText.replace(/\$[\d]/g, "");
  329. if (repText === "") {
  330. cursor = rt.indexOf("$1");
  331. repText = rt.replace(/\$[\d]/g, "");
  332. if (cursor === -1) {
  333. cursor = Math.floor(rt.length / 2);
  334. }
  335. }
  336. }
  337.  
  338. // append if necessary;
  339. if (definitionObject.append) {
  340. if (repText === selText) {
  341. reselect = false;
  342. }
  343. repText += definitionObject.append;
  344. }
  345.  
  346. if (repText) {
  347. if (definitionObject.forceNewline === true && (selPos.start > 0 && txt.substr(Math.max(0, selPos.start - 1), 1) !== "\n")) {
  348. repText = "\n" + repText;
  349. }
  350. replaceFieldSelection(commentForm, repText, reselect, cursor);
  351. }
  352. }
  353.  
  354. // Source: https://github.com/gollum/gollum/blob/9c714e768748db4560bc017cacef4afa0c751a63/lib/gollum/public/gollum/javascript/editor/gollum.editor.js#L708
  355. function replaceFieldSelection(commentForm, replaceText, reselect, cursorOffset) {
  356. var txt = commentForm.value,
  357. selPos = {
  358. start: commentForm.selectionStart,
  359. end: commentForm.selectionEnd
  360. };
  361.  
  362. var selectNew = true;
  363. if (reselect === false) {
  364. selectNew = false;
  365. }
  366.  
  367. var scrollTop = null;
  368. if (commentForm.scrollTop) {
  369. scrollTop = commentForm.scrollTop;
  370. }
  371.  
  372. commentForm.value = txt.substring(0, selPos.start) + replaceText + txt.substring(selPos.end);
  373. commentForm.focus();
  374.  
  375. if (selectNew) {
  376. if (cursorOffset) {
  377. commentForm.setSelectionRange(selPos.start + cursorOffset, selPos.start + cursorOffset);
  378. } else {
  379. commentForm.setSelectionRange(selPos.start, selPos.start + replaceText.length);
  380. }
  381. }
  382.  
  383. if (scrollTop) {
  384. commentForm.scrollTop = scrollTop;
  385. }
  386. }
  387.  
  388. function isWiki() {
  389. return /\/wiki\//.test(location.href);
  390. }
  391. function isGist() {
  392. return location.host === "gist.github.com";
  393. }
  394.  
  395. function overrideGollumMarkdown() {
  396. unsafeWindow.$.GollumEditor.defineLanguage("markdown", MarkDown);
  397. }
  398. function unbindGollumFunctions() {
  399. window.setTimeout(function() {
  400. unsafeWindow.$(".function-button:not(#function-help)").unbind("click");
  401. }, 1);
  402. }
  403.  
  404. var functionButtonClick = function(e) {
  405. e.preventDefault();
  406. executeAction(MarkDown[this.id], this.commentForm);
  407. return false;
  408. };
  409.  
  410. function addToolbar() {
  411. if (isWiki()) {
  412. // Override existing language with improved & missing functions and remove existing click events;
  413. overrideGollumMarkdown();
  414. unbindGollumFunctions();
  415.  
  416. // Remove existing click events when changing languages;
  417. document.getElementById("wiki_format").addEventListener("change", function() {
  418. unbindGollumFunctions();
  419.  
  420. Array.forEach(document.querySelectorAll(".comment-form-textarea .function-button"), function(button) {
  421. button.removeEventListener("click", functionButtonClick);
  422. });
  423. });
  424. }
  425.  
  426. Array.forEach(document.querySelectorAll(".comment-form-textarea,.js-comment-field"), function(commentForm) {
  427. var gollumEditor;
  428. if (commentForm.classList.contains("GithubCommentEnhancer")) {
  429. gollumEditor = commentForm.previousSibling;
  430. } else {
  431. commentForm.classList.add("GithubCommentEnhancer");
  432.  
  433. if (isWiki()) {
  434. gollumEditor = document.getElementById("gollum-editor-function-bar");
  435. var temp = document.createElement("div");
  436. temp.innerHTML = editorHTML;
  437. temp.firstChild.appendChild(document.getElementById("function-help")); // restore the help button;
  438. gollumEditor.replaceChild(temp.querySelector("#gollum-editor-function-buttons"), document.getElementById("gollum-editor-function-buttons"));
  439. Array.forEach(temp.children, function(elm) {
  440. elm.style.position = "absolute";
  441. elm.style.right = "30px";
  442. elm.style.top = "0";
  443. commentForm.parentNode.insertBefore(elm, commentForm);
  444. });
  445. temp = null;
  446. } else {
  447. gollumEditor = document.createElement("div");
  448. gollumEditor.innerHTML = editorHTML;
  449. gollumEditor.id = "gollum-editor-function-bar";
  450. gollumEditor.style.height = "26px";
  451. gollumEditor.style.margin = "10px 0";
  452. gollumEditor.classList.add("active");
  453. commentForm.parentNode.insertBefore(gollumEditor, commentForm);
  454. }
  455. }
  456.  
  457. if (isGist()) {
  458. Array.forEach(gollumEditor.parentNode.querySelectorAll(".select-menu-button"), function(button) {
  459. button.style.paddingRight = "25px";
  460. });
  461. }
  462.  
  463. Array.forEach(gollumEditor.parentNode.querySelectorAll(".function-button"), function(button) {
  464. if (isGist() && button.classList.contains("minibutton")) {
  465. button.style.padding = "0px";
  466. button.style.textAlign = "center";
  467. button.style.width = "30px";
  468. button.firstElementChild.style.marginRight = "0px";
  469. }
  470. button.commentForm = commentForm; // remove event listener doesn't accept `bind`;
  471. button.addEventListener("click", functionButtonClick);
  472. });
  473. });
  474. }
  475.  
  476. // init;
  477. addToolbar();
  478.  
  479. // on pjax;
  480. unsafeWindow.$(document).on("pjax:end", addToolbar); // `pjax:end` also runs on history back;
  481.  
  482. // inline comment;
  483. var files = document.querySelectorAll('.file-code');
  484. Array.forEach(files, function(file) {
  485. file = file.firstElementChild;
  486. new MutationObserver(function(mutations) {
  487. mutations.forEach(function(mutation) {
  488. if (mutation.target === file) {
  489. addToolbar();
  490. }
  491. });
  492. }).observe(file, { childList: true, subtree: true });
  493. });
  494.  
  495. })();