initial commit
This commit is contained in:
commit
9d4801e388
30 changed files with 11232 additions and 0 deletions
28
.gitignore
vendored
Normal file
28
.gitignore
vendored
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
lerna-debug.log*
|
||||||
|
|
||||||
|
node_modules
|
||||||
|
dist
|
||||||
|
dist-ssr
|
||||||
|
*.local
|
||||||
|
|
||||||
|
# Editor directories and files
|
||||||
|
.vscode/*
|
||||||
|
!.vscode/extensions.json
|
||||||
|
.idea
|
||||||
|
.DS_Store
|
||||||
|
*.suo
|
||||||
|
*.ntvs*
|
||||||
|
*.njsproj
|
||||||
|
*.sln
|
||||||
|
*.sw?
|
||||||
|
|
||||||
|
# Config files
|
||||||
|
.webextrc
|
||||||
|
.webextrc.*
|
9
LICENSE
Normal file
9
LICENSE
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
The MIT License (MIT)
|
||||||
|
|
||||||
|
Copyright (c) 2025 - 2026, monoid
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
28
README.md
Normal file
28
README.md
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
# Style Copy Browser Extension
|
||||||
|
|
||||||
|
This is a browser extension that allows you to copy the styles of any element on a webpage and paste them into your own CSS.
|
||||||
|
|
||||||
|
This is a work in progress and is not yet available for download.
|
||||||
|
|
||||||
|
## How to use
|
||||||
|
|
||||||
|
Open dev tools and click on an element. Sidebar will open with the styles of the element. Click on the copy button to copy the styles.
|
||||||
|
|
||||||
|
## Motivation
|
||||||
|
|
||||||
|
I wanted to create a tool that would allow me to easily copy the styles of an element on a webpage and paste them into my own CSS.
|
||||||
|
other tools 'divMagic' and 'CSS Peeper' are great but they are not open source and I wanted to create something that was open source and free to use.
|
||||||
|
|
||||||
|
## Tech Stack
|
||||||
|
|
||||||
|
- React
|
||||||
|
- TypeScript
|
||||||
|
- WebExtensions API
|
||||||
|
- Vite
|
||||||
|
|
||||||
|
## Development
|
||||||
|
|
||||||
|
1. Clone the repository
|
||||||
|
2. Run `pnpm install`
|
||||||
|
3. Run `pnpm run dev` to start the development server
|
||||||
|
4. browser will open with the extension installed
|
25
package.json
Normal file
25
package.json
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
{
|
||||||
|
"name": "style-copy-extension",
|
||||||
|
"private": true,
|
||||||
|
"version": "1.0.0",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite",
|
||||||
|
"build": "tsc && vite build"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"react": "^18.2.0",
|
||||||
|
"react-dom": "^18.2.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/react": "^18.0.26",
|
||||||
|
"@types/react-dom": "^18.0.9",
|
||||||
|
"@types/webextension-polyfill": "^0.10.0",
|
||||||
|
"@vitejs/plugin-react": "^4.2.1",
|
||||||
|
"typescript": "~5.6.3",
|
||||||
|
"vite": "^5.0.0",
|
||||||
|
"vite-plugin-web-extension": "^4.0.0",
|
||||||
|
"webextension-polyfill": "^0.10.0"
|
||||||
|
},
|
||||||
|
"packageManager": "pnpm@9.15.4+sha512.b2dc20e2fc72b3e18848459b37359a32064663e5627a51e4c74b2c29dd8e8e0491483c3abb40789cfd578bf362fb6ba8261b05f0387d76792ed6e23ea3b1b6a0"
|
||||||
|
}
|
2766
pnpm-lock.yaml
generated
Normal file
2766
pnpm-lock.yaml
generated
Normal file
File diff suppressed because it is too large
Load diff
29
public/icon-with-shadow.svg
Normal file
29
public/icon-with-shadow.svg
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
<svg width="585" height="585" viewBox="0 0 585 585" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<mask id="mask0_2_13" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="0" y="0" width="585" height="585">
|
||||||
|
<path d="M585 292.5C585 454.043 454.043 585 292.5 585C130.957 585 0 454.043 0 292.5C0 130.957 130.957 0 292.5 0C454.043 0 585 130.957 585 292.5Z" fill="url(#paint0_radial_2_13)"/>
|
||||||
|
</mask>
|
||||||
|
<g mask="url(#mask0_2_13)">
|
||||||
|
<path d="M585 292.5C585 454.043 454.043 585 292.5 585C130.957 585 0 454.043 0 292.5C0 130.957 130.957 0 292.5 0C454.043 0 585 130.957 585 292.5Z" fill="url(#paint1_linear_2_13)"/>
|
||||||
|
</g>
|
||||||
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M281 182C308.614 182 331 159.614 331 132L401 132C417.569 132 431 145.431 431 162V232.158C456.744 234.195 477 255.732 477 282C477 308.268 456.744 329.805 431 331.842V402C431 418.569 417.569 432 401 432H331C331 459.614 308.614 482 281 482C253.386 482 231 459.614 231 432H161C144.431 432 131 418.569 131 402L131 332C158.614 332 181 309.614 181 282C181 254.386 158.614 232 131 232L131 162C131 145.431 144.431 132 161 132L231 132C231 159.614 253.386 182 281 182Z" fill="url(#paint2_linear_2_13)"/>
|
||||||
|
<path d="M364.115 102.193L234.791 127.497C232.666 127.913 231.092 129.712 230.964 131.871L223.009 266.034C222.821 269.194 225.728 271.647 228.816 270.936L264.822 262.638C268.191 261.862 271.235 264.825 270.543 268.208L259.845 320.515C259.125 324.035 262.435 327.046 265.878 326.001L288.117 319.254C291.565 318.209 294.877 321.228 294.148 324.751L277.148 406.914C276.084 412.053 282.93 414.856 285.785 410.449L287.692 407.507L393.073 197.505C394.838 193.989 391.795 189.98 387.927 190.725L350.865 197.867C347.382 198.538 344.419 195.299 345.402 191.897L369.592 108.161C370.576 104.753 367.602 101.511 364.115 102.193Z" fill="url(#paint3_linear_2_13)"/>
|
||||||
|
<defs>
|
||||||
|
<radialGradient id="paint0_radial_2_13" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(292.958 292.042) rotate(90) scale(292.958 292.958)">
|
||||||
|
<stop/>
|
||||||
|
<stop offset="1" stop-opacity="0"/>
|
||||||
|
</radialGradient>
|
||||||
|
<linearGradient id="paint1_linear_2_13" x1="216.056" y1="140.528" x2="477.969" y2="374.671" gradientUnits="userSpaceOnUse">
|
||||||
|
<stop stop-color="#41D1FF"/>
|
||||||
|
<stop offset="1" stop-color="#BD34FE"/>
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient id="paint2_linear_2_13" x1="116.5" y1="223" x2="414.5" y2="482" gradientUnits="userSpaceOnUse">
|
||||||
|
<stop stop-color="#41D1FF"/>
|
||||||
|
<stop offset="1" stop-color="#BD34FE"/>
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient id="paint3_linear_2_13" x1="270.74" y1="109.063" x2="309.973" y2="378.586" gradientUnits="userSpaceOnUse">
|
||||||
|
<stop stop-color="#FFEA83"/>
|
||||||
|
<stop offset="0.0833333" stop-color="#FFDD35"/>
|
||||||
|
<stop offset="1" stop-color="#FFA800"/>
|
||||||
|
</linearGradient>
|
||||||
|
</defs>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 2.7 KiB |
BIN
public/icon/128.png
Normal file
BIN
public/icon/128.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 12 KiB |
BIN
public/icon/16.png
Normal file
BIN
public/icon/16.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 698 B |
BIN
public/icon/32.png
Normal file
BIN
public/icon/32.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.6 KiB |
BIN
public/icon/48.png
Normal file
BIN
public/icon/48.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 2.9 KiB |
BIN
public/icon/96.png
Normal file
BIN
public/icon/96.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 7.9 KiB |
33
src/background.ts
Normal file
33
src/background.ts
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
import browser from "webextension-polyfill";
|
||||||
|
|
||||||
|
console.log("Hello from the background!");
|
||||||
|
|
||||||
|
browser.runtime.onInstalled.addListener((details) => {
|
||||||
|
console.log("Extension installed:", details);
|
||||||
|
console.log("undefined",document);
|
||||||
|
});
|
||||||
|
|
||||||
|
browser.runtime.onMessage.addListener((message, sender, sendResponse) => {
|
||||||
|
console.log("Got message", message);
|
||||||
|
if (message.type === "fetch") {
|
||||||
|
return ( async () => {
|
||||||
|
const response = await fetch(message.url);
|
||||||
|
if (!response.ok) {
|
||||||
|
console.error(`Failed to fetch: ${response.statusText}`);
|
||||||
|
throw new Error(`Failed to fetch: ${response.statusText}`);
|
||||||
|
}
|
||||||
|
if ( message.resType === "text" ) {
|
||||||
|
const text = await response.text();
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
else if ( message.resType === "json" ) {
|
||||||
|
const json = await response.json();
|
||||||
|
return json;
|
||||||
|
}
|
||||||
|
else if ( message.resType === "array" ) {
|
||||||
|
const array = await response.arrayBuffer();
|
||||||
|
return array;
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
}
|
||||||
|
});
|
54
src/content.ts
Normal file
54
src/content.ts
Normal file
|
@ -0,0 +1,54 @@
|
||||||
|
import { cloneWithStyle, getInheritedStyle, styleFromCSSStyleRule } from "./lib/extract";
|
||||||
|
import browser from "webextension-polyfill";
|
||||||
|
|
||||||
|
console.log("Hello from content!");
|
||||||
|
|
||||||
|
browser.runtime.onMessage.addListener((event, sender, sendResponse) => {
|
||||||
|
console.log("Got message", event);
|
||||||
|
if (event.type != "cloneWithStyle") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
console.log("Cloning with style");
|
||||||
|
const { elementSelector, id } = event;
|
||||||
|
try {
|
||||||
|
const element = document.querySelector(elementSelector) as HTMLElement;
|
||||||
|
if (!element) {
|
||||||
|
console.error("Element not found");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
return cloneWithStyle(element).then((vnode) => {
|
||||||
|
const inheritedStyle = getInheritedStyle(element);
|
||||||
|
const ret = {
|
||||||
|
type: "cloneWithStyleResult",
|
||||||
|
vnode,
|
||||||
|
inheritedStyle,
|
||||||
|
id
|
||||||
|
};
|
||||||
|
console.log("Sending response", ret);
|
||||||
|
return (ret);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
catch (e) {
|
||||||
|
console.error("Error cloning with style", e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// const functionCallTable = {
|
||||||
|
// cloneWithStyle,
|
||||||
|
// styleFromCSSStyleRule
|
||||||
|
// } as any;
|
||||||
|
// window.addEventListener("message", async (event) => {
|
||||||
|
// if (event.source !== window) {
|
||||||
|
// return;
|
||||||
|
// }
|
||||||
|
// if (event.data.type !== "functionCall") {
|
||||||
|
// return;
|
||||||
|
// }
|
||||||
|
// const { functionName, args, id } = event.data;
|
||||||
|
// if (!functionCallTable[functionName]) {
|
||||||
|
// console.error(`Function ${functionName} not found`);
|
||||||
|
// return;
|
||||||
|
// }
|
||||||
|
// const result = await functionCallTable[functionName](...args);
|
||||||
|
// window.postMessage({ type: "functionCallResult", result, id }, "*");
|
||||||
|
// });
|
13
src/devtools.html
Normal file
13
src/devtools.html
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>DevTools</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="app"></div>
|
||||||
|
test
|
||||||
|
<script type="module" src="./devtools.ts"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
7
src/devtools.ts
Normal file
7
src/devtools.ts
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
import browser from "webextension-polyfill";
|
||||||
|
|
||||||
|
|
||||||
|
console.log("Hello from devtools!");
|
||||||
|
browser.devtools.panels.elements.createSidebarPane("My Sidebar").then((sidebar) => {
|
||||||
|
sidebar.setPage("src/sidebar.html");
|
||||||
|
});
|
388
src/lib/CSSPropertiesDefaultValue.ts
Normal file
388
src/lib/CSSPropertiesDefaultValue.ts
Normal file
|
@ -0,0 +1,388 @@
|
||||||
|
// Chrome default css properties
|
||||||
|
// TODO: Firefox, Edge, Safari, Opera, etc.
|
||||||
|
export const cssPropertiesDefault = {
|
||||||
|
"accent-color": "auto",
|
||||||
|
"align-content": "normal",
|
||||||
|
"align-items": "normal",
|
||||||
|
"align-self": "auto",
|
||||||
|
"alignment-baseline": "auto",
|
||||||
|
"anchor-name": "none",
|
||||||
|
"anchor-scope": "none",
|
||||||
|
"animation-composition": "replace",
|
||||||
|
"animation-delay": "0s",
|
||||||
|
"animation-direction": "normal",
|
||||||
|
"animation-duration": "0s",
|
||||||
|
"animation-fill-mode": "none",
|
||||||
|
"animation-iteration-count": "1",
|
||||||
|
"animation-name": "none",
|
||||||
|
"animation-play-state": "running",
|
||||||
|
"animation-range-end": "normal",
|
||||||
|
"animation-range-start": "normal",
|
||||||
|
"animation-timeline": "auto",
|
||||||
|
"animation-timing-function": "ease",
|
||||||
|
"app-region": "none",
|
||||||
|
"appearance": "none",
|
||||||
|
"backdrop-filter": "none",
|
||||||
|
"backface-visibility": "visible",
|
||||||
|
"background-attachment": "scroll",
|
||||||
|
"background-blend-mode": "normal",
|
||||||
|
"background-clip": "border-box",
|
||||||
|
"background-color": "rgba(0, 0, 0, 0)",
|
||||||
|
"background-image": "none",
|
||||||
|
"background-origin": "padding-box",
|
||||||
|
"background-position": "0% 0%",
|
||||||
|
"background-repeat": "repeat",
|
||||||
|
"background-size": "auto",
|
||||||
|
"baseline-shift": "0px",
|
||||||
|
"baseline-source": "auto",
|
||||||
|
"block-size": "0px",
|
||||||
|
"border-block-end-color": "rgb(0, 0, 0)",
|
||||||
|
"border-block-end-style": "none",
|
||||||
|
"border-block-end-width": "0px",
|
||||||
|
"border-block-start-color": "rgb(0, 0, 0)",
|
||||||
|
"border-block-start-style": "none",
|
||||||
|
"border-block-start-width": "0px",
|
||||||
|
"border-bottom-color": "rgb(0, 0, 0)",
|
||||||
|
"border-bottom-left-radius": "0px",
|
||||||
|
"border-bottom-right-radius": "0px",
|
||||||
|
"border-bottom-style": "none",
|
||||||
|
"border-bottom-width": "0px",
|
||||||
|
"border-collapse": "separate",
|
||||||
|
"border-end-end-radius": "0px",
|
||||||
|
"border-end-start-radius": "0px",
|
||||||
|
"border-image-outset": "0",
|
||||||
|
"border-image-repeat": "stretch",
|
||||||
|
"border-image-slice": "100%",
|
||||||
|
"border-image-source": "none",
|
||||||
|
"border-image-width": "1",
|
||||||
|
"border-inline-end-color": "rgb(0, 0, 0)",
|
||||||
|
"border-inline-end-style": "none",
|
||||||
|
"border-inline-end-width": "0px",
|
||||||
|
"border-inline-start-color": "rgb(0, 0, 0)",
|
||||||
|
"border-inline-start-style": "none",
|
||||||
|
"border-inline-start-width": "0px",
|
||||||
|
"border-left-color": "rgb(0, 0, 0)",
|
||||||
|
"border-left-style": "none",
|
||||||
|
"border-left-width": "0px",
|
||||||
|
"border-right-color": "rgb(0, 0, 0)",
|
||||||
|
"border-right-style": "none",
|
||||||
|
"border-right-width": "0px",
|
||||||
|
"border-start-end-radius": "0px",
|
||||||
|
"border-start-start-radius": "0px",
|
||||||
|
"border-top-color": "rgb(0, 0, 0)",
|
||||||
|
"border-top-left-radius": "0px",
|
||||||
|
"border-top-right-radius": "0px",
|
||||||
|
"border-top-style": "none",
|
||||||
|
"border-top-width": "0px",
|
||||||
|
"bottom": "auto",
|
||||||
|
"box-decoration-break": "slice",
|
||||||
|
"box-shadow": "none",
|
||||||
|
"box-sizing": "content-box",
|
||||||
|
"break-after": "auto",
|
||||||
|
"break-before": "auto",
|
||||||
|
"break-inside": "auto",
|
||||||
|
"buffered-rendering": "auto",
|
||||||
|
"caption-side": "top",
|
||||||
|
"caret-color": "rgb(0, 0, 0)",
|
||||||
|
"clear": "none",
|
||||||
|
"clip": "auto",
|
||||||
|
"clip-path": "none",
|
||||||
|
"clip-rule": "nonzero",
|
||||||
|
"color": "rgb(0, 0, 0)",
|
||||||
|
"color-interpolation": "srgb",
|
||||||
|
"color-interpolation-filters": "linearrgb",
|
||||||
|
"color-rendering": "auto",
|
||||||
|
"column-count": "auto",
|
||||||
|
"column-gap": "normal",
|
||||||
|
"column-rule-color": "rgb(0, 0, 0)",
|
||||||
|
"column-rule-style": "none",
|
||||||
|
"column-rule-width": "0px",
|
||||||
|
"column-span": "none",
|
||||||
|
"column-width": "auto",
|
||||||
|
"contain-intrinsic-block-size": "none",
|
||||||
|
"contain-intrinsic-height": "none",
|
||||||
|
"contain-intrinsic-inline-size": "none",
|
||||||
|
"contain-intrinsic-size": "none",
|
||||||
|
"contain-intrinsic-width": "none",
|
||||||
|
"container-name": "none",
|
||||||
|
"container-type": "normal",
|
||||||
|
"content": "normal",
|
||||||
|
"cursor": "auto",
|
||||||
|
"cx": "0px",
|
||||||
|
"cy": "0px",
|
||||||
|
"d": "none",
|
||||||
|
"direction": "ltr",
|
||||||
|
"display": "block",
|
||||||
|
"dominant-baseline": "auto",
|
||||||
|
"empty-cells": "show",
|
||||||
|
"field-sizing": "fixed",
|
||||||
|
"fill": "rgb(0, 0, 0)",
|
||||||
|
"fill-opacity": "1",
|
||||||
|
"fill-rule": "nonzero",
|
||||||
|
"filter": "none",
|
||||||
|
"flex-basis": "auto",
|
||||||
|
"flex-direction": "row",
|
||||||
|
"flex-grow": "0",
|
||||||
|
"flex-shrink": "1",
|
||||||
|
"flex-wrap": "nowrap",
|
||||||
|
"float": "none",
|
||||||
|
"flood-color": "rgb(0, 0, 0)",
|
||||||
|
"flood-opacity": "1",
|
||||||
|
"font-family": "\"Malgun Gothic\"",
|
||||||
|
"font-kerning": "auto",
|
||||||
|
"font-optical-sizing": "auto",
|
||||||
|
"font-palette": "normal",
|
||||||
|
"font-size": "16px",
|
||||||
|
"font-size-adjust": "none",
|
||||||
|
"font-stretch": "100%",
|
||||||
|
"font-style": "normal",
|
||||||
|
"font-synthesis-small-caps": "auto",
|
||||||
|
"font-synthesis-style": "auto",
|
||||||
|
"font-synthesis-weight": "auto",
|
||||||
|
"font-variant": "normal",
|
||||||
|
"font-variant-alternates": "normal",
|
||||||
|
"font-variant-caps": "normal",
|
||||||
|
"font-variant-east-asian": "normal",
|
||||||
|
"font-variant-emoji": "normal",
|
||||||
|
"font-variant-ligatures": "normal",
|
||||||
|
"font-variant-numeric": "normal",
|
||||||
|
"font-variant-position": "normal",
|
||||||
|
"font-weight": "400",
|
||||||
|
"grid-auto-columns": "auto",
|
||||||
|
"grid-auto-flow": "row",
|
||||||
|
"grid-auto-rows": "auto",
|
||||||
|
"grid-column-end": "auto",
|
||||||
|
"grid-column-start": "auto",
|
||||||
|
"grid-row-end": "auto",
|
||||||
|
"grid-row-start": "auto",
|
||||||
|
"grid-template-areas": "none",
|
||||||
|
"grid-template-columns": "none",
|
||||||
|
"grid-template-rows": "none",
|
||||||
|
"height": "0px",
|
||||||
|
"hyphenate-character": "auto",
|
||||||
|
"hyphenate-limit-chars": "auto",
|
||||||
|
"hyphens": "manual",
|
||||||
|
"image-orientation": "from-image",
|
||||||
|
"image-rendering": "auto",
|
||||||
|
"initial-letter": "normal",
|
||||||
|
"inline-size": "1904px",
|
||||||
|
"inset-block-end": "auto",
|
||||||
|
"inset-block-start": "auto",
|
||||||
|
"inset-inline-end": "auto",
|
||||||
|
"inset-inline-start": "auto",
|
||||||
|
"interpolate-size": "numeric-only",
|
||||||
|
"isolation": "auto",
|
||||||
|
"justify-content": "normal",
|
||||||
|
"justify-items": "normal",
|
||||||
|
"justify-self": "auto",
|
||||||
|
"left": "auto",
|
||||||
|
"letter-spacing": "normal",
|
||||||
|
"lighting-color": "rgb(255, 255, 255)",
|
||||||
|
"line-break": "auto",
|
||||||
|
"line-height": "normal",
|
||||||
|
"list-style-image": "none",
|
||||||
|
"list-style-position": "outside",
|
||||||
|
"list-style-type": "disc",
|
||||||
|
"margin-block-end": "0px",
|
||||||
|
"margin-block-start": "0px",
|
||||||
|
"margin-bottom": "0px",
|
||||||
|
"margin-inline-end": "0px",
|
||||||
|
"margin-inline-start": "0px",
|
||||||
|
"margin-left": "0px",
|
||||||
|
"margin-right": "0px",
|
||||||
|
"margin-top": "0px",
|
||||||
|
"marker-end": "none",
|
||||||
|
"marker-mid": "none",
|
||||||
|
"marker-start": "none",
|
||||||
|
"mask-clip": "border-box",
|
||||||
|
"mask-composite": "add",
|
||||||
|
"mask-image": "none",
|
||||||
|
"mask-mode": "match-source",
|
||||||
|
"mask-origin": "border-box",
|
||||||
|
"mask-position": "0% 0%",
|
||||||
|
"mask-repeat": "repeat",
|
||||||
|
"mask-size": "auto",
|
||||||
|
"mask-type": "luminance",
|
||||||
|
"math-depth": "0",
|
||||||
|
"math-shift": "normal",
|
||||||
|
"math-style": "normal",
|
||||||
|
"max-block-size": "none",
|
||||||
|
"max-height": "none",
|
||||||
|
"max-inline-size": "none",
|
||||||
|
"max-width": "none",
|
||||||
|
"min-block-size": "0px",
|
||||||
|
"min-height": "0px",
|
||||||
|
"min-inline-size": "0px",
|
||||||
|
"min-width": "0px",
|
||||||
|
"mix-blend-mode": "normal",
|
||||||
|
"object-fit": "fill",
|
||||||
|
"object-position": "50% 50%",
|
||||||
|
"object-view-box": "none",
|
||||||
|
"offset-anchor": "auto",
|
||||||
|
"offset-distance": "0px",
|
||||||
|
"offset-path": "none",
|
||||||
|
"offset-position": "normal",
|
||||||
|
"offset-rotate": "auto 0deg",
|
||||||
|
"opacity": "1",
|
||||||
|
"order": "0",
|
||||||
|
"orphans": "2",
|
||||||
|
"outline-color": "rgb(0, 0, 0)",
|
||||||
|
"outline-offset": "0px",
|
||||||
|
"outline-style": "none",
|
||||||
|
"outline-width": "0px",
|
||||||
|
"overflow-anchor": "auto",
|
||||||
|
"overflow-clip-margin": "0px",
|
||||||
|
"overflow-wrap": "normal",
|
||||||
|
"overflow-x": "visible",
|
||||||
|
"overflow-y": "visible",
|
||||||
|
"overlay": "none",
|
||||||
|
"overscroll-behavior-block": "auto",
|
||||||
|
"overscroll-behavior-inline": "auto",
|
||||||
|
"padding-block-end": "0px",
|
||||||
|
"padding-block-start": "0px",
|
||||||
|
"padding-bottom": "0px",
|
||||||
|
"padding-inline-end": "0px",
|
||||||
|
"padding-inline-start": "0px",
|
||||||
|
"padding-left": "0px",
|
||||||
|
"padding-right": "0px",
|
||||||
|
"padding-top": "0px",
|
||||||
|
"paint-order": "normal",
|
||||||
|
"perspective": "none",
|
||||||
|
"perspective-origin": "952px 0px",
|
||||||
|
"pointer-events": "auto",
|
||||||
|
"position": "static",
|
||||||
|
"position-anchor": "auto",
|
||||||
|
"position-area": "none",
|
||||||
|
"position-try-fallbacks": "none",
|
||||||
|
"position-try-order": "normal",
|
||||||
|
"position-visibility": "always",
|
||||||
|
"r": "0px",
|
||||||
|
"resize": "none",
|
||||||
|
"right": "auto",
|
||||||
|
"rotate": "none",
|
||||||
|
"row-gap": "normal",
|
||||||
|
"ruby-align": "space-around",
|
||||||
|
"ruby-position": "over",
|
||||||
|
"rx": "auto",
|
||||||
|
"ry": "auto",
|
||||||
|
"scale": "none",
|
||||||
|
"scroll-behavior": "auto",
|
||||||
|
"scroll-margin-block-end": "0px",
|
||||||
|
"scroll-margin-block-start": "0px",
|
||||||
|
"scroll-margin-inline-end": "0px",
|
||||||
|
"scroll-margin-inline-start": "0px",
|
||||||
|
"scroll-padding-block-end": "auto",
|
||||||
|
"scroll-padding-block-start": "auto",
|
||||||
|
"scroll-padding-inline-end": "auto",
|
||||||
|
"scroll-padding-inline-start": "auto",
|
||||||
|
"scroll-timeline-axis": "block",
|
||||||
|
"scroll-timeline-name": "none",
|
||||||
|
"scrollbar-color": "auto",
|
||||||
|
"scrollbar-gutter": "auto",
|
||||||
|
"scrollbar-width": "auto",
|
||||||
|
"shape-image-threshold": "0",
|
||||||
|
"shape-margin": "0px",
|
||||||
|
"shape-outside": "none",
|
||||||
|
"shape-rendering": "auto",
|
||||||
|
"speak": "normal",
|
||||||
|
"stop-color": "rgb(0, 0, 0)",
|
||||||
|
"stop-opacity": "1",
|
||||||
|
"stroke": "none",
|
||||||
|
"stroke-dasharray": "none",
|
||||||
|
"stroke-dashoffset": "0px",
|
||||||
|
"stroke-linecap": "butt",
|
||||||
|
"stroke-linejoin": "miter",
|
||||||
|
"stroke-miterlimit": "4",
|
||||||
|
"stroke-opacity": "1",
|
||||||
|
"stroke-width": "1px",
|
||||||
|
"tab-size": "8",
|
||||||
|
"table-layout": "auto",
|
||||||
|
"text-align": "start",
|
||||||
|
"text-align-last": "auto",
|
||||||
|
"text-anchor": "start",
|
||||||
|
"text-decoration": "none solid rgb(0, 0, 0)",
|
||||||
|
"text-decoration-color": "rgb(0, 0, 0)",
|
||||||
|
"text-decoration-line": "none",
|
||||||
|
"text-decoration-skip-ink": "auto",
|
||||||
|
"text-decoration-style": "solid",
|
||||||
|
"text-emphasis-color": "rgb(0, 0, 0)",
|
||||||
|
"text-emphasis-position": "over",
|
||||||
|
"text-emphasis-style": "none",
|
||||||
|
"text-indent": "0px",
|
||||||
|
"text-overflow": "clip",
|
||||||
|
"text-rendering": "auto",
|
||||||
|
"text-shadow": "none",
|
||||||
|
"text-size-adjust": "auto",
|
||||||
|
"text-spacing-trim": "normal",
|
||||||
|
"text-transform": "none",
|
||||||
|
"text-underline-position": "auto",
|
||||||
|
"text-wrap-mode": "wrap",
|
||||||
|
"text-wrap-style": "auto",
|
||||||
|
"timeline-scope": "none",
|
||||||
|
"top": "auto",
|
||||||
|
"touch-action": "auto",
|
||||||
|
"transform": "none",
|
||||||
|
"transform-origin": "952px 0px",
|
||||||
|
"transform-style": "flat",
|
||||||
|
"transition-behavior": "normal",
|
||||||
|
"transition-delay": "0s",
|
||||||
|
"transition-duration": "0s",
|
||||||
|
"transition-property": "all",
|
||||||
|
"transition-timing-function": "ease",
|
||||||
|
"translate": "none",
|
||||||
|
"unicode-bidi": "isolate",
|
||||||
|
"user-select": "auto",
|
||||||
|
"vector-effect": "none",
|
||||||
|
"vertical-align": "baseline",
|
||||||
|
"view-timeline-axis": "block",
|
||||||
|
"view-timeline-inset": "auto",
|
||||||
|
"view-timeline-name": "none",
|
||||||
|
"view-transition-class": "none",
|
||||||
|
"view-transition-name": "none",
|
||||||
|
"visibility": "visible",
|
||||||
|
"white-space-collapse": "collapse",
|
||||||
|
"widows": "2",
|
||||||
|
"width": "1904px",
|
||||||
|
"will-change": "auto",
|
||||||
|
"word-break": "normal",
|
||||||
|
"word-spacing": "0px",
|
||||||
|
"writing-mode": "horizontal-tb",
|
||||||
|
"x": "0px",
|
||||||
|
"y": "0px",
|
||||||
|
"z-index": "auto",
|
||||||
|
"zoom": "1",
|
||||||
|
"-webkit-border-horizontal-spacing": "0px",
|
||||||
|
"-webkit-border-image": "none",
|
||||||
|
"-webkit-border-vertical-spacing": "0px",
|
||||||
|
"-webkit-box-align": "stretch",
|
||||||
|
"-webkit-box-decoration-break": "slice",
|
||||||
|
"-webkit-box-direction": "normal",
|
||||||
|
"-webkit-box-flex": "0",
|
||||||
|
"-webkit-box-ordinal-group": "1",
|
||||||
|
"-webkit-box-orient": "horizontal",
|
||||||
|
"-webkit-box-pack": "start",
|
||||||
|
"-webkit-box-reflect": "none",
|
||||||
|
"-webkit-font-smoothing": "auto",
|
||||||
|
"-webkit-line-break": "auto",
|
||||||
|
"-webkit-line-clamp": "none",
|
||||||
|
"-webkit-locale": "\"en\"",
|
||||||
|
"-webkit-mask-box-image": "none",
|
||||||
|
"-webkit-mask-box-image-outset": "0",
|
||||||
|
"-webkit-mask-box-image-repeat": "stretch",
|
||||||
|
"-webkit-mask-box-image-slice": "0 fill",
|
||||||
|
"-webkit-mask-box-image-source": "none",
|
||||||
|
"-webkit-mask-box-image-width": "auto",
|
||||||
|
"-webkit-print-color-adjust": "economy",
|
||||||
|
"-webkit-rtl-ordering": "logical",
|
||||||
|
"-webkit-tap-highlight-color": "rgba(0, 0, 0, 0.18)",
|
||||||
|
"-webkit-text-combine": "none",
|
||||||
|
"-webkit-text-decorations-in-effect": "none",
|
||||||
|
"-webkit-text-fill-color": "rgb(0, 0, 0)",
|
||||||
|
"-webkit-text-orientation": "vertical-right",
|
||||||
|
"-webkit-text-security": "none",
|
||||||
|
"-webkit-text-stroke-color": "rgb(0, 0, 0)",
|
||||||
|
"-webkit-text-stroke-width": "0px",
|
||||||
|
"-webkit-user-drag": "auto",
|
||||||
|
"-webkit-user-modify": "read-only",
|
||||||
|
"-webkit-writing-mode": "horizontal-tb"
|
||||||
|
}
|
313
src/lib/extract.ts
Normal file
313
src/lib/extract.ts
Normal file
|
@ -0,0 +1,313 @@
|
||||||
|
import browser from "webextension-polyfill";
|
||||||
|
|
||||||
|
// TODO: check if this is the correct way to calculate specificity. css parser might be better
|
||||||
|
function calculateSpecificity(selector: string): [number, number, number] {
|
||||||
|
// 결과를 저장할 Specificity 배열: [ID, Class/Attribute/Pseudo-class, Tag/Pseudo-element]
|
||||||
|
const specificity = [0, 0, 0] as [number, number, number];
|
||||||
|
|
||||||
|
// 정규식을 사용하여 선택자에서 ID, 클래스/속성/가상 클래스, 태그/가상 요소를 분리
|
||||||
|
const regex = {
|
||||||
|
id: /#[\w-]+/g, // ID 선택자
|
||||||
|
classAttrPseudo: /\.[\w-]+|\[[^\]]+\]|:[^\s>+~.#\[:]+/g, // 클래스, 속성 선택자, 가상 클래스
|
||||||
|
tagPseudoElement: /^[a-z]+|::[\w-]+/gi // 태그 이름 또는 가상 요소 (::before, ::after 등)
|
||||||
|
};
|
||||||
|
|
||||||
|
// ID 선택자 개수 계산
|
||||||
|
const idMatches = selector.match(regex.id);
|
||||||
|
if (idMatches) {
|
||||||
|
specificity[0] += idMatches.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 클래스, 속성 선택자 및 가상 클래스 개수 계산
|
||||||
|
const classAttrPseudoMatches = selector.match(regex.classAttrPseudo);
|
||||||
|
if (classAttrPseudoMatches) {
|
||||||
|
specificity[1] += classAttrPseudoMatches.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 태그 및 가상 요소 개수 계산
|
||||||
|
const tagPseudoElementMatches = selector.match(regex.tagPseudoElement);
|
||||||
|
if (tagPseudoElementMatches) {
|
||||||
|
specificity[2] += tagPseudoElementMatches.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
return specificity;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getCssRules(sheet: CSSStyleSheet) {
|
||||||
|
try {
|
||||||
|
return sheet.cssRules;
|
||||||
|
} catch (error) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getCssRulesCORS(sheet: CSSStyleSheet) {
|
||||||
|
const r = await getCssRules(sheet);
|
||||||
|
if (r) {
|
||||||
|
return r;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
// TODO: refactor this to extract the fetch function
|
||||||
|
const text = await browser.runtime.sendMessage({ type: "fetch", url: sheet.href, resType: "text" });
|
||||||
|
console.log("Fetched CSS", text.slice(0, 100));
|
||||||
|
const styleElem = document.createElement("style");
|
||||||
|
styleElem.textContent = text;
|
||||||
|
// TODO: check if this is the correct way.
|
||||||
|
// 스타일을 넣지 않으면 sheet가 생성되지 않음.
|
||||||
|
// 브라우저 엔진 말고 파싱 라이브러리 사용해서 성능 테스트 필요
|
||||||
|
document.head.appendChild(styleElem);
|
||||||
|
const rules = styleElem.sheet?.cssRules;
|
||||||
|
document.head.removeChild(styleElem);
|
||||||
|
return rules;
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error fetching CSS", error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class StyleStore {
|
||||||
|
private store = new Map<string, { rule: CSSStyleRule, specificity: [number, number, number] }>();
|
||||||
|
public add(rule: CSSStyleRule, order = 0) {
|
||||||
|
this.store.set(rule.selectorText, {
|
||||||
|
rule,
|
||||||
|
specificity: calculateSpecificity(rule.selectorText)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
public get(selector: string) {
|
||||||
|
return this.store.get(selector);
|
||||||
|
}
|
||||||
|
public getAll() {
|
||||||
|
return this.store;
|
||||||
|
}
|
||||||
|
static async fromDocument() {
|
||||||
|
const store = new StyleStore();
|
||||||
|
const sheets = document.styleSheets;
|
||||||
|
for (let i = 0; i < sheets.length; i++) {
|
||||||
|
let sheet = sheets[i];
|
||||||
|
let rules = await getCssRulesCORS(sheet as CSSStyleSheet);
|
||||||
|
if (rules) {
|
||||||
|
for (let j = 0; j < rules.length; j++) {
|
||||||
|
const rule = rules[j];
|
||||||
|
if (rule instanceof CSSStyleRule) {
|
||||||
|
const styleRule = rule;
|
||||||
|
store.add(styleRule);
|
||||||
|
}
|
||||||
|
else if (rule instanceof CSSMediaRule) {
|
||||||
|
const conditionText = rule.conditionText;
|
||||||
|
// assume all media rules are screen or bigger than 1024px
|
||||||
|
if (!conditionText || window.matchMedia(conditionText).matches) {
|
||||||
|
const mediaRules = rule.cssRules;
|
||||||
|
if (mediaRules) {
|
||||||
|
for (let k = 0; k < mediaRules.length; k++) {
|
||||||
|
const mediaStyleRule = mediaRules[k];
|
||||||
|
if (mediaStyleRule instanceof CSSStyleRule) {
|
||||||
|
store.add(mediaStyleRule);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return store;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let styleStore: StyleStore | null = null;
|
||||||
|
async function getStoreSingleton() {
|
||||||
|
if (!styleStore) {
|
||||||
|
styleStore = await StyleStore.fromDocument();
|
||||||
|
}
|
||||||
|
return styleStore;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getAllAppliedRuleOfElement(element: Element) {
|
||||||
|
const store = await getStoreSingleton();
|
||||||
|
const rules = [] as { rule: CSSStyleRule, specificity: [number, number, number], selectorText: string }[];
|
||||||
|
for (const [selector, { rule, specificity }] of store.getAll()) {
|
||||||
|
if (element.matches(selector)) {
|
||||||
|
rules.push({
|
||||||
|
rule,
|
||||||
|
specificity,
|
||||||
|
selectorText: selector
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
rules.sort((a, b) => {
|
||||||
|
if (a.specificity[0] !== b.specificity[0]) {
|
||||||
|
return a.specificity[0] - b.specificity[0];
|
||||||
|
} else if (a.specificity[1] !== b.specificity[1]) {
|
||||||
|
return a.specificity[1] - b.specificity[1];
|
||||||
|
} else {
|
||||||
|
return a.specificity[2] - b.specificity[2];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return rules;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type VElementNode = {
|
||||||
|
type: "element";
|
||||||
|
tagName: string;
|
||||||
|
style: { [key: string]: string };
|
||||||
|
children: VNode[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export type VTextNode = {
|
||||||
|
type: "text";
|
||||||
|
text: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type VNode = VElementNode | VTextNode;
|
||||||
|
|
||||||
|
export function styleFromCSSStyleRule(rule: CSSStyleRule) {
|
||||||
|
const style = {} as { [key: string]: string };
|
||||||
|
for (let i = 0; i < rule.style.length; i++) {
|
||||||
|
const property = rule.style.item(i);
|
||||||
|
style[property] = rule.style.getPropertyValue(property);
|
||||||
|
}
|
||||||
|
return style;
|
||||||
|
}
|
||||||
|
|
||||||
|
import { cssPropertiesDefault } from "./CSSPropertiesDefaultValue";
|
||||||
|
|
||||||
|
export async function cloneWithStyle(element: HTMLElement): Promise<VNode> {
|
||||||
|
const dom = {
|
||||||
|
type: "element",
|
||||||
|
tagName: element.tagName.toLowerCase(),
|
||||||
|
style: {} as { [key: string]: string },
|
||||||
|
children: [] as VNode[]
|
||||||
|
} as VElementNode;
|
||||||
|
const rules = await getAllAppliedRuleOfElement(element);
|
||||||
|
// rules.reverse();
|
||||||
|
// const computedStyle = getComputedStyle(element);
|
||||||
|
const cssInheritanceSet = new Set(cssInheritanceProperties);
|
||||||
|
for (const { rule } of rules) {
|
||||||
|
const style = styleFromCSSStyleRule(rule);
|
||||||
|
for (const property in style) {
|
||||||
|
const value = style[property];
|
||||||
|
if (value === "initial" && (!cssInheritanceSet.has(property))) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (cssPropertiesDefault[property as keyof typeof cssPropertiesDefault] === value && (!cssInheritanceSet.has(property))) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
dom.style[property] = style[property];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
element.style.cssText.split(";")
|
||||||
|
.filter((property) => property.trim())
|
||||||
|
.forEach((property) => {
|
||||||
|
const [key, value] = property.split(":");
|
||||||
|
dom.style[key.trim()] = value.trim();
|
||||||
|
});
|
||||||
|
for (let i = 0; i < element.childNodes.length; i++) {
|
||||||
|
const child = element.childNodes[i];
|
||||||
|
if (child.nodeType === Node.ELEMENT_NODE) {
|
||||||
|
dom.children.push(await cloneWithStyle(child as HTMLElement));
|
||||||
|
}
|
||||||
|
else if (child.nodeType === Node.TEXT_NODE) {
|
||||||
|
const text = child.textContent?.trim();
|
||||||
|
if (text) {
|
||||||
|
dom.children.push({
|
||||||
|
type: "text",
|
||||||
|
text
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return dom;
|
||||||
|
}
|
||||||
|
|
||||||
|
import { generatedProperties } from "./supportedCSSProperties";
|
||||||
|
|
||||||
|
const cssInheritanceProperties = (() => {
|
||||||
|
const inherit = generatedProperties.filter(p => p.inherited)
|
||||||
|
const longhands = new Set<string>();
|
||||||
|
for (const property of inherit) {
|
||||||
|
if (property.longhands) {
|
||||||
|
property.longhands.forEach(longhand => longhands.add(longhand));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return inherit.filter(p => !longhands.has(p.name))
|
||||||
|
.map(p => p.name)
|
||||||
|
.filter(p => !p.startsWith("-webkit-"));
|
||||||
|
// TODO: remove this filter after fixing the issue with -webkit- properties
|
||||||
|
})();
|
||||||
|
|
||||||
|
export function getInheritedStyle(element: HTMLElement) {
|
||||||
|
const style = {} as { [key: string]: string };
|
||||||
|
const computedStyle = window.getComputedStyle(element);
|
||||||
|
for (const property of cssInheritanceProperties) {
|
||||||
|
style[property] = computedStyle.getPropertyValue(property);
|
||||||
|
}
|
||||||
|
return style;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// function getComputedStyleWithoutDefault(element) {
|
||||||
|
// const inheritedProperties = new Set([
|
||||||
|
// "azimuth",
|
||||||
|
// "border-collapse",
|
||||||
|
// "border-spacing",
|
||||||
|
// "caption-side",
|
||||||
|
// "color",
|
||||||
|
// "cursor",
|
||||||
|
// "direction",
|
||||||
|
// "elevation",
|
||||||
|
// "empty-cells",
|
||||||
|
// "font-family",
|
||||||
|
// "font-size",
|
||||||
|
// "font-style",
|
||||||
|
// "font-variant",
|
||||||
|
// "font-weight",
|
||||||
|
// "font",
|
||||||
|
// "letter-spacing",
|
||||||
|
// "line-height",
|
||||||
|
// "list-style-image",
|
||||||
|
// "list-style-position",
|
||||||
|
// "list-style-type",
|
||||||
|
// "list-style",
|
||||||
|
// "orphans",
|
||||||
|
// "pitch-range",
|
||||||
|
// "pitch",
|
||||||
|
// "quotes",
|
||||||
|
// "richness",
|
||||||
|
// "speak-header",
|
||||||
|
// "speak-numeral",
|
||||||
|
// "speak-punctuation",
|
||||||
|
// "speak",
|
||||||
|
// "speech-rate",
|
||||||
|
// "stress",
|
||||||
|
// "text-align",
|
||||||
|
// "text-indent",
|
||||||
|
// "text-transform",
|
||||||
|
// "visibility",
|
||||||
|
// "voice-family",
|
||||||
|
// "volume",
|
||||||
|
// "white-space",
|
||||||
|
// "widows",
|
||||||
|
// "word-spacing",
|
||||||
|
// ]);
|
||||||
|
// const computedStyle = window.getComputedStyle(element);
|
||||||
|
// const parentComputedStyle = window.getComputedStyle(element.parentElement);
|
||||||
|
// const style = {};
|
||||||
|
// // TODO: add element's style to style
|
||||||
|
// const defaultStyle = window.getComputedStyle(document.documentElement);
|
||||||
|
// for (let i = 0; i < computedStyle.length; i++) {
|
||||||
|
// const property = computedStyle[i];
|
||||||
|
// if (inheritedProperties.has(property)) {
|
||||||
|
// if (computedStyle[property] !== parentComputedStyle[property]) {
|
||||||
|
// style[property] = computedStyle[property];
|
||||||
|
// }
|
||||||
|
// } else {
|
||||||
|
// // if default value is not equal to computed value then add it to style
|
||||||
|
// // remove default value like 0px, 0em, 0%, none, normal, auto
|
||||||
|
// const value = computedStyle.getPropertyValue(property);
|
||||||
|
// if (value !== defaultStyle.getPropertyValue(property)) {
|
||||||
|
// style[property] = value;
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// return style;
|
||||||
|
// }
|
6996
src/lib/supportedCSSProperties.ts
Normal file
6996
src/lib/supportedCSSProperties.ts
Normal file
File diff suppressed because it is too large
Load diff
32
src/manifest.json
Normal file
32
src/manifest.json
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
{
|
||||||
|
"{{chrome}}.manifest_version": 3,
|
||||||
|
"{{firefox}}.manifest_version": 3,
|
||||||
|
"icons": {
|
||||||
|
"16": "icon/16.png",
|
||||||
|
"32": "icon/32.png",
|
||||||
|
"48": "icon/48.png",
|
||||||
|
"96": "icon/96.png",
|
||||||
|
"128": "icon/128.png"
|
||||||
|
},
|
||||||
|
"{{chrome}}.action": {
|
||||||
|
"default_popup": "src/popup.html"
|
||||||
|
},
|
||||||
|
"devtools_page": "src/devtools.html",
|
||||||
|
"sidebar_action": {
|
||||||
|
"default_panel": "src/sidebar.html",
|
||||||
|
"default_title": "My Sidebar"
|
||||||
|
},
|
||||||
|
"{{firefox}}.browser_action": {
|
||||||
|
"default_popup": "src/popup.html"
|
||||||
|
},
|
||||||
|
"host_permissions": ["<all_urls>"],
|
||||||
|
"background": {
|
||||||
|
"service_worker": "src/background.ts"
|
||||||
|
},
|
||||||
|
"content_scripts": [
|
||||||
|
{
|
||||||
|
"matches": ["<all_urls>"],
|
||||||
|
"js": ["src/content.ts"]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
33
src/pages/Popup.css
Normal file
33
src/pages/Popup.css
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
div {
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 16px;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
img {
|
||||||
|
width: 200px;
|
||||||
|
height: 200px;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
font-size: 18px;
|
||||||
|
color: white;
|
||||||
|
font-weight: bold;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
color: white;
|
||||||
|
opacity: 0.7;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
code {
|
||||||
|
font-size: 12px;
|
||||||
|
padding: 2px 4px;
|
||||||
|
background-color: #ffffff24;
|
||||||
|
border-radius: 2px;
|
||||||
|
}
|
18
src/pages/Popup.tsx
Normal file
18
src/pages/Popup.tsx
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
import { useEffect } from 'react';
|
||||||
|
import "./Popup.css";
|
||||||
|
|
||||||
|
export default function() {
|
||||||
|
useEffect(() => {
|
||||||
|
console.log("Hello from the popup!");
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<img src="/icon-with-shadow.svg" />
|
||||||
|
<h1>vite-plugin-web-extension</h1>
|
||||||
|
<p>
|
||||||
|
Template: <code>react-ts</code>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
11
src/popup.css
Normal file
11
src/popup.css
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
html,
|
||||||
|
body {
|
||||||
|
width: 300px;
|
||||||
|
height: 400px;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
background-color: rgb(36, 36, 36);
|
||||||
|
}
|
14
src/popup.html
Normal file
14
src/popup.html
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<link rel="stylesheet" href="./popup.css" />
|
||||||
|
<title>Popup</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="app"></div>
|
||||||
|
</body>
|
||||||
|
<script type="module" src="./popup.tsx"></script>
|
||||||
|
</html>
|
9
src/popup.tsx
Normal file
9
src/popup.tsx
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
import React from "react";
|
||||||
|
import ReactDOM from "react-dom/client";
|
||||||
|
import Popup from "./pages/Popup";
|
||||||
|
|
||||||
|
ReactDOM.createRoot(document.querySelector("#app")!).render(
|
||||||
|
<React.StrictMode>
|
||||||
|
<Popup />
|
||||||
|
</React.StrictMode>
|
||||||
|
);
|
19
src/sidebar.html
Normal file
19
src/sidebar.html
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Sidebar</title>
|
||||||
|
<style>
|
||||||
|
code {
|
||||||
|
font-family: monospace;
|
||||||
|
background: #f0f0f0;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="app"></div>
|
||||||
|
<script type="module" src="./sidebar.tsx"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
352
src/sidebar.tsx
Normal file
352
src/sidebar.tsx
Normal file
|
@ -0,0 +1,352 @@
|
||||||
|
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 <Tag style={htmlStyleToReactStyle(style)}>
|
||||||
|
{children.map((child, i) => <VnodeToReact key={i} vnode={child} />)}
|
||||||
|
</Tag>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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("")}
|
||||||
|
</${Tag}>`
|
||||||
|
}
|
||||||
|
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 <div>
|
||||||
|
<h1>Vnode</h1>
|
||||||
|
<code
|
||||||
|
lang="json"
|
||||||
|
style={{
|
||||||
|
width: "100%",
|
||||||
|
height: "4rem",
|
||||||
|
overflow: "auto",
|
||||||
|
position: "relative"
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{inheritedStyleText}
|
||||||
|
</code>
|
||||||
|
<div>
|
||||||
|
<code lang="html"
|
||||||
|
style={{
|
||||||
|
width: "100%",
|
||||||
|
height: "20rem",
|
||||||
|
overflow: "auto",
|
||||||
|
position: "relative"
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{html}
|
||||||
|
</code>
|
||||||
|
{data &&
|
||||||
|
<div style={data.inheritedStyle}>
|
||||||
|
<VnodeToReact vnode={data.vnode} />
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
ReactDOM.createRoot(document.querySelector("#app")!).render(
|
||||||
|
<React.StrictMode>
|
||||||
|
<Content />
|
||||||
|
</React.StrictMode>
|
||||||
|
);
|
1
src/vite-env.d.ts
vendored
Normal file
1
src/vite-env.d.ts
vendored
Normal file
|
@ -0,0 +1 @@
|
||||||
|
/// <reference types="vite/client" />
|
21
tsconfig.json
Normal file
21
tsconfig.json
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ESNext",
|
||||||
|
"useDefineForClassFields": true,
|
||||||
|
"lib": ["DOM", "DOM.Iterable", "ESNext"],
|
||||||
|
"allowJs": false,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"esModuleInterop": false,
|
||||||
|
"allowSyntheticDefaultImports": true,
|
||||||
|
"strict": true,
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"module": "ESNext",
|
||||||
|
"moduleResolution": "Node",
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"noEmit": true,
|
||||||
|
"jsx": "react-jsx"
|
||||||
|
},
|
||||||
|
"include": ["src"],
|
||||||
|
"references": [{ "path": "./tsconfig.node.json" }]
|
||||||
|
}
|
9
tsconfig.node.json
Normal file
9
tsconfig.node.json
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"composite": true,
|
||||||
|
"module": "ESNext",
|
||||||
|
"moduleResolution": "Node",
|
||||||
|
"allowSyntheticDefaultImports": true
|
||||||
|
},
|
||||||
|
"include": ["vite.config.ts"]
|
||||||
|
}
|
24
vite.config.ts
Normal file
24
vite.config.ts
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
import { defineConfig } from "vite";
|
||||||
|
import react from "@vitejs/plugin-react";
|
||||||
|
import webExtension, { readJsonFile } from "vite-plugin-web-extension";
|
||||||
|
|
||||||
|
function generateManifest() {
|
||||||
|
const manifest = readJsonFile("src/manifest.json");
|
||||||
|
const pkg = readJsonFile("package.json");
|
||||||
|
return {
|
||||||
|
name: pkg.name,
|
||||||
|
description: pkg.description,
|
||||||
|
version: pkg.version,
|
||||||
|
...manifest,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// https://vitejs.dev/config/
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [
|
||||||
|
react(),
|
||||||
|
webExtension({
|
||||||
|
manifest: generateManifest,
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
});
|
Loading…
Add table
Reference in a new issue