util.js 8.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320
  1. // SAFETY WARNING! Exports used by `injected` must make ::safe() calls and use __proto__:null
  2. import { browser } from '#/common/consts';
  3. export function i18n(name, args) {
  4. return browser.i18n.getMessage(name, args) || name;
  5. }
  6. export function toString(param) {
  7. if (param == null) return '';
  8. return `${param}`;
  9. }
  10. export function memoize(func, resolver = toString) {
  11. const cacheMap = {};
  12. function memoized(...args) {
  13. // Used in safe context
  14. // eslint-disable-next-line no-restricted-syntax
  15. const key = resolver(...args);
  16. let cache = cacheMap[key];
  17. if (!cache) {
  18. cache = {
  19. value: func.apply(this, args),
  20. };
  21. cacheMap[key] = cache;
  22. }
  23. return cache.value;
  24. }
  25. return memoized;
  26. }
  27. export function debounce(func, time) {
  28. let startTime;
  29. let timer;
  30. let callback;
  31. time = Math.max(0, +time || 0);
  32. function checkTime() {
  33. timer = null;
  34. if (performance.now() >= startTime) callback();
  35. else checkTimer();
  36. }
  37. function checkTimer() {
  38. if (!timer) {
  39. const delta = startTime - performance.now();
  40. timer = setTimeout(checkTime, delta);
  41. }
  42. }
  43. function debouncedFunction(...args) {
  44. startTime = performance.now() + time;
  45. callback = () => {
  46. callback = null;
  47. func.apply(this, args);
  48. };
  49. checkTimer();
  50. }
  51. return debouncedFunction;
  52. }
  53. export function throttle(func, time) {
  54. let lastTime = 0;
  55. time = Math.max(0, +time || 0);
  56. function throttledFunction(...args) {
  57. const now = performance.now();
  58. if (lastTime + time < now) {
  59. lastTime = now;
  60. func.apply(this, args);
  61. }
  62. }
  63. return throttledFunction;
  64. }
  65. export function noop() {}
  66. export function getUniqId(prefix = 'VM') {
  67. const now = performance.now();
  68. return prefix
  69. + Math.floor((now - Math.floor(now)) * 1e12).toString(36)
  70. + Math.floor(Math.random() * 1e12).toString(36);
  71. }
  72. /**
  73. * @param {ArrayBuffer|Uint8Array|Array} buf
  74. * @param {number} [offset]
  75. * @param {number} [length]
  76. * @return {string} a binary string i.e. one byte per character
  77. */
  78. export function buffer2string(buf, offset = 0, length = 1e99) {
  79. // The max number of arguments varies between JS engines but it's >32k so we're safe
  80. const sliceSize = 8192;
  81. const slices = [];
  82. const arrayLen = buf.length; // present on Uint8Array/Array
  83. const end = Math.min(arrayLen || buf.byteLength, offset + length);
  84. const needsSlicing = arrayLen == null || offset || end > sliceSize;
  85. for (; offset < end; offset += sliceSize) {
  86. slices.push(String.fromCharCode.apply(null,
  87. needsSlicing
  88. ? new Uint8Array(buf, offset, Math.min(sliceSize, end - offset))
  89. : buf));
  90. }
  91. return slices.join('');
  92. }
  93. /**
  94. * Faster than buffer2string+btoa: 2x in Chrome, 10x in FF
  95. * @param {Blob} blob
  96. * @param {number} [offset]
  97. * @param {number} [length]
  98. * @return {Promise<string>} base64-encoded contents
  99. */
  100. export function blob2base64(blob, offset = 0, length = 1e99) {
  101. if (offset || length < blob.size) {
  102. blob = blob.slice(offset, offset + length);
  103. }
  104. return !blob.size ? '' : new Promise(resolve => {
  105. const reader = new FileReader();
  106. reader.readAsDataURL(blob);
  107. reader.onload = () => {
  108. const res = reader.result;
  109. resolve(res.slice(res.indexOf(',') + 1));
  110. };
  111. });
  112. }
  113. export function string2uint8array(str) {
  114. const len = str.length;
  115. const array = new Uint8Array(len);
  116. for (let i = 0; i < len; i += 1) {
  117. array[i] = str.charCodeAt(i);
  118. }
  119. return array;
  120. }
  121. const VERSION_RE = /^(.*?)-([-.0-9a-z]+)|$/i;
  122. const DIGITS_RE = /^\d+$/; // using regexp to avoid +'1e2' being parsed as 100
  123. /** @return -1 | 0 | 1 */
  124. export function compareVersion(ver1, ver2) {
  125. // Used in safe context
  126. // eslint-disable-next-line no-restricted-syntax
  127. const [, main1 = ver1 || '', pre1] = VERSION_RE.exec(ver1);
  128. // eslint-disable-next-line no-restricted-syntax
  129. const [, main2 = ver2 || '', pre2] = VERSION_RE.exec(ver2);
  130. const delta = compareVersionChunk(main1, main2)
  131. || !pre1 - !pre2 // 1.2.3-pre-release is less than 1.2.3
  132. || pre1 && compareVersionChunk(pre1, pre2, true); // if pre1 is present, pre2 is too
  133. return delta < 0 ? -1 : +!!delta;
  134. }
  135. function compareVersionChunk(ver1, ver2, isSemverMode) {
  136. const parts1 = ver1.split('.');
  137. const parts2 = ver2.split('.');
  138. const len1 = parts1.length;
  139. const len2 = parts2.length;
  140. const len = (isSemverMode ? Math.min : Math.max)(len1, len2);
  141. let delta;
  142. for (let i = 0; !delta && i < len; i += 1) {
  143. const a = parts1[i];
  144. const b = parts2[i];
  145. if (isSemverMode) {
  146. delta = DIGITS_RE.test(a) && DIGITS_RE.test(b)
  147. ? a - b
  148. : a > b || a < b && -1;
  149. } else {
  150. delta = (parseInt(a, 10) || 0) - (parseInt(b, 10) || 0);
  151. }
  152. }
  153. return delta || isSemverMode && (len1 - len2);
  154. }
  155. const units = [
  156. ['min', 60],
  157. ['h', 24],
  158. ['d', 1000, 365],
  159. ['y'],
  160. ];
  161. export function formatTime(duration) {
  162. duration /= 60 * 1000;
  163. const unitInfo = units.find((item) => {
  164. const max = item[1];
  165. if (!max || duration < max) return true;
  166. const step = item[2] || max;
  167. duration /= step;
  168. return false;
  169. });
  170. return `${duration | 0}${unitInfo[0]}`;
  171. }
  172. export function formatByteLength(len, noBytes) {
  173. if (!len) return '';
  174. if (len < 1024 && !noBytes) return `${len} B`;
  175. if ((len /= 1024) < 1024) return `${Math.round(len)} k`;
  176. return `${+(len / 1024).toFixed(1)} M`;
  177. }
  178. // Used by `injected`
  179. export function isEmpty(obj) {
  180. for (const key in obj) {
  181. if (obj::hasOwnProperty(key)) {
  182. return false;
  183. }
  184. }
  185. return true;
  186. }
  187. export function ensureArray(data) {
  188. return Array.isArray(data) ? data : [data];
  189. }
  190. const binaryTypes = [
  191. 'blob',
  192. 'arraybuffer',
  193. ];
  194. export async function requestLocalFile(url, options = {}) {
  195. // only GET method is allowed for local files
  196. // headers is meaningless
  197. return new Promise((resolve, reject) => {
  198. const result = {};
  199. const xhr = new XMLHttpRequest();
  200. const { responseType } = options;
  201. xhr.open('GET', url, true);
  202. if (binaryTypes.includes(responseType)) xhr.responseType = responseType;
  203. xhr.onload = () => {
  204. // status for `file:` protocol will always be `0`
  205. result.status = xhr.status || 200;
  206. result.data = binaryTypes.includes(responseType) ? xhr.response : xhr.responseText;
  207. if (responseType === 'json') {
  208. try {
  209. result.data = JSON.parse(result.data);
  210. } catch {
  211. // ignore invalid JSON
  212. }
  213. }
  214. if (result.status > 300) {
  215. reject(result);
  216. } else {
  217. resolve(result);
  218. }
  219. };
  220. xhr.onerror = () => {
  221. result.status = -1;
  222. reject(result);
  223. };
  224. xhr.send();
  225. });
  226. }
  227. /**
  228. * Excludes `text/html` to avoid LINK header that Chrome uses to prefetch js and css,
  229. * because GreasyFork's 404 error response causes CSP violations in console of our page.
  230. */
  231. const FORCED_ACCEPT = {
  232. 'greasyfork.org': 'application/javascript, text/plain, text/css',
  233. };
  234. export const isRemote = url => url
  235. && !(/^(file:\/\/|data:|https?:\/\/(localhost|127\.0\.0\.1|(192\.168|172\.16|10\.0)\.[0-9]+\.[0-9]+|\[(::1|(fe80|fc00)::[.:0-9a-f]+)\]|.+\.(test|example|invalid|localhost))(:[0-9]+|\/|$))/i.test(url));
  236. /** @typedef {{
  237. url: string,
  238. status: number,
  239. headers: Headers,
  240. data: string|ArrayBuffer|Blob|Object
  241. }} VMRequestResponse */
  242. /**
  243. * Make a request.
  244. * @param {string} url
  245. * @param {RequestInit} options
  246. * @return Promise<VMRequestResponse>
  247. */
  248. export async function request(url, options = {}) {
  249. // fetch does not support local file
  250. if (url.startsWith('file://')) return requestLocalFile(url, options);
  251. const { body, headers, responseType } = options;
  252. const isBodyObj = body && body::({}).toString() === '[object Object]';
  253. const hostname = url.split('/', 3)[2];
  254. const accept = FORCED_ACCEPT[hostname];
  255. // Not using ...spread because Babel mistakenly adds its polyfill to injected-web
  256. const init = Object.assign({
  257. cache: isRemote(url) ? undefined : 'no-cache',
  258. }, options, {
  259. body: isBodyObj ? JSON.stringify(body) : body,
  260. headers: isBodyObj || accept
  261. ? Object.assign({},
  262. headers,
  263. isBodyObj && { 'Content-Type': 'application/json' },
  264. accept && { accept })
  265. : headers,
  266. });
  267. const result = { url, status: -1 };
  268. try {
  269. const resp = await fetch(url, init);
  270. const loadMethod = {
  271. arraybuffer: 'arrayBuffer',
  272. blob: 'blob',
  273. json: 'json',
  274. }[responseType] || 'text';
  275. // status for `file:` protocol will always be `0`
  276. result.status = resp.status || 200;
  277. result.headers = resp.headers;
  278. result.data = await resp[loadMethod]();
  279. } catch { /* NOP */ }
  280. if (result.status < 0 || result.status > 300) throw result;
  281. return result;
  282. }
  283. // Used by `injected`
  284. const SIMPLE_VALUE_TYPE = {
  285. __proto__: null,
  286. string: 's',
  287. number: 'n',
  288. boolean: 'b',
  289. };
  290. // Used by `injected`
  291. export function dumpScriptValue(value, jsonDump = JSON.stringify) {
  292. if (value !== undefined) {
  293. const simple = SIMPLE_VALUE_TYPE[typeof value];
  294. return `${simple || 'o'}${simple ? value : jsonDump(value)}`;
  295. }
  296. }