Code.js 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501
  1. import {camelCase, kebabCase, snakeCase} from "lodash";
  2. import Utils from "../Utils.js";
  3. import vkbeautify from "vkbeautify";
  4. import {DOMParser as dom} from "xmldom";
  5. import xpath from "xpath";
  6. import prettyPrintOne from "imports-loader?window=>global!exports-loader?prettyPrintOne!google-code-prettify/bin/prettify.min.js";
  7. /**
  8. * Code operations.
  9. *
  10. * @author n1474335 [n1474335@gmail.com]
  11. * @copyright Crown Copyright 2016
  12. * @license Apache-2.0
  13. *
  14. * @namespace
  15. */
  16. const Code = {
  17. /**
  18. * @constant
  19. * @default
  20. */
  21. LANGUAGES: ["default-code", "default-markup", "bash", "bsh", "c", "cc", "coffee", "cpp", "cs", "csh", "cv", "cxx", "cyc", "htm", "html", "in.tag", "java", "javascript", "js", "json", "m", "mxml", "perl", "pl", "pm", "py", "python", "rb", "rc", "rs", "ruby", "rust", "sh", "uq.val", "xhtml", "xml", "xsl"],
  22. /**
  23. * @constant
  24. * @default
  25. */
  26. LINE_NUMS: false,
  27. /**
  28. * Syntax highlighter operation.
  29. *
  30. * @param {string} input
  31. * @param {Object[]} args
  32. * @returns {html}
  33. */
  34. runSyntaxHighlight: function(input, args) {
  35. let language = args[0],
  36. lineNums = args[1];
  37. return "<code class='prettyprint'>" + prettyPrintOne(Utils.escapeHtml(input), language, lineNums) + "</code>";
  38. },
  39. /**
  40. * @constant
  41. * @default
  42. */
  43. BEAUTIFY_INDENT: "\\t",
  44. /**
  45. * XML Beautify operation.
  46. *
  47. * @param {string} input
  48. * @param {Object[]} args
  49. * @returns {string}
  50. */
  51. runXmlBeautify: function(input, args) {
  52. const indentStr = args[0];
  53. return vkbeautify.xml(input, indentStr);
  54. },
  55. /**
  56. * JSON Beautify operation.
  57. *
  58. * @param {string} input
  59. * @param {Object[]} args
  60. * @returns {string}
  61. */
  62. runJsonBeautify: function(input, args) {
  63. const indentStr = args[0];
  64. if (!input) return "";
  65. return vkbeautify.json(input, indentStr);
  66. },
  67. /**
  68. * CSS Beautify operation.
  69. *
  70. * @param {string} input
  71. * @param {Object[]} args
  72. * @returns {string}
  73. */
  74. runCssBeautify: function(input, args) {
  75. const indentStr = args[0];
  76. return vkbeautify.css(input, indentStr);
  77. },
  78. /**
  79. * SQL Beautify operation.
  80. *
  81. * @param {string} input
  82. * @param {Object[]} args
  83. * @returns {string}
  84. */
  85. runSqlBeautify: function(input, args) {
  86. const indentStr = args[0];
  87. return vkbeautify.sql(input, indentStr);
  88. },
  89. /**
  90. * @constant
  91. * @default
  92. */
  93. PRESERVE_COMMENTS: false,
  94. /**
  95. * XML Minify operation.
  96. *
  97. * @param {string} input
  98. * @param {Object[]} args
  99. * @returns {string}
  100. */
  101. runXmlMinify: function(input, args) {
  102. const preserveComments = args[0];
  103. return vkbeautify.xmlmin(input, preserveComments);
  104. },
  105. /**
  106. * JSON Minify operation.
  107. *
  108. * @param {string} input
  109. * @param {Object[]} args
  110. * @returns {string}
  111. */
  112. runJsonMinify: function(input, args) {
  113. if (!input) return "";
  114. return vkbeautify.jsonmin(input);
  115. },
  116. /**
  117. * CSS Minify operation.
  118. *
  119. * @param {string} input
  120. * @param {Object[]} args
  121. * @returns {string}
  122. */
  123. runCssMinify: function(input, args) {
  124. const preserveComments = args[0];
  125. return vkbeautify.cssmin(input, preserveComments);
  126. },
  127. /**
  128. * SQL Minify operation.
  129. *
  130. * @param {string} input
  131. * @param {Object[]} args
  132. * @returns {string}
  133. */
  134. runSqlMinify: function(input, args) {
  135. return vkbeautify.sqlmin(input);
  136. },
  137. /**
  138. * Generic Code Beautify operation.
  139. *
  140. * Yeeeaaah...
  141. *
  142. * I'm not proud of this code, but seriously, try writing a generic lexer and parser that
  143. * correctly generates an AST for multiple different languages. I have tried, and I can tell
  144. * you it's pretty much impossible.
  145. *
  146. * This basically works. That'll have to be good enough. It's not meant to produce working code,
  147. * just slightly more readable code.
  148. *
  149. * Things that don't work:
  150. * - For loop formatting
  151. * - Do-While loop formatting
  152. * - Switch/Case indentation
  153. * - Bit shift operators
  154. *
  155. * @author n1474335 [n1474335@gmail.com]
  156. * @param {string} input
  157. * @param {Object[]} args
  158. * @returns {string}
  159. */
  160. runGenericBeautify: function(input, args) {
  161. let code = input,
  162. t = 0,
  163. preservedTokens = [],
  164. m;
  165. // Remove strings
  166. const sstrings = /'([^'\\]|\\.)*'/g;
  167. while ((m = sstrings.exec(code))) {
  168. code = preserveToken(code, m, t++);
  169. sstrings.lastIndex = m.index;
  170. }
  171. const dstrings = /"([^"\\]|\\.)*"/g;
  172. while ((m = dstrings.exec(code))) {
  173. code = preserveToken(code, m, t++);
  174. dstrings.lastIndex = m.index;
  175. }
  176. // Remove comments
  177. const scomments = /\/\/[^\n\r]*/g;
  178. while ((m = scomments.exec(code))) {
  179. code = preserveToken(code, m, t++);
  180. scomments.lastIndex = m.index;
  181. }
  182. const mcomments = /\/\*[\s\S]*?\*\//gm;
  183. while ((m = mcomments.exec(code))) {
  184. code = preserveToken(code, m, t++);
  185. mcomments.lastIndex = m.index;
  186. }
  187. const hcomments = /(^|\n)#[^\n\r#]+/g;
  188. while ((m = hcomments.exec(code))) {
  189. code = preserveToken(code, m, t++);
  190. hcomments.lastIndex = m.index;
  191. }
  192. // Remove regexes
  193. const regexes = /\/.*?[^\\]\/[gim]{0,3}/gi;
  194. while ((m = regexes.exec(code))) {
  195. code = preserveToken(code, m, t++);
  196. regexes.lastIndex = m.index;
  197. }
  198. code = code
  199. // Create newlines after ;
  200. .replace(/;/g, ";\n")
  201. // Create newlines after { and around }
  202. .replace(/{/g, "{\n")
  203. .replace(/}/g, "\n}\n")
  204. // Remove carriage returns
  205. .replace(/\r/g, "")
  206. // Remove all indentation
  207. .replace(/^\s+/g, "")
  208. .replace(/\n\s+/g, "\n")
  209. // Remove trailing spaces
  210. .replace(/\s*$/g, "")
  211. .replace(/\n{/g, "{");
  212. // Indent
  213. let i = 0,
  214. level = 0,
  215. indent;
  216. while (i < code.length) {
  217. switch (code[i]) {
  218. case "{":
  219. level++;
  220. break;
  221. case "\n":
  222. if (i+1 >= code.length) break;
  223. if (code[i+1] === "}") level--;
  224. indent = (level >= 0) ? Array(level*4+1).join(" ") : "";
  225. code = code.substring(0, i+1) + indent + code.substring(i+1);
  226. if (level > 0) i += level*4;
  227. break;
  228. }
  229. i++;
  230. }
  231. code = code
  232. // Add strategic spaces
  233. .replace(/\s*([!<>=+-/*]?)=\s*/g, " $1= ")
  234. .replace(/\s*<([=]?)\s*/g, " <$1 ")
  235. .replace(/\s*>([=]?)\s*/g, " >$1 ")
  236. .replace(/([^+])\+([^+=])/g, "$1 + $2")
  237. .replace(/([^-])-([^-=])/g, "$1 - $2")
  238. .replace(/([^*])\*([^*=])/g, "$1 * $2")
  239. .replace(/([^/])\/([^/=])/g, "$1 / $2")
  240. .replace(/\s*,\s*/g, ", ")
  241. .replace(/\s*{/g, " {")
  242. .replace(/}\n/g, "}\n\n")
  243. // Hacky horribleness
  244. .replace(/(if|for|while|with|elif|elseif)\s*\(([^\n]*)\)\s*\n([^{])/gim, "$1 ($2)\n $3")
  245. .replace(/(if|for|while|with|elif|elseif)\s*\(([^\n]*)\)([^{])/gim, "$1 ($2) $3")
  246. .replace(/else\s*\n([^{])/gim, "else\n $1")
  247. .replace(/else\s+([^{])/gim, "else $1")
  248. // Remove strategic spaces
  249. .replace(/\s+;/g, ";")
  250. .replace(/\{\s+\}/g, "{}")
  251. .replace(/\[\s+\]/g, "[]")
  252. .replace(/}\s*(else|catch|except|finally|elif|elseif|else if)/gi, "} $1");
  253. // Replace preserved tokens
  254. const ptokens = /###preservedToken(\d+)###/g;
  255. while ((m = ptokens.exec(code))) {
  256. const ti = parseInt(m[1], 10);
  257. code = code.substring(0, m.index) + preservedTokens[ti] + code.substring(m.index + m[0].length);
  258. ptokens.lastIndex = m.index;
  259. }
  260. return code;
  261. /**
  262. * Replaces a matched token with a placeholder value.
  263. */
  264. function preserveToken(str, match, t) {
  265. preservedTokens[t] = match[0];
  266. return str.substring(0, match.index) +
  267. "###preservedToken" + t + "###" +
  268. str.substring(match.index + match[0].length);
  269. }
  270. },
  271. /**
  272. * @constant
  273. * @default
  274. */
  275. XPATH_INITIAL: "",
  276. /**
  277. * @constant
  278. * @default
  279. */
  280. XPATH_DELIMITER: "\\n",
  281. /**
  282. * XPath expression operation.
  283. *
  284. * @author Mikescher (https://github.com/Mikescher | https://mikescher.com)
  285. * @param {string} input
  286. * @param {Object[]} args
  287. * @returns {string}
  288. */
  289. runXpath: function(input, args) {
  290. let query = args[0],
  291. delimiter = args[1];
  292. let doc;
  293. try {
  294. doc = new dom().parseFromString(input);
  295. } catch (err) {
  296. return "Invalid input XML.";
  297. }
  298. let nodes;
  299. try {
  300. nodes = xpath.select(query, doc);
  301. } catch (err) {
  302. return "Invalid XPath. Details:\n" + err.message;
  303. }
  304. const nodeToString = function(node) {
  305. return node.toString();
  306. };
  307. return nodes.map(nodeToString).join(delimiter);
  308. },
  309. /**
  310. * @constant
  311. * @default
  312. */
  313. CSS_SELECTOR_INITIAL: "",
  314. /**
  315. * @constant
  316. * @default
  317. */
  318. CSS_QUERY_DELIMITER: "\\n",
  319. /**
  320. * CSS selector operation.
  321. *
  322. * @author Mikescher (https://github.com/Mikescher | https://mikescher.com)
  323. * @author n1474335 [n1474335@gmail.com]
  324. * @param {string} input
  325. * @param {Object[]} args
  326. * @returns {string}
  327. */
  328. runCSSQuery: function(input, args) {
  329. let query = args[0],
  330. delimiter = args[1],
  331. parser = new DOMParser(),
  332. html,
  333. result;
  334. if (!query.length || !input.length) {
  335. return "";
  336. }
  337. try {
  338. html = parser.parseFromString(input, "text/html");
  339. } catch (err) {
  340. return "Invalid input HTML.";
  341. }
  342. try {
  343. result = html.querySelectorAll(query);
  344. } catch (err) {
  345. return "Invalid CSS Selector. Details:\n" + err.message;
  346. }
  347. const nodeToString = function(node) {
  348. switch (node.nodeType) {
  349. case Node.ELEMENT_NODE: return node.outerHTML;
  350. case Node.ATTRIBUTE_NODE: return node.value;
  351. case Node.COMMENT_NODE: return node.data;
  352. case Node.TEXT_NODE: return node.wholeText;
  353. case Node.DOCUMENT_NODE: return node.outerHTML;
  354. default: throw new Error("Unknown Node Type: " + node.nodeType);
  355. }
  356. };
  357. return Array.apply(null, Array(result.length))
  358. .map(function(_, i) {
  359. return result[i];
  360. })
  361. .map(nodeToString)
  362. .join(delimiter);
  363. },
  364. /**
  365. * This tries to rename variable names in a code snippet according to a function.
  366. *
  367. * @param {string} input
  368. * @param {function} replacer - this function will be fed the token which should be renamed.
  369. * @returns {string}
  370. */
  371. _replaceVariableNames(input, replacer) {
  372. const tokenRegex = /\\"|"(?:\\"|[^"])*"|(\b[a-z0-9\-_]+\b)/ig;
  373. return input.replace(tokenRegex, (...args) => {
  374. let match = args[0],
  375. quotes = args[1];
  376. if (!quotes) {
  377. return match;
  378. } else {
  379. return replacer(match);
  380. }
  381. });
  382. },
  383. /**
  384. * Converts to snake_case.
  385. *
  386. * @param {string} input
  387. * @param {Object[]} args
  388. * @returns {string}
  389. *
  390. */
  391. runToSnakeCase(input, args) {
  392. const smart = args[0];
  393. if (smart) {
  394. return Code._replaceVariableNames(input, snakeCase);
  395. } else {
  396. return snakeCase(input);
  397. }
  398. },
  399. /**
  400. * Converts to camelCase.
  401. *
  402. * @param {string} input
  403. * @param {Object[]} args
  404. * @returns {string}
  405. *
  406. */
  407. runToCamelCase(input, args) {
  408. const smart = args[0];
  409. if (smart) {
  410. return Code._replaceVariableNames(input, camelCase);
  411. } else {
  412. return camelCase(input);
  413. }
  414. },
  415. /**
  416. * Converts to kebab-case.
  417. *
  418. * @param {string} input
  419. * @param {Object[]} args
  420. * @returns {string}
  421. *
  422. */
  423. runToKebabCase(input, args) {
  424. const smart = args[0];
  425. if (smart) {
  426. return Code._replaceVariableNames(input, kebabCase);
  427. } else {
  428. return kebabCase(input);
  429. }
  430. },
  431. };
  432. export default Code;