Code.js 14 KB

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