import React from "react"; import ReactDOM from "react-dom/client"; import browser from "webextension-polyfill"; import type { VNode } from "./lib/extract"; import { cssPropertiesDefault } from "./lib/CSSPropertiesDefaultValue"; console.log("Hello from sidebar!"); function getUniqueSelector(el: Element) { if (!(el instanceof Element)) throw new Error("Invalid argument"); var path = []; while (el.nodeType === Node.ELEMENT_NODE) { let selector = el.nodeName.toLowerCase(); const parentElement = el.parentElement; if (el.id) { selector += '#' + el.id; path.unshift(selector); break; } if (!parentElement) { break; } let nth = 1; let sib = el.previousElementSibling; while (sib) { nth++; sib = sib.previousElementSibling; } selector += ":nth-child(" + nth + ")"; path.unshift(selector); el = parentElement; } return path.join(" > "); } const code = ` (() => { let getUniqueSelector = ${getUniqueSelector.toString()}; return getUniqueSelector($0); })() ` function useSelectedElement() { const [element, setElement] = React.useState<{ vnode: VNode, inheritedStyle: { [key: string]: string } } | null>(); React.useEffect(() => { const handler = async () => { const id = Math.floor(Math.random() * 1000000); // get selector of the selected element console.log("evaluating code"); const [selector, isException] = await browser.devtools.inspectedWindow.eval(code); if (isException) { console.error("Error evaluating code", isException); return; } console.log(selector); const tab = await browser.tabs.query({ active: true, currentWindow: true }); const tabId = browser.devtools.inspectedWindow.tabId; console.log("sending message", tabId); // post message to content script to get vnode of the selected element const res = await browser.tabs.sendMessage(tabId, { type: "cloneWithStyle", elementSelector: selector, id }); console.log("got response", res); if (!res) { console.error("No response"); return; } if (res.type !== "cloneWithStyleResult") { console.error("Unexpected response", res); return; } console.log("got response"); setElement({ vnode: res.vnode, inheritedStyle: res.inheritedStyle }); }; if (!element) { handler(); } browser.devtools.panels.elements.onSelectionChanged.addListener(handler); return () => { browser.devtools.panels.elements.onSelectionChanged.removeListener(handler); }; }, []); return element; } function useGetComputedStyle() { const [styles, setStyles] = React.useState<{ [key: string]: string } | null>(null); React.useEffect(() => { const handler = async () => { const [styles, isException] = await browser.devtools.inspectedWindow.eval(` (() => { const element = $0; const style = getComputedStyle(element); const styles = {}; for (let i = 0; i < style.length; i++) { const property = style.item(i); styles[property] = style.getPropertyValue(property); } return styles; })() `); if (isException) { console.error("Error evaluating code", isException); return; } let newStyles = {} as { [key: string]: string }; for (const key in styles) { if (key in cssPropertiesDefault) { const value = cssPropertiesDefault[key as keyof typeof cssPropertiesDefault]; if (styles[key] != value) { newStyles[key] = value; } } else { newStyles[key] = styles[key]; } } setStyles(newStyles); }; if (!styles) { handler(); } browser.devtools.panels.elements.onSelectionChanged.addListener(handler); return () => { browser.devtools.panels.elements.onSelectionChanged.removeListener(handler); }; }, []); return styles; } function kebabToCamel(str: string) { return str.replace(/-./g, (match) => match[1].toUpperCase()); } function htmlStyleToReactStyle(style: { [key: string]: string }) { return Object.fromEntries(Object.entries(style).map(([key, value]) => [kebabToCamel(key), value])); } const HTMLComponentTable: { [key: string]: keyof JSX.IntrinsicElements } = { "a": "a", "abbr": "abbr", "address": "address", "area": "area", "article": "article", "aside": "aside", // "audio": "audio", "b": "b", "base": "base", "bdi": "bdi", "bdo": "bdo", "big": "big", "blockquote": "blockquote", "body": "body", "br": "br", "button": "button", "canvas": "canvas", "caption": "caption", "center": "center", "cite": "cite", "code": "code", "col": "col", "colgroup": "colgroup", "data": "data", "datalist": "datalist", "dd": "dd", "del": "del", "details": "details", "dfn": "dfn", "dialog": "dialog", "div": "div", "dl": "dl", "dt": "dt", "em": "em", "embed": "embed", "fieldset": "fieldset", "figcaption": "figcaption", "figure": "figure", "footer": "footer", "form": "form", "h1": "h1", "h2": "h2", "h3": "h3", "h4": "h4", "h5": "h5", "h6": "h6", // "head": "head", "header": "header", "hgroup": "hgroup", "hr": "hr", // "html": "html", "i": "i", // "iframe": "iframe", // "img": "img", "input": "input", "ins": "ins", "kbd": "kbd", "keygen": "keygen", "label": "label", "legend": "legend", "li": "li", "link": "link", "main": "main", "map": "map", "mark": "mark", "menu": "menu", "menuitem": "menuitem", // "meta": "meta", "meter": "meter", "nav": "nav", "noindex": "noindex", "noscript": "noscript", "object": "object", "ol": "ol", "optgroup": "optgroup", "option": "option", "output": "output", "p": "p", "param": "param", "picture": "picture", "pre": "pre", "progress": "progress", "q": "q", "rp": "rp", "rt": "rt", "ruby": "ruby", "s": "s", "samp": "samp", "search": "search", "slot": "slot", "script": "script", "section": "section", "select": "select", "small": "small", "source": "source", "span": "span", "strong": "strong", "style": "style", "sub": "sub", "summary": "summary", "sup": "sup", "table": "table", "template": "template", "tbody": "tbody", "td": "td", "textarea": "textarea", "tfoot": "tfoot", "th": "th", "thead": "thead", "time": "time", "title": "title", "tr": "tr", "track": "track", "u": "u", "ul": "ul", "var": "var", // "video": "video", "wbr": "wbr", "webview": "webview", } function VnodeToReact({ vnode }: { vnode: VNode }): React.ReactNode { if (vnode.type === "text") { return vnode.text; } else if (vnode.type === "element") { // TODO: support svg const Tag = HTMLComponentTable[vnode.tagName] ?? "div"; const style = vnode.style; const children = vnode.children; return {children.map((child, i) => )} } } function escapseAttrValue(value: string) { return value.replaceAll('"', '"') } function VnodeToHTML({ vnode }: { vnode: VNode }): string { if (vnode.type === "text") { return vnode.text; } else if (vnode.type === "element") { const Tag = vnode.tagName; const style = vnode.style; const children = vnode.children; return `<${Tag} style="${Object.entries(style).map(([key, value]) => `${key}: ${escapseAttrValue(value)}`).join(";")}"> ${children.map((child, i) => VnodeToHTML({ vnode: child })).join("")} ` } return ""; } function inheritStyleToStyleText(inheritedStyle: { [key: string]: string }) { return Object.entries(inheritedStyle).map(([key, value]) => `${key}: ${value}`).join(";\n"); } function Content() { const data = useSelectedElement(); const inheritedStyleText = data ? inheritStyleToStyleText(data.inheritedStyle) : null; const html = data ? VnodeToHTML({ vnode: data?.vnode }) : ""; return

Vnode

{inheritedStyleText}
{html} {data &&
}
; } ReactDOM.createRoot(document.querySelector("#app")!).render( );