Github Comment Enhancer

Enhances Github comments

目前为 2014-10-05 提交的版本。查看 最新版本

  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 2.0.3
  12. // @grant none
  13. // @run-at document-end
  14. // @include https://github.com/*/*/issues
  15. // @include https://github.com/*/*/issues/*
  16. // @include https://github.com/*/*/pulls
  17. // @include https://github.com/*/*/pull/*
  18. // @include https://github.com/*/*/commit/*
  19. // @include https://github.com/*/*/compare/*
  20. // @include https://github.com/*/*/wiki/*
  21. // @include https://gist.github.com/*
  22. // ==/UserScript==
  23. /* global unsafeWindow */
  24.  
  25. (function(unsafeWindow) {
  26.  
  27. String.format = function(string) {
  28. var args = Array.prototype.slice.call(arguments, 1, arguments.length);
  29. return string.replace(/{(\d+)}/g, function(match, number) {
  30. return typeof args[number] !== "undefined" ? args[number] : match;
  31. });
  32. };
  33.  
  34. // Source: https://github.com/gollum/gollum/blob/9c714e768748db4560bc017cacef4afa0c751a63/lib/gollum/public/gollum/javascript/editor/langs/markdown.js
  35. var MarkDown = (function MarkDown() {
  36. return {
  37. "function-bold": {
  38. search: /^(\s*)([\s\S]*?)(\s*)$/g,
  39. replace: "$1**$2**$3"
  40. },
  41. "function-italic": {
  42. search: /^(\s*)([\s\S]*?)(\s*)$/g,
  43. replace: "$1_$2_$3"
  44. },
  45. "function-strikethrough": {
  46. search: /^(\s*)([\s\S]*?)(\s*)$/g,
  47. replace: "$1~~$2~~$3"
  48. },
  49.  
  50. "function-h1": {
  51. search: /(.+)([\n]?)/g,
  52. replace: "# $1$2",
  53. forceNewline: true
  54. },
  55. "function-h2": {
  56. search: /(.+)([\n]?)/g,
  57. replace: "## $1$2",
  58. forceNewline: true
  59. },
  60. "function-h3": {
  61. search: /(.+)([\n]?)/g,
  62. replace: "### $1$2",
  63. forceNewline: true
  64. },
  65. "function-h4": {
  66. search: /(.+)([\n]?)/g,
  67. replace: "#### $1$2",
  68. forceNewline: true
  69. },
  70. "function-h5": {
  71. search: /(.+)([\n]?)/g,
  72. replace: "##### $1$2",
  73. forceNewline: true
  74. },
  75. "function-h6": {
  76. search: /(.+)([\n]?)/g,
  77. replace: "###### $1$2",
  78. forceNewline: true
  79. },
  80.  
  81. "function-link": {
  82. exec: function(txt, selText, commentForm, next) {
  83. var selTxt = selText.trim(),
  84. isUrl = selTxt && /(?:https?:\/\/)|(?:www\.)/.test(selTxt),
  85. href = window.prompt("Link href:", isUrl ? selTxt : ""),
  86. text = window.prompt("Link text:", isUrl ? "" : selTxt);
  87. if (href) {
  88. next(String.format("[{0}]({1}){2}", text || href, href, (/\s+$/.test(selText) ? " " : "")));
  89. }
  90. }
  91. },
  92. "function-image": {
  93. exec: function(txt, selText, commentForm, next) {
  94. var selTxt = selText.trim(),
  95. isUrl = selTxt && /(?:https?:\/\/)|(?:www\.)/.test(selTxt),
  96. href = window.prompt("Image href:", isUrl ? selTxt : ""),
  97. text = window.prompt("Image text:", isUrl ? "" : selTxt);
  98. if (href) {
  99. next(String.format("![{0}]({1}){2}", text || href, href, (/\s+$/.test(selText) ? " " : "")));
  100. }
  101. }
  102. },
  103.  
  104. "function-ul": {
  105. search: /(.+)([\n]?)/g,
  106. replace: "* $1$2",
  107. forceNewline: true
  108. },
  109. "function-ol": {
  110. exec: function(txt, selText, commentForm, next) {
  111. var repText = "";
  112. if (!selText) {
  113. repText = "1. ";
  114. } else {
  115. var lines = selText.split("\n"),
  116. hasContent = /[\w]+/;
  117. for (var i = 0; i < lines.length; i++) {
  118. if (hasContent.test(lines[i])) {
  119. repText += String.format("{0}. {1}\n", i + 1, lines[i]);
  120. }
  121. }
  122. }
  123. next(repText);
  124. }
  125. },
  126. "function-checklist": {
  127. search: /(.+)([\n]?)/g,
  128. replace: "* [ ] $1$2",
  129. forceNewline: true
  130. },
  131.  
  132. "function-code": {
  133. exec: function(txt, selText, commentForm, next) {
  134. var rt = selText.indexOf("\n") > -1 ? "$1\n```\n$2\n```$3" : "$1`$2`$3";
  135. next(selText.replace(/^(\s*)([\s\S]*?)(\s*)$/g, rt));
  136. }
  137. },
  138. "function-blockquote": {
  139. search: /(.+)([\n]?)/g,
  140. replace: "> $1$2",
  141. forceNewline: true
  142. },
  143. "function-hr": {
  144. append: "\n***\n",
  145. forceNewline: true
  146. },
  147. "function-table": {
  148. append: "\n" +
  149. "| Head | Head | Head |\n" +
  150. "| :--- | :--: | ---: |\n" +
  151. "| Cell | Cell | Cell |\n" +
  152. "| Cell | Cell | Cell |\n",
  153. forceNewline: true
  154. },
  155.  
  156. "function-clear": {
  157. exec: function(txt, selText, commentForm, next) {
  158. commentForm.value = "";
  159. next("");
  160. }
  161. },
  162.  
  163. "function-snippets-useragent": {
  164. exec: function(txt, selText, commentForm, next) {
  165. next("`" + navigator.userAgent + "`");
  166. }
  167. },
  168. "function-snippets-contributing": {
  169. exec: function(txt, selText, commentForm, next) {
  170. next("Please, always consider reviewing the [guidelines for contributing](../blob/master/CONTRIBUTING.md) to this repository.");
  171. }
  172. }
  173. };
  174. })();
  175.  
  176. var editorHTML = (function editorHTML() {
  177. return '<div id="gollum-editor-function-buttons" style="float: left;">' +
  178. ' <div class="button-group">' +
  179. ' <a href="#" id="function-bold" class="minibutton function-button tooltipped tooltipped-ne" aria-label="Bold" style="height:26px;">' +
  180. ' <b style="font-weight: bolder;">B</b>' +
  181. ' </a>' +
  182. ' <a href="#" id="function-italic" class="minibutton function-button tooltipped tooltipped-ne" aria-label="Italic">' +
  183. ' <em>i</em>' +
  184. ' </a>' +
  185. ' <a href="#" id="function-strikethrough" class="minibutton function-button tooltipped tooltipped-ne" aria-label="Strikethrough">' +
  186. ' <s>S</s>' +
  187. ' </a>' +
  188. ' </div>' +
  189.  
  190. ' <div class="button-group">' +
  191. ' <div class="select-menu js-menu-container js-select-menu">' +
  192. ' <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;">' +
  193. ' <span class="js-select-button">h#</span>' +
  194. ' </span>' +
  195. ' <div class="select-menu-modal-holder js-menu-content js-navigation-container js-active-navigation-container" style="top: 26px;">' +
  196. ' <div class="select-menu-modal" style="width:auto; overflow:visible;">' +
  197. ' <div class="select-menu-header">' +
  198. ' <span class="select-menu-title">Choose header</span>' +
  199. ' <span class="octicon octicon-remove-close js-menu-close"></span>' +
  200. ' </div>' +
  201. ' <div class="button-group">' +
  202. ' <a href="#" id="function-h1" class="minibutton function-button js-navigation-item js-menu-close tooltipped tooltipped-s" aria-label="Header 1">' +
  203. ' <b class="select-menu-item-text js-select-button-text">h1</b>' +
  204. ' </a>' +
  205. ' <a href="#" id="function-h2" class="minibutton function-button js-navigation-item js-menu-close tooltipped tooltipped-s" aria-label="Header 2">' +
  206. ' <b class="select-menu-item-text js-select-button-text">h2</b>' +
  207. ' </a>' +
  208. ' <a href="#" id="function-h3" class="minibutton function-button js-navigation-item js-menu-close tooltipped tooltipped-s" aria-label="Header 3">' +
  209. ' <b class="select-menu-item-text js-select-button-text">h3</b>' +
  210. ' </a>' +
  211. ' <a href="#" id="function-h4" class="minibutton function-button js-navigation-item js-menu-close tooltipped tooltipped-s" aria-label="Header 4">' +
  212. ' <b class="select-menu-item-text js-select-button-text">h4</b>' +
  213. ' </a>' +
  214. ' <a href="#" id="function-h5" class="minibutton function-button js-navigation-item js-menu-close tooltipped tooltipped-s" aria-label="Header 5">' +
  215. ' <b class="select-menu-item-text js-select-button-text">h5</b>' +
  216. ' </a>' +
  217. ' <a href="#" id="function-h6" class="minibutton function-button js-navigation-item js-menu-close tooltipped tooltipped-s" aria-label="Header 6">' +
  218. ' <b class="select-menu-item-text js-select-button-text">h6</b>' +
  219. ' </a>' +
  220. ' </div>' +
  221. ' </div>' +
  222. ' </div>' +
  223. ' </div>' +
  224. ' </div>' +
  225.  
  226. ' <div class="button-group">' +
  227. ' <a href="#" id="function-link" class="minibutton function-button tooltipped tooltipped-ne" aria-label="Link">' +
  228. ' <span class="octicon octicon-link"></span>' +
  229. ' </a>' +
  230. ' <a href="#" id="function-image" class="minibutton function-button tooltipped tooltipped-ne" aria-label="Image">' +
  231. ' <span class="octicon octicon-file-media"></span>' +
  232. ' </a>' +
  233. ' </div>' +
  234. ' <div class="button-group">' +
  235. ' <a href="#" id="function-ul" class="minibutton function-button tooltipped tooltipped-ne" aria-label="Unordered List">' +
  236. ' <span class="octicon octicon-list-unordered"></span>' +
  237. ' </a>' +
  238. ' <a href="#" id="function-ol" class="minibutton function-button tooltipped tooltipped-ne" aria-label="Ordered List">' +
  239. ' <span class="octicon octicon-list-ordered"></span>' +
  240. ' </a>' +
  241. ' <a href="#" id="function-checklist" class="minibutton function-button tooltipped tooltipped-ne" aria-label="Task List">' +
  242. ' <span class="octicon octicon-checklist"></span>' +
  243. ' </a>' +
  244. ' </div>' +
  245.  
  246. ' <div class="button-group">' +
  247. ' <a href="#" id="function-code" class="minibutton function-button tooltipped tooltipped-ne" aria-label="Code">' +
  248. ' <span class="octicon octicon-code"></span>' +
  249. ' </a>' +
  250. ' <a href="#" id="function-blockquote" class="minibutton function-button tooltipped tooltipped-ne" aria-label="Blockquote">' +
  251. ' <span class="octicon octicon-quote"></span>' +
  252. ' </a>' +
  253. ' <a href="#" id="function-hr" class="minibutton function-button tooltipped tooltipped-ne" aria-label="Horizontal Rule">' +
  254. ' <span class="octicon octicon-horizontal-rule"></span>' +
  255. ' </a>' +
  256. ' <a href="#" id="function-table" class="minibutton function-button tooltipped tooltipped-ne" aria-label="Table">' +
  257. ' <span class="octicon octicon-three-bars"></span>' +
  258. ' </a>' +
  259. ' </div>' +
  260.  
  261. ' <div class="button-group">' +
  262. ' <div class="select-menu js-menu-container js-select-menu">' +
  263. ' <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;">' +
  264. ' <span class="octicon octicon-pin"></span>' +
  265. ' </span>' +
  266. ' <div class="select-menu-modal-holder js-menu-content js-navigation-container js-active-navigation-container">' +
  267. ' <div class="select-menu-modal" style="overflow:visible;">' +
  268. ' <div class="select-menu-header">' +
  269. ' <span class="select-menu-title">Snippets</span>' +
  270. ' <span class="octicon octicon-remove-close js-menu-close"></span>' +
  271. ' </div>' +
  272. ' <div class="select-menu-filters">' +
  273. ' <div class="select-menu-text-filter">' +
  274. ' <input type="text" placeholder="Filter snippets..." class="js-filterable-field js-navigation-enable" id="context-snippets-filter-field">' +
  275. ' </div>' +
  276. ' </div>' +
  277. ' <div class="select-menu-list" style="overflow:visible;">' +
  278. ' <div data-filterable-for="context-snippets-filter-field">' +
  279. ' <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;">' +
  280. ' <span class="select-menu-item-text js-select-button-text">Add UserAgent</span>' +
  281. ' </a>' +
  282. ' <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;">' +
  283. ' <span class="select-menu-item-text">' +
  284. ' <span class="js-select-button-text">Contributing</span>' +
  285. ' <span class="description">Add contributing message</span>' +
  286. ' </span>' +
  287. ' </a>' +
  288. ' </div>' +
  289. ' <div class="select-menu-no-results">Nothing to show</div>' +
  290. ' </div>' +
  291. ' </div>' +
  292. ' </div>' +
  293. ' </div>' +
  294. ' </div>' +
  295.  
  296. '</div>' +
  297.  
  298. '<div class="button-group" style="float:right;">' +
  299. ' <a href="#" id="function-clear" class="minibutton function-button tooltipped tooltipped-nw" aria-label="Clear">' +
  300. ' <span class="octicon octicon-circle-slash"></span>' +
  301. ' </a>' +
  302. '</div>';
  303. })();
  304.  
  305. // Source: https://github.com/gollum/gollum/blob/9c714e768748db4560bc017cacef4afa0c751a63/lib/gollum/public/gollum/javascript/editor/gollum.editor.js#L516
  306. function executeAction(definitionObject, commentForm) {
  307. var txt = commentForm.value,
  308. selPos = {
  309. start: commentForm.selectionStart,
  310. end: commentForm.selectionEnd
  311. },
  312. selText = txt.substring(selPos.start, selPos.end),
  313. repText = selText,
  314. reselect = true,
  315. cursor = null;
  316.  
  317. // execute replacement function;
  318. if (definitionObject.exec) {
  319. definitionObject.exec(txt, selText, commentForm, function(repText) {
  320. replaceFieldSelection(commentForm, repText);
  321. });
  322. return;
  323. }
  324.  
  325. // execute a search;
  326. var searchExp = new RegExp(definitionObject.search || /([^\n]+)/gi);
  327.  
  328. // replace text;
  329. if (definitionObject.replace) {
  330. var rt = definitionObject.replace;
  331. repText = repText.replace(searchExp, rt);
  332. repText = repText.replace(/\$[\d]/g, "");
  333. if (repText === "") {
  334. cursor = rt.indexOf("$1");
  335. repText = rt.replace(/\$[\d]/g, "");
  336. if (cursor === -1) {
  337. cursor = Math.floor(rt.length / 2);
  338. }
  339. }
  340. }
  341.  
  342. // append if necessary;
  343. if (definitionObject.append) {
  344. if (repText === selText) {
  345. reselect = false;
  346. }
  347. repText += definitionObject.append;
  348. }
  349.  
  350. if (repText) {
  351. if (definitionObject.forceNewline === true && (selPos.start > 0 && txt.substr(Math.max(0, selPos.start - 1), 1) !== "\n")) {
  352. repText = "\n" + repText;
  353. }
  354. replaceFieldSelection(commentForm, repText, reselect, cursor);
  355. }
  356. }
  357.  
  358. // Source: https://github.com/gollum/gollum/blob/9c714e768748db4560bc017cacef4afa0c751a63/lib/gollum/public/gollum/javascript/editor/gollum.editor.js#L708
  359. function replaceFieldSelection(commentForm, replaceText, reselect, cursorOffset) {
  360. var txt = commentForm.value,
  361. selPos = {
  362. start: commentForm.selectionStart,
  363. end: commentForm.selectionEnd
  364. };
  365.  
  366. var selectNew = true;
  367. if (reselect === false) {
  368. selectNew = false;
  369. }
  370.  
  371. var scrollTop = null;
  372. if (commentForm.scrollTop) {
  373. scrollTop = commentForm.scrollTop;
  374. }
  375.  
  376. commentForm.value = txt.substring(0, selPos.start) + replaceText + txt.substring(selPos.end);
  377. commentForm.focus();
  378.  
  379. if (selectNew) {
  380. if (cursorOffset) {
  381. commentForm.setSelectionRange(selPos.start + cursorOffset, selPos.start + cursorOffset);
  382. } else {
  383. commentForm.setSelectionRange(selPos.start, selPos.start + replaceText.length);
  384. }
  385. }
  386.  
  387. if (scrollTop) {
  388. commentForm.scrollTop = scrollTop;
  389. }
  390. }
  391.  
  392. function isWiki() {
  393. return /\/wiki\//.test(location.href);
  394. }
  395. function isGist() {
  396. return location.host === "gist.github.com";
  397. }
  398.  
  399. function overrideGollumMarkdown() {
  400. unsafeWindow.$.GollumEditor.defineLanguage("markdown", MarkDown);
  401. }
  402. function unbindGollumFunctions() {
  403. window.setTimeout(function() {
  404. unsafeWindow.$(".function-button:not(#function-help)").unbind("click");
  405. }, 1);
  406. }
  407.  
  408. var functionButtonClick = function(e) {
  409. e.preventDefault();
  410. executeAction(MarkDown[this.id], this.commentForm);
  411. return false;
  412. };
  413.  
  414. function addToolbar() {
  415. if (isWiki()) {
  416. // Override existing language with improved & missing functions and remove existing click events;
  417. overrideGollumMarkdown();
  418. unbindGollumFunctions();
  419.  
  420. // Remove existing click events when changing languages;
  421. document.getElementById("wiki_format").addEventListener("change", function() {
  422. unbindGollumFunctions();
  423.  
  424. Array.prototype.forEach.call(document.querySelectorAll(".comment-form-textarea .function-button"), function(button) {
  425. button.removeEventListener("click", functionButtonClick);
  426. });
  427. });
  428. }
  429.  
  430. Array.prototype.forEach.call(document.querySelectorAll(".comment-form-textarea,.js-comment-field"), function(commentForm) {
  431. var gollumEditor;
  432. if (commentForm.classList.contains("GithubCommentEnhancer")) {
  433. gollumEditor = commentForm.previousSibling;
  434. } else {
  435. commentForm.classList.add("GithubCommentEnhancer");
  436.  
  437. if (isWiki()) {
  438. gollumEditor = document.getElementById("gollum-editor-function-bar");
  439. var temp = document.createElement("div");
  440. temp.innerHTML = editorHTML;
  441. temp.firstElementChild.appendChild(document.getElementById("function-help")); // restore the help button;
  442. gollumEditor.replaceChild(temp.querySelector("#gollum-editor-function-buttons"), document.getElementById("gollum-editor-function-buttons"));
  443. Array.prototype.forEach.call(temp.children, function(elm) {
  444. elm.style.position = "absolute";
  445. elm.style.right = "30px";
  446. elm.style.top = "0";
  447. commentForm.parentNode.insertBefore(elm, commentForm);
  448. });
  449. temp = null;
  450. } else {
  451. gollumEditor = document.createElement("div");
  452. gollumEditor.innerHTML = editorHTML;
  453. gollumEditor.id = "gollum-editor-function-bar";
  454. gollumEditor.style.height = "26px";
  455. gollumEditor.style.margin = "10px 0";
  456. gollumEditor.classList.add("active");
  457. commentForm.parentNode.insertBefore(gollumEditor, commentForm);
  458. }
  459.  
  460. var tabnavExtras = commentForm.parentNode.parentNode.querySelector(".comment-form-head .tabnav-right");
  461. if (tabnavExtras) {
  462. var sponsored = document.createElement("a");
  463. sponsored.setAttribute("href", "https://github.com/jerone/UserScripts/tree/master/Github_Comment_Enhancer");
  464. sponsored.setAttribute("target", "_blank");
  465. sponsored.classList.add("tabnav-widget", "text", "tabnav-extras");
  466. var sponsoredIcon = document.createElement("span");
  467. sponsoredIcon.classList.add("octicon", "octicon-question");
  468. sponsored.appendChild(sponsoredIcon);
  469. sponsored.appendChild(document.createTextNode("Enhanced by Github Comment Enhancer"));
  470. tabnavExtras.insertBefore(sponsored, tabnavExtras.firstElementChild);
  471. }
  472. }
  473.  
  474. if (isGist()) {
  475. Array.prototype.forEach.call(gollumEditor.parentNode.querySelectorAll(".select-menu-button"), function(button) {
  476. button.style.paddingRight = "25px";
  477. });
  478. }
  479.  
  480. Array.prototype.forEach.call(gollumEditor.parentNode.querySelectorAll(".function-button"), function(button) {
  481. if (isGist() && button.classList.contains("minibutton")) {
  482. button.style.padding = "0px";
  483. button.style.textAlign = "center";
  484. button.style.width = "30px";
  485. button.firstElementChild.style.marginRight = "0px";
  486. }
  487. button.commentForm = commentForm; // remove event listener doesn't accept `bind`;
  488. button.addEventListener("click", functionButtonClick);
  489. });
  490. });
  491. }
  492.  
  493. // Source: https://github.com/domchristie/to-markdown
  494. var toMarkdown = function(string) {
  495.  
  496. var ELEMENTS = [
  497. {
  498. patterns: 'p',
  499. replacement: function(str, attrs, innerHTML) {
  500. return innerHTML ? '\n\n' + innerHTML + '\n' : '';
  501. }
  502. },
  503. {
  504. patterns: 'br',
  505. type: 'void',
  506. replacement: '\n'
  507. },
  508. {
  509. patterns: 'h([1-6])',
  510. replacement: function(str, hLevel, attrs, innerHTML) {
  511. var hPrefix = '';
  512. for (var i = 0; i < hLevel; i++) {
  513. hPrefix += '#';
  514. }
  515. return '\n\n' + hPrefix + ' ' + innerHTML + '\n';
  516. }
  517. },
  518. {
  519. patterns: 'hr',
  520. type: 'void',
  521. replacement: '\n\n* * *\n'
  522. },
  523. {
  524. patterns: 'a',
  525. replacement: function(str, attrs, innerHTML) {
  526. var href = attrs.match(attrRegExp('href')),
  527. title = attrs.match(attrRegExp('title'));
  528. return href ? '[' + innerHTML + ']' + '(' + href[1] + (title && title[1] ? ' "' + title[1] + '"' : '') + ')' : str;
  529. }
  530. },
  531. {
  532. patterns: ['b', 'strong'],
  533. replacement: function(str, attrs, innerHTML) {
  534. return innerHTML ? '**' + innerHTML + '**' : '';
  535. }
  536. },
  537. {
  538. patterns: ['i', 'em'],
  539. replacement: function(str, attrs, innerHTML) {
  540. return innerHTML ? '_' + innerHTML + '_' : '';
  541. }
  542. },
  543. {
  544. patterns: 'code',
  545. replacement: function(str, attrs, innerHTML) {
  546. //return innerHTML ? '`' + he.decode(innerHTML) + '`' : '';
  547. return innerHTML ? '`' + innerHTML + '`' : '';
  548. }
  549. },
  550. {
  551. patterns: 'img',
  552. type: 'void',
  553. replacement: function(str, attrs/*, innerHTML*/) {
  554. var src = attrs.match(attrRegExp('src')),
  555. alt = attrs.match(attrRegExp('alt')),
  556. title = attrs.match(attrRegExp('title'));
  557. return '![' + (alt && alt[1] ? alt[1] : '') + ']' + '(' + src[1] + (title && title[1] ? ' "' + title[1] + '"' : '') + ')';
  558. }
  559. }
  560. ];
  561.  
  562. for (var i = 0, len = ELEMENTS.length; i < len; i++) {
  563. if (typeof ELEMENTS[i].patterns === 'string') {
  564. string = replaceEls(string, { tag: ELEMENTS[i].patterns, replacement: ELEMENTS[i].replacement, type: ELEMENTS[i].type });
  565. } else {
  566. for (var j = 0, pLen = ELEMENTS[i].patterns.length; j < pLen; j++) {
  567. string = replaceEls(string, { tag: ELEMENTS[i].patterns[j], replacement: ELEMENTS[i].replacement, type: ELEMENTS[i].type });
  568. }
  569. }
  570. }
  571.  
  572. function replaceEls(html, elProperties) {
  573. var pattern = elProperties.type === 'void' ? '<' + elProperties.tag + '\\b([^>]*)\\/?>' : '<' + elProperties.tag + '\\b([^>]*)>([\\s\\S]*?)<\\/' + elProperties.tag + '>',
  574. regex = new RegExp(pattern, 'gi'),
  575. markdown = '';
  576. if (typeof elProperties.replacement === 'string') {
  577. markdown = html.replace(regex, elProperties.replacement);
  578. } else {
  579. markdown = html.replace(regex, function(str, p1, p2, p3) {
  580. return elProperties.replacement.call(this, str, p1, p2, p3);
  581. });
  582. }
  583. return markdown;
  584. }
  585.  
  586. function attrRegExp(attr) {
  587. return new RegExp(attr + '\\s*=\\s*["\']?([^"\']*)["\']?', 'i');
  588. }
  589.  
  590. // Pre code blocks
  591.  
  592. string = string.replace(/<pre\b[^>]*>`([\s\S]*)`<\/pre>/gi, function(str, innerHTML) {
  593. //var text = he.decode(innerHTML);
  594. var text = innerHTML;
  595. text = text.replace(/^\t+/g, ' '); // convert tabs to spaces (you know it makes sense)
  596. text = text.replace(/\n/g, '\n ');
  597. return '\n\n ' + text + '\n';
  598. });
  599.  
  600. // Lists
  601.  
  602. // Escape numbers that could trigger an ol
  603. // If there are more than three spaces before the code, it would be in a pre tag
  604. // Make sure we are escaping the period not matching any character
  605. string = string.replace(/^(\s{0,3}\d+)\. /g, '$1\\. ');
  606.  
  607. // Converts lists that have no child lists (of same type) first, then works its way up
  608. var noChildrenRegex = /<(ul|ol)\b[^>]*>(?:(?!<ul|<ol)[\s\S])*?<\/\1>/gi;
  609. var replaceListsFn = function(str) {
  610. return replaceLists(str);
  611. };
  612. while (string.match(noChildrenRegex)) {
  613. string = string.replace(noChildrenRegex, replaceListsFn);
  614. }
  615.  
  616. function replaceLists(html) {
  617.  
  618. html = html.replace(/<(ul|ol)\b[^>]*>([\s\S]*?)<\/\1>/gi, function(str, listType, innerHTML) {
  619. var lis = innerHTML.split('</li>');
  620. lis.splice(lis.length - 1, 1);
  621.  
  622. var lisReplace = function(str, innerHTML) {
  623. innerHTML = innerHTML.replace(/^\s+/, '');
  624. innerHTML = innerHTML.replace(/\n\n/g, '\n\n ');
  625. // indent nested lists
  626. innerHTML = innerHTML.replace(/\n([ ]*)+(\*|\d+\.) /g, '\n$1 $2 ');
  627. return prefix + innerHTML;
  628. };
  629.  
  630. for (i = 0, len = lis.length; i < len; i++) {
  631. if (lis[i]) {
  632. var prefix = (listType === 'ol') ? (i + 1) + ". " : "* ";
  633. lis[i] = lis[i].replace(/\s*<li[^>]*>([\s\S]*)/i, lisReplace);
  634. }
  635. }
  636. return lis.join('\n');
  637. });
  638. return '\n\n' + html.replace(/[ \t]+\n|\s+$/g, '');
  639. }
  640.  
  641. // Blockquotes
  642. var deepest = /<blockquote\b[^>]*>((?:(?!<blockquote)[\s\S])*?)<\/blockquote>/gi;
  643. var replaceBlockquotesFn = function(str) {
  644. return replaceBlockquotes(str);
  645. };
  646. while (string.match(deepest)) {
  647. string = string.replace(deepest, replaceBlockquotesFn);
  648. }
  649.  
  650. function replaceBlockquotes(html) {
  651. html = html.replace(/<blockquote\b[^>]*>([\s\S]*?)<\/blockquote>/gi, function(str, inner) {
  652. inner = inner.replace(/^\s+|\s+$/g, '');
  653. inner = cleanUp(inner);
  654. inner = inner.replace(/^/gm, '> ');
  655. inner = inner.replace(/^(>([ \t]{2,}>)+)/gm, '> >');
  656. return inner;
  657. });
  658. return html;
  659. }
  660.  
  661. function cleanUp(string) {
  662. string = string.replace(/^[\t\r\n]+|[\t\r\n]+$/g, ''); // trim leading/trailing whitespace
  663. string = string.replace(/\n\s+\n/g, '\n\n');
  664. string = string.replace(/\n{3,}/g, '\n\n'); // limit consecutive linebreaks to 2
  665. return string;
  666. }
  667.  
  668. return cleanUp(string);
  669. };
  670.  
  671. function addReplyButtons() {
  672. Array.prototype.forEach.call(document.querySelectorAll(".comment"), function(comment) {
  673. var oldReply = comment.querySelector(".GithubCommentEnhancerReply");
  674. if (oldReply) { oldReply.parentNode.removeChild(oldReply); }
  675.  
  676. var header = comment.querySelector(".timeline-comment-header"),
  677. actions = comment.querySelector(".timeline-comment-actions"),
  678. newComment = document.querySelector(".timeline-new-comment .comment-form-textarea");
  679.  
  680. if (!header) { return; }
  681. if (!actions) {
  682. actions = document.createElement("div");
  683. actions.classList.add("timeline-comment-actions");
  684. header.insertBefore(actions, header.firstElementChild);
  685. }
  686.  
  687. var reply = document.createElement("a");
  688. reply.setAttribute("href", "#");
  689. reply.setAttribute("aria-label", "Reply to this comment");
  690. reply.classList.add("GithubCommentEnhancerReply", "timeline-comment-action", "tooltipped", "tooltipped-ne");
  691. reply.addEventListener("click", function(e) {
  692. e.preventDefault();
  693.  
  694. var timestamp = comment.querySelector(".timestamp");
  695.  
  696. var commentText = comment.querySelector(".comment-form-textarea");
  697. if (commentText) {
  698. commentText = commentText.value;
  699. } else {
  700. commentText = toMarkdown(comment.querySelector(".comment-body").innerHTML);
  701. }
  702. commentText = commentText.trim().split("\n").map(function(line) {
  703. return "> " + line;
  704. }).join("\n");
  705.  
  706. var text = newComment.value.length > 0 ? "\n" : "";
  707. text += String.format('@{0} commented on [{1}]({2} "{3} - Replied by Github Comment Enhancer"):\n{4}\n\n',
  708. comment.querySelector(".author").textContent,
  709. timestamp.firstElementChild.getAttribute("title"),
  710. timestamp.href,
  711. timestamp.firstElementChild.getAttribute("datetime"),
  712. commentText);
  713.  
  714. newComment.value += text;
  715. newComment.setSelectionRange(newComment.value.length, newComment.value.length);
  716. newComment.focus();
  717. });
  718.  
  719. var replyIcon = document.createElement("span");
  720. replyIcon.classList.add("octicon", "octicon-mail-reply");
  721. reply.appendChild(replyIcon);
  722.  
  723. actions.appendChild(reply);
  724. });
  725. }
  726.  
  727. // init;
  728. function init() {
  729. addToolbar();
  730. addReplyButtons();
  731. }
  732. init();
  733.  
  734. // on pjax;
  735. unsafeWindow.$(document).on("pjax:end", init); // `pjax:end` also runs on history back;
  736.  
  737. // for inline comments;
  738. var files = document.querySelectorAll('.file-code');
  739. Array.prototype.forEach.call(files, function(file) {
  740. file = file.firstElementChild;
  741. new MutationObserver(function(mutations) {
  742. mutations.forEach(function(mutation) {
  743. if (mutation.target === file) {
  744. addToolbar();
  745. }
  746. });
  747. }).observe(file, { childList: true, subtree: true });
  748. });
  749.  
  750. })(typeof unsafeWindow !== "undefined" ? unsafeWindow : window);