=> {
19 | return new Promise((resolve, _reject) => {
20 | chrome.runtime.sendMessage(
21 | {
22 | action: 'getResourceContent',
23 | url,
24 | },
25 | (response) => {
26 | // console.log('response',response);
27 | resolve(response.content)
28 | }
29 | )
30 | })
31 | }
32 |
33 | export function getFileContent(url: string) {
34 | if (debugMode) {
35 | return getByFetch(url)
36 | }
37 | return getByChromeAPI(url)
38 | }
39 |
--------------------------------------------------------------------------------
/test/css/font.css:
--------------------------------------------------------------------------------
1 | i {
2 | font-family: 'Cutive Mono';
3 | }
4 |
5 | @media only screen and (max-width:480px) {
6 | @import url("https://fonts.googleapis.com/css?family=Cutive+Mono");
7 |
8 | b {
9 | font-size: 20px;
10 | }
11 | }
12 |
13 | body {
14 | font-family: 'Cutive Mono';
15 | }
16 |
17 | @font-face {
18 | font-family: 'icon-font';
19 | src: url("http://img.t.sinajs.cn/t6/style/images/common/font/swbficon.eot?id=141111232016");
20 | src: url("http://img.t.sinajs.cn/t6/style/images/common/font/swbficon.eot?id=141111232016#iefix") format('embedded-opentype'),
21 | url("http://img.t.sinajs.cn/t6/style/images/common/font/swbficon.svg?id=141111232016") format('svg'),
22 | url("http://img.t.sinajs.cn/t6/style/images/common/font/swbficon.woff?id=141111232016") format('woff'),
23 | url("http://img.t.sinajs.cn/t6/style/images/common/font/swbficon.ttf?id=141111232016") format('truetype');
24 | src: url("http://img.t.sinajs.cn/t6/style/images/common/font/swbficon.eot?id=141111232016") \9;
25 | font-weight: normal;
26 | font-style: normal;
27 | }
28 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2019 Bobscript
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/src/util/cleanHTML.ts:
--------------------------------------------------------------------------------
1 | import convUrlToAbs from './convUrlToAbs'
2 |
3 | function makeTagReg(tagName:string){
4 | return new RegExp('<'+tagName+'[\\s\\S]*?>[\\s\\S]*?<\/'+tagName+'>','gi')
5 | }
6 |
7 | export default function (dirty: string, doc: Document): string {
8 | return dirty
9 | .replace(makeTagReg('script'), '')
10 | .replace(makeTagReg('style'), '')
11 | .replace(//gi, '')
12 | .replace(/ on\w+=".*?"/gi, "")
13 | .replace(/(
]+src=(['"]))(.*?)(\2.*?>)/g, function () {
14 | var src = convUrlToAbs(doc.location.href, arguments[3])
15 | return arguments[1] + src + arguments[4]
16 | })
17 | .replace(/(
]+srcset=(['"]))(.*?)(\2.*?>)/g, function () {
18 | var srcset = arguments[3].split(/,\s*/)
19 | srcset.forEach(function (ele, index) {
20 | var src = ele.replace(/([^ ]*)(.*)/, function () {
21 | var _src = convUrlToAbs(doc.location.href, arguments[1])
22 | return _src + ' ' + arguments[2]
23 | })
24 | srcset[index] = src
25 | })
26 | return arguments[1] + srcset.join(',') + arguments[4]
27 | })
28 | }
29 |
--------------------------------------------------------------------------------
/src/ui/app.css:
--------------------------------------------------------------------------------
1 | :root {
2 | font-family: Inter, Avenir, Helvetica, Arial, sans-serif;
3 | font-size: 16px;
4 | line-height: 24px;
5 | font-weight: 400;
6 |
7 | font-synthesis: none;
8 | text-rendering: optimizeLegibility;
9 | -webkit-font-smoothing: antialiased;
10 | -moz-osx-font-smoothing: grayscale;
11 | -webkit-text-size-adjust: 100%;
12 | }
13 |
14 | a {
15 | font-weight: 500;
16 | color: #87d21d;
17 | text-decoration: inherit;
18 | }
19 | a:hover {
20 | color: #74b419;
21 | }
22 |
23 | body {
24 | font-family: inherit;
25 | font-size: inherit;
26 | margin: 0;
27 | min-height: 100vh;
28 | }
29 |
30 | h1 {
31 | font-size: 3.2em;
32 | line-height: 1.1;
33 | }
34 |
35 | .card {
36 | padding: 2em;
37 | }
38 |
39 | button {
40 | border-radius: 8px;
41 | border: 1px solid transparent;
42 | padding: 0.6em 1.2em;
43 | font-size: 1em;
44 | font-weight: 500;
45 | font-family: inherit;
46 | background-color: #1a1a1a;
47 | /* cursor: pointer; */
48 | transition: border-color 0.25s;
49 | }
50 | button:hover {
51 | border-color: #87d21d;
52 | }
53 | button:focus,
54 | button:focus-visible {
55 | outline: 4px auto -webkit-focus-ring-color;
56 | }
57 |
58 | button:disabled:hover{
59 | cursor: not-allowed;
60 | }
61 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "css-used-chrome-ext",
3 | "version": "3.0.0",
4 | "type": "module",
5 | "description": "A chrome extension to get all css rules used by the selected DOM and its descendants.",
6 | "scripts": {
7 | "dev:content": "npm run build:ui && vite build --watch --config vite.config.content.js",
8 | "dev:ui": "vite --config vite.config.ui.js",
9 | "build:content": "tsc && vite build --minify --config vite.config.content.js",
10 | "build:ui": "vite build --minify --config vite.config.ui.js",
11 | "build": "npm run build:ui && npm run build:content"
12 | },
13 | "repository": {
14 | "type": "git",
15 | "url": "git+https://github.com/painty/CSS-Used-ChromeExt.git"
16 | },
17 | "keywords": [
18 | "chrome-extension",
19 | "chrome-devtools",
20 | "css-usage",
21 | "css-rules"
22 | ],
23 | "author": "Bobscript",
24 | "license": "MIT",
25 | "bugs": {
26 | "url": "https://github.com/painty/CSS-Used-ChromeExt/issues"
27 | },
28 | "homepage": "https://github.com/painty/CSS-Used-ChromeExt#readme",
29 | "dependencies": {
30 | "postcss": "^8.4.21",
31 | "postcss-safe-parser": "^6.0.0"
32 | },
33 | "devDependencies": {
34 | "@sveltejs/vite-plugin-svelte": "^2.0.0",
35 | "@tsconfig/svelte": "^3.0.0",
36 | "@types/chrome": "^0.0.206",
37 | "@types/postcss-safe-parser": "^5.0.1",
38 | "svelte": "^3.54.0",
39 | "typescript": "^4.9.4",
40 | "vite": "^4.0.4",
41 | "vite-plugin-environment": "^1.1.3"
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/src/util/cssHelper.ts:
--------------------------------------------------------------------------------
1 | export type cssObj = {
2 | normRule: string[]
3 | keyFram: string[]
4 | fontFace: string[]
5 | }
6 |
7 | import convTextToRules from './convTextToRules'
8 |
9 | const cssHelper = {
10 | mergeobjCss: function (a: cssObj, b: cssObj) {
11 | ['normRule', 'fontFace', 'keyFram'].forEach(function (ele) {
12 | if (!a[ele] || !b[ele]) {
13 | // console.log('NO '+ele);
14 | }
15 | a[ele] = a[ele].concat(b[ele]).filter(e=>e)
16 | })
17 | },
18 | normRuleNodeToText: function (node) {
19 | var s = ''
20 | node.nodes.forEach(function (ele) {
21 | if (ele.prop && ele.value) {
22 | var before = ele.raws.before.replace(/[\s]*/, '')
23 | s +=
24 | before +
25 | ele.prop +
26 | ':' +
27 | ele.value +
28 | (ele.important ? '!important;' : ';')
29 | }
30 | })
31 | return s
32 | },
33 | keyFramNodeToText: function (node) {
34 | var s = '@' + node.name + ' ' + node.params + '{'
35 | node.nodes.forEach(function (_node) {
36 | s += _node.selector + '{' + cssHelper.normRuleNodeToText(_node) + '}'
37 | })
38 | s += '}'
39 | return s
40 | },
41 | fontFaceNodeToText: function (node) {
42 | var s = '@' + node.name + '{'
43 | s += cssHelper.normRuleNodeToText(node)
44 | s += '}'
45 | return s
46 | },
47 | textToCss: async function (styleContent: string) {
48 | const parsedCss = await convTextToRules(styleContent)
49 | return parsedCss
50 | },
51 | }
52 |
53 | export { cssHelper }
54 |
--------------------------------------------------------------------------------
/src/util/postTideCss.ts:
--------------------------------------------------------------------------------
1 | function clean(s) {
2 | s = s.map(function (ele) {
3 | return ele.replace(/[\n\r]/g, ' ');
4 | });
5 | s = s.join('\n');
6 | // empty rules
7 | var reg1 = /^[^{}\n]*{\s*}/gm;
8 | // multiple empty lines
9 | var reg2 = /\n\n/g;
10 | // no import rule used
11 | var reg3 = /\/\*! @import.*\n\/\*! end @import \*\//g;
12 |
13 | while (s.match(reg1) !== null || s.match(reg2) !== null || s.match(reg3) !== null) {
14 | s = s.replace(reg1, '');
15 | s = s.replace(reg2, '\n');
16 | s = s.replace(reg3, '');
17 | }
18 | //trim heading white space
19 | s = s.replace(/^\s*/mg, '');
20 | return (s);
21 | }
22 |
23 | function fix(s) {
24 |
25 | s = clean(s);
26 |
27 | s = s.split(/\n+/);
28 |
29 | // remove the last comments line
30 | // which have no rules
31 | // endOfRuleLine:the end of the lastRule line number
32 | var endOfRuleLine = s.length;
33 | var fontFacePosition = s.indexOf('/*! CSS Used fontfaces */');
34 | var keyFramsPosition = s.indexOf('/*! CSS Used keyframes */');
35 | if (keyFramsPosition !== -1) {
36 | endOfRuleLine = keyFramsPosition;
37 | } else if (fontFacePosition !== -1) {
38 | endOfRuleLine = fontFacePosition;
39 | }
40 | while (s.length > 0 && s[endOfRuleLine - 1].match(/^\/\*! |^$/) !== null) {
41 | s.splice(endOfRuleLine - 1, 1);
42 | endOfRuleLine--;
43 | }
44 | var arr = [],
45 | regFrom = /^\/\*! CSS Used from: /;
46 | for (var i = 0; i < endOfRuleLine; i++) {
47 | if ((s[i].match(regFrom) !== null) && (i + 1 === endOfRuleLine || (s[i + 1].match(regFrom) !== null))) {
48 | continue;
49 | } else {
50 | arr.push(s[i]);
51 | }
52 | }
53 | // concat the latter fontface and keyframs part
54 | arr = arr.concat(s.slice(endOfRuleLine));
55 |
56 | return arr.join('\n').replace(/(['"']?)\u5fae\u8f6f\u96c5\u9ed1\1/, '"Microsoft Yahei"'); //.replace(/(['"']?)宋体\1/,' simsun ');
57 | }
58 |
59 | export default fix
--------------------------------------------------------------------------------
/src/ui/mockChromeAPI.ts:
--------------------------------------------------------------------------------
1 | import debugMode from '../const/debugMode'
2 |
3 | let chromeObj
4 |
5 | if (debugMode) {
6 | chromeObj = {
7 | devtools: {
8 | panels: {
9 | // themeName: 'default',
10 | themeName: 'dark',
11 | },
12 | inspectedWindow: {
13 | tabId: 123,
14 | },
15 | },
16 | extension:{
17 | isAllowedFileSchemeAccess:(fn)=>{
18 | fn.call(null,true)
19 | }
20 | },
21 | tabs:{
22 | query:(_obj,fn)=>{
23 | const arr=[{
24 | id: 123,
25 | url: 'file:///Volumes/index.html'
26 | // url: 'http://localhost/index.html'
27 | }]
28 | fn.call(null,arr)
29 | }
30 | },
31 | runtime: {
32 | _messageHandlerStack: [],
33 | onMessage: {
34 | addListener: (fn) => {
35 | // message, sender, sendResponse
36 | chromeObj.runtime._messageHandlerStack.push({
37 | target: this,
38 | handler: fn,
39 | })
40 | },
41 | },
42 | sendMessage: (message, responseFn) => {
43 | // self send and slef response
44 | chromeObj.runtime._messageHandlerStack.forEach((h) => {
45 | let sender: { tab?: { id: number } } = {}
46 | // if (message._from === 'devtools') {}
47 | if (message._from === 'content') {
48 | sender.tab = {
49 | id: 123,
50 | }
51 | }
52 | let handler = h.handler
53 | let sendResponse = (r) => {
54 | responseFn.call(null, r)
55 | }
56 | let isAsyncResponse = handler.call(
57 | chromeObj,
58 | message,
59 | sender,
60 | sendResponse
61 | )
62 | if (isAsyncResponse === true) {
63 | // async response
64 | } else {
65 | }
66 | })
67 | },
68 | },
69 | }
70 | } else {
71 | chromeObj = chrome
72 | }
73 | export default chromeObj
74 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # CSS-Used
2 |
3 | 
4 |
5 | Get all css rules used by the selected DOM and its descendants.
6 |
7 | Get it at the Chrome Web Store : [CSS Used >>](https://chrome.google.com/webstore/detail/css-used/cdopjfddjlonogibjahpnmjpoangjfff)
8 |
9 | ## Overview
10 |
11 | Get all css rules used by the selected DOM and its descendants. Used to extrac only the used CSS. So the unused css will be left out.
12 |
13 | If the selected DOM is the body, the result will be all the used css by the whole page curently.
14 |
15 | ## Usage
16 |
17 | 
18 |
19 | F12 open the Developer Tools, select the dom and active the "CSS Used" panel. The used CSS rules of the Selected dom and its descendants will be listed.
20 |
21 | You can click "Preview" to see the selected part with clean style rules.
22 |
23 | ## FAQ
24 |
25 | 1. No result
26 |
27 | 1. Check whether it's Chrome Web Store pages, which is `https://chrome.google.com/webstore/....`, which won't allow content script injection.
28 | 1. If it's a normal webpage, check site access permission https://github.com/painty/CSS-Used-ChromeExt/issues/13#issuecomment-687244215
29 | 1. If it's a local file, chrome won't allow local file access by default. You can turn on the "Allow access to file URLs" in the extension management page.
30 |
31 | 1. Preview not right
32 |
33 | As for the CSS rule like `.wrap p{...}`, if only `` is selected, the result `.wrap p{...}` may not apply directly to `
`.
34 | Either changing this rule's selector to `p{...}` or giving the `
` a `.wrap` parent in the final HTML.
35 |
36 | 1. Not all the CSS is got
37 |
38 | 1. The result is generated based on the CURRENT HTML DOM. If a div doesn't exist in the document unless a specific user interaction, the result may miss out the style rules for the newly born div.
39 | 1. CSS custom properties (variables) are partially supported. Not working for declarations defined by $0's ancestor. Thinking it as a inheritable CSS property, as this tool won't handle inherit style.
40 |
41 | ## Changelog
42 |
43 | Go to the [Changelog page](CHANGELOG.md)
44 |
--------------------------------------------------------------------------------
/src/util/convLinkToText.ts:
--------------------------------------------------------------------------------
1 | import convUrlToAbs from './convUrlToAbs'
2 | import { getFileContent } from './getFileContent'
3 |
4 | function getSavedSettings() {
5 | return new Promise((resolve) => {
6 | if (chrome && chrome.storage) {
7 | chrome.storage.sync.get(
8 | {
9 | convUrlToAbsolute: true,
10 | },
11 | function (items) {
12 | resolve(items.convUrlToAbsolute)
13 | }
14 | )
15 | } else {
16 | resolve(true)
17 | }
18 | })
19 | }
20 |
21 | interface customCssObj {
22 | url: string
23 | cssraw: string
24 | }
25 |
26 | function makeRequest(url: string): Promise {
27 | const result: customCssObj = { url, cssraw: '' }
28 | chrome.runtime.sendMessage({
29 | action: 'inform',
30 | info: 'Getting : ' + url,
31 | })
32 | return new Promise(function (resolve) {
33 | getFileContent(url)
34 | .then((data) => {
35 | result.cssraw = data
36 | // console.log("Success:", url, data);
37 | getSavedSettings().then((willConvUrlToAbs) => {
38 | if (willConvUrlToAbs) {
39 | result.cssraw = result.cssraw.replace(
40 | /url\((['"]?)(.*?)\1\)/g,
41 | function (_a: string, p1: string, p2: string) {
42 | return `url(${p1}${convUrlToAbs(url, p2)}${p1})`
43 | }
44 | )
45 | }
46 | resolve(result)
47 | chrome.runtime.sendMessage({
48 | action: 'inform',
49 | info: 'Parsing : ' + url,
50 | })
51 | })
52 | })
53 | .catch((error) => {
54 | console.log('CSS-Used: Fail to get: ' + url, error)
55 | result.cssraw = ''
56 | resolve(result)
57 | })
58 | })
59 | }
60 |
61 | function convLinkToText(links: string[]): Promise {
62 | var promises = []
63 | return new Promise(function (resolve, reject) {
64 | if (links.length === 0) {
65 | resolve([])
66 | } else {
67 | for (var i = 0; i < links.length; i++) {
68 | promises.push(makeRequest(links[i]))
69 | }
70 | Promise.all(promises)
71 | .then((result: customCssObj[]) => {
72 | resolve(result)
73 | })
74 | .catch(function (err) {
75 | reject(err)
76 | })
77 | }
78 | })
79 | }
80 |
81 | export default convLinkToText
82 |
--------------------------------------------------------------------------------
/test/css/index.css:
--------------------------------------------------------------------------------
1 | @import "base.css" screen, projection;
2 |
3 | @import url('base2.css') screen and (orientation:portrait);
4 | @import 'skin.css';
5 |
6 | :AFTER {
7 | content: 'after';
8 | background: #38c;
9 | color: #fff;
10 | font-size: 12px;
11 | opacity: 0;
12 | pointer-events: none;
13 | }
14 |
15 | html {
16 | height: 100%;
17 | }
18 |
19 | A {
20 | background: url(../img/logo.jpg);
21 | background-size: 100%;
22 | animation: teatnamekey;
23 | }
24 |
25 | BANNER {}
26 |
27 | @-webkit-keyframes teatnamekey {
28 | 0% {
29 | width: 10px;
30 | }
31 |
32 | 100% {
33 | height: 10px;
34 | }
35 | }
36 |
37 | @keyframes teatnamekey {
38 | 0% {
39 | width: 10px;
40 | }
41 |
42 | 100% {
43 | height: 10px;
44 | }
45 | }
46 |
47 | @media screen and (min-width:500px) {
48 | a {
49 | width: 16.666%;
50 | }
51 | }
52 |
53 | @media screen and (min-width:300px) {
54 | @media screen and (min-height:10px) {
55 | za {
56 | width: 16.666%;
57 | }
58 |
59 | b {
60 | height: 10px;
61 | }
62 | }
63 |
64 | za:nth-of-type(3n) {
65 | border-right: 1px solid #dcdddd;
66 | }
67 |
68 | za:nth-of-type(6n) {
69 | border-right: none;
70 | }
71 | }
72 |
73 | @import 'skin.css';
74 |
75 | /*body,.S_page{background-image:url('http://ww3.sinaimg.cn/woriginal/4e71f332gw1e3r8b7kep9j.jpg');background-repeat:repeat;background-attachment:fixed;background-position:center top;}*/
76 |
77 | body {
78 | font-family: "Cutive Mono";
79 | }
80 |
81 | .font-style {
82 | font-family: 'Cutive Mono', icon-font, Inter, Avenir, Helvetica , Arial, sans-serif ;
83 | }
84 | .svg-bg {
85 | background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24'%3E%3Cpath fill='rgba(0,0,0,.54)' d='M20.49 19l-5.73-5.73C15.53 12.2 16 10.91 16 9.5 16 5.91 13.09 3 9.5 3S3 5.91 3 9.5 5.91 16 9.5 16c1.41 0 2.7-.47 3.77-1.24L19 20.49 20.49 19zM5 9.5C5 7.01 7.01 5 9.5 5S14 7.01 14 9.5 11.99 14 9.5 14 5 11.99 5 9.5z'/%3E%3C/svg%3E");
86 | background-position: center;
87 | background-repeat: no-repeat;
88 | background-size: 20px;
89 | height: 20px;
90 | padding: 10px;
91 | width: 20px;
92 | font-size:0;
93 | }
94 | .not-focus{
95 | width: 50px;
96 | height: 50px;
97 | border: 1px solid #000;
98 | }
99 | .not-focus:not(:focus){
100 | background: rgb(10, 149, 86);
101 | }
102 | .not-focus:focus{
103 | background: rgb(158, 51, 204);
104 | }
105 | @media print {
106 | i {
107 | font-size: 10pt;
108 | }
109 | }
110 |
--------------------------------------------------------------------------------
/src/util/generateRulesAll.ts:
--------------------------------------------------------------------------------
1 | import traversalCSSRuleList from './traversalCSSRuleList'
2 | import convTextToRules from './convTextToRules'
3 | import { cssHelper } from './cssHelper'
4 | import convUrlToAbs from './convUrlToAbs'
5 |
6 | type cssNodeObj = Awaited>
7 |
8 | function generateRulesAll(
9 | doc: Document,
10 | externalCssCache: { [index: cssNodeObj['href']]: cssNodeObj }
11 | ) {
12 | var x: number
13 |
14 | var objCss = {
15 | normRule: [],
16 | fontFace: [],
17 | keyFram: [],
18 | }
19 |
20 | var promises = []
21 |
22 | return new Promise(function (resolve, reject) {
23 | // loop every styleSheets
24 | for (x = 0; x < doc.styleSheets.length; x++) {
25 | const styleSheet = doc.styleSheets[x]
26 | promises.push(
27 | new Promise(function (res) {
28 | var cssNodeArr: cssNodeObj
29 | if (styleSheet.href !== null) {
30 | // can be link tag
31 | cssNodeArr = externalCssCache[styleSheet.href]
32 | cssNodeArr.media = doc.styleSheets[x].media
33 | traversalCSSRuleList(doc, externalCssCache, cssNodeArr).then(res)
34 | } else if (styleSheet.ownerNode instanceof Element) {
35 | // style tag
36 | let html: string = styleSheet.ownerNode.innerHTML
37 | if (html === '') {
38 | // style may be in style-tag's cssRules but not show in innerHTML
39 | for (
40 | let index = 0;
41 | index < doc.styleSheets[x].cssRules.length;
42 | index++
43 | ) {
44 | const rule = doc.styleSheets[x].cssRules[index]
45 | html += rule.cssText
46 | }
47 | }
48 | // convert urls in style tag to abs
49 | html = html.replace(
50 | /url\((['"]?)(.*?)\1\)/g,
51 | function (_a, p1, p2) {
52 | return (
53 | 'url(' + p1 + convUrlToAbs(doc.location.href, p2) + p1 + ')'
54 | )
55 | }
56 | )
57 | // the next operation is asynchronous
58 | // store the current x value
59 | let _x = x
60 | convTextToRules(html, doc.location.href).then((cssNodeObj) => {
61 | cssNodeObj.media = doc.styleSheets[_x].media
62 | traversalCSSRuleList(doc, externalCssCache, cssNodeObj).then(res)
63 | })
64 | } else {
65 | // console.log('ProcessingInstruction', styleSheet.ownerNode);
66 | res({})
67 | }
68 | })
69 | )
70 | }
71 |
72 | Promise.all(promises)
73 | .then(function (result) {
74 | result.forEach(function (ele) {
75 | cssHelper.mergeobjCss(objCss, ele)
76 | })
77 | resolve(objCss)
78 | })
79 | .catch(function (err) {
80 | reject(err)
81 | })
82 | })
83 | }
84 | export default generateRulesAll
85 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | **ver 3.0.0 | 15/01/2023**
2 |
3 | 1. Migrated to Manifest V3.
4 | 1. Resources are read from local caches, making it much faster and solves the [cross-origin issue](https://github.com/painty/CSS-Used-ChromeExt/issues/52).
5 | 1. Optimized style parsing in a more efficient way.
6 | 1. Dropped support for some outdated CSS, mostly -o- and -ms- prefixed properties.
7 | 1. Implemented a new UI powered by Svelte, with some visual tweaks.
8 |
9 | **ver 2.5.0 | 15/03/2021**
10 |
11 | 1. New. Option page.
12 | 2. New. Option to preserve relative URLs. [#31](https://github.com/painty/CSS-Used-ChromeExt/issues/31)
13 |
14 | **ver 2.4.3 | 04/03/2021**
15 |
16 | 1. Fix. Send to CodePen may not work. [#38](https://github.com/painty/CSS-Used-ChromeExt/issues/38)
17 |
18 | **ver 2.3.2 | 18/11/2018**
19 |
20 | 1. More friendly prompt text for first use or error display.
21 | 2. The add tabs permission is used to identify special urls. Such as chrome://newtab
22 |
23 | **ver 2.2.12 | 11/11/2018**
24 |
25 | 1. No new feature or bugfix. Just split the content.js into some modules.
26 |
27 | **ver 2.2.11 | 10/06/2018**
28 |
29 | 1. Fix. preview button doesn't work [#15](https://github.com/painty/CSS-Used-ChromeExt/issues/15) [#16](https://github.com/painty/CSS-Used-ChromeExt/pull/16)
30 | 2. specify protocol for img in Preview
31 |
32 | **ver 2.2.10 | 07/11/2018**
33 |
34 | 1. Better regex for cleaning empty rules.
35 | 2. Add. scrollbar style
36 |
37 | **ver 2.2.8 | 07/08/2018**
38 |
39 | 1. Fix. @import inside media query should be invalid.
40 |
41 | **ver 2.2.7 | 06/27/2018**
42 |
43 | 1. Add. Media property of css link/tag preserved.
44 | 2. Update dependencies
45 |
46 | "postcss": "6.0.23",
47 | "postcss-safe-parser": "3.0.1"
48 |
49 | **ver 2.2.6 | 08/30/2017**
50 |
51 | 1.Fix. css link with empty href
52 |
53 | **ver 2.2.5 | 08/25/2017**
54 |
55 | 1.Change Send-to-codepen's url from "http://" to "https://"
56 |
57 | **ver 2.2.4 | 08/23/2017**
58 |
59 | 1.Add. User-friendly way of requesting file access
60 |
61 | **ver 2.2.3 | 04/30/2017**
62 |
63 | 1.fix bug that the first comment is lost
64 | 2.won't import the invalid @import css
65 |
66 | **ver 2.2.2 | 04/29/2017**
67 |
68 | 1.Endless loop @import handling. eg: a.css import b.css and b.css import a.css
69 |
70 | **ver 2.2.1 | 04/27/2017**
71 |
72 | 1.Better tips for first time use
73 | 2.changed CSS parsing from Chrome to postcss
74 |
75 | **ver 2.1.1 | 04/25/2017**
76 |
77 | 1. Better external CSS Cache
78 |
79 | **ver 2.1.0 | 04/24/2017**
80 |
81 | 1. Now works with iframes
82 |
83 | **ver 2.0.0 | 04/24/2017**
84 |
85 | 1. A Break Though in speed improvement.
86 |
87 | **ver 1.5.0 | 04/23/2017**
88 |
89 | 1. Rewrite. to async way. Now CSS-Used could have a quicker UI response. Still be CAUTIOUS when selecting too many elements which may freeze the current tab.
90 | 2. Add. Two buttons: `Copy to clipboard` & `Send to codepen`
91 | 3. Better handling for pseudo element/class
92 |
93 | **ver 1.4.2 | 04/15/2017**
94 |
95 | 1. Add. calculating progress display.
96 |
97 | **ver 1.4.1 | 04/09/2017**
98 |
99 | 1. multiple pseudo class/element detection
100 |
101 | **ver 1.4.0 | 04/09/2017**
102 |
103 | 1. Add. media query support
104 | 2. Add. font-face support
105 | 3. Fix. unused stylesheet will not show in the result.
106 |
107 | **ver 1.3**
108 |
109 | 1. Convert all background image url to absolute path.
110 |
111 | **ver 1.2**
112 |
113 | 1. The link tag is preserved now.
114 |
115 | **ver 1.1.5**
116 |
117 | 1. Keyframe animation extract is now supported.
118 |
--------------------------------------------------------------------------------
/src/content.ts:
--------------------------------------------------------------------------------
1 | import filterRules from './util/filterRules'
2 | import convLinkToText from './util/convLinkToText'
3 | import convTextToRules from './util/convTextToRules'
4 | import postTideCss from './util/postTideCss'
5 | import generateRulesAll from './util/generateRulesAll'
6 | import cleanHTML from './util/cleanHTML'
7 |
8 | type cssNodeObj = Awaited>
9 |
10 | const externalCssCache: { [index: string]: cssNodeObj } = {}
11 | //to store timers of testing if a html element matches a rule selector.
12 | const arrTimerOfTestingIfMatched: ReturnType[] = []
13 | let doc = document
14 | async function getC($0: HTMLElement) {
15 | arrTimerOfTestingIfMatched.forEach(function (ele) {
16 | clearTimeout(ele)
17 | })
18 | // reset to empty
19 | arrTimerOfTestingIfMatched.length = 0
20 |
21 | if (
22 | $0 === null ||
23 | typeof $0 === 'undefined' ||
24 | typeof $0.nodeName === 'undefined'
25 | ) {
26 | return
27 | }
28 |
29 | if ($0.nodeName.match(/^ {
73 | // if href==='' , ele.getAttribute('href') !== ele.href
74 | const current = externalCssCache[ele.href]
75 | if (
76 | ele.getAttribute('href') &&
77 | (current === undefined || current.nodes.length === 0)
78 | ) {
79 | links.push(ele.href)
80 | }
81 | })
82 |
83 | convLinkToText(links)
84 | .then(async (result) => {
85 | var promises: cssNodeObj[] = []
86 | for (var i = 0; i < result.length; i++) {
87 | let ele = result[i],
88 | idx = i
89 | const rulesObj = await convTextToRules(ele.cssraw, links[idx])
90 | promises.push(rulesObj)
91 | }
92 | return promises
93 | })
94 | .catch(function (err) {
95 | console.error('CSS-Used: ', err)
96 | chrome.runtime.sendMessage({
97 | action: 'inform',
98 | info: 'convLinkToText error, see detail in console',
99 | })
100 | })
101 | .then(function (result) {
102 | if (Array.isArray(result)) {
103 | result.forEach(function (rulesObj) {
104 | externalCssCache[rulesObj.href] = rulesObj
105 | })
106 | }
107 | })
108 | .then(function () {
109 | return generateRulesAll(doc, externalCssCache)
110 | })
111 | .then(function (objCss) {
112 | // {fontFace : Array, keyFram : Array, normRule : Array}
113 | return filterRules($0, objCss, arrTimerOfTestingIfMatched)
114 | })
115 | .then(function (data) {
116 | chrome.runtime.sendMessage({
117 | action: 'celebrate',
118 | css: postTideCss(data),
119 | html: cleanHTML($0.outerHTML, doc),
120 | })
121 | })
122 | }
123 |
124 | chrome.runtime
125 | .sendMessage({
126 | action: 'evalGetCssUsed',
127 | info: 'page loaded',
128 | })
129 | .catch(() => {
130 | // console.log('error',error);
131 | })
132 |
133 | export default getC
134 |
--------------------------------------------------------------------------------
/public/static/js/devtools.js:
--------------------------------------------------------------------------------
1 | let panelVisible = false
2 | let isPageLoaded = true
3 |
4 | function getAllFramesUrl() {
5 | const framesURLArray = []
6 | return new Promise((resolve) => {
7 | chrome.devtools.inspectedWindow.getResources((resources) => {
8 | for (var i = 0; i < resources.length; i++) {
9 | if (
10 | resources[i].type === 'document' &&
11 | resources[i].url.match(/^(https?:|file:\/)\/\//) !== null
12 | ) {
13 | framesURLArray.push(resources[i].url)
14 | }
15 | }
16 | resolve(framesURLArray)
17 | })
18 | })
19 | }
20 |
21 | function evalGetCssUsed(cancel = false) {
22 | if ((!cancel && !panelVisible) || !isPageLoaded) {
23 | return
24 | }
25 | getAllFramesUrl().then((arrFrameURL) => {
26 | // console.log('arrFrameURL', arrFrameURL)
27 | if (arrFrameURL.length === 0) {
28 | chrome.runtime.sendMessage({
29 | action: 'inform',
30 | info: 'frameURLsEmpty',
31 | tabId: chrome.devtools.inspectedWindow.tabId, // to specify message from
32 | })
33 | }
34 | arrFrameURL.forEach(function (ele) {
35 | chrome.devtools.inspectedWindow.eval(
36 | 'getCssUsed(' + (cancel ? '' : '$0') + ')',
37 | {
38 | frameURL: ele,
39 | useContentScriptContext: true,
40 | },
41 | function (result, isException) {
42 | if (isException) {
43 | console.log("evalGetCssUsed isException: ",isException);
44 | } else {
45 | // console.log('evalGetCssUsed result: ',result);
46 | }
47 | }
48 | )
49 | })
50 | })
51 | }
52 |
53 | chrome.devtools.panels.elements.onSelectionChanged.addListener(function () {
54 | evalGetCssUsed()
55 | // console.log('onSelectionChanged')
56 | chrome.runtime.sendMessage({
57 | action: 'inform',
58 | info: 'onSelectionChanged',
59 | tabId: chrome.devtools.inspectedWindow.tabId,
60 | }).catch(console.log)
61 | })
62 |
63 | chrome.devtools.network.onNavigated.addListener(function () {
64 | // console.log('onNavigated')
65 | isPageLoaded = false
66 | chrome.runtime.sendMessage({
67 | action: 'inform',
68 | info: 'onNavigated',
69 | tabId: chrome.devtools.inspectedWindow.tabId,
70 | })
71 | // initialText = `on Navigated.Select another dom on the left orReopen the Devtool`
72 | })
73 |
74 | chrome.devtools.panels.elements.createSidebarPane(
75 | 'CSS Used',
76 | function (sidebar) {
77 | // sidebar.setHeight('calc(100vh - 48px)')
78 | sidebar.setPage('panel.html')
79 | sidebar.onShown.addListener(function (win) {
80 | // console.log('onShown')
81 | panelVisible = true
82 | evalGetCssUsed()
83 | chrome.runtime.sendMessage({
84 | action: 'inform',
85 | info: 'onShown',
86 | tabId: chrome.devtools.inspectedWindow.tabId,
87 | })
88 | })
89 | sidebar.onHidden.addListener(function () {
90 | // console.log('onHidden')
91 | evalGetCssUsed(true)
92 | panelVisible = false
93 | chrome.runtime.sendMessage({
94 | action: 'inform',
95 | info: 'onHidden',
96 | tabId: chrome.devtools.inspectedWindow.tabId,
97 | })
98 | })
99 | }
100 | )
101 |
102 | // passing resources to content script
103 | chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
104 | // console.log('sender,message', sender, message)
105 | // Messages from content scripts should have sender.tab set
106 | if (sender.tab && sender.tab.id === chrome.devtools.inspectedWindow.tabId) {
107 | if (message.action == 'getResourceContent') {
108 | chrome.devtools.inspectedWindow.getResources((resources) => {
109 | // console.log('resources', resources);
110 | const resourceMatched = resources.find((r) => r.url === message.url)
111 | resourceMatched.getContent((content, encoding) => {
112 | // https://developer.chrome.com/docs/extensions/reference/devtools_inspectedWindow/#method-getResources
113 | // encoding:Currently, only base64 is supported.
114 | // console.log(resourceMatched, encoding, content.length);
115 | sendResponse({
116 | url: message.url,
117 | content,
118 | })
119 | })
120 | })
121 | // https://stackoverflow.com/questions/44056271/chrome-runtime-onmessage-response-with-async-await
122 | return true
123 | } else if (message.action == 'evalGetCssUsed') {
124 | isPageLoaded = true
125 | evalGetCssUsed()
126 | }
127 | } else {
128 | // Messages from panel scripts
129 | }
130 | })
131 |
--------------------------------------------------------------------------------
/src/ui/Options.svelte:
--------------------------------------------------------------------------------
1 |
86 |
87 |
88 |
89 |
94 |
95 |
96 | CSS Used Options
97 |
98 |
99 |
118 |
119 |
120 |
121 |
122 |
123 |
124 |
125 |
126 | Source code on GitHub
131 |
132 |
133 |
134 |
167 |
--------------------------------------------------------------------------------
/test/css/skin.css:
--------------------------------------------------------------------------------
1 | .S_spetxt,
2 | a.S_ficon:hover,
3 | a:hover .S_ficon,
4 | a.current .S_ficon {
5 | color: #fa7d3c;
6 | }
7 |
8 | .S_error {
9 | color: #f00;
10 | }
11 |
12 | .S_spetxt_bg {
13 | background-color: #fa7d3c;
14 | }
15 |
16 | .W_btn_prev:hover,
17 | .W_btn_next:hover {
18 | border-color: #fa7d3c;
19 | }
20 |
21 | .W_btn_prev:hover,
22 | .W_btn_next:hover,
23 | a.S_ficon:hover,
24 | a:hover .S_ficon,
25 | a.current .S_ficon,
26 | a.S_txt1:hover,
27 | a.current .S_txt1,
28 | a.S_txt2:hover,
29 | .SW_fun:hover .S_func1 {
30 | text-decoration: none;
31 | cursor: pointer;
32 | }
33 |
34 | .S_ficon_dis,
35 | a.S_ficon_dis:hover,
36 | a:hover .S_ficon_dis {
37 | cursor: default;
38 | opacity: 0.5;
39 | filter: alpha(opacity=50);
40 | }
41 |
42 | .WB_timeline .current em {
43 | color: #fff;
44 | }
45 |
46 | .send_weibo {
47 | background-image: none;
48 | }
49 |
50 | .WB_miniblog {
51 | padding-top: 50px;
52 | }
53 |
54 | .S_page .WB_miniblog {
55 | padding-top: 50px;
56 | }
57 |
58 | .S_page .WB_frame {
59 | background-color: transparent !important;
60 | }
61 |
62 | #js_skin_public_base {
63 | height: 42px;
64 | }
65 |
66 | body,
67 | .S_page {
68 | background-color: #b4d66b;
69 | background-image: none;
70 | }
71 |
72 | .send_weibo .ficon_swtxt {
73 | color: #78a11f;
74 | }
75 |
76 | body,
77 | legend,
78 | .W_input:focus,
79 | .S_txt1,
80 | .W_btn_b,
81 | .SW_fun .S_func1 {
82 | color: #333;
83 | text-decoration: none;
84 | }
85 |
86 | .S_txt1_bg {
87 | background-color: #333;
88 | }
89 |
90 | .S_txt1_br {
91 | border-color: #333;
92 | }
93 |
94 | .S_txt2,
95 | .W_input,
96 | .W_btn_b_disable,
97 | .W_btn_b_disable:hover {
98 | color: #808080;
99 | text-decoration: none;
100 | }
101 |
102 | .S_txt2_bg {
103 | background-color: #808080;
104 | }
105 |
106 | .S_txt2_br {
107 | border-color: #808080;
108 | }
109 |
110 | .S_ficon,
111 | .S_ficon_dis,
112 | a.S_ficon_dis:hover,
113 | a:hover .S_ficon_dis {
114 | color: #696e78;
115 | }
116 |
117 | .S_ficon_bg {
118 | background-color: #696e78;
119 | }
120 |
121 | .S_ficon_br {
122 | border-color: #696e78;
123 | }
124 |
125 | a,
126 | .S_link1,
127 | a.S_txt1:hover,
128 | a.current .S_txt1,
129 | a.S_txt2:hover,
130 | .SW_fun:hover .S_func1 {
131 | color: #76a513;
132 | }
133 |
134 | .S_link1_bg {
135 | background-color: #76a513;
136 | }
137 |
138 | .S_link1_br {
139 | border-color: #76a513;
140 | }
141 |
142 | .S_bg1,
143 | .SW_fun_bg:hover,
144 | .SW_fun_bg_active {
145 | background-color: #f2f2f5;
146 | }
147 |
148 | .S_bg1_c {
149 | color: #f2f2f5;
150 | }
151 |
152 | .S_bg1_br {
153 | border-color: #f2f2f5;
154 | }
155 |
156 | .W_btn_cardlink,
157 | .W_btn_tag_cur,
158 | .W_btn_tag_cur:hover {
159 | background-color: #f2f2f5 !important;
160 | }
161 |
162 | .S_bg2,
163 | blockquote,
164 | .W_btn_b,
165 | .W_input,
166 | .SW_fun_bg {
167 | background-color: #fff;
168 | }
169 |
170 | .S_bg2_c {
171 | color: #fff;
172 | }
173 |
174 | .S_bg2_br {
175 | color: #fff;
176 | border-color: #fff;
177 | }
178 |
179 | .S_bg3 {
180 | background-color: #eaeaec;
181 | }
182 |
183 | .S_line1,
184 | .W_btn_prev,
185 | .W_btn_next,
186 | .W_btn_b {
187 | border-color: #d9d9d9;
188 | }
189 |
190 | .W_btn_b_disable,
191 | .W_btn_b_disable:hover,
192 | .W_btn_tag_cur,
193 | .W_btn_tag_cur:hover {
194 | border-color: #d9d9d9 !important;
195 | }
196 |
197 | .S_line1_c {
198 | color: #d9d9d9;
199 | }
200 |
201 | .W_btn_b_disable .S_ficon {
202 | color: #d9d9d9 !important;
203 | }
204 |
205 | .S_line2 {
206 | border-color: #f2f2f5;
207 | }
208 |
209 | .S_line2_c {
210 | color: #f2f2f5;
211 | }
212 |
213 | .S_line3,
214 | .W_input,
215 | .send_weibo .input,
216 | .W_btn_b:hover {
217 | border-color: #ccc;
218 | }
219 |
220 | .W_input,
221 | .send_weibo .input {
222 | background-color: #fff;
223 | }
224 |
225 | .WB_left_nav .S_txt1,
226 | .WB_left_nav a.S_txt1:hover,
227 | .WB_left_nav .S_ficon,
228 | .WB_left_nav a:hover .S_ficon,
229 | .WB_left_nav a.S_ficon:hover {
230 | color: #fff;
231 | }
232 |
233 | .WB_left_nav .lev a:hover,
234 | .WB_left_nav .lev_curr,
235 | .WB_left_nav .lev_curr:hover,
236 | .WB_left_nav .levmore .more {
237 | background-color: #b0c97b;
238 | background-color: rgba(255, 255, 255, 0.2);
239 | *background-color: #b0c97b;
240 | }
241 |
242 | .WB_left_nav .lev_Box,
243 | .WB_left_nav fieldset {
244 | border-color: #b0c97b;
245 | border-color: rgba(255, 255, 255, 0.1);
246 | *border-color: #b0c97b;
247 | }
248 |
249 | .WB_frame {
250 | background-color: #9ebd5b;
251 | background-color: rgba(94, 118, 47, 0.25);
252 | *background-color: #9ebd5b;
253 | }
254 |
255 | .WB_timeline .S {
256 | color: #87a840;
257 | border-left-color: #87a840;
258 | }
259 |
260 | .WB_timeline .current em,
261 | .WB_timeline .S_dot,
262 | .W_gotop {
263 | background-color: #87a840;
264 | }
265 |
266 | .WB_miniblog {
267 | background: none;
268 | }
269 |
270 | .S_page .WB_miniblog {
271 | background: none;
272 | }
--------------------------------------------------------------------------------
/test/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CSS Used Test
6 |
7 |
8 |
14 |
26 |
27 |
28 |
29 |
32 |
33 | Bold
34 |
35 |
36 | Italic
37 |
38 | FontFamily
39 | .S_spetxt
40 | svg-bg
41 | svg-bg-inpage
42 |
43 |
44 |
45 | Compare text below with result in devtools, with <html> tag
46 | selected.
47 |
48 |
49 |
115 |
116 |
117 |
--------------------------------------------------------------------------------
/src/util/traversalCSSRuleList.ts:
--------------------------------------------------------------------------------
1 | import { cssHelper } from './cssHelper'
2 | import type { cssObj } from './cssHelper'
3 | import convUrlToAbs from './convUrlToAbs'
4 | import convLinkToText from './convLinkToText'
5 | import convTextToRules from './convTextToRules'
6 |
7 | type cssNodeObj = Awaited>
8 |
9 | function traversalCSSRuleList(
10 | doc: Document,
11 | externalCssCache: { [index: cssNodeObj['href']]: cssNodeObj },
12 | cssNodeObj: cssNodeObj
13 | ): Promise {
14 | var promises = []
15 |
16 | var objCss: cssObj = {
17 | normRule: [],
18 | keyFram: [],
19 | fontFace: [],
20 | }
21 |
22 | return new Promise(function (resolve, reject) {
23 | if (cssNodeObj === undefined || cssNodeObj.nodes.length === 0) {
24 | resolve(objCss)
25 | } else if (cssNodeObj.nodes.length > 0) {
26 | // annotion where the CSS rule from
27 | let strMediaText = ''
28 | if (cssNodeObj.media && cssNodeObj.media.length > 0) {
29 | strMediaText = `; media=${cssNodeObj.media.mediaText} `
30 | }
31 | if (cssNodeObj.href === doc.location.href) {
32 | objCss.normRule.push(`/*! CSS Used from: Embedded ${strMediaText}*/`)
33 | } else if (cssNodeObj.href && !cssNodeObj.parentHref) {
34 | objCss.normRule.push(
35 | `/*! CSS Used from: ${cssNodeObj.href} ${strMediaText}*/`
36 | )
37 | }
38 | }
39 |
40 | for (var i = 0; i < cssNodeObj.nodes.length; i++) {
41 | ;(function (CSSRuleListItem, i) {
42 | promises.push(
43 | new Promise(function (res) {
44 | var _objCss = {
45 | normRule: [],
46 | keyFram: [],
47 | fontFace: [],
48 | }
49 | if (
50 | CSSRuleListItem.type === 'atrule' &&
51 | CSSRuleListItem.name.match(/^(-(webkit|moz)-)?keyframes$/)
52 | ) {
53 | // CSSKeyframesRule
54 | _objCss.keyFram.push(CSSRuleListItem)
55 | res(_objCss)
56 | } else if (
57 | CSSRuleListItem.type === 'atrule' &&
58 | CSSRuleListItem.name === 'font-face'
59 | ) {
60 | // CSSFontFaceRule
61 | _objCss.fontFace.push(CSSRuleListItem)
62 | res(_objCss)
63 | } else if (
64 | CSSRuleListItem.type === 'atrule' &&
65 | CSSRuleListItem.name === 'media'
66 | ) {
67 | // CSSMediaRule
68 | traversalCSSRuleList(doc, externalCssCache, {
69 | nodes: CSSRuleListItem.nodes,
70 | }).then(function (obj) {
71 | _objCss.normRule.push(
72 | '\n@media ' + CSSRuleListItem.params + '{'
73 | )
74 | cssHelper.mergeobjCss(_objCss, obj)
75 | _objCss.normRule.push('}')
76 | res(_objCss)
77 | })
78 | } else if (
79 | CSSRuleListItem.type === 'atrule' &&
80 | CSSRuleListItem.name === 'import'
81 | ) {
82 | // CSSImportRule
83 | let isValidImport = true
84 | for (let j = 0; j < i; j++) {
85 | let rule = cssNodeObj.nodes[j]
86 | if (
87 | rule.type === 'rule' ||
88 | (rule.type === 'atrule' &&
89 | rule.name.match(/^charset|import$/) === null)
90 | ) {
91 | isValidImport = false
92 | break
93 | }
94 | }
95 | if (!cssNodeObj.href) {
96 | // such as import inside media query
97 | isValidImport = false
98 | }
99 | let importParamsMatch = CSSRuleListItem.params.match(
100 | /^(url\((['"]?)(.*?)\2\)|(['"])(.*?)\4)\s*(.*)$/
101 | )
102 | let href = importParamsMatch[3] || importParamsMatch[5] || ''
103 | let media = importParamsMatch[6]
104 | if (
105 | isValidImport &&
106 | (href = convUrlToAbs(cssNodeObj.href, href)) &&
107 | href !== cssNodeObj.parentHref
108 | ) {
109 | new Promise((resolve) => {
110 | if (externalCssCache[href] !== undefined) {
111 | resolve(externalCssCache[href])
112 | } else {
113 | convLinkToText([href])
114 | .then(function (result) {
115 | return convTextToRules(result[0].cssraw)
116 | })
117 | .then(function (nodeArr) {
118 | nodeArr.href = href
119 | nodeArr.parentHref = cssNodeObj.href
120 | externalCssCache[href] = nodeArr
121 | resolve(nodeArr)
122 | })
123 | }
124 | })
125 | .then(function (nodeArr: cssNodeObj) {
126 | return traversalCSSRuleList(doc, externalCssCache, nodeArr)
127 | })
128 | .then(function (obj) {
129 | if (obj.normRule.length > 0) {
130 | _objCss.normRule.push(
131 | ['/*! @import', href, media, '*/']
132 | .join(' ')
133 | .replace(/ {2,}/g, ' ')
134 | )
135 | media.length &&
136 | _objCss.normRule.push('\n@media ' + media + '{')
137 | cssHelper.mergeobjCss(_objCss, obj)
138 | media.length && _objCss.normRule.push('}')
139 | _objCss.normRule.push('/*! end @import */')
140 | } else {
141 | cssHelper.mergeobjCss(_objCss, obj)
142 | }
143 | res(_objCss)
144 | })
145 | } else {
146 | res(_objCss)
147 | }
148 | } else if (
149 | CSSRuleListItem.type === 'rule' &&
150 | CSSRuleListItem.selector !== ''
151 | ) {
152 | // the normal "CSSStyleRule"
153 | _objCss.normRule.push(CSSRuleListItem)
154 | res(_objCss)
155 | } else {
156 | res(_objCss)
157 | }
158 | })
159 | )
160 | })(cssNodeObj.nodes[i], i)
161 | }
162 |
163 | Promise.all(promises)
164 | .then(function (result) {
165 | result.forEach(function (ele) {
166 | cssHelper.mergeobjCss(objCss, ele)
167 | })
168 | if (cssNodeObj.media && cssNodeObj.media.length > 0) {
169 | objCss.normRule.splice(1, 0, `@media ${cssNodeObj.media.mediaText}{`)
170 | objCss.normRule.push('}')
171 | }
172 | resolve(objCss)
173 | })
174 | .catch(function (err) {
175 | reject(err)
176 | })
177 | })
178 | }
179 |
180 | export default traversalCSSRuleList
181 |
--------------------------------------------------------------------------------
/src/util/filterRules.ts:
--------------------------------------------------------------------------------
1 | // this module is used to filter rules
2 | // by testing the dom and its descendants one by one.
3 | // each testing is wrapped by a settimeout timmer to make it async
4 | // because the testing can be a long time if too many.
5 |
6 | import debugMode from '../const/debugMode'
7 | import { cssHelper } from './cssHelper'
8 |
9 | // may match accoding to interaction
10 | const PseudoClass =
11 | 'active|checked|disabled|empty|enabled|focus|hover|in-range|invalid|link|out-of-range|target|valid|visited|focus-within|focus-visible|fullscreen',
12 | PseudoElement =
13 | '((-(webkit|moz)-)?(scrollbar(-(button|thumb|corner|track(-piece)?))?))|-webkit-(details-marker|resizer)|after|before|first-letter|first-line|placeholder|selection',
14 | MaxPossiblePseudoLength = 30,
15 | REG0 = new RegExp(
16 | '^(:(' + PseudoClass + ')|::?(' + PseudoElement + '))+$',
17 | ''
18 | ),
19 | REG1 = new RegExp(
20 | '( |^)(:(' + PseudoClass + ')|::?(' + PseudoElement + '))+( |$)',
21 | 'ig'
22 | ),
23 | REG2 = new RegExp(
24 | '\\((:(' + PseudoClass + ')|::?(' + PseudoElement + '))+\\)',
25 | 'ig'
26 | ),
27 | REG3 = new RegExp(
28 | '(:(' + PseudoClass + ')|::?(' + PseudoElement + '))+',
29 | 'ig'
30 | )
31 |
32 | function filterRules($0: HTMLElement, objCss, taskTimerRecord) {
33 | var promises = []
34 | var matched = []
35 | var keyFramUsed = []
36 | var fontFaceUsed = []
37 |
38 | const descendantsCount = $0.querySelectorAll('*').length
39 |
40 | return new Promise(function (resolve, reject) {
41 | // loop every dom
42 | objCss.normRule.forEach(function (rule, idx) {
43 | promises.push(
44 | new Promise(async function (res) {
45 | var timer = setTimeout(function () {
46 | if (idx % 1000 === 0) {
47 | let nRule = objCss.normRule.length
48 | chrome.runtime.sendMessage({
49 | action: 'inform',
50 | info: `The selected dom has ${descendantsCount} descendants.\nPage rules are about ${nRule}.\nTraversing the ${idx}th rule...`,
51 | })
52 | }
53 |
54 | if (typeof rule === 'string') {
55 | res(rule)
56 | return
57 | } else {
58 | var selMatched = []
59 | var arrSel = rule.selectors.filter(function (v, i, self) {
60 | return self.indexOf(v) === i
61 | })
62 | arrSel.forEach(function (sel) {
63 | if (selMatched.indexOf(sel) !== -1) {
64 | return
65 | }
66 | // these pseudo class/elements can apply to any ele
67 | // but wont apply now
68 | // eg. :active{xxx}
69 | // only works when clicked on and actived
70 | if (sel.length < MaxPossiblePseudoLength && sel.match(REG0)) {
71 | selMatched.push(sel)
72 | } else {
73 | let errorArray = []
74 | let replacedSel = sel
75 | .replace(REG1, ' * ')
76 | .replace(REG2, '(*)')
77 | .replace(REG3, '')
78 | .replace(/:not\(\*\)/ig, '')
79 |
80 | try {
81 | if (
82 | $0.matches(replacedSel) ||
83 | $0.querySelectorAll(replacedSel).length !== 0
84 | ) {
85 | selMatched.push(sel)
86 | }
87 | } catch (e) {
88 | errorArray.push({
89 | selector: replacedSel,
90 | error: e,
91 | })
92 | }
93 | if (debugMode) {
94 | console.warn('selector match error: ', errorArray)
95 | }
96 | }
97 | })
98 | if (selMatched.length !== 0) {
99 | // remove duplicate selector
100 | var cssText = selMatched
101 | .filter(function (v, i, self) {
102 | return self.indexOf(v) === i
103 | })
104 | .join(',')
105 | cssText += '{' + cssHelper.normRuleNodeToText(rule) + '}'
106 | res(cssText)
107 | rule.nodes.forEach(function (ele) {
108 | if (
109 | ele.prop &&
110 | ele.prop.match(/^(-(webkit|moz)-)?animation(-name)?$/i) !==
111 | null
112 | ) {
113 | keyFramUsed = keyFramUsed.concat(
114 | ele.value.split(/ *, */).map(function (ele) {
115 | return ele.split(' ')[0]
116 | })
117 | )
118 | }
119 | })
120 |
121 | if (rule && rule.nodes) {
122 | for (let index = 0; index < rule.nodes.length; index++) {
123 | const declaration = rule.nodes[index]
124 | if (declaration && declaration.prop === 'font-family') {
125 | fontFaceUsed = [
126 | ...fontFaceUsed,
127 | ...declaration.value
128 | .split(/ *, */)
129 | .filter((e: string) => !!e),
130 | ]
131 | }
132 | }
133 | }
134 | return
135 | }
136 | }
137 | res('')
138 | }, 0)
139 | taskTimerRecord.push(timer)
140 | })
141 | )
142 | })
143 |
144 | Promise.all(promises)
145 | .then(function (result) {
146 | keyFramUsed = keyFramUsed.filter(function (v, i, self) {
147 | return self.indexOf(v) === i
148 | })
149 | fontFaceUsed = fontFaceUsed.filter(function (v, i, self) {
150 | return self.indexOf(v) === i
151 | })
152 | result.forEach(function (ele) {
153 | // typeof ele:string
154 | if (ele.length > 0) {
155 | matched.push(ele)
156 | }
157 | })
158 | var frameCommentMarkUsed = false
159 | keyFramUsed.forEach(function (ele) {
160 | objCss.keyFram.forEach(function (e) {
161 | if (ele === e.params) {
162 | if (!frameCommentMarkUsed) {
163 | matched.push('/*! CSS Used keyframes */')
164 | frameCommentMarkUsed = true
165 | }
166 | matched.push(cssHelper.keyFramNodeToText(e))
167 | }
168 | })
169 | })
170 | var fontCommentMarkUsed = false
171 | fontFaceUsed.forEach(function (ele) {
172 | objCss.fontFace.forEach(function (e) {
173 | e.nodes.forEach(function (n) {
174 | if (
175 | n.prop === 'font-family' &&
176 | ele.replace(/^(['"])?(.*)\1$/, '$2') ===
177 | n.value.replace(/^(['"])?(.*)\1$/, '$2')
178 | ) {
179 | if (!fontCommentMarkUsed) {
180 | matched.push('/*! CSS Used fontfaces */')
181 | fontCommentMarkUsed = true
182 | }
183 | matched.push(cssHelper.fontFaceNodeToText(e))
184 | }
185 | })
186 | })
187 | })
188 | resolve(matched)
189 | })
190 | .catch(function (err) {
191 | reject(err)
192 | })
193 | })
194 | }
195 |
196 | export default filterRules
197 |
--------------------------------------------------------------------------------
/src/ui/Panel.svelte:
--------------------------------------------------------------------------------
1 |
177 |
178 |
179 |
180 | CSS Used by $0 and its descendants:
181 |
182 |
183 |
184 |
191 |
192 |
193 |
196 |
197 |
205 |
206 |
207 | {#if popVisible}
208 |
209 |
210 | {#if tipsVisible}
211 |
212 |
Extensions can't work on file:/// pages by default.
213 |
214 | You can the "Allow access to file URLs"
217 |
218 |
And restart chrome if necessary.
219 |
220 | {:else}
221 |
224 | {/if}
225 |
226 | {/if}
227 |
228 | {#if debugMode}
229 |
230 |
231 |
232 |
233 | {/if}
234 |
235 |
236 |
444 |
--------------------------------------------------------------------------------