A couple of days ago, it got to my attention that most websites where I read my articles have bad printable pages. So, I decided to make a chrome extension that "prettifies" the print preview for some popular websites that I print.
The idea was to write a certain set of rules and/or conditions, which once satisfied tell the extension which CSS file improves the print media for that page.
Among those websites were multiple Medium based websites such as TowardsDataScience, Medium, etc.. I had to find a solution that wasn't only url or host specific; otherwise, I would end up having to enter every Medium based website's url or host.
Thus, the solution was to check if a certain element existed in the page using a CSS selector, which meant that I had to be able to get the page's HTML source first... Moreover, the CSS print media file needed to be later on injected to the page automatically.
However, injecting the CSS file programmatically is done through chrome.tabs.insertCSS
. The function requires the activeTab
permission.
According to the chrome developer api, user gestures (through clicking a certain action or item, or using a keyboard shortcut command) are necessary to enable activeTab
.
Yet again, the file needs to be injected automatically.
This post will be explaining how I managed to inject CSS files automatically without using activeTab
permission or chrome.tabs.insertCSS
Here are the approaches I took, in order:
The Basic
The basic approach is the activeTab
permission approach. Unfortunately, there is another problem to it, other than injecting the CSS file.
Getting the page's HTML source is not possible because the chrome developer api does not have any method to get a certain tab's HTML document. Consequently, I had to inject JS to the tab to query the selector and check if the element exists.
This prevented me from being able to check if a CSS selector matches an element in the page, or even inject the CSS print media file, unless I interact with the action to enable activeTab
permission on that tab.
So clearly, I needed a different solution.
The Fantasy
Luckily, the fantasy unveiled itself to me while reading their developer guide. The thing that caught my eye was chrome.declarativeContent
api. It had everything I could ever dream of...
-
PageStateMatcher
that supports CSS matching and pageUrl matching. -
RequestContentScript
that supports injecting CSS and JS files after the rules and/or conditions were satisfied.
Please note that this api requires the use of the
declarativeContent
permission.
So this approach would work in the following way
How It Works
- Chrome checks for the rules defined in the extension
- For every rule, if one of the conditions or
PageStateMatcher
is satisfied, then Chrome executes the actions specified in the rule.
So, here's the rule I would be using for Medium based websites...
{
conditions: [
new chrome.declarativeContent.PageStateMatcher({
css: [
"meta[property='og:site_name'][content='Medium']"
]
})
],
actions: [
new chrome.declarativeContent.RequestContentScript({
css: [
"css/medium.com.css"
]
})
]
}
Check the whole code on GitHub
Oh, yes would be, because according to the api that action is not supported on stable builds of Chrome.
Since Chrome 38.
Declarative event action that injects a content script.
WARNING: This action is still experimental and is not supported on stable builds of Chrome.
I tried so hard and got so far, but in the end it didn't even matter
The "Hack" Fantasy
The fantasy approach was just too good to go unnoticed and ignored. It was the solution I needed, an automatic CSS file injection. Hence, I needed to implement it myself in a hacky way. To implement that hacky approach, I used two different apis.
Although we are using those apis, it is important to note that no permissions are needed in
manifest.json
Additionally, the rules and/or conditions are defined in a similar way to chrome.declarativeContent
.
new Rule({
conditions: [
new Condition({
css: [
"meta[property='og:site_name'][content='Medium']"
]
})
],
cssFiles: [
"medium.com.css"
]
})
Check the whole code on GitHub
So here's how the hacky implementation worked.
Thought Process
-
injector.js
that is loaded to all pages (<all_urls>
). -
injector.js
sends to the extension- page
window.location
- page
document
object
- page
- Extension's
background.js
receives the message from the page - Extension's
validator.js
checks if any rules and/or conditions satisfies the page depending on:- pageUrl matching
- CSS matching by selector
- If a rule satisfies a page, the extension's
background.js
sends back the path of all thecssFiles
associated with thatRule
. -
injector.js
receives the response from the extension'sbackground.js
and adds the CSS file paths to the page.
Please note that there are multiple
manifest.json
attributes that need to be added in order to make all that possible. However, for the sake of keeping this short, the code is commented in detail on GitHub.Check the whole implementation on GitHub
Special thanks to slice for his review and constructive comments.
Top comments (4)
Is it possible to add JS file instead of CSS file paths to the page?
The script injects code directly in the webpage, so yeah it should work on JS files (but you might have to make slight tweaks to the existing code)
Isn't it possible to add the css file in content_scripts in manifest.json to inject it on page load?
Ah, yes there is. There is
content_scripts
where you can specify url matches, javascript, and css files. There's actually a good reason why I didn't use them, if I recall correctly.So the reason is that you need permissions to run it. So, it requires user interaction and the user to "allow it to run" every time this page is opened. I wanted a more "automatic" injection where the user doesn't have to interact with the extension. The approach I took eliminates any need for any permissions.