usercss-compiler.js 5.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187
  1. 'use strict';
  2. let builderChain = Promise.resolve();
  3. const BUILDERS = Object.assign(Object.create(null), {
  4. default: {
  5. post(sections, vars) {
  6. require(['/js/sections-util']); /* global styleCodeEmpty */
  7. let varDef = Object.keys(vars).map(k => ` --${k}: ${vars[k].value};\n`).join('');
  8. if (!varDef) return;
  9. varDef = ':root {\n' + varDef + '}\n';
  10. for (const section of sections) {
  11. if (!styleCodeEmpty(section.code)) {
  12. spliceCssAfterGlobals(section, varDef, styleCodeEmpty.lastIndex);
  13. }
  14. }
  15. },
  16. },
  17. stylus: {
  18. pre(source, vars) {
  19. require(['/vendor/stylus-lang-bundle/stylus-renderer.min']); /* global StylusRenderer */
  20. return new Promise((resolve, reject) => {
  21. const varDef = Object.keys(vars).map(key => `${key} = ${vars[key].value};\n`).join('');
  22. new StylusRenderer(varDef + source)
  23. .render((err, output) => err ? reject(err) : resolve(output));
  24. });
  25. },
  26. },
  27. less: {
  28. async pre(source, vars) {
  29. if (!self.less) {
  30. self.less = {
  31. logLevel: 0,
  32. useFileCache: false,
  33. };
  34. }
  35. require(['/vendor/less-bundle/less.min']); /* global less */
  36. const varDefs = Object.keys(vars).map(key => `@${key}:${vars[key].value};\n`).join('');
  37. const res = await less.render(varDefs + source, {
  38. math: 'parens-division',
  39. });
  40. return res.css;
  41. },
  42. },
  43. uso: {
  44. pre(source, vars) {
  45. require(['/js/color/color-converter']); /* global colorConverter */
  46. const pool = Object.create(null);
  47. return doReplace(source);
  48. function doReplace(text) {
  49. return text.replace(/(\/\*\[\[([\w-]+)]]\*\/)([0-9a-f]{2}(?=\W))?/gi, (_, cmt, name, alpha) => {
  50. const key = alpha ? name + '[A]' : name;
  51. let val = pool[key];
  52. if (val === undefined) {
  53. val = pool[key] = getValue(name, null, alpha);
  54. }
  55. return (val != null ? val : cmt) + (alpha || '');
  56. });
  57. }
  58. function getValue(name, isUsoRgb, alpha) {
  59. const v = vars[name];
  60. if (!v) {
  61. return name.endsWith('-rgb')
  62. ? getValue(name.slice(0, -4), true)
  63. : null;
  64. }
  65. let {value} = v;
  66. switch (v.type) {
  67. case 'color':
  68. value = colorConverter.parse(value) || null;
  69. if (value) {
  70. /* #rrggbb - inline alpha is present; an opaque hsl/a; #rrggbb originally
  71. * rgba(r, g, b, a) - transparency <1 is present (Chrome pre-66 compatibility)
  72. * rgb(r, g, b) - if color is rgb/a with a=1, note: r/g/b will be rounded
  73. * r, g, b - if the var has `-rgb` suffix per USO specification
  74. * TODO: when minimum_chrome_version >= 66 try to keep `value` intact */
  75. if (alpha) delete value.a;
  76. const isRgb = isUsoRgb || value.type === 'rgb' || value.a != null && value.a !== 1;
  77. const usoMode = isUsoRgb || !isRgb;
  78. value = colorConverter.format(value, isRgb ? 'rgb' : 'hex', undefined, usoMode);
  79. }
  80. return value;
  81. case 'dropdown':
  82. case 'select':
  83. pool[name] = ''; // prevent infinite recursion
  84. return doReplace(value);
  85. }
  86. return value;
  87. }
  88. },
  89. },
  90. });
  91. /* exported compileUsercss */
  92. /**
  93. * @param {string} preprocessor
  94. * @param {string} code
  95. * @param {Object} [vars] - WARNING: each var's `value` will be overwritten
  96. (not a problem currently as this code runs in a worker so `vars` is just a copy)
  97. * @returns {Promise<{sections, errors}>}
  98. */
  99. async function compileUsercss(preprocessor, code, vars) {
  100. let builder = BUILDERS[preprocessor];
  101. if (!builder) {
  102. builder = BUILDERS.default;
  103. if (preprocessor != null) console.warn(`Unknown preprocessor "${preprocessor}"`);
  104. }
  105. if (vars) {
  106. simplifyUsercssVars(vars);
  107. } else {
  108. vars = {};
  109. }
  110. const log = [];
  111. if (builder.pre) {
  112. // another compileUsercss may(?) become active while this one is awaited so let's chain
  113. builderChain = builderChain.catch(() => {}).then(async () => {
  114. const logFn = console.log;
  115. console.log = (...args) => log.push(args);
  116. code = await builder.pre(code, vars);
  117. console.log = logFn;
  118. });
  119. await builderChain;
  120. }
  121. require(['/js/moz-parser']); /* global extractSections */
  122. const res = extractSections({code});
  123. if (builder.post) {
  124. builder.post(res.sections, vars);
  125. }
  126. if (log.length) {
  127. res.log = log;
  128. }
  129. return res;
  130. }
  131. /**
  132. * Adds units and sets `null` values to their defaults
  133. * WARNING: the old value is overwritten
  134. */
  135. function simplifyUsercssVars(vars) {
  136. for (const va of Object.values(vars)) {
  137. let value = va.value != null ? va.value : va.default;
  138. switch (va.type) {
  139. case 'select':
  140. case 'dropdown':
  141. case 'image':
  142. // TODO: handle customized image
  143. for (const opt of va.options) {
  144. if (opt.name === value) {
  145. value = opt.value;
  146. break;
  147. }
  148. }
  149. break;
  150. case 'number':
  151. case 'range':
  152. value += va.units || '';
  153. break;
  154. }
  155. va.value = value;
  156. }
  157. }
  158. function spliceCssAfterGlobals(section, newText, after) {
  159. const {code} = section;
  160. const RX_IMPORT = /@import\s/gi;
  161. RX_IMPORT.lastIndex = after;
  162. if (RX_IMPORT.test(code)) {
  163. require(['/js/csslint/parserlib']); /* global parserlib */
  164. const parser = new parserlib.css.Parser();
  165. parser._tokenStream = new parserlib.css.TokenStream(code);
  166. parser._sheetGlobals();
  167. const {col, line, offset} = parser._tokenStream._token;
  168. // normalizing newlines in non-usercss to match line:col from parserlib
  169. if ((code.indexOf('\r') + 1 || 1e99) - 1 < offset) {
  170. after = col + code.split('\n', line).reduce((len, s) => len + s.length + 1, 0);
  171. } else {
  172. after = offset + 1;
  173. }
  174. }
  175. section.code = (after ? code.slice(0, after) + '\n' : '') + newText + code.slice(after);
  176. }