High-performance library for inlining CSS into HTML 'style' attributes
Project description
css_inline
css_inline
is a high-performance library for inlining CSS into HTML 'style' attributes.
This library is designed for scenarios such as preparing HTML emails or embedding HTML into third-party web pages.
For instance, the library transforms HTML like this:
<html>
<head>
<style>h1 { color:blue; }</style>
</head>
<body>
<h1>Big Text</h1>
</body>
</html>
into:
<html>
<head></head>
<body>
<h1 style="color:blue;">Big Text</h1>
</body>
</html>
- Uses reliable components from Mozilla's Servo project
- 10-400x faster than alternatives
- Inlines CSS from
style
andlink
tags - Removes
style
andlink
tags - Resolves external stylesheets (including local files)
- Optionally caches external stylesheets
- Can process multiple documents in parallel
- Works on Linux, Windows, and macOS
- Supports HTML5 & CSS3
- Tested on CPython 3.7, 3.8, 3.9, 3.10, 3.11, 3.12 and PyPy 3.7, 3.8, 3.9, 3.10.
Playground
If you'd like to try css-inline
, you can check the WebAssembly-powered playground to see the results instantly.
Installation
Install with pip
:
pip install css_inline
Pre-compiled wheels are available for most popular platforms. If not available for your platform, a Rust compiler will be needed to build this package from source. Rust version 1.65 or higher is required.
Usage
import css_inline
HTML = """<html>
<head>
<style>h1 { color:blue; }</style>
</head>
<body>
<h1>Big Text</h1>
</body>
</html>"""
inlined = css_inline.inline(HTML)
# HTML becomes this:
#
# <html>
# <head>
# <style>h1 { color:blue; }</style>
# </head>
# <body>
# <h1 style="color:blue;">Big Text</h1>
# </body>
# </html>
Note that css-inline
automatically adds missing html
and body
tags, so the output is a valid HTML document.
Alternatively, you can inline CSS into an HTML fragment, preserving the original structure:
FRAGMENT = """<main>
<h1>Hello</h1>
<section>
<p>who am i</p>
</section>
</main>"""
CSS = """
p {
color: red;
}
h1 {
color: blue;
}
"""
inlined = css_inline.inline_fragment(FRAGMENT, CSS)
# HTML becomes this:
# <main>
# <h1 style="color: blue;">Hello</h1>
# <section>
# <p style="color: red;">who am i</p>
# </section>
# </main>
When there is a need to inline multiple HTML documents simultaneously, css_inline
offers inline_many
and inline_many_fragments
functions.
This feature allows for concurrent processing of several inputs, significantly improving performance when dealing with a large number of documents.
import css_inline
css_inline.inline_many(["<...>", "<...>"])
Under the hood, inline_many
, spawns threads at the Rust layer to handle the parallel processing of inputs.
This results in faster execution times compared to employing parallel processing techniques at the Python level.
Note: To fully benefit from inline_many
, you should run your application on a multicore machine.
Configuration
For configuration options use the CSSInliner
class:
import css_inline
inliner = css_inline.CSSInliner(keep_style_tags=True)
inliner.inline("...")
inline_style_tags
. Specifies whether to inline CSS from "style" tags. Default:True
keep_style_tags
. Specifies whether to keep "style" tags after inlining. Default:False
keep_link_tags
. Specifies whether to keep "link" tags after inlining. Default:False
base_url
. The base URL used to resolve relative URLs. If you'd like to load stylesheets from your filesystem, use thefile://
scheme. Default:None
load_remote_stylesheets
. Specifies whether remote stylesheets should be loaded. Default:True
cache
. Specifies caching options for external stylesheets (for example,StylesheetCache(size=5)
). Default:None
extra_css
. Extra CSS to be inlined. Default:None
preallocate_node_capacity
. Advanced. Preallocates capacity for HTML nodes during parsing. This can improve performance when you have an estimate of the number of nodes in your HTML document. Default:32
You can also skip CSS inlining for an HTML tag by adding the data-css-inline="ignore"
attribute to it:
<head>
<style>h1 { color:blue; }</style>
</head>
<body>
<!-- The tag below won't receive additional styles -->
<h1 data-css-inline="ignore">Big Text</h1>
</body>
The data-css-inline="ignore"
attribute also allows you to skip link
and style
tags:
<head>
<!-- Styles below are ignored -->
<style data-css-inline="ignore">h1 { color:blue; }</style>
</head>
<body>
<h1>Big Text</h1>
</body>
Alternatively, you may keep style
from being removed by using the data-css-inline="keep"
attribute.
This is useful if you want to keep @media
queries for responsive emails in separate style
tags:
<head>
<!-- Styles below are not removed -->
<style data-css-inline="keep">h1 { color:blue; }</style>
</head>
<body>
<h1>Big Text</h1>
</body>
Such tags will be kept in the resulting HTML even if the keep_style_tags
option is set to false
.
If you'd like to load stylesheets from your filesystem, use the file://
scheme:
import css_inline
# styles/email is relative to the current directory
inliner = css_inline.CSSInliner(base_url="file://styles/email/")
inliner.inline("...")
You can also cache external stylesheets to avoid excessive network requests:
import css_inline
inliner = css_inline.CSSInliner(
cache=css_inline.StylesheetCache(size=5)
)
inliner.inline("...")
Caching is disabled by default.
XHTML compatibility
If you'd like to work around some XHTML compatibility issues like closing empty tags (<hr>
vs. <hr/>
), you can use the following snippet that involves lxml
:
import css_inline
from lxml import html, etree
document = "..." # Your HTML document
inlined = css_inline.inline(document)
tree = html.fromstring(inlined)
inlined = etree.tostring(tree).decode(encoding="utf-8")
Performance
css-inline
is powered by efficient tooling from Mozilla's Servo project and significantly outperforms other Python alternatives in terms of speed.
Most of the time it achieves over a 10x speed advantage compared to the next fastest alternative.
Here is the performance comparison:
Size | css_inline 0.14.0 |
premailer 3.10.0 |
toronado 0.1.0 |
inlinestyler 0.2.5 |
pynliner 0.8.0 |
|
---|---|---|---|---|---|---|
Basic | 230 B | 6.79 µs | 130.25 µs (19.18x) | 683.04 µs (100.58x) | 1.06 ms (157.10x) | 1.21ms (178.61x) |
Realistic-1 | 8.58 KB | 135.37 µs | 1.40 ms (10.38x) | 16.29 ms (120.37x) | 26.64 ms (196.82x) | 52.36 ms (386.85x) |
Realistic-2 | 4.3 KB | 85.41 µs | 2.70 ms (31.70x) | ERROR | 17.57 ms (205.73x) | ERROR |
GitHub page | 1.81 MB | 230.63 ms | 24.32 s (105.45x) | ERROR | ERROR | ERROR |
The "Basic" case was obtained from benchmarking the example from the Usage section.
Note that the toronado
, inlinestyler
and pynliner
libraries both encountered errors when used to inline CSS in the last scenario.
The benchmarking code is available in the benches/bench.py
file. The benchmarks were conducted using the stable rustc 1.77.1
on Python 3.11.7
.
Comparison with other libraries
Besides performance, css-inline
differs from other Python libraries for CSS inlining.
- Generally supports more CSS features than other libraries (for example,
toronado
andpynliner
do not support pseudo-elements); - It has fewer configuration options and not as flexible as
premailer
; - Works on fewer platforms than LXML-based libraries (
premailer
,inlinestyler
,toronado
, and optionallypynliner
); - Does not have debug logs yet;
- Supports only HTML 5.
Further reading
If you want to know how this library was created & how it works internally, you could take a look at these articles:
License
This project is licensed under the terms of the MIT license.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distributions
Hashes for css_inline-0.14.1-pp310-pypy310_pp73-manylinux_2_24_x86_64.whl
Algorithm | Hash digest | |
---|---|---|
SHA256 | d1e6b348fc98eb6086c81d84619fc4db68534d32460382051de809d5bd83beea |
|
MD5 | 0e96353baa3ca02774f91c4007acdc8f |
|
BLAKE2b-256 | 971d71fa6054cbb743b96bf3b7b9e529c29b3ea1187055e814658050c477008e |
Hashes for css_inline-0.14.1-pp310-pypy310_pp73-manylinux_2_24_aarch64.whl
Algorithm | Hash digest | |
---|---|---|
SHA256 | 776b2d228da427c22130f024aa3f824a7a459c5139b0a26a271747bbc7ed9cf2 |
|
MD5 | 1373eebcf6a172e5714e7b5a02b126e0 |
|
BLAKE2b-256 | a7e4ced62f5df04823c49e936cc11ec07310b35d8907548c9938130e1cc1b1c0 |
Hashes for css_inline-0.14.1-pp310-pypy310_pp73-macosx_10_12_x86_64.whl
Algorithm | Hash digest | |
---|---|---|
SHA256 | ab1ac4edcc8ccd69ac5b7e3f88689400cbdee79503512609a1519e744816bb80 |
|
MD5 | d346a0015ded67f19b8e864477994afe |
|
BLAKE2b-256 | 948b00c4634259da264bfa6f08edefaeae98997d595adafb4f138190ba9630cd |
Hashes for css_inline-0.14.1-pp39-pypy39_pp73-manylinux_2_24_x86_64.whl
Algorithm | Hash digest | |
---|---|---|
SHA256 | 07245808f48332e0381fe12b9e9675ee4b61c8063abc6dc90b46393f89b3ef3f |
|
MD5 | f36d3a5d15414ce7376e3ff20743e959 |
|
BLAKE2b-256 | 78c3218c0c8fb2d7c282a5cd60a6fe477ea3861a33451351627f74d266283be3 |
Hashes for css_inline-0.14.1-pp39-pypy39_pp73-manylinux_2_24_aarch64.whl
Algorithm | Hash digest | |
---|---|---|
SHA256 | 5d14bf191e4df53a91b9608e767cf0eb41efd9b4b047f81dba88a755d9300497 |
|
MD5 | ee404937ffee5071653eeabc40019dd6 |
|
BLAKE2b-256 | f2f9dd44c8dd98be18ad2d9be1b31ce6f0ca6f946cf1cee4e84b5953d1be554c |
Hashes for css_inline-0.14.1-pp39-pypy39_pp73-macosx_10_12_x86_64.whl
Algorithm | Hash digest | |
---|---|---|
SHA256 | 11958f80f7001739af70cecc3ce5d34871579a76bcdbb9cb67be709bd4b68b15 |
|
MD5 | 95fb4ca9e08b2fef116cc8345263a3e1 |
|
BLAKE2b-256 | 731ef6a6182cbd5b80b7a712ba67b32e56ffb4917056a0e2abcf34f3bdcb1ffe |
Hashes for css_inline-0.14.1-pp38-pypy38_pp73-manylinux_2_24_x86_64.whl
Algorithm | Hash digest | |
---|---|---|
SHA256 | 8a82d7658524d2f6ef215e7054dd5efb01767877c02e63199aa0a6b94ea9dddc |
|
MD5 | 8ccff4f989171349f6ce3726cb39d206 |
|
BLAKE2b-256 | f5d77d98a5164caaff6b2fb7c473c4a4a441f16dcc32d3436797fdb4ce2aad32 |
Hashes for css_inline-0.14.1-pp38-pypy38_pp73-manylinux_2_24_aarch64.whl
Algorithm | Hash digest | |
---|---|---|
SHA256 | 1da8244e369906d74379bb50c0424df9d4f11a8eb17cfe7e254de7589254bfb3 |
|
MD5 | 805dce7a6773e50fe3c87e5449affc18 |
|
BLAKE2b-256 | c1ff514436f41675ff880b40004a08322d01d3098d92bded29e614e80dbad6d1 |
Hashes for css_inline-0.14.1-pp38-pypy38_pp73-macosx_10_12_x86_64.whl
Algorithm | Hash digest | |
---|---|---|
SHA256 | cda499277cd3866820efbd93440267028c0faeb57f02492d5e449a62dc0bf297 |
|
MD5 | 4d5e1d5c855e72f4209f109aa406f7ec |
|
BLAKE2b-256 | 848a8e7cd209a4d8478194d7431d5b7340de6d1684347cda7d6b9c8259f40550 |
Hashes for css_inline-0.14.1-pp37-pypy37_pp73-manylinux_2_24_x86_64.whl
Algorithm | Hash digest | |
---|---|---|
SHA256 | 4e6b175aca68b1063a820e3d2e2631646331b87caaa19d9c20fc4e638ca7c4b3 |
|
MD5 | 15440ebe6f70fe7ac7faf2aec6901aba |
|
BLAKE2b-256 | e463047253fce86bf1b9f046a557425dede0b98632c2cfa7e62a0cde40b1b4ee |
Hashes for css_inline-0.14.1-pp37-pypy37_pp73-manylinux_2_24_aarch64.whl
Algorithm | Hash digest | |
---|---|---|
SHA256 | 0b1dede818706207a3aff184073fc92caea0014f91d7ee497ca6bcf578c6d941 |
|
MD5 | 55140f48e504a196977828f4fa5f30da |
|
BLAKE2b-256 | d8064fe9c5bb58b559b6402d2480af7ccd03f59c99de580571063f108ca11db1 |
Hashes for css_inline-0.14.1-pp37-pypy37_pp73-macosx_10_12_x86_64.whl
Algorithm | Hash digest | |
---|---|---|
SHA256 | ebc56162752a5dd1d5d64c57e61b06efba42add243a8f900f465276cf0207930 |
|
MD5 | 291607d7a1c3e65d228ca382c338fa13 |
|
BLAKE2b-256 | 0a63ac3cc8b8196b3517d8e577bb726ced1e2e7c5240e1600ec074b4b1aeeb0d |
Hashes for css_inline-0.14.1-cp37-abi3-win_amd64.whl
Algorithm | Hash digest | |
---|---|---|
SHA256 | b8d62ebb7f868f50139cf37c0855f24645c0d2b78f14a90f95e2e975c1008a7e |
|
MD5 | e01526d327c892eea9d9ee607ea58b4b |
|
BLAKE2b-256 | 67c124f5afb6512b29bff1f83144082c0fa69065c102edfa283bd7b51176f64a |
Hashes for css_inline-0.14.1-cp37-abi3-win32.whl
Algorithm | Hash digest | |
---|---|---|
SHA256 | e0b2f8df51d5ad947765f3c2a644687ae3a8678e430c8af72a8b58cb2fefddfb |
|
MD5 | fb6ac8775b132453ed774d2567b67939 |
|
BLAKE2b-256 | 83628bd89b77eca2272974246ee0bc5e41ee59f523c10a958e4b2f7804d47a87 |
Hashes for css_inline-0.14.1-cp37-abi3-musllinux_1_2_x86_64.whl
Algorithm | Hash digest | |
---|---|---|
SHA256 | fe163ec5504496cd842b5ed8da914033b93ede09a1c23b6322b5b0efe8d57499 |
|
MD5 | 551531ae06cdfe62e9f576144d214a72 |
|
BLAKE2b-256 | 664478fcc5ffa41f8ef806888c70b07c8eca9c12edadebf3a0d6bb1b7e0354b5 |
Hashes for css_inline-0.14.1-cp37-abi3-musllinux_1_2_armv7l.whl
Algorithm | Hash digest | |
---|---|---|
SHA256 | 8841bc4f1eb64d502bcbde1367a75afcb28f9b70b8e3bf4effc2c5b6db10cfdc |
|
MD5 | c76882099221cf80e192172ea7b2b87f |
|
BLAKE2b-256 | ccc05a582c6d9b349e1f73a46b925361fcc895717d45037dcc7af00a458c8a9e |
Hashes for css_inline-0.14.1-cp37-abi3-musllinux_1_2_aarch64.whl
Algorithm | Hash digest | |
---|---|---|
SHA256 | 2ee58ad5fd000526748db9851989b306a2e4cb84cc788bb784b4d9eb045286a8 |
|
MD5 | bdebb65b7e82c1b61495012817348a92 |
|
BLAKE2b-256 | 2793d91166bb4d1605684f420776765e4c996c7c29c6cfda1531f37f6e62b451 |
Hashes for css_inline-0.14.1-cp37-abi3-manylinux_2_24_armv7l.whl
Algorithm | Hash digest | |
---|---|---|
SHA256 | 56676cf0a113ec76f4d773206cafda05d7708d81004d8f952aaf40b7ad14aa99 |
|
MD5 | 272d6bd45a406f457c0b22e5f0b11ccf |
|
BLAKE2b-256 | 06b7efc10585fd993acc85fe9240c05350d3c6ffb4d511249beb1bee827222d7 |
Hashes for css_inline-0.14.1-cp37-abi3-manylinux_2_24_aarch64.whl
Algorithm | Hash digest | |
---|---|---|
SHA256 | 56b63a535d78261b1205bf84e7c59abf7c20c5265a644171ff6e025e8933655b |
|
MD5 | ebd8decf0d610bccddc4163ae1678609 |
|
BLAKE2b-256 | ff8fc9018347a2273f331a2da22a09df7eea5b5024515a48c78c61b53b8bff13 |
Hashes for css_inline-0.14.1-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
Algorithm | Hash digest | |
---|---|---|
SHA256 | 8e07489135fc354bafe7ff0f417de1a0532927bdd25d66fd90bd09a47305d0de |
|
MD5 | b815102387c108c82d7b33cb7d4e71cb |
|
BLAKE2b-256 | aec921bb6894ad10a637bd3d2610842b0ab955cef6449606df2da3e7c4573119 |
Hashes for css_inline-0.14.1-cp37-abi3-manylinux_2_12_i686.manylinux2010_i686.whl
Algorithm | Hash digest | |
---|---|---|
SHA256 | 14ac1c24f312a9000d521ca914dab3c9c9281d08d41f0c4be9af40d217de57d1 |
|
MD5 | e4847f3529b8db0d35f2efea14638781 |
|
BLAKE2b-256 | c500b76dc0db6a6d3fb72d34f6085964428e2b7f5b17c0fcc2b09e9774925ce1 |
Hashes for css_inline-0.14.1-cp37-abi3-macosx_10_12_x86_64.whl
Algorithm | Hash digest | |
---|---|---|
SHA256 | 901465d67691a68bf849966a9e986dc7dc87021321caf1e958e9c48c02bfae32 |
|
MD5 | f41cfa29a2924b91c49c5c03a6f7c26a |
|
BLAKE2b-256 | a97f178fdd4c609052d7977f917ea1845fd1a7b184f479e9786ac00722d69b9f |
Hashes for css_inline-0.14.1-cp37-abi3-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl
Algorithm | Hash digest | |
---|---|---|
SHA256 | c6a5c2691a17d4cfc2e2fcfa6c2c42cc72f44f06dc34f63d5b08ab21769035e5 |
|
MD5 | 36eaa5084a1d1877a82d3ec7ecf961c4 |
|
BLAKE2b-256 | f57eebaee0f1ec3f8bda1d08fcd07bfb8c1c79f876b8d912596e873dc89410ca |