Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
135 changes: 135 additions & 0 deletions contents/shadow-dom.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
import type { PlasmoCSConfig } from "plasmo"

import { Storage } from "@plasmohq/storage"

import { buildCSSString } from "~helpers/buildCSSString"
import type { TStyle } from "~helpers/constants"

export const config: PlasmoCSConfig = {
matches: ["<all_urls>"],
all_frames: true,
match_about_blank: true,
run_at: "document_idle"
}

const STYLE_ID = "tse-shadow-styles"

// Track which shadow roots already have our <style> injected
const processed = new WeakSet<ShadowRoot>()

const storage = new Storage({ area: "local" })

let currentCSS = ""

/**
* Inject or update a <style> element inside a single shadow root.
*/
function injectIntoShadowRoot(shadowRoot: ShadowRoot) {
if (!currentCSS) {
// No styles to inject — remove existing if present
const existing = shadowRoot.getElementById(STYLE_ID)
if (existing) existing.remove()
processed.delete(shadowRoot)
return
}

let style = shadowRoot.getElementById(STYLE_ID) as HTMLStyleElement | null
if (!style) {
style = document.createElement("style")
style.id = STYLE_ID
shadowRoot.prepend(style)
}
style.textContent = currentCSS
processed.add(shadowRoot)
}

/**
* Recursively walk a DOM subtree and inject styles into every open shadow root.
*/
function walkAndInject(root: Node) {
const walker = document.createTreeWalker(root, NodeFilter.SHOW_ELEMENT)
let node: Element | null = walker.currentNode as Element

while (node) {
if (node.shadowRoot) {
injectIntoShadowRoot(node.shadowRoot)
// Recurse into the shadow root to find nested shadow DOMs
walkAndInject(node.shadowRoot)
}
node = walker.nextNode() as Element | null
}
}

/**
* Update all already-processed shadow roots with new CSS.
*/
function updateAllShadowRoots() {
walkAndInject(document.body)
}

/**
* Observe the document for newly added elements that might have shadow roots.
*/
const observer = new MutationObserver((mutations) => {
const added: Element[] = []

for (const mutation of mutations) {
for (const node of mutation.addedNodes) {
if (node.nodeType === Node.ELEMENT_NODE) {
const el = node as Element
if (el.shadowRoot) {
injectIntoShadowRoot(el.shadowRoot)
walkAndInject(el.shadowRoot)
}
walkAndInject(el)
added.push(el)
}
}
}

// Deferred re-check: some components attach shadowRoot in connectedCallback
if (added.length > 0) {
requestAnimationFrame(() => {
for (const el of added) {
if (el.shadowRoot && !processed.has(el.shadowRoot)) {
injectIntoShadowRoot(el.shadowRoot)
walkAndInject(el.shadowRoot)
}
}
})
}
})

async function updateCSS() {
const enabled = await storage.get<boolean>("enabled")
const styles = await storage.get<TStyle>("styles")

if (enabled === false || !styles) {
currentCSS = ""
} else {
currentCSS = buildCSSString(styles)
}
updateAllShadowRoots()
}

/**
* Listen for storage changes (styles or enabled flag) and update shadow roots.
*/
storage.watch({
styles: () => updateCSS(),
enabled: () => updateCSS()
})

/**
* Initialize: read current state and do first pass.
*/
async function init() {
await updateCSS()

observer.observe(document.body, {
childList: true,
subtree: true
})
}

init()
21 changes: 21 additions & 0 deletions helpers/buildCSSString.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import type { TStyle } from "./constants"

export const buildCSSString = (css: TStyle): string => {
if (!css) return ""

let globalStyles = ""
let paragraphStyles = ""

for (const [key, value] of Object.entries(css)) {
if (value === "") continue
if (key === "paragraph-spacing") {
paragraphStyles += `margin-bottom: ${value}em !important;`
} else if (key === "line-height") {
globalStyles += `${key}: ${value} !important;`
} else {
globalStyles += `${key}: ${value}em !important;`
}
}

return `* { ${globalStyles} } p { ${paragraphStyles} }`
}
36 changes: 4 additions & 32 deletions helpers/buildCSSToInject.ts
Original file line number Diff line number Diff line change
@@ -1,42 +1,14 @@
import type { TStyle } from "./constants"
import t from "./t"
import { buildCSSString } from "./buildCSSString"

export const buildCSSToInject = (css: TStyle | string, tabId: number) => {
let globalStyles = ""
let paragraphStyles = ""
const cssString = typeof css === "string" ? css : buildCSSString(css)

if (typeof css === "string") {
return {
target: {
tabId: tabId,
allFrames: true
},
css
}
}

for (const [key, value] of Object.entries(css)) {
// Parse style object and format CSS properties
if (key === "paragraph-spacing") {
paragraphStyles += `margin-bottom: ${value}em !important;`
} else if (key !== "line-height") {
globalStyles += `${key}: ${
value !== "" ? `${value}em !important;` : value
}`
} else {
globalStyles += `${key}: ${
value !== "" ? `${value} !important;` : value
}`
}
}

const payload = {
return {
target: {
tabId: tabId,
allFrames: true
},
css: `* { ${globalStyles} } p { ${paragraphStyles} }`
css: cssString
}

return payload
}