diff --git a/src/scripts/darkreader.js b/src/scripts/darkreader.js index 080a68869..0daefcf2f 100644 --- a/src/scripts/darkreader.js +++ b/src/scripts/darkreader.js @@ -1,5 +1,5 @@ /** - * Dark Reader v4.9.86 + * Dark Reader v4.9.100 * https://darkreader.org/ */ @@ -7,12 +7,12 @@ typeof exports === "object" && typeof module !== "undefined" ? factory(exports) : typeof define === "function" && define.amd - ? define(["exports"], factory) - : ((global = + ? define(["exports"], factory) + : ((global = typeof globalThis !== "undefined" ? globalThis : global || self), - factory((global.DarkReader = {}))); + factory((global.DarkReader = {}))); })(this, function (exports) { "use strict"; @@ -97,17 +97,17 @@ const isNavigatorDefined = typeof navigator !== "undefined"; const userAgent = isNavigatorDefined ? navigator.userAgentData && - Array.isArray(navigator.userAgentData.brands) + Array.isArray(navigator.userAgentData.brands) ? navigator.userAgentData.brands - .map( - (brand) => `${brand.brand.toLowerCase()} ${brand.version}` - ) - .join(" ") + .map( + (brand) => `${brand.brand.toLowerCase()} ${brand.version}` + ) + .join(" ") : navigator.userAgent.toLowerCase() : "some useragent"; const platform = isNavigatorDefined ? navigator.userAgentData && - typeof navigator.userAgentData.platform === "string" + typeof navigator.userAgentData.platform === "string" ? navigator.userAgentData.platform.toLowerCase() : navigator.platform.toLowerCase() : "some platform"; @@ -170,9 +170,11 @@ })(); async function getOKResponse(url, mimeType, origin) { + const credentials = + origin && url.startsWith(`${origin}/`) ? undefined : "omit"; const response = await fetch(url, { cache: "force-cache", - credentials: "omit", + credentials, referrer: origin }); if ( @@ -253,9 +255,9 @@ const messageListeners = new Set(); async function sendMessage(...args) { if (args[0] && args[0].type === MessageTypeCStoBG.FETCH) { - const { id } = args[0]; + const {id} = args[0]; try { - const { url, responseType } = args[0].data; + const {url, responseType} = args[0].data; const response = await callFetchMethod(url); let text; if (responseType === "data-url") { @@ -346,8 +348,8 @@ fontFamily: isMacOS ? "Helvetica Neue" : isWindows - ? "Segoe UI" - : "Open Sans", + ? "Segoe UI" + : "Open Sans", textStroke: 0, engine: ThemeEngine.dynamicTheme, stylesheet: "", @@ -355,20 +357,35 @@ darkSchemeTextColor: DEFAULT_COLORS.darkScheme.text, lightSchemeBackgroundColor: DEFAULT_COLORS.lightScheme.background, lightSchemeTextColor: DEFAULT_COLORS.lightScheme.text, - scrollbarColor: isMacOS ? "" : "auto", + scrollbarColor: "", selectionColor: "auto", styleSystemControls: !isCSSColorSchemePropSupported, lightColorScheme: "Default", darkColorScheme: "Default", immediateModify: false }; + const filterModeSites = [ + "*.officeapps.live.com", + "*.sharepoint.com", + "docs.google.com", + "onedrive.live.com" + ]; ({ schemeVersion: 0, enabled: true, fetchNews: true, theme: DEFAULT_THEME, presets: [], - customThemes: [], + customThemes: filterModeSites.map((url) => { + const engine = isChromium + ? ThemeEngine.svgFilter + : ThemeEngine.cssFilter; + return { + url: [url], + theme: {...DEFAULT_THEME, engine}, + builtIn: true + }; + }), enabledByDefault: true, enabledFor: [], disabledFor: [], @@ -389,12 +406,218 @@ longitude: null }, previewNewDesign: false, + previewNewestDesign: false, enableForPDF: true, enableForProtectedPages: false, enableContextMenus: false, - detectDarkTheme: false + detectDarkTheme: true }); + function getMatches(regex, input, group = 0) { + const matches = []; + let m; + while ((m = regex.exec(input))) { + matches.push(m[group]); + } + return matches; + } + function getMatchesWithOffsets(regex, input, group = 0) { + const matches = []; + let m; + while ((m = regex.exec(input))) { + matches.push({text: m[group], offset: m.index}); + } + return matches; + } + function getHashCode(text) { + const len = text.length; + let hash = 0; + for (let i = 0; i < len; i++) { + const c = text.charCodeAt(i); + hash = ((hash << 5) - hash + c) & 4294967295; + } + return hash; + } + function escapeRegExpSpecialChars(input) { + return input.replaceAll(/[\^$.*+?\(\)\[\]{}|\-\\]/g, "\\$&"); + } + function getParenthesesRange(input, searchStartIndex = 0) { + return getOpenCloseRange(input, searchStartIndex, "(", ")", []); + } + function getOpenCloseRange( + input, + searchStartIndex, + openToken, + closeToken, + excludeRanges + ) { + let indexOf; + if (excludeRanges.length === 0) { + indexOf = (token, pos) => input.indexOf(token, pos); + } else { + indexOf = (token, pos) => + indexOfExcluding(input, token, pos, excludeRanges); + } + const {length} = input; + let depth = 0; + let firstOpenIndex = -1; + for (let i = searchStartIndex; i < length; i++) { + if (depth === 0) { + const openIndex = indexOf(openToken, i); + if (openIndex < 0) { + break; + } + firstOpenIndex = openIndex; + depth++; + i = openIndex; + } else { + const closeIndex = indexOf(closeToken, i); + if (closeIndex < 0) { + break; + } + const openIndex = indexOf(openToken, i); + if (openIndex < 0 || closeIndex <= openIndex) { + depth--; + if (depth === 0) { + return {start: firstOpenIndex, end: closeIndex + 1}; + } + i = closeIndex; + } else { + depth++; + i = openIndex; + } + } + } + return null; + } + function indexOfExcluding(input, search, position, excludeRanges) { + const i = input.indexOf(search, position); + const exclusion = excludeRanges.find((r) => i >= r.start && i < r.end); + if (exclusion) { + return indexOfExcluding( + input, + search, + exclusion.end, + excludeRanges + ); + } + return i; + } + function splitExcluding(input, separator, excludeRanges) { + const parts = []; + let commaIndex = -1; + let currIndex = 0; + while ( + (commaIndex = indexOfExcluding( + input, + separator, + currIndex, + excludeRanges + )) >= 0 + ) { + parts.push(input.substring(currIndex, commaIndex).trim()); + currIndex = commaIndex + 1; + } + parts.push(input.substring(currIndex).trim()); + return parts; + } + + let anchor; + const parsedURLCache = new Map(); + function fixBaseURL($url) { + if (!anchor) { + anchor = document.createElement("a"); + } + anchor.href = $url; + return anchor.href; + } + function parseURL($url, $base = null) { + const key = `${$url}${$base ? `;${$base}` : ""}`; + if (parsedURLCache.has(key)) { + return parsedURLCache.get(key); + } + if ($base) { + const parsedURL = new URL($url, fixBaseURL($base)); + parsedURLCache.set(key, parsedURL); + return parsedURL; + } + const parsedURL = new URL(fixBaseURL($url)); + parsedURLCache.set($url, parsedURL); + return parsedURL; + } + function getAbsoluteURL($base, $relative) { + if ($relative.match(/^data\\?\:/)) { + return $relative; + } + if (/^\/\//.test($relative)) { + return `${location.protocol}${$relative}`; + } + const b = parseURL($base); + const a = parseURL($relative, b.href); + return a.href; + } + function isRelativeHrefOnAbsolutePath(href) { + if (href.startsWith("data:")) { + return true; + } + const url = parseURL(href); + if (url.protocol !== location.protocol) { + return false; + } + if (url.hostname !== location.hostname) { + return false; + } + if (url.port !== location.port) { + return false; + } + return url.pathname === location.pathname; + } + + const excludedSelectors = [ + "pre", + "pre *", + "code", + '[aria-hidden="true"]', + '[class*="fa-"]', + ".fa", + ".fab", + ".fad", + ".fal", + ".far", + ".fas", + ".fass", + ".fasr", + ".fat", + ".icofont", + '[style*="font-"]', + '[class*="icon"]', + '[class*="Icon"]', + '[class*="symbol"]', + '[class*="Symbol"]', + ".glyphicon", + '[class*="material-symbol"]', + '[class*="material-icon"]', + "mu", + '[class*="mu-"]', + ".typcn", + '[class*="vjs-"]' + ]; + function createTextStyle(config) { + const lines = []; + lines.push(`*:not(${excludedSelectors.join(", ")}) {`); + if (config.useFont && config.fontFamily) { + lines.push(` font-family: ${config.fontFamily} !important;`); + } + if (config.textStroke > 0) { + lines.push( + ` -webkit-text-stroke: ${config.textStroke}px !important;` + ); + lines.push(` text-stroke: ${config.textStroke}px !important;`); + } + lines.push("}"); + return lines.join("\n"); + } + function isArrayLike(items) { return items.length != null; } @@ -420,1766 +643,1573 @@ return results; } - function logInfo(...args) { } - function logWarn(...args) { } - - function throttle(callback) { - let pending = false; - let frameId = null; - let lastArgs; - const throttled = (...args) => { - lastArgs = args; - if (frameId) { - pending = true; - } else { - callback(...lastArgs); - frameId = requestAnimationFrame(() => { - frameId = null; - if (pending) { - callback(...lastArgs); - pending = false; - } - }); - } - }; - const cancel = () => { - cancelAnimationFrame(frameId); - pending = false; - frameId = null; - }; - return Object.assign(throttled, { cancel }); + function scale(x, inLow, inHigh, outLow, outHigh) { + return ((x - inLow) * (outHigh - outLow)) / (inHigh - inLow) + outLow; } - function createAsyncTasksQueue() { - const tasks = []; - let frameId = null; - function runTasks() { - let task; - while ((task = tasks.shift())) { - task(); + function clamp(x, min, max) { + return Math.min(max, Math.max(min, x)); + } + function multiplyMatrices(m1, m2) { + const result = []; + for (let i = 0, len = m1.length; i < len; i++) { + result[i] = []; + for (let j = 0, len2 = m2[0].length; j < len2; j++) { + let sum = 0; + for (let k = 0, len3 = m1[0].length; k < len3; k++) { + sum += m1[i][k] * m2[k][j]; + } + result[i][j] = sum; } - frameId = null; } - function add(task) { - tasks.push(task); - if (!frameId) { - frameId = requestAnimationFrame(runTasks); - } + return result; + } + + function createFilterMatrix(config) { + let m = Matrix.identity(); + if (config.sepia !== 0) { + m = multiplyMatrices(m, Matrix.sepia(config.sepia / 100)); } - function cancel() { - tasks.splice(0); - cancelAnimationFrame(frameId); - frameId = null; + if (config.grayscale !== 0) { + m = multiplyMatrices(m, Matrix.grayscale(config.grayscale / 100)); } - return { add, cancel }; - } - const delayTokens = new Set(); - function requestAnimationFrameOnce(token, callback) { - if (delayTokens.has(token)) { - return; + if (config.contrast !== 100) { + m = multiplyMatrices(m, Matrix.contrast(config.contrast / 100)); } - delayTokens.add(token); - requestAnimationFrame(() => { - delayTokens.delete(token); - callback(); - }); + if (config.brightness !== 100) { + m = multiplyMatrices(m, Matrix.brightness(config.brightness / 100)); + } + if (config.mode === 1) { + m = multiplyMatrices(m, Matrix.invertNHue()); + } + return m; + } + function applyColorMatrix([r, g, b], matrix) { + const rgb = [[r / 255], [g / 255], [b / 255], [1], [1]]; + const result = multiplyMatrices(matrix, rgb); + return [0, 1, 2].map((i) => + clamp(Math.round(result[i][0] * 255), 0, 255) + ); } + const Matrix = { + identity() { + return [ + [1, 0, 0, 0, 0], + [0, 1, 0, 0, 0], + [0, 0, 1, 0, 0], + [0, 0, 0, 1, 0], + [0, 0, 0, 0, 1] + ]; + }, + invertNHue() { + return [ + [0.333, -0.667, -0.667, 0, 1], + [-0.667, 0.333, -0.667, 0, 1], + [-0.667, -0.667, 0.333, 0, 1], + [0, 0, 0, 1, 0], + [0, 0, 0, 0, 1] + ]; + }, + brightness(v) { + return [ + [v, 0, 0, 0, 0], + [0, v, 0, 0, 0], + [0, 0, v, 0, 0], + [0, 0, 0, 1, 0], + [0, 0, 0, 0, 1] + ]; + }, + contrast(v) { + const t = (1 - v) / 2; + return [ + [v, 0, 0, 0, t], + [0, v, 0, 0, t], + [0, 0, v, 0, t], + [0, 0, 0, 1, 0], + [0, 0, 0, 0, 1] + ]; + }, + sepia(v) { + return [ + [ + 0.393 + 0.607 * (1 - v), + 0.769 - 0.769 * (1 - v), + 0.189 - 0.189 * (1 - v), + 0, + 0 + ], + [ + 0.349 - 0.349 * (1 - v), + 0.686 + 0.314 * (1 - v), + 0.168 - 0.168 * (1 - v), + 0, + 0 + ], + [ + 0.272 - 0.272 * (1 - v), + 0.534 - 0.534 * (1 - v), + 0.131 + 0.869 * (1 - v), + 0, + 0 + ], + [0, 0, 0, 1, 0], + [0, 0, 0, 0, 1] + ]; + }, + grayscale(v) { + return [ + [ + 0.2126 + 0.7874 * (1 - v), + 0.7152 - 0.7152 * (1 - v), + 0.0722 - 0.0722 * (1 - v), + 0, + 0 + ], + [ + 0.2126 - 0.2126 * (1 - v), + 0.7152 + 0.2848 * (1 - v), + 0.0722 - 0.0722 * (1 - v), + 0, + 0 + ], + [ + 0.2126 - 0.2126 * (1 - v), + 0.7152 - 0.7152 * (1 - v), + 0.0722 + 0.9278 * (1 - v), + 0, + 0 + ], + [0, 0, 0, 1, 0], + [0, 0, 0, 0, 1] + ]; + } + }; - function getDuration(time) { - let duration = 0; - if (time.seconds) { - duration += time.seconds * 1000; + var FilterMode; + (function (FilterMode) { + FilterMode[(FilterMode["light"] = 0)] = "light"; + FilterMode[(FilterMode["dark"] = 1)] = "dark"; + })(FilterMode || (FilterMode = {})); + function getCSSFilterValue(config) { + const filters = []; + if (config.mode === FilterMode.dark) { + filters.push("invert(100%) hue-rotate(180deg)"); } - if (time.minutes) { - duration += time.minutes * 60 * 1000; + if (config.brightness !== 100) { + filters.push(`brightness(${config.brightness}%)`); } - if (time.hours) { - duration += time.hours * 60 * 60 * 1000; + if (config.contrast !== 100) { + filters.push(`contrast(${config.contrast}%)`); } - if (time.days) { - duration += time.days * 24 * 60 * 60 * 1000; + if (config.grayscale !== 0) { + filters.push(`grayscale(${config.grayscale}%)`); } - return duration; - } - - function removeNode(node) { - node && node.parentNode && node.parentNode.removeChild(node); - } - function watchForNodePosition(node, mode, onRestore = Function.prototype) { - const MAX_ATTEMPTS_COUNT = 10; - const RETRY_TIMEOUT = getDuration({ seconds: 2 }); - const ATTEMPTS_INTERVAL = getDuration({ seconds: 10 }); - const prevSibling = node.previousSibling; - let parent = node.parentNode; - if (!parent) { - throw new Error( - "Unable to watch for node position: parent element not found" - ); + if (config.sepia !== 0) { + filters.push(`sepia(${config.sepia}%)`); } - if (mode === "prev-sibling" && !prevSibling) { - throw new Error( - "Unable to watch for node position: there is no previous sibling" - ); + if (filters.length === 0) { + return null; } - let attempts = 0; - let start = null; - let timeoutId = null; - const restore = throttle(() => { - if (timeoutId) { - return; - } - attempts++; - const now = Date.now(); - if (start == null) { - start = now; - } else if (attempts >= MAX_ATTEMPTS_COUNT) { - if (now - start < ATTEMPTS_INTERVAL) { - timeoutId = setTimeout(() => { - start = null; - attempts = 0; - timeoutId = null; - restore(); - }, RETRY_TIMEOUT); - return; - } - start = now; - attempts = 1; + return filters.join(" "); + } + + function evalMath(expression) { + const rpnStack = []; + const workingStack = []; + let lastToken; + for (let i = 0, len = expression.length; i < len; i++) { + const token = expression[i]; + if (!token || token === " ") { + continue; } - if (mode === "head") { - if (prevSibling && prevSibling.parentNode !== parent) { - stop(); - return; + if (operators.has(token)) { + const op = operators.get(token); + while (workingStack.length) { + const currentOp = operators.get(workingStack[0]); + if (!currentOp) { + break; + } + if (op.lessOrEqualThan(currentOp)) { + rpnStack.push(workingStack.shift()); + } else { + break; + } } + workingStack.unshift(token); + } else if (!lastToken || operators.has(lastToken)) { + rpnStack.push(token); + } else { + rpnStack[rpnStack.length - 1] += token; } - if (mode === "prev-sibling") { - if (prevSibling.parentNode == null) { - stop(); - return; - } - if (prevSibling.parentNode !== parent) { - updateParent(prevSibling.parentNode); - } + lastToken = token; + } + rpnStack.push(...workingStack); + const stack = []; + for (let i = 0, len = rpnStack.length; i < len; i++) { + const op = operators.get(rpnStack[i]); + if (op) { + const args = stack.splice(0, 2); + stack.push(op.exec(args[1], args[0])); + } else { + stack.unshift(parseFloat(rpnStack[i])); } - if (mode === "head" && !parent.isConnected) { - parent = document.head; - } - parent.insertBefore( - node, - prevSibling && prevSibling.isConnected - ? prevSibling.nextSibling - : parent.firstChild - ); - observer.takeRecords(); - onRestore && onRestore(); - }); - const observer = new MutationObserver(() => { - if ( - (mode === "head" && - (node.parentNode !== parent || - !node.parentNode.isConnected)) || - (mode === "prev-sibling" && - node.previousSibling !== prevSibling) - ) { - restore(); - } - }); - const run = () => { - observer.observe(parent, { childList: true }); - }; - const stop = () => { - clearTimeout(timeoutId); - observer.disconnect(); - restore.cancel(); - }; - const skip = () => { - observer.takeRecords(); - }; - const updateParent = (parentNode) => { - parent = parentNode; - stop(); - run(); - }; - run(); - return { run, stop, skip }; + } + return stack[0]; } - function iterateShadowHosts(root, iterator) { - if (root == null) { - return; + class Operator { + constructor(precedence, method) { + this.precendce = precedence; + this.execMethod = method; } - const walker = document.createTreeWalker( - root, - NodeFilter.SHOW_ELEMENT, - { - acceptNode(node) { - return node.shadowRoot == null - ? NodeFilter.FILTER_SKIP - : NodeFilter.FILTER_ACCEPT; - } - } - ); - for ( - let node = root.shadowRoot ? walker.currentNode : walker.nextNode(); - node != null; - node = walker.nextNode() - ) { - if (node.classList.contains("surfingkeys_hints_host")) { - continue; - } - iterator(node); - iterateShadowHosts(node.shadowRoot, iterator); + exec(left, right) { + return this.execMethod(left, right); + } + lessOrEqualThan(op) { + return this.precendce <= op.precendce; } } - let isDOMReady = () => { - return ( - document.readyState === "complete" || - document.readyState === "interactive" - ); - }; - function setIsDOMReady(newFunc) { - isDOMReady = newFunc; - } - const readyStateListeners = new Set(); - function addDOMReadyListener(listener) { - isDOMReady() ? listener() : readyStateListeners.add(listener); + const operators = new Map([ + ["+", new Operator(1, (left, right) => left + right)], + ["-", new Operator(1, (left, right) => left - right)], + ["*", new Operator(2, (left, right) => left * right)], + ["/", new Operator(2, (left, right) => left / right)] + ]); + + const isSystemDarkModeEnabled = () => + matchMedia("(prefers-color-scheme: dark)").matches; + + const hslaParseCache = new Map(); + const rgbaParseCache = new Map(); + function parseColorWithCache($color) { + $color = $color.trim(); + if (rgbaParseCache.has($color)) { + return rgbaParseCache.get($color); + } + if ($color.includes("calc(")) { + $color = lowerCalcExpression($color); + } + const color = parse($color); + if (color) { + rgbaParseCache.set($color, color); + return color; + } + return null; } - function removeDOMReadyListener(listener) { - readyStateListeners.delete(listener); + function parseToHSLWithCache(color) { + if (hslaParseCache.has(color)) { + return hslaParseCache.get(color); + } + const rgb = parseColorWithCache(color); + if (!rgb) { + return null; + } + const hsl = rgbToHSL(rgb); + hslaParseCache.set(color, hsl); + return hsl; } - function isReadyStateComplete() { - return document.readyState === "complete"; + function clearColorCache() { + hslaParseCache.clear(); + rgbaParseCache.clear(); } - const readyStateCompleteListeners = new Set(); - function addReadyStateCompleteListener(listener) { - isReadyStateComplete() - ? listener() - : readyStateCompleteListeners.add(listener); + function hslToRGB({h, s, l, a = 1}) { + if (s === 0) { + const [r, b, g] = [l, l, l].map((x) => Math.round(x * 255)); + return {r, g, b, a}; + } + const c = (1 - Math.abs(2 * l - 1)) * s; + const x = c * (1 - Math.abs(((h / 60) % 2) - 1)); + const m = l - c / 2; + const [r, g, b] = ( + h < 60 + ? [c, x, 0] + : h < 120 + ? [x, c, 0] + : h < 180 + ? [0, c, x] + : h < 240 + ? [0, x, c] + : h < 300 + ? [x, 0, c] + : [c, 0, x] + ).map((n) => Math.round((n + m) * 255)); + return {r, g, b, a}; } - function cleanReadyStateCompleteListeners() { - readyStateCompleteListeners.clear(); + function rgbToHSL({r: r255, g: g255, b: b255, a = 1}) { + const r = r255 / 255; + const g = g255 / 255; + const b = b255 / 255; + const max = Math.max(r, g, b); + const min = Math.min(r, g, b); + const c = max - min; + const l = (max + min) / 2; + if (c === 0) { + return {h: 0, s: 0, l, a}; + } + let h = + (max === r + ? ((g - b) / c) % 6 + : max === g + ? (b - r) / c + 2 + : (r - g) / c + 4) * 60; + if (h < 0) { + h += 360; + } + const s = c / (1 - Math.abs(2 * l - 1)); + return {h, s, l, a}; } - if (!isDOMReady()) { - const onReadyStateChange = () => { - if (isDOMReady()) { - readyStateListeners.forEach((listener) => listener()); - readyStateListeners.clear(); - if (isReadyStateComplete()) { - document.removeEventListener( - "readystatechange", - onReadyStateChange - ); - readyStateCompleteListeners.forEach((listener) => - listener() - ); - readyStateCompleteListeners.clear(); + function toFixed(n, digits = 0) { + const fixed = n.toFixed(digits); + if (digits === 0) { + return fixed; + } + const dot = fixed.indexOf("."); + if (dot >= 0) { + const zerosMatch = fixed.match(/0+$/); + if (zerosMatch) { + if (zerosMatch.index === dot + 1) { + return fixed.substring(0, dot); } + return fixed.substring(0, zerosMatch.index); } - }; - document.addEventListener("readystatechange", onReadyStateChange); + } + return fixed; } - const HUGE_MUTATIONS_COUNT = 1000; - function isHugeMutation(mutations) { - if (mutations.length > HUGE_MUTATIONS_COUNT) { - return true; + function rgbToString(rgb) { + const {r, g, b, a} = rgb; + if (a != null && a < 1) { + return `rgba(${toFixed(r)}, ${toFixed(g)}, ${toFixed(b)}, ${toFixed(a, 2)})`; } - let addedNodesCount = 0; - for (let i = 0; i < mutations.length; i++) { - addedNodesCount += mutations[i].addedNodes.length; - if (addedNodesCount > HUGE_MUTATIONS_COUNT) { - return true; - } + return `rgb(${toFixed(r)}, ${toFixed(g)}, ${toFixed(b)})`; + } + function rgbToHexString({r, g, b, a}) { + return `#${(a != null && a < 1 + ? [r, g, b, Math.round(a * 255)] + : [r, g, b] + ) + .map((x) => { + return `${x < 16 ? "0" : ""}${x.toString(16)}`; + }) + .join("")}`; + } + function hslToString(hsl) { + const {h, s, l, a} = hsl; + if (a != null && a < 1) { + return `hsla(${toFixed(h)}, ${toFixed(s * 100)}%, ${toFixed(l * 100)}%, ${toFixed(a, 2)})`; } - return false; + return `hsl(${toFixed(h)}, ${toFixed(s * 100)}%, ${toFixed(l * 100)}%)`; } - function getElementsTreeOperations(mutations) { - const additions = new Set(); - const deletions = new Set(); - const moves = new Set(); - mutations.forEach((m) => { - forEach(m.addedNodes, (n) => { - if (n instanceof Element && n.isConnected) { - additions.add(n); - } - }); - forEach(m.removedNodes, (n) => { - if (n instanceof Element) { - if (n.isConnected) { - moves.add(n); - additions.delete(n); - } else { - deletions.add(n); - } - } - }); - }); - const duplicateAdditions = []; - const duplicateDeletions = []; - additions.forEach((node) => { - if (additions.has(node.parentElement)) { - duplicateAdditions.push(node); + const rgbMatch = /^rgba?\([^\(\)]+\)$/; + const hslMatch = /^hsla?\([^\(\)]+\)$/; + const hexMatch = /^#[0-9a-f]+$/i; + function parse($color) { + const c = $color.trim().toLowerCase(); + if ($color.includes("(from ")) { + return domParseColor(c); + } + if (c.match(rgbMatch)) { + if (c.startsWith("rgb(#") || c.startsWith("rgba(#")) { + return domParseColor(c); } - }); - deletions.forEach((node) => { - if (deletions.has(node.parentElement)) { - duplicateDeletions.push(node); - } - }); - duplicateAdditions.forEach((node) => additions.delete(node)); - duplicateDeletions.forEach((node) => deletions.delete(node)); - return { additions, moves, deletions }; - } - const optimizedTreeObservers = new Map(); - const optimizedTreeCallbacks = new WeakMap(); - function createOptimizedTreeObserver(root, callbacks) { - let observer; - let observerCallbacks; - let domReadyListener; - if (optimizedTreeObservers.has(root)) { - observer = optimizedTreeObservers.get(root); - observerCallbacks = optimizedTreeCallbacks.get(observer); - } else { - let hadHugeMutationsBefore = false; - let subscribedForReadyState = false; - observer = new MutationObserver((mutations) => { - if (isHugeMutation(mutations)) { - if (!hadHugeMutationsBefore || isDOMReady()) { - observerCallbacks.forEach(({ onHugeMutations }) => - onHugeMutations(root) - ); - } else if (!subscribedForReadyState) { - domReadyListener = () => - observerCallbacks.forEach(({ onHugeMutations }) => - onHugeMutations(root) - ); - addDOMReadyListener(domReadyListener); - subscribedForReadyState = true; - } - hadHugeMutationsBefore = true; - } else { - const elementsOperations = - getElementsTreeOperations(mutations); - observerCallbacks.forEach(({ onMinorMutations }) => - onMinorMutations(root, elementsOperations) - ); - } - }); - observer.observe(root, { childList: true, subtree: true }); - optimizedTreeObservers.set(root, observer); - observerCallbacks = new Set(); - optimizedTreeCallbacks.set(observer, observerCallbacks); + return parseRGB(c); } - observerCallbacks.add(callbacks); - return { - disconnect() { - observerCallbacks.delete(callbacks); - if (domReadyListener) { - removeDOMReadyListener(domReadyListener); - } - if (observerCallbacks.size === 0) { - observer.disconnect(); - optimizedTreeCallbacks.delete(observer); - optimizedTreeObservers.delete(root); - } - } - }; - } - - function getMatches(regex, input, group = 0) { - const matches = []; - let m; - while ((m = regex.exec(input))) { - matches.push(m[group]); + if (c.match(hslMatch)) { + return parseHSL(c); } - return matches; - } - function getHashCode(text) { - const len = text.length; - let hash = 0; - for (let i = 0; i < len; i++) { - const c = text.charCodeAt(i); - hash = ((hash << 5) - hash + c) & 4294967295; + if (c.match(hexMatch)) { + return parseHex(c); } - return hash; - } - function escapeRegExpSpecialChars(input) { - return input.replaceAll(/[\^$.*+?\(\)\[\]{}|\-\\]/g, "\\$&"); - } - function getParenthesesRange(input, searchStartIndex = 0) { - return getOpenCloseRange(input, searchStartIndex, "(", ")", []); - } - function getOpenCloseRange( - input, - searchStartIndex, - openToken, - closeToken, - excludeRanges - ) { - let indexOf; - if (excludeRanges.length === 0) { - indexOf = (token, pos) => input.indexOf(token, pos); - } else { - indexOf = (token, pos) => - indexOfExcluding(input, token, pos, excludeRanges); + if (knownColors.has(c)) { + return getColorByName(c); } - const { length } = input; - let depth = 0; - let firstOpenIndex = -1; - for (let i = searchStartIndex; i < length; i++) { - if (depth === 0) { - const openIndex = indexOf(openToken, i); - if (openIndex < 0) { - break; - } - firstOpenIndex = openIndex; - depth++; - i = openIndex; - } else { - const closeIndex = indexOf(closeToken, i); - if (closeIndex < 0) { - break; - } - const openIndex = indexOf(openToken, i); - if (openIndex < 0 || closeIndex <= openIndex) { - depth--; - if (depth === 0) { - return { start: firstOpenIndex, end: closeIndex + 1 }; - } - i = closeIndex; - } else { - depth++; - i = openIndex; - } - } + if (systemColors.has(c)) { + return getSystemColor(c); } - return null; - } - function indexOfExcluding(input, search, position, excludeRanges) { - const i = input.indexOf(search, position); - const exclusion = excludeRanges.find((r) => i >= r.start && i < r.end); - if (exclusion) { - return indexOfExcluding( - input, - search, - exclusion.end, - excludeRanges - ); + if ($color === "transparent") { + return {r: 0, g: 0, b: 0, a: 0}; } - return i; - } - function splitExcluding(input, separator, excludeRanges) { - const parts = []; - let commaIndex = -1; - let currIndex = 0; - while ( - (commaIndex = indexOfExcluding( - input, - separator, - currIndex, - excludeRanges - )) >= 0 + if ( + (c.startsWith("color(") || c.startsWith("color-mix(")) && + c.endsWith(")") ) { - parts.push(input.substring(currIndex, commaIndex).trim()); - currIndex = commaIndex + 1; + return domParseColor(c); } - parts.push(input.substring(currIndex).trim()); - return parts; - } - - let anchor; - const parsedURLCache = new Map(); - function fixBaseURL($url) { - if (!anchor) { - anchor = document.createElement("a"); + if (c.startsWith("light-dark(") && c.endsWith(")")) { + const match = c.match( + /^light-dark\(\s*([a-z]+(\(.*\))?),\s*([a-z]+(\(.*\))?)\s*\)$/ + ); + if (match) { + const schemeColor = isSystemDarkModeEnabled() + ? match[3] + : match[1]; + return parse(schemeColor); + } } - anchor.href = $url; - return anchor.href; + return null; } - function parseURL($url, $base = null) { - const key = `${$url}${$base ? `;${$base}` : ""}`; - if (parsedURLCache.has(key)) { - return parsedURLCache.get(key); + function getNumbers($color) { + const numbers = []; + let prevPos = 0; + let isMining = false; + const startIndex = $color.indexOf("("); + $color = $color.substring(startIndex + 1, $color.length - 1); + for (let i = 0; i < $color.length; i++) { + const c = $color[i]; + if ((c >= "0" && c <= "9") || c === "." || c === "+" || c === "-") { + isMining = true; + } else if (isMining && (c === " " || c === "," || c === "/")) { + numbers.push($color.substring(prevPos, i)); + isMining = false; + prevPos = i + 1; + } else if (!isMining) { + prevPos = i + 1; + } } - if ($base) { - const parsedURL = new URL($url, fixBaseURL($base)); - parsedURLCache.set(key, parsedURL); - return parsedURL; + if (isMining) { + numbers.push($color.substring(prevPos, $color.length)); } - const parsedURL = new URL(fixBaseURL($url)); - parsedURLCache.set($url, parsedURL); - return parsedURL; + return numbers; } - function getAbsoluteURL($base, $relative) { - if ($relative.match(/^data\\?\:/)) { - return $relative; - } - if (/^\/\//.test($relative)) { - return `${location.protocol}${$relative}`; + function getNumbersFromString(str, range, units) { + const raw = getNumbers(str); + const unitsList = Object.entries(units); + const numbers = raw + .map((r) => r.trim()) + .map((r, i) => { + let n; + const unit = unitsList.find(([u]) => r.endsWith(u)); + if (unit) { + n = + (parseFloat(r.substring(0, r.length - unit[0].length)) / + unit[1]) * + range[i]; + } else { + n = parseFloat(r); + } + if (range[i] > 1) { + return Math.round(n); + } + return n; + }); + return numbers; + } + const rgbRange = [255, 255, 255, 1]; + const rgbUnits = {"%": 100}; + function parseRGB($rgb) { + const [r, g, b, a = 1] = getNumbersFromString($rgb, rgbRange, rgbUnits); + if (r == null || g == null || b == null || a == null) { + return null; } - const b = parseURL($base); - const a = parseURL($relative, b.href); - return a.href; + return {r, g, b, a}; } - function isRelativeHrefOnAbsolutePath(href) { - if (href.startsWith("data:")) { - return true; + const hslRange = [360, 1, 1, 1]; + const hslUnits = {"%": 100, "deg": 360, "rad": 2 * Math.PI, "turn": 1}; + function parseHSL($hsl) { + const [h, s, l, a = 1] = getNumbersFromString($hsl, hslRange, hslUnits); + if (h == null || s == null || l == null || a == null) { + return null; } - const url = parseURL(href); - if (url.protocol !== location.protocol) { - return false; - } - if (url.hostname !== location.hostname) { - return false; - } - if (url.port !== location.port) { - return false; - } - return url.pathname === location.pathname; + return hslToRGB({h, s, l, a}); } - - function iterateCSSRules(rules, iterate, onImportError) { - forEach(rules, (rule) => { - if (isStyleRule(rule)) { - iterate(rule); - } else if (isImportRule(rule)) { - try { - iterateCSSRules( - rule.styleSheet.cssRules, - iterate, - onImportError - ); - } catch (err) { - onImportError?.(); - } - } else if (isMediaRule(rule)) { - const media = Array.from(rule.media); - const isScreenOrAllOrQuery = media.some( - (m) => - m.startsWith("screen") || - m.startsWith("all") || - m.startsWith("(") - ); - const isPrintOrSpeech = media.some( - (m) => m.startsWith("print") || m.startsWith("speech") + function parseHex($hex) { + const h = $hex.substring(1); + switch (h.length) { + case 3: + case 4: { + const [r, g, b] = [0, 1, 2].map((i) => + parseInt(`${h[i]}${h[i]}`, 16) ); - if (isScreenOrAllOrQuery || !isPrintOrSpeech) { - iterateCSSRules(rule.cssRules, iterate, onImportError); - } - } else if (isSupportsRule(rule)) { - if (CSS.supports(rule.conditionText)) { - iterateCSSRules(rule.cssRules, iterate, onImportError); - } - } else if (isLayerRule(rule)) { - iterateCSSRules(rule.cssRules, iterate, onImportError); - } else; - }); - } - const shorthandVarDependantProperties = [ - "background", - "border", - "border-color", - "border-bottom", - "border-left", - "border-right", - "border-top", - "outline", - "outline-color" - ]; - const shorthandVarDepPropRegexps = isSafari - ? shorthandVarDependantProperties.map((prop) => { - const regexp = new RegExp(`${prop}:\\s*(.*?)\\s*;`); - return [prop, regexp]; - }) - : null; - function iterateCSSDeclarations(style, iterate) { - forEach(style, (property) => { - const value = style.getPropertyValue(property).trim(); - if (!value) { - return; - } - iterate(property, value); - }); - const cssText = style.cssText; - if (cssText.includes("var(")) { - if (isSafari) { - shorthandVarDepPropRegexps.forEach(([prop, regexp]) => { - const match = cssText.match(regexp); - if (match && match[1]) { - const val = match[1].trim(); - iterate(prop, val); - } - }); - } else { - shorthandVarDependantProperties.forEach((prop) => { - const val = style.getPropertyValue(prop); - if (val && val.includes("var(")) { - iterate(prop, val); - } - }); + const a = + h.length === 3 ? 1 : parseInt(`${h[3]}${h[3]}`, 16) / 255; + return {r, g, b, a}; } - } - if ( - cssText.includes("background-color: ;") && - !style.getPropertyValue("background") - ) { - handleEmptyShorthand("background", style, iterate); - } - if ( - cssText.includes("border-") && - cssText.includes("-color: ;") && - !style.getPropertyValue("border") - ) { - handleEmptyShorthand("border", style, iterate); - } - } - function handleEmptyShorthand(shorthand, style, iterate) { - const parentRule = style.parentRule; - if (isStyleRule(parentRule)) { - const sourceCSSText = - parentRule.parentStyleSheet?.ownerNode?.textContent; - if (sourceCSSText) { - let escapedSelector = escapeRegExpSpecialChars( - parentRule.selectorText - ); - escapedSelector = escapedSelector.replaceAll(/\s+/g, "\\s*"); - escapedSelector = escapedSelector.replaceAll(/::/g, "::?"); - const regexp = new RegExp( - `${escapedSelector}\\s*{[^}]*${shorthand}:\\s*([^;}]+)` + case 6: + case 8: { + const [r, g, b] = [0, 2, 4].map((i) => + parseInt(h.substring(i, i + 2), 16) ); - const match = sourceCSSText.match(regexp); - if (match) { - iterate(shorthand, match[1]); - } - } else if (shorthand === "background") { - iterate("background-color", "#ffffff"); + const a = + h.length === 6 ? 1 : parseInt(h.substring(6, 8), 16) / 255; + return {r, g, b, a}; } } + return null; } - const cssURLRegex = /url\((('.*?')|(".*?")|([^\)]*?))\)/g; - const cssImportRegex = - /@import\s*(url\()?(('.+?')|(".+?")|([^\)]*?))\)? ?(screen)?;?/gi; - function getCSSURLValue(cssURL) { - return cssURL - .trim() - .replace(/[\n\r\\]+/g, "") - .replace(/^url\((.*)\)$/, "$1") - .trim() - .replace(/^"(.*)"$/, "$1") - .replace(/^'(.*)'$/, "$1") - .replace(/(?:\\(.))/g, "$1"); + function getColorByName($color) { + const n = knownColors.get($color); + return { + r: (n >> 16) & 255, + g: (n >> 8) & 255, + b: (n >> 0) & 255, + a: 1 + }; } - function getCSSBaseBath(url) { - const cssURL = parseURL(url); - return `${cssURL.origin}${cssURL.pathname.replace(/\?.*$/, "").replace(/(\/)([^\/]+)$/i, "$1")}`; + function getSystemColor($color) { + const n = systemColors.get($color); + return { + r: (n >> 16) & 255, + g: (n >> 8) & 255, + b: (n >> 0) & 255, + a: 1 + }; } - function replaceCSSRelativeURLsWithAbsolute($css, cssBasePath) { - return $css.replace(cssURLRegex, (match) => { - try { - const url = getCSSURLValue(match); - const absoluteURL = getAbsoluteURL(cssBasePath, url); - const escapedURL = absoluteURL.replaceAll("'", "\\'"); - return `url('${escapedURL}')`; - } catch (err) { - return match; + function lowerCalcExpression(color) { + let searchIndex = 0; + const replaceBetweenIndices = (start, end, replacement) => { + color = + color.substring(0, start) + replacement + color.substring(end); + }; + while ((searchIndex = color.indexOf("calc(")) !== -1) { + const range = getParenthesesRange(color, searchIndex); + if (!range) { + break; } - }); - } - const fontFaceRegex = /@font-face\s*{[^}]*}/g; - function replaceCSSFontFace($css) { - return $css.replace(fontFaceRegex, ""); - } - const styleRules = new WeakSet(); - const importRules = new WeakSet(); - const mediaRules = new WeakSet(); - const supportsRules = new WeakSet(); - const layerRules = new WeakSet(); - function isStyleRule(rule) { - if (!rule) { - return false; - } - if (styleRules.has(rule)) { - return true; - } - if (rule.selectorText) { - styleRules.add(rule); - return true; - } - return false; - } - function isImportRule(rule) { - if (!rule) { - return false; - } - if (styleRules.has(rule)) { - return false; - } - if (importRules.has(rule)) { - return true; - } - if (rule.href) { - importRules.add(rule); - return true; - } - return false; - } - function isMediaRule(rule) { - if (!rule) { - return false; - } - if (styleRules.has(rule)) { - return false; - } - if (mediaRules.has(rule)) { - return true; - } - if (rule.media) { - mediaRules.add(rule); - return true; + let slice = color.slice(range.start + 1, range.end - 1); + const includesPercentage = slice.includes("%"); + slice = slice.split("%").join(""); + const output = Math.round(evalMath(slice)); + replaceBetweenIndices( + range.start - 4, + range.end, + output + (includesPercentage ? "%" : "") + ); } - return false; + return color; } - function isSupportsRule(rule) { - if (!rule) { - return false; - } - if (styleRules.has(rule)) { - return false; - } - if (supportsRules.has(rule)) { - return true; - } - if (rule instanceof CSSSupportsRule) { - supportsRules.add(rule); - return true; - } - return false; + const knownColors = new Map( + Object.entries({ + aliceblue: 0xf0f8ff, + antiquewhite: 0xfaebd7, + aqua: 0x00ffff, + aquamarine: 0x7fffd4, + azure: 0xf0ffff, + beige: 0xf5f5dc, + bisque: 0xffe4c4, + black: 0x000000, + blanchedalmond: 0xffebcd, + blue: 0x0000ff, + blueviolet: 0x8a2be2, + brown: 0xa52a2a, + burlywood: 0xdeb887, + cadetblue: 0x5f9ea0, + chartreuse: 0x7fff00, + chocolate: 0xd2691e, + coral: 0xff7f50, + cornflowerblue: 0x6495ed, + cornsilk: 0xfff8dc, + crimson: 0xdc143c, + cyan: 0x00ffff, + darkblue: 0x00008b, + darkcyan: 0x008b8b, + darkgoldenrod: 0xb8860b, + darkgray: 0xa9a9a9, + darkgrey: 0xa9a9a9, + darkgreen: 0x006400, + darkkhaki: 0xbdb76b, + darkmagenta: 0x8b008b, + darkolivegreen: 0x556b2f, + darkorange: 0xff8c00, + darkorchid: 0x9932cc, + darkred: 0x8b0000, + darksalmon: 0xe9967a, + darkseagreen: 0x8fbc8f, + darkslateblue: 0x483d8b, + darkslategray: 0x2f4f4f, + darkslategrey: 0x2f4f4f, + darkturquoise: 0x00ced1, + darkviolet: 0x9400d3, + deeppink: 0xff1493, + deepskyblue: 0x00bfff, + dimgray: 0x696969, + dimgrey: 0x696969, + dodgerblue: 0x1e90ff, + firebrick: 0xb22222, + floralwhite: 0xfffaf0, + forestgreen: 0x228b22, + fuchsia: 0xff00ff, + gainsboro: 0xdcdcdc, + ghostwhite: 0xf8f8ff, + gold: 0xffd700, + goldenrod: 0xdaa520, + gray: 0x808080, + grey: 0x808080, + green: 0x008000, + greenyellow: 0xadff2f, + honeydew: 0xf0fff0, + hotpink: 0xff69b4, + indianred: 0xcd5c5c, + indigo: 0x4b0082, + ivory: 0xfffff0, + khaki: 0xf0e68c, + lavender: 0xe6e6fa, + lavenderblush: 0xfff0f5, + lawngreen: 0x7cfc00, + lemonchiffon: 0xfffacd, + lightblue: 0xadd8e6, + lightcoral: 0xf08080, + lightcyan: 0xe0ffff, + lightgoldenrodyellow: 0xfafad2, + lightgray: 0xd3d3d3, + lightgrey: 0xd3d3d3, + lightgreen: 0x90ee90, + lightpink: 0xffb6c1, + lightsalmon: 0xffa07a, + lightseagreen: 0x20b2aa, + lightskyblue: 0x87cefa, + lightslategray: 0x778899, + lightslategrey: 0x778899, + lightsteelblue: 0xb0c4de, + lightyellow: 0xffffe0, + lime: 0x00ff00, + limegreen: 0x32cd32, + linen: 0xfaf0e6, + magenta: 0xff00ff, + maroon: 0x800000, + mediumaquamarine: 0x66cdaa, + mediumblue: 0x0000cd, + mediumorchid: 0xba55d3, + mediumpurple: 0x9370db, + mediumseagreen: 0x3cb371, + mediumslateblue: 0x7b68ee, + mediumspringgreen: 0x00fa9a, + mediumturquoise: 0x48d1cc, + mediumvioletred: 0xc71585, + midnightblue: 0x191970, + mintcream: 0xf5fffa, + mistyrose: 0xffe4e1, + moccasin: 0xffe4b5, + navajowhite: 0xffdead, + navy: 0x000080, + oldlace: 0xfdf5e6, + olive: 0x808000, + olivedrab: 0x6b8e23, + orange: 0xffa500, + orangered: 0xff4500, + orchid: 0xda70d6, + palegoldenrod: 0xeee8aa, + palegreen: 0x98fb98, + paleturquoise: 0xafeeee, + palevioletred: 0xdb7093, + papayawhip: 0xffefd5, + peachpuff: 0xffdab9, + peru: 0xcd853f, + pink: 0xffc0cb, + plum: 0xdda0dd, + powderblue: 0xb0e0e6, + purple: 0x800080, + rebeccapurple: 0x663399, + red: 0xff0000, + rosybrown: 0xbc8f8f, + royalblue: 0x4169e1, + saddlebrown: 0x8b4513, + salmon: 0xfa8072, + sandybrown: 0xf4a460, + seagreen: 0x2e8b57, + seashell: 0xfff5ee, + sienna: 0xa0522d, + silver: 0xc0c0c0, + skyblue: 0x87ceeb, + slateblue: 0x6a5acd, + slategray: 0x708090, + slategrey: 0x708090, + snow: 0xfffafa, + springgreen: 0x00ff7f, + steelblue: 0x4682b4, + tan: 0xd2b48c, + teal: 0x008080, + thistle: 0xd8bfd8, + tomato: 0xff6347, + turquoise: 0x40e0d0, + violet: 0xee82ee, + wheat: 0xf5deb3, + white: 0xffffff, + whitesmoke: 0xf5f5f5, + yellow: 0xffff00, + yellowgreen: 0x9acd32 + }) + ); + const systemColors = new Map( + Object.entries({ + "ActiveBorder": 0x3b99fc, + "ActiveCaption": 0x000000, + "AppWorkspace": 0xaaaaaa, + "Background": 0x6363ce, + "ButtonFace": 0xffffff, + "ButtonHighlight": 0xe9e9e9, + "ButtonShadow": 0x9fa09f, + "ButtonText": 0x000000, + "CaptionText": 0x000000, + "GrayText": 0x7f7f7f, + "Highlight": 0xb2d7ff, + "HighlightText": 0x000000, + "InactiveBorder": 0xffffff, + "InactiveCaption": 0xffffff, + "InactiveCaptionText": 0x000000, + "InfoBackground": 0xfbfcc5, + "InfoText": 0x000000, + "Menu": 0xf6f6f6, + "MenuText": 0xffffff, + "Scrollbar": 0xaaaaaa, + "ThreeDDarkShadow": 0x000000, + "ThreeDFace": 0xc0c0c0, + "ThreeDHighlight": 0xffffff, + "ThreeDLightShadow": 0xffffff, + "ThreeDShadow": 0x000000, + "Window": 0xececec, + "WindowFrame": 0xaaaaaa, + "WindowText": 0x000000, + "-webkit-focus-ring-color": 0xe59700 + }).map(([key, value]) => [key.toLowerCase(), value]) + ); + function getSRGBLightness(r, g, b) { + return (0.2126 * r + 0.7152 * g + 0.0722 * b) / 255; } - function isLayerRule(rule) { - if (!rule) { - return false; - } - if (styleRules.has(rule)) { - return false; - } - if (layerRules.has(rule)) { - return true; - } - if (isLayerRuleSupported && rule instanceof CSSLayerBlockRule) { - layerRules.add(rule); - return true; + let canvas$1; + let context$1; + function domParseColor($color) { + if (!context$1) { + canvas$1 = document.createElement("canvas"); + canvas$1.width = 1; + canvas$1.height = 1; + context$1 = canvas$1.getContext("2d", {willReadFrequently: true}); } - return false; + context$1.fillStyle = $color; + context$1.fillRect(0, 0, 1, 1); + const d = context$1.getImageData(0, 0, 1, 1).data; + const color = `rgba(${d[0]}, ${d[1]}, ${d[2]}, ${(d[3] / 255).toFixed(2)})`; + return parseRGB(color); } - function evalMath(expression) { - const rpnStack = []; - const workingStack = []; - let lastToken; - for (let i = 0, len = expression.length; i < len; i++) { - const token = expression[i]; - if (!token || token === " ") { - continue; - } - if (operators.has(token)) { - const op = operators.get(token); - while (workingStack.length) { - const currentOp = operators.get(workingStack[0]); - if (!currentOp) { - break; + function throttle(callback) { + let pending = false; + let frameId = null; + let lastArgs; + const throttled = (...args) => { + lastArgs = args; + if (frameId) { + pending = true; + } else { + callback(...lastArgs); + frameId = requestAnimationFrame(() => { + frameId = null; + if (pending) { + callback(...lastArgs); + pending = false; } - if (op.lessOrEqualThan(currentOp)) { - rpnStack.push(workingStack.shift()); - } else { - break; - } - } - workingStack.unshift(token); - } else if (!lastToken || operators.has(lastToken)) { - rpnStack.push(token); - } else { - rpnStack[rpnStack.length - 1] += token; + }); } - lastToken = token; - } - rpnStack.push(...workingStack); - const stack = []; - for (let i = 0, len = rpnStack.length; i < len; i++) { - const op = operators.get(rpnStack[i]); - if (op) { - const args = stack.splice(0, 2); - stack.push(op.exec(args[1], args[0])); - } else { - stack.unshift(parseFloat(rpnStack[i])); + }; + const cancel = () => { + cancelAnimationFrame(frameId); + pending = false; + frameId = null; + }; + return Object.assign(throttled, {cancel}); + } + function createAsyncTasksQueue() { + const tasks = []; + let frameId = null; + function runTasks() { + let task; + while ((task = tasks.shift())) { + task(); } + frameId = null; } - return stack[0]; - } - class Operator { - constructor(precedence, method) { - this.precendce = precedence; - this.execMethod = method; + function add(task) { + tasks.push(task); + if (!frameId) { + frameId = requestAnimationFrame(runTasks); + } } - exec(left, right) { - return this.execMethod(left, right); + function cancel() { + tasks.splice(0); + cancelAnimationFrame(frameId); + frameId = null; } - lessOrEqualThan(op) { - return this.precendce <= op.precendce; + return {add, cancel}; + } + const delayTokens = new Set(); + function requestAnimationFrameOnce(token, callback) { + if (delayTokens.has(token)) { + return; } + delayTokens.add(token); + requestAnimationFrame(() => { + delayTokens.delete(token); + callback(); + }); } - const operators = new Map([ - ["+", new Operator(1, (left, right) => left + right)], - ["-", new Operator(1, (left, right) => left - right)], - ["*", new Operator(2, (left, right) => left * right)], - ["/", new Operator(2, (left, right) => left / right)] - ]); - - const isSystemDarkModeEnabled = () => - matchMedia("(prefers-color-scheme: dark)").matches; - const hslaParseCache = new Map(); - const rgbaParseCache = new Map(); - function parseColorWithCache($color) { - $color = $color.trim(); - if (rgbaParseCache.has($color)) { - return rgbaParseCache.get($color); - } - if ($color.includes("calc(")) { - $color = lowerCalcExpression($color); - } - const color = parse($color); - color && rgbaParseCache.set($color, color); - return color; + function hexify(number) { + return (number < 16 ? "0" : "") + number.toString(16); } - function parseToHSLWithCache(color) { - if (hslaParseCache.has(color)) { - return hslaParseCache.get(color); + function generateUID() { + if ("randomUUID" in crypto) { + const uuid = crypto.randomUUID(); + return ( + uuid.substring(0, 8) + + uuid.substring(9, 13) + + uuid.substring(14, 18) + + uuid.substring(19, 23) + + uuid.substring(24) + ); } - const rgb = parseColorWithCache(color); - if (!rgb) { - return null; + if ("getRandomValues" in crypto) { + return Array.from(crypto.getRandomValues(new Uint8Array(16))) + .map((x) => hexify(x)) + .join(""); } - const hsl = rgbToHSL(rgb); - hslaParseCache.set(color, hsl); - return hsl; - } - function clearColorCache() { - hslaParseCache.clear(); - rgbaParseCache.clear(); + return Math.floor(Math.random() * 2 ** 55).toString(36); } - function hslToRGB({ h, s, l, a = 1 }) { - if (s === 0) { - const [r, b, g] = [l, l, l].map((x) => Math.round(x * 255)); - return { r, g, b, a }; - } - const c = (1 - Math.abs(2 * l - 1)) * s; - const x = c * (1 - Math.abs(((h / 60) % 2) - 1)); - const m = l - c / 2; - const [r, g, b] = ( - h < 60 - ? [c, x, 0] - : h < 120 - ? [x, c, 0] - : h < 180 - ? [0, c, x] - : h < 240 - ? [0, x, c] - : h < 300 - ? [x, 0, c] - : [c, 0, x] - ).map((n) => Math.round((n + m) * 255)); - return { r, g, b, a }; + + let documentVisibilityListener = null; + let documentIsVisible_ = !document.hidden; + const listenerOptions = { + capture: true, + passive: true + }; + function watchForDocumentVisibility() { + document.addEventListener( + "visibilitychange", + documentVisibilityListener, + listenerOptions + ); + window.addEventListener( + "pageshow", + documentVisibilityListener, + listenerOptions + ); + window.addEventListener( + "focus", + documentVisibilityListener, + listenerOptions + ); } - function rgbToHSL({ r: r255, g: g255, b: b255, a = 1 }) { - const r = r255 / 255; - const g = g255 / 255; - const b = b255 / 255; - const max = Math.max(r, g, b); - const min = Math.min(r, g, b); - const c = max - min; - const l = (max + min) / 2; - if (c === 0) { - return { h: 0, s: 0, l, a }; - } - let h = - (max === r - ? ((g - b) / c) % 6 - : max === g - ? (b - r) / c + 2 - : (r - g) / c + 4) * 60; - if (h < 0) { - h += 360; - } - const s = c / (1 - Math.abs(2 * l - 1)); - return { h, s, l, a }; + function stopWatchingForDocumentVisibility() { + document.removeEventListener( + "visibilitychange", + documentVisibilityListener, + listenerOptions + ); + window.removeEventListener( + "pageshow", + documentVisibilityListener, + listenerOptions + ); + window.removeEventListener( + "focus", + documentVisibilityListener, + listenerOptions + ); } - function toFixed(n, digits = 0) { - const fixed = n.toFixed(digits); - if (digits === 0) { - return fixed; - } - const dot = fixed.indexOf("."); - if (dot >= 0) { - const zerosMatch = fixed.match(/0+$/); - if (zerosMatch) { - if (zerosMatch.index === dot + 1) { - return fixed.substring(0, dot); - } - return fixed.substring(0, zerosMatch.index); + function setDocumentVisibilityListener(callback) { + const alreadyWatching = Boolean(documentVisibilityListener); + documentVisibilityListener = () => { + if (!document.hidden) { + removeDocumentVisibilityListener(); + callback(); + documentIsVisible_ = true; } + }; + if (!alreadyWatching) { + watchForDocumentVisibility(); } - return fixed; - } - function rgbToString(rgb) { - const { r, g, b, a } = rgb; - if (a != null && a < 1) { - return `rgba(${toFixed(r)}, ${toFixed(g)}, ${toFixed(b)}, ${toFixed(a, 2)})`; - } - return `rgb(${toFixed(r)}, ${toFixed(g)}, ${toFixed(b)})`; } - function rgbToHexString({ r, g, b, a }) { - return `#${(a != null && a < 1 - ? [r, g, b, Math.round(a * 255)] - : [r, g, b] - ) - .map((x) => { - return `${x < 16 ? "0" : ""}${x.toString(16)}`; - }) - .join("")}`; + function removeDocumentVisibilityListener() { + stopWatchingForDocumentVisibility(); + documentVisibilityListener = null; } - function hslToString(hsl) { - const { h, s, l, a } = hsl; - if (a != null && a < 1) { - return `hsla(${toFixed(h)}, ${toFixed(s * 100)}%, ${toFixed(l * 100)}%, ${toFixed(a, 2)})`; - } - return `hsl(${toFixed(h)}, ${toFixed(s * 100)}%, ${toFixed(l * 100)}%)`; + function documentIsVisible() { + return documentIsVisible_; } - const rgbMatch = /^rgba?\([^\(\)]+\)$/; - const hslMatch = /^hsla?\([^\(\)]+\)$/; - const hexMatch = /^#[0-9a-f]+$/i; - function parse($color) { - const c = $color.trim().toLowerCase(); - if (c.match(rgbMatch)) { - return parseRGB(c); + + function getDuration(time) { + let duration = 0; + if (time.seconds) { + duration += time.seconds * 1000; } - if (c.match(hslMatch)) { - return parseHSL(c); + if (time.minutes) { + duration += time.minutes * 60 * 1000; } - if (c.match(hexMatch)) { - return parseHex(c); + if (time.hours) { + duration += time.hours * 60 * 60 * 1000; } - if (knownColors.has(c)) { - return getColorByName(c); + if (time.days) { + duration += time.days * 24 * 60 * 60 * 1000; } - if (systemColors.has(c)) { - return getSystemColor(c); - } - if ($color === "transparent") { - return { r: 0, g: 0, b: 0, a: 0 }; - } - if ( - (c.startsWith("color(") || c.startsWith("color-mix(")) && - c.endsWith(")") - ) { - return domParseColor(c); + return duration; + } + + function logInfo(...args) {} + function logWarn(...args) {} + + function removeNode(node) { + node && node.parentNode && node.parentNode.removeChild(node); + } + function watchForNodePosition(node, mode, onRestore = Function.prototype) { + const MAX_ATTEMPTS_COUNT = 10; + const RETRY_TIMEOUT = getDuration({seconds: 2}); + const ATTEMPTS_INTERVAL = getDuration({seconds: 10}); + const prevSibling = node.previousSibling; + let parent = node.parentNode; + if (!parent) { + throw new Error( + "Unable to watch for node position: parent element not found" + ); } - if (c.startsWith("light-dark(") && c.endsWith(")")) { - const match = c.match( - /^light-dark\(\s*([a-z]+(\(.*\))?),\s*([a-z]+(\(.*\))?)\s*\)$/ + if (mode === "prev-sibling" && !prevSibling) { + throw new Error( + "Unable to watch for node position: there is no previous sibling" ); - if (match) { - const schemeColor = isSystemDarkModeEnabled() - ? match[3] - : match[1]; - return parse(schemeColor); - } } - return null; - } - function getNumbers($color) { - const numbers = []; - let prevPos = 0; - let isMining = false; - const startIndex = $color.indexOf("("); - $color = $color.substring(startIndex + 1, $color.length - 1); - for (let i = 0; i < $color.length; i++) { - const c = $color[i]; - if ((c >= "0" && c <= "9") || c === "." || c === "+" || c === "-") { - isMining = true; - } else if (isMining && (c === " " || c === "," || c === "/")) { - numbers.push($color.substring(prevPos, i)); - isMining = false; - prevPos = i + 1; - } else if (!isMining) { - prevPos = i + 1; + let attempts = 0; + let start = null; + let timeoutId = null; + const restore = throttle(() => { + if (timeoutId) { + return; } - } - if (isMining) { - numbers.push($color.substring(prevPos, $color.length)); - } - return numbers; - } - function getNumbersFromString(str, range, units) { - const raw = getNumbers(str); - const unitsList = Object.entries(units); - const numbers = raw - .map((r) => r.trim()) - .map((r, i) => { - let n; - const unit = unitsList.find(([u]) => r.endsWith(u)); - if (unit) { - n = - (parseFloat(r.substring(0, r.length - unit[0].length)) / - unit[1]) * - range[i]; - } else { - n = parseFloat(r); + attempts++; + const now = Date.now(); + if (start == null) { + start = now; + } else if (attempts >= MAX_ATTEMPTS_COUNT) { + if (now - start < ATTEMPTS_INTERVAL) { + timeoutId = setTimeout(() => { + start = null; + attempts = 0; + timeoutId = null; + restore(); + }, RETRY_TIMEOUT); + return; } - if (range[i] > 1) { - return Math.round(n); + start = now; + attempts = 1; + } + if (mode === "head") { + if (prevSibling && prevSibling.parentNode !== parent) { + stop(); + return; } - return n; - }); - return numbers; - } - const rgbRange = [255, 255, 255, 1]; - const rgbUnits = { "%": 100 }; - function parseRGB($rgb) { - const [r, g, b, a = 1] = getNumbersFromString($rgb, rgbRange, rgbUnits); - return { r, g, b, a }; - } - const hslRange = [360, 1, 1, 1]; - const hslUnits = { "%": 100, "deg": 360, "rad": 2 * Math.PI, "turn": 1 }; - function parseHSL($hsl) { - const [h, s, l, a = 1] = getNumbersFromString($hsl, hslRange, hslUnits); - return hslToRGB({ h, s, l, a }); - } - function parseHex($hex) { - const h = $hex.substring(1); - switch (h.length) { - case 3: - case 4: { - const [r, g, b] = [0, 1, 2].map((i) => - parseInt(`${h[i]}${h[i]}`, 16) - ); - const a = - h.length === 3 ? 1 : parseInt(`${h[3]}${h[3]}`, 16) / 255; - return { r, g, b, a }; } - case 6: - case 8: { - const [r, g, b] = [0, 2, 4].map((i) => - parseInt(h.substring(i, i + 2), 16) - ); - const a = - h.length === 6 ? 1 : parseInt(h.substring(6, 8), 16) / 255; - return { r, g, b, a }; + if (mode === "prev-sibling") { + if (prevSibling.parentNode == null) { + stop(); + return; + } + if (prevSibling.parentNode !== parent) { + updateParent(prevSibling.parentNode); + } } - } - return null; - } - function getColorByName($color) { - const n = knownColors.get($color); - return { - r: (n >> 16) & 255, - g: (n >> 8) & 255, - b: (n >> 0) & 255, - a: 1 + if (mode === "head" && !parent.isConnected) { + parent = document.head; + } + parent.insertBefore( + node, + prevSibling && prevSibling.isConnected + ? prevSibling.nextSibling + : parent.firstChild + ); + observer.takeRecords(); + onRestore && onRestore(); + }); + const observer = new MutationObserver(() => { + if ( + (mode === "head" && + (node.parentNode !== parent || + !node.parentNode.isConnected)) || + (mode === "prev-sibling" && + node.previousSibling !== prevSibling) + ) { + restore(); + } + }); + const run = () => { + observer.observe(parent, {childList: true}); }; - } - function getSystemColor($color) { - const n = systemColors.get($color); - return { - r: (n >> 16) & 255, - g: (n >> 8) & 255, - b: (n >> 0) & 255, - a: 1 + const stop = () => { + clearTimeout(timeoutId); + observer.disconnect(); + restore.cancel(); }; - } - function lowerCalcExpression(color) { - let searchIndex = 0; - const replaceBetweenIndices = (start, end, replacement) => { - color = - color.substring(0, start) + replacement + color.substring(end); + const skip = () => { + observer.takeRecords(); }; - while ((searchIndex = color.indexOf("calc(")) !== -1) { - const range = getParenthesesRange(color, searchIndex); - if (!range) { - break; + const updateParent = (parentNode) => { + parent = parentNode; + stop(); + run(); + }; + run(); + return {run, stop, skip}; + } + function iterateShadowHosts(root, iterator) { + if (root == null) { + return; + } + const walker = document.createTreeWalker( + root, + NodeFilter.SHOW_ELEMENT, + { + acceptNode(node) { + return node.shadowRoot == null + ? NodeFilter.FILTER_SKIP + : NodeFilter.FILTER_ACCEPT; + } } - let slice = color.slice(range.start + 1, range.end - 1); - const includesPercentage = slice.includes("%"); - slice = slice.split("%").join(""); - const output = Math.round(evalMath(slice)); - replaceBetweenIndices( - range.start - 4, - range.end, - output + (includesPercentage ? "%" : "") - ); + ); + for ( + let node = root.shadowRoot ? walker.currentNode : walker.nextNode(); + node != null; + node = walker.nextNode() + ) { + if (node.classList.contains("surfingkeys_hints_host")) { + continue; + } + iterator(node); + iterateShadowHosts(node.shadowRoot, iterator); } - return color; } - const knownColors = new Map( - Object.entries({ - aliceblue: 0xf0f8ff, - antiquewhite: 0xfaebd7, - aqua: 0x00ffff, - aquamarine: 0x7fffd4, - azure: 0xf0ffff, - beige: 0xf5f5dc, - bisque: 0xffe4c4, - black: 0x000000, - blanchedalmond: 0xffebcd, - blue: 0x0000ff, - blueviolet: 0x8a2be2, - brown: 0xa52a2a, - burlywood: 0xdeb887, - cadetblue: 0x5f9ea0, - chartreuse: 0x7fff00, - chocolate: 0xd2691e, - coral: 0xff7f50, - cornflowerblue: 0x6495ed, - cornsilk: 0xfff8dc, - crimson: 0xdc143c, - cyan: 0x00ffff, - darkblue: 0x00008b, - darkcyan: 0x008b8b, - darkgoldenrod: 0xb8860b, - darkgray: 0xa9a9a9, - darkgrey: 0xa9a9a9, - darkgreen: 0x006400, - darkkhaki: 0xbdb76b, - darkmagenta: 0x8b008b, - darkolivegreen: 0x556b2f, - darkorange: 0xff8c00, - darkorchid: 0x9932cc, - darkred: 0x8b0000, - darksalmon: 0xe9967a, - darkseagreen: 0x8fbc8f, - darkslateblue: 0x483d8b, - darkslategray: 0x2f4f4f, - darkslategrey: 0x2f4f4f, - darkturquoise: 0x00ced1, - darkviolet: 0x9400d3, - deeppink: 0xff1493, - deepskyblue: 0x00bfff, - dimgray: 0x696969, - dimgrey: 0x696969, - dodgerblue: 0x1e90ff, - firebrick: 0xb22222, - floralwhite: 0xfffaf0, - forestgreen: 0x228b22, - fuchsia: 0xff00ff, - gainsboro: 0xdcdcdc, - ghostwhite: 0xf8f8ff, - gold: 0xffd700, - goldenrod: 0xdaa520, - gray: 0x808080, - grey: 0x808080, - green: 0x008000, - greenyellow: 0xadff2f, - honeydew: 0xf0fff0, - hotpink: 0xff69b4, - indianred: 0xcd5c5c, - indigo: 0x4b0082, - ivory: 0xfffff0, - khaki: 0xf0e68c, - lavender: 0xe6e6fa, - lavenderblush: 0xfff0f5, - lawngreen: 0x7cfc00, - lemonchiffon: 0xfffacd, - lightblue: 0xadd8e6, - lightcoral: 0xf08080, - lightcyan: 0xe0ffff, - lightgoldenrodyellow: 0xfafad2, - lightgray: 0xd3d3d3, - lightgrey: 0xd3d3d3, - lightgreen: 0x90ee90, - lightpink: 0xffb6c1, - lightsalmon: 0xffa07a, - lightseagreen: 0x20b2aa, - lightskyblue: 0x87cefa, - lightslategray: 0x778899, - lightslategrey: 0x778899, - lightsteelblue: 0xb0c4de, - lightyellow: 0xffffe0, - lime: 0x00ff00, - limegreen: 0x32cd32, - linen: 0xfaf0e6, - magenta: 0xff00ff, - maroon: 0x800000, - mediumaquamarine: 0x66cdaa, - mediumblue: 0x0000cd, - mediumorchid: 0xba55d3, - mediumpurple: 0x9370db, - mediumseagreen: 0x3cb371, - mediumslateblue: 0x7b68ee, - mediumspringgreen: 0x00fa9a, - mediumturquoise: 0x48d1cc, - mediumvioletred: 0xc71585, - midnightblue: 0x191970, - mintcream: 0xf5fffa, - mistyrose: 0xffe4e1, - moccasin: 0xffe4b5, - navajowhite: 0xffdead, - navy: 0x000080, - oldlace: 0xfdf5e6, - olive: 0x808000, - olivedrab: 0x6b8e23, - orange: 0xffa500, - orangered: 0xff4500, - orchid: 0xda70d6, - palegoldenrod: 0xeee8aa, - palegreen: 0x98fb98, - paleturquoise: 0xafeeee, - palevioletred: 0xdb7093, - papayawhip: 0xffefd5, - peachpuff: 0xffdab9, - peru: 0xcd853f, - pink: 0xffc0cb, - plum: 0xdda0dd, - powderblue: 0xb0e0e6, - purple: 0x800080, - rebeccapurple: 0x663399, - red: 0xff0000, - rosybrown: 0xbc8f8f, - royalblue: 0x4169e1, - saddlebrown: 0x8b4513, - salmon: 0xfa8072, - sandybrown: 0xf4a460, - seagreen: 0x2e8b57, - seashell: 0xfff5ee, - sienna: 0xa0522d, - silver: 0xc0c0c0, - skyblue: 0x87ceeb, - slateblue: 0x6a5acd, - slategray: 0x708090, - slategrey: 0x708090, - snow: 0xfffafa, - springgreen: 0x00ff7f, - steelblue: 0x4682b4, - tan: 0xd2b48c, - teal: 0x008080, - thistle: 0xd8bfd8, - tomato: 0xff6347, - turquoise: 0x40e0d0, - violet: 0xee82ee, - wheat: 0xf5deb3, - white: 0xffffff, - whitesmoke: 0xf5f5f5, - yellow: 0xffff00, - yellowgreen: 0x9acd32 - }) - ); - const systemColors = new Map( - Object.entries({ - "ActiveBorder": 0x3b99fc, - "ActiveCaption": 0x000000, - "AppWorkspace": 0xaaaaaa, - "Background": 0x6363ce, - "ButtonFace": 0xffffff, - "ButtonHighlight": 0xe9e9e9, - "ButtonShadow": 0x9fa09f, - "ButtonText": 0x000000, - "CaptionText": 0x000000, - "GrayText": 0x7f7f7f, - "Highlight": 0xb2d7ff, - "HighlightText": 0x000000, - "InactiveBorder": 0xffffff, - "InactiveCaption": 0xffffff, - "InactiveCaptionText": 0x000000, - "InfoBackground": 0xfbfcc5, - "InfoText": 0x000000, - "Menu": 0xf6f6f6, - "MenuText": 0xffffff, - "Scrollbar": 0xaaaaaa, - "ThreeDDarkShadow": 0x000000, - "ThreeDFace": 0xc0c0c0, - "ThreeDHighlight": 0xffffff, - "ThreeDLightShadow": 0xffffff, - "ThreeDShadow": 0x000000, - "Window": 0xececec, - "WindowFrame": 0xaaaaaa, - "WindowText": 0x000000, - "-webkit-focus-ring-color": 0xe59700 - }).map(([key, value]) => [key.toLowerCase(), value]) - ); - function getSRGBLightness(r, g, b) { - return (0.2126 * r + 0.7152 * g + 0.0722 * b) / 255; + let isDOMReady = () => { + return ( + document.readyState === "complete" || + document.readyState === "interactive" + ); + }; + function setIsDOMReady(newFunc) { + isDOMReady = newFunc; } - let canvas$1; - let context$1; - function domParseColor($color) { - if (!context$1) { - canvas$1 = document.createElement("canvas"); - canvas$1.width = 1; - canvas$1.height = 1; - context$1 = canvas$1.getContext("2d", { willReadFrequently: true }); - } - context$1.fillStyle = $color; - context$1.fillRect(0, 0, 1, 1); - const d = context$1.getImageData(0, 0, 1, 1).data; - const color = `rgba(${d[0]}, ${d[1]}, ${d[2]}, ${(d[3] / 255).toFixed(2)})`; - return parseRGB(color); + const readyStateListeners = new Set(); + function addDOMReadyListener(listener) { + isDOMReady() ? listener() : readyStateListeners.add(listener); } - - function scale(x, inLow, inHigh, outLow, outHigh) { - return ((x - inLow) * (outHigh - outLow)) / (inHigh - inLow) + outLow; + function removeDOMReadyListener(listener) { + readyStateListeners.delete(listener); } - function clamp(x, min, max) { - return Math.min(max, Math.max(min, x)); + function isReadyStateComplete() { + return document.readyState === "complete"; } - function multiplyMatrices(m1, m2) { - const result = []; - for (let i = 0, len = m1.length; i < len; i++) { - result[i] = []; - for (let j = 0, len2 = m2[0].length; j < len2; j++) { - let sum = 0; - for (let k = 0, len3 = m1[0].length; k < len3; k++) { - sum += m1[i][k] * m2[k][j]; + const readyStateCompleteListeners = new Set(); + function addReadyStateCompleteListener(listener) { + isReadyStateComplete() + ? listener() + : readyStateCompleteListeners.add(listener); + } + function cleanReadyStateCompleteListeners() { + readyStateCompleteListeners.clear(); + } + if (!isDOMReady()) { + const onReadyStateChange = () => { + if (isDOMReady()) { + readyStateListeners.forEach((listener) => listener()); + readyStateListeners.clear(); + if (isReadyStateComplete()) { + document.removeEventListener( + "readystatechange", + onReadyStateChange + ); + readyStateCompleteListeners.forEach((listener) => + listener() + ); + readyStateCompleteListeners.clear(); } - result[i][j] = sum; } - } - return result; + }; + document.addEventListener("readystatechange", onReadyStateChange); } - - function createFilterMatrix(config) { - let m = Matrix.identity(); - if (config.sepia !== 0) { - m = multiplyMatrices(m, Matrix.sepia(config.sepia / 100)); + const HUGE_MUTATIONS_COUNT = 1000; + function isHugeMutation(mutations) { + if (mutations.length > HUGE_MUTATIONS_COUNT) { + return true; } - if (config.grayscale !== 0) { - m = multiplyMatrices(m, Matrix.grayscale(config.grayscale / 100)); + let addedNodesCount = 0; + for (let i = 0; i < mutations.length; i++) { + addedNodesCount += mutations[i].addedNodes.length; + if (addedNodesCount > HUGE_MUTATIONS_COUNT) { + return true; + } } - if (config.contrast !== 100) { - m = multiplyMatrices(m, Matrix.contrast(config.contrast / 100)); - } - if (config.brightness !== 100) { - m = multiplyMatrices(m, Matrix.brightness(config.brightness / 100)); - } - if (config.mode === 1) { - m = multiplyMatrices(m, Matrix.invertNHue()); - } - return m; - } - function applyColorMatrix([r, g, b], matrix) { - const rgb = [[r / 255], [g / 255], [b / 255], [1], [1]]; - const result = multiplyMatrices(matrix, rgb); - return [0, 1, 2].map((i) => - clamp(Math.round(result[i][0] * 255), 0, 255) - ); - } - const Matrix = { - identity() { - return [ - [1, 0, 0, 0, 0], - [0, 1, 0, 0, 0], - [0, 0, 1, 0, 0], - [0, 0, 0, 1, 0], - [0, 0, 0, 0, 1] - ]; - }, - invertNHue() { - return [ - [0.333, -0.667, -0.667, 0, 1], - [-0.667, 0.333, -0.667, 0, 1], - [-0.667, -0.667, 0.333, 0, 1], - [0, 0, 0, 1, 0], - [0, 0, 0, 0, 1] - ]; - }, - brightness(v) { - return [ - [v, 0, 0, 0, 0], - [0, v, 0, 0, 0], - [0, 0, v, 0, 0], - [0, 0, 0, 1, 0], - [0, 0, 0, 0, 1] - ]; - }, - contrast(v) { - const t = (1 - v) / 2; - return [ - [v, 0, 0, 0, t], - [0, v, 0, 0, t], - [0, 0, v, 0, t], - [0, 0, 0, 1, 0], - [0, 0, 0, 0, 1] - ]; - }, - sepia(v) { - return [ - [ - 0.393 + 0.607 * (1 - v), - 0.769 - 0.769 * (1 - v), - 0.189 - 0.189 * (1 - v), - 0, - 0 - ], - [ - 0.349 - 0.349 * (1 - v), - 0.686 + 0.314 * (1 - v), - 0.168 - 0.168 * (1 - v), - 0, - 0 - ], - [ - 0.272 - 0.272 * (1 - v), - 0.534 - 0.534 * (1 - v), - 0.131 + 0.869 * (1 - v), - 0, - 0 - ], - [0, 0, 0, 1, 0], - [0, 0, 0, 0, 1] - ]; - }, - grayscale(v) { - return [ - [ - 0.2126 + 0.7874 * (1 - v), - 0.7152 - 0.7152 * (1 - v), - 0.0722 - 0.0722 * (1 - v), - 0, - 0 - ], - [ - 0.2126 - 0.2126 * (1 - v), - 0.7152 + 0.2848 * (1 - v), - 0.0722 - 0.0722 * (1 - v), - 0, - 0 - ], - [ - 0.2126 - 0.2126 * (1 - v), - 0.7152 - 0.7152 * (1 - v), - 0.0722 + 0.9278 * (1 - v), - 0, - 0 - ], - [0, 0, 0, 1, 0], - [0, 0, 0, 0, 1] - ]; - } - }; - - function getBgPole(theme) { - const isDarkScheme = theme.mode === 1; - const prop = isDarkScheme - ? "darkSchemeBackgroundColor" - : "lightSchemeBackgroundColor"; - return theme[prop]; - } - function getFgPole(theme) { - const isDarkScheme = theme.mode === 1; - const prop = isDarkScheme - ? "darkSchemeTextColor" - : "lightSchemeTextColor"; - return theme[prop]; - } - const colorModificationCache = new Map(); - function clearColorModificationCache() { - colorModificationCache.clear(); + return false; } - const rgbCacheKeys = ["r", "g", "b", "a"]; - const themeCacheKeys$1 = [ - "mode", - "brightness", - "contrast", - "grayscale", - "sepia", - "darkSchemeBackgroundColor", - "darkSchemeTextColor", - "lightSchemeBackgroundColor", - "lightSchemeTextColor" - ]; - function getCacheId(rgb, theme) { - let resultId = ""; - rgbCacheKeys.forEach((key) => { - resultId += `${rgb[key]};`; + function getElementsTreeOperations(mutations) { + const additions = new Set(); + const deletions = new Set(); + const moves = new Set(); + mutations.forEach((m) => { + forEach(m.addedNodes, (n) => { + if (n instanceof Element && n.isConnected) { + additions.add(n); + } + }); + forEach(m.removedNodes, (n) => { + if (n instanceof Element) { + if (n.isConnected) { + moves.add(n); + additions.delete(n); + } else { + deletions.add(n); + } + } + }); }); - themeCacheKeys$1.forEach((key) => { - resultId += `${theme[key]};`; + const duplicateAdditions = []; + const duplicateDeletions = []; + additions.forEach((node) => { + if (additions.has(node.parentElement)) { + duplicateAdditions.push(node); + } }); - return resultId; + deletions.forEach((node) => { + if (deletions.has(node.parentElement)) { + duplicateDeletions.push(node); + } + }); + duplicateAdditions.forEach((node) => additions.delete(node)); + duplicateDeletions.forEach((node) => deletions.delete(node)); + return {additions, moves, deletions}; } - function modifyColorWithCache( - rgb, - theme, - modifyHSL, - poleColor, - anotherPoleColor - ) { - let fnCache; - if (colorModificationCache.has(modifyHSL)) { - fnCache = colorModificationCache.get(modifyHSL); + const optimizedTreeObservers = new Map(); + const optimizedTreeCallbacks = new WeakMap(); + function createOptimizedTreeObserver(root, callbacks) { + let observer; + let observerCallbacks; + let domReadyListener; + if (optimizedTreeObservers.has(root)) { + observer = optimizedTreeObservers.get(root); + observerCallbacks = optimizedTreeCallbacks.get(observer); } else { - fnCache = new Map(); - colorModificationCache.set(modifyHSL, fnCache); - } - const id = getCacheId(rgb, theme); - if (fnCache.has(id)) { - return fnCache.get(id); + let hadHugeMutationsBefore = false; + let subscribedForReadyState = false; + observer = new MutationObserver((mutations) => { + if (isHugeMutation(mutations)) { + if (!hadHugeMutationsBefore || isDOMReady()) { + observerCallbacks.forEach(({onHugeMutations}) => + onHugeMutations(root) + ); + } else if (!subscribedForReadyState) { + domReadyListener = () => + observerCallbacks.forEach(({onHugeMutations}) => + onHugeMutations(root) + ); + addDOMReadyListener(domReadyListener); + subscribedForReadyState = true; + } + hadHugeMutationsBefore = true; + } else { + const elementsOperations = + getElementsTreeOperations(mutations); + observerCallbacks.forEach(({onMinorMutations}) => + onMinorMutations(root, elementsOperations) + ); + } + }); + observer.observe(root, {childList: true, subtree: true}); + optimizedTreeObservers.set(root, observer); + observerCallbacks = new Set(); + optimizedTreeCallbacks.set(observer, observerCallbacks); } - const hsl = rgbToHSL(rgb); - const pole = poleColor == null ? null : parseToHSLWithCache(poleColor); - const anotherPole = - anotherPoleColor == null - ? null - : parseToHSLWithCache(anotherPoleColor); - const modified = modifyHSL(hsl, pole, anotherPole); - const { r, g, b, a } = hslToRGB(modified); - const matrix = createFilterMatrix(theme); - const [rf, gf, bf] = applyColorMatrix([r, g, b], matrix); - const color = - a === 1 - ? rgbToHexString({ r: rf, g: gf, b: bf }) - : rgbToString({ r: rf, g: gf, b: bf, a }); - fnCache.set(id, color); - return color; - } - function noopHSL(hsl) { - return hsl; - } - function modifyColor(rgb, theme) { - return modifyColorWithCache(rgb, theme, noopHSL); - } - function modifyLightSchemeColor(rgb, theme) { - const poleBg = getBgPole(theme); - const poleFg = getFgPole(theme); - return modifyColorWithCache( - rgb, - theme, - modifyLightModeHSL, - poleFg, - poleBg - ); - } - function modifyLightModeHSL({ h, s, l, a }, poleFg, poleBg) { - const isDark = l < 0.5; - let isNeutral; - if (isDark) { - isNeutral = l < 0.2 || s < 0.12; - } else { - const isBlue = h > 200 && h < 280; - isNeutral = s < 0.24 || (l > 0.8 && isBlue); - } - let hx = h; - let sx = l; - if (isNeutral) { - if (isDark) { - hx = poleFg.h; - sx = poleFg.s; - } else { - hx = poleBg.h; - sx = poleBg.s; + observerCallbacks.add(callbacks); + return { + disconnect() { + observerCallbacks.delete(callbacks); + if (domReadyListener) { + removeDOMReadyListener(domReadyListener); + } + if (observerCallbacks.size === 0) { + observer.disconnect(); + optimizedTreeCallbacks.delete(observer); + optimizedTreeObservers.delete(root); + } } - } - const lx = scale(l, 0, 1, poleFg.l, poleBg.l); - return { h: hx, s: sx, l: lx, a }; + }; } - const MAX_BG_LIGHTNESS = 0.4; - function modifyBgHSL({ h, s, l, a }, pole) { - const isDark = l < 0.5; - const isBlue = h > 200 && h < 280; - const isNeutral = s < 0.12 || (l > 0.8 && isBlue); - if (isDark) { - const lx = scale(l, 0, 0.5, 0, MAX_BG_LIGHTNESS); - if (isNeutral) { - const hx = pole.h; - const sx = pole.s; - return { h: hx, s: sx, l: lx, a }; + + function iterateCSSRules(rules, iterate, onImportError) { + forEach(rules, (rule) => { + if (isStyleRule(rule)) { + iterate(rule); + } else if (isImportRule(rule)) { + try { + iterateCSSRules( + rule.styleSheet.cssRules, + iterate, + onImportError + ); + } catch (err) { + onImportError?.(); + } + } else if (isMediaRule(rule)) { + const media = Array.from(rule.media); + const isScreenOrAllOrQuery = media.some( + (m) => + m.startsWith("screen") || + m.startsWith("all") || + m.startsWith("(") + ); + const isPrintOrSpeech = media.some( + (m) => m.startsWith("print") || m.startsWith("speech") + ); + if (isScreenOrAllOrQuery || !isPrintOrSpeech) { + iterateCSSRules(rule.cssRules, iterate, onImportError); + } + } else if (isSupportsRule(rule)) { + if (CSS.supports(rule.conditionText)) { + iterateCSSRules(rule.cssRules, iterate, onImportError); + } + } else if (isLayerRule(rule)) { + iterateCSSRules(rule.cssRules, iterate, onImportError); + } else; + }); + } + const shorthandVarDependantProperties = [ + "background", + "border", + "border-color", + "border-bottom", + "border-left", + "border-right", + "border-top", + "outline", + "outline-color" + ]; + const shorthandVarDepPropRegexps = isSafari + ? shorthandVarDependantProperties.map((prop) => { + const regexp = new RegExp(`${prop}:\\s*(.*?)\\s*;`); + return [prop, regexp]; + }) + : null; + function iterateCSSDeclarations(style, iterate) { + forEach(style, (property) => { + const value = style.getPropertyValue(property).trim(); + if (!value) { + return; } - return { h, s, l: lx, a }; - } - let lx = scale(l, 0.5, 1, MAX_BG_LIGHTNESS, pole.l); - if (isNeutral) { - const hx = pole.h; - const sx = pole.s; - return { h: hx, s: sx, l: lx, a }; - } - let hx = h; - const isYellow = h > 60 && h < 180; - if (isYellow) { - const isCloserToGreen = h > 120; - if (isCloserToGreen) { - hx = scale(h, 120, 180, 135, 180); + iterate(property, value); + }); + const cssText = style.cssText; + if (cssText.includes("var(")) { + if (isSafari) { + shorthandVarDepPropRegexps.forEach(([prop, regexp]) => { + const match = cssText.match(regexp); + if (match && match[1]) { + const val = match[1].trim(); + iterate(prop, val); + } + }); } else { - hx = scale(h, 60, 120, 60, 105); + shorthandVarDependantProperties.forEach((prop) => { + const val = style.getPropertyValue(prop); + if (val && val.includes("var(")) { + iterate(prop, val); + } + }); } } - if (hx > 40 && hx < 80) { - lx *= 0.75; + if ( + cssText.includes("background-color: ;") && + !style.getPropertyValue("background") + ) { + handleEmptyShorthand("background", style, iterate); + } + if ( + cssText.includes("border-") && + cssText.includes("-color: ;") && + !style.getPropertyValue("border") + ) { + handleEmptyShorthand("border", style, iterate); } - return { h: hx, s, l: lx, a }; } - function modifyBackgroundColor(rgb, theme) { - if (theme.mode === 0) { - return modifyLightSchemeColor(rgb, theme); + function handleEmptyShorthand(shorthand, style, iterate) { + const parentRule = style.parentRule; + if (isStyleRule(parentRule)) { + const sourceCSSText = + parentRule.parentStyleSheet?.ownerNode?.textContent; + if (sourceCSSText) { + let escapedSelector = escapeRegExpSpecialChars( + parentRule.selectorText + ); + escapedSelector = escapedSelector.replaceAll(/\s+/g, "\\s*"); + escapedSelector = escapedSelector.replaceAll(/::/g, "::?"); + const regexp = new RegExp( + `${escapedSelector}\\s*{[^}]*${shorthand}:\\s*([^;}]+)` + ); + const match = sourceCSSText.match(regexp); + if (match) { + iterate(shorthand, match[1]); + } + } else if (shorthand === "background") { + iterate("background-color", "#ffffff"); + } } - const pole = getBgPole(theme); - return modifyColorWithCache( - rgb, - { ...theme, mode: 0 }, - modifyBgHSL, - pole - ); } - const MIN_FG_LIGHTNESS = 0.55; - function modifyBlueFgHue(hue) { - return scale(hue, 205, 245, 205, 220); + const cssURLRegex = /url\((('.*?')|(".*?")|([^\)]*?))\)/g; + const cssImportRegex = + /@import\s*(url\()?(('.+?')|(".+?")|([^\)]*?))\)? ?(screen)?;?/gi; + function getCSSURLValue(cssURL) { + return cssURL + .trim() + .replace(/[\n\r\\]+/g, "") + .replace(/^url\((.*)\)$/, "$1") + .trim() + .replace(/^"(.*)"$/, "$1") + .replace(/^'(.*)'$/, "$1") + .replace(/(?:\\(.))/g, "$1"); } - function modifyFgHSL({ h, s, l, a }, pole) { - const isLight = l > 0.5; - const isNeutral = l < 0.2 || s < 0.24; - const isBlue = !isNeutral && h > 205 && h < 245; - if (isLight) { - const lx = scale(l, 0.5, 1, MIN_FG_LIGHTNESS, pole.l); - if (isNeutral) { - const hx = pole.h; - const sx = pole.s; - return { h: hx, s: sx, l: lx, a }; - } - let hx = h; - if (isBlue) { - hx = modifyBlueFgHue(h); + function getCSSBaseBath(url) { + const cssURL = parseURL(url); + return `${cssURL.origin}${cssURL.pathname.replace(/\?.*$/, "").replace(/(\/)([^\/]+)$/i, "$1")}`; + } + function replaceCSSRelativeURLsWithAbsolute($css, cssBasePath) { + return $css.replace(cssURLRegex, (match) => { + try { + const url = getCSSURLValue(match); + const absoluteURL = getAbsoluteURL(cssBasePath, url); + const escapedURL = absoluteURL.replaceAll("'", "\\'"); + return `url('${escapedURL}')`; + } catch (err) { + return match; } - return { h: hx, s, l: lx, a }; + }); + } + const fontFaceRegex = /@font-face\s*{[^}]*}/g; + function replaceCSSFontFace($css) { + return $css.replace(fontFaceRegex, ""); + } + const styleRules = new WeakSet(); + const importRules = new WeakSet(); + const mediaRules = new WeakSet(); + const supportsRules = new WeakSet(); + const layerRules = new WeakSet(); + function isStyleRule(rule) { + if (!rule) { + return false; } - if (isNeutral) { - const hx = pole.h; - const sx = pole.s; - const lx = scale(l, 0, 0.5, pole.l, MIN_FG_LIGHTNESS); - return { h: hx, s: sx, l: lx, a }; + if (styleRules.has(rule)) { + return true; } - let hx = h; - let lx; - if (isBlue) { - hx = modifyBlueFgHue(h); - lx = scale(l, 0, 0.5, pole.l, Math.min(1, MIN_FG_LIGHTNESS + 0.05)); - } else { - lx = scale(l, 0, 0.5, pole.l, MIN_FG_LIGHTNESS); + if (rule.selectorText) { + styleRules.add(rule); + return true; } - return { h: hx, s, l: lx, a }; + return false; } - function modifyForegroundColor(rgb, theme) { - if (theme.mode === 0) { - return modifyLightSchemeColor(rgb, theme); + function isImportRule(rule) { + if (!rule) { + return false; } - const pole = getFgPole(theme); - return modifyColorWithCache( - rgb, - { ...theme, mode: 0 }, - modifyFgHSL, - pole - ); - } - function modifyBorderHSL({ h, s, l, a }, poleFg, poleBg) { - const isDark = l < 0.5; - const isNeutral = l < 0.2 || s < 0.24; - let hx = h; - let sx = s; - if (isNeutral) { - if (isDark) { - hx = poleFg.h; - sx = poleFg.s; - } else { - hx = poleBg.h; - sx = poleBg.s; - } + if (styleRules.has(rule)) { + return false; } - const lx = scale(l, 0, 1, 0.5, 0.2); - return { h: hx, s: sx, l: lx, a }; - } - function modifyBorderColor(rgb, theme) { - if (theme.mode === 0) { - return modifyLightSchemeColor(rgb, theme); + if (importRules.has(rule)) { + return true; } - const poleFg = getFgPole(theme); - const poleBg = getBgPole(theme); - return modifyColorWithCache( - rgb, - { ...theme, mode: 0 }, - modifyBorderHSL, - poleFg, - poleBg - ); + if (rule.href) { + importRules.add(rule); + return true; + } + return false; } - function modifyShadowColor(rgb, theme) { - return modifyBackgroundColor(rgb, theme); + function isMediaRule(rule) { + if (!rule) { + return false; + } + if (styleRules.has(rule)) { + return false; + } + if (mediaRules.has(rule)) { + return true; + } + if (rule.media) { + mediaRules.add(rule); + return true; + } + return false; } - function modifyGradientColor(rgb, theme) { - return modifyBackgroundColor(rgb, theme); + function isSupportsRule(rule) { + if (!rule) { + return false; + } + if (styleRules.has(rule)) { + return false; + } + if (supportsRules.has(rule)) { + return true; + } + if (rule instanceof CSSSupportsRule) { + supportsRules.add(rule); + return true; + } + return false; } - - const excludedSelectors = [ - "pre", - "pre *", - "code", - '[aria-hidden="true"]', - '[class*="fa-"]', - ".fa", - ".fab", - ".fad", - ".fal", - ".far", - ".fas", - ".fass", - ".fasr", - ".fat", - ".icofont", - '[style*="font-"]', - '[class*="icon"]', - '[class*="Icon"]', - '[class*="symbol"]', - '[class*="Symbol"]', - ".glyphicon", - '[class*="material-symbol"]', - '[class*="material-icon"]', - "mu", - '[class*="mu-"]', - ".typcn", - '[class*="vjs-"]' - ]; - function createTextStyle(config) { - const lines = []; - lines.push(`*:not(${excludedSelectors.join(", ")}) {`); - if (config.useFont && config.fontFamily) { - lines.push(` font-family: ${config.fontFamily} !important;`); + function isLayerRule(rule) { + if (!rule) { + return false; } - if (config.textStroke > 0) { - lines.push( - ` -webkit-text-stroke: ${config.textStroke}px !important;` - ); - lines.push(` text-stroke: ${config.textStroke}px !important;`); + if (styleRules.has(rule)) { + return false; } - lines.push("}"); - return lines.join("\n"); + if (layerRules.has(rule)) { + return true; + } + if (isLayerRuleSupported && rule instanceof CSSLayerBlockRule) { + layerRules.add(rule); + return true; + } + return false; } - var FilterMode; - (function (FilterMode) { - FilterMode[(FilterMode["light"] = 0)] = "light"; - FilterMode[(FilterMode["dark"] = 1)] = "dark"; - })(FilterMode || (FilterMode = {})); - function getCSSFilterValue(config) { - const filters = []; - if (config.mode === FilterMode.dark) { - filters.push("invert(100%) hue-rotate(180deg)"); + const sheetsScopes = new WeakMap(); + function defineSheetScope(sheet, node) { + sheetsScopes.set(sheet, node); + } + function getSheetScope(sheet) { + if (!sheet.ownerNode) { + return null; } - if (config.brightness !== 100) { - filters.push(`brightness(${config.brightness}%)`); + if (sheetsScopes.has(sheet)) { + return sheetsScopes.get(sheet); } - if (config.contrast !== 100) { - filters.push(`contrast(${config.contrast}%)`); + let node = sheet.ownerNode; + while (node) { + if (node instanceof ShadowRoot || node instanceof Document) { + defineSheetScope(sheet, node); + return node; + } + node = node.parentNode; } - if (config.grayscale !== 0) { - filters.push(`grayscale(${config.grayscale}%)`); + return null; + } + + const gradientLength = "gradient".length; + const conicGradient = "conic-"; + const conicGradientLength = conicGradient.length; + const radialGradient = "radial-"; + const linearGradient = "linear-"; + function parseGradient(value) { + const result = []; + let index = 0; + let startIndex = conicGradient.length; + while ((index = value.indexOf("gradient", startIndex)) !== -1) { + let typeGradient; + [linearGradient, radialGradient, conicGradient].find( + (possibleType) => { + if (index - possibleType.length >= 0) { + const possibleGradient = value.substring( + index - possibleType.length, + index + ); + if (possibleGradient === possibleType) { + if ( + value.slice( + index - possibleType.length - 10, + index - possibleType.length - 1 + ) === "repeating" + ) { + typeGradient = `repeating-${possibleType}gradient`; + return true; + } + if ( + value.slice( + index - possibleType.length - 8, + index - possibleType.length - 1 + ) === "-webkit" + ) { + typeGradient = `-webkit-${possibleType}gradient`; + return true; + } + typeGradient = `${possibleType}gradient`; + return true; + } + } + } + ); + if (!typeGradient) { + break; + } + const {start, end} = getParenthesesRange( + value, + index + gradientLength + ); + const match = value.substring(start + 1, end - 1); + startIndex = end + 1 + conicGradientLength; + result.push({ + typeGradient, + match, + offset: typeGradient.length + 2, + index: index - typeGradient.length + gradientLength, + hasComma: true + }); } - if (config.sepia !== 0) { - filters.push(`sepia(${config.sepia}%)`); + if (result.length) { + result[result.length - 1].hasComma = false; } - if (filters.length === 0) { - return null; + return result; + } + + const STORAGE_KEY_IMAGE_DETAILS_LIST = "__darkreader__imageDetails_v2_list"; + const STORAGE_KEY_IMAGE_DETAILS_PREFIX = "__darkreader__imageDetails_v2_"; + const STORAGE_KEY_CSS_FETCH_PREFIX = "__darkreader__cssFetch_"; + let imageCacheTimeout = 0; + const imageDetailsCacheQueue = new Map(); + const cachedImageUrls = []; + function writeImageDetailsQueue() { + imageDetailsCacheQueue.forEach((details, url) => { + if (url && url.startsWith("https://")) { + try { + const json = JSON.stringify(details); + sessionStorage.setItem( + `${STORAGE_KEY_IMAGE_DETAILS_PREFIX}${url}`, + json + ); + cachedImageUrls.push(url); + } catch (err) {} + } + }); + imageDetailsCacheQueue.clear(); + sessionStorage.setItem( + STORAGE_KEY_IMAGE_DETAILS_LIST, + JSON.stringify(cachedImageUrls) + ); + } + function writeImageDetailsCache(url, imageDetails) { + if (!url || !url.startsWith("https://")) { + return; } - return filters.join(" "); + imageDetailsCacheQueue.set(url, imageDetails); + clearTimeout(imageCacheTimeout); + imageCacheTimeout = setTimeout(writeImageDetailsQueue, 1000); + } + function readImageDetailsCache(targetMap) { + try { + const jsonList = sessionStorage.getItem( + STORAGE_KEY_IMAGE_DETAILS_LIST + ); + if (!jsonList) { + return; + } + const list = JSON.parse(jsonList); + list.forEach((url) => { + const json = sessionStorage.getItem( + `${STORAGE_KEY_IMAGE_DETAILS_PREFIX}${url}` + ); + if (json) { + const details = JSON.parse(json); + targetMap.set(url, details); + } + }); + } catch (err) {} + } + function writeCSSFetchCache(url, cssText) { + const key = `${STORAGE_KEY_CSS_FETCH_PREFIX}${url}`; + try { + sessionStorage.setItem(key, cssText); + } catch (err) {} + } + function readCSSFetchCache(url) { + const key = `${STORAGE_KEY_CSS_FETCH_PREFIX}${url}`; + try { + return sessionStorage.getItem(key) ?? null; + } catch (err) {} + return null; } function toSVGMatrix(matrix) { @@ -2192,26 +2222,40 @@ return toSVGMatrix(createFilterMatrix(config)); } - function hexify(number) { - return (number < 16 ? "0" : "") + number.toString(16); - } - function generateUID() { - if ("randomUUID" in crypto) { - const uuid = crypto.randomUUID(); - return ( - uuid.substring(0, 8) + - uuid.substring(9, 13) + - uuid.substring(14, 18) + - uuid.substring(19, 23) + - uuid.substring(24) - ); + const MAX_FRAME_DURATION = 1000 / 60; + class AsyncQueue { + constructor() { + this.queue = []; + this.timerId = null; } - if ("getRandomValues" in crypto) { - return Array.from(crypto.getRandomValues(new Uint8Array(16))) - .map((x) => hexify(x)) - .join(""); + addTask(task) { + this.queue.push(task); + this.scheduleFrame(); + } + stop() { + if (this.timerId !== null) { + cancelAnimationFrame(this.timerId); + this.timerId = null; + } + this.queue = []; + } + scheduleFrame() { + if (this.timerId) { + return; + } + this.timerId = requestAnimationFrame(() => { + this.timerId = null; + const start = Date.now(); + let cb; + while ((cb = this.queue.shift())) { + cb(); + if (Date.now() - start >= MAX_FRAME_DURATION) { + this.scheduleFrame(); + break; + } + } + }); } - return Math.floor(Math.random() * 2 ** 55).toString(36); } const resolvers$1 = new Map(); @@ -2231,7 +2275,7 @@ }); }); } - chrome.runtime.onMessage.addListener(({ type, data, error, id }) => { + chrome.runtime.onMessage.addListener(({type, data, error, id}) => { if (type === MessageTypeBGtoCS.FETCH_RESPONSE) { const resolve = resolvers$1.get(id); const reject = rejectors.get(id); @@ -2245,42 +2289,6 @@ } }); - const MAX_FRAME_DURATION = 1000 / 60; - class AsyncQueue { - constructor() { - this.queue = []; - this.timerId = null; - } - addTask(task) { - this.queue.push(task); - this.scheduleFrame(); - } - stop() { - if (this.timerId !== null) { - cancelAnimationFrame(this.timerId); - this.timerId = null; - } - this.queue = []; - } - scheduleFrame() { - if (this.timerId) { - return; - } - this.timerId = requestAnimationFrame(() => { - this.timerId = null; - const start = Date.now(); - let cb; - while ((cb = this.queue.shift())) { - cb(); - if (Date.now() - start >= MAX_FRAME_DURATION) { - this.scheduleFrame(); - break; - } - } - }); - } - } - const imageManager = new AsyncQueue(); async function getImageDetails(url) { return new Promise(async (resolve, reject) => { @@ -2319,7 +2327,7 @@ if (parsedURL.origin === location.origin) { return await loadAsDataURL(url); } - return await bgFetch({ url, responseType: "data-url" }); + return await bgFetch({url, responseType: "data-url"}); } async function tryCreateImageBitmap(blob) { try { @@ -2357,7 +2365,7 @@ canvas = document.createElement("canvas"); canvas.width = maxWidth; canvas.height = maxHeight; - context = canvas.getContext("2d", { willReadFrequently: true }); + context = canvas.getContext("2d", {willReadFrequently: true}); context.imageSmoothingEnabled = false; } function removeCanvas() { @@ -2449,7 +2457,7 @@ document.addEventListener( "__darkreader__inlineScriptsAllowed", () => (canUseProxy = true), - { once: true } + {once: true} ); async function requestBlobURLCheck() { if (!canUseProxy) { @@ -2470,7 +2478,7 @@ blobURLCheckAwaiters.forEach((r) => r()); blobURLCheckAwaiters.splice(0); }, - { once: true } + {once: true} ); document.dispatchEvent( new CustomEvent("__darkreader__blobURLCheckRequest") @@ -2488,7 +2496,7 @@ } document.addEventListener("securitypolicyviolation", onCSPError); const objectURLs = new Set(); - function getFilteredImageURL({ dataURL, width, height }, theme) { + function getFilteredImageURL({dataURL, width, height}, theme) { if (dataURL.startsWith("data:image/svg+xml")) { dataURL = escapeXML(dataURL); } @@ -2510,7 +2518,7 @@ for (let i = 0; i < svg.length; i++) { bytes[i] = svg.charCodeAt(i); } - const blob = new Blob([bytes], { type: "image/svg+xml" }); + const blob = new Blob([bytes], {type: "image/svg+xml"}); const objectURL = URL.createObjectURL(blob); objectURLs.add(objectURL); return objectURL; @@ -2542,7 +2550,7 @@ for (let i = 0; i < characters.length; i++) { bytes[i] = characters.charCodeAt(i); } - return new Blob([bytes], { type: mediaType }); + return new Blob([bytes], {type: mediaType}); } async function tryConvertDataURLToBlobURL(dataURL) { if (!isBlobURLSupported) { @@ -2571,2156 +2579,1891 @@ dataURLBlobURLs.clear(); } - const gradientLength = "gradient".length; - const conicGradient = "conic-"; - const conicGradientLength = conicGradient.length; - const radialGradient = "radial-"; - const linearGradient = "linear-"; - function parseGradient(value) { - const result = []; - let index = 0; - let startIndex = conicGradient.length; - while ((index = value.indexOf("gradient", startIndex)) !== -1) { - let typeGradient; - [linearGradient, radialGradient, conicGradient].find( - (possibleType) => { - if (index - possibleType.length >= 0) { - const possibleGradient = value.substring( - index - possibleType.length, - index - ); - if (possibleGradient === possibleType) { - if ( - value.slice( - index - possibleType.length - 10, - index - possibleType.length - 1 - ) === "repeating" - ) { - typeGradient = `repeating-${possibleType}gradient`; - return true; - } - if ( - value.slice( - index - possibleType.length - 8, - index - possibleType.length - 1 - ) === "-webkit" - ) { - typeGradient = `-webkit-${possibleType}gradient`; - return true; - } - typeGradient = `${possibleType}gradient`; - return true; - } - } + let variablesSheet; + const registeredColors = new Map(); + function registerVariablesSheet(sheet) { + variablesSheet = sheet; + const types = ["background", "text", "border"]; + registeredColors.forEach((registered) => { + types.forEach((type) => { + if (registered[type]) { + const {variable, value} = registered[type]; + variablesSheet?.cssRules[0].style.setProperty( + variable, + value + ); } - ); - if (!typeGradient) { - break; - } - const { start, end } = getParenthesesRange( - value, - index + gradientLength - ); - const match = value.substring(start + 1, end - 1); - startIndex = end + 1 + conicGradientLength; - result.push({ - typeGradient, - match, - offset: typeGradient.length + 2, - index: index - typeGradient.length + gradientLength, - hasComma: true }); + }); + } + function releaseVariablesSheet() { + variablesSheet = null; + clearColorPalette(); + } + function getRegisteredVariableValue(type, registered) { + return `var(${registered[type].variable}, ${registered[type].value})`; + } + function getRegisteredColor(type, parsed) { + const hex = rgbToHexString(parsed); + const registered = registeredColors.get(hex); + if (registered?.[type]) { + return getRegisteredVariableValue(type, registered); } - if (result.length) { - result[result.length - 1].hasComma = false; + return null; + } + function registerColor(type, parsed, value) { + const hex = rgbToHexString(parsed); + let registered; + if (registeredColors.has(hex)) { + registered = registeredColors.get(hex); + } else { + const parsed = parseColorWithCache(hex); + registered = {parsed}; + registeredColors.set(hex, registered); } - return result; + const variable = `--darkreader-${type}-${hex.replace("#", "")}`; + registered[type] = {variable, value}; + if (variablesSheet?.cssRules[0]?.style) { + variablesSheet?.cssRules[0].style.setProperty(variable, value); + } + return getRegisteredVariableValue(type, registered); + } + function getColorPalette() { + const background = []; + const border = []; + const text = []; + registeredColors.forEach((registered) => { + if (registered.background) { + background.push(registered.parsed); + } + if (registered.border) { + border.push(registered.parsed); + } + if (registered.text) { + text.push(registered.parsed); + } + }); + return {background, border, text}; + } + function clearColorPalette() { + registeredColors.clear(); } - function getPriority(ruleStyle, property) { - return Boolean(ruleStyle && ruleStyle.getPropertyPriority(property)); + function getBgPole(theme) { + const isDarkScheme = theme.mode === 1; + const prop = isDarkScheme + ? "darkSchemeBackgroundColor" + : "lightSchemeBackgroundColor"; + return theme[prop]; } - function getModifiableCSSDeclaration( - property, - value, - rule, - variablesStore, - ignoreImageSelectors, - isCancelled - ) { - let modifier = null; - if (property.startsWith("--")) { - modifier = getVariableModifier( - variablesStore, - property, - value, - rule, - ignoreImageSelectors, - isCancelled - ); - } else if (value.includes("var(")) { - modifier = getVariableDependantModifier( - variablesStore, - property, - value - ); - } else if (property === "color-scheme") { - modifier = getColorSchemeModifier(); - } else if (property === "scrollbar-color") { - modifier = getScrollbarColorModifier(value); - } else if ( - (property.includes("color") && - property !== "-webkit-print-color-adjust") || - property === "fill" || - property === "stroke" || - property === "stop-color" - ) { - if ( - property.startsWith("border") && - property !== "border-color" && - value === "initial" - ) { - const borderSideProp = property.substring( - 0, - property.length - 6 - ); - const borderSideVal = - rule.style.getPropertyValue(borderSideProp); - if ( - borderSideVal.startsWith("0px") || - borderSideVal === "none" - ) { - property = borderSideProp; - modifier = borderSideVal; - } else { - modifier = value; - } - } else { - modifier = getColorModifier(property, value, rule); - } - } else if ( - property === "background-image" || - property === "list-style-image" - ) { - modifier = getBgImageModifier( - value, - rule, - ignoreImageSelectors, - isCancelled - ); - } else if (property.includes("shadow")) { - modifier = getShadowModifier(value); - } - if (!modifier) { - return null; - } - return { - property, - value: modifier, - important: getPriority(rule.style, property), - sourceValue: value - }; + function getFgPole(theme) { + const isDarkScheme = theme.mode === 1; + const prop = isDarkScheme + ? "darkSchemeTextColor" + : "lightSchemeTextColor"; + return theme[prop]; } - function joinSelectors(...selectors) { - return selectors.filter(Boolean).join(", "); + const colorModificationCache = new Map(); + function clearColorModificationCache() { + colorModificationCache.clear(); } - function getModifiedUserAgentStyle(theme, isIFrame, styleSystemControls) { - const lines = []; - if (!isIFrame) { - lines.push("html {"); - lines.push( - ` background-color: ${modifyBackgroundColor({ r: 255, g: 255, b: 255 }, theme)} !important;` - ); - lines.push("}"); - } - if (isCSSColorSchemePropSupported && theme.mode === 1) { - lines.push("html {"); - lines.push(` color-scheme: dark !important;`); - lines.push("}"); - lines.push("iframe {"); - lines.push(` color-scheme: initial;`); - lines.push("}"); - } - const bgSelectors = joinSelectors( - isIFrame ? "" : "html, body", - styleSystemControls ? "input, textarea, select, button, dialog" : "" - ); - if (bgSelectors) { - lines.push(`${bgSelectors} {`); - lines.push( - ` background-color: ${modifyBackgroundColor({ r: 255, g: 255, b: 255 }, theme)};` - ); - lines.push("}"); - } - lines.push( - `${joinSelectors("html, body", styleSystemControls ? "input, textarea, select, button" : "")} {` - ); - lines.push( - ` border-color: ${modifyBorderColor({ r: 76, g: 76, b: 76 }, theme)};` - ); - lines.push( - ` color: ${modifyForegroundColor({ r: 0, g: 0, b: 0 }, theme)};` - ); - lines.push("}"); - lines.push("a {"); - lines.push( - ` color: ${modifyForegroundColor({ r: 0, g: 64, b: 255 }, theme)};` - ); - lines.push("}"); - lines.push("table {"); - lines.push( - ` border-color: ${modifyBorderColor({ r: 128, g: 128, b: 128 }, theme)};` - ); - lines.push("}"); - lines.push("mark {"); - lines.push( - ` color: ${modifyForegroundColor({ r: 0, g: 0, b: 0 }, theme)};` - ); - lines.push("}"); - lines.push("::placeholder {"); - lines.push( - ` color: ${modifyForegroundColor({ r: 169, g: 169, b: 169 }, theme)};` - ); - lines.push("}"); - lines.push("input:-webkit-autofill,"); - lines.push("textarea:-webkit-autofill,"); - lines.push("select:-webkit-autofill {"); - lines.push( - ` background-color: ${modifyBackgroundColor({ r: 250, g: 255, b: 189 }, theme)} !important;` - ); - lines.push( - ` color: ${modifyForegroundColor({ r: 0, g: 0, b: 0 }, theme)} !important;` - ); - lines.push("}"); - if (theme.scrollbarColor) { - lines.push(getModifiedScrollbarStyle(theme)); - } - if (theme.selectionColor) { - lines.push(getModifiedSelectionStyle(theme)); + const rgbCacheKeys = ["r", "g", "b", "a"]; + const themeCacheKeys$1 = [ + "mode", + "brightness", + "contrast", + "grayscale", + "sepia", + "darkSchemeBackgroundColor", + "darkSchemeTextColor", + "lightSchemeBackgroundColor", + "lightSchemeTextColor" + ]; + function getCacheId(rgb, theme) { + let resultId = ""; + rgbCacheKeys.forEach((key) => { + resultId += `${rgb[key]};`; + }); + themeCacheKeys$1.forEach((key) => { + resultId += `${theme[key]};`; + }); + return resultId; + } + function modifyColorWithCache( + rgb, + theme, + modifyHSL, + poleColor, + anotherPoleColor + ) { + let fnCache; + if (colorModificationCache.has(modifyHSL)) { + fnCache = colorModificationCache.get(modifyHSL); + } else { + fnCache = new Map(); + colorModificationCache.set(modifyHSL, fnCache); } - if (isLayerRuleSupported) { - lines.unshift("@layer {"); - lines.push("}"); + const id = getCacheId(rgb, theme); + if (fnCache.has(id)) { + return fnCache.get(id); } - return lines.join("\n"); + const hsl = rgbToHSL(rgb); + const pole = poleColor == null ? null : parseToHSLWithCache(poleColor); + const anotherPole = + anotherPoleColor == null + ? null + : parseToHSLWithCache(anotherPoleColor); + const modified = modifyHSL(hsl, pole, anotherPole); + const {r, g, b, a} = hslToRGB(modified); + const matrix = createFilterMatrix(theme); + const [rf, gf, bf] = applyColorMatrix([r, g, b], matrix); + const color = + a === 1 + ? rgbToHexString({r: rf, g: gf, b: bf}) + : rgbToString({r: rf, g: gf, b: bf, a}); + fnCache.set(id, color); + return color; } - function getSelectionColor(theme) { - let backgroundColorSelection; - let foregroundColorSelection; - if (theme.selectionColor === "auto") { - backgroundColorSelection = modifyBackgroundColor( - { r: 0, g: 96, b: 212 }, - { ...theme, grayscale: 0 } - ); - foregroundColorSelection = modifyForegroundColor( - { r: 255, g: 255, b: 255 }, - { ...theme, grayscale: 0 } - ); - } else { - const rgb = parseColorWithCache(theme.selectionColor); - const hsl = rgbToHSL(rgb); - backgroundColorSelection = theme.selectionColor; - if (hsl.l < 0.5) { - foregroundColorSelection = "#FFF"; - } else { - foregroundColorSelection = "#000"; - } + function modifyAndRegisterColor(type, rgb, theme, modifier) { + const registered = getRegisteredColor(type, rgb); + if (registered) { + return registered; } - return { backgroundColorSelection, foregroundColorSelection }; + const value = modifier(rgb, theme); + return registerColor(type, rgb, value); } - function getModifiedSelectionStyle(theme) { - const lines = []; - const modifiedSelectionColor = getSelectionColor(theme); - const backgroundColorSelection = - modifiedSelectionColor.backgroundColorSelection; - const foregroundColorSelection = - modifiedSelectionColor.foregroundColorSelection; - ["::selection", "::-moz-selection"].forEach((selection) => { - lines.push(`${selection} {`); - lines.push( - ` background-color: ${backgroundColorSelection} !important;` - ); - lines.push(` color: ${foregroundColorSelection} !important;`); - lines.push("}"); - }); - return lines.join("\n"); + function modifyLightSchemeColor(rgb, theme) { + const poleBg = getBgPole(theme); + const poleFg = getFgPole(theme); + return modifyColorWithCache( + rgb, + theme, + modifyLightModeHSL, + poleFg, + poleBg + ); } - function getModifiedScrollbarStyle(theme) { - const lines = []; - let colorTrack; - let colorIcons; - let colorThumb; - let colorThumbHover; - let colorThumbActive; - let colorCorner; - if (theme.scrollbarColor === "auto") { - colorTrack = modifyBackgroundColor({ r: 241, g: 241, b: 241 }, theme); - colorIcons = modifyForegroundColor({ r: 96, g: 96, b: 96 }, theme); - colorThumb = modifyBackgroundColor({ r: 176, g: 176, b: 176 }, theme); - colorThumbHover = modifyBackgroundColor( - { r: 144, g: 144, b: 144 }, - theme - ); - colorThumbActive = modifyBackgroundColor( - { r: 96, g: 96, b: 96 }, - theme - ); - colorCorner = modifyBackgroundColor( - { r: 255, g: 255, b: 255 }, - theme - ); + function modifyLightModeHSL({h, s, l, a}, poleFg, poleBg) { + const isDark = l < 0.5; + let isNeutral; + if (isDark) { + isNeutral = l < 0.2 || s < 0.12; } else { - const rgb = parseColorWithCache(theme.scrollbarColor); - const hsl = rgbToHSL(rgb); - const isLight = hsl.l > 0.5; - const lighten = (lighter) => ({ - ...hsl, - l: clamp(hsl.l + lighter, 0, 1) - }); - const darken = (darker) => ({ - ...hsl, - l: clamp(hsl.l - darker, 0, 1) - }); - colorTrack = hslToString(darken(0.4)); - colorIcons = hslToString(isLight ? darken(0.4) : lighten(0.4)); - colorThumb = hslToString(hsl); - colorThumbHover = hslToString(lighten(0.1)); - colorThumbActive = hslToString(lighten(0.2)); - colorCorner = hslToString(darken(0.5)); + const isBlue = h > 200 && h < 280; + isNeutral = s < 0.24 || (l > 0.8 && isBlue); } - lines.push("::-webkit-scrollbar {"); - lines.push(` background-color: ${colorTrack};`); - lines.push(` color: ${colorIcons};`); - lines.push("}"); - lines.push("::-webkit-scrollbar-thumb {"); - lines.push(` background-color: ${colorThumb};`); - lines.push("}"); - lines.push("::-webkit-scrollbar-thumb:hover {"); - lines.push(` background-color: ${colorThumbHover};`); - lines.push("}"); - lines.push("::-webkit-scrollbar-thumb:active {"); - lines.push(` background-color: ${colorThumbActive};`); - lines.push("}"); - lines.push("::-webkit-scrollbar-corner {"); - lines.push(` background-color: ${colorCorner};`); - lines.push("}"); - if (isFirefox) { - lines.push("* {"); - lines.push(` scrollbar-color: ${colorThumb} ${colorTrack};`); - lines.push("}"); + let hx = h; + let sx = l; + if (isNeutral) { + if (isDark) { + hx = poleFg.h; + sx = poleFg.s; + } else { + hx = poleBg.h; + sx = poleBg.s; + } } - return lines.join("\n"); - } - function getModifiedFallbackStyle(theme, { strict }) { - const factory = defaultFallbackFactory; - return factory(theme, { strict }); - } - function defaultFallbackFactory(theme, { strict }) { - const lines = []; - lines.push( - `html, body, ${strict ? "body :not(iframe)" : "body > :not(iframe)"} {` - ); - lines.push( - ` background-color: ${modifyBackgroundColor({ r: 255, g: 255, b: 255 }, theme)} !important;` - ); - lines.push( - ` border-color: ${modifyBorderColor({ r: 64, g: 64, b: 64 }, theme)} !important;` - ); - lines.push( - ` color: ${modifyForegroundColor({ r: 0, g: 0, b: 0 }, theme)} !important;` - ); - lines.push("}"); - lines.push(`div[style*="background-color: rgb(135, 135, 135)"] {`); - lines.push(` background-color: #878787 !important;`); - lines.push("}"); - return lines.join("\n"); + const lx = scale(l, 0, 1, poleFg.l, poleBg.l); + return {h: hx, s: sx, l: lx, a}; } - const unparsableColors = new Set([ - "inherit", - "transparent", - "initial", - "currentcolor", - "none", - "unset", - "auto" - ]); - function getColorModifier(prop, value, rule) { - if (unparsableColors.has(value.toLowerCase())) { - return value; + const MAX_BG_LIGHTNESS = 0.4; + function modifyBgHSL({h, s, l, a}, pole) { + const isDark = l < 0.5; + const isBlue = h > 200 && h < 280; + const isNeutral = s < 0.12 || (l > 0.8 && isBlue); + if (isDark) { + const lx = scale(l, 0, 0.5, 0, MAX_BG_LIGHTNESS); + if (isNeutral) { + const hx = pole.h; + const sx = pole.s; + return {h: hx, s: sx, l: lx, a}; + } + return {h, s, l: lx, a}; } - const rgb = parseColorWithCache(value); - if (!rgb) { - return null; + let lx = scale(l, 0.5, 1, MAX_BG_LIGHTNESS, pole.l); + if (isNeutral) { + const hx = pole.h; + const sx = pole.s; + return {h: hx, s: sx, l: lx, a}; } - if (prop.includes("background")) { - if ( - (rule.style.webkitMaskImage && - rule.style.webkitMaskImage !== "none") || - (rule.style.webkitMask && - !rule.style.webkitMask.startsWith("none")) || - (rule.style.mask && rule.style.mask !== "none") || - (rule.style.getPropertyValue("mask-image") && - rule.style.getPropertyValue("mask-image") !== "none") - ) { - return (theme) => modifyForegroundColor(rgb, theme); + let hx = h; + const isYellow = h > 60 && h < 180; + if (isYellow) { + const isCloserToGreen = h > 120; + if (isCloserToGreen) { + hx = scale(h, 120, 180, 135, 180); + } else { + hx = scale(h, 60, 120, 60, 105); } - return (theme) => modifyBackgroundColor(rgb, theme); } - if (prop.includes("border") || prop.includes("outline")) { - return (theme) => modifyBorderColor(rgb, theme); + if (hx > 40 && hx < 80) { + lx *= 0.75; } - return (theme) => modifyForegroundColor(rgb, theme); + return {h: hx, s, l: lx, a}; } - const imageDetailsCache = new Map(); - const awaitingForImageLoading = new Map(); - function shouldIgnoreImage(selectorText, selectors) { - if (!selectorText || selectors.length === 0) { - return false; + function _modifyBackgroundColor(rgb, theme) { + if (theme.mode === 0) { + return modifyLightSchemeColor(rgb, theme); } - if (selectors.some((s) => s === "*")) { - return true; + const pole = getBgPole(theme); + return modifyColorWithCache( + rgb, + {...theme, mode: 0}, + modifyBgHSL, + pole + ); + } + function modifyBackgroundColor( + rgb, + theme, + shouldRegisterColorVariable = true + ) { + if (!shouldRegisterColorVariable) { + return _modifyBackgroundColor(rgb, theme); } - const ruleSelectors = selectorText.split(/,\s*/g); - for (let i = 0; i < selectors.length; i++) { - const ignoredSelector = selectors[i]; - if (ruleSelectors.some((s) => s === ignoredSelector)) { - return true; + return modifyAndRegisterColor( + "background", + rgb, + theme, + _modifyBackgroundColor + ); + } + const MIN_FG_LIGHTNESS = 0.55; + function modifyBlueFgHue(hue) { + return scale(hue, 205, 245, 205, 220); + } + function modifyFgHSL({h, s, l, a}, pole) { + const isLight = l > 0.5; + const isNeutral = l < 0.2 || s < 0.24; + const isBlue = !isNeutral && h > 205 && h < 245; + if (isLight) { + const lx = scale(l, 0.5, 1, MIN_FG_LIGHTNESS, pole.l); + if (isNeutral) { + const hx = pole.h; + const sx = pole.s; + return {h: hx, s: sx, l: lx, a}; + } + let hx = h; + if (isBlue) { + hx = modifyBlueFgHue(h); } + return {h: hx, s, l: lx, a}; } - return false; + if (isNeutral) { + const hx = pole.h; + const sx = pole.s; + const lx = scale(l, 0, 0.5, pole.l, MIN_FG_LIGHTNESS); + return {h: hx, s: sx, l: lx, a}; + } + let hx = h; + let lx; + if (isBlue) { + hx = modifyBlueFgHue(h); + lx = scale(l, 0, 0.5, pole.l, Math.min(1, MIN_FG_LIGHTNESS + 0.05)); + } else { + lx = scale(l, 0, 0.5, pole.l, MIN_FG_LIGHTNESS); + } + return {h: hx, s, l: lx, a}; } - function getBgImageModifier( + function _modifyForegroundColor(rgb, theme) { + if (theme.mode === 0) { + return modifyLightSchemeColor(rgb, theme); + } + const pole = getFgPole(theme); + return modifyColorWithCache( + rgb, + {...theme, mode: 0}, + modifyFgHSL, + pole + ); + } + function modifyForegroundColor( + rgb, + theme, + shouldRegisterColorVariable = true + ) { + if (!shouldRegisterColorVariable) { + return _modifyForegroundColor(rgb, theme); + } + return modifyAndRegisterColor( + "text", + rgb, + theme, + _modifyForegroundColor + ); + } + function modifyBorderHSL({h, s, l, a}, poleFg, poleBg) { + const isDark = l < 0.5; + const isNeutral = l < 0.2 || s < 0.24; + let hx = h; + let sx = s; + if (isNeutral) { + if (isDark) { + hx = poleFg.h; + sx = poleFg.s; + } else { + hx = poleBg.h; + sx = poleBg.s; + } + } + const lx = scale(l, 0, 1, 0.5, 0.2); + return {h: hx, s: sx, l: lx, a}; + } + function _modifyBorderColor(rgb, theme) { + if (theme.mode === 0) { + return modifyLightSchemeColor(rgb, theme); + } + const poleFg = getFgPole(theme); + const poleBg = getBgPole(theme); + return modifyColorWithCache( + rgb, + {...theme, mode: 0}, + modifyBorderHSL, + poleFg, + poleBg + ); + } + function modifyBorderColor(rgb, theme, shouldRegisterColorVariable = true) { + if (!shouldRegisterColorVariable) { + return _modifyBorderColor(rgb, theme); + } + return modifyAndRegisterColor("border", rgb, theme, _modifyBorderColor); + } + function modifyShadowColor(rgb, theme) { + return modifyBackgroundColor(rgb, theme); + } + function modifyGradientColor(rgb, theme) { + return modifyBackgroundColor(rgb, theme); + } + + function getPriority(ruleStyle, property) { + return Boolean(ruleStyle && ruleStyle.getPropertyPriority(property)); + } + function getModifiableCSSDeclaration( + property, value, rule, + variablesStore, ignoreImageSelectors, isCancelled ) { - try { - const gradients = parseGradient(value); - const urls = getMatches(cssURLRegex, value); - if (urls.length === 0 && gradients.length === 0) { - return value; - } - const getIndices = (matches) => { - let index = 0; - return matches.map((match) => { - const valueIndex = value.indexOf(match, index); - index = valueIndex + match.length; - return { match, index: valueIndex }; - }); - }; - const matches = gradients - .map((i) => ({ type: "gradient", ...i })) - .concat( - getIndices(urls).map((i) => ({ - type: "url", - offset: 0, - ...i - })) - ) - .sort((a, b) => (a.index > b.index ? 1 : -1)); - const getGradientModifier = (gradient) => { - const { typeGradient, match, hasComma } = gradient; - const partsRegex = - /([^\(\),]+(\([^\(\)]*(\([^\(\)]*\)*[^\(\)]*)?\))?([^\(\), ]|( (?!calc)))*),?/g; - const colorStopRegex = - /^(from|color-stop|to)\(([^\(\)]*?,\s*)?(.*?)\)$/; - const parts = getMatches(partsRegex, match, 1).map((part) => { - part = part.trim(); - let rgb = parseColorWithCache(part); - if (rgb) { - return (theme) => modifyGradientColor(rgb, theme); - } - const space = part.lastIndexOf(" "); - rgb = parseColorWithCache(part.substring(0, space)); - if (rgb) { - return (theme) => - `${modifyGradientColor(rgb, theme)} ${part.substring(space + 1)}`; - } - const colorStopMatch = part.match(colorStopRegex); - if (colorStopMatch) { - rgb = parseColorWithCache(colorStopMatch[3]); - if (rgb) { - return (theme) => - `${colorStopMatch[1]}(${colorStopMatch[2] ? `${colorStopMatch[2]}, ` : ""}${modifyGradientColor(rgb, theme)})`; - } - } - return () => part; - }); - return (theme) => { - return `${typeGradient}(${parts.map((modify) => modify(theme)).join(", ")})${hasComma ? ", " : ""}`; - }; - }; - const getURLModifier = (urlValue) => { + let modifier = null; + if (property.startsWith("--")) { + modifier = getVariableModifier( + variablesStore, + property, + value, + rule, + ignoreImageSelectors, + isCancelled + ); + } else if (value.includes("var(")) { + modifier = getVariableDependantModifier( + variablesStore, + property, + value + ); + } else if (property === "color-scheme") { + modifier = getColorSchemeModifier(); + } else if (property === "scrollbar-color") { + modifier = getScrollbarColorModifier(value); + } else if ( + (property.includes("color") && + property !== "-webkit-print-color-adjust") || + property === "fill" || + property === "stroke" || + property === "stop-color" + ) { + if ( + property.startsWith("border") && + property !== "border-color" && + value === "initial" + ) { + const borderSideProp = property.substring( + 0, + property.length - 6 + ); + const borderSideVal = + rule.style.getPropertyValue(borderSideProp); if ( - shouldIgnoreImage(rule.selectorText, ignoreImageSelectors) + borderSideVal.startsWith("0px") || + borderSideVal === "none" ) { - return null; - } - let url = getCSSURLValue(urlValue); - const isURLEmpty = url.length === 0; - const { parentStyleSheet } = rule; - const baseURL = - parentStyleSheet && parentStyleSheet.href - ? getCSSBaseBath(parentStyleSheet.href) - : parentStyleSheet?.ownerNode?.baseURI || - location.origin; - url = getAbsoluteURL(baseURL, url); - return async (theme) => { - if (isURLEmpty) { - return "url('')"; - } - let imageDetails = null; - if (imageDetailsCache.has(url)) { - imageDetails = imageDetailsCache.get(url); - } else { - try { - if (!isBlobURLCheckResultReady()) { - await requestBlobURLCheck(); - } - if (awaitingForImageLoading.has(url)) { - const awaiters = - awaitingForImageLoading.get(url); - imageDetails = await new Promise((resolve) => - awaiters.push(resolve) - ); - if (!imageDetails) { - return null; - } - } else { - awaitingForImageLoading.set(url, []); - imageDetails = await getImageDetails(url); - imageDetailsCache.set(url, imageDetails); - awaitingForImageLoading - .get(url) - .forEach((resolve) => - resolve(imageDetails) - ); - awaitingForImageLoading.delete(url); - } - if (isCancelled()) { - return null; - } - } catch (err) { - logWarn(err); - if (awaitingForImageLoading.has(url)) { - awaitingForImageLoading - .get(url) - .forEach((resolve) => resolve(null)); - awaitingForImageLoading.delete(url); - } - } - } - if (imageDetails) { - const bgImageValue = getBgImageValue( - imageDetails, - theme - ); - if (bgImageValue) { - return bgImageValue; - } - } - if (url.startsWith("data:")) { - const blobURL = await tryConvertDataURLToBlobURL(url); - if (blobURL) { - return `url("${blobURL}")`; - } - } - return `url("${url}")`; - }; - }; - const getBgImageValue = (imageDetails, theme) => { - const { isDark, isLight, isTransparent, isLarge, width } = - imageDetails; - let result; - const logSrc = imageDetails.src.startsWith("data:") - ? "data:" - : imageDetails.src; - if (isLarge && isLight && !isTransparent && theme.mode === 1) { - logInfo(`Hiding large light image ${logSrc}`); - result = "none"; - } else if ( - isDark && - isTransparent && - theme.mode === 1 && - width > 2 - ) { - logInfo(`Inverting dark image ${logSrc}`); - const inverted = getFilteredImageURL(imageDetails, { - ...theme, - sepia: clamp(theme.sepia + 10, 0, 100) - }); - result = `url("${inverted}")`; - } else if (isLight && !isTransparent && theme.mode === 1) { - logInfo(`Dimming light image ${logSrc}`); - const dimmed = getFilteredImageURL(imageDetails, theme); - result = `url("${dimmed}")`; - } else if (theme.mode === 0 && isLight) { - logInfo(`Applying filter to image ${logSrc}`); - const filtered = getFilteredImageURL(imageDetails, { - ...theme, - brightness: clamp(theme.brightness - 10, 5, 200), - sepia: clamp(theme.sepia + 10, 0, 100) - }); - result = `url("${filtered}")`; - } else { - logInfo(`Not modifying the image ${logSrc}`); - result = null; - } - return result; - }; - const modifiers = []; - let matchIndex = 0; - let prevHasComma = false; - matches.forEach( - ({ type, match, index, typeGradient, hasComma, offset }, i) => { - const matchStart = index; - const prefixStart = matchIndex; - const matchEnd = matchStart + match.length + offset; - matchIndex = matchEnd; - if (prefixStart !== matchStart) { - if (prevHasComma) { - modifiers.push(() => { - let betweenValue = value.substring( - prefixStart, - matchStart - ); - if (betweenValue[0] === ",") { - betweenValue = betweenValue.substring(1); - } - return betweenValue; - }); - } else { - modifiers.push(() => - value.substring(prefixStart, matchStart) - ); - } - } - prevHasComma = hasComma || false; - if (type === "url") { - modifiers.push(getURLModifier(match)); - } else if (type === "gradient") { - modifiers.push( - getGradientModifier({ - match, - index, - typeGradient: typeGradient, - hasComma: hasComma || false, - offset - }) - ); - } - if (i === matches.length - 1) { - modifiers.push(() => value.substring(matchEnd)); - } + property = borderSideProp; + modifier = borderSideVal; + } else { + modifier = value; } + } else { + modifier = getColorModifier(property, value, rule); + } + } else if ( + property === "background-image" || + property === "list-style-image" + ) { + modifier = getBgImageModifier( + value, + rule, + ignoreImageSelectors, + isCancelled ); - return (theme) => { - const results = modifiers - .filter(Boolean) - .map((modify) => modify(theme)); - if (results.some((r) => r instanceof Promise)) { - return Promise.all(results).then((asyncResults) => { - return asyncResults.filter(Boolean).join(""); - }); - } - const combinedResult = results.join(""); - if (combinedResult.endsWith(", initial")) { - return combinedResult.slice(0, -9); - } - return combinedResult; - }; - } catch (err) { + } else if (property.includes("shadow")) { + modifier = getShadowModifier(value); + } + if (!modifier) { return null; } + return { + property, + value: modifier, + important: getPriority(rule.style, property), + sourceValue: value + }; } - function getShadowModifierWithInfo(value) { - try { - let index = 0; - const colorMatches = getMatches( - /(^|\s)(?!calc)([a-z]+\(.+?\)|#[0-9a-f]+|[a-z]+)(.*?(inset|outset)?($|,))/gi, - value, - 2 + function joinSelectors(...selectors) { + return selectors.filter(Boolean).join(", "); + } + const hostsWithOddScrollbars = ["calendar.google.com"]; + function getModifiedUserAgentStyle(theme, isIFrame, styleSystemControls) { + const lines = []; + if (!isIFrame) { + lines.push("html {"); + lines.push( + ` background-color: ${modifyBackgroundColor({r: 255, g: 255, b: 255}, theme)} !important;` ); - let notParsed = 0; - const modifiers = colorMatches.map((match, i) => { - const prefixIndex = index; - const matchIndex = value.indexOf(match, index); - const matchEnd = matchIndex + match.length; - index = matchEnd; - const rgb = parseColorWithCache(match); - if (!rgb) { - notParsed++; - return () => value.substring(prefixIndex, matchEnd); - } - return (theme) => - `${value.substring(prefixIndex, matchIndex)}${modifyShadowColor(rgb, theme)}${i === colorMatches.length - 1 ? value.substring(matchEnd) : ""}`; - }); - return (theme) => { - const modified = modifiers - .map((modify) => modify(theme)) - .join(""); - return { - matchesLength: colorMatches.length, - unparseableMatchesLength: notParsed, - result: modified - }; - }; - } catch (err) { - return null; + lines.push("}"); } - } - function getShadowModifier(value) { - const shadowModifier = getShadowModifierWithInfo(value); - if (!shadowModifier) { - return null; + if (isCSSColorSchemePropSupported && theme.mode === 1) { + lines.push("html {"); + lines.push(` color-scheme: dark !important;`); + lines.push("}"); + lines.push("iframe {"); + lines.push(` color-scheme: dark !important;`); + lines.push("}"); } - return (theme) => shadowModifier(theme).result; - } - function getScrollbarColorModifier(value) { - const colorsMatch = value.match( - /^\s*([a-z]+(\(.*\))?)\s+([a-z]+(\(.*\))?)\s*$/ + const bgSelectors = joinSelectors( + isIFrame ? "" : "html, body", + styleSystemControls ? "input, textarea, select, button, dialog" : "" ); - if (!colorsMatch) { - return value; - } - const thumb = parseColorWithCache(colorsMatch[1]); - const track = parseColorWithCache(colorsMatch[3]); - if (!thumb || !track) { - return null; + if (bgSelectors) { + lines.push(`${bgSelectors} {`); + lines.push( + ` background-color: ${modifyBackgroundColor({r: 255, g: 255, b: 255}, theme)};` + ); + lines.push("}"); } - return (theme) => - `${modifyForegroundColor(thumb, theme)} ${modifyBackgroundColor(thumb, theme)}`; - } - function getColorSchemeModifier() { - return (theme) => (theme.mode === 0 ? "dark light" : "dark"); - } - function getVariableModifier( - variablesStore, - prop, - value, - rule, - ignoredImgSelectors, - isCancelled - ) { - return variablesStore.getModifierForVariable({ - varName: prop, - sourceValue: value, - rule, - ignoredImgSelectors, - isCancelled - }); - } - function getVariableDependantModifier(variablesStore, prop, value) { - return variablesStore.getModifierForVarDependant(prop, value); - } - function cleanModificationCache() { - clearColorModificationCache(); - imageDetailsCache.clear(); - cleanImageProcessingCache(); - awaitingForImageLoading.clear(); - } - - const VAR_TYPE_BGCOLOR = 1 << 0; - const VAR_TYPE_TEXTCOLOR = 1 << 1; - const VAR_TYPE_BORDERCOLOR = 1 << 2; - const VAR_TYPE_BGIMG = 1 << 3; - class VariablesStore { - constructor() { - this.varTypes = new Map(); - this.rulesQueue = new Set(); - this.inlineStyleQueue = []; - this.definedVars = new Set(); - this.varRefs = new Map(); - this.unknownColorVars = new Set(); - this.unknownBgVars = new Set(); - this.undefinedVars = new Set(); - this.initialVarTypes = new Map(); - this.changedTypeVars = new Set(); - this.typeChangeSubscriptions = new Map(); - this.unstableVarValues = new Map(); + lines.push( + `${joinSelectors("html, body", styleSystemControls ? "input, textarea, select, button" : "")} {` + ); + lines.push( + ` border-color: ${modifyBorderColor({r: 76, g: 76, b: 76}, theme)};` + ); + lines.push( + ` color: ${modifyForegroundColor({r: 0, g: 0, b: 0}, theme)};` + ); + lines.push("}"); + lines.push("a {"); + lines.push( + ` color: ${modifyForegroundColor({r: 0, g: 64, b: 255}, theme)};` + ); + lines.push("}"); + lines.push("table {"); + lines.push( + ` border-color: ${modifyBorderColor({r: 128, g: 128, b: 128}, theme)};` + ); + lines.push("}"); + lines.push("mark {"); + lines.push( + ` color: ${modifyForegroundColor({r: 0, g: 0, b: 0}, theme)};` + ); + lines.push("}"); + lines.push("::placeholder {"); + lines.push( + ` color: ${modifyForegroundColor({r: 169, g: 169, b: 169}, theme)};` + ); + lines.push("}"); + lines.push("input:-webkit-autofill,"); + lines.push("textarea:-webkit-autofill,"); + lines.push("select:-webkit-autofill {"); + lines.push( + ` background-color: ${modifyBackgroundColor({r: 250, g: 255, b: 189}, theme)} !important;` + ); + lines.push( + ` color: ${modifyForegroundColor({r: 0, g: 0, b: 0}, theme)} !important;` + ); + lines.push("}"); + if ( + theme.scrollbarColor && + !hostsWithOddScrollbars.includes(location.hostname) + ) { + lines.push(getModifiedScrollbarStyle(theme)); } - clear() { - this.varTypes.clear(); - this.rulesQueue.clear(); - this.inlineStyleQueue.splice(0); - this.definedVars.clear(); - this.varRefs.clear(); - this.unknownColorVars.clear(); - this.unknownBgVars.clear(); - this.undefinedVars.clear(); - this.initialVarTypes.clear(); - this.changedTypeVars.clear(); - this.typeChangeSubscriptions.clear(); - this.unstableVarValues.clear(); + if (theme.selectionColor) { + lines.push(getModifiedSelectionStyle(theme)); } - isVarType(varName, typeNum) { - return ( - this.varTypes.has(varName) && - (this.varTypes.get(varName) & typeNum) > 0 + if (isLayerRuleSupported) { + lines.unshift("@layer {"); + lines.push("}"); + } + return lines.join("\n"); + } + function getSelectionColor(theme) { + let backgroundColorSelection; + let foregroundColorSelection; + if (theme.selectionColor === "auto") { + backgroundColorSelection = modifyBackgroundColor( + {r: 0, g: 96, b: 212}, + {...theme, grayscale: 0} + ); + foregroundColorSelection = modifyForegroundColor( + {r: 255, g: 255, b: 255}, + {...theme, grayscale: 0} ); + } else { + const rgb = parseColorWithCache(theme.selectionColor); + const hsl = rgbToHSL(rgb); + backgroundColorSelection = theme.selectionColor; + if (hsl.l < 0.5) { + foregroundColorSelection = "#FFF"; + } else { + foregroundColorSelection = "#000"; + } } - addRulesForMatching(rules) { - this.rulesQueue.add(rules); + return {backgroundColorSelection, foregroundColorSelection}; + } + function getModifiedSelectionStyle(theme) { + const lines = []; + const modifiedSelectionColor = getSelectionColor(theme); + const backgroundColorSelection = + modifiedSelectionColor.backgroundColorSelection; + const foregroundColorSelection = + modifiedSelectionColor.foregroundColorSelection; + ["::selection", "::-moz-selection"].forEach((selection) => { + lines.push(`${selection} {`); + lines.push( + ` background-color: ${backgroundColorSelection} !important;` + ); + lines.push(` color: ${foregroundColorSelection} !important;`); + lines.push("}"); + }); + return lines.join("\n"); + } + function getModifiedScrollbarStyle(theme) { + let colorTrack; + let colorThumb; + if (theme.scrollbarColor === "auto") { + colorTrack = modifyBackgroundColor({r: 241, g: 241, b: 241}, theme); + colorThumb = modifyBackgroundColor({r: 176, g: 176, b: 176}, theme); + } else { + const rgb = parseColorWithCache(theme.scrollbarColor); + const hsl = rgbToHSL(rgb); + const darken = (darker) => ({ + ...hsl, + l: clamp(hsl.l - darker, 0, 1) + }); + colorTrack = hslToString(darken(0.4)); + colorThumb = hslToString(hsl); } - addInlineStyleForMatching(style) { - this.inlineStyleQueue.push(style); + return [ + `* {`, + ` scrollbar-color: ${colorThumb} ${colorTrack};`, + `}` + ].join("\n"); + } + function getModifiedFallbackStyle(theme, {strict}) { + const factory = defaultFallbackFactory; + return factory(theme, {strict}); + } + function defaultFallbackFactory(theme, {strict}) { + const lines = []; + lines.push( + `html, body, ${strict ? "body :not(iframe)" : "body > :not(iframe)"} {` + ); + lines.push( + ` background-color: ${modifyBackgroundColor({r: 255, g: 255, b: 255}, theme)} !important;` + ); + lines.push( + ` border-color: ${modifyBorderColor({r: 64, g: 64, b: 64}, theme)} !important;` + ); + lines.push( + ` color: ${modifyForegroundColor({r: 0, g: 0, b: 0}, theme)} !important;` + ); + lines.push("}"); + lines.push(`div[style*="background-color: rgb(135, 135, 135)"] {`); + lines.push(` background-color: #878787 !important;`); + lines.push("}"); + return lines.join("\n"); + } + const unparsableColors = new Set([ + "inherit", + "transparent", + "initial", + "currentcolor", + "none", + "unset", + "auto" + ]); + function getColorModifier(prop, value, rule) { + if (unparsableColors.has(value.toLowerCase())) { + return value; } - matchVariablesAndDependents() { + const rgb = parseColorWithCache(value); + if (!rgb) { + return null; + } + if (prop.includes("background")) { if ( - this.rulesQueue.size === 0 && - this.inlineStyleQueue.length === 0 + (rule.style.webkitMaskImage && + rule.style.webkitMaskImage !== "none") || + (rule.style.webkitMask && + !rule.style.webkitMask.startsWith("none")) || + (rule.style.mask && rule.style.mask !== "none") || + (rule.style.getPropertyValue("mask-image") && + rule.style.getPropertyValue("mask-image") !== "none") ) { - return; + return (theme) => modifyForegroundColor(rgb, theme); } - this.changedTypeVars.clear(); - this.initialVarTypes = new Map(this.varTypes); - this.collectRootVariables(); - this.collectVariablesAndVarDep(); - this.collectRootVarDependents(); - this.varRefs.forEach((refs, v) => { - refs.forEach((r) => { - if (this.varTypes.has(v)) { - this.resolveVariableType(r, this.varTypes.get(v)); + return (theme) => modifyBackgroundColor(rgb, theme); + } + if (prop.includes("border") || prop.includes("outline")) { + return (theme) => modifyBorderColor(rgb, theme); + } + return (theme) => modifyForegroundColor(rgb, theme); + } + const imageDetailsCache = new Map(); + const awaitingForImageLoading = new Map(); + let didTryLoadCache = false; + function shouldIgnoreImage(selectorText, selectors) { + if (!selectorText || selectors.length === 0) { + return false; + } + if (selectors.some((s) => s === "*")) { + return true; + } + const ruleSelectors = selectorText.split(/,\s*/g); + for (let i = 0; i < selectors.length; i++) { + const ignoredSelector = selectors[i]; + if (ignoredSelector.startsWith("^")) { + const beginning = ignoredSelector.slice(1); + if (ruleSelectors.some((s) => s.startsWith(beginning))) { + return true; + } + } else if (ignoredSelector.endsWith("$")) { + const ending = ignoredSelector.slice( + 0, + ignoredSelector.length - 1 + ); + if (ruleSelectors.some((s) => s.endsWith(ending))) { + return true; + } + } else if (ruleSelectors.some((s) => s === ignoredSelector)) { + return true; + } + } + return false; + } + const imageSelectorQueue = new Map(); + const imageSelectorValues = new Map(); + const imageSelectorNodeQueue = new Set(); + let imageSelectorQueueFrameId = null; + let classObserver = null; + function checkImageSelectors(node) { + for (const [selector, callbacks] of imageSelectorQueue) { + if ( + node.querySelector(selector) || + (node instanceof Element && node.matches(selector)) + ) { + imageSelectorQueue.delete(selector); + callbacks.forEach((cb) => cb()); + } + } + if (!classObserver) { + classObserver = new MutationObserver((mutations) => { + mutations.forEach((mutation) => { + imageSelectorNodeQueue.add(mutation.target); + if (!imageSelectorQueueFrameId) { + imageSelectorQueueFrameId = requestAnimationFrame( + () => { + imageSelectorNodeQueue.forEach((element) => { + checkImageSelectors(element); + }); + imageSelectorNodeQueue.clear(); + imageSelectorQueueFrameId = null; + } + ); } }); }); - this.unknownColorVars.forEach((v) => { - if (this.unknownBgVars.has(v)) { - this.unknownColorVars.delete(v); - this.unknownBgVars.delete(v); - this.resolveVariableType(v, VAR_TYPE_BGCOLOR); - } else if ( - this.isVarType( - v, - VAR_TYPE_BGCOLOR | - VAR_TYPE_TEXTCOLOR | - VAR_TYPE_BORDERCOLOR - ) - ) { - this.unknownColorVars.delete(v); - } else { - this.undefinedVars.add(v); - } - }); - this.unknownBgVars.forEach((v) => { - const hasColor = - this.findVarRef(v, (ref) => { - return ( - this.unknownColorVars.has(ref) || - this.isVarType( - ref, - VAR_TYPE_BGCOLOR | - VAR_TYPE_TEXTCOLOR | - VAR_TYPE_BORDERCOLOR - ) - ); - }) != null; - if (hasColor) { - this.iterateVarRefs(v, (ref) => { - this.resolveVariableType(ref, VAR_TYPE_BGCOLOR); - }); - } else if ( - this.isVarType(v, VAR_TYPE_BGCOLOR | VAR_TYPE_BGIMG) - ) { - this.unknownBgVars.delete(v); - } else { - this.undefinedVars.add(v); - } + classObserver.observe(document.documentElement, { + attributes: true, + attributeFilter: ["class"], + subtree: true }); - this.changedTypeVars.forEach((varName) => { - if (this.typeChangeSubscriptions.has(varName)) { - this.typeChangeSubscriptions - .get(varName) - .forEach((callback) => { - callback(); - }); - } - }); - this.changedTypeVars.clear(); } - getModifierForVariable(options) { - return (theme) => { - const { - varName, - sourceValue, - rule, - ignoredImgSelectors, - isCancelled - } = options; - const getDeclarations = () => { - const declarations = []; - const addModifiedValue = ( - typeNum, - varNameWrapper, - colorModifier - ) => { - if (!this.isVarType(varName, typeNum)) { - return; + } + function getBgImageModifier( + value, + rule, + ignoreImageSelectors, + isCancelled + ) { + try { + if (shouldIgnoreImage(rule.selectorText, ignoreImageSelectors)) { + return value; + } + const gradients = parseGradient(value); + const urls = getMatches(cssURLRegex, value); + if (urls.length === 0 && gradients.length === 0) { + return value; + } + const getIndices = (matches) => { + let index = 0; + return matches.map((match) => { + const valueIndex = value.indexOf(match, index); + index = valueIndex + match.length; + return {match, index: valueIndex}; + }); + }; + const matches = gradients + .map((i) => ({type: "gradient", ...i})) + .concat( + getIndices(urls).map((i) => ({ + type: "url", + offset: 0, + ...i + })) + ) + .sort((a, b) => (a.index > b.index ? 1 : -1)); + const getGradientModifier = (gradient) => { + const {typeGradient, match, hasComma} = gradient; + const partsRegex = + /([^\(\),]+(\([^\(\)]*(\([^\(\)]*\)*[^\(\)]*)?\))?([^\(\), ]|( (?!calc)))*),?/g; + const colorStopRegex = + /^(from|color-stop|to)\(([^\(\)]*?,\s*)?(.*?)\)$/; + const parts = getMatches(partsRegex, match, 1).map((part) => { + part = part.trim(); + let rgb = parseColorWithCache(part); + if (rgb) { + return (theme) => modifyGradientColor(rgb, theme); + } + const space = part.lastIndexOf(" "); + rgb = parseColorWithCache(part.substring(0, space)); + if (rgb) { + return (theme) => + `${modifyGradientColor(rgb, theme)} ${part.substring(space + 1)}`; + } + const colorStopMatch = part.match(colorStopRegex); + if (colorStopMatch) { + rgb = parseColorWithCache(colorStopMatch[3]); + if (rgb) { + return (theme) => + `${colorStopMatch[1]}(${colorStopMatch[2] ? `${colorStopMatch[2]}, ` : ""}${modifyGradientColor(rgb, theme)})`; } - const property = varNameWrapper(varName); - let modifiedValue; - if (isVarDependant(sourceValue)) { - if (isConstructedColorVar(sourceValue)) { - let value = insertVarValues( - sourceValue, - this.unstableVarValues + } + return () => part; + }); + return (theme) => { + return `${typeGradient}(${parts.map((modify) => modify(theme)).join(", ")})${hasComma ? ", " : ""}`; + }; + }; + const getURLModifier = (urlValue) => { + if (!didTryLoadCache) { + didTryLoadCache = true; + readImageDetailsCache(imageDetailsCache); + } + let url = getCSSURLValue(urlValue); + const isURLEmpty = url.length === 0; + const {parentStyleSheet} = rule; + const ownerNode = parentStyleSheet?.ownerNode; + const scope = + (parentStyleSheet && getSheetScope(parentStyleSheet)) ?? + document; + const baseURL = + parentStyleSheet && parentStyleSheet.href + ? getCSSBaseBath(parentStyleSheet.href) + : ownerNode?.baseURI || location.origin; + url = getAbsoluteURL(baseURL, url); + return async (theme) => { + if (isURLEmpty) { + return "url('')"; + } + let selector = rule.selectorText; + if (selector) { + if (selector.includes("::before")) { + selector = selector.replaceAll("::before", ""); + } + if (selector.includes("::after")) { + selector = selector.replaceAll("::after", ""); + } + if (!scope.querySelector(selector)) { + await new Promise((resolve) => { + if (imageSelectorQueue.has(selector)) { + imageSelectorQueue + .get(selector) + .push(resolve); + } else { + imageSelectorQueue.set(selector, [resolve]); + imageSelectorValues.set(selector, urlValue); + } + }); + } + } + let imageDetails = null; + if (imageDetailsCache.has(url)) { + imageDetails = imageDetailsCache.get(url); + } else { + try { + if (!isBlobURLCheckResultReady()) { + await requestBlobURLCheck(); + } + if (awaitingForImageLoading.has(url)) { + const awaiters = + awaitingForImageLoading.get(url); + imageDetails = await new Promise((resolve) => + awaiters.push(resolve) ); - if (!value) { - value = - typeNum === VAR_TYPE_BGCOLOR - ? "#ffffff" - : "#000000"; + if (!imageDetails) { + return null; } - modifiedValue = colorModifier(value, theme); } else { - modifiedValue = replaceCSSVariablesNames( - sourceValue, - (v) => varNameWrapper(v), - (fallback) => colorModifier(fallback, theme) - ); + awaitingForImageLoading.set(url, []); + imageDetails = await getImageDetails(url); + imageDetailsCache.set(url, imageDetails); + writeImageDetailsCache(url, imageDetails); + awaitingForImageLoading + .get(url) + .forEach((resolve) => + resolve(imageDetails) + ); + awaitingForImageLoading.delete(url); + } + if (isCancelled()) { + return null; + } + } catch (err) { + logWarn(err); + if (awaitingForImageLoading.has(url)) { + awaitingForImageLoading + .get(url) + .forEach((resolve) => resolve(null)); + awaitingForImageLoading.delete(url); } - } else { - modifiedValue = colorModifier(sourceValue, theme); - } - declarations.push({ - property, - value: modifiedValue - }); - }; - addModifiedValue( - VAR_TYPE_BGCOLOR, - wrapBgColorVariableName, - tryModifyBgColor - ); - addModifiedValue( - VAR_TYPE_TEXTCOLOR, - wrapTextColorVariableName, - tryModifyTextColor - ); - addModifiedValue( - VAR_TYPE_BORDERCOLOR, - wrapBorderColorVariableName, - tryModifyBorderColor - ); - if (this.isVarType(varName, VAR_TYPE_BGIMG)) { - const property = wrapBgImgVariableName(varName); - let modifiedValue = sourceValue; - if (isVarDependant(sourceValue)) { - modifiedValue = replaceCSSVariablesNames( - sourceValue, - (v) => wrapBgColorVariableName(v), - (fallback) => tryModifyBgColor(fallback, theme) - ); } - const bgModifier = getBgImageModifier( - modifiedValue, - rule, - ignoredImgSelectors, - isCancelled - ); - modifiedValue = - typeof bgModifier === "function" - ? bgModifier(theme) - : bgModifier; - declarations.push({ - property, - value: modifiedValue - }); } - return declarations; - }; - const callbacks = new Set(); - const addListener = (onTypeChange) => { - const callback = () => { - const decs = getDeclarations(); - onTypeChange(decs); - }; - callbacks.add(callback); - this.subscribeForVarTypeChange(varName, callback); - }; - const removeListeners = () => { - callbacks.forEach((callback) => { - this.unsubscribeFromVariableTypeChanges( - varName, - callback + if (imageDetails) { + const bgImageValue = getBgImageValue( + imageDetails, + theme ); - }); - }; - return { - declarations: getDeclarations(), - onTypeChange: { addListener, removeListeners } - }; - }; - } - getModifierForVarDependant(property, sourceValue) { - const isConstructedColor = sourceValue.match(/^\s*(rgb|hsl)a?\(/); - const isSimpleConstructedColor = sourceValue.match( - /^rgba?\(var\(--[\-_A-Za-z0-9]+\)(\s*,?\/?\s*0?\.\d+)?\)$/ - ); - if (isConstructedColor && !isSimpleConstructedColor) { - const isBg = property.startsWith("background"); - const isText = isTextColorProperty(property); - return (theme) => { - let value = insertVarValues( - sourceValue, - this.unstableVarValues - ); - if (!value) { - value = isBg ? "#ffffff" : "#000000"; + if (bgImageValue) { + return bgImageValue; + } } - const modifier = isBg - ? tryModifyBgColor - : isText - ? tryModifyTextColor - : tryModifyBorderColor; - return modifier(value, theme); + if (url.startsWith("data:")) { + const blobURL = await tryConvertDataURLToBlobURL(url); + if (blobURL) { + return `url("${blobURL}")`; + } + } + return `url("${url}")`; }; - } - if ( - property === "background-color" || - (isSimpleConstructedColor && property === "background") - ) { - return (theme) => { - const defaultFallback = tryModifyBgColor( - isConstructedColor ? "255, 255, 255" : "#ffffff", - theme - ); - return replaceCSSVariablesNames( - sourceValue, - (v) => wrapBgColorVariableName(v), - (fallback) => tryModifyBgColor(fallback, theme), - defaultFallback - ); - }; - } - if (isTextColorProperty(property)) { - return (theme) => { - const defaultFallback = tryModifyTextColor( - isConstructedColor ? "0, 0, 0" : "#000000", - theme - ); - return replaceCSSVariablesNames( - sourceValue, - (v) => wrapTextColorVariableName(v), - (fallback) => tryModifyTextColor(fallback, theme), - defaultFallback - ); - }; - } - if ( - property === "background" || - property === "background-image" || - property === "box-shadow" - ) { - return (theme) => { - const unknownVars = new Set(); - const modify = () => { - const variableReplaced = replaceCSSVariablesNames( - sourceValue, - (v) => { - if (this.isVarType(v, VAR_TYPE_BGCOLOR)) { - return wrapBgColorVariableName(v); - } - if (this.isVarType(v, VAR_TYPE_BGIMG)) { - return wrapBgImgVariableName(v); + }; + const getBgImageValue = (imageDetails, theme) => { + const {isDark, isLight, isTransparent, isLarge, width} = + imageDetails; + let result; + const logSrc = imageDetails.src.startsWith("data:") + ? "data:" + : imageDetails.src; + if (isLarge && isLight && !isTransparent && theme.mode === 1) { + logInfo(`Hiding large light image ${logSrc}`); + result = "none"; + } else if ( + isDark && + isTransparent && + theme.mode === 1 && + width > 2 + ) { + logInfo(`Inverting dark image ${logSrc}`); + const inverted = getFilteredImageURL(imageDetails, { + ...theme, + sepia: clamp(theme.sepia + 10, 0, 100) + }); + result = `url("${inverted}")`; + } else if (isLight && !isTransparent && theme.mode === 1) { + logInfo(`Dimming light image ${logSrc}`); + const dimmed = getFilteredImageURL(imageDetails, theme); + result = `url("${dimmed}")`; + } else if (theme.mode === 0 && isLight) { + logInfo(`Applying filter to image ${logSrc}`); + const filtered = getFilteredImageURL(imageDetails, { + ...theme, + brightness: clamp(theme.brightness - 10, 5, 200), + sepia: clamp(theme.sepia + 10, 0, 100) + }); + result = `url("${filtered}")`; + } else { + logInfo(`Not modifying the image ${logSrc}`); + result = null; + } + return result; + }; + const modifiers = []; + let matchIndex = 0; + let prevHasComma = false; + matches.forEach( + ({type, match, index, typeGradient, hasComma, offset}, i) => { + const matchStart = index; + const prefixStart = matchIndex; + const matchEnd = matchStart + match.length + offset; + matchIndex = matchEnd; + if (prefixStart !== matchStart) { + if (prevHasComma) { + modifiers.push(() => { + let betweenValue = value.substring( + prefixStart, + matchStart + ); + if (betweenValue[0] === ",") { + betweenValue = betweenValue.substring(1); } - unknownVars.add(v); - return v; - }, - (fallback) => tryModifyBgColor(fallback, theme) - ); - if (property === "box-shadow") { - const shadowModifier = - getShadowModifierWithInfo(variableReplaced); - const modifiedShadow = shadowModifier(theme); - if ( - modifiedShadow.unparseableMatchesLength !== - modifiedShadow.matchesLength - ) { - return modifiedShadow.result; - } + return betweenValue; + }); + } else { + modifiers.push(() => + value.substring(prefixStart, matchStart) + ); } - return variableReplaced; - }; - const modified = modify(); - if (unknownVars.size > 0) { - const isFallbackResolved = modified.match( - /^var\(.*?, var\(--darkreader-bg--.*\)\)$/ + } + prevHasComma = hasComma || false; + if (type === "url") { + modifiers.push(getURLModifier(match)); + } else if (type === "gradient") { + modifiers.push( + getGradientModifier({ + match, + index, + typeGradient: typeGradient, + hasComma: hasComma || false, + offset + }) ); - if (isFallbackResolved) { - return modified; - } - return new Promise((resolve) => { - for (const unknownVar of unknownVars.values()) { - const callback = () => { - this.unsubscribeFromVariableTypeChanges( - unknownVar, - callback - ); - const newValue = modify(); - resolve(newValue); - }; - this.subscribeForVarTypeChange( - unknownVar, - callback - ); - } - }); } - return modified; - }; - } - if ( - property.startsWith("border") || - property.startsWith("outline") - ) { - return (theme) => { - return replaceCSSVariablesNames( - sourceValue, - (v) => wrapBorderColorVariableName(v), - (fallback) => tryModifyBorderColor(fallback, theme) - ); - }; - } - return null; - } - subscribeForVarTypeChange(varName, callback) { - if (!this.typeChangeSubscriptions.has(varName)) { - this.typeChangeSubscriptions.set(varName, new Set()); - } - const rootStore = this.typeChangeSubscriptions.get(varName); - if (!rootStore.has(callback)) { - rootStore.add(callback); - } - } - unsubscribeFromVariableTypeChanges(varName, callback) { - if (this.typeChangeSubscriptions.has(varName)) { - this.typeChangeSubscriptions.get(varName).delete(callback); - } - } - collectVariablesAndVarDep() { - this.rulesQueue.forEach((rules) => { - iterateCSSRules(rules, (rule) => { - if (rule.style) { - this.collectVarsFromCSSDeclarations(rule.style); + if (i === matches.length - 1) { + modifiers.push(() => value.substring(matchEnd)); } - }); - }); - this.inlineStyleQueue.forEach((style) => { - this.collectVarsFromCSSDeclarations(style); - }); - this.rulesQueue.clear(); - this.inlineStyleQueue.splice(0); - } - collectVarsFromCSSDeclarations(style) { - iterateCSSDeclarations(style, (property, value) => { - if (isVariable(property)) { - this.inspectVariable(property, value); } - if (isVarDependant(value)) { - this.inspectVarDependant(property, value); - } - }); - } - shouldProcessRootVariables() { - return ( - this.rulesQueue.size > 0 && - document.documentElement.getAttribute("style")?.includes("--") ); - } - collectRootVariables() { - if (!this.shouldProcessRootVariables()) { - return; - } - iterateCSSDeclarations( - document.documentElement.style, - (property, value) => { - if (isVariable(property)) { - this.inspectVariable(property, value); - } + return (theme) => { + const results = modifiers + .filter(Boolean) + .map((modify) => modify(theme)); + if (results.some((r) => r instanceof Promise)) { + return Promise.all(results).then((asyncResults) => { + return asyncResults.filter(Boolean).join(""); + }); } - ); + const combinedResult = results.join(""); + if (combinedResult.endsWith(", initial")) { + return combinedResult.slice(0, -9); + } + return combinedResult; + }; + } catch (err) { + return null; } - inspectVariable(varName, value) { - this.unstableVarValues.set(varName, value); - if (isVarDependant(value) && isConstructedColorVar(value)) { - this.unknownColorVars.add(varName); - this.definedVars.add(varName); - } - if (this.definedVars.has(varName)) { - return; - } - this.definedVars.add(varName); - const isColor = Boolean( - value.match(rawRGBSpaceRegex) || - value.match(rawRGBCommaRegex) || - parseColorWithCache(value) - ); - if (isColor) { - this.unknownColorVars.add(varName); - } else if ( - value.includes("url(") || - value.includes("linear-gradient(") || - value.includes("radial-gradient(") - ) { - this.resolveVariableType(varName, VAR_TYPE_BGIMG); - } - } - resolveVariableType(varName, typeNum) { - const initialType = this.initialVarTypes.get(varName) || 0; - const currentType = this.varTypes.get(varName) || 0; - const newType = currentType | typeNum; - this.varTypes.set(varName, newType); - if (newType !== initialType || this.undefinedVars.has(varName)) { - this.changedTypeVars.add(varName); - this.undefinedVars.delete(varName); - } - this.unknownColorVars.delete(varName); - this.unknownBgVars.delete(varName); - } - collectRootVarDependents() { - if (!this.shouldProcessRootVariables()) { - return; - } - iterateCSSDeclarations( - document.documentElement.style, - (property, value) => { - if (isVarDependant(value)) { - this.inspectVarDependant(property, value); - } - } + } + function getShadowModifierWithInfo(value) { + try { + let index = 0; + const colorMatches = getMatches( + /(^|\s)(?!calc)([a-z]+\(.+?\)|#[0-9a-f]+|[a-z]+)(.*?(inset|outset)?($|,))/gi, + value, + 2 ); - } - inspectVarDependant(property, value) { - if (isVariable(property)) { - this.iterateVarDeps(value, (ref) => { - if (!this.varRefs.has(property)) { - this.varRefs.set(property, new Set()); - } - this.varRefs.get(property).add(ref); - }); - } else if ( - property === "background-color" || - property === "box-shadow" - ) { - this.iterateVarDeps(value, (v) => - this.resolveVariableType(v, VAR_TYPE_BGCOLOR) - ); - } else if (isTextColorProperty(property)) { - this.iterateVarDeps(value, (v) => - this.resolveVariableType(v, VAR_TYPE_TEXTCOLOR) - ); - } else if ( - property.startsWith("border") || - property.startsWith("outline") - ) { - this.iterateVarDeps(value, (v) => - this.resolveVariableType(v, VAR_TYPE_BORDERCOLOR) - ); - } else if ( - property === "background" || - property === "background-image" - ) { - this.iterateVarDeps(value, (v) => { - if (this.isVarType(v, VAR_TYPE_BGCOLOR | VAR_TYPE_BGIMG)) { - return; - } - const isBgColor = - this.findVarRef(v, (ref) => { - return ( - this.unknownColorVars.has(ref) || - this.isVarType( - ref, - VAR_TYPE_BGCOLOR | - VAR_TYPE_TEXTCOLOR | - VAR_TYPE_BORDERCOLOR - ) - ); - }) != null; - this.iterateVarRefs(v, (ref) => { - if (isBgColor) { - this.resolveVariableType(ref, VAR_TYPE_BGCOLOR); - } else { - this.unknownBgVars.add(ref); - } - }); - }); - } - } - iterateVarDeps(value, iterator) { - const varDeps = new Set(); - iterateVarDependencies(value, (v) => varDeps.add(v)); - varDeps.forEach((v) => iterator(v)); - } - findVarRef(varName, iterator, stack = new Set()) { - if (stack.has(varName)) { - return null; - } - stack.add(varName); - const result = iterator(varName); - if (result) { - return varName; - } - const refs = this.varRefs.get(varName); - if (!refs || refs.size === 0) { - return null; - } - for (const ref of refs) { - const found = this.findVarRef(ref, iterator, stack); - if (found) { - return found; + let notParsed = 0; + const modifiers = colorMatches.map((match, i) => { + const prefixIndex = index; + const matchIndex = value.indexOf(match, index); + const matchEnd = matchIndex + match.length; + index = matchEnd; + const rgb = parseColorWithCache(match); + if (!rgb) { + notParsed++; + return () => value.substring(prefixIndex, matchEnd); } - } - return null; - } - iterateVarRefs(varName, iterator) { - this.findVarRef(varName, (ref) => { - iterator(ref); - return false; + return (theme) => + `${value.substring(prefixIndex, matchIndex)}${modifyShadowColor(rgb, theme)}${i === colorMatches.length - 1 ? value.substring(matchEnd) : ""}`; }); + return (theme) => { + const modified = modifiers + .map((modify) => modify(theme)) + .join(""); + return { + matchesLength: colorMatches.length, + unparsableMatchesLength: notParsed, + result: modified + }; + }; + } catch (err) { + return null; } - setOnRootVariableChange(callback) { - this.onRootVariableDefined = callback; - } - putRootVars(styleElement, theme) { - const sheet = styleElement.sheet; - if (sheet.cssRules.length > 0) { - sheet.deleteRule(0); - } - const declarations = new Map(); - iterateCSSDeclarations( - document.documentElement.style, - (property, value) => { - if (isVariable(property)) { - if (this.isVarType(property, VAR_TYPE_BGCOLOR)) { - declarations.set( - wrapBgColorVariableName(property), - tryModifyBgColor(value, theme) - ); - } - if (this.isVarType(property, VAR_TYPE_TEXTCOLOR)) { - declarations.set( - wrapTextColorVariableName(property), - tryModifyTextColor(value, theme) - ); - } - if (this.isVarType(property, VAR_TYPE_BORDERCOLOR)) { - declarations.set( - wrapBorderColorVariableName(property), - tryModifyBorderColor(value, theme) - ); - } - this.subscribeForVarTypeChange( - property, - this.onRootVariableDefined - ); - } - } - ); - const cssLines = []; - cssLines.push(":root {"); - for (const [property, value] of declarations) { - cssLines.push(` ${property}: ${value};`); - } - cssLines.push("}"); - const cssText = cssLines.join("\n"); - sheet.insertRule(cssText); - } - } - const variablesStore = new VariablesStore(); - function getVariableRange(input, searchStart = 0) { - const start = input.indexOf("var(", searchStart); - if (start >= 0) { - const range = getParenthesesRange(input, start + 3); - if (range) { - return { start, end: range.end }; - } - } - return null; } - function getVariablesMatches(input) { - const ranges = []; - let i = 0; - let range; - while ((range = getVariableRange(input, i))) { - const { start, end } = range; - ranges.push({ start, end, value: input.substring(start, end) }); - i = range.end + 1; + function getShadowModifier(value) { + const shadowModifier = getShadowModifierWithInfo(value); + if (!shadowModifier) { + return null; } - return ranges; + return (theme) => shadowModifier(theme).result; } - function replaceVariablesMatches(input, replacer) { - const matches = getVariablesMatches(input); - const matchesCount = matches.length; - if (matchesCount === 0) { - return input; - } - const inputLength = input.length; - const replacements = matches.map((m) => - replacer(m.value, matches.length) + function getScrollbarColorModifier(value) { + const colorsMatch = value.match( + /^\s*([a-z]+(\(.*\))?)\s+([a-z]+(\(.*\))?)\s*$/ ); - const parts = []; - parts.push(input.substring(0, matches[0].start)); - for (let i = 0; i < matchesCount; i++) { - parts.push(replacements[i]); - const start = matches[i].end; - const end = - i < matchesCount - 1 ? matches[i + 1].start : inputLength; - parts.push(input.substring(start, end)); + if (!colorsMatch) { + return value; } - return parts.join(""); - } - function getVariableNameAndFallback(match) { - const commaIndex = match.indexOf(","); - let name; - let fallback; - if (commaIndex >= 0) { - name = match.substring(4, commaIndex).trim(); - fallback = match.substring(commaIndex + 1, match.length - 1).trim(); - } else { - name = match.substring(4, match.length - 1).trim(); - fallback = ""; + const thumb = parseColorWithCache(colorsMatch[1]); + const track = parseColorWithCache(colorsMatch[3]); + if (!thumb || !track) { + return null; } - return { name, fallback }; + return (theme) => + `${modifyForegroundColor(thumb, theme)} ${modifyBackgroundColor(thumb, theme)}`; } - function replaceCSSVariablesNames( + function getColorSchemeModifier() { + return (theme) => (theme.mode === 0 ? "dark light" : "dark"); + } + function getVariableModifier( + variablesStore, + prop, value, - nameReplacer, - fallbackReplacer, - finalFallback + rule, + ignoredImgSelectors, + isCancelled ) { - const matchReplacer = (match) => { - const { name, fallback } = getVariableNameAndFallback(match); - const newName = nameReplacer(name); - if (!fallback) { - if (finalFallback) { - return `var(${newName}, ${finalFallback})`; - } - return `var(${newName})`; - } - let newFallback; - if (isVarDependant(fallback)) { - newFallback = replaceCSSVariablesNames( - fallback, - nameReplacer, - fallbackReplacer - ); - } else if (fallbackReplacer) { - newFallback = fallbackReplacer(fallback); - } else { - newFallback = fallback; - } - return `var(${newName}, ${newFallback})`; - }; - return replaceVariablesMatches(value, matchReplacer); - } - function iterateVarDependencies(value, iterator) { - replaceCSSVariablesNames(value, (varName) => { - iterator(varName); - return varName; + return variablesStore.getModifierForVariable({ + varName: prop, + sourceValue: value, + rule, + ignoredImgSelectors, + isCancelled }); } - function wrapBgColorVariableName(name) { - return `--darkreader-bg${name}`; - } - function wrapTextColorVariableName(name) { - return `--darkreader-text${name}`; - } - function wrapBorderColorVariableName(name) { - return `--darkreader-border${name}`; - } - function wrapBgImgVariableName(name) { - return `--darkreader-bgimg${name}`; - } - function isVariable(property) { - return property.startsWith("--"); - } - function isVarDependant(value) { - return value.includes("var("); - } - function isConstructedColorVar(value) { - return ( - value.match(/^\s*(rgb|hsl)a?\(/) || - value.match(/^(((\d{1,3})|(var\([\-_A-Za-z0-9]+\))),?\s*?){3}$/) - ); + function getVariableDependantModifier(variablesStore, prop, value) { + return variablesStore.getModifierForVarDependant(prop, value); } - function isTextColorProperty(property) { - return ( - property === "color" || - property === "caret-color" || - property === "-webkit-text-fill-color" - ); + function cleanModificationCache() { + clearColorModificationCache(); + imageDetailsCache.clear(); + cleanImageProcessingCache(); + awaitingForImageLoading.clear(); + imageSelectorQueue.clear(); + classObserver?.disconnect(); + classObserver = null; } - const rawRGBSpaceRegex = /^(\d{1,3})\s+(\d{1,3})\s+(\d{1,3})$/; - const rawRGBCommaRegex = /^(\d{1,3}),\s*(\d{1,3}),\s*(\d{1,3})$/; - function parseRawColorValue(input) { - const match = - input.match(rawRGBSpaceRegex) ?? input.match(rawRGBCommaRegex); - if (match) { - const color = `rgb(${match[1]}, ${match[2]}, ${match[3]})`; - return { isRaw: true, color }; + + const VAR_TYPE_BG_COLOR = 1 << 0; + const VAR_TYPE_TEXT_COLOR = 1 << 1; + const VAR_TYPE_BORDER_COLOR = 1 << 2; + const VAR_TYPE_BG_IMG = 1 << 3; + class VariablesStore { + constructor() { + this.varTypes = new Map(); + this.rulesQueue = new Set(); + this.inlineStyleQueue = []; + this.definedVars = new Set(); + this.varRefs = new Map(); + this.unknownColorVars = new Set(); + this.unknownBgVars = new Set(); + this.undefinedVars = new Set(); + this.initialVarTypes = new Map(); + this.changedTypeVars = new Set(); + this.typeChangeSubscriptions = new Map(); + this.unstableVarValues = new Map(); } - return { isRaw: false, color: input }; - } - function handleRawColorValue(input, theme, modifyFunction) { - const { isRaw, color } = parseRawColorValue(input); - const rgb = parseColorWithCache(color); - if (rgb) { - const outputColor = modifyFunction(rgb, theme); - if (isRaw) { - const outputInRGB = parseColorWithCache(outputColor); - return outputInRGB - ? `${outputInRGB.r}, ${outputInRGB.g}, ${outputInRGB.b}` - : outputColor; - } - return outputColor; + clear() { + this.varTypes.clear(); + this.rulesQueue.clear(); + this.inlineStyleQueue.splice(0); + this.definedVars.clear(); + this.varRefs.clear(); + this.unknownColorVars.clear(); + this.unknownBgVars.clear(); + this.undefinedVars.clear(); + this.initialVarTypes.clear(); + this.changedTypeVars.clear(); + this.typeChangeSubscriptions.clear(); + this.unstableVarValues.clear(); } - return color; - } - function tryModifyBgColor(color, theme) { - return handleRawColorValue(color, theme, modifyBackgroundColor); - } - function tryModifyTextColor(color, theme) { - return handleRawColorValue(color, theme, modifyForegroundColor); - } - function tryModifyBorderColor(color, theme) { - return handleRawColorValue(color, theme, modifyBorderColor); - } - function insertVarValues(source, varValues, fullStack = new Set()) { - let containsUnresolvedVar = false; - const matchReplacer = (match, count) => { - const { name, fallback } = getVariableNameAndFallback(match); - const stack = count > 1 ? new Set(fullStack) : fullStack; - if (stack.has(name)) { - containsUnresolvedVar = true; - return null; + isVarType(varName, typeNum) { + return ( + this.varTypes.has(varName) && + (this.varTypes.get(varName) & typeNum) > 0 + ); + } + addRulesForMatching(rules) { + this.rulesQueue.add(rules); + } + addInlineStyleForMatching(style) { + this.inlineStyleQueue.push(style); + } + matchVariablesAndDependents() { + if ( + this.rulesQueue.size === 0 && + this.inlineStyleQueue.length === 0 + ) { + return; } - stack.add(name); - const varValue = varValues.get(name) || fallback; - let inserted = null; - if (varValue) { - if (isVarDependant(varValue)) { - inserted = insertVarValues(varValue, varValues, stack); + this.changedTypeVars.clear(); + this.initialVarTypes = new Map(this.varTypes); + this.collectRootVariables(); + this.collectVariablesAndVarDep(); + this.collectRootVarDependents(); + this.varRefs.forEach((refs, v) => { + refs.forEach((r) => { + if (this.varTypes.has(v)) { + this.resolveVariableType(r, this.varTypes.get(v)); + } + }); + }); + this.unknownColorVars.forEach((v) => { + if (this.unknownBgVars.has(v)) { + this.unknownColorVars.delete(v); + this.unknownBgVars.delete(v); + this.resolveVariableType(v, VAR_TYPE_BG_COLOR); + } else if ( + this.isVarType( + v, + VAR_TYPE_BG_COLOR | + VAR_TYPE_TEXT_COLOR | + VAR_TYPE_BORDER_COLOR + ) + ) { + this.unknownColorVars.delete(v); } else { - inserted = varValue; + this.undefinedVars.add(v); } - } - if (!inserted) { - containsUnresolvedVar = true; - return null; - } - return inserted; - }; - const replaced = replaceVariablesMatches(source, matchReplacer); - if (containsUnresolvedVar) { - return null; + }); + this.unknownBgVars.forEach((v) => { + const hasColor = + this.findVarRef(v, (ref) => { + return ( + this.unknownColorVars.has(ref) || + this.isVarType( + ref, + VAR_TYPE_BG_COLOR | + VAR_TYPE_TEXT_COLOR | + VAR_TYPE_BORDER_COLOR + ) + ); + }) != null; + if (hasColor) { + this.iterateVarRefs(v, (ref) => { + this.resolveVariableType(ref, VAR_TYPE_BG_COLOR); + }); + } else if ( + this.isVarType(v, VAR_TYPE_BG_COLOR | VAR_TYPE_BG_IMG) + ) { + this.unknownBgVars.delete(v); + } else { + this.undefinedVars.add(v); + } + }); + this.changedTypeVars.forEach((varName) => { + if (this.typeChangeSubscriptions.has(varName)) { + this.typeChangeSubscriptions + .get(varName) + .forEach((callback) => { + callback(); + }); + } + }); + this.changedTypeVars.clear(); } - return replaced; - } - - const overrides$1 = { - "background-color": { - customProp: "--darkreader-inline-bgcolor", - cssProp: "background-color", - dataAttr: "data-darkreader-inline-bgcolor" - }, - "background-image": { - customProp: "--darkreader-inline-bgimage", - cssProp: "background-image", - dataAttr: "data-darkreader-inline-bgimage" - }, - "border-color": { - customProp: "--darkreader-inline-border", - cssProp: "border-color", - dataAttr: "data-darkreader-inline-border" - }, - "border-bottom-color": { - customProp: "--darkreader-inline-border-bottom", - cssProp: "border-bottom-color", - dataAttr: "data-darkreader-inline-border-bottom" - }, - "border-left-color": { - customProp: "--darkreader-inline-border-left", - cssProp: "border-left-color", - dataAttr: "data-darkreader-inline-border-left" - }, - "border-right-color": { - customProp: "--darkreader-inline-border-right", - cssProp: "border-right-color", - dataAttr: "data-darkreader-inline-border-right" - }, - "border-top-color": { - customProp: "--darkreader-inline-border-top", - cssProp: "border-top-color", - dataAttr: "data-darkreader-inline-border-top" - }, - "box-shadow": { - customProp: "--darkreader-inline-boxshadow", - cssProp: "box-shadow", - dataAttr: "data-darkreader-inline-boxshadow" - }, - "color": { - customProp: "--darkreader-inline-color", - cssProp: "color", - dataAttr: "data-darkreader-inline-color" - }, - "fill": { - customProp: "--darkreader-inline-fill", - cssProp: "fill", - dataAttr: "data-darkreader-inline-fill" - }, - "stroke": { - customProp: "--darkreader-inline-stroke", - cssProp: "stroke", - dataAttr: "data-darkreader-inline-stroke" - }, - "outline-color": { - customProp: "--darkreader-inline-outline", - cssProp: "outline-color", - dataAttr: "data-darkreader-inline-outline" - }, - "stop-color": { - customProp: "--darkreader-inline-stopcolor", - cssProp: "stop-color", - dataAttr: "data-darkreader-inline-stopcolor" - } - }; - const shorthandOverrides = { - background: { - customProp: "--darkreader-inline-bg", - cssProp: "background", - dataAttr: "data-darkreader-inline-bg" - } - }; - const overridesList = Object.values(overrides$1); - const normalizedPropList = {}; - overridesList.forEach( - ({ cssProp, customProp }) => (normalizedPropList[customProp] = cssProp) - ); - const INLINE_STYLE_ATTRS = [ - "style", - "fill", - "stop-color", - "stroke", - "bgcolor", - "color", - "background" - ]; - const INLINE_STYLE_SELECTOR = INLINE_STYLE_ATTRS.map( - (attr) => `[${attr}]` - ).join(", "); - function getInlineOverrideStyle() { - const allOverrides = overridesList.concat( - Object.values(shorthandOverrides) - ); - return allOverrides - .map(({ dataAttr, customProp, cssProp }) => { - return [ - `[${dataAttr}] {`, - ` ${cssProp}: var(${customProp}) !important;`, - "}" - ].join("\n"); - }) - .concat([ - "[data-darkreader-inline-invert] {", - " filter: invert(100%) hue-rotate(180deg);", - "}" - ]) - .join("\n"); - } - function getInlineStyleElements(root) { - const results = []; - if (root instanceof Element && root.matches(INLINE_STYLE_SELECTOR)) { - results.push(root); - } - if ( - root instanceof Element || - (isShadowDomSupported && root instanceof ShadowRoot) || - root instanceof Document - ) { - push(results, root.querySelectorAll(INLINE_STYLE_SELECTOR)); + getModifierForVariable(options) { + return (theme) => { + const { + varName, + sourceValue, + rule, + ignoredImgSelectors, + isCancelled + } = options; + const getDeclarations = () => { + const declarations = []; + const addModifiedValue = ( + typeNum, + varNameWrapper, + colorModifier + ) => { + if (!this.isVarType(varName, typeNum)) { + return; + } + const property = varNameWrapper(varName); + let modifiedValue; + if (isVarDependant(sourceValue)) { + if (isConstructedColorVar(sourceValue)) { + let value = insertVarValues( + sourceValue, + this.unstableVarValues + ); + if (!value) { + value = + typeNum === VAR_TYPE_BG_COLOR + ? "#ffffff" + : "#000000"; + } + modifiedValue = colorModifier(value, theme); + } else { + modifiedValue = replaceCSSVariablesNames( + sourceValue, + (v) => varNameWrapper(v), + (fallback) => colorModifier(fallback, theme) + ); + } + } else { + modifiedValue = colorModifier(sourceValue, theme); + } + declarations.push({ + property, + value: modifiedValue + }); + }; + addModifiedValue( + VAR_TYPE_BG_COLOR, + wrapBgColorVariableName, + tryModifyBgColor + ); + addModifiedValue( + VAR_TYPE_TEXT_COLOR, + wrapTextColorVariableName, + tryModifyTextColor + ); + addModifiedValue( + VAR_TYPE_BORDER_COLOR, + wrapBorderColorVariableName, + tryModifyBorderColor + ); + if (this.isVarType(varName, VAR_TYPE_BG_IMG)) { + const property = wrapBgImgVariableName(varName); + let modifiedValue = sourceValue; + if (isVarDependant(sourceValue)) { + modifiedValue = replaceCSSVariablesNames( + sourceValue, + (v) => wrapBgColorVariableName(v), + (fallback) => tryModifyBgColor(fallback, theme) + ); + } + const bgModifier = getBgImageModifier( + modifiedValue, + rule, + ignoredImgSelectors, + isCancelled + ); + modifiedValue = + typeof bgModifier === "function" + ? bgModifier(theme) + : bgModifier; + declarations.push({ + property, + value: modifiedValue + }); + } + return declarations; + }; + const callbacks = new Set(); + const addListener = (onTypeChange) => { + const callback = () => { + const decs = getDeclarations(); + onTypeChange(decs); + }; + callbacks.add(callback); + this.subscribeForVarTypeChange(varName, callback); + }; + const removeListeners = () => { + callbacks.forEach((callback) => { + this.unsubscribeFromVariableTypeChanges( + varName, + callback + ); + }); + }; + return { + declarations: getDeclarations(), + onTypeChange: {addListener, removeListeners} + }; + }; } - return results; - } - const treeObservers = new Map(); - const attrObservers = new Map(); - function watchForInlineStyles(elementStyleDidChange, shadowRootDiscovered) { - deepWatchForInlineStyles( - document, - elementStyleDidChange, - shadowRootDiscovered - ); - iterateShadowHosts(document.documentElement, (host) => { - deepWatchForInlineStyles( - host.shadowRoot, - elementStyleDidChange, - shadowRootDiscovered + getModifierForVarDependant(property, sourceValue) { + const isConstructedColor = sourceValue.match(/^\s*(rgb|hsl)a?\(/); + const isSimpleConstructedColor = sourceValue.match( + /^rgba?\(var\(--[\-_A-Za-z0-9]+\)(\s*,?\/?\s*0?\.\d+)?\)$/ ); - }); - } - function deepWatchForInlineStyles( - root, - elementStyleDidChange, - shadowRootDiscovered - ) { - if (treeObservers.has(root)) { - treeObservers.get(root).disconnect(); - attrObservers.get(root).disconnect(); - } - const discoveredNodes = new WeakSet(); - function discoverNodes(node) { - getInlineStyleElements(node).forEach((el) => { - if (discoveredNodes.has(el)) { - return; - } - discoveredNodes.add(el); - elementStyleDidChange(el); - }); - iterateShadowHosts(node, (n) => { - if (discoveredNodes.has(node)) { - return; - } - discoveredNodes.add(node); - shadowRootDiscovered(n.shadowRoot); - deepWatchForInlineStyles( - n.shadowRoot, - elementStyleDidChange, - shadowRootDiscovered - ); - }); - variablesStore.matchVariablesAndDependents(); - } - const treeObserver = createOptimizedTreeObserver(root, { - onMinorMutations: (_root, { additions }) => { - additions.forEach((added) => discoverNodes(added)); - }, - onHugeMutations: () => { - discoverNodes(root); + if (isConstructedColor && !isSimpleConstructedColor) { + const isBg = property.startsWith("background"); + const isText = isTextColorProperty(property); + return (theme) => { + let value = insertVarValues( + sourceValue, + this.unstableVarValues + ); + if (!value) { + value = isBg ? "#ffffff" : "#000000"; + } + const modifier = isBg + ? tryModifyBgColor + : isText + ? tryModifyTextColor + : tryModifyBorderColor; + return modifier(value, theme); + }; } - }); - treeObservers.set(root, treeObserver); - let attemptCount = 0; - let start = null; - const ATTEMPTS_INTERVAL = getDuration({ seconds: 10 }); - const RETRY_TIMEOUT = getDuration({ seconds: 2 }); - const MAX_ATTEMPTS_COUNT = 50; - let cache = []; - let timeoutId = null; - const handleAttributeMutations = throttle((mutations) => { - const handledTargets = new Set(); - mutations.forEach((m) => { - const target = m.target; - if (handledTargets.has(target)) { - return; - } - if (INLINE_STYLE_ATTRS.includes(m.attributeName)) { - handledTargets.add(target); - elementStyleDidChange(target); - } - }); - variablesStore.matchVariablesAndDependents(); - }); - const attrObserver = new MutationObserver((mutations) => { - if (timeoutId) { - cache.push(...mutations); - return; + if ( + property === "background-color" || + (isSimpleConstructedColor && property === "background") + ) { + return (theme) => { + const defaultFallback = tryModifyBgColor( + isConstructedColor ? "255, 255, 255" : "#ffffff", + theme + ); + return replaceCSSVariablesNames( + sourceValue, + (v) => wrapBgColorVariableName(v), + (fallback) => tryModifyBgColor(fallback, theme), + defaultFallback + ); + }; } - attemptCount++; - const now = Date.now(); - if (start == null) { - start = now; - } else if (attemptCount >= MAX_ATTEMPTS_COUNT) { - if (now - start < ATTEMPTS_INTERVAL) { - timeoutId = setTimeout(() => { - start = null; - attemptCount = 0; - timeoutId = null; - const attributeCache = cache; - cache = []; - handleAttributeMutations(attributeCache); - }, RETRY_TIMEOUT); - cache.push(...mutations); - return; - } - start = now; - attemptCount = 1; + if (isTextColorProperty(property)) { + return (theme) => { + const defaultFallback = tryModifyTextColor( + isConstructedColor ? "0, 0, 0" : "#000000", + theme + ); + return replaceCSSVariablesNames( + sourceValue, + (v) => wrapTextColorVariableName(v), + (fallback) => tryModifyTextColor(fallback, theme), + defaultFallback + ); + }; + } + if ( + property === "background" || + property === "background-image" || + property === "box-shadow" + ) { + return (theme) => { + const unknownVars = new Set(); + const modify = () => { + const variableReplaced = replaceCSSVariablesNames( + sourceValue, + (v) => { + if (this.isVarType(v, VAR_TYPE_BG_COLOR)) { + return wrapBgColorVariableName(v); + } + if (this.isVarType(v, VAR_TYPE_BG_IMG)) { + return wrapBgImgVariableName(v); + } + unknownVars.add(v); + return v; + }, + (fallback) => tryModifyBgColor(fallback, theme) + ); + if (property === "box-shadow") { + const shadowModifier = + getShadowModifierWithInfo(variableReplaced); + const modifiedShadow = shadowModifier(theme); + if ( + modifiedShadow.unparsableMatchesLength !== + modifiedShadow.matchesLength + ) { + return modifiedShadow.result; + } + } + return variableReplaced; + }; + const modified = modify(); + if (unknownVars.size > 0) { + const isFallbackResolved = modified.match( + /^var\(.*?, (var\(--darkreader-bg--.*\))|(#[0-9A-Fa-f]+)|([a-z]+)|(rgba?\(.+\))|(hsla?\(.+\))\)$/ + ); + if (isFallbackResolved) { + return modified; + } + return new Promise((resolve) => { + for (const unknownVar of unknownVars.values()) { + const callback = () => { + this.unsubscribeFromVariableTypeChanges( + unknownVar, + callback + ); + const newValue = modify(); + resolve(newValue); + }; + this.subscribeForVarTypeChange( + unknownVar, + callback + ); + } + }); + } + return modified; + }; + } + if ( + property.startsWith("border") || + property.startsWith("outline") + ) { + return (theme) => { + return replaceCSSVariablesNames( + sourceValue, + (v) => wrapBorderColorVariableName(v), + (fallback) => tryModifyBorderColor(fallback, theme) + ); + }; } - handleAttributeMutations(mutations); - }); - attrObserver.observe(root, { - attributes: true, - attributeFilter: INLINE_STYLE_ATTRS.concat( - overridesList.map(({ dataAttr }) => dataAttr) - ), - subtree: true - }); - attrObservers.set(root, attrObserver); - } - function stopWatchingForInlineStyles() { - treeObservers.forEach((o) => o.disconnect()); - attrObservers.forEach((o) => o.disconnect()); - treeObservers.clear(); - attrObservers.clear(); - } - const inlineStyleCache = new WeakMap(); - const svgInversionCache = new WeakSet(); - const svgAnalysisConditionCache = new WeakMap(); - const themeProps = ["brightness", "contrast", "grayscale", "sepia", "mode"]; - function shouldAnalyzeSVGAsImage(svg) { - if (svgAnalysisConditionCache.has(svg)) { - return svgAnalysisConditionCache.get(svg); + return null; } - const shouldAnalyze = Boolean( - svg && - (svg.getAttribute("class")?.includes("logo") || - svg.parentElement?.getAttribute("class")?.includes("logo")) - ); - svgAnalysisConditionCache.set(svg, shouldAnalyze); - return shouldAnalyze; - } - function getInlineStyleCacheKey(el, theme) { - return INLINE_STYLE_ATTRS.map( - (attr) => `${attr}="${el.getAttribute(attr)}"` - ) - .concat(themeProps.map((prop) => `${prop}="${theme[prop]}"`)) - .join(" "); - } - function shouldIgnoreInlineStyle(element, selectors) { - for (let i = 0, len = selectors.length; i < len; i++) { - const ingnoredSelector = selectors[i]; - if (element.matches(ingnoredSelector)) { - return true; + subscribeForVarTypeChange(varName, callback) { + if (!this.typeChangeSubscriptions.has(varName)) { + this.typeChangeSubscriptions.set(varName, new Set()); + } + const rootStore = this.typeChangeSubscriptions.get(varName); + if (!rootStore.has(callback)) { + rootStore.add(callback); } } - return false; - } - function overrideInlineStyle( - element, - theme, - ignoreInlineSelectors, - ignoreImageSelectors - ) { - const cacheKey = getInlineStyleCacheKey(element, theme); - if (cacheKey === inlineStyleCache.get(element)) { - return; + unsubscribeFromVariableTypeChanges(varName, callback) { + if (this.typeChangeSubscriptions.has(varName)) { + this.typeChangeSubscriptions.get(varName).delete(callback); + } } - const unsetProps = new Set(Object.keys(overrides$1)); - function setCustomProp(targetCSSProp, modifierCSSProp, cssVal) { - const mod = getModifiableCSSDeclaration( - modifierCSSProp, - cssVal, - { style: element.style }, - variablesStore, - ignoreImageSelectors, - null + collectVariablesAndVarDep() { + this.rulesQueue.forEach((rules) => { + iterateCSSRules(rules, (rule) => { + if (rule.style) { + this.collectVarsFromCSSDeclarations(rule.style); + } + }); + }); + this.inlineStyleQueue.forEach((style) => { + this.collectVarsFromCSSDeclarations(style); + }); + this.rulesQueue.clear(); + this.inlineStyleQueue.splice(0); + } + collectVarsFromCSSDeclarations(style) { + iterateCSSDeclarations(style, (property, value) => { + if (isVariable(property)) { + this.inspectVariable(property, value); + } + if (isVarDependant(value)) { + this.inspectVarDependant(property, value); + } + }); + } + shouldProcessRootVariables() { + return ( + this.rulesQueue.size > 0 && + document.documentElement.getAttribute("style")?.includes("--") ); - if (!mod) { + } + collectRootVariables() { + if (!this.shouldProcessRootVariables()) { return; } - function setStaticValue(value) { - const { customProp, dataAttr } = - overrides$1[targetCSSProp] ?? - shorthandOverrides[targetCSSProp]; - element.style.setProperty(customProp, value); - if (!element.hasAttribute(dataAttr)) { - element.setAttribute(dataAttr, ""); - } - unsetProps.delete(targetCSSProp); - } - function setVarDeclaration(mod) { - let prevDeclarations = []; - function setProps(declarations) { - prevDeclarations.forEach(({ property }) => { - element.style.removeProperty(property); - }); - declarations.forEach(({ property, value }) => { - if (!(value instanceof Promise)) { - element.style.setProperty(property, value); - } - }); - prevDeclarations = declarations; + iterateCSSDeclarations( + document.documentElement.style, + (property, value) => { + if (isVariable(property)) { + this.inspectVariable(property, value); + } } - setProps(mod.declarations); - mod.onTypeChange.addListener(setProps); + ); + } + inspectVariable(varName, value) { + this.unstableVarValues.set(varName, value); + if (isVarDependant(value) && isConstructedColorVar(value)) { + this.unknownColorVars.add(varName); + this.definedVars.add(varName); } - function setAsyncValue(promise, sourceValue) { - promise.then((value) => { - if ( - value && - targetCSSProp === "background" && - value.startsWith("var(--darkreader-bg--") - ) { - setStaticValue(value); - } - if (value && targetCSSProp === "background-image") { - if ( - (element === document.documentElement || - element === document.body) && - value === sourceValue - ) { - value = "none"; - } - setStaticValue(value); - } - inlineStyleCache.set( - element, - getInlineStyleCacheKey(element, theme) - ); - }); + if (this.definedVars.has(varName)) { + return; } - const value = - typeof mod.value === "function" ? mod.value(theme) : mod.value; - if (typeof value === "string") { - setStaticValue(value); - } else if (value instanceof Promise) { - setAsyncValue(value, cssVal); - } else if (typeof value === "object") { - setVarDeclaration(value); + this.definedVars.add(varName); + const isColor = Boolean( + value.match(rawRGBSpaceRegex) || + value.match(rawRGBCommaRegex) || + parseColorWithCache(value) + ); + if (isColor) { + this.unknownColorVars.add(varName); + } else if ( + value.includes("url(") || + value.includes("linear-gradient(") || + value.includes("radial-gradient(") + ) { + this.resolveVariableType(varName, VAR_TYPE_BG_IMG); } } - if (ignoreInlineSelectors.length > 0) { - if (shouldIgnoreInlineStyle(element, ignoreInlineSelectors)) { - unsetProps.forEach((cssProp) => { - element.removeAttribute(overrides$1[cssProp].dataAttr); - }); - return; + resolveVariableType(varName, typeNum) { + const initialType = this.initialVarTypes.get(varName) || 0; + const currentType = this.varTypes.get(varName) || 0; + const newType = currentType | typeNum; + this.varTypes.set(varName, newType); + if (newType !== initialType || this.undefinedVars.has(varName)) { + this.changedTypeVars.add(varName); + this.undefinedVars.delete(varName); } + this.unknownColorVars.delete(varName); + this.unknownBgVars.delete(varName); } - const isSVGElement = element instanceof SVGElement; - const svg = isSVGElement - ? element.ownerSVGElement ?? - (element instanceof SVGSVGElement ? element : null) - : null; - if (isSVGElement && theme.mode === 1 && svg) { - if (svgInversionCache.has(svg)) { + collectRootVarDependents() { + if (!this.shouldProcessRootVariables()) { return; } - if (shouldAnalyzeSVGAsImage(svg)) { - svgInversionCache.add(svg); - const analyzeSVGAsImage = () => { - let svgString = svg.outerHTML; - svgString = svgString.replaceAll( - '', - "" - ); - const dataURL = `data:image/svg+xml;base64,${btoa(svgString)}`; - getImageDetails(dataURL).then((details) => { - if ( - (details.isDark && details.isTransparent) || - (details.isLarge && - details.isLight && - !details.isTransparent) - ) { - svg.setAttribute( - "data-darkreader-inline-invert", - "" + iterateCSSDeclarations( + document.documentElement.style, + (property, value) => { + if (isVarDependant(value)) { + this.inspectVarDependant(property, value); + } + } + ); + } + inspectVarDependant(property, value) { + if (isVariable(property)) { + this.iterateVarDeps(value, (ref) => { + if (!this.varRefs.has(property)) { + this.varRefs.set(property, new Set()); + } + this.varRefs.get(property).add(ref); + }); + } else if ( + property === "background-color" || + property === "box-shadow" + ) { + this.iterateVarDeps(value, (v) => + this.resolveVariableType(v, VAR_TYPE_BG_COLOR) + ); + } else if (isTextColorProperty(property)) { + this.iterateVarDeps(value, (v) => + this.resolveVariableType(v, VAR_TYPE_TEXT_COLOR) + ); + } else if ( + property.startsWith("border") || + property.startsWith("outline") + ) { + this.iterateVarDeps(value, (v) => + this.resolveVariableType(v, VAR_TYPE_BORDER_COLOR) + ); + } else if ( + property === "background" || + property === "background-image" + ) { + this.iterateVarDeps(value, (v) => { + if ( + this.isVarType(v, VAR_TYPE_BG_COLOR | VAR_TYPE_BG_IMG) + ) { + return; + } + const isBgColor = + this.findVarRef(v, (ref) => { + return ( + this.unknownColorVars.has(ref) || + this.isVarType( + ref, + VAR_TYPE_BG_COLOR | + VAR_TYPE_TEXT_COLOR | + VAR_TYPE_BORDER_COLOR + ) ); + }) != null; + this.iterateVarRefs(v, (ref) => { + if (isBgColor) { + this.resolveVariableType(ref, VAR_TYPE_BG_COLOR); } else { - svg.removeAttribute( - "data-darkreader-inline-invert" - ); + this.unknownBgVars.add(ref); } }); - }; - analyzeSVGAsImage(); - if (!isDOMReady()) { - addDOMReadyListener(analyzeSVGAsImage); - } - return; + }); } } - if (element.hasAttribute("bgcolor")) { - let value = element.getAttribute("bgcolor"); - if ( - value.match(/^[0-9a-f]{3}$/i) || - value.match(/^[0-9a-f]{6}$/i) - ) { - value = `#${value}`; + iterateVarDeps(value, iterator) { + const varDeps = new Set(); + iterateVarDependencies(value, (v) => varDeps.add(v)); + varDeps.forEach((v) => iterator(v)); + } + findVarRef(varName, iterator, stack = new Set()) { + if (stack.has(varName)) { + return null; } - setCustomProp("background-color", "background-color", value); + stack.add(varName); + const result = iterator(varName); + if (result) { + return varName; + } + const refs = this.varRefs.get(varName); + if (!refs || refs.size === 0) { + return null; + } + for (const ref of refs) { + const found = this.findVarRef(ref, iterator, stack); + if (found) { + return found; + } + } + return null; } - if ( - (element === document.documentElement || - element === document.body) && - element.hasAttribute("background") - ) { - const url = getAbsoluteURL( - location.href, - element.getAttribute("background") ?? "" - ); - const value = `url("${url}")`; - setCustomProp("background-image", "background-image", value); + iterateVarRefs(varName, iterator) { + this.findVarRef(varName, (ref) => { + iterator(ref); + return false; + }); } - if (element.hasAttribute("color") && element.rel !== "mask-icon") { - let value = element.getAttribute("color"); - if ( - value.match(/^[0-9a-f]{3}$/i) || - value.match(/^[0-9a-f]{6}$/i) - ) { - value = `#${value}`; - } - setCustomProp("color", "color", value); + setOnRootVariableChange(callback) { + this.onRootVariableDefined = callback; } - if (isSVGElement) { - if (element.hasAttribute("fill")) { - const SMALL_SVG_LIMIT = 32; - const value = element.getAttribute("fill"); - if (value !== "none") { - if (!(element instanceof SVGTextElement)) { - const handleSVGElement = () => { - const { width, height } = - element.getBoundingClientRect(); - const isBg = - width > SMALL_SVG_LIMIT || - height > SMALL_SVG_LIMIT; - setCustomProp( - "fill", - isBg ? "background-color" : "color", - value + putRootVars(styleElement, theme) { + const sheet = styleElement.sheet; + if (sheet.cssRules.length > 0) { + sheet.deleteRule(0); + } + const declarations = new Map(); + iterateCSSDeclarations( + document.documentElement.style, + (property, value) => { + if (isVariable(property)) { + if (this.isVarType(property, VAR_TYPE_BG_COLOR)) { + declarations.set( + wrapBgColorVariableName(property), + tryModifyBgColor(value, theme) ); - }; - if (isReadyStateComplete()) { - handleSVGElement(); - } else { - addReadyStateCompleteListener(handleSVGElement); } - } else { - setCustomProp("fill", "color", value); + if (this.isVarType(property, VAR_TYPE_TEXT_COLOR)) { + declarations.set( + wrapTextColorVariableName(property), + tryModifyTextColor(value, theme) + ); + } + if (this.isVarType(property, VAR_TYPE_BORDER_COLOR)) { + declarations.set( + wrapBorderColorVariableName(property), + tryModifyBorderColor(value, theme) + ); + } + this.subscribeForVarTypeChange( + property, + this.onRootVariableDefined + ); } } + ); + const cssLines = []; + cssLines.push(":root {"); + for (const [property, value] of declarations) { + cssLines.push(` ${property}: ${value};`); } - if (element.hasAttribute("stop-color")) { - setCustomProp( - "stop-color", - "background-color", - element.getAttribute("stop-color") - ); + cssLines.push("}"); + const cssText = cssLines.join("\n"); + sheet.insertRule(cssText); + } + } + const variablesStore = new VariablesStore(); + function getVariableRange(input, searchStart = 0) { + const start = input.indexOf("var(", searchStart); + if (start >= 0) { + const range = getParenthesesRange(input, start + 3); + if (range) { + return {start, end: range.end}; } } - if (element.hasAttribute("stroke")) { - const value = element.getAttribute("stroke"); - setCustomProp( - "stroke", - element instanceof SVGLineElement || - element instanceof SVGTextElement - ? "border-color" - : "color", - value - ); + return null; + } + function getVariablesMatches(input) { + const ranges = []; + let i = 0; + let range; + while ((range = getVariableRange(input, i))) { + const {start, end} = range; + ranges.push({start, end, value: input.substring(start, end)}); + i = range.end + 1; } - element.style && - iterateCSSDeclarations(element.style, (property, value) => { - if (property === "background-image" && value.includes("url")) { - if ( - element === document.documentElement || - element === document.body - ) { - setCustomProp(property, property, value); - } - return; - } - if ( - overrides$1.hasOwnProperty(property) || - (property.startsWith("--") && !normalizedPropList[property]) - ) { - setCustomProp(property, property, value); - } else if ( - property === "background" && - value.includes("var(") - ) { - setCustomProp("background", "background", value); - } else { - const overriddenProp = normalizedPropList[property]; - if ( - overriddenProp && - !element.style.getPropertyValue(overriddenProp) && - !element.hasAttribute(overriddenProp) - ) { - if ( - overriddenProp === "background-color" && - element.hasAttribute("bgcolor") - ) { - return; - } - element.style.setProperty(property, ""); - } - } - }); - if ( - element.style && - element instanceof SVGTextElement && - element.style.fill - ) { - setCustomProp( - "fill", - "color", - element.style.getPropertyValue("fill") - ); + return ranges; + } + function replaceVariablesMatches(input, replacer) { + const matches = getVariablesMatches(input); + const matchesCount = matches.length; + if (matchesCount === 0) { + return input; } - if (element.getAttribute("style")?.includes("--")) { - variablesStore.addInlineStyleForMatching(element.style); + const inputLength = input.length; + const replacements = matches.map((m) => + replacer(m.value, matches.length) + ); + const parts = []; + parts.push(input.substring(0, matches[0].start)); + for (let i = 0; i < matchesCount; i++) { + parts.push(replacements[i]); + const start = matches[i].end; + const end = + i < matchesCount - 1 ? matches[i + 1].start : inputLength; + parts.push(input.substring(start, end)); + } + return parts.join(""); + } + function getVariableNameAndFallback(match) { + const commaIndex = match.indexOf(","); + let name; + let fallback; + if (commaIndex >= 0) { + name = match.substring(4, commaIndex).trim(); + fallback = match.substring(commaIndex + 1, match.length - 1).trim(); + } else { + name = match.substring(4, match.length - 1).trim(); + fallback = ""; } - forEach(unsetProps, (cssProp) => { - element.removeAttribute(overrides$1[cssProp].dataAttr); + return {name, fallback}; + } + function replaceCSSVariablesNames( + value, + nameReplacer, + fallbackReplacer, + finalFallback + ) { + const matchReplacer = (match) => { + const {name, fallback} = getVariableNameAndFallback(match); + const newName = nameReplacer(name); + if (!fallback) { + if (finalFallback) { + return `var(${newName}, ${finalFallback})`; + } + return `var(${newName})`; + } + let newFallback; + if (isVarDependant(fallback)) { + newFallback = replaceCSSVariablesNames( + fallback, + nameReplacer, + fallbackReplacer + ); + } else if (fallbackReplacer) { + newFallback = fallbackReplacer(fallback); + } else { + newFallback = fallback; + } + return `var(${newName}, ${newFallback})`; + }; + return replaceVariablesMatches(value, matchReplacer); + } + function iterateVarDependencies(value, iterator) { + replaceCSSVariablesNames(value, (varName) => { + iterator(varName); + return varName; }); - inlineStyleCache.set(element, getInlineStyleCacheKey(element, theme)); } - - const metaThemeColorName = "theme-color"; - const metaThemeColorSelector = `meta[name="${metaThemeColorName}"]`; - let srcMetaThemeColor = null; - let observer = null; - function changeMetaThemeColor(meta, theme) { - srcMetaThemeColor = srcMetaThemeColor || meta.content; - const color = parseColorWithCache(srcMetaThemeColor); - if (!color) { - return; + function wrapBgColorVariableName(name) { + return `--darkreader-bg${name}`; + } + function wrapTextColorVariableName(name) { + return `--darkreader-text${name}`; + } + function wrapBorderColorVariableName(name) { + return `--darkreader-border${name}`; + } + function wrapBgImgVariableName(name) { + return `--darkreader-bgimg${name}`; + } + function isVariable(property) { + return property.startsWith("--"); + } + function isVarDependant(value) { + return value.includes("var("); + } + function isConstructedColorVar(value) { + return ( + value.match(/^\s*(rgb|hsl)a?\(/) || + value.match(/^(((\d{1,3})|(var\([\-_A-Za-z0-9]+\))),?\s*?){3}$/) + ); + } + const textColorProps = [ + "color", + "caret-color", + "-webkit-text-fill-color", + "fill", + "stroke" + ]; + function isTextColorProperty(property) { + return textColorProps.includes(property); + } + const rawRGBSpaceRegex = /^(\d{1,3})\s+(\d{1,3})\s+(\d{1,3})$/; + const rawRGBCommaRegex = /^(\d{1,3}),\s*(\d{1,3}),\s*(\d{1,3})$/; + function parseRawColorValue(input) { + const match = + input.match(rawRGBSpaceRegex) ?? input.match(rawRGBCommaRegex); + if (match) { + const color = `rgb(${match[1]}, ${match[2]}, ${match[3]})`; + return {isRaw: true, color}; } - meta.content = modifyBackgroundColor(color, theme); + return {isRaw: false, color: input}; } - function changeMetaThemeColorWhenAvailable(theme) { - const meta = document.querySelector(metaThemeColorSelector); - if (meta) { - changeMetaThemeColor(meta, theme); - } else { - if (observer) { - observer.disconnect(); + function handleRawColorValue(input, theme, modifyFunction) { + const {isRaw, color} = parseRawColorValue(input); + const rgb = parseColorWithCache(color); + if (rgb) { + const outputColor = modifyFunction(rgb, theme); + if (isRaw) { + const outputInRGB = parseColorWithCache(outputColor); + return outputInRGB + ? `${outputInRGB.r}, ${outputInRGB.g}, ${outputInRGB.b}` + : outputColor; } - observer = new MutationObserver((mutations) => { - loop: for (let i = 0; i < mutations.length; i++) { - const { addedNodes } = mutations[i]; - for (let j = 0; j < addedNodes.length; j++) { - const node = addedNodes[j]; - if ( - node instanceof HTMLMetaElement && - node.name === metaThemeColorName - ) { - observer.disconnect(); - observer = null; - changeMetaThemeColor(node, theme); - break loop; - } - } - } - }); - observer.observe(document.head, { childList: true }); + return outputColor; } + return color; } - function restoreMetaThemeColor() { - if (observer) { - observer.disconnect(); - observer = null; - } - const meta = document.querySelector(metaThemeColorSelector); - if (meta && srcMetaThemeColor) { - meta.content = srcMetaThemeColor; - } + function tryModifyBgColor(color, theme) { + return handleRawColorValue(color, theme, modifyBackgroundColor); } - - const cssCommentsRegex = /\/\*[\s\S]*?\*\//g; - function removeCSSComments(cssText) { - return cssText.replace(cssCommentsRegex, ""); + function tryModifyTextColor(color, theme) { + return handleRawColorValue(color, theme, modifyForegroundColor); + } + function tryModifyBorderColor(color, theme) { + return handleRawColorValue(color, theme, modifyBorderColor); + } + function insertVarValues(source, varValues, fullStack = new Set()) { + let containsUnresolvedVar = false; + const matchReplacer = (match, count) => { + const {name, fallback} = getVariableNameAndFallback(match); + const stack = count > 1 ? new Set(fullStack) : fullStack; + if (stack.has(name)) { + containsUnresolvedVar = true; + return null; + } + stack.add(name); + const varValue = varValues.get(name) || fallback; + let inserted = null; + if (varValue) { + if (isVarDependant(varValue)) { + inserted = insertVarValues(varValue, varValues, stack); + } else { + inserted = varValue; + } + } + if (!inserted) { + containsUnresolvedVar = true; + return null; + } + return inserted; + }; + const replaced = replaceVariablesMatches(source, matchReplacer); + if (containsUnresolvedVar) { + return null; + } + return replaced; } const themeCacheKeys = [ @@ -4840,7 +4583,7 @@ } renderId++; function setRule(target, index, rule) { - const { selector, declarations } = rule; + const {selector, declarations} = rule; let selectorText = selector; const emptyIsWhereSelector = isChromium && @@ -4856,7 +4599,7 @@ } let ruleText = `${selectorText} {`; for (const dec of declarations) { - const { property, value, important } = dec; + const {property, value, important} = dec; if (value) { ruleText += ` ${property}: ${value}${important ? " !important" : ""};`; } @@ -4868,7 +4611,7 @@ const varDeclarations = new Map(); let asyncDeclarationCounter = 0; let varDeclarationCounter = 0; - const rootReadyGroup = { rule: null, rules: [], isGroup: true }; + const rootReadyGroup = {rule: null, rules: [], isGroup: true}; const groupRefs = new WeakMap(); function getGroup(rule) { if (rule == null) { @@ -4877,7 +4620,7 @@ if (groupRefs.has(rule)) { return groupRefs.get(rule); } - const group = { rule, rules: [], isGroup: true }; + const group = {rule, rules: [], isGroup: true}; groupRefs.set(rule, group); const parentGroup = getGroup(rule.parentRule); parentGroup.rules.push(group); @@ -4887,7 +4630,7 @@ varTypeChangeCleaners.clear(); modRules .filter((r) => r) - .forEach(({ selector, declarations, parentRule }) => { + .forEach(({selector, declarations, parentRule}) => { const group = getGroup(parentRule); const readyStyleRule = { selector, @@ -4938,7 +4681,7 @@ important, sourceValue ) { - const { declarations: varDecs, onTypeChange } = modified; + const {declarations: varDecs, onTypeChange} = modified; const varKey = ++varDeclarationCounter; const currentRenderId = renderId; const initialIndex = readyDeclarations.length; @@ -5007,7 +4750,7 @@ ); } declarations.forEach( - ({ property, value, important, sourceValue }) => { + ({property, value, important, sourceValue}) => { if (typeof value === "function") { const modified = value(theme); if (modified instanceof Promise) { @@ -5046,9 +4789,9 @@ const sheet = prepareSheet(); function buildStyleSheet() { function createTarget(group, parent) { - const { rule } = group; + const {rule} = group; if (isMediaRule(rule)) { - const { media } = rule; + const {media} = rule; const index = parent.cssRules.length; parent.insertRule( `@media ${media.mediaText} {}`, @@ -5057,7 +4800,7 @@ return parent.cssRules[index]; } if (isLayerRule(rule)) { - const { name } = rule; + const {name} = rule; const index = parent.cssRules.length; parent.insertRule(`@layer ${name} {}`, index); return parent.cssRules[index]; @@ -5076,7 +4819,7 @@ } iterateReadyRules(rootReadyGroup, sheet, (rule, target) => { const index = target.cssRules.length; - rule.declarations.forEach(({ asyncKey, varKey }) => { + rule.declarations.forEach(({asyncKey, varKey}) => { if (asyncKey != null) { asyncDeclarations.set(asyncKey, { rule, @@ -5085,1708 +4828,2321 @@ }); } if (varKey != null) { - varDeclarations.set(varKey, { rule, target, index }); + varDeclarations.set(varKey, {rule, target, index}); } }); setRule(target, index, rule); }); } function rebuildAsyncRule(key) { - const { rule, target, index } = asyncDeclarations.get(key); + const {rule, target, index} = asyncDeclarations.get(key); target.deleteRule(index); setRule(target, index, rule); asyncDeclarations.delete(key); } function rebuildVarRule(key) { - const { rule, target, index } = varDeclarations.get(key); + const {rule, target, index} = varDeclarations.get(key); target.deleteRule(index); setRule(target, index, rule); } buildStyleSheet(); } - return { modifySheet, shouldRebuildStyle }; + return {modifySheet, shouldRebuildStyle}; } let canUseSheetProxy$1 = false; document.addEventListener( "__darkreader__inlineScriptsAllowed", () => (canUseSheetProxy$1 = true), - { once: true } + {once: true} ); - function createSheetWatcher( - element, - safeGetSheetRules, - callback, - isCancelled - ) { - let rafSheetWatcher = null; - function watchForSheetChanges() { - watchForSheetChangesUsingProxy(); - if (!(canUseSheetProxy$1 && element.sheet)) { - rafSheetWatcher = createRAFSheetWatcher( - element, - safeGetSheetRules, - callback, - isCancelled + const overrides$1 = new WeakSet(); + const overridesBySource = new WeakMap(); + function canHaveAdoptedStyleSheets(node) { + return Array.isArray(node.adoptedStyleSheets); + } + function createAdoptedStyleSheetOverride(node) { + let cancelAsyncOperations = false; + function iterateSourceSheets(iterator) { + node.adoptedStyleSheets.forEach((sheet) => { + if (!overrides$1.has(sheet)) { + iterator(sheet); + } + defineSheetScope(sheet, node); + }); + } + function injectSheet(sheet, override) { + const newSheets = [...node.adoptedStyleSheets]; + const sheetIndex = newSheets.indexOf(sheet); + const overrideIndex = newSheets.indexOf(override); + if (overrideIndex >= 0) { + newSheets.splice(overrideIndex, 1); + } + newSheets.splice(sheetIndex + 1, 0, override); + node.adoptedStyleSheets = newSheets; + } + function clear() { + const newSheets = [...node.adoptedStyleSheets]; + for (let i = newSheets.length - 1; i >= 0; i--) { + const sheet = newSheets[i]; + if (overrides$1.has(sheet)) { + newSheets.splice(i, 1); + } + } + if (node.adoptedStyleSheets.length !== newSheets.length) { + node.adoptedStyleSheets = newSheets; + } + sourceSheets = new WeakSet(); + sourceDeclarations = new WeakSet(); + } + const cleaners = []; + function destroy() { + cleaners.forEach((c) => c()); + cleaners.splice(0); + cancelAsyncOperations = true; + clear(); + if (frameId) { + cancelAnimationFrame(frameId); + frameId = null; + } + } + let rulesChangeKey = 0; + function getRulesChangeKey() { + let count = 0; + iterateSourceSheets((sheet) => { + count += sheet.cssRules.length; + }); + if (count === 1) { + const rule = node.adoptedStyleSheets[0].cssRules[0]; + return rule instanceof CSSStyleRule ? rule.style.length : count; + } + return count; + } + let sourceSheets = new WeakSet(); + let sourceDeclarations = new WeakSet(); + function render(theme, ignoreImageAnalysis) { + clear(); + for (let i = node.adoptedStyleSheets.length - 1; i >= 0; i--) { + const sheet = node.adoptedStyleSheets[i]; + if (overrides$1.has(sheet)) { + continue; + } + sourceSheets.add(sheet); + const readyOverride = overridesBySource.get(sheet); + if (readyOverride) { + rulesChangeKey = getRulesChangeKey(); + injectSheet(sheet, readyOverride); + continue; + } + const rules = sheet.cssRules; + const override = new CSSStyleSheet(); + overridesBySource.set(sheet, override); + iterateCSSRules(rules, (rule) => + sourceDeclarations.add(rule.style) ); - rafSheetWatcher.start(); + const prepareSheet = () => { + for (let i = override.cssRules.length - 1; i >= 0; i--) { + override.deleteRule(i); + } + override.insertRule("#__darkreader__adoptedOverride {}"); + injectSheet(sheet, override); + overrides$1.add(override); + return override; + }; + const sheetModifier = createStyleSheetModifier(); + sheetModifier.modifySheet({ + prepareSheet, + sourceCSSRules: rules, + theme, + ignoreImageAnalysis, + force: false, + isAsyncCancelled: () => cancelAsyncOperations + }); } + rulesChangeKey = getRulesChangeKey(); } - let areSheetChangesPending = false; - function onSheetChange() { - canUseSheetProxy$1 = true; - rafSheetWatcher?.stop(); - if (areSheetChangesPending) { + let callbackRequested = false; + function handleArrayChange(callback) { + if (callbackRequested) { return; } - function handleSheetChanges() { - areSheetChangesPending = false; - if (isCancelled()) { + callbackRequested = true; + queueMicrotask(() => { + callbackRequested = false; + const sheets = node.adoptedStyleSheets.filter( + (s) => !overrides$1.has(s) + ); + sheets.forEach((sheet) => overridesBySource.delete(sheet)); + callback(sheets); + }); + } + function checkForUpdates() { + return getRulesChangeKey() !== rulesChangeKey; + } + let frameId = null; + function watchUsingRAF(callback) { + frameId = requestAnimationFrame(() => { + if (canUseSheetProxy$1) { return; } - callback(); + if (checkForUpdates()) { + handleArrayChange(callback); + } + watchUsingRAF(callback); + }); + } + function addSheetChangeEventListener(type, listener) { + node.addEventListener(type, listener); + cleaners.push(() => node.removeEventListener(type, listener)); + } + function watch(callback) { + const onAdoptedSheetsChange = () => { + canUseSheetProxy$1 = true; + handleArrayChange(callback); + }; + addSheetChangeEventListener( + "__darkreader__adoptedStyleSheetsChange", + onAdoptedSheetsChange + ); + addSheetChangeEventListener( + "__darkreader__adoptedStyleSheetChange", + onAdoptedSheetsChange + ); + addSheetChangeEventListener( + "__darkreader__adoptedStyleDeclarationChange", + onAdoptedSheetsChange + ); + if (canUseSheetProxy$1) { + return; } - areSheetChangesPending = true; - queueMicrotask(handleSheetChanges); + watchUsingRAF(callback); + } + return { + render, + destroy, + watch + }; + } + class StyleSheetCommandBuilder { + constructor() { + this.cssRules = []; + this.commands = []; + } + insertRule(cssText, index = 0) { + this.commands.push({type: "insert", index, cssText}); + this.cssRules.splice(index, 0, new StyleSheetCommandBuilder()); + return index; + } + deleteRule(index) { + this.commands.push({type: "delete", index}); + this.cssRules.splice(index, 1); + } + replaceSync(cssText) { + this.commands.splice(0); + this.commands.push({type: "replace", cssText}); + if (cssText === "") { + this.cssRules.splice(0); + } else { + throw new Error( + "StyleSheetCommandBuilder.replaceSync() is not fully supported" + ); + } + } + getDeepCSSCommands() { + const deep = []; + this.commands.forEach((command) => { + deep.push({ + type: command.type, + cssText: command.type !== "delete" ? command.cssText : "", + path: command.type === "replace" ? [] : [command.index] + }); + }); + this.cssRules.forEach((rule, i) => { + const childCommands = rule.getDeepCSSCommands(); + childCommands.forEach((c) => c.path.unshift(i)); + }); + return deep; + } + clearDeepCSSCommands() { + this.commands.splice(0); + this.cssRules.forEach((rule) => rule.clearDeepCSSCommands()); + } + } + function createAdoptedStyleSheetFallback() { + let cancelAsyncOperations = false; + const builder = new StyleSheetCommandBuilder(); + function render(options) { + const prepareSheet = () => { + builder.replaceSync(""); + return builder; + }; + const sheetModifier = createStyleSheetModifier(); + sheetModifier.modifySheet({ + prepareSheet, + sourceCSSRules: options.cssRules, + theme: options.theme, + ignoreImageAnalysis: options.ignoreImageAnalysis, + force: false, + isAsyncCancelled: () => cancelAsyncOperations + }); + } + function commands() { + const commands = builder.getDeepCSSCommands(); + builder.clearDeepCSSCommands(); + return commands; + } + function destroy() { + cancelAsyncOperations = true; + } + return {render, destroy, commands}; + } + + const overrides = { + "background-color": { + customProp: "--darkreader-inline-bgcolor", + cssProp: "background-color", + dataAttr: "data-darkreader-inline-bgcolor" + }, + "background-image": { + customProp: "--darkreader-inline-bgimage", + cssProp: "background-image", + dataAttr: "data-darkreader-inline-bgimage" + }, + "border-color": { + customProp: "--darkreader-inline-border", + cssProp: "border-color", + dataAttr: "data-darkreader-inline-border" + }, + "border-bottom-color": { + customProp: "--darkreader-inline-border-bottom", + cssProp: "border-bottom-color", + dataAttr: "data-darkreader-inline-border-bottom" + }, + "border-left-color": { + customProp: "--darkreader-inline-border-left", + cssProp: "border-left-color", + dataAttr: "data-darkreader-inline-border-left" + }, + "border-right-color": { + customProp: "--darkreader-inline-border-right", + cssProp: "border-right-color", + dataAttr: "data-darkreader-inline-border-right" + }, + "border-top-color": { + customProp: "--darkreader-inline-border-top", + cssProp: "border-top-color", + dataAttr: "data-darkreader-inline-border-top" + }, + "box-shadow": { + customProp: "--darkreader-inline-boxshadow", + cssProp: "box-shadow", + dataAttr: "data-darkreader-inline-boxshadow" + }, + "color": { + customProp: "--darkreader-inline-color", + cssProp: "color", + dataAttr: "data-darkreader-inline-color" + }, + "fill": { + customProp: "--darkreader-inline-fill", + cssProp: "fill", + dataAttr: "data-darkreader-inline-fill" + }, + "stroke": { + customProp: "--darkreader-inline-stroke", + cssProp: "stroke", + dataAttr: "data-darkreader-inline-stroke" + }, + "outline-color": { + customProp: "--darkreader-inline-outline", + cssProp: "outline-color", + dataAttr: "data-darkreader-inline-outline" + }, + "stop-color": { + customProp: "--darkreader-inline-stopcolor", + cssProp: "stop-color", + dataAttr: "data-darkreader-inline-stopcolor" + } + }; + const shorthandOverrides = { + background: { + customProp: "--darkreader-inline-bg", + cssProp: "background", + dataAttr: "data-darkreader-inline-bg" + } + }; + const overridesList = Object.values(overrides); + const normalizedPropList = {}; + overridesList.forEach( + ({cssProp, customProp}) => (normalizedPropList[customProp] = cssProp) + ); + const INLINE_STYLE_ATTRS = [ + "style", + "fill", + "stop-color", + "stroke", + "bgcolor", + "color", + "background" + ]; + const INLINE_STYLE_SELECTOR = INLINE_STYLE_ATTRS.map( + (attr) => `[${attr}]` + ).join(", "); + function getInlineOverrideStyle() { + const allOverrides = overridesList.concat( + Object.values(shorthandOverrides) + ); + return allOverrides + .map(({dataAttr, customProp, cssProp}) => { + return [ + `[${dataAttr}] {`, + ` ${cssProp}: var(${customProp}) !important;`, + "}" + ].join("\n"); + }) + .concat([ + "[data-darkreader-inline-invert] {", + " filter: invert(100%) hue-rotate(180deg);", + "}" + ]) + .join("\n"); + } + function getInlineStyleElements(root) { + const results = []; + if (root instanceof Element && root.matches(INLINE_STYLE_SELECTOR)) { + results.push(root); } - function watchForSheetChangesUsingProxy() { - element.addEventListener( - "__darkreader__updateSheet", - onSheetChange - ); + if ( + root instanceof Element || + (isShadowDomSupported && root instanceof ShadowRoot) || + root instanceof Document + ) { + push(results, root.querySelectorAll(INLINE_STYLE_SELECTOR)); } - function stopWatchingForSheetChangesUsingProxy() { - element.removeEventListener( - "__darkreader__updateSheet", - onSheetChange + return results; + } + const treeObservers = new Map(); + const attrObservers = new Map(); + function watchForInlineStyles(elementStyleDidChange, shadowRootDiscovered) { + deepWatchForInlineStyles( + document, + elementStyleDidChange, + shadowRootDiscovered + ); + iterateShadowHosts(document.documentElement, (host) => { + deepWatchForInlineStyles( + host.shadowRoot, + elementStyleDidChange, + shadowRootDiscovered ); - } - function stopWatchingForSheetChanges() { - stopWatchingForSheetChangesUsingProxy(); - rafSheetWatcher?.stop(); - } - return { - start: watchForSheetChanges, - stop: stopWatchingForSheetChanges - }; + }); } - function createRAFSheetWatcher( - element, - safeGetSheetRules, - callback, - isCancelled + function deepWatchForInlineStyles( + root, + elementStyleDidChange, + shadowRootDiscovered ) { - let rulesChangeKey = null; - let rulesCheckFrameId = null; - function getRulesChangeKey() { - const rules = safeGetSheetRules(); - return rules ? rules.length : null; - } - function didRulesKeyChange() { - return getRulesChangeKey() !== rulesChangeKey; + if (treeObservers.has(root)) { + treeObservers.get(root).disconnect(); + attrObservers.get(root).disconnect(); } - function watchForSheetChangesUsingRAF() { - rulesChangeKey = getRulesChangeKey(); - stopWatchingForSheetChangesUsingRAF(); - const checkForUpdate = () => { - const cancelled = isCancelled(); - if (!cancelled && didRulesKeyChange()) { - rulesChangeKey = getRulesChangeKey(); - callback(); + const discoveredNodes = new WeakSet(); + function discoverNodes(node) { + getInlineStyleElements(node).forEach((el) => { + if (discoveredNodes.has(el)) { + return; } - if (cancelled || (canUseSheetProxy$1 && element.sheet)) { - stopWatchingForSheetChangesUsingRAF(); + discoveredNodes.add(el); + elementStyleDidChange(el); + }); + iterateShadowHosts(node, (n) => { + if (discoveredNodes.has(node)) { return; } - rulesCheckFrameId = requestAnimationFrame(checkForUpdate); - }; - checkForUpdate(); - } - function stopWatchingForSheetChangesUsingRAF() { - rulesCheckFrameId && cancelAnimationFrame(rulesCheckFrameId); - } - return { - start: watchForSheetChangesUsingRAF, - stop: stopWatchingForSheetChangesUsingRAF - }; - } - - const STYLE_SELECTOR = 'style, link[rel*="stylesheet" i]:not([disabled])'; - function isFontsGoogleApiStyle(element) { - if (!element.href) { - return false; - } - try { - const elementURL = new URL(element.href); - return elementURL.hostname === "fonts.googleapis.com"; - } catch (err) { - logInfo(`Couldn't construct ${element.href} as URL`); - return false; - } - } - const hostsBreakingOnSVGStyleOverride = ["www.onet.pl"]; - function shouldManageStyle(element) { - return ( - (element instanceof HTMLStyleElement || - (element instanceof SVGStyleElement && - !hostsBreakingOnSVGStyleOverride.includes( - location.hostname - )) || - (element instanceof HTMLLinkElement && - Boolean(element.rel) && - element.rel.toLowerCase().includes("stylesheet") && - Boolean(element.href) && - !element.disabled && - (isFirefox - ? !element.href.startsWith("moz-extension://") - : true) && - !isFontsGoogleApiStyle(element))) && - !element.classList.contains("darkreader") && - element.media.toLowerCase() !== "print" && - !element.classList.contains("stylus") - ); - } - function getManageableStyles(node, results = [], deep = true) { - if (shouldManageStyle(node)) { - results.push(node); - } else if ( - node instanceof Element || - (isShadowDomSupported && node instanceof ShadowRoot) || - node === document - ) { - forEach(node.querySelectorAll(STYLE_SELECTOR), (style) => - getManageableStyles(style, results, false) - ); - if (deep) { - iterateShadowHosts(node, (host) => - getManageableStyles(host.shadowRoot, results, false) + discoveredNodes.add(node); + shadowRootDiscovered(n.shadowRoot); + deepWatchForInlineStyles( + n.shadowRoot, + elementStyleDidChange, + shadowRootDiscovered ); - } - } - return results; - } - const syncStyleSet = new WeakSet(); - const corsStyleSet = new WeakSet(); - let loadingLinkCounter = 0; - const rejectorsForLoadingLinks = new Map(); - function cleanLoadingLinks() { - rejectorsForLoadingLinks.clear(); - } - function manageStyle(element, { update, loadingStart, loadingEnd }) { - const prevStyles = []; - let next = element; - while ( - (next = next.nextElementSibling) && - next.matches(".darkreader") - ) { - prevStyles.push(next); + }); + variablesStore.matchVariablesAndDependents(); } - let corsCopy = - prevStyles.find( - (el) => el.matches(".darkreader--cors") && !corsStyleSet.has(el) - ) || null; - let syncStyle = - prevStyles.find( - (el) => el.matches(".darkreader--sync") && !syncStyleSet.has(el) - ) || null; - let corsCopyPositionWatcher = null; - let syncStylePositionWatcher = null; - let cancelAsyncOperations = false; - let isOverrideEmpty = true; - const isAsyncCancelled = () => cancelAsyncOperations; - const sheetModifier = createStyleSheetModifier(); - const observer = new MutationObserver((mutations) => { - if ( - mutations.some((m) => m.type === "characterData") && - containsCSSImport() - ) { - const cssText = (element.textContent ?? "").trim(); - createOrUpdateCORSCopy(cssText, location.href).then(update); - } else { - update(); + const treeObserver = createOptimizedTreeObserver(root, { + onMinorMutations: (_root, {additions}) => { + additions.forEach((added) => discoverNodes(added)); + }, + onHugeMutations: () => { + discoverNodes(root); } }); - const observerOptions = { - attributes: true, - childList: true, - subtree: true, - characterData: true - }; - function containsCSSImport() { - if (!(element instanceof HTMLStyleElement)) { - return false; - } - const cssText = removeCSSComments(element.textContent ?? "").trim(); - return cssText.match(cssImportRegex); - } - function hasImports(cssRules, checkCrossOrigin) { - let result = false; - if (cssRules) { - let rule; - cssRulesLoop: for ( - let i = 0, len = cssRules.length; - i < len; - i++ - ) { - rule = cssRules[i]; - if (rule.href) { - if (checkCrossOrigin) { - if ( - !rule.href.startsWith( - "https://fonts.googleapis.com/" - ) && - rule.href.startsWith("http") && - !rule.href.startsWith(location.origin) - ) { - result = true; - break cssRulesLoop; - } - } else { - result = true; - break cssRulesLoop; - } - } + treeObservers.set(root, treeObserver); + let attemptCount = 0; + let start = null; + const ATTEMPTS_INTERVAL = getDuration({seconds: 10}); + const RETRY_TIMEOUT = getDuration({seconds: 2}); + const MAX_ATTEMPTS_COUNT = 50; + let cache = []; + let timeoutId = null; + const handleAttributeMutations = throttle((mutations) => { + const handledTargets = new Set(); + mutations.forEach((m) => { + const target = m.target; + if (handledTargets.has(target)) { + return; } + if (INLINE_STYLE_ATTRS.includes(m.attributeName)) { + handledTargets.add(target); + elementStyleDidChange(target); + } + }); + variablesStore.matchVariablesAndDependents(); + }); + const attrObserver = new MutationObserver((mutations) => { + if (timeoutId) { + cache.push(...mutations); + return; } - return result; - } - function getRulesSync() { - if (corsCopy) { - return corsCopy.sheet.cssRules; - } - if (containsCSSImport()) { - return null; - } - const cssRules = safeGetSheetRules(); - if ( - element instanceof HTMLLinkElement && - !isRelativeHrefOnAbsolutePath(element.href) && - hasImports(cssRules, false) - ) { - return null; + attemptCount++; + const now = Date.now(); + if (start == null) { + start = now; + } else if (attemptCount >= MAX_ATTEMPTS_COUNT) { + if (now - start < ATTEMPTS_INTERVAL) { + timeoutId = setTimeout(() => { + start = null; + attemptCount = 0; + timeoutId = null; + const attributeCache = cache; + cache = []; + handleAttributeMutations(attributeCache); + }, RETRY_TIMEOUT); + cache.push(...mutations); + return; + } + start = now; + attemptCount = 1; } - if (hasImports(cssRules, true)) { - return null; + handleAttributeMutations(mutations); + }); + attrObserver.observe(root, { + attributes: true, + attributeFilter: INLINE_STYLE_ATTRS.concat( + overridesList.map(({dataAttr}) => dataAttr) + ), + subtree: true + }); + attrObservers.set(root, attrObserver); + } + function stopWatchingForInlineStyles() { + treeObservers.forEach((o) => o.disconnect()); + attrObservers.forEach((o) => o.disconnect()); + treeObservers.clear(); + attrObservers.clear(); + } + const inlineStyleCache = new WeakMap(); + const svgInversionCache = new WeakSet(); + const svgAnalysisConditionCache = new WeakMap(); + const themeProps = ["brightness", "contrast", "grayscale", "sepia", "mode"]; + function shouldAnalyzeSVGAsImage(svg) { + if (svgAnalysisConditionCache.has(svg)) { + return svgAnalysisConditionCache.get(svg); + } + const shouldAnalyze = Boolean( + svg && + (svg.getAttribute("class")?.includes("logo") || + svg.parentElement?.getAttribute("class")?.includes("logo")) + ); + svgAnalysisConditionCache.set(svg, shouldAnalyze); + return shouldAnalyze; + } + function getInlineStyleCacheKey(el, theme) { + return INLINE_STYLE_ATTRS.map( + (attr) => `${attr}="${el.getAttribute(attr)}"` + ) + .concat(themeProps.map((prop) => `${prop}="${theme[prop]}"`)) + .join(" "); + } + function shouldIgnoreInlineStyle(element, selectors) { + for (let i = 0, len = selectors.length; i < len; i++) { + const ingnoredSelector = selectors[i]; + if (element.matches(ingnoredSelector)) { + return true; } - return cssRules; } - function insertStyle() { - if (corsCopy) { - if (element.nextSibling !== corsCopy) { - element.parentNode.insertBefore( - corsCopy, - element.nextSibling - ); - } - if (corsCopy.nextSibling !== syncStyle) { - element.parentNode.insertBefore( - syncStyle, - corsCopy.nextSibling - ); + return false; + } + function overrideInlineStyle( + element, + theme, + ignoreInlineSelectors, + ignoreImageSelectors + ) { + const cacheKey = getInlineStyleCacheKey(element, theme); + if (cacheKey === inlineStyleCache.get(element)) { + return; + } + const unsetProps = new Set(Object.keys(overrides)); + function setCustomProp(targetCSSProp, modifierCSSProp, cssVal) { + const mod = getModifiableCSSDeclaration( + modifierCSSProp, + cssVal, + {style: element.style}, + variablesStore, + ignoreImageSelectors, + null + ); + if (!mod) { + return; + } + function setStaticValue(value) { + const {customProp, dataAttr} = + overrides[targetCSSProp] ?? + shorthandOverrides[targetCSSProp]; + element.style.setProperty(customProp, value); + if (!element.hasAttribute(dataAttr)) { + element.setAttribute(dataAttr, ""); } - } else if (element.nextSibling !== syncStyle) { - element.parentNode.insertBefore(syncStyle, element.nextSibling); + unsetProps.delete(targetCSSProp); } - } - function createSyncStyle() { - syncStyle = - element instanceof SVGStyleElement - ? document.createElementNS( - "http://www.w3.org/2000/svg", - "style" - ) - : document.createElement("style"); - syncStyle.classList.add("darkreader"); - syncStyle.classList.add("darkreader--sync"); - syncStyle.media = "screen"; - if (element.title) { - syncStyle.title = element.title; + function setVarDeclaration(mod) { + let prevDeclarations = []; + function setProps(declarations) { + prevDeclarations.forEach(({property}) => { + element.style.removeProperty(property); + }); + declarations.forEach(({property, value}) => { + if (!(value instanceof Promise)) { + element.style.setProperty(property, value); + } + }); + prevDeclarations = declarations; + } + setProps(mod.declarations); + mod.onTypeChange.addListener(setProps); } - syncStyleSet.add(syncStyle); - } - let isLoadingRules = false; - let wasLoadingError = false; - const loadingLinkId = ++loadingLinkCounter; - async function getRulesAsync() { - let cssText; - let cssBasePath; - if (element instanceof HTMLLinkElement) { - let [cssRules, accessError] = getRulesOrError(); - if ( - (isSafari && !element.sheet) || - (!isSafari && !cssRules && !accessError) || - isStillLoadingError(accessError) - ) { - try { - logInfo( - `Linkelement ${loadingLinkId} is not loaded yet and thus will be await for`, - element - ); - await linkLoading(element, loadingLinkId); - } catch (err) { - wasLoadingError = true; - } - if (cancelAsyncOperations) { - return null; + function setAsyncValue(promise, sourceValue) { + promise.then((value) => { + if ( + value && + targetCSSProp === "background" && + value.startsWith("var(--darkreader-bg--") + ) { + setStaticValue(value); } - [cssRules, accessError] = getRulesOrError(); - } - if (cssRules) { - if (!hasImports(cssRules, false)) { - return cssRules; + if (value && targetCSSProp === "background-image") { + if ( + (element === document.documentElement || + element === document.body) && + value === sourceValue + ) { + value = "none"; + } + setStaticValue(value); } - } - cssText = await loadText(element.href); - cssBasePath = getCSSBaseBath(element.href); - if (cancelAsyncOperations) { - return null; - } - } else if (containsCSSImport()) { - cssText = element.textContent.trim(); - cssBasePath = getCSSBaseBath(location.href); - } else { - return null; + inlineStyleCache.set( + element, + getInlineStyleCacheKey(element, theme) + ); + }); } - await createOrUpdateCORSCopy(cssText, cssBasePath); - if (corsCopy) { - return corsCopy.sheet.cssRules; + const value = + typeof mod.value === "function" ? mod.value(theme) : mod.value; + if (typeof value === "string") { + setStaticValue(value); + } else if (value instanceof Promise) { + setAsyncValue(value, cssVal); + } else if (typeof value === "object") { + setVarDeclaration(value); } - return null; } - async function createOrUpdateCORSCopy(cssText, cssBasePath) { - if (cssText) { - try { - const fullCSSText = await replaceCSSImports( - cssText, - cssBasePath + if (ignoreInlineSelectors.length > 0) { + if (shouldIgnoreInlineStyle(element, ignoreInlineSelectors)) { + unsetProps.forEach((cssProp) => { + element.removeAttribute(overrides[cssProp].dataAttr); + }); + return; + } + } + const isSVGElement = element instanceof SVGElement; + const svg = isSVGElement + ? (element.ownerSVGElement ?? + (element instanceof SVGSVGElement ? element : null)) + : null; + if (isSVGElement && theme.mode === 1 && svg) { + if (svgInversionCache.has(svg)) { + return; + } + if (shouldAnalyzeSVGAsImage(svg)) { + svgInversionCache.add(svg); + const analyzeSVGAsImage = () => { + let svgString = svg.outerHTML; + svgString = svgString.replaceAll( + '', + "" ); - if (corsCopy) { + const dataURL = `data:image/svg+xml;base64,${btoa(svgString)}`; + getImageDetails(dataURL).then((details) => { if ( - (corsCopy.textContent?.length ?? 0) < - fullCSSText.length + (details.isDark && details.isTransparent) || + (details.isLarge && + details.isLight && + !details.isTransparent) ) { - corsCopy.textContent = fullCSSText; + svg.setAttribute( + "data-darkreader-inline-invert", + "" + ); + } else { + svg.removeAttribute( + "data-darkreader-inline-invert" + ); } - } else { - corsCopy = createCORSCopy(element, fullCSSText); - } - } catch (err) { } - if (corsCopy) { - corsCopyPositionWatcher = watchForNodePosition( - corsCopy, - "prev-sibling" - ); + }); + }; + analyzeSVGAsImage(); + if (!isDOMReady()) { + addDOMReadyListener(analyzeSVGAsImage); } + return; } } - function details(options) { - const rules = getRulesSync(); - if (!rules) { - if (options.secondRound) { - return null; - } - if (isLoadingRules || wasLoadingError) { - return null; - } - isLoadingRules = true; - loadingStart(); - getRulesAsync() - .then((results) => { - isLoadingRules = false; - loadingEnd(); - if (results) { - update(); - } - }) - .catch((err) => { - isLoadingRules = false; - loadingEnd(); - }); - return null; + if (element.hasAttribute("bgcolor")) { + let value = element.getAttribute("bgcolor"); + if ( + value.match(/^[0-9a-f]{3}$/i) || + value.match(/^[0-9a-f]{6}$/i) + ) { + value = `#${value}`; } - return { rules }; + setCustomProp("background-color", "background-color", value); } - let forceRenderStyle = false; - function render(theme, ignoreImageAnalysis) { - const rules = getRulesSync(); - if (!rules) { - return; + if ( + (element === document.documentElement || + element === document.body) && + element.hasAttribute("background") + ) { + const url = getAbsoluteURL( + location.href, + element.getAttribute("background") ?? "" + ); + const value = `url("${url}")`; + setCustomProp("background-image", "background-image", value); + } + if (element.hasAttribute("color") && element.rel !== "mask-icon") { + let value = element.getAttribute("color"); + if ( + value.match(/^[0-9a-f]{3}$/i) || + value.match(/^[0-9a-f]{6}$/i) + ) { + value = `#${value}`; } - cancelAsyncOperations = false; - function removeCSSRulesFromSheet(sheet) { - if (!sheet) { - return; - } - for (let i = sheet.cssRules.length - 1; i >= 0; i--) { - sheet.deleteRule(i); + setCustomProp("color", "color", value); + } + if (isSVGElement) { + if (element.hasAttribute("fill")) { + const SMALL_SVG_LIMIT = 32; + const value = element.getAttribute("fill"); + if (value !== "none") { + if (!(element instanceof SVGTextElement)) { + const handleSVGElement = () => { + const {width, height} = + element.getBoundingClientRect(); + const isBg = + width > SMALL_SVG_LIMIT || + height > SMALL_SVG_LIMIT; + setCustomProp( + "fill", + isBg ? "background-color" : "color", + value + ); + }; + if (isReadyStateComplete()) { + handleSVGElement(); + } else { + addReadyStateCompleteListener(handleSVGElement); + } + } else { + setCustomProp("fill", "color", value); + } } } - function prepareOverridesSheet() { - if (!syncStyle) { - createSyncStyle(); - } - syncStylePositionWatcher && syncStylePositionWatcher.stop(); - insertStyle(); - if (syncStyle.sheet == null) { - syncStyle.textContent = ""; + if (element.hasAttribute("stop-color")) { + setCustomProp( + "stop-color", + "background-color", + element.getAttribute("stop-color") + ); + } + } + if (element.hasAttribute("stroke")) { + const value = element.getAttribute("stroke"); + setCustomProp( + "stroke", + element instanceof SVGLineElement || + element instanceof SVGTextElement + ? "border-color" + : "color", + value + ); + } + element.style && + iterateCSSDeclarations(element.style, (property, value) => { + if (property === "background-image" && value.includes("url")) { + if ( + element === document.documentElement || + element === document.body + ) { + setCustomProp(property, property, value); + } + return; } - const sheet = syncStyle.sheet; - removeCSSRulesFromSheet(sheet); - if (syncStylePositionWatcher) { - syncStylePositionWatcher.run(); + if ( + overrides.hasOwnProperty(property) || + (property.startsWith("--") && !normalizedPropList[property]) + ) { + setCustomProp(property, property, value); + } else if ( + property === "background" && + value.includes("var(") + ) { + setCustomProp("background", "background", value); } else { - syncStylePositionWatcher = watchForNodePosition( - syncStyle, - "prev-sibling", - () => { - forceRenderStyle = true; - buildOverrides(); + const overriddenProp = normalizedPropList[property]; + if ( + overriddenProp && + !element.style.getPropertyValue(overriddenProp) && + !element.hasAttribute(overriddenProp) + ) { + if ( + overriddenProp === "background-color" && + element.hasAttribute("bgcolor") + ) { + return; } - ); - } - return syncStyle.sheet; - } - function buildOverrides() { - const force = forceRenderStyle; - forceRenderStyle = false; - sheetModifier.modifySheet({ - prepareSheet: prepareOverridesSheet, - sourceCSSRules: rules, - theme, - ignoreImageAnalysis, - force, - isAsyncCancelled - }); - isOverrideEmpty = syncStyle.sheet.cssRules.length === 0; - if (sheetModifier.shouldRebuildStyle()) { - addReadyStateCompleteListener(() => update()); + element.style.setProperty(property, ""); + } } - } - buildOverrides(); + }); + if ( + element.style && + element instanceof SVGTextElement && + element.style.fill + ) { + setCustomProp( + "fill", + "color", + element.style.getPropertyValue("fill") + ); } - function getRulesOrError() { - try { - if (element.sheet == null) { - return [null, null]; - } - return [element.sheet.cssRules, null]; - } catch (err) { - return [null, err]; - } + if (element.getAttribute("style")?.includes("--")) { + variablesStore.addInlineStyleForMatching(element.style); } - function isStillLoadingError(error) { - return error && error.message && error.message.includes("loading"); + forEach(unsetProps, (cssProp) => { + element.removeAttribute(overrides[cssProp].dataAttr); + }); + inlineStyleCache.set(element, getInlineStyleCacheKey(element, theme)); + } + + const metaThemeColorName = "theme-color"; + const metaThemeColorSelector = `meta[name="${metaThemeColorName}"]`; + let srcMetaThemeColor = null; + let observer = null; + function changeMetaThemeColor(meta, theme) { + srcMetaThemeColor = srcMetaThemeColor || meta.content; + const color = parseColorWithCache(srcMetaThemeColor); + if (!color) { + return; } - function safeGetSheetRules() { - const [cssRules, err] = getRulesOrError(); - if (err) { - return null; + meta.content = modifyBackgroundColor(color, theme, false); + } + function changeMetaThemeColorWhenAvailable(theme) { + const meta = document.querySelector(metaThemeColorSelector); + if (meta) { + changeMetaThemeColor(meta, theme); + } else { + if (observer) { + observer.disconnect(); } - return cssRules; + observer = new MutationObserver((mutations) => { + loop: for (let i = 0; i < mutations.length; i++) { + const {addedNodes} = mutations[i]; + for (let j = 0; j < addedNodes.length; j++) { + const node = addedNodes[j]; + if ( + node instanceof HTMLMetaElement && + node.name === metaThemeColorName + ) { + observer.disconnect(); + observer = null; + changeMetaThemeColor(node, theme); + break loop; + } + } + } + }); + observer.observe(document.head, {childList: true}); } - const sheetChangeWatcher = createSheetWatcher( - element, - safeGetSheetRules, - update, - isAsyncCancelled - ); - function pause() { + } + function restoreMetaThemeColor() { + if (observer) { observer.disconnect(); - cancelAsyncOperations = true; - corsCopyPositionWatcher && corsCopyPositionWatcher.stop(); - syncStylePositionWatcher && syncStylePositionWatcher.stop(); - sheetChangeWatcher.stop(); + observer = null; } - function destroy() { - pause(); - removeNode(corsCopy); - removeNode(syncStyle); - loadingEnd(); - if (rejectorsForLoadingLinks.has(loadingLinkId)) { - const reject = rejectorsForLoadingLinks.get(loadingLinkId); - rejectorsForLoadingLinks.delete(loadingLinkId); - reject && reject(); - } + const meta = document.querySelector(metaThemeColorSelector); + if (meta && srcMetaThemeColor) { + meta.content = srcMetaThemeColor; } - function watch() { - observer.observe(element, observerOptions); - if (element instanceof HTMLStyleElement) { - sheetChangeWatcher.start(); + } + + const cssCommentsRegex = /\/\*[\s\S]*?\*\//g; + function removeCSSComments(cssText) { + return cssText.replace(cssCommentsRegex, ""); + } + + let canUseSheetProxy = false; + document.addEventListener( + "__darkreader__inlineScriptsAllowed", + () => (canUseSheetProxy = true), + {once: true} + ); + function createSheetWatcher( + element, + safeGetSheetRules, + callback, + isCancelled + ) { + let rafSheetWatcher = null; + function watchForSheetChanges() { + watchForSheetChangesUsingProxy(); + if (!(canUseSheetProxy && element.sheet)) { + rafSheetWatcher = createRAFSheetWatcher( + element, + safeGetSheetRules, + callback, + isCancelled + ); + rafSheetWatcher.start(); } } - const maxMoveCount = 10; - let moveCount = 0; - function restore() { - if (!syncStyle) { - return; - } - moveCount++; - if (moveCount > maxMoveCount) { + let areSheetChangesPending = false; + function onSheetChange() { + canUseSheetProxy = true; + rafSheetWatcher?.stop(); + if (areSheetChangesPending) { return; } - insertStyle(); - corsCopyPositionWatcher && corsCopyPositionWatcher.skip(); - syncStylePositionWatcher && syncStylePositionWatcher.skip(); - if (!isOverrideEmpty) { - forceRenderStyle = true; - update(); + function handleSheetChanges() { + areSheetChangesPending = false; + if (isCancelled()) { + return; + } + callback(); } + areSheetChangesPending = true; + queueMicrotask(handleSheetChanges); + } + function watchForSheetChangesUsingProxy() { + element.addEventListener( + "__darkreader__updateSheet", + onSheetChange + ); + } + function stopWatchingForSheetChangesUsingProxy() { + element.removeEventListener( + "__darkreader__updateSheet", + onSheetChange + ); + } + function stopWatchingForSheetChanges() { + stopWatchingForSheetChangesUsingProxy(); + rafSheetWatcher?.stop(); } return { - details, - render, - pause, - destroy, - watch, - restore + start: watchForSheetChanges, + stop: stopWatchingForSheetChanges }; } - async function linkLoading(link, loadingId) { - return new Promise((resolve, reject) => { - const cleanUp = () => { - link.removeEventListener("load", onLoad); - link.removeEventListener("error", onError); - rejectorsForLoadingLinks.delete(loadingId); - }; - const onLoad = () => { - cleanUp(); - resolve(); - }; - const onError = () => { - cleanUp(); - reject( - `Linkelement ${loadingId} couldn't be loaded. ${link.href}` - ); + function createRAFSheetWatcher( + element, + safeGetSheetRules, + callback, + isCancelled + ) { + let rulesChangeKey = null; + let rulesCheckFrameId = null; + function getRulesChangeKey() { + const rules = safeGetSheetRules(); + return rules ? rules.length : null; + } + function didRulesKeyChange() { + return getRulesChangeKey() !== rulesChangeKey; + } + function watchForSheetChangesUsingRAF() { + rulesChangeKey = getRulesChangeKey(); + stopWatchingForSheetChangesUsingRAF(); + const checkForUpdate = () => { + const cancelled = isCancelled(); + if (!cancelled && didRulesKeyChange()) { + rulesChangeKey = getRulesChangeKey(); + callback(); + } + if (cancelled || (canUseSheetProxy && element.sheet)) { + stopWatchingForSheetChangesUsingRAF(); + return; + } + rulesCheckFrameId = requestAnimationFrame(checkForUpdate); }; - rejectorsForLoadingLinks.set(loadingId, () => { - cleanUp(); - reject(); - }); - link.addEventListener("load", onLoad, { passive: true }); - link.addEventListener("error", onError, { passive: true }); - if (!link.href) { - onError(); - } - }); - } - function getCSSImportURL(importDeclaration) { - return getCSSURLValue( - importDeclaration - .substring(7) - .trim() - .replace(/;$/, "") - .replace(/screen$/, "") - ); + checkForUpdate(); + } + function stopWatchingForSheetChangesUsingRAF() { + rulesCheckFrameId && cancelAnimationFrame(rulesCheckFrameId); + } + return { + start: watchForSheetChangesUsingRAF, + stop: stopWatchingForSheetChangesUsingRAF + }; } - async function loadText(url) { - if (url.startsWith("data:")) { - return await (await fetch(url)).text(); + + const STYLE_SELECTOR = 'style, link[rel*="stylesheet" i]:not([disabled])'; + function isFontsGoogleApiStyle(element) { + if (!element.href) { + return false; } - const parsedURL = new URL(url); - if (parsedURL.origin === location.origin) { - return await loadAsText(url, "text/css", location.origin); + try { + const elementURL = new URL(element.href); + return elementURL.hostname === "fonts.googleapis.com"; + } catch (err) { + logInfo(`Couldn't construct ${element.href} as URL`); + return false; } - return await bgFetch({ - url, - responseType: "text", - mimeType: "text/css", - origin: location.origin - }); } - async function replaceCSSImports(cssText, basePath, cache = new Map()) { - cssText = removeCSSComments(cssText); - cssText = replaceCSSFontFace(cssText); - cssText = replaceCSSRelativeURLsWithAbsolute(cssText, basePath); - const importMatches = getMatches(cssImportRegex, cssText); - for (const match of importMatches) { - const importURL = getCSSImportURL(match); - const absoluteURL = getAbsoluteURL(basePath, importURL); - let importedCSS; - if (cache.has(absoluteURL)) { - importedCSS = cache.get(absoluteURL); - } else { - try { - importedCSS = await loadText(absoluteURL); - cache.set(absoluteURL, importedCSS); - importedCSS = await replaceCSSImports( - importedCSS, - getCSSBaseBath(absoluteURL), - cache - ); - } catch (err) { - importedCSS = ""; - } + const hostsBreakingOnSVGStyleOverride = [ + "account.containerstore.com", + "containerstore.com", + "www.onet.pl" + ]; + function shouldManageStyle(element) { + return ( + (element instanceof HTMLStyleElement || + (element instanceof SVGStyleElement && + !hostsBreakingOnSVGStyleOverride.includes( + location.hostname + )) || + (element instanceof HTMLLinkElement && + Boolean(element.rel) && + element.rel.toLowerCase().includes("stylesheet") && + Boolean(element.href) && + !element.disabled && + (isFirefox + ? !element.href.startsWith("moz-extension://") + : true) && + !isFontsGoogleApiStyle(element))) && + !element.classList.contains("darkreader") && + element.media.toLowerCase() !== "print" && + !element.classList.contains("stylus") + ); + } + function getManageableStyles(node, results = [], deep = true) { + if (shouldManageStyle(node)) { + results.push(node); + } else if ( + node instanceof Element || + (isShadowDomSupported && node instanceof ShadowRoot) || + node === document + ) { + forEach(node.querySelectorAll(STYLE_SELECTOR), (style) => + getManageableStyles(style, results, false) + ); + if (deep) { + iterateShadowHosts(node, (host) => + getManageableStyles(host.shadowRoot, results, false) + ); } - cssText = cssText.split(match).join(importedCSS); } - cssText = cssText.trim(); - return cssText; + return results; } - function createCORSCopy(srcElement, cssText) { - if (!cssText) { - return null; - } - const cors = document.createElement("style"); - cors.classList.add("darkreader"); - cors.classList.add("darkreader--cors"); - cors.media = "screen"; - cors.textContent = cssText; - srcElement.parentNode.insertBefore(cors, srcElement.nextSibling); - cors.sheet.disabled = true; - corsStyleSet.add(cors); - return cors; + const syncStyleSet = new WeakSet(); + const corsStyleSet = new WeakSet(); + let loadingLinkCounter = 0; + const rejectorsForLoadingLinks = new Map(); + function cleanLoadingLinks() { + rejectorsForLoadingLinks.clear(); } - - const definedCustomElements = new Set(); - const undefinedGroups = new Map(); - let elementsDefinitionCallback; - function isCustomElement(element) { - if (element.tagName.includes("-") || element.getAttribute("is")) { - return true; + function manageStyle(element, {update, loadingStart, loadingEnd}) { + const prevStyles = []; + let next = element; + while ( + (next = next.nextElementSibling) && + next.matches(".darkreader") + ) { + prevStyles.push(next); } - return false; - } - function recordUndefinedElement(element) { - let tag = element.tagName.toLowerCase(); - if (!tag.includes("-")) { - const extendedTag = element.getAttribute("is"); - if (extendedTag) { - tag = extendedTag; + let corsCopy = + prevStyles.find( + (el) => el.matches(".darkreader--cors") && !corsStyleSet.has(el) + ) || null; + let syncStyle = + prevStyles.find( + (el) => el.matches(".darkreader--sync") && !syncStyleSet.has(el) + ) || null; + let corsCopyPositionWatcher = null; + let syncStylePositionWatcher = null; + let cancelAsyncOperations = false; + let isOverrideEmpty = true; + const isAsyncCancelled = () => cancelAsyncOperations; + const sheetModifier = createStyleSheetModifier(); + const observer = new MutationObserver((mutations) => { + if ( + mutations.some((m) => m.type === "characterData") && + containsCSSImport() + ) { + const cssText = (element.textContent ?? "").trim(); + createOrUpdateCORSCopy(cssText, location.href).then(update); } else { - return; + update(); + } + }); + const observerOptions = { + attributes: true, + childList: true, + subtree: true, + characterData: true + }; + function containsCSSImport() { + if (!(element instanceof HTMLStyleElement)) { + return false; } + const cssText = removeCSSComments(element.textContent ?? "").trim(); + return cssText.match(cssImportRegex); } - if (!undefinedGroups.has(tag)) { - undefinedGroups.set(tag, new Set()); - customElementsWhenDefined(tag).then(() => { - if (elementsDefinitionCallback) { - const elements = undefinedGroups.get(tag); - undefinedGroups.delete(tag); - elementsDefinitionCallback(Array.from(elements)); + function hasImports(cssRules, checkCrossOrigin) { + let result = false; + if (cssRules) { + let rule; + cssRulesLoop: for ( + let i = 0, len = cssRules.length; + i < len; + i++ + ) { + rule = cssRules[i]; + if (rule.href) { + if (checkCrossOrigin) { + if ( + !rule.href.startsWith( + "https://fonts.googleapis.com/" + ) && + rule.href.startsWith("http") && + !rule.href.startsWith(location.origin) + ) { + result = true; + break cssRulesLoop; + } + } else { + result = true; + break cssRulesLoop; + } + } } - }); + } + return result; } - undefinedGroups.get(tag).add(element); - } - function collectUndefinedElements(root) { - if (!isDefinedSelectorSupported) { - return; + function getRulesSync() { + if (corsCopy) { + return corsCopy.sheet.cssRules; + } + if (containsCSSImport()) { + return null; + } + const cssRules = safeGetSheetRules(); + if ( + element instanceof HTMLLinkElement && + !isRelativeHrefOnAbsolutePath(element.href) && + hasImports(cssRules, false) + ) { + return null; + } + if (hasImports(cssRules, true)) { + return null; + } + return cssRules; } - forEach( - root.querySelectorAll(":not(:defined)"), - recordUndefinedElement - ); - } - let canOptimizeUsingProxy = false; - document.addEventListener( - "__darkreader__inlineScriptsAllowed", - () => { - canOptimizeUsingProxy = true; - }, - { once: true, passive: true } - ); - const resolvers = new Map(); - function handleIsDefined(e) { - canOptimizeUsingProxy = true; - const tag = e.detail.tag; - definedCustomElements.add(tag); - if (resolvers.has(tag)) { - const r = resolvers.get(tag); - resolvers.delete(tag); - r.forEach((r) => r()); + function insertStyle() { + if (corsCopy) { + if (element.nextSibling !== corsCopy) { + element.parentNode.insertBefore( + corsCopy, + element.nextSibling + ); + } + if (corsCopy.nextSibling !== syncStyle) { + element.parentNode.insertBefore( + syncStyle, + corsCopy.nextSibling + ); + } + } else if (element.nextSibling !== syncStyle) { + element.parentNode.insertBefore(syncStyle, element.nextSibling); + } } - } - async function customElementsWhenDefined(tag) { - if (definedCustomElements.has(tag)) { - return; + function createSyncStyle() { + syncStyle = + element instanceof SVGStyleElement + ? document.createElementNS( + "http://www.w3.org/2000/svg", + "style" + ) + : document.createElement("style"); + syncStyle.classList.add("darkreader"); + syncStyle.classList.add("darkreader--sync"); + syncStyle.media = "screen"; + if (element.title) { + syncStyle.title = element.title; + } + syncStyleSet.add(syncStyle); } - return new Promise((resolve) => { - if ( - window.customElements && - typeof customElements.whenDefined === "function" - ) { - customElements.whenDefined(tag).then(() => resolve()); - } else if (canOptimizeUsingProxy) { - if (resolvers.has(tag)) { - resolvers.get(tag).push(resolve); - } else { - resolvers.set(tag, [resolve]); + let isLoadingRules = false; + let wasLoadingError = false; + const loadingLinkId = ++loadingLinkCounter; + async function getRulesAsync() { + let cssText; + let cssBasePath; + if (element instanceof HTMLLinkElement) { + let [cssRules, accessError] = getRulesOrError(); + if ( + (isSafari && !element.sheet) || + (!isSafari && !cssRules && !accessError) || + isStillLoadingError(accessError) + ) { + try { + logInfo( + `Linkelement ${loadingLinkId} is not loaded yet and thus will be await for`, + element + ); + await linkLoading(element, loadingLinkId); + } catch (err) { + wasLoadingError = true; + } + if (cancelAsyncOperations) { + return null; + } + [cssRules, accessError] = getRulesOrError(); } - document.dispatchEvent( - new CustomEvent("__darkreader__addUndefinedResolver", { - detail: { tag } - }) - ); + if (cssRules) { + if (!hasImports(cssRules, false)) { + return cssRules; + } + } + cssText = await loadText(element.href); + cssBasePath = getCSSBaseBath(element.href); + if (cancelAsyncOperations) { + return null; + } + } else if (containsCSSImport()) { + cssText = element.textContent.trim(); + cssBasePath = getCSSBaseBath(location.href); } else { - const checkIfDefined = () => { - const elements = undefinedGroups.get(tag); - if (elements && elements.size > 0) { + return null; + } + await createOrUpdateCORSCopy(cssText, cssBasePath); + if (corsCopy) { + return corsCopy.sheet.cssRules; + } + return null; + } + async function createOrUpdateCORSCopy(cssText, cssBasePath) { + if (cssText) { + try { + const fullCSSText = await replaceCSSImports( + cssText, + cssBasePath + ); + if (corsCopy) { if ( - elements.values().next().value.matches(":defined") + (corsCopy.textContent?.length ?? 0) < + fullCSSText.length ) { - resolve(); - } else { - requestAnimationFrame(checkIfDefined); + corsCopy.textContent = fullCSSText; } + } else { + corsCopy = createCORSCopy(element, fullCSSText); } - }; - requestAnimationFrame(checkIfDefined); - } - }); - } - function watchWhenCustomElementsDefined(callback) { - elementsDefinitionCallback = callback; - } - function unsubscribeFromDefineCustomElements() { - elementsDefinitionCallback = null; - undefinedGroups.clear(); - document.removeEventListener( - "__darkreader__isDefined", - handleIsDefined - ); - } - - const observers = []; - let observedRoots; - function watchForStylePositions( - currentStyles, - update, - shadowRootDiscovered - ) { - stopWatchingForStylePositions(); - const prevStylesByRoot = new WeakMap(); - const getPrevStyles = (root) => { - if (!prevStylesByRoot.has(root)) { - prevStylesByRoot.set(root, new Set()); - } - return prevStylesByRoot.get(root); - }; - currentStyles.forEach((node) => { - let root = node; - while ((root = root.parentNode)) { - if ( - root === document || - root.nodeType === Node.DOCUMENT_FRAGMENT_NODE - ) { - const prevStyles = getPrevStyles(root); - prevStyles.add(node); - break; + } catch (err) {} + if (corsCopy) { + corsCopyPositionWatcher = watchForNodePosition( + corsCopy, + "prev-sibling" + ); } } - }); - const prevStyleSiblings = new WeakMap(); - const nextStyleSiblings = new WeakMap(); - function saveStylePosition(style) { - prevStyleSiblings.set(style, style.previousElementSibling); - nextStyleSiblings.set(style, style.nextElementSibling); - } - function forgetStylePosition(style) { - prevStyleSiblings.delete(style); - nextStyleSiblings.delete(style); - } - function didStylePositionChange(style) { - return ( - style.previousElementSibling !== prevStyleSiblings.get(style) || - style.nextElementSibling !== nextStyleSiblings.get(style) - ); } - currentStyles.forEach(saveStylePosition); - function handleStyleOperations(root, operations) { - const { createdStyles, removedStyles, movedStyles } = operations; - createdStyles.forEach((s) => saveStylePosition(s)); - movedStyles.forEach((s) => saveStylePosition(s)); - removedStyles.forEach((s) => forgetStylePosition(s)); - const prevStyles = getPrevStyles(root); - createdStyles.forEach((s) => prevStyles.add(s)); - removedStyles.forEach((s) => prevStyles.delete(s)); - if ( - createdStyles.size + removedStyles.size + movedStyles.size > - 0 - ) { - update({ - created: Array.from(createdStyles), - removed: Array.from(removedStyles), - moved: Array.from(movedStyles), - updated: [] - }); + function details(options) { + const rules = getRulesSync(); + if (!rules) { + if (options.secondRound) { + return null; + } + if (isLoadingRules || wasLoadingError) { + return null; + } + isLoadingRules = true; + loadingStart(); + getRulesAsync() + .then((results) => { + isLoadingRules = false; + loadingEnd(); + if (results) { + update(); + } + }) + .catch((err) => { + isLoadingRules = false; + loadingEnd(); + }); + return null; } + return {rules}; } - function handleMinorTreeMutations(root, { additions, moves, deletions }) { - const createdStyles = new Set(); - const removedStyles = new Set(); - const movedStyles = new Set(); - additions.forEach((node) => - getManageableStyles(node).forEach((style) => - createdStyles.add(style) - ) - ); - deletions.forEach((node) => - getManageableStyles(node).forEach((style) => - removedStyles.add(style) - ) - ); - moves.forEach((node) => - getManageableStyles(node).forEach((style) => - movedStyles.add(style) - ) - ); - handleStyleOperations(root, { - createdStyles, - removedStyles, - movedStyles - }); - additions.forEach((n) => { - deepObserve(n); - collectUndefinedElements(n); - }); - additions.forEach( - (node) => isCustomElement(node) && recordUndefinedElement(node) - ); - } - function handleHugeTreeMutations(root) { - const styles = new Set(getManageableStyles(root)); - const createdStyles = new Set(); - const removedStyles = new Set(); - const movedStyles = new Set(); - const prevStyles = getPrevStyles(root); - styles.forEach((s) => { - if (!prevStyles.has(s)) { - createdStyles.add(s); + let forceRenderStyle = false; + function render(theme, ignoreImageAnalysis) { + const rules = getRulesSync(); + if (!rules) { + return; + } + cancelAsyncOperations = false; + function removeCSSRulesFromSheet(sheet) { + if (!sheet) { + return; } - }); - prevStyles.forEach((s) => { - if (!styles.has(s)) { - removedStyles.add(s); + for (let i = sheet.cssRules.length - 1; i >= 0; i--) { + sheet.deleteRule(i); } - }); - styles.forEach((s) => { - if ( - !createdStyles.has(s) && - !removedStyles.has(s) && - didStylePositionChange(s) - ) { - movedStyles.add(s); + } + function prepareOverridesSheet() { + if (!syncStyle) { + createSyncStyle(); } - }); - handleStyleOperations(root, { - createdStyles, - removedStyles, - movedStyles - }); - deepObserve(root); - collectUndefinedElements(root); - } - function handleAttributeMutations(mutations) { - const updatedStyles = new Set(); - const removedStyles = new Set(); - mutations.forEach((m) => { - const { target } = m; - if (target.isConnected) { - if (shouldManageStyle(target)) { - updatedStyles.add(target); - } else if ( - target instanceof HTMLLinkElement && - target.disabled - ) { - removedStyles.add(target); - } + syncStylePositionWatcher && syncStylePositionWatcher.stop(); + insertStyle(); + if (syncStyle.sheet == null) { + syncStyle.textContent = ""; + } + const sheet = syncStyle.sheet; + removeCSSRulesFromSheet(sheet); + if (syncStylePositionWatcher) { + syncStylePositionWatcher.run(); + } else { + syncStylePositionWatcher = watchForNodePosition( + syncStyle, + "prev-sibling", + () => { + forceRenderStyle = true; + buildOverrides(); + } + ); } - }); - if (updatedStyles.size + removedStyles.size > 0) { - update({ - updated: Array.from(updatedStyles), - created: [], - removed: Array.from(removedStyles), - moved: [] + return syncStyle.sheet; + } + function buildOverrides() { + const force = forceRenderStyle; + forceRenderStyle = false; + sheetModifier.modifySheet({ + prepareSheet: prepareOverridesSheet, + sourceCSSRules: rules, + theme, + ignoreImageAnalysis, + force, + isAsyncCancelled }); + isOverrideEmpty = syncStyle.sheet.cssRules.length === 0; + if (sheetModifier.shouldRebuildStyle()) { + addReadyStateCompleteListener(() => update()); + } } + buildOverrides(); } - function observe(root) { - if (observedRoots.has(root)) { - return; + function getRulesOrError() { + try { + if (element.sheet == null) { + return [null, null]; + } + return [element.sheet.cssRules, null]; + } catch (err) { + return [null, err]; } - const treeObserver = createOptimizedTreeObserver(root, { - onMinorMutations: handleMinorTreeMutations, - onHugeMutations: handleHugeTreeMutations - }); - const attrObserver = new MutationObserver(handleAttributeMutations); - attrObserver.observe(root, { - attributeFilter: ["rel", "disabled", "media", "href"], - subtree: true - }); - observers.push(treeObserver, attrObserver); - observedRoots.add(root); } - function subscribeForShadowRootChanges(node) { - const { shadowRoot } = node; - if (shadowRoot == null || observedRoots.has(shadowRoot)) { - return; + function isStillLoadingError(error) { + return error && error.message && error.message.includes("loading"); + } + function safeGetSheetRules() { + const [cssRules, err] = getRulesOrError(); + if (err) { + return null; } - observe(shadowRoot); - shadowRootDiscovered(shadowRoot); + return cssRules; } - function deepObserve(node) { - iterateShadowHosts(node, subscribeForShadowRootChanges); + const sheetChangeWatcher = createSheetWatcher( + element, + safeGetSheetRules, + update, + isAsyncCancelled + ); + function pause() { + observer.disconnect(); + cancelAsyncOperations = true; + corsCopyPositionWatcher && corsCopyPositionWatcher.stop(); + syncStylePositionWatcher && syncStylePositionWatcher.stop(); + sheetChangeWatcher.stop(); } - observe(document); - deepObserve(document.documentElement); - watchWhenCustomElementsDefined((hosts) => { - const newStyles = []; - hosts.forEach((host) => - push(newStyles, getManageableStyles(host.shadowRoot)) - ); - update({ created: newStyles, updated: [], removed: [], moved: [] }); - hosts.forEach((host) => { - const { shadowRoot } = host; - if (shadowRoot == null) { - return; - } - subscribeForShadowRootChanges(host); - deepObserve(shadowRoot); - collectUndefinedElements(shadowRoot); + function destroy() { + pause(); + removeNode(corsCopy); + removeNode(syncStyle); + loadingEnd(); + if (rejectorsForLoadingLinks.has(loadingLinkId)) { + const reject = rejectorsForLoadingLinks.get(loadingLinkId); + rejectorsForLoadingLinks.delete(loadingLinkId); + reject && reject(); + } + } + function watch() { + observer.observe(element, observerOptions); + if (element instanceof HTMLStyleElement) { + sheetChangeWatcher.start(); + } + } + const maxMoveCount = 10; + let moveCount = 0; + function restore() { + if (!syncStyle) { + return; + } + moveCount++; + if (moveCount > maxMoveCount) { + return; + } + insertStyle(); + corsCopyPositionWatcher && corsCopyPositionWatcher.skip(); + syncStylePositionWatcher && syncStylePositionWatcher.skip(); + if (!isOverrideEmpty) { + forceRenderStyle = true; + update(); + } + } + return { + details, + render, + pause, + destroy, + watch, + restore + }; + } + async function linkLoading(link, loadingId) { + return new Promise((resolve, reject) => { + const cleanUp = () => { + link.removeEventListener("load", onLoad); + link.removeEventListener("error", onError); + rejectorsForLoadingLinks.delete(loadingId); + }; + const onLoad = () => { + cleanUp(); + resolve(); + }; + const onError = () => { + cleanUp(); + reject( + `Linkelement ${loadingId} couldn't be loaded. ${link.href}` + ); + }; + rejectorsForLoadingLinks.set(loadingId, () => { + cleanUp(); + reject(); }); + link.addEventListener("load", onLoad, {passive: true}); + link.addEventListener("error", onError, {passive: true}); + if (!link.href) { + onError(); + } }); - document.addEventListener("__darkreader__isDefined", handleIsDefined); - collectUndefinedElements(document); } - function resetObservers() { - observers.forEach((o) => o.disconnect()); - observers.splice(0, observers.length); - observedRoots = new WeakSet(); + function getCSSImportURL(importDeclaration) { + return getCSSURLValue( + importDeclaration + .substring(7) + .trim() + .replace(/;$/, "") + .replace(/screen$/, "") + ); } - function stopWatchingForStylePositions() { - resetObservers(); - unsubscribeFromDefineCustomElements(); + async function loadText(url) { + if (url.startsWith("data:")) { + return await (await fetch(url)).text(); + } + const cache = readCSSFetchCache(url); + if (cache) { + return cache; + } + const parsedURL = new URL(url); + let text; + if (parsedURL.origin === location.origin) { + text = await loadAsText(url, "text/css", location.origin); + } + text = await bgFetch({ + url, + responseType: "text", + mimeType: "text/css", + origin: location.origin + }); + writeCSSFetchCache(url, text); + return text; } - - function watchForStyleChanges(currentStyles, update, shadowRootDiscovered) { - watchForStylePositions(currentStyles, update, shadowRootDiscovered); + async function replaceCSSImports(cssText, basePath, cache = new Map()) { + cssText = removeCSSComments(cssText); + cssText = replaceCSSFontFace(cssText); + cssText = replaceCSSRelativeURLsWithAbsolute(cssText, basePath); + const importMatches = getMatchesWithOffsets(cssImportRegex, cssText); + let prev = null; + let shouldIgnoreImportsInBetween = false; + let diff = 0; + for (const match of importMatches) { + let importedCSS; + const prevImportEnd = prev ? prev.offset + prev.text.length : 0; + const nextImportStart = match.offset; + const openBraceIndex = cssText.indexOf("{", prevImportEnd); + const closeBraceIndex = cssText.indexOf("}", prevImportEnd); + if ( + shouldIgnoreImportsInBetween || + (openBraceIndex >= 0 && + openBraceIndex < nextImportStart && + closeBraceIndex >= 0 && + closeBraceIndex < nextImportStart) + ) { + shouldIgnoreImportsInBetween = true; + importedCSS = ""; + } else { + const importURL = getCSSImportURL(match.text); + const absoluteURL = getAbsoluteURL(basePath, importURL); + if (cache.has(absoluteURL)) { + importedCSS = cache.get(absoluteURL); + } else { + try { + importedCSS = await loadText(absoluteURL); + cache.set(absoluteURL, importedCSS); + importedCSS = await replaceCSSImports( + importedCSS, + getCSSBaseBath(absoluteURL), + cache + ); + } catch (err) { + importedCSS = ""; + } + } + } + cssText = + cssText.substring(0, match.offset + diff) + + importedCSS + + cssText.substring(match.offset + match.text.length + diff); + diff = importedCSS.length - match.text.length; + prev = match; + } + cssText = cssText.trim(); + return cssText; } - function stopWatchingForStyleChanges() { - stopWatchingForStylePositions(); + function createCORSCopy(srcElement, cssText) { + if (!cssText) { + return null; + } + const cors = document.createElement("style"); + cors.classList.add("darkreader"); + cors.classList.add("darkreader--cors"); + cors.media = "screen"; + cors.textContent = cssText; + srcElement.parentNode.insertBefore(cors, srcElement.nextSibling); + cors.sheet.disabled = true; + corsStyleSet.add(cors); + return cors; } - let canUseSheetProxy = false; - document.addEventListener( - "__darkreader__inlineScriptsAllowed", - () => (canUseSheetProxy = true), - { once: true } - ); - const overrides = new WeakSet(); - const overridesBySource = new WeakMap(); - function canHaveAdoptedStyleSheets(node) { - return Array.isArray(node.adoptedStyleSheets); - } - function createAdoptedStyleSheetOverride(node) { - let cancelAsyncOperations = false; - function iterateSourceSheets(iterator) { - node.adoptedStyleSheets.forEach((sheet) => { - if (!overrides.has(sheet)) { - iterator(sheet); + function injectProxy( + enableStyleSheetsProxy, + enableCustomElementRegistryProxy + ) { + document.dispatchEvent( + new CustomEvent("__darkreader__inlineScriptsAllowed") + ); + const cleaners = []; + function cleanUp() { + cleaners.forEach((clean) => clean()); + cleaners.splice(0); + } + function documentEventListener(type, listener, options) { + document.addEventListener(type, listener, options); + cleaners.push(() => document.removeEventListener(type, listener)); + } + function disableConflictingPlugins() { + const disableWPDarkMode = () => { + if (window?.WPDarkMode?.deactivate) { + window.WPDarkMode.deactivate(); } - }); + }; + disableWPDarkMode(); } - function injectSheet(sheet, override) { - const newSheets = [...node.adoptedStyleSheets]; - const sheetIndex = newSheets.indexOf(sheet); - const overrideIndex = newSheets.indexOf(override); - if (overrideIndex >= 0) { - newSheets.splice(overrideIndex, 1); + documentEventListener("__darkreader__cleanUp", cleanUp); + documentEventListener( + "__darkreader__disableConflictingPlugins", + disableConflictingPlugins + ); + function overrideProperty(cls, prop, overrides) { + const proto = cls.prototype; + const oldDescriptor = Object.getOwnPropertyDescriptor(proto, prop); + if (!oldDescriptor) { + return; } - newSheets.splice(sheetIndex + 1, 0, override); - node.adoptedStyleSheets = newSheets; + const newDescriptor = {...oldDescriptor}; + Object.keys(overrides).forEach((key) => { + const factory = overrides[key]; + newDescriptor[key] = factory(oldDescriptor[key]); + }); + Object.defineProperty(proto, prop, newDescriptor); + cleaners.push(() => + Object.defineProperty(proto, prop, oldDescriptor) + ); } - function clear() { - const newSheets = [...node.adoptedStyleSheets]; - for (let i = newSheets.length - 1; i >= 0; i--) { - const sheet = newSheets[i]; - if (overrides.has(sheet)) { - newSheets.splice(i, 1); + function override(cls, prop, factory) { + overrideProperty(cls, prop, {value: factory}); + } + function isDRElement(element) { + return element?.classList?.contains("darkreader"); + } + function isDRSheet(sheet) { + return isDRElement(sheet.ownerNode); + } + const updateSheetEvent = new CustomEvent("__darkreader__updateSheet"); + const adoptedSheetChangeEvent = new CustomEvent( + "__darkreader__adoptedStyleSheetChange" + ); + const shadowDomAttachingEvent = new CustomEvent( + "__darkreader__shadowDomAttaching", + {bubbles: true} + ); + const adoptedSheetOwners = new WeakMap(); + const adoptedDeclarationSheets = new WeakMap(); + function onAdoptedSheetChange(sheet) { + const owners = adoptedSheetOwners.get(sheet); + owners?.forEach((node) => { + if (node.isConnected) { + node.dispatchEvent(adoptedSheetChangeEvent); + } else { + owners.delete(node); } + }); + } + function reportSheetChange(sheet) { + if (sheet.ownerNode && !isDRSheet(sheet)) { + sheet.ownerNode.dispatchEvent(updateSheetEvent); } - if (node.adoptedStyleSheets.length !== newSheets.length) { - node.adoptedStyleSheets = newSheets; + if (adoptedSheetOwners.has(sheet)) { + onAdoptedSheetChange(sheet); } - sourceSheets = new WeakSet(); - sourceDeclarations = new WeakSet(); } - const cleaners = []; - function destroy() { - cleaners.forEach((c) => c()); - cleaners.splice(0); - cancelAsyncOperations = true; - clear(); - if (frameId) { - cancelAnimationFrame(frameId); - frameId = null; + function reportSheetChangeAsync(sheet, promise) { + const {ownerNode} = sheet; + if ( + ownerNode && + !isDRSheet(sheet) && + promise && + promise instanceof Promise + ) { + promise.then(() => ownerNode.dispatchEvent(updateSheetEvent)); } - } - let rulesChangeKey = 0; - function getRulesChangeKey() { - let count = 0; - iterateSourceSheets((sheet) => { - count += sheet.cssRules.length; - }); - if (count === 1) { - const rule = node.adoptedStyleSheets[0].cssRules[0]; - return rule instanceof CSSStyleRule ? rule.style.length : count; + if (adoptedSheetOwners.has(sheet)) { + if (promise && promise instanceof Promise) { + promise.then(() => onAdoptedSheetChange(sheet)); + } } - return count; } - let sourceSheets = new WeakSet(); - let sourceDeclarations = new WeakSet(); - function render(theme, ignoreImageAnalysis) { - clear(); - for (let i = node.adoptedStyleSheets.length - 1; i >= 0; i--) { - const sheet = node.adoptedStyleSheets[i]; - if (overrides.has(sheet)) { - continue; + override( + CSSStyleSheet, + "addRule", + (native) => + function (selector, style, index) { + native.call(this, selector, style, index); + reportSheetChange(this); + return -1; } - sourceSheets.add(sheet); - const readyOverride = overridesBySource.get(sheet); - if (readyOverride) { - rulesChangeKey = getRulesChangeKey(); - injectSheet(sheet, readyOverride); - continue; + ); + override( + CSSStyleSheet, + "insertRule", + (native) => + function (rule, index) { + const returnValue = native.call(this, rule, index); + reportSheetChange(this); + return returnValue; } - const rules = sheet.cssRules; - const override = new CSSStyleSheet(); - overridesBySource.set(sheet, override); - iterateCSSRules(rules, (rule) => - sourceDeclarations.add(rule.style) - ); - const prepareSheet = () => { - for (let i = override.cssRules.length - 1; i >= 0; i--) { - override.deleteRule(i); - } - override.insertRule("#__darkreader__adoptedOverride {}"); - injectSheet(sheet, override); - overrides.add(override); - return override; - }; - const sheetModifier = createStyleSheetModifier(); - sheetModifier.modifySheet({ - prepareSheet, - sourceCSSRules: rules, - theme, - ignoreImageAnalysis, - force: false, - isAsyncCancelled: () => cancelAsyncOperations - }); - } - rulesChangeKey = getRulesChangeKey(); - } - let callbackRequested = false; - function handleArrayChange(callback) { - if (callbackRequested) { - return; - } - callbackRequested = true; - queueMicrotask(() => { - callbackRequested = false; - const sheets = node.adoptedStyleSheets.filter( - (s) => !overrides.has(s) - ); - sheets.forEach((sheet) => overridesBySource.delete(sheet)); - callback(sheets); - }); - } - function checkForUpdates() { - return getRulesChangeKey() !== rulesChangeKey; - } - let frameId = null; - function watchUsingRAF(callback) { - frameId = requestAnimationFrame(() => { - if (canUseSheetProxy) { - return; + ); + override( + CSSStyleSheet, + "deleteRule", + (native) => + function (index) { + native.call(this, index); + reportSheetChange(this); } - if (checkForUpdates()) { - handleArrayChange(callback); + ); + override( + CSSStyleSheet, + "removeRule", + (native) => + function (index) { + native.call(this, index); + reportSheetChange(this); } - watchUsingRAF(callback); + ); + override( + CSSStyleSheet, + "replace", + (native) => + function (cssText) { + const returnValue = native.call(this, cssText); + reportSheetChangeAsync(this, returnValue); + return returnValue; + } + ); + override( + CSSStyleSheet, + "replaceSync", + (native) => + function (cssText) { + native.call(this, cssText); + reportSheetChange(this); + } + ); + override( + Element, + "attachShadow", + (native) => + function (options) { + this.dispatchEvent(shadowDomAttachingEvent); + return native.call(this, options); + } + ); + const shouldWrapHTMLElement = + location.hostname === "baidu.com" || + location.hostname.endsWith(".baidu.com"); + if (shouldWrapHTMLElement) { + override( + Element, + "getElementsByTagName", + (native) => + function (tagName) { + if (tagName !== "style") { + return native.call(this, tagName); + } + const getCurrentElementValue = () => { + const elements = native.call(this, tagName); + return Object.setPrototypeOf( + [...elements].filter( + (element) => + element && !isDRElement(element) + ), + NodeList.prototype + ); + }; + let elements = getCurrentElementValue(); + const nodeListBehavior = { + get: function (_, property) { + return getCurrentElementValue()[ + Number(property) || property + ]; + } + }; + elements = new Proxy(elements, nodeListBehavior); + return elements; + } + ); + } + const shouldProxyChildNodes = ["brilliant.org", "www.vy.no"].includes( + location.hostname + ); + if (shouldProxyChildNodes) { + overrideProperty(Node, "childNodes", { + get: (native) => + function () { + const childNodes = native.call(this); + return Object.setPrototypeOf( + [...childNodes].filter((element) => { + return !isDRElement(element); + }), + NodeList.prototype + ); + } }); } - function addSheetChangeEventListener(type, listener) { - node.addEventListener(type, listener); - cleaners.push(() => node.removeEventListener(type, listener)); + function resolveCustomElement(tag) { + customElements.whenDefined(tag).then(() => { + document.dispatchEvent( + new CustomEvent("__darkreader__isDefined", {detail: {tag}}) + ); + }); } - function watch(callback) { - const onAdoptedSheetsChange = () => { - canUseSheetProxy = true; - handleArrayChange(callback); - }; - addSheetChangeEventListener( - "__darkreader__adoptedStyleSheetsChange", - onAdoptedSheetsChange - ); - addSheetChangeEventListener( - "__darkreader__adoptedStyleSheetChange", - onAdoptedSheetsChange - ); - addSheetChangeEventListener( - "__darkreader__adoptedStyleDeclarationChange", - onAdoptedSheetsChange + documentEventListener("__darkreader__addUndefinedResolver", (e) => + resolveCustomElement(e.detail.tag) + ); + if (enableCustomElementRegistryProxy) { + override( + CustomElementRegistry, + "define", + (native) => + function (name, constructor, options) { + resolveCustomElement(name); + native.call(this, name, constructor, options); + } ); - if (canUseSheetProxy) { + } + let blobURLAllowed = null; + async function checkBlobURLSupport() { + if (blobURLAllowed != null) { + document.dispatchEvent( + new CustomEvent("__darkreader__blobURLCheckResponse", { + detail: {blobURLAllowed} + }) + ); return; } - watchUsingRAF(callback); - } - return { - render, - destroy, - watch - }; - } - class StyleSheetCommandBuilder { - constructor(onChange) { - this.cssRules = []; - this.commands = []; - this.onChange = onChange; - } - insertRule(cssText, index = 0) { - this.commands.push({ type: "insert", index, cssText }); - this.cssRules.splice( - index, - 0, - new StyleSheetCommandBuilder(this.onChange) + const svg = + ''; + const bytes = new Uint8Array(svg.length); + for (let i = 0; i < svg.length; i++) { + bytes[i] = svg.charCodeAt(i); + } + const blob = new Blob([bytes], {type: "image/svg+xml"}); + const objectURL = URL.createObjectURL(blob); + try { + const image = new Image(); + await new Promise((resolve, reject) => { + image.onload = () => resolve(); + image.onerror = () => reject(); + image.src = objectURL; + }); + blobURLAllowed = true; + } catch (err) { + blobURLAllowed = false; + } + document.dispatchEvent( + new CustomEvent("__darkreader__blobURLCheckResponse", { + detail: {blobURLAllowed} + }) ); - this.onChange(); - return index; } - deleteRule(index) { - this.commands.push({ type: "delete", index }); - this.cssRules.splice(index, 1); - this.onChange(); + documentEventListener( + "__darkreader__blobURLCheckRequest", + checkBlobURLSupport + ); + if (enableStyleSheetsProxy) { + overrideProperty(Document, "styleSheets", { + get: (native) => + function () { + const getCurrentValue = () => { + const docSheets = native.call(this); + const filteredSheets = [...docSheets].filter( + (styleSheet) => + styleSheet.ownerNode && + !isDRSheet(styleSheet) + ); + filteredSheets.item = (item) => + filteredSheets[item]; + return Object.setPrototypeOf( + filteredSheets, + StyleSheetList.prototype + ); + }; + let elements = getCurrentValue(); + const styleSheetListBehavior = { + get: function (_, property) { + return getCurrentValue()[property]; + } + }; + elements = new Proxy(elements, styleSheetListBehavior); + return elements; + } + }); } - replaceSync(cssText) { - this.commands.splice(0); - this.commands.push({ type: "replace", cssText }); - if (cssText === "") { - this.cssRules.splice(0); - } else { - throw new Error( - "StyleSheetCommandBuilder.replaceSync() is not fully supported" + { + const adoptedSheetsSourceProxies = new WeakMap(); + const adoptedSheetsProxySources = new WeakMap(); + const adoptedSheetsChangeEvent = new CustomEvent( + "__darkreader__adoptedStyleSheetsChange" + ); + const adoptedSheetOverrideCache = new WeakSet(); + const adoptedSheetsSnapshots = new WeakMap(); + const isDRAdoptedSheetOverride = (sheet) => { + if (!sheet || !sheet.cssRules) { + return false; + } + if (adoptedSheetOverrideCache.has(sheet)) { + return true; + } + if ( + sheet.cssRules.length > 0 && + sheet.cssRules[0].cssText.startsWith( + "#__darkreader__adoptedOverride" + ) + ) { + adoptedSheetOverrideCache.add(sheet); + return true; + } + return false; + }; + const areArraysEqual = (a, b) => { + return a.length === b.length && a.every((x, i) => x === b[i]); + }; + const onAdoptedSheetsChange = (node) => { + const prev = adoptedSheetsSnapshots.get(node); + const curr = (node.adoptedStyleSheets || []).filter( + (s) => !isDRAdoptedSheetOverride(s) ); - } - this.onChange(); - } - getDeepCSSCommands() { - const deep = []; - this.commands.forEach((command) => { - deep.push({ - type: command.type, - cssText: command.type !== "delete" ? command.cssText : "", - path: command.type === "replace" ? [] : [command.index] + adoptedSheetsSnapshots.set(node, curr); + if (!prev || !areArraysEqual(prev, curr)) { + curr.forEach((sheet) => { + if (!adoptedSheetOwners.has(sheet)) { + adoptedSheetOwners.set(sheet, new Set()); + } + adoptedSheetOwners.get(sheet).add(node); + for (const rule of sheet.cssRules) { + const declaration = rule.style; + if (declaration) { + adoptedDeclarationSheets.set( + declaration, + sheet + ); + } + } + }); + node.dispatchEvent(adoptedSheetsChangeEvent); + } + }; + const proxyAdoptedSheetsArray = (node, source) => { + if (adoptedSheetsProxySources.has(source)) { + return source; + } + if (adoptedSheetsSourceProxies.has(source)) { + return adoptedSheetsSourceProxies.get(source); + } + const proxy = new Proxy(source, { + deleteProperty(target, property) { + delete target[property]; + return true; + }, + set(target, property, value) { + target[property] = value; + if (property === "length") { + onAdoptedSheetsChange(node); + } + return true; + } }); - }); - this.cssRules.forEach((rule, i) => { - const childCommands = rule.getDeepCSSCommands(); - childCommands.forEach((c) => c.path.unshift(i)); - }); - return deep; - } - clearDeepCSSCommands() { - this.commands.splice(0); - this.cssRules.forEach((rule) => rule.clearDeepCSSCommands()); - } - } - function createAdoptedStyleSheetFallback(onChange) { - let cancelAsyncOperations = false; - let sourceCSSRules = []; - let lastTheme; - let lastIgnoreImageAnalysis; - function updateCSS(cssRules) { - sourceCSSRules = cssRules; - if (lastTheme && lastIgnoreImageAnalysis) { - render(lastTheme, lastIgnoreImageAnalysis); - } - } - const builder = new StyleSheetCommandBuilder(onChange); - function render(theme, ignoreImageAnalysis) { - lastTheme = theme; - lastIgnoreImageAnalysis = ignoreImageAnalysis; - const prepareSheet = () => { - builder.replaceSync(""); - return builder; + adoptedSheetsSourceProxies.set(source, proxy); + adoptedSheetsProxySources.set(proxy, source); + return proxy; }; - const sheetModifier = createStyleSheetModifier(); - sheetModifier.modifySheet({ - prepareSheet, - sourceCSSRules, - theme, - ignoreImageAnalysis, - force: false, - isAsyncCancelled: () => cancelAsyncOperations + [Document, ShadowRoot].forEach((ctor) => { + overrideProperty(ctor, "adoptedStyleSheets", { + get: (native) => + function () { + const source = native.call(this); + return proxyAdoptedSheetsArray(this, source); + }, + set: (native) => + function (source) { + if (adoptedSheetsProxySources.has(source)) { + source = adoptedSheetsProxySources.get(source); + } + native.call(this, source); + onAdoptedSheetsChange(this); + } + }); + }); + const adoptedDeclarationChangeEvent = new CustomEvent( + "__darkreader__adoptedStyleDeclarationChange" + ); + ["setProperty", "removeProperty"].forEach((key) => { + override(CSSStyleDeclaration, key, (native) => { + return function (...args) { + const returnValue = native.apply(this, args); + const sheet = adoptedDeclarationSheets.get(this); + if (sheet) { + const owners = adoptedSheetOwners.get(sheet); + if (owners) { + owners.forEach((node) => { + node.dispatchEvent( + adoptedDeclarationChangeEvent + ); + }); + } + } + return returnValue; + }; + }); }); } - function commands() { - const commands = builder.getDeepCSSCommands(); - builder.clearDeepCSSCommands(); - return commands; - } - function destroy() { - cancelAsyncOperations = true; - } - return { render, destroy, updateCSS, commands }; } - function injectProxy( - enableStyleSheetsProxy, - enableCustomElementRegistryProxy - ) { - document.dispatchEvent( - new CustomEvent("__darkreader__inlineScriptsAllowed") - ); - const cleaners = []; - function cleanUp() { - cleaners.forEach((clean) => clean()); - cleaners.splice(0); + const definedCustomElements = new Set(); + const undefinedGroups = new Map(); + let elementsDefinitionCallback; + function isCustomElement(element) { + if (element.tagName.includes("-") || element.getAttribute("is")) { + return true; } - function documentEventListener(type, listener, options) { - document.addEventListener(type, listener, options); - cleaners.push(() => document.removeEventListener(type, listener)); + return false; + } + function recordUndefinedElement(element) { + let tag = element.tagName.toLowerCase(); + if (!tag.includes("-")) { + const extendedTag = element.getAttribute("is"); + if (extendedTag) { + tag = extendedTag; + } else { + return; + } } - documentEventListener("__darkreader__cleanUp", cleanUp); - function overrideProperty(cls, prop, overrides) { - const proto = cls.prototype; - const oldDescriptor = Object.getOwnPropertyDescriptor(proto, prop); - const newDescriptor = { ...oldDescriptor }; - Object.keys(overrides).forEach((key) => { - const factory = overrides[key]; - newDescriptor[key] = factory(oldDescriptor[key]); + if (!undefinedGroups.has(tag)) { + undefinedGroups.set(tag, new Set()); + customElementsWhenDefined(tag).then(() => { + if (elementsDefinitionCallback) { + const elements = undefinedGroups.get(tag); + undefinedGroups.delete(tag); + elementsDefinitionCallback(Array.from(elements)); + } }); - Object.defineProperty(proto, prop, newDescriptor); - cleaners.push(() => - Object.defineProperty(proto, prop, oldDescriptor) - ); } - function override(cls, prop, factory) { - overrideProperty(cls, prop, { value: factory }); - } - function isDRElement(element) { - return element?.classList?.contains("darkreader"); - } - function isDRSheet(sheet) { - return isDRElement(sheet.ownerNode); + undefinedGroups.get(tag).add(element); + } + function collectUndefinedElements(root) { + if (!isDefinedSelectorSupported) { + return; } - const updateSheetEvent = new CustomEvent("__darkreader__updateSheet"); - const adoptedSheetChangeEvent = new CustomEvent( - "__darkreader__adoptedStyleSheetChange" + forEach( + root.querySelectorAll(":not(:defined)"), + recordUndefinedElement ); - const adoptedSheetOwners = new WeakMap(); - const adoptedDeclarationSheets = new WeakMap(); - function onAdoptedSheetChange(sheet) { - const owners = adoptedSheetOwners.get(sheet); - owners?.forEach((node) => { - if (node.isConnected) { - node.dispatchEvent(adoptedSheetChangeEvent); - } else { - owners.delete(node); - } + } + let canOptimizeUsingProxy = false; + document.addEventListener( + "__darkreader__inlineScriptsAllowed", + () => { + canOptimizeUsingProxy = true; + }, + {once: true, passive: true} + ); + const unhandledShadowHosts = new Set(); + document.addEventListener("__darkreader__shadowDomAttaching", (e) => { + const host = e.target; + if (unhandledShadowHosts.size === 0) { + queueMicrotask(() => { + const hosts = [...unhandledShadowHosts].filter( + (el) => el.shadowRoot + ); + elementsDefinitionCallback?.(hosts); + unhandledShadowHosts.clear(); }); } - function reportSheetChange(sheet) { - if (sheet.ownerNode && !isDRSheet(sheet)) { - sheet.ownerNode.dispatchEvent(updateSheetEvent); - } - if (adoptedSheetOwners.has(sheet)) { - onAdoptedSheetChange(sheet); - } + unhandledShadowHosts.add(host); + }); + const resolvers = new Map(); + function handleIsDefined(e) { + canOptimizeUsingProxy = true; + const tag = e.detail.tag; + definedCustomElements.add(tag); + if (resolvers.has(tag)) { + const r = resolvers.get(tag); + resolvers.delete(tag); + r.forEach((r) => r()); } - function reportSheetChangeAsync(sheet, promise) { - const { ownerNode } = sheet; + } + async function customElementsWhenDefined(tag) { + if (definedCustomElements.has(tag)) { + return; + } + return new Promise((resolve) => { if ( - ownerNode && - !isDRSheet(sheet) && - promise && - promise instanceof Promise + window.customElements && + typeof customElements.whenDefined === "function" ) { - promise.then(() => ownerNode.dispatchEvent(updateSheetEvent)); - } - if (adoptedSheetOwners.has(sheet)) { - if (promise && promise instanceof Promise) { - promise.then(() => onAdoptedSheetChange(sheet)); - } - } - } - override( - CSSStyleSheet, - "addRule", - (native) => - function (selector, style, index) { - native.call(this, selector, style, index); - reportSheetChange(this); - return -1; - } - ); - override( - CSSStyleSheet, - "insertRule", - (native) => - function (rule, index) { - const returnValue = native.call(this, rule, index); - reportSheetChange(this); - return returnValue; - } - ); - override( - CSSStyleSheet, - "deleteRule", - (native) => - function (index) { - native.call(this, index); - reportSheetChange(this); - } - ); - override( - CSSStyleSheet, - "removeRule", - (native) => - function (index) { - native.call(this, index); - reportSheetChange(this); - } - ); - override( - CSSStyleSheet, - "replace", - (native) => - function (cssText) { - const returnValue = native.call(this, cssText); - reportSheetChangeAsync(this, returnValue); - return returnValue; - } - ); - override( - CSSStyleSheet, - "replaceSync", - (native) => - function (cssText) { - native.call(this, cssText); - reportSheetChange(this); + customElements.whenDefined(tag).then(() => resolve()); + } else if (canOptimizeUsingProxy) { + if (resolvers.has(tag)) { + resolvers.get(tag).push(resolve); + } else { + resolvers.set(tag, [resolve]); } - ); - const shouldWrapHTMLElement = - location.hostname === "baidu.com" || - location.hostname.endsWith(".baidu.com"); - if (shouldWrapHTMLElement) { - override( - Element, - "getElementsByTagName", - (native) => - function (tagName) { - if (tagName !== "style") { - return native.call(this, tagName); - } - const getCurrentElementValue = () => { - const elements = native.call(this, tagName); - return Object.setPrototypeOf( - [...elements].filter( - (element) => - element && !isDRElement(element) - ), - NodeList.prototype - ); - }; - let elements = getCurrentElementValue(); - const nodeListBehavior = { - get: function (_, property) { - return getCurrentElementValue()[ - Number(property) || property - ]; - } - }; - elements = new Proxy(elements, nodeListBehavior); - return elements; + document.dispatchEvent( + new CustomEvent("__darkreader__addUndefinedResolver", { + detail: {tag} + }) + ); + } else { + const checkIfDefined = () => { + const elements = undefinedGroups.get(tag); + if (elements && elements.size > 0) { + if ( + elements.values().next().value.matches(":defined") + ) { + resolve(); + } else { + requestAnimationFrame(checkIfDefined); + } } - ); - } - const shouldProxyChildNodes = ["brilliant.org", "www.vy.no"].includes( - location.hostname + }; + requestAnimationFrame(checkIfDefined); + } + }); + } + function watchWhenCustomElementsDefined(callback) { + elementsDefinitionCallback = callback; + } + function unsubscribeFromDefineCustomElements() { + elementsDefinitionCallback = null; + undefinedGroups.clear(); + document.removeEventListener( + "__darkreader__isDefined", + handleIsDefined ); - if (shouldProxyChildNodes) { - overrideProperty(Node, "childNodes", { - get: (native) => - function () { - const childNodes = native.call(this); - return Object.setPrototypeOf( - [...childNodes].filter((element) => { - return !isDRElement(element); - }), - NodeList.prototype - ); - } - }); + } + + const observers = []; + let observedRoots; + let handledShadowHosts; + function watchForStylePositions( + currentStyles, + update, + shadowRootDiscovered + ) { + stopWatchingForStylePositions(); + const prevStylesByRoot = new WeakMap(); + const getPrevStyles = (root) => { + if (!prevStylesByRoot.has(root)) { + prevStylesByRoot.set(root, new Set()); + } + return prevStylesByRoot.get(root); + }; + currentStyles.forEach((node) => { + let root = node; + while ((root = root.parentNode)) { + if ( + root === document || + root.nodeType === Node.DOCUMENT_FRAGMENT_NODE + ) { + const prevStyles = getPrevStyles(root); + prevStyles.add(node); + break; + } + } + }); + const prevStyleSiblings = new WeakMap(); + const nextStyleSiblings = new WeakMap(); + function saveStylePosition(style) { + prevStyleSiblings.set(style, style.previousElementSibling); + nextStyleSiblings.set(style, style.nextElementSibling); } - function resolveCustomElement(tag) { - customElements.whenDefined(tag).then(() => { - document.dispatchEvent( - new CustomEvent("__darkreader__isDefined", { detail: { tag } }) - ); - }); + function forgetStylePosition(style) { + prevStyleSiblings.delete(style); + nextStyleSiblings.delete(style); } - documentEventListener("__darkreader__addUndefinedResolver", (e) => - resolveCustomElement(e.detail.tag) - ); - if (enableCustomElementRegistryProxy) { - override( - CustomElementRegistry, - "define", - (native) => - function (name, constructor, options) { - resolveCustomElement(name); - native.call(this, name, constructor, options); - } + function didStylePositionChange(style) { + return ( + style.previousElementSibling !== prevStyleSiblings.get(style) || + style.nextElementSibling !== nextStyleSiblings.get(style) ); } - async function checkBlobURLSupport() { - const svg = - ''; - const bytes = new Uint8Array(svg.length); - for (let i = 0; i < svg.length; i++) { - bytes[i] = svg.charCodeAt(i); - } - const blob = new Blob([bytes], { type: "image/svg+xml" }); - const objectURL = URL.createObjectURL(blob); - let blobURLAllowed; - try { - const image = new Image(); - await new Promise((resolve, reject) => { - image.onload = () => resolve(); - image.onerror = () => reject(); - image.src = objectURL; + currentStyles.forEach(saveStylePosition); + function handleStyleOperations(root, operations) { + const {createdStyles, removedStyles, movedStyles} = operations; + createdStyles.forEach((s) => saveStylePosition(s)); + movedStyles.forEach((s) => saveStylePosition(s)); + removedStyles.forEach((s) => forgetStylePosition(s)); + const prevStyles = getPrevStyles(root); + createdStyles.forEach((s) => prevStyles.add(s)); + removedStyles.forEach((s) => prevStyles.delete(s)); + if ( + createdStyles.size + removedStyles.size + movedStyles.size > + 0 + ) { + update({ + created: Array.from(createdStyles), + removed: Array.from(removedStyles), + moved: Array.from(movedStyles), + updated: [] }); - blobURLAllowed = true; - } catch (err) { - blobURLAllowed = false; } - document.dispatchEvent( - new CustomEvent("__darkreader__blobURLCheckResponse", { - detail: { blobURLAllowed } - }) - ); } - documentEventListener( - "__darkreader__blobURLCheckRequest", - checkBlobURLSupport, - { once: true } - ); - if (enableStyleSheetsProxy) { - overrideProperty(Document, "styleSheets", { - get: (native) => - function () { - const getCurrentValue = () => { - const docSheets = native.call(this); - const filteredSheets = [...docSheets].filter( - (styleSheet) => - styleSheet.ownerNode && - !isDRSheet(styleSheet) - ); - filteredSheets.item = (item) => - filteredSheets[item]; - return Object.setPrototypeOf( - filteredSheets, - StyleSheetList.prototype - ); - }; - let elements = getCurrentValue(); - const styleSheetListBehavior = { - get: function (_, property) { - return getCurrentValue()[property]; - } - }; - elements = new Proxy(elements, styleSheetListBehavior); - return elements; - } + function handleMinorTreeMutations(root, {additions, moves, deletions}) { + const createdStyles = new Set(); + const removedStyles = new Set(); + const movedStyles = new Set(); + additions.forEach((node) => + getManageableStyles(node).forEach((style) => + createdStyles.add(style) + ) + ); + deletions.forEach((node) => + getManageableStyles(node).forEach((style) => + removedStyles.add(style) + ) + ); + moves.forEach((node) => + getManageableStyles(node).forEach((style) => + movedStyles.add(style) + ) + ); + handleStyleOperations(root, { + createdStyles, + removedStyles, + movedStyles }); - } - { - const adoptedSheetsSourceProxies = new WeakMap(); - const adoptedSheetsProxySources = new WeakMap(); - const adoptedSheetsChangeEvent = new CustomEvent( - "__darkreader__adoptedStyleSheetsChange" + additions.forEach((n) => { + deepObserve(n); + collectUndefinedElements(n); + }); + additions.forEach( + (node) => isCustomElement(node) && recordUndefinedElement(node) ); - const adoptedSheetOverrideCache = new WeakSet(); - const adoptedSheetsSnapshots = new WeakMap(); - const isDRAdoptedSheetOverride = (sheet) => { - if (!sheet || !sheet.cssRules) { - return false; - } - if (adoptedSheetOverrideCache.has(sheet)) { - return true; - } - if ( - sheet.cssRules.length > 0 && - sheet.cssRules[0].cssText.startsWith( - "#__darkreader__adoptedOverride" - ) - ) { - adoptedSheetOverrideCache.add(sheet); - return true; - } - return false; - }; - const areArraysEqual = (a, b) => { - return a.length === b.length && a.every((x, i) => x === b[i]); - }; - const onAdoptedSheetsChange = (node) => { - const prev = adoptedSheetsSnapshots.get(node); - const curr = (node.adoptedStyleSheets || []).filter( - (s) => !isDRAdoptedSheetOverride(s) - ); - adoptedSheetsSnapshots.set(node, curr); - if (!prev || !areArraysEqual(prev, curr)) { - curr.forEach((sheet) => { - if (!adoptedSheetOwners.has(sheet)) { - adoptedSheetOwners.set(sheet, new Set()); - } - adoptedSheetOwners.get(sheet).add(node); - for (const rule of sheet.cssRules) { - const declaration = rule.style; - if (declaration) { - adoptedDeclarationSheets.set( - declaration, - sheet - ); - } - } - }); - node.dispatchEvent(adoptedSheetsChangeEvent); + additions.forEach((node) => checkImageSelectors(node)); + } + function handleHugeTreeMutations(root) { + const styles = new Set(getManageableStyles(root)); + const createdStyles = new Set(); + const removedStyles = new Set(); + const movedStyles = new Set(); + const prevStyles = getPrevStyles(root); + styles.forEach((s) => { + if (!prevStyles.has(s)) { + createdStyles.add(s); } - }; - const proxyAdoptedSheetsArray = (node, source) => { - if (adoptedSheetsProxySources.has(source)) { - return source; + }); + prevStyles.forEach((s) => { + if (!styles.has(s)) { + removedStyles.add(s); } - if (adoptedSheetsSourceProxies.has(source)) { - return adoptedSheetsSourceProxies.get(source); + }); + styles.forEach((s) => { + if ( + !createdStyles.has(s) && + !removedStyles.has(s) && + didStylePositionChange(s) + ) { + movedStyles.add(s); } - const proxy = new Proxy(source, { - deleteProperty(target, property) { - delete target[property]; - return true; - }, - set(target, property, value) { - target[property] = value; - if (property === "length") { - onAdoptedSheetsChange(node); - } - return true; + }); + handleStyleOperations(root, { + createdStyles, + removedStyles, + movedStyles + }); + deepObserve(root); + collectUndefinedElements(root); + checkImageSelectors(root); + } + function handleAttributeMutations(mutations) { + const updatedStyles = new Set(); + const removedStyles = new Set(); + mutations.forEach((m) => { + const {target} = m; + if (target.isConnected) { + if (shouldManageStyle(target)) { + updatedStyles.add(target); + } else if ( + target instanceof HTMLLinkElement && + target.disabled + ) { + removedStyles.add(target); } - }); - adoptedSheetsSourceProxies.set(source, proxy); - adoptedSheetsProxySources.set(proxy, source); - return proxy; - }; - [Document, ShadowRoot].forEach((ctor) => { - overrideProperty(ctor, "adoptedStyleSheets", { - get: (native) => - function () { - const source = native.call(this); - return proxyAdoptedSheetsArray(this, source); - }, - set: (native) => - function (source) { - if (adoptedSheetsProxySources.has(source)) { - source = adoptedSheetsProxySources.get(source); - } - native.call(this, source); - onAdoptedSheetsChange(this); - } - }); + } }); - const adoptedDeclarationChangeEvent = new CustomEvent( - "__darkreader__adoptedStyleDeclarationChange" - ); - ["setProperty", "removeProperty"].forEach((key) => { - override(CSSStyleDeclaration, key, (native) => { - return function (...args) { - const returnValue = native.apply(this, args); - const sheet = adoptedDeclarationSheets.get(this); - if (sheet) { - const owners = adoptedSheetOwners.get(sheet); - if (owners) { - owners.forEach((node) => { - node.dispatchEvent( - adoptedDeclarationChangeEvent - ); - }); - } - } - return returnValue; - }; + if (updatedStyles.size + removedStyles.size > 0) { + update({ + updated: Array.from(updatedStyles), + created: [], + removed: Array.from(removedStyles), + moved: [] }); + } + } + function observe(root) { + if (observedRoots.has(root)) { + return; + } + const treeObserver = createOptimizedTreeObserver(root, { + onMinorMutations: handleMinorTreeMutations, + onHugeMutations: handleHugeTreeMutations + }); + const attrObserver = new MutationObserver(handleAttributeMutations); + attrObserver.observe(root, { + attributeFilter: ["rel", "disabled", "media", "href"], + subtree: true }); + observers.push(treeObserver, attrObserver); + observedRoots.add(root); } - } - - let documentVisibilityListener = null; - let documentIsVisible_ = !document.hidden; - const listenerOptions = { - capture: true, - passive: true - }; - function watchForDocumentVisibility() { - document.addEventListener( - "visibilitychange", - documentVisibilityListener, - listenerOptions - ); - window.addEventListener( - "pageshow", - documentVisibilityListener, - listenerOptions - ); - window.addEventListener( - "focus", - documentVisibilityListener, - listenerOptions - ); - } - function stopWatchingForDocumentVisibility() { - document.removeEventListener( - "visibilitychange", - documentVisibilityListener, - listenerOptions - ); - window.removeEventListener( - "pageshow", - documentVisibilityListener, - listenerOptions - ); - window.removeEventListener( - "focus", - documentVisibilityListener, - listenerOptions - ); - } - function setDocumentVisibilityListener(callback) { - const alreadyWatching = Boolean(documentVisibilityListener); - documentVisibilityListener = () => { - if (!document.hidden) { - removeDocumentVisibilityListener(); - callback(); - documentIsVisible_ = true; + function subscribeForShadowRootChanges(node) { + const {shadowRoot} = node; + if (shadowRoot == null || observedRoots.has(shadowRoot)) { + return; } - }; - if (!alreadyWatching) { - watchForDocumentVisibility(); + observe(shadowRoot); + shadowRootDiscovered(shadowRoot); } + function deepObserve(node) { + iterateShadowHosts(node, subscribeForShadowRootChanges); + } + observe(document); + deepObserve(document.documentElement); + watchWhenCustomElementsDefined((hosts) => { + hosts = hosts.filter((node) => !handledShadowHosts.has(node)); + const newStyles = []; + hosts.forEach((host) => + push(newStyles, getManageableStyles(host.shadowRoot)) + ); + update({created: newStyles, updated: [], removed: [], moved: []}); + hosts.forEach((host) => { + const {shadowRoot} = host; + if (shadowRoot == null) { + return; + } + subscribeForShadowRootChanges(host); + deepObserve(shadowRoot); + collectUndefinedElements(shadowRoot); + }); + hosts.forEach((node) => handledShadowHosts.add(node)); + }); + document.addEventListener("__darkreader__isDefined", handleIsDefined); + collectUndefinedElements(document); } - function removeDocumentVisibilityListener() { - stopWatchingForDocumentVisibility(); - documentVisibilityListener = null; + function resetObservers() { + observers.forEach((o) => o.disconnect()); + observers.splice(0, observers.length); + observedRoots = new WeakSet(); + handledShadowHosts = new WeakSet(); } - function documentIsVisible() { - return documentIsVisible_; + function stopWatchingForStylePositions() { + resetObservers(); + unsubscribeFromDefineCustomElements(); + } + + function watchForStyleChanges(currentStyles, update, shadowRootDiscovered) { + watchForStylePositions(currentStyles, update, shadowRootDiscovered); + } + function stopWatchingForStyleChanges() { + stopWatchingForStylePositions(); } const INSTANCE_ID = generateUID(); const styleManagers = new Map(); const adoptedStyleManagers = []; const adoptedStyleFallbacks = new Map(); - const adoptedStyleNodeIds = new WeakMap(); const adoptedStyleChangeTokens = new WeakMap(); let theme = null; let fixes = null; @@ -6877,36 +7233,28 @@ document.head.appendChild(overrideStyle); setupNodePositionWatcher(overrideStyle, "override"); const variableStyle = createOrUpdateStyle("darkreader--variables"); - const selectionColors = getSelectionColor(theme); - const { - darkSchemeBackgroundColor, - darkSchemeTextColor, - lightSchemeBackgroundColor, - lightSchemeTextColor, - mode - } = theme; - let schemeBackgroundColor = - mode === 0 ? lightSchemeBackgroundColor : darkSchemeBackgroundColor; - let schemeTextColor = - mode === 0 ? lightSchemeTextColor : darkSchemeTextColor; - schemeBackgroundColor = modifyBackgroundColor( - parseColorWithCache(schemeBackgroundColor), + const selectionColors = theme?.selectionColor + ? getSelectionColor(theme) + : null; + const neutralBackgroundColor = modifyBackgroundColor( + parseColorWithCache("#ffffff"), theme ); - schemeTextColor = modifyForegroundColor( - parseColorWithCache(schemeTextColor), + const neutralTextColor = modifyForegroundColor( + parseColorWithCache("#000000"), theme ); variableStyle.textContent = [ `:root {`, - ` --darkreader-neutral-background: ${schemeBackgroundColor};`, - ` --darkreader-neutral-text: ${schemeTextColor};`, - ` --darkreader-selection-background: ${selectionColors.backgroundColorSelection};`, - ` --darkreader-selection-text: ${selectionColors.foregroundColorSelection};`, + ` --darkreader-neutral-background: ${neutralBackgroundColor};`, + ` --darkreader-neutral-text: ${neutralTextColor};`, + ` --darkreader-selection-background: ${selectionColors?.backgroundColorSelection ?? "initial"};`, + ` --darkreader-selection-text: ${selectionColors?.foregroundColorSelection ?? "initial"};`, `}` ].join("\n"); document.head.insertBefore(variableStyle, inlineStyle.nextSibling); setupNodePositionWatcher(variableStyle, "variables"); + registerVariablesSheet(variableStyle.sheet); const rootVarsStyle = createOrUpdateStyle("darkreader--root-vars"); document.head.insertBefore(rootVarsStyle, variableStyle.nextSibling); const enableStyleSheetsProxy = !( @@ -6956,9 +7304,9 @@ function delayedCreateShadowStaticStyleOverrides(root) { const observer = new MutationObserver((mutations, observer) => { observer.disconnect(); - for (const { type, removedNodes } of mutations) { + for (const {type, removedNodes} of mutations) { if (type === "childList") { - for (const { nodeName, className } of removedNodes) { + for (const {nodeName, className} of removedNodes) { if ( nodeName === "STYLE" && [ @@ -6974,7 +7322,7 @@ } } }); - observer.observe(root, { childList: true }); + observer.observe(root, {childList: true}); } function createShadowStaticStyleOverrides(root) { const delayed = root.firstChild === null; @@ -6987,7 +7335,11 @@ return $cssText.replace(/\${(.+?)}/g, (_, $color) => { const color = parseColorWithCache($color); if (color) { - return modifyColor(color, theme); + const lightness = getSRGBLightness(color.r, color.g, color.b); + if (lightness > 0.5) { + return modifyBackgroundColor(color, theme); + } + return modifyForegroundColor(color, theme); } return $color; }); @@ -7005,7 +7357,7 @@ .filter((style) => !styleManagers.has(style)) .map((style) => createManager(style)); newManagers - .map((manager) => manager.details({ secondRound: false })) + .map((manager) => manager.details({secondRound: false})) .filter((detail) => detail && detail.rules.length > 0) .forEach((detail) => { variablesStore.addRulesForMatching(detail.rules); @@ -7047,40 +7399,48 @@ handleAdoptedStyleSheets(document); variablesStore.matchVariablesAndDependents(); if (isFirefox) { - const MATCH_VAR = Symbol(); - const onAdoptedCSSChange = (e) => { - const { node, id, cssRules, entries } = e.detail; - if (Array.isArray(entries)) { - entries.forEach((e) => { - const cssRules = e[2]; - variablesStore.addRulesForMatching(cssRules); - }); - variablesStore.matchVariablesAndDependents(); - } else if (cssRules) { - variablesStore.addRulesForMatching(cssRules); - requestAnimationFrameOnce(MATCH_VAR, () => - variablesStore.matchVariablesAndDependents() - ); + const onAdoptedCssChange = (e) => { + const {sheets} = e.detail; + if (!Array.isArray(sheets) || sheets.length === 0) { + return; } - const tuples = Array.isArray(entries) - ? entries - : node && cssRules - ? [[node, id, cssRules]] - : []; - tuples.forEach(([node, id, cssRules]) => { - adoptedStyleNodeIds.set(node, id); - const fallback = getAdoptedStyleSheetFallback(node); - fallback.updateCSS(cssRules); + sheets.forEach(({sheet}) => { + const {cssRules} = sheet; + variablesStore.addRulesForMatching(cssRules); + }); + variablesStore.matchVariablesAndDependents(); + const response = []; + sheets.forEach(({sheetId, sheet}) => { + const fallback = getAdoptedStyleSheetFallback(sheet); + const cssRules = sheet.cssRules; + fallback.render({ + theme: theme, + ignoreImageAnalysis: ignoredImageAnalysisSelectors, + cssRules + }); + const commands = fallback.commands(); + response.push({sheetId, commands}); }); + requestAnimationFrameOnce( + getAdoptedStyleChangeToken(sheets[0].sheet), + () => { + document.dispatchEvent( + new CustomEvent( + "__darkreader__adoptedStyleSheetCommands", + {detail: JSON.stringify(response)} + ) + ); + } + ); }; document.addEventListener( "__darkreader__adoptedStyleSheetsChange", - onAdoptedCSSChange + onAdoptedCssChange ); cleaners.push(() => document.removeEventListener( "__darkreader__adoptedStyleSheetsChange", - onAdoptedCSSChange + onAdoptedCssChange ) ); document.dispatchEvent( @@ -7104,7 +7464,7 @@ if (!fallbackStyle.textContent) { fallbackStyle.textContent = getModifiedFallbackStyle( theme, - { strict: false } + {strict: false} ); } } @@ -7119,7 +7479,7 @@ } } function update() { - const details = manager.details({ secondRound: true }); + const details = manager.details({secondRound: true}); if (!details) { return; } @@ -7175,8 +7535,6 @@ } function handleAdoptedStyleSheets(node) { if (isFirefox) { - const fallback = getAdoptedStyleSheetFallback(node); - fallback.render(theme, ignoredImageAnalysisSelectors); return; } if (canHaveAdoptedStyleSheets(node)) { @@ -7195,35 +7553,19 @@ }); } } - function getAdoptedStyleChangeToken(node) { - if (adoptedStyleChangeTokens.has(node)) { - return adoptedStyleChangeTokens.get(node); + function getAdoptedStyleChangeToken(sheet) { + if (adoptedStyleChangeTokens.has(sheet)) { + return adoptedStyleChangeTokens.get(sheet); } const token = Symbol(); - adoptedStyleChangeTokens.set(node, token); + adoptedStyleChangeTokens.set(sheet, token); return token; } - function getAdoptedStyleSheetFallback(node) { - let fallback = adoptedStyleFallbacks.get(node); + function getAdoptedStyleSheetFallback(sheet) { + let fallback = adoptedStyleFallbacks.get(sheet); if (!fallback) { - fallback = createAdoptedStyleSheetFallback(() => { - const token = getAdoptedStyleChangeToken(node); - requestAnimationFrameOnce(token, () => { - const id = adoptedStyleNodeIds.get(node); - const commands = fallback?.commands(); - if (!id || !commands) { - return; - } - const data = { id, commands }; - document.dispatchEvent( - new CustomEvent( - "__darkreader__adoptedStyleSheetCommands", - { detail: JSON.stringify(data) } - ) - ); - }); - }); - adoptedStyleFallbacks.set(node, fallback); + fallback = createAdoptedStyleSheetFallback(); + adoptedStyleFallbacks.set(sheet, fallback); } return fallback; } @@ -7231,7 +7573,7 @@ const managedStyles = Array.from(styleManagers.keys()); watchForStyleChanges( managedStyles, - ({ created, updated, removed, moved }) => { + ({created, updated, removed, moved}) => { const stylesToRemove = removed; const stylesToManage = created .concat(updated) @@ -7245,7 +7587,7 @@ createManager(style) ); newManagers - .map((manager) => manager.details({ secondRound: false })) + .map((manager) => manager.details({secondRound: false})) .filter((detail) => detail && detail.rules.length > 0) .forEach((detail) => { variablesStore.addRulesForMatching(detail.rules); @@ -7318,7 +7660,7 @@ removeDynamicTheme(); } }); - metaObserver.observe(document.head, { childList: true, subtree: true }); + metaObserver.observe(document.head, {childList: true, subtree: true}); } function createDarkReaderInstanceMarker() { const metaElement = document.createElement("meta"); @@ -7326,10 +7668,10 @@ metaElement.content = INSTANCE_ID; document.head.appendChild(metaElement); } + function isDRLocked() { + return document.querySelector('meta[name="darkreader-lock"]') != null; + } function isAnotherDarkReaderInstanceActive() { - if (document.querySelector('meta[name="darkreader-lock"]')) { - return true; - } const meta = document.querySelector('meta[name="darkreader"]'); if (meta) { if (meta.content !== INSTANCE_ID) { @@ -7342,7 +7684,7 @@ return false; } let interceptorAttempts = 2; - function interceptOldScript({ success, failure }) { + function interceptOldScript({success, failure}) { if (--interceptorAttempts <= 0) { failure(); return; @@ -7359,6 +7701,40 @@ success(); }); } + function disableConflictingPlugins() { + if (document.documentElement.hasAttribute("data-wp-dark-mode-preset")) { + const disableWPDarkMode = () => { + document.dispatchEvent( + new CustomEvent("__darkreader__disableConflictingPlugins") + ); + document.documentElement.classList.remove( + "wp-dark-mode-active" + ); + document.documentElement.removeAttribute( + "data-wp-dark-mode-active" + ); + }; + disableWPDarkMode(); + const observer = new MutationObserver(() => { + if ( + document.documentElement.classList.contains( + "wp-dark-mode-active" + ) || + document.documentElement.hasAttribute( + "data-wp-dark-mode-active" + ) + ) { + disableWPDarkMode(); + } + }); + observer.observe(document.documentElement, { + attributes: true, + attributeFilter: ["class", "data-wp-dark-mode-active"] + }); + } + } + let prevTheme = null; + let prevFixes = null; function createOrUpdateDynamicThemeInternal( themeConfig, dynamicThemeFixes, @@ -7366,6 +7742,53 @@ ) { theme = themeConfig; fixes = dynamicThemeFixes; + const colorAffectingKeys = [ + "brightness", + "contrast", + "darkSchemeBackgroundColor", + "darkSchemeTextColor", + "grayscale", + "lightSchemeBackgroundColor", + "lightSchemeTextColor", + "sepia" + ]; + if (prevTheme && prevFixes) { + const themeKeys = new Set([ + ...Object.keys(theme), + ...Object.keys(prevTheme) + ]); + let onlyColorsChanged = true; + for (const key of themeKeys) { + if ( + theme[key] !== prevTheme[key] && + !colorAffectingKeys.includes(key) + ) { + onlyColorsChanged = false; + break; + } + } + if ( + onlyColorsChanged && + JSON.stringify(fixes) !== JSON.stringify(prevFixes) + ) { + onlyColorsChanged = false; + } + if (onlyColorsChanged) { + const palette = getColorPalette(); + clearColorPalette(); + palette.background.forEach((color) => + modifyBackgroundColor(color, theme) + ); + palette.text.forEach((color) => + modifyForegroundColor(color, theme) + ); + palette.border.forEach((color) => + modifyBorderColor(color, theme) + ); + return; + } + clearColorPalette(); + } if (fixes) { ignoredImageAnalysisSelectors = Array.isArray( fixes.ignoreImageAnalysis @@ -7387,6 +7810,7 @@ isIFrame$1 = iframe; const ready = () => { const success = () => { + disableConflictingPlugins(); document.documentElement.setAttribute( "data-darkreader-mode", "dynamic" @@ -7400,7 +7824,9 @@ const failure = () => { removeDynamicTheme(); }; - if (isAnotherDarkReaderInstanceActive()) { + if (isDRLocked()) { + removeNode(document.querySelector(".darkreader--fallback")); + } else if (isAnotherDarkReaderInstanceActive()) { interceptOldScript({ success, failure @@ -7427,8 +7853,10 @@ ready(); } }); - headObserver.observe(document, { childList: true, subtree: true }); + headObserver.observe(document, {childList: true, subtree: true}); } + prevTheme = theme; + prevFixes = fixes; } function removeProxy() { document.dispatchEvent(new CustomEvent("__darkreader__cleanUp")); @@ -7441,15 +7869,20 @@ cleanDynamicThemeCache(); removeNode(document.querySelector(".darkreader--fallback")); if (document.head) { + const selectors = [ + ".darkreader--user-agent", + ".darkreader--text", + ".darkreader--invert", + ".darkreader--inline", + ".darkreader--override", + ".darkreader--variables", + ".darkreader--root-vars", + 'meta[name="darkreader"]' + ]; restoreMetaThemeColor(); - removeNode(document.head.querySelector(".darkreader--user-agent")); - removeNode(document.head.querySelector(".darkreader--text")); - removeNode(document.head.querySelector(".darkreader--invert")); - removeNode(document.head.querySelector(".darkreader--inline")); - removeNode(document.head.querySelector(".darkreader--override")); - removeNode(document.head.querySelector(".darkreader--variables")); - removeNode(document.head.querySelector(".darkreader--root-vars")); - removeNode(document.head.querySelector('meta[name="darkreader"]')); + selectors.forEach((selector) => + removeNode(document.head.querySelector(selector)) + ); removeProxy(); } shadowRootsWithOverrides.forEach((root) => { @@ -7477,6 +7910,9 @@ stopWatchingForUpdates(); cleanModificationCache(); clearColorCache(); + releaseVariablesSheet(); + prevTheme = null; + prevFixes = null; } function parseCSS(cssText) { @@ -7619,12 +8055,12 @@ formatAtRule(rule, indent); } } - function formatAtRule({ type, query, rules }, indent) { + function formatAtRule({type, query, rules}, indent) { lines.push(`${indent}${type} ${query} {`); rules.forEach((child) => formatRule(child, `${indent}${tab}`)); lines.push(`${indent}}`); } - function formatStyleRule({ selectors, declarations }, indent) { + function formatStyleRule({selectors, declarations}, indent) { const lastSelectorIndex = selectors.length - 1; selectors.forEach((selector, i) => { lines.push( @@ -7632,7 +8068,7 @@ ); }); const sorted = sortDeclarations(declarations); - sorted.forEach(({ property, value, important }) => { + sorted.forEach(({property, value, important}) => { lines.push( `${indent}${tab}${property}: ${value}${important ? " !important" : ""};` ); @@ -7748,7 +8184,7 @@ _______|_______/__/ ____ \\__\\__|___\\__\\__|___\\__\\____ } })(); function enable(themeOptions = {}, fixes = null) { - const theme = { ...DEFAULT_THEME, ...themeOptions }; + const theme = {...DEFAULT_THEME, ...themeOptions}; if (theme.engine !== ThemeEngine.dynamicTheme) { throw new Error("Theme engine is not supported."); } @@ -7762,13 +8198,16 @@ _______|_______/__/ ____ \\__\\__|___\\__\\__|___\\__\\____ removeDynamicTheme(); isDarkReaderEnabled = false; } - const darkScheme = matchMedia("(prefers-color-scheme: dark)"); + const darkScheme = + typeof matchMedia === "function" + ? matchMedia("(prefers-color-scheme: dark)") + : undefined; let store = { themeOptions: null, fixes: null }; function handleColorScheme() { - if (darkScheme.matches) { + if (darkScheme?.matches) { enable(store.themeOptions, store.fixes); } else { disable(); @@ -7776,18 +8215,18 @@ _______|_______/__/ ____ \\__\\__|___\\__\\__|___\\__\\____ } function auto(themeOptions = {}, fixes = null) { if (themeOptions) { - store = { themeOptions, fixes }; + store = {themeOptions, fixes}; handleColorScheme(); if (isMatchMediaChangeEventListenerSupported) { - darkScheme.addEventListener("change", handleColorScheme); + darkScheme?.addEventListener("change", handleColorScheme); } else { - darkScheme.addListener(handleColorScheme); + darkScheme?.addListener(handleColorScheme); } } else { if (isMatchMediaChangeEventListenerSupported) { - darkScheme.removeEventListener("change", handleColorScheme); + darkScheme?.removeEventListener("change", handleColorScheme); } else { - darkScheme.removeListener(handleColorScheme); + darkScheme?.removeListener(handleColorScheme); } disable(); } @@ -7804,5 +8243,5 @@ _______|_______/__/ ____ \\__\\__|___\\__\\__|___\\__\\____ exports.isEnabled = isEnabled; exports.setFetchMethod = setFetchMethod; - Object.defineProperty(exports, "__esModule", { value: true }); + Object.defineProperty(exports, "__esModule", {value: true}); }); \ No newline at end of file