Code.js 13 KB

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