Advertisement
Guest User

Untitled

a guest
Oct 31st, 2014
197
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
text 20.70 KB | None | 0 0
  1. // ==UserScript==
  2. // @name Stack-Exchange-Editor-Toolkit
  3. // @author Cameron Bernhardt (AstroCB)
  4. // @developer jt0dd
  5. // @contributor Unihedron
  6. // @namespace http://github.com/AstroCB
  7. // @version 3.0
  8. // @description Fix common grammar/usage annoyances on Stack Exchange posts with a click
  9. // @include http://*.stackexchange.com/questions/*
  10. // @include http://stackoverflow.com/questions/*
  11. // @include http://meta.stackoverflow.com/questions/*
  12. // @include http://serverfault.com/questions/*
  13. // @include http://meta.serverfault.com/questions/*
  14. // @include http://superuser.com/questions/*
  15. // @include http://meta.superuser.com/questions/*
  16. // @include http://askubuntu.com/questions/*
  17. // @include http://meta.askubuntu.com/questions/*
  18. // @include http://stackapps.com/questions/*
  19. // @include http://*.stackexchange.com/posts/*
  20. // @include http://stackoverflow.com/posts/*
  21. // @include http://meta.stackoverflow.com/posts/*
  22. // @include http://serverfault.com/posts/*
  23. // @include http://meta.serverfault.com/posts/*
  24. // @include http://superuser.com/posts/*
  25. // @include http://meta.superuser.com/posts/*
  26. // @include http://askubuntu.com/posts/*
  27. // @include http://meta.askubuntu.com/posts/*
  28. // @include http://stackapps.com/posts/*
  29. // @exclude http://*.stackexchange.com/questions/tagged/*
  30. // @exclude http://stackoverflow.com/questions/tagged/*
  31. // @exclude http://meta.stackoverflow.com/questions/tagged/*
  32. // @exclude http://serverfault.com/questions/tagged/*
  33. // @exclude http://meta.serverfault.com/questions/*
  34. // @exclude http://superuser.com/questions/tagged/*
  35. // @exclude http://meta.superuser.com/questions/tagged/*
  36. // @exclude http://askubuntu.com/questions/tagged/*
  37. // @exclude http://meta.askubuntu.com/questions/tagged/*
  38. // @exclude http://stackapps.com/questions/tagged/*
  39. // ==/UserScript==
  40. var main = function () {
  41.  
  42. /*
  43.  
  44. Note that in the new version I place many things needlessly into wrappers (container
  45. functions) and namespaces (container variables); this is simply an effort to promote modularity
  46. in the structure and keep focused on what's going where and when.
  47.  
  48. Some of this may have no use at all once the code is all in place, and we may be able to simplify it
  49. extensively. This is one of my first user-script projects, and it's confusing putting
  50. so many different functionalities into one single file.
  51.  
  52. */
  53.  
  54. // Define app namespace
  55. var App = {};
  56.  
  57. // Place edit items here
  58. App.items = [];
  59.  
  60. // Place selected JQuery items here
  61. App.selections = {};
  62.  
  63. // Place "global" app data here
  64. App.globals = {};
  65.  
  66. // Place "helper" functions here
  67. App.funcs = {};
  68.  
  69. //Preload icon alt
  70.  
  71. var SEETicon = new Image();
  72.  
  73. SEETicon.src = 'http://i.imgur.com/d5ZL09o.png';
  74.  
  75. // Populate global data
  76.  
  77. // Get url for question id used in id and class names
  78. App.globals.URL = window.location.href;
  79.  
  80. // Get question num from URL
  81. App.globals.questionNum = App.globals.URL.match(/d/g);
  82.  
  83. // Join
  84. App.globals.questionNum = App.globals.questionNum.join("");
  85.  
  86. // Define varables for later use
  87. App.globals.barReady = false;
  88. App.globals.editsMade = false;
  89. App.globals.editCount = 0;
  90. App.globals.infoContent = '';
  91.  
  92. App.globals.privileges = true;
  93. App.globals.spacerHTML = '<li class="wmd-spacer wmd-spacer3" id="wmd-spacer3-' + App.globals.questionNum + '" style="left: 400px !important;"></li>';
  94. App.globals.buttonHTML = '<div id="ToolkitButtonWrapper"><button class="wmd-button" id="ToolkitFix"></button><div id="ToolkitInfo"></div></div>';
  95.  
  96. App.globals.reasons = [];
  97. App.globals.numReasons = 0;
  98.  
  99. App.globals.replacedStrings = {
  100. "block": [],
  101. "inline": []
  102. };
  103. App.globals.placeHolders = {
  104. "block": "_xCodexBlockxPlacexHolderx_",
  105. "inline": "_xCodexInlinexPlacexHolderx_"
  106. };
  107. App.globals.checks = {
  108. "block": /( )+.*/gm,
  109. "inline": /`.*`/gm
  110. };
  111.  
  112. // Assign modules here
  113. App.globals.pipeMods = {};
  114.  
  115. // Define order in which mods affect here
  116. App.globals.order = [
  117. "omit",
  118. "edit",
  119. "replace"];
  120.  
  121.  
  122. // Define edit rules
  123. App.edits = {
  124. i: {
  125. expr: /(^|s|()i(s|,|.|!|?|;|/|)|'|$)/gm,
  126. replacement: "$1I$2",
  127. reason: "basic capitalization"
  128. },
  129. so: {
  130. expr: /(^|s)[Ss]tacks*overflow|StackOverflow(.|$)/gm,
  131. replacement: "$1Stack Overflow$2",
  132. reason: "'Stack Overflow' is the proper capitalization"
  133. },
  134. se: {
  135. expr: /(^|s)[Ss]tacks*exchange|StackExchange(.|$)/gm,
  136. replacement: "$1Stack Exchange$2",
  137. reason: "'Stack Exchange' is the proper capitalization"
  138. },
  139. expansionSO: {
  140. expr: /(^|s)SO(s|,|.|!|?|;|/|)|$)/gm,
  141. replacement: "$1Stack Overflow$2",
  142. reason: "'SO' expansion"
  143. },
  144. expansionSE: {
  145. expr: /(^|s)SE(s|,|.|!|?|;|/|)|$)/gm,
  146. replacement: "$1Stack Exchange$2",
  147. reason: "'SE' expansion"
  148. },
  149. javascript: {
  150. expr: /(^|s)[Jj]avas*script(.|$)/gm,
  151. replacement: "$1JavaScript$2",
  152. reason: "'JavaScript' is the proper capitalization"
  153. },
  154. jsfiddle: {
  155. expr: /(^|s)[Jj][Ss][Ff]iddle(.|$)/gm,
  156. replacement: "$1JSFiddle$2",
  157. reason: "'JSFiddle' is the currently accepted capitalization"
  158. },
  159. caps: {
  160. expr: /^(?!https?)([a-z])/gm,
  161. replacement: "$1",
  162. reason: "basic capitalization"
  163. },
  164. jquery: {
  165. expr: /(^|s)[Jj][Qq]uery(.|$)/gm,
  166. replacement: "$1jQuery$2",
  167. reason: "'jQuery' is the proper capitalization"
  168. },
  169. html: {
  170. expr: /(^|s)[Hh]tml(?:5*)(s|$)/gm,
  171. replacement: "$1HTML$2",
  172. reason: "HTML: HyperText Markup Language"
  173. },
  174. css: {
  175. expr: /(^|s)[Cc]ss(s|$)/gm,
  176. replacement: "$1CSS$2",
  177. reason: "CSS: Cascading Style Sheets"
  178. },
  179. json: {
  180. expr: /(^|s)[Jj]son(s|$)/gm,
  181. replacement: "$1JSON$2",
  182. reason: "JSON: JavaScript Object Notation"
  183. },
  184. ajax: {
  185. expr: /(^|s)[Aa]jax(s|$)/gm,
  186. replacement: "$1AJAX$2",
  187. reason: "AJAX: Asynchronous JavaScript and XML"
  188. },
  189. angular: {
  190. expr: /[Aa]ngular[Jj][Ss]/g,
  191. replacement: "AngularJS",
  192. reason: "'AngularJS is the proper capitalization"
  193. },
  194. thanks: {
  195. expr: /(thanks|pleases+help|cheers|regards|thx|thanks+you|mys+firsts+question).*$/gmi,
  196. replacement: "",
  197. reason: "'$1' is unnecessary noise"
  198. },
  199. commas: {
  200. expr: /,([^s])/g,
  201. replacement: ", $1",
  202. reason: "punctuation & spacing"
  203. },
  204. php: {
  205. expr: /(^|s)[Pp]hp(s|$)/gm,
  206. replacement: "$1PHP$2",
  207. reason: "PHP: PHP: Hypertext Preprocessor"
  208. },
  209. hello: {
  210. expr: /(?:^|s)(his+guys|goods(?:evening|morning|day|afternoon))(?:.|!)/gmi,
  211. replacement: "",
  212. reason: "Greetings like '$1' are unnecessary noise"
  213. },
  214. edit: {
  215. expr: /(?:^**)(edit|update):?(?:**):?/gmi,
  216. replacement: "",
  217. reason: "Stack Exchange has an advanced revision history system: 'Edit' or 'Update' is unnecessary"
  218. },
  219. voting: {
  220. expr: /([Dd]own|[Uu]p)[s*-]vot/g,
  221. replacement: "$1vote",
  222. reason: "the proper spelling (despite the tag name) is '$1vote' (one word)"
  223. },
  224. mysite: {
  225. expr: /mysite./g,
  226. replacement: "example.",
  227. reason: "links to mysite.domain are not allowed: use example.domain instead"
  228. }
  229.  
  230. //expansion reminder: let's support those non web devs with capitalization for popular languages such as C#
  231. };
  232.  
  233. // Populate funcs
  234. App.popFuncs = function () {
  235. // This is where the magic happens: this function takes a few pieces of information and applies edits to the post with a couple exceptions
  236. App.funcs.fixIt = function (input, expression, replacement, reasoning) {
  237.  
  238. // Scan the post text using the expression to see if there are any matches
  239. var match = input.search(expression);
  240.  
  241. // If so, increase the number of edits performed (used later for edit summary formation)
  242. if (match !== -1) {
  243. App.globals.editCount++;
  244.  
  245. // Later, this will store what is removed for the first case
  246. var phrase;
  247.  
  248. // Then, perform the edits using replace()
  249. // What follows is a series of exceptions, which I will explain below; I perform special actions by overriding replace()
  250.  
  251. // This is used for removing things entirely without giving a replacement; it matches the expression and then replaces it with nothing
  252. if (replacement === "") {
  253. input = input.replace(expression, function (data, match1) {
  254.  
  255. // Save what is removed for the edit summary (see below)
  256. phrase = match1;
  257.  
  258. // Replace with nothing
  259. return "";
  260. });
  261.  
  262. // This is an interesting tidbit: if you want to make the edit summaries dynamic, you can keep track of a match that you receive
  263. //from overriding the replace() function and then use that in the summary
  264. reasoning = reasoning.replace("$1", phrase);
  265.  
  266. // This allows me to combine the upvote and downvote replacement schemes into one
  267. } else if (replacement == "$1vote") {
  268. input = input.replace(expression, function (data, match1) {
  269. phrase = match1;
  270. return phrase + "vot";
  271. });
  272. reasoning = reasoning.replace("$1", phrase.toLowerCase());
  273.  
  274. // This is used to capitalize letters; it merely takes what is matched, uppercases it, and replaces what was matched with the uppercased verison
  275. } else if (replacement === "$1") {
  276. input = input.replace(expression, function (data, match1) {
  277. return match1.toUpperCase();
  278. });
  279.  
  280. // Default: just replace it with the indicated replacement
  281. } else {
  282. input = input.replace(expression, replacement);
  283. }
  284.  
  285. // Return a dictionary with the reasoning for the fix and what is edited (used later to prevent duplicates in the edit summary)
  286. return {
  287. reason: reasoning,
  288. fixed: input
  289. };
  290. } else {
  291.  
  292. // If nothing needs to be fixed, return null
  293. return null;
  294. }
  295. };
  296.  
  297. // Omit code
  298. App.funcs.omitCode = function (str, type) {
  299. console.log(str);
  300. str = str.replace(App.globals.checks[type], function (match) {
  301. App.globals.replacedStrings[type].push(match);
  302. return App.globals.placeHolders[type];
  303. });
  304. return str;
  305. };
  306.  
  307. // Replace code
  308. App.funcs.replaceCode = function (str, type) {
  309. for (var i = 0; i < App.globals.replacedStrings[type].length; i++) {
  310. str = str.replace(App.globals.placeHolders[type], App.globals.replacedStrings[type][i]);
  311. }
  312. return str;
  313. };
  314.  
  315. // Eliminate duplicates in array (awesome method I found on SO, check it out!)
  316. App.funcs.eliminateDuplicates = function (arr) {
  317. var i,
  318. len = arr.length,
  319. out = [],
  320. obj = {};
  321.  
  322. for (i = 0; i < len; i++) {
  323. obj[arr[i]] = 0;
  324. }
  325. for (i in obj) {
  326. out.push(i);
  327. }
  328. return out;
  329. };
  330.  
  331. // Wait for relevant dynamic content to finish loading
  332. App.funcs.dynamicDelay = function (callback) {
  333. App.selections.buttonBar = $('#wmd-button-bar-' + App.globals.questionNum);
  334.  
  335. // When button bar updates, dynamic DOM is ready for selection
  336. App.selections.buttonBar.unbind().on('DOMSubtreeModified', function () {
  337.  
  338. // Avoid running it more than once
  339. if (!App.globals.barReady) {
  340. App.globals.barReady = true;
  341.  
  342. // Run asynchronously - this lets the bar finish updating before continuing
  343. setTimeout(function () {
  344. callback();
  345. }, 0);
  346. }
  347. });
  348. };
  349.  
  350. // Populate or refresh DOM selections
  351. App.funcs.popSelections = function () {
  352. App.selections.redoButton = $('#wmd-redo-button-' + App.globals.questionNum);
  353. App.selections.bodyBox = $(".wmd-input");
  354. App.selections.titleBox = $(".ask-title-field");
  355. App.selections.summaryBox = $("#edit-comment");
  356. };
  357.  
  358. // Populate edit item sets from DOM selections - currently does not support inline edits
  359. App.funcs.popItems = function () {
  360. App.items[0] = {
  361. title: App.selections.titleBox.val(),
  362. body: App.selections.bodyBox.val(),
  363. summary: ''
  364. };
  365. };
  366.  
  367. // Insert editing button(s)
  368. App.funcs.createButton = function () {
  369.  
  370. // Insert button
  371. App.selections.redoButton.after(App.globals.buttonHTML);
  372.  
  373. // Insert spacer
  374. App.selections.redoButton.after(App.globals.spacerHTML);
  375.  
  376. // Add new elements to selections
  377. App.selections.buttonWrapper = $('#ToolkitButtonWrapper');
  378. App.selections.buttonFix = $('#ToolkitFix');
  379. App.selections.buttonInfo = $('#ToolkitInfo');
  380. };
  381.  
  382. // Style button
  383. App.funcs.styleButton = function () {
  384. App.selections.buttonWrapper.css({
  385. 'position': 'relative',
  386. 'left': '430px'
  387. });
  388. App.selections.buttonFix.css({
  389. 'position': 'static',
  390. 'float': 'left',
  391. 'border-width': '0px',
  392. 'background-color': 'white',
  393. 'background-image': 'url("http://i.imgur.com/79qYzkQ.png")',
  394. 'background-size': '100% 100%',
  395. 'width': '18px',
  396. 'height': '18px',
  397. 'outline': 'none'
  398. });
  399. App.selections.buttonInfo.css({
  400. 'position': 'static',
  401. 'float': 'left',
  402. 'margin-left': '5px',
  403. 'font-size': '12px',
  404. 'color': '#424242',
  405. 'line-height': '19px'
  406. });
  407.  
  408. App.selections.buttonFix.hover(function () {
  409. App.globals.infoContent = App.selections.buttonInfo.text();
  410. App.selections.buttonInfo.text('Fix the content!');
  411. App.selections.buttonFix.css({
  412. 'background-image': 'url("http://i.imgur.com/d5ZL09o.png")'
  413. });
  414. }, function () {
  415. App.selections.buttonInfo.text(App.globals.infoContent);
  416. App.selections.buttonFix.css({
  417. 'background-image': 'url("http://i.imgur.com/79qYzkQ.png")'
  418. });
  419. });
  420. };
  421.  
  422. // Listen to button click
  423. App.funcs.listenButton = function () {
  424. App.selections.buttonFix.click(function (e) {
  425. e.preventDefault();
  426. if (!App.globals.editsMade) {
  427.  
  428. // Refresh item population
  429. App.funcs.popItems();
  430.  
  431. // Pipe data through editing modules
  432. App.pipe(App.items, App.globals.pipeMods, App.globals.order);
  433. App.globals.editsMade = true;
  434. }
  435. });
  436. };
  437.  
  438. // Handle pipe output
  439. App.funcs.output = function (data) {
  440. App.selections.titleBox.val(data[0].title);
  441. App.selections.bodyBox.val(data[0].body);
  442. App.selections.summaryBox.val(data[0].summary);
  443.  
  444. // Update the comment: focusing on the input field to remove placeholder text,
  445. //but scroll back to the user's original location
  446. var currentPos = document.body.scrollTop;
  447. $("#wmd-input").focus();
  448. $("#edit-comment").focus();
  449. $("#wmd-input").focus();
  450. window.scrollTo(0, currentPos);
  451. App.globals.infoContent = App.globals.editCount + ' changes made';
  452. App.selections.buttonInfo.text(App.globals.editCount + ' changes made');
  453. };
  454. };
  455.  
  456. // Pipe data through modules in proper order, returning the result
  457. App.pipe = function (data, mods, order) {
  458. console.log("Piping edits...");
  459. var modName;
  460. for (var i in order) {
  461. modName = order[i];
  462. data = mods[modName](data);
  463. console.log(data[0].body);
  464. }
  465. App.funcs.output(data);
  466. console.log("Edits complete!");
  467. };
  468.  
  469. // Init app
  470. App.init = function () {
  471. App.popFuncs();
  472. App.funcs.dynamicDelay(function () {
  473. App.funcs.popSelections();
  474. App.funcs.createButton();
  475. App.funcs.styleButton();
  476. App.funcs.popItems();
  477. App.funcs.listenButton();
  478. });
  479. };
  480.  
  481. App.globals.pipeMods.omit = function (data) {
  482. data[0].body = App.funcs.omitCode(data[0].body, "block");
  483. data[0].body = App.funcs.omitCode(data[0].body, "inline");
  484. return data;
  485. };
  486.  
  487. App.globals.pipeMods.replace = function(data){
  488. data[0].body = App.funcs.replaceCode(data[0].body, "block");
  489. data[0].body = App.funcs.replaceCode(data[0].body, "inline");
  490. return data;
  491. };
  492.  
  493. App.globals.pipeMods.edit = function (data) {
  494.  
  495. // Visually confirm edit - SE makes it easy because the jQuery color animation plugin seems to be there by default
  496. App.selections.bodyBox.animate({
  497. backgroundColor: '#c8ffa7'
  498. }, 10);
  499. App.selections.bodyBox.animate({
  500. backgroundColor: '#fff'
  501. }, 1000);
  502.  
  503. //loop through all editing rules
  504. for (var j in App.edits) {
  505. if (App.edits.hasOwnProperty(j)) {
  506.  
  507. // Check body
  508. var fix = App.funcs.fixIt(data[0].body, App.edits[j].expr, App.edits[j].replacement, App.edits[j].reason);
  509. if (fix) {
  510. App.globals.reasons[App.globals.numReasons] = fix.reason;
  511. data[0].body = fix.fixed;
  512. App.globals.numReasons++;
  513. App.edits[j].fixed = true;
  514. }
  515.  
  516. // Check title
  517. fix = App.funcs.fixIt(data[0].title, App.edits[j].expr, App.edits[j].replacement, App.edits[j].reason);
  518. if (fix) {
  519. data[0].title = fix.fixed;
  520. if (!App.edits[j].fixed) {
  521. App.globals.reasons[App.globals.numReasons] = fix.reason;
  522. App.globals.numReasons++;
  523. App.edits[j].fixed = true;
  524. }
  525. }
  526. }
  527. }
  528.  
  529. //eliminate duplicate reasons
  530. App.globals.reasons = App.funcs.eliminateDuplicates(App.globals.reasons);
  531.  
  532. for (var z = 0; z < App.globals.reasons.length; z++) {
  533.  
  534. //check that summary is not getting too long
  535. if (data[0].summary.length < 200) {
  536.  
  537. //capitalize first letter
  538. if (z === 0) {
  539. data[0].summary += App.globals.reasons[z][0].toUpperCase() + App.globals.reasons[z].substring(1);
  540.  
  541. //post rest of reasons normally
  542. } else {
  543. data[0].summary += App.globals.reasons[z];
  544. }
  545.  
  546. //if it's not the last reason
  547. if (z !== App.globals.reasons.length - 1) {
  548. data[0].summary += "; ";
  549.  
  550. //if at end, punctuate
  551. } else {
  552. data[0].summary += ".";
  553. }
  554. }
  555. }
  556.  
  557. return data;
  558. };
  559.  
  560. // Start
  561. App.init();
  562.  
  563. };
  564.  
  565. // Inject the main script
  566. var script = document.createElement('script');
  567. script.type = "text/javascript";
  568. script.textContent = '(' + main.toString() + ')();';
  569. document.body.appendChild(script);
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement