| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809 |
- /* global LZString */
- 'use strict';
- const RX_NAMESPACE = new RegExp([/[\s\r\n]*/,
- /(@namespace[\s\r\n]+(?:[^\s\r\n]+[\s\r\n]+)?url\(http:\/\/.*?\);)/,
- /[\s\r\n]*/].map(rx => rx.source).join(''), 'g');
- const RX_CSS_COMMENTS = /\/\*[\s\S]*?\*\//g;
- // eslint-disable-next-line no-var
- var SLOPPY_REGEXP_PREFIX = '\0';
- // CSS transition bug workaround: since we insert styles asynchronously,
- // the browsers, especially Firefox, may apply all transitions on page load
- const CSS_TRANSITION_SUPPRESSOR = '* { transition: none !important; }';
- const RX_CSS_TRANSITION_DETECTOR = /([\s\n;/{]|-webkit-|-moz-)transition[\s\n]*:[\s\n]*(?!none)/;
- // Note, only 'var'-declared variables are visible from another extension page
- // eslint-disable-next-line no-var
- var cachedStyles = {
- list: null, // array of all styles
- byId: new Map(), // all styles indexed by id
- filters: new Map(), // filterStyles() parameters mapped to the returned results, 10k max
- regexps: new Map(), // compiled style regexps
- urlDomains: new Map(), // getDomain() results for 100 last checked urls
- needTransitionPatch: new Map(), // FF bug workaround
- mutex: {
- inProgress: true, // while getStyles() is reading IndexedDB all subsequent calls
- // (initially 'true' to prevent rogue getStyles before dbExec.initialized)
- onDone: [], // to getStyles() are queued and resolved when the first one finishes
- },
- };
- // eslint-disable-next-line no-var
- var chromeLocal = {
- get(options) {
- return new Promise(resolve => {
- chrome.storage.local.get(options, data => resolve(data));
- });
- },
- set(data) {
- return new Promise(resolve => {
- chrome.storage.local.set(data, () => resolve(data));
- });
- },
- remove(keyOrKeys) {
- return new Promise(resolve => {
- chrome.storage.local.remove(keyOrKeys, resolve);
- });
- },
- getValue(key) {
- return chromeLocal.get(key).then(data => data[key]);
- },
- setValue(key, value) {
- return chromeLocal.set({[key]: value});
- },
- };
- // eslint-disable-next-line no-var
- var chromeSync = {
- get(options) {
- return new Promise(resolve => {
- chrome.storage.sync.get(options, resolve);
- });
- },
- set(data) {
- return new Promise(resolve => {
- chrome.storage.sync.set(data, () => resolve(data));
- });
- },
- getLZValue(key) {
- return chromeSync.getLZValues([key]).then(data => data[key]);
- },
- getLZValues(keys) {
- return chromeSync.get(keys).then((data = {}) => {
- for (const key of keys) {
- const value = data[key];
- data[key] = value && tryJSONparse(LZString.decompressFromUTF16(value));
- }
- return data;
- });
- },
- setLZValue(key, value) {
- return chromeSync.set({[key]: LZString.compressToUTF16(JSON.stringify(value))});
- }
- };
- // eslint-disable-next-line no-var
- var dbExec = dbExecIndexedDB;
- dbExec.initialized = false;
- // we use chrome.storage.local fallback if IndexedDB doesn't save data,
- // which, once detected on the first run, is remembered in chrome.storage.local
- // for reliablility and in localStorage for fast synchronous access
- // (FF may block localStorage depending on its privacy options)
- do {
- const done = () => {
- cachedStyles.mutex.inProgress = false;
- getStyles().then(() => {
- dbExec.initialized = true;
- window.dispatchEvent(new Event('storageReady'));
- });
- };
- const fallback = () => {
- dbExec = dbExecChromeStorage;
- chromeLocal.set({dbInChromeStorage: true});
- localStorage.dbInChromeStorage = 'true';
- ignoreChromeError();
- done();
- };
- const fallbackSet = localStorage.dbInChromeStorage;
- if (fallbackSet === 'true' || !tryCatch(() => indexedDB)) {
- fallback();
- break;
- } else if (fallbackSet === 'false') {
- done();
- break;
- }
- chromeLocal.get('dbInChromeStorage')
- .then(data =>
- data && data.dbInChromeStorage && Promise.reject())
- .then(() =>
- tryCatch(dbExecIndexedDB, 'getAllKeys', IDBKeyRange.lowerBound(1), 1) ||
- Promise.reject())
- .then(({target}) => (
- (target.result || [])[0] ?
- Promise.reject('ok') :
- dbExecIndexedDB('put', {id: -1})))
- .then(() =>
- dbExecIndexedDB('get', -1))
- .then(({target}) => (
- (target.result || {}).id === -1 ?
- dbExecIndexedDB('delete', -1) :
- Promise.reject()))
- .then(() =>
- Promise.reject('ok'))
- .catch(result => {
- if (result === 'ok') {
- chromeLocal.set({dbInChromeStorage: false});
- localStorage.dbInChromeStorage = 'false';
- done();
- } else {
- fallback();
- }
- });
- } while (0);
- function dbExecIndexedDB(method, ...args) {
- return new Promise((resolve, reject) => {
- Object.assign(indexedDB.open('stylish', 2), {
- onsuccess(event) {
- const database = event.target.result;
- if (!method) {
- resolve(database);
- } else {
- const transaction = database.transaction(['styles'], 'readwrite');
- const store = transaction.objectStore('styles');
- Object.assign(store[method](...args), {
- onsuccess: event => resolve(event, store, transaction, database),
- onerror: reject,
- });
- }
- },
- onerror(event) {
- console.warn(event.target.error || event.target.errorCode);
- reject(event);
- },
- onupgradeneeded(event) {
- if (event.oldVersion === 0) {
- event.target.result.createObjectStore('styles', {
- keyPath: 'id',
- autoIncrement: true,
- });
- }
- },
- });
- });
- }
- function dbExecChromeStorage(method, data) {
- const STYLE_KEY_PREFIX = 'style-';
- switch (method) {
- case 'get':
- return chromeLocal.getValue(STYLE_KEY_PREFIX + data)
- .then(result => ({target: {result}}));
- case 'put':
- if (!data.id) {
- return getStyles().then(() => {
- data.id = 1;
- for (const style of cachedStyles.list) {
- data.id = Math.max(data.id, style.id + 1);
- }
- return dbExecChromeStorage('put', data);
- });
- }
- return chromeLocal.setValue(STYLE_KEY_PREFIX + data.id, data)
- .then(() => (chrome.runtime.lastError ? Promise.reject() : data.id));
- case 'delete':
- return chromeLocal.remove(STYLE_KEY_PREFIX + data);
- case 'getAll':
- return chromeLocal.get(null).then(storage => {
- const styles = [];
- const leftovers = [];
- for (const key in storage) {
- if (key.startsWith(STYLE_KEY_PREFIX) &&
- Number(key.substr(STYLE_KEY_PREFIX.length))) {
- styles.push(storage[key]);
- } else if (key.startsWith('tempUsercssCode')) {
- leftovers.push(key);
- }
- }
- if (leftovers.length) {
- chromeLocal.remove(leftovers);
- }
- return {target: {result: styles}};
- });
- }
- return Promise.reject();
- }
- function getStyles(options) {
- if (cachedStyles.list) {
- return Promise.resolve(filterStyles(options));
- }
- if (cachedStyles.mutex.inProgress) {
- return new Promise(resolve => {
- cachedStyles.mutex.onDone.push({options, resolve});
- });
- }
- cachedStyles.mutex.inProgress = true;
- return dbExec('getAll').then(event => {
- cachedStyles.list = event.target.result || [];
- cachedStyles.byId.clear();
- for (const style of cachedStyles.list) {
- cachedStyles.byId.set(style.id, style);
- }
- cachedStyles.mutex.inProgress = false;
- for (const {options, resolve} of cachedStyles.mutex.onDone) {
- resolve(filterStyles(options));
- }
- cachedStyles.mutex.onDone = [];
- return filterStyles(options);
- });
- }
- function filterStyles({
- enabled = null,
- url = null,
- id = null,
- matchUrl = null,
- asHash = null,
- strictRegexp = true, // used by the popup to detect bad regexps
- } = {}) {
- enabled = enabled === null || typeof enabled === 'boolean' ? enabled :
- typeof enabled === 'string' ? enabled === 'true' : null;
- id = id === null ? null : Number(id);
- if (
- enabled === null &&
- url === null &&
- id === null &&
- matchUrl === null &&
- asHash !== true
- ) {
- return cachedStyles.list;
- }
- if (matchUrl && !URLS.supported(matchUrl)) {
- return asHash ? {} : [];
- }
- const blankHash = asHash && {
- disableAll: prefs.get('disableAll'),
- exposeIframes: prefs.get('exposeIframes'),
- };
- // add \t after url to prevent collisions (not sure it can actually happen though)
- const cacheKey = ' ' + enabled + url + '\t' + id + matchUrl + '\t' + asHash + strictRegexp;
- const cached = cachedStyles.filters.get(cacheKey);
- if (cached) {
- cached.hits++;
- cached.lastHit = Date.now();
- return asHash
- ? Object.assign(blankHash, cached.styles)
- : cached.styles;
- }
- return filterStylesInternal({
- enabled,
- url,
- id,
- matchUrl,
- asHash,
- strictRegexp,
- blankHash,
- cacheKey,
- });
- }
- function filterStylesInternal({
- // js engines don't like big functions (V8 often deoptimized the original filterStyles)
- // it also makes sense to extract the less frequently executed code
- enabled,
- url,
- id,
- matchUrl,
- asHash,
- strictRegexp,
- blankHash,
- cacheKey,
- }) {
- if (matchUrl && !cachedStyles.urlDomains.has(matchUrl)) {
- cachedStyles.urlDomains.set(matchUrl, getDomains(matchUrl));
- for (let i = cachedStyles.urlDomains.size - 100; i > 0; i--) {
- const firstKey = cachedStyles.urlDomains.keys().next().value;
- cachedStyles.urlDomains.delete(firstKey);
- }
- }
- const styles = id === null
- ? cachedStyles.list
- : [cachedStyles.byId.get(id)];
- if (!styles[0]) {
- // may happen when users [accidentally] reopen an old URL
- // of edit.html with a non-existent style id parameter
- return asHash ? blankHash : [];
- }
- const filtered = asHash ? {} : [];
- const needSections = asHash || matchUrl !== null;
- const matchUrlBase = matchUrl && matchUrl.includes('#') && matchUrl.split('#', 1)[0];
- let style;
- for (let i = 0; (style = styles[i]); i++) {
- if ((enabled === null || style.enabled === enabled)
- && (url === null || style.url === url)
- && (id === null || style.id === id)) {
- const sections = needSections &&
- getApplicableSections({
- style,
- matchUrl,
- strictRegexp,
- stopOnFirst: !asHash,
- skipUrlCheck: true,
- matchUrlBase,
- });
- if (asHash) {
- if (sections.length) {
- filtered[style.id] = sections;
- }
- } else if (matchUrl === null || sections.length) {
- filtered.push(style);
- }
- }
- }
- cachedStyles.filters.set(cacheKey, {
- styles: filtered,
- lastHit: Date.now(),
- hits: 1,
- });
- if (cachedStyles.filters.size > 10000) {
- cleanupCachedFilters();
- }
- // a shallow copy is needed because the cache doesn't store options like disableAll
- return asHash
- ? Object.assign(blankHash, filtered)
- : filtered;
- }
- function saveStyle(style) {
- const id = Number(style.id) || null;
- const reason = style.reason;
- const notify = style.notify !== false;
- delete style.method;
- delete style.reason;
- delete style.notify;
- if (!style.name) {
- delete style.name;
- }
- let existed;
- let codeIsUpdated;
- return maybeCalcDigest()
- .then(maybeImportFix)
- .then(decide);
- function maybeCalcDigest() {
- if (reason === 'update' || reason === 'update-digest') {
- return calcStyleDigest(style).then(digest => {
- style.originalDigest = digest;
- });
- }
- return Promise.resolve();
- }
- function maybeImportFix() {
- if (reason === 'import') {
- style.originalDigest = style.originalDigest || style.styleDigest; // TODO: remove in the future
- delete style.styleDigest; // TODO: remove in the future
- if (typeof style.originalDigest !== 'string' || style.originalDigest.length !== 40) {
- delete style.originalDigest;
- }
- }
- }
- function decide() {
- if (id !== null) {
- // Update or create
- style.id = id;
- return dbExec('get', id).then((event, store) => {
- const oldStyle = event.target.result;
- existed = Boolean(oldStyle);
- if (reason === 'update-digest' && oldStyle.originalDigest === style.originalDigest) {
- return style;
- }
- codeIsUpdated = !existed || 'sections' in style && !styleSectionsEqual(style, oldStyle);
- style = Object.assign({}, oldStyle, style);
- return write(style, store);
- });
- } else {
- // Create
- delete style.id;
- style = Object.assign({
- // Set optional things if they're undefined
- enabled: true,
- updateUrl: null,
- md5Url: null,
- url: null,
- originalMd5: null,
- }, style);
- return write(style);
- }
- }
- function write(style, store) {
- style.sections = normalizeStyleSections(style);
- if (store) {
- return new Promise(resolve => {
- store.put(style).onsuccess = event => resolve(done(event));
- });
- } else {
- return dbExec('put', style).then(done);
- }
- }
- function done(event) {
- if (reason === 'update-digest') {
- return style;
- }
- style.id = style.id || event.target.result;
- invalidateCache(existed ? {updated: style} : {added: style});
- if (notify) {
- notifyAllTabs({
- method: existed ? 'styleUpdated' : 'styleAdded',
- style, codeIsUpdated, reason,
- });
- }
- return style;
- }
- }
- function deleteStyle({id, notify = true}) {
- id = Number(id);
- return dbExec('delete', id).then(() => {
- invalidateCache({deletedId: id});
- if (notify) {
- notifyAllTabs({method: 'styleDeleted', id});
- }
- return id;
- });
- }
- function getApplicableSections({
- style,
- matchUrl,
- strictRegexp = true,
- // filterStylesInternal() sets the following to avoid recalc on each style:
- stopOnFirst,
- skipUrlCheck,
- matchUrlBase = matchUrl.includes('#') && matchUrl.split('#', 1)[0],
- // as per spec the fragment portion is ignored in @-moz-document:
- // https://www.w3.org/TR/2012/WD-css3-conditional-20120911/#url-of-doc
- // but the spec is outdated and doesn't account for SPA sites
- // so we only respect it in case of url("http://exact.url/without/hash")
- }) {
- if (!skipUrlCheck && !URLS.supported(matchUrl)) {
- return [];
- }
- const sections = [];
- for (const section of style.sections) {
- const {urls, domains, urlPrefixes, regexps, code} = section;
- const isGlobal = !urls.length && !urlPrefixes.length && !domains.length && !regexps.length;
- const isMatching = !isGlobal && (
- urls.length
- && (urls.includes(matchUrl) || matchUrlBase && urls.includes(matchUrlBase))
- || urlPrefixes.length
- && arraySomeIsPrefix(urlPrefixes, matchUrl)
- || domains.length
- && arraySomeIn(cachedStyles.urlDomains.get(matchUrl) || getDomains(matchUrl), domains)
- || regexps.length
- && arraySomeMatches(regexps, matchUrl, strictRegexp));
- if (isGlobal && !styleCodeEmpty(code) || isMatching) {
- sections.push(section);
- if (stopOnFirst) {
- break;
- }
- }
- }
- return sections;
- function arraySomeIsPrefix(array, string) {
- for (const prefix of array) {
- if (string.startsWith(prefix)) {
- return true;
- }
- }
- return false;
- }
- function arraySomeIn(array, haystack) {
- for (const el of array) {
- if (haystack.indexOf(el) >= 0) {
- return true;
- }
- }
- return false;
- }
- function arraySomeMatches(array, matchUrl, strictRegexp) {
- for (const regexp of array) {
- for (let pass = 1; pass <= (strictRegexp ? 1 : 2); pass++) {
- const cacheKey = pass === 1 ? regexp : SLOPPY_REGEXP_PREFIX + regexp;
- let rx = cachedStyles.regexps.get(cacheKey);
- if (rx === false) {
- // invalid regexp
- break;
- }
- if (!rx) {
- const anchored = pass === 1 ? '^(?:' + regexp + ')$' : '^' + regexp + '$';
- rx = tryRegExp(anchored);
- cachedStyles.regexps.set(cacheKey, rx || false);
- if (!rx) {
- // invalid regexp
- break;
- }
- }
- if (rx.test(matchUrl)) {
- return true;
- }
- }
- }
- return false;
- }
- }
- function styleCodeEmpty(code) {
- // Collect the global section if it's not empty, not comment-only, not namespace-only.
- const cmtOpen = code && code.indexOf('/*');
- if (cmtOpen >= 0) {
- const cmtCloseLast = code.lastIndexOf('*/');
- if (cmtCloseLast < 0) {
- code = code.substr(0, cmtOpen);
- } else {
- code = code.substr(0, cmtOpen) +
- code.substring(cmtOpen, cmtCloseLast + 2).replace(RX_CSS_COMMENTS, '') +
- code.substr(cmtCloseLast + 2);
- }
- }
- return !code
- || !code.trim()
- || code.includes('@namespace') && !code.replace(RX_NAMESPACE, '').trim();
- }
- function styleSectionsEqual({sections: a}, {sections: b}) {
- if (!a || !b) {
- return undefined;
- }
- if (a.length !== b.length) {
- return false;
- }
- // order of sections should be identical to account for the case of multiple
- // sections matching the same URL because the order of rules is part of cascading
- return a.every((sectionA, index) => propertiesEqual(sectionA, b[index]));
- function propertiesEqual(secA, secB) {
- for (const name of ['urlPrefixes', 'urls', 'domains', 'regexps']) {
- if (!equalOrEmpty(secA[name], secB[name], 'every', arrayMirrors)) {
- return false;
- }
- }
- return equalOrEmpty(secA.code, secB.code, 'substr', (a, b) => a === b);
- }
- function equalOrEmpty(a, b, telltale, comparator) {
- const typeA = a && typeof a[telltale] === 'function';
- const typeB = b && typeof b[telltale] === 'function';
- return (
- (a === null || a === undefined || (typeA && !a.length)) &&
- (b === null || b === undefined || (typeB && !b.length))
- ) || typeA && typeB && a.length === b.length && comparator(a, b);
- }
- function arrayMirrors(array1, array2) {
- return (
- array1.every(el => array2.includes(el)) &&
- array2.every(el => array1.includes(el))
- );
- }
- }
- function invalidateCache({added, updated, deletedId} = {}) {
- if (!cachedStyles.list) {
- return;
- }
- const id = added ? added.id : updated ? updated.id : deletedId;
- const cached = cachedStyles.byId.get(id);
- if (updated) {
- if (cached) {
- Object.assign(cached, updated);
- cachedStyles.filters.clear();
- cachedStyles.needTransitionPatch.delete(id);
- return;
- } else {
- added = updated;
- }
- }
- if (added) {
- if (!cached) {
- cachedStyles.list.push(added);
- cachedStyles.byId.set(added.id, added);
- cachedStyles.filters.clear();
- cachedStyles.needTransitionPatch.delete(id);
- }
- return;
- }
- if (deletedId !== undefined) {
- if (cached) {
- const cachedIndex = cachedStyles.list.indexOf(cached);
- cachedStyles.list.splice(cachedIndex, 1);
- cachedStyles.byId.delete(deletedId);
- cachedStyles.filters.clear();
- cachedStyles.needTransitionPatch.delete(id);
- return;
- }
- }
- cachedStyles.list = null;
- cachedStyles.filters.clear();
- cachedStyles.needTransitionPatch.clear(id);
- }
- function cleanupCachedFilters({force = false} = {}) {
- if (!force) {
- debounce(cleanupCachedFilters, 1000, {force: true});
- return;
- }
- const size = cachedStyles.filters.size;
- const oldestHit = cachedStyles.filters.values().next().value.lastHit;
- const now = Date.now();
- const timeSpan = now - oldestHit;
- const recencyWeight = 5 / size;
- const hitWeight = 1 / 4; // we make ~4 hits per URL
- const lastHitWeight = 10;
- // delete the oldest 10%
- [...cachedStyles.filters.entries()]
- .map(([id, v], index) => ({
- id,
- weight:
- index * recencyWeight +
- v.hits * hitWeight +
- (v.lastHit - oldestHit) / timeSpan * lastHitWeight,
- }))
- .sort((a, b) => a.weight - b.weight)
- .slice(0, size / 10 + 1)
- .forEach(({id}) => cachedStyles.filters.delete(id));
- }
- function getDomains(url) {
- let d = /.*?:\/*([^/:]+)|$/.exec(url)[1];
- if (!d || url.startsWith('file:')) {
- return [];
- }
- const domains = [d];
- while (d.indexOf('.') !== -1) {
- d = d.substring(d.indexOf('.') + 1);
- domains.push(d);
- }
- return domains;
- }
- function normalizeStyleSections({sections}) {
- // retain known properties in an arbitrarily predefined order
- return (sections || []).map(section => ({
- code: section.code || '',
- urls: section.urls || [],
- urlPrefixes: section.urlPrefixes || [],
- domains: section.domains || [],
- regexps: section.regexps || [],
- }));
- }
- function calcStyleDigest(style) {
- const jsonString = style.usercssData ?
- style.sourceCode : JSON.stringify(normalizeStyleSections(style));
- const text = new TextEncoder('utf-8').encode(jsonString);
- return crypto.subtle.digest('SHA-1', text).then(hex);
- function hex(buffer) {
- const parts = [];
- const PAD8 = '00000000';
- const view = new DataView(buffer);
- for (let i = 0; i < view.byteLength; i += 4) {
- parts.push((PAD8 + view.getUint32(i).toString(16)).slice(-8));
- }
- return parts.join('');
- }
- }
- function handleCssTransitionBug({tabId, frameId, url, styles}) {
- for (let id in styles) {
- id |= 0;
- if (!id) {
- continue;
- }
- let need = cachedStyles.needTransitionPatch.get(id);
- if (need === false) {
- continue;
- }
- if (need !== true) {
- need = styles[id].some(sectionContainsTransitions);
- cachedStyles.needTransitionPatch.set(id, need);
- if (!need) {
- continue;
- }
- }
- if (FIREFOX && !url.startsWith(URLS.ownOrigin)) {
- patchFirefox();
- } else {
- styles.needTransitionPatch = true;
- }
- break;
- }
- function patchFirefox() {
- const options = {
- frameId,
- code: CSS_TRANSITION_SUPPRESSOR,
- matchAboutBlank: true,
- };
- if (FIREFOX >= 53) {
- options.cssOrigin = 'user';
- }
- browser.tabs.insertCSS(tabId, Object.assign(options, {
- runAt: 'document_start',
- })).then(() => setTimeout(() => {
- browser.tabs.removeCSS(tabId, options).catch(ignoreChromeError);
- })).catch(ignoreChromeError);
- }
- function sectionContainsTransitions(section) {
- let code = section.code;
- const firstTransition = code.indexOf('transition');
- if (firstTransition < 0) {
- return false;
- }
- const firstCmt = code.indexOf('/*');
- // check the part before the first comment
- if (firstCmt < 0 || firstTransition < firstCmt) {
- if (quickCheckAround(code, firstTransition)) {
- return true;
- } else if (firstCmt < 0) {
- return false;
- }
- }
- // check the rest
- const lastCmt = code.lastIndexOf('*/');
- if (lastCmt < firstCmt) {
- // the comment is unclosed and we already checked the preceding part
- return false;
- }
- let mid = code.slice(firstCmt, lastCmt + 2);
- mid = mid.indexOf('*/') === mid.length - 2 ? '' : mid.replace(RX_CSS_COMMENTS, '');
- code = mid + code.slice(lastCmt + 2);
- return quickCheckAround(code) || RX_CSS_TRANSITION_DETECTOR.test(code);
- }
- function quickCheckAround(code, pos = code.indexOf('transition')) {
- return RX_CSS_TRANSITION_DETECTOR.test(code.substr(Math.max(0, pos - 10), 50));
- }
- }
|