usercss-manager.js 5.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160
  1. /* global API */// msg.js
  2. /* global RX_META deepCopy download */// toolbox.js
  3. 'use strict';
  4. const usercssMan = {
  5. GLOBAL_META: Object.entries({
  6. author: null,
  7. description: null,
  8. homepageURL: 'url',
  9. updateURL: 'updateUrl',
  10. name: null,
  11. }),
  12. async assignVars(style, oldStyle) {
  13. const meta = style.usercssData;
  14. const vars = meta.vars;
  15. const oldVars = (oldStyle.usercssData || {}).vars;
  16. if (vars && oldVars) {
  17. // The type of var might be changed during the update. Set value to null if the value is invalid.
  18. for (const [key, v] of Object.entries(vars)) {
  19. const old = oldVars[key] && oldVars[key].value;
  20. if (old) v.value = old;
  21. }
  22. meta.vars = await API.worker.nullifyInvalidVars(vars);
  23. }
  24. },
  25. async build({
  26. styleId,
  27. sourceCode,
  28. vars,
  29. checkDup,
  30. metaOnly,
  31. assignVars,
  32. initialUrl,
  33. }) {
  34. // downloading here while install-usercss page is loading to avoid the wait
  35. if (initialUrl) sourceCode = await download(initialUrl);
  36. const style = await usercssMan.buildMeta({sourceCode});
  37. const dup = (checkDup || assignVars) &&
  38. await usercssMan.find(styleId ? {id: styleId} : style);
  39. let log;
  40. if (!metaOnly) {
  41. if (vars || assignVars) {
  42. await usercssMan.assignVars(style, vars ? {usercssData: {vars}} : dup);
  43. }
  44. await usercssMan.buildCode(style);
  45. log = style.log; // extracting the non-enumerable prop, otherwise it won't survive messaging
  46. }
  47. return {style, dup, log};
  48. },
  49. async buildCode(style) {
  50. const {sourceCode: code, usercssData: {vars, preprocessor}} = style;
  51. const match = code.match(RX_META);
  52. const i = match.index;
  53. const j = i + match[0].length;
  54. const codeNoMeta = code.slice(0, i) + blankOut(code, i, j) + code.slice(j);
  55. const {sections, errors, log} = await API.worker.compileUsercss(preprocessor, codeNoMeta, vars);
  56. const recoverable = errors.every(e => e.recoverable);
  57. if (!sections.length || !recoverable) {
  58. throw !recoverable ? errors : 'Style does not contain any actual CSS to apply.';
  59. }
  60. style.sections = sections;
  61. // adding a non-enumerable prop so it won't be written to storage
  62. if (log) Object.defineProperty(style, 'log', {value: log});
  63. return style;
  64. },
  65. async buildMeta(style) {
  66. if (style.usercssData) {
  67. return style;
  68. }
  69. // remember normalized sourceCode
  70. let code = style.sourceCode = style.sourceCode.replace(/\r\n?/g, '\n');
  71. style = Object.assign({
  72. enabled: true,
  73. sections: [],
  74. }, style);
  75. const match = code.match(RX_META);
  76. if (!match) {
  77. return Promise.reject(new Error('Could not find metadata.'));
  78. }
  79. try {
  80. code = blankOut(code, 0, match.index) + match[0];
  81. const {metadata} = await API.worker.parseUsercssMeta(code);
  82. style.usercssData = metadata;
  83. // https://github.com/openstyles/stylus/issues/560#issuecomment-440561196
  84. for (const [key, globalKey] of usercssMan.GLOBAL_META) {
  85. const val = metadata[key];
  86. if (val !== undefined) {
  87. style[globalKey || key] = val;
  88. }
  89. }
  90. return style;
  91. } catch (err) {
  92. if (err.code) {
  93. const args = err.code === 'missingMandatory' || err.code === 'missingChar'
  94. ? err.args.map(e => e.length === 1 ? JSON.stringify(e) : e).join(', ')
  95. : err.args;
  96. const msg = chrome.i18n.getMessage(`meta_${(err.code)}`, args);
  97. if (msg) err.message = msg;
  98. }
  99. return Promise.reject(err);
  100. }
  101. },
  102. async configVars(id, vars) {
  103. const style = deepCopy(await API.styles.get(id));
  104. style.usercssData.vars = vars;
  105. await usercssMan.buildCode(style);
  106. return (await API.styles.install(style, 'config'))
  107. .usercssData.vars;
  108. },
  109. async editSave(style) {
  110. style = await usercssMan.parse(style);
  111. return {
  112. log: style.log, // extracting the non-enumerable prop, otherwise it won't survive messaging
  113. style: await API.styles.editSave(style),
  114. };
  115. },
  116. async find(styleOrData) {
  117. if (styleOrData.id) {
  118. return API.styles.get(styleOrData.id);
  119. }
  120. const {name, namespace} = styleOrData.usercssData || styleOrData;
  121. for (const dup of await API.styles.getAll()) {
  122. const data = dup.usercssData;
  123. if (data &&
  124. data.name === name &&
  125. data.namespace === namespace) {
  126. return dup;
  127. }
  128. }
  129. },
  130. async install(style) {
  131. return API.styles.install(await usercssMan.parse(style));
  132. },
  133. async parse(style) {
  134. style = await usercssMan.buildMeta(style);
  135. // preserve style.vars during update
  136. const dup = await usercssMan.find(style);
  137. if (dup) {
  138. style.id = dup.id;
  139. await usercssMan.assignVars(style, dup);
  140. }
  141. return usercssMan.buildCode(style);
  142. },
  143. };
  144. /** Replaces everything with spaces to keep the original length,
  145. * but preserves the line breaks to keep the original line/col relation */
  146. function blankOut(str, start = 0, end = str.length) {
  147. return str.slice(start, end).replace(/[^\r\n]/g, ' ');
  148. }