tmtheme.js 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412
  1. var fs = require("fs");
  2. var path = require("path");
  3. var util = require("util");
  4. var cssParse = require("css-parse");
  5. var cssStringify = require("css-stringify");
  6. var parseString = require("plist").parseString;
  7. function parseTheme(themeXml, callback) {
  8. parseString(themeXml, function(_, theme) {
  9. callback(theme[0])
  10. });
  11. }
  12. var unsupportedScopes = { };
  13. var supportedScopes = {
  14. "keyword": "keyword",
  15. "keyword.operator": "keyword.operator",
  16. "keyword.other.unit": "keyword.other.unit",
  17. "constant": "constant",
  18. "constant.language": "constant.language",
  19. "constant.library": "constant.library",
  20. "constant.numeric": "constant.numeric",
  21. "constant.character" : "constant.character",
  22. "constant.character.escape" : "constant.character.escape",
  23. "constant.character.entity": "constant.character.entity",
  24. "constant.other" : "constant.other",
  25. "support": "support",
  26. "support.function": "support.function",
  27. "support.function.dom": "support.function.dom",
  28. "support.function.firebug": "support.firebug",
  29. "support.function.constant": "support.function.constant",
  30. "support.constant": "support.constant",
  31. "support.constant.property-value": "support.constant.property-value",
  32. "support.class": "support.class",
  33. "support.type": "support.type",
  34. "support.other": "support.other",
  35. "function": "function",
  36. "function.buildin": "function.buildin",
  37. "storage": "storage",
  38. "storage.type": "storage.type",
  39. "invalid": "invalid",
  40. "invalid.illegal": "invalid.illegal",
  41. "invalid.deprecated": "invalid.deprecated",
  42. "string": "string",
  43. "string.regexp": "string.regexp",
  44. "comment": "comment",
  45. "comment.documentation": "comment.doc",
  46. "comment.documentation.tag": "comment.doc.tag",
  47. "variable": "variable",
  48. "variable.language": "variable.language",
  49. "variable.parameter": "variable.parameter",
  50. "meta": "meta",
  51. "meta.tag.sgml.doctype": "xml-pe",
  52. "meta.tag": "meta.tag",
  53. "meta.selector": "meta.selector",
  54. "entity.other.attribute-name": "entity.other.attribute-name",
  55. "entity.name.function": "entity.name.function",
  56. "entity.name": "entity.name",
  57. "entity.name.tag": "entity.name.tag",
  58. "markup.heading": "markup.heading",
  59. "markup.heading.1": "markup.heading.1",
  60. "markup.heading.2": "markup.heading.2",
  61. "markup.heading.3": "markup.heading.3",
  62. "markup.heading.4": "markup.heading.4",
  63. "markup.heading.5": "markup.heading.5",
  64. "markup.heading.6": "markup.heading.6",
  65. "markup.list": "markup.list",
  66. "collab.user1": "collab.user1"
  67. };
  68. var fallbackScopes = {
  69. "keyword": "meta",
  70. "support.type": "storage.type",
  71. "variable": "entity.name.function"
  72. };
  73. function extractStyles(theme) {
  74. var globalSettings = theme.settings[0].settings;
  75. var colors = {
  76. "printMargin": "#e8e8e8",
  77. "background": parseColor(globalSettings.background),
  78. "foreground": parseColor(globalSettings.foreground),
  79. "gutter": "#e8e8e8",
  80. "selection": parseColor(globalSettings.selection),
  81. "step": "rgb(198, 219, 174)",
  82. "bracket": parseColor(globalSettings.invisibles),
  83. "active_line": parseColor(globalSettings.lineHighlight),
  84. "cursor": parseColor(globalSettings.caret),
  85. "invisible": "color: " + parseColor(globalSettings.invisibles) + ";"
  86. };
  87. for (var i=1; i<theme.settings.length; i++) {
  88. var element = theme.settings[i];
  89. if (!element.scope || !element.settings)
  90. continue;
  91. var scopes = element.scope.split(/\s*[|,]\s*/g);
  92. for (var j = 0; j < scopes.length; j++) {
  93. var scope = scopes[j];
  94. var style = parseStyles(element.settings);
  95. var aceScope = supportedScopes[scope];
  96. if (aceScope) {
  97. colors[aceScope] = style;
  98. }
  99. else if (style) {
  100. unsupportedScopes[scope] = (unsupportedScopes[scope] || 0) + 1;
  101. }
  102. }
  103. }
  104. for (var i in fallbackScopes) {
  105. if (!colors[i])
  106. colors[i] = colors[fallbackScopes[i]];
  107. }
  108. if (!colors.fold) {
  109. var foldSource = colors["entity.name.function"] || colors.keyword;
  110. if (foldSource) {
  111. colors.fold = foldSource.match(/\:([^;]+)/)[1];
  112. }
  113. }
  114. colors.gutterBg = colors.background
  115. colors.gutterFg = mix(colors.foreground, colors.background, 0.5)
  116. if (!colors.selected_word_highlight)
  117. colors.selected_word_highlight = "border: 1px solid " + colors.selection + ";";
  118. colors.isDark = (luma(colors.background) < 0.5) + "";
  119. return colors;
  120. };
  121. function mix(c1, c2, a1, a2) {
  122. c1 = rgbColor(c1);
  123. c2 = rgbColor(c2);
  124. if (a2 === undefined)
  125. a2 = 1 - a1
  126. return "rgb(" + [
  127. Math.round(a1*c1[0] + a2*c2[0]),
  128. Math.round(a1*c1[1] + a2*c2[1]),
  129. Math.round(a1*c1[2] + a2*c2[2])
  130. ].join(",") + ")";
  131. }
  132. function rgbColor(color) {
  133. if (typeof color == "object")
  134. return color;
  135. if (color[0]=="#")
  136. return color.match(/^#(..)(..)(..)/).slice(1).map(function(c) {
  137. return parseInt(c, 16);
  138. });
  139. else
  140. return color.match(/\(([^,]+),([^,]+),([^,]+)/).slice(1).map(function(c) {
  141. return parseInt(c, 10);
  142. });
  143. }
  144. function luma(color) {
  145. var rgb = rgbColor(color);
  146. return (0.21 * rgb[0] + 0.72 * rgb[1] + 0.07 * rgb[2]) / 255;
  147. }
  148. function parseColor(color) {
  149. if (color.length == 4)
  150. color = color.replace(/[a-fA-F\d]/g, "$&$&");
  151. if (color.length == 7)
  152. return color;
  153. else {
  154. if (!color.match(/^#(..)(..)(..)(..)$/))
  155. console.error("can't parse color", color);
  156. var rgba = color.match(/^#(..)(..)(..)(..)$/).slice(1).map(function(c) {
  157. return parseInt(c, 16);
  158. });
  159. rgba[3] = (rgba[3] / 0xFF).toPrecision(2);
  160. return "rgba(" + rgba.join(", ") + ")";
  161. }
  162. }
  163. function parseStyles(styles) {
  164. var css = [];
  165. var fontStyle = styles.fontStyle || "";
  166. if (fontStyle.indexOf("underline") !== -1) {
  167. css.push("text-decoration:underline;");
  168. }
  169. if (fontStyle.indexOf("italic") !== -1) {
  170. css.push("font-style:italic;");
  171. }
  172. if (styles.foreground) {
  173. css.push("color:" + parseColor(styles.foreground) + ";");
  174. }
  175. if (styles.background) {
  176. css.push("background-color:" + parseColor(styles.background) + ";");
  177. }
  178. return css.join("\n");
  179. }
  180. function fillTemplate(template, replacements) {
  181. return template.replace(/%(.+?)%/g, function(str, m) {
  182. return replacements[m] || "";
  183. });
  184. }
  185. function hyphenate(str) {
  186. return str.replace(/([A-Z])/g, "-$1").replace(/_/g, "-").toLowerCase();
  187. }
  188. function quoteString(str) {
  189. return '"' + str.replace(/\\/, "\\\\").replace(/"/g, '\\"').replace(/\n/g, "\\\n") + '"';
  190. }
  191. var cssTemplate = fs.readFileSync(__dirname + "/templates/theme.css", "utf8");
  192. var jsTemplate = fs.readFileSync(__dirname + "/templates/theme.js", "utf8");
  193. function normalizeStylesheet(rules) {
  194. for (var i = rules.length; i--; ) {
  195. var s = JSON.stringify(rules[i].declarations);
  196. for (var j = i; j --; ) {
  197. if (s == JSON.stringify(rules[j].declarations)) {
  198. console.log(rules[j].selectors, rules[i].selectors)
  199. console.log(i, j)
  200. rules[j].selectors = rules[i].selectors.concat(rules[j].selectors);
  201. rules.splice(i, 1);
  202. break;
  203. }
  204. }
  205. }
  206. for (var i = rules.length; i--; ) {
  207. var s = rules[i].selectors.sort();
  208. rules[i].selectors = s.filter(function(x, i) {
  209. return x && x != s[i + 1];
  210. });
  211. }
  212. return rules;
  213. }
  214. var themes = {
  215. //"chrome": "Chrome DevTools",
  216. "clouds": "Clouds",
  217. "clouds_midnight": "Clouds Midnight",
  218. "cobalt": "Cobalt",
  219. //"crimson_editor": "Crimson Editor",
  220. "dawn": "Dawn",
  221. //"dreamweaver": "Dreamweaver",
  222. //"eclipse": "Eclipse",
  223. //"github": "GitHub",
  224. "idle_fingers": "idleFingers",
  225. "kr_theme": "krTheme",
  226. "merbivore": "Merbivore",
  227. "merbivore_soft": "Merbivore Soft",
  228. "mono_industrial": "monoindustrial",
  229. "monokai": "Monokai",
  230. "pastel_on_dark": "Pastels on Dark",
  231. "solarized_dark": "Solarized-dark",
  232. "solarized_light": "Solarized-light",
  233. "katzenmilch": "Katzenmilch",
  234. "kuroir": "Kuroir Theme",
  235. //"textmate": "Textmate (Mac Classic)",
  236. "tomorrow": "Tomorrow",
  237. "tomorrow_night": "Tomorrow-Night",
  238. "tomorrow_night_blue": "Tomorrow-Night-Blue",
  239. "tomorrow_night_bright": "Tomorrow-Night-Bright",
  240. "tomorrow_night_eighties": "Tomorrow-Night-Eighties",
  241. "twilight": "Twilight",
  242. "vibrant_ink": "Vibrant Ink",
  243. "xcode": "Xcode_default"
  244. };
  245. function convertBuiltinTheme(name) {
  246. return convertTheme(name, __dirname + "/tmthemes/" + themes[name] + ".tmTheme", __dirname + "/../lib/ace/theme");
  247. }
  248. function convertTheme(name, tmThemePath, outputDirectory) {
  249. console.log("Converting " + name);
  250. var tmTheme = fs.readFileSync(tmThemePath, "utf8");
  251. parseTheme(tmTheme, function(theme) {
  252. var styles = extractStyles(theme);
  253. styles.cssClass = "ace-" + hyphenate(name);
  254. styles.uuid = theme.uuid;
  255. var css = fillTemplate(cssTemplate, styles);
  256. css = css.replace(/[^\{\}]+{\s*}/g, "");
  257. for (var i in supportedScopes) {
  258. if (!styles[i])
  259. continue;
  260. css += "." + styles.cssClass + " " +
  261. i.replace(/^|\./g, ".ace_") + "{" + styles[i] + "}";
  262. }
  263. // we're going to look for NEW rules in the parsed content only
  264. // if such a rule exists, add it to the destination file
  265. // this way, we preserve all hand-modified rules in the <theme>.css rules,
  266. // (because some exist, for collab1 and ace_indentation_guide
  267. try {
  268. var outThemeCss = fs.readFileSync(outputDirectory + "/" + name + ".css");
  269. var oldRules = cssParse(outThemeCss).stylesheet.rules;
  270. var newRules = cssParse(css).stylesheet.rules;
  271. for (var i = 0; i < newRules.length; i++) {
  272. var newSelectors = newRules[i].selectors;
  273. for (var j = 0; j < oldRules.length; j++) {
  274. var oldSelectors = oldRules[j].selectors;
  275. newSelectors = newSelectors.filter(function(s) {
  276. return oldSelectors.indexOf(s) == -1;
  277. })
  278. if (!newSelectors.length)
  279. break;
  280. }
  281. if (newSelectors.length) {
  282. newRules[i].selectors = newSelectors;
  283. console.log("Adding NEW rule: ", newRules[i])
  284. oldRules.splice(i, 0, newRules[i]);
  285. }
  286. }
  287. oldRules = normalizeStylesheet(oldRules);
  288. css = cssStringify({stylesheet: {rules: oldRules}}, { compress: false });
  289. } catch(e) {
  290. console.log("Creating new file: " + name + ".css")
  291. css = cssStringify(cssParse(css), { compress: false });
  292. }
  293. var js = fillTemplate(jsTemplate, {
  294. name: name,
  295. css: 'require("../requirejs/text!./' + name + '.css")', // quoteString(css), //
  296. cssClass: "ace-" + hyphenate(name),
  297. isDark: styles.isDark
  298. });
  299. fs.writeFileSync(outputDirectory + "/" + name + ".js", js);
  300. fs.writeFileSync(outputDirectory + "/" + name + ".css", css);
  301. })
  302. }
  303. if (process.argv.length > 1) {
  304. var args = process.argv.splice(2);
  305. if (args.length < 3) {
  306. console.error("Usage: node tmtheme.js [theme_name, path/to/theme.tmTheme path/to/output/directory]");
  307. process.exit(1);
  308. }
  309. var name = args[0];
  310. var themePath = args[1];
  311. var outputDirectory = args[2];
  312. convertTheme(name, themePath, outputDirectory);
  313. } else {
  314. for (var name in themes) {
  315. convertBuiltinTheme(name);
  316. }
  317. }
  318. var sortedUnsupportedScopes = {};
  319. for (var u in unsupportedScopes) {
  320. var value = unsupportedScopes[u];
  321. if (sortedUnsupportedScopes[value] === undefined) {
  322. sortedUnsupportedScopes[value] = [];
  323. }
  324. sortedUnsupportedScopes[value].push(u);
  325. }
  326. console.log("I found these unsupported scopes:");
  327. console.log(sortedUnsupportedScopes);
  328. console.log("It's safe to ignore these, but they may affect your syntax highlighting if your mode depends on any of these rules.");
  329. console.log("Refer to the docs on ace.ajax.org for information on how to add a scope to the CSS generator.");
  330. /*** TODO: generate images for indent guides in node
  331. var indentGuideColor = "#2D2D2D"
  332. var canvas = document.createElement("canvas")
  333. canvas.width = 1; canvas.height = 2;
  334. var ctx = canvas.getContext("2d")
  335. imageData = ctx.getImageData(0,0,1,2)
  336. function getColor(color) {
  337. ctx.fillStyle = color;
  338. ctx.fillRect(0,0,1,2);
  339. return Array.slice(ctx.getImageData(0,0,1,2).data).slice(0,4)
  340. }
  341. bgColor = getComputedStyle(ace.renderer.scroller).backgroundColor
  342. var a = [].concat(getColor(bgColor), getColor(indentGuideColor));
  343. a.forEach(function(val,i){imageData.data[i] = val})
  344. ctx.putImageData(imageData,0,0)
  345. image = canvas.toDataURL("png")
  346. var rule = "."+ace.renderer.$theme +" .ace_indent-guide {\n\
  347. background: url(" + image +") right repeat-y;\n\
  348. }"
  349. console.log(rule)
  350. require("ace/lib/dom").importCssString(rule)
  351. */