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.0-pp310-pypy310_pp73-manylinux_2_24_x86_64.whl
Algorithm | Hash digest | |
---|---|---|
SHA256 | a59576ff476dcfaf2212ad5caa61ce0485745a770d312db38aa8deeedc209a6f |
|
MD5 | bd5de9cbb5453b839147795f022d4d53 |
|
BLAKE2b-256 | b8ad3304f3bf85ca91f554c5e80ba62d11a7e56657bf19f0b6c2f84cdb6e3e64 |
Hashes for css_inline-0.14.0-pp310-pypy310_pp73-manylinux_2_24_aarch64.whl
Algorithm | Hash digest | |
---|---|---|
SHA256 | 9e29abd0c63faca59bffed63fcaef27c2c9923f992963d299fce34daa8104583 |
|
MD5 | 3d1eeeb7f32a2a57e3417936ec6d9294 |
|
BLAKE2b-256 | 6354acbb7213d2058c5dd7de1e4e848722eadbada6c32896dfda5b028c8778d0 |
Hashes for css_inline-0.14.0-pp310-pypy310_pp73-macosx_10_12_x86_64.whl
Algorithm | Hash digest | |
---|---|---|
SHA256 | 8b3eddb595d785a86e55a7f768782aa86529b28a13aaee86a36d0f4dbd641848 |
|
MD5 | 9ee08131002e70f32d19e2b4b93d1878 |
|
BLAKE2b-256 | 9662d95099e46fc46b76640b7463eb3559940a10b5a638d1d50741fad4a52e14 |
Hashes for css_inline-0.14.0-pp39-pypy39_pp73-manylinux_2_24_x86_64.whl
Algorithm | Hash digest | |
---|---|---|
SHA256 | a56d887fbcaccf1cee5fdf8a54ebe60cb69978cb7550a1dda136d3ed43e45363 |
|
MD5 | e55befd32e85563857963f2232d57f47 |
|
BLAKE2b-256 | 1665278121f7a8aa7133d37e23dd0165642d3b7309463e8e8d595e4d42a0913a |
Hashes for css_inline-0.14.0-pp39-pypy39_pp73-manylinux_2_24_aarch64.whl
Algorithm | Hash digest | |
---|---|---|
SHA256 | 13e591616c2a0d239f5ab3d598db85669740504bced9c019ec43c7106b0508cc |
|
MD5 | 3630570308c59a97c3d1d7ac61cf9b3e |
|
BLAKE2b-256 | 4e045262a45300ed083745fcaca97c43af078a3ae26a392865b4dab013196c6b |
Hashes for css_inline-0.14.0-pp39-pypy39_pp73-macosx_10_12_x86_64.whl
Algorithm | Hash digest | |
---|---|---|
SHA256 | 7bfefab6bb58a78e7fdb5dbcd0712714665e426f9558d7113c5ebe84d4ccc258 |
|
MD5 | a0a42fb97feeb97e105dc04cc7c9817d |
|
BLAKE2b-256 | 0ab0c1e1262f2eb1b15cf7c00949597f74824bbdb8ffaca926e69a155f8c0515 |
Hashes for css_inline-0.14.0-pp38-pypy38_pp73-manylinux_2_24_x86_64.whl
Algorithm | Hash digest | |
---|---|---|
SHA256 | 1fb0ba42e95c372cb7bdb7adacb7feed9187dc7cb6f5cbfbbdcd4d74b3a2bfc5 |
|
MD5 | 7260832492e1137fac1a8731feaf5054 |
|
BLAKE2b-256 | 8a09fb1c51ca1f3b771818cc576df95d517e74ec74d3c6fa8580b0e18278c2d7 |
Hashes for css_inline-0.14.0-pp38-pypy38_pp73-manylinux_2_24_aarch64.whl
Algorithm | Hash digest | |
---|---|---|
SHA256 | 62dc60087a0c4ef1f99e1347048ee811273069c150c971d470e394d0f5929bdc |
|
MD5 | 94e29745a60f3743d5267d2f22a0bb1a |
|
BLAKE2b-256 | 2acff4d3b3bc21b5ee831bf2f0fa535d8fb2984d8f4fbf36b8c3ba62f0208b2d |
Hashes for css_inline-0.14.0-pp38-pypy38_pp73-macosx_10_12_x86_64.whl
Algorithm | Hash digest | |
---|---|---|
SHA256 | dad01a269b161d12beed9f74363b6f329fe7ca0b30bbed448c66bc551cce58d7 |
|
MD5 | 326882b9d6cae3a1ea577cafaa0301b1 |
|
BLAKE2b-256 | 67b474f07c53d7834ff87c32c6c4c4addce07ca73be722c30cf81e72efff5272 |
Hashes for css_inline-0.14.0-pp37-pypy37_pp73-manylinux_2_24_x86_64.whl
Algorithm | Hash digest | |
---|---|---|
SHA256 | f4868a27e2ab794e6fa6a49db84483e23930208a8e2d6eedfdaf0db40e6327ce |
|
MD5 | 89f1b06c5fb1d26a6069a2e0b611cdfb |
|
BLAKE2b-256 | 7399eb54e75c293bbd0d2c0686da392a55ceb2472ebb86dfa3b797211ee8f2a6 |
Hashes for css_inline-0.14.0-pp37-pypy37_pp73-manylinux_2_24_aarch64.whl
Algorithm | Hash digest | |
---|---|---|
SHA256 | 929387a7eec70a97bc9592c2bb76c9db2c4a9b4e9317ead78f39c36f705c3021 |
|
MD5 | b6ac3dae2157954d8b6aad1d64dd687b |
|
BLAKE2b-256 | 22965d8295c48422395002079f9bd722d68a04e0a1d81fe00013944d7a970215 |
Hashes for css_inline-0.14.0-pp37-pypy37_pp73-macosx_10_12_x86_64.whl
Algorithm | Hash digest | |
---|---|---|
SHA256 | f63fc8b79ca407950d316bf3b0280a753510b6d318c78617c6ff43af190a09e5 |
|
MD5 | 52bdb8d5aea0bc9cd9bca39da3367a4f |
|
BLAKE2b-256 | c84a987337b648b8b7e0baf1cb210500eedff4dc9d3d115ec9227482d911b2b1 |
Hashes for css_inline-0.14.0-cp37-abi3-win_amd64.whl
Algorithm | Hash digest | |
---|---|---|
SHA256 | fc7f4e703fb422e815b1e69baeeb200773351f54c4d00e1c95179742f43703f3 |
|
MD5 | 776cb58c696698aa9f8b00ef55e2deb6 |
|
BLAKE2b-256 | ea997cec1af0677583509eb35b58985c8ed2fcdcacc5817ce1ae4ebf1d116f0b |
Hashes for css_inline-0.14.0-cp37-abi3-win32.whl
Algorithm | Hash digest | |
---|---|---|
SHA256 | 6ba47697c54d36d7a7b94ec2be43c7bc462a9fee5787289678923225e1a47ba8 |
|
MD5 | 38438f094cbf92ae43ae47865fc63fdd |
|
BLAKE2b-256 | 1cbb6e7c6755a67477028b3a650b09c8d005efb663963c538bf2fda5030f5771 |
Hashes for css_inline-0.14.0-cp37-abi3-musllinux_1_2_x86_64.whl
Algorithm | Hash digest | |
---|---|---|
SHA256 | c21ad2e70b6291dca8d44d750d6d231312e6478d3389ec55a1db295d31f12e73 |
|
MD5 | 3a6eb6bbd6d1cef00c249129123ca2e9 |
|
BLAKE2b-256 | 8217f01a8a9be710ade5232d42f227296a5f4ac083032990b95f5028b51c8bcc |
Hashes for css_inline-0.14.0-cp37-abi3-musllinux_1_2_armv7l.whl
Algorithm | Hash digest | |
---|---|---|
SHA256 | 464594341395c03e81b327302667a2d8a28a8849e0ca27957f2928eebefda257 |
|
MD5 | 6e15a477abc726e478a5337f52e06f26 |
|
BLAKE2b-256 | f6c171f4234a751f99ce0719237ea6cf60a6eb2bfedda33286f1c115c0c1f0ac |
Hashes for css_inline-0.14.0-cp37-abi3-musllinux_1_2_aarch64.whl
Algorithm | Hash digest | |
---|---|---|
SHA256 | 78a016d063f56d566778f1e494d16c42cbe94f139dba0a98bd1a3af0fc5289b3 |
|
MD5 | 7f4217cddf0aebf913fdf77131cdc3f1 |
|
BLAKE2b-256 | b1c87e347ad84e24efa48f2b18b97956984d2f41e8f1ffe795a593c5668ae3ca |
Hashes for css_inline-0.14.0-cp37-abi3-manylinux_2_24_armv7l.whl
Algorithm | Hash digest | |
---|---|---|
SHA256 | dbb80664f3f65a2835917de80a502a8383ade04fa38fa65c0dfcd5d96f80c6c0 |
|
MD5 | 2a591941a8ef1243297aae522985df48 |
|
BLAKE2b-256 | 1be9f354410c902678c1f5dbc894634003266cd274b9347a0c39ac1896f992ab |
Hashes for css_inline-0.14.0-cp37-abi3-manylinux_2_24_aarch64.whl
Algorithm | Hash digest | |
---|---|---|
SHA256 | eb1471b708f9757fc10447f0ce38ddb76fd3d946b44dda82d4bc01ffcd24dbe2 |
|
MD5 | 875db047416d9f89b3a7b395c8fdf3ab |
|
BLAKE2b-256 | 9e54e61884489ebf15b83697c97723b33950410b7f005209ca545d4f8353c198 |
Hashes for css_inline-0.14.0-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
Algorithm | Hash digest | |
---|---|---|
SHA256 | 27809b9fc3814e30a95dace0b3a5e8f2404d77d49474a2df977f939403b13f44 |
|
MD5 | 7c2ab60cf00b595b567e52b6a206489a |
|
BLAKE2b-256 | e60e5966d5ec7ec469b82ec5f37258fd5808eb138b0490640b8cc1f73b9437d4 |
Hashes for css_inline-0.14.0-cp37-abi3-manylinux_2_12_i686.manylinux2010_i686.whl
Algorithm | Hash digest | |
---|---|---|
SHA256 | ff8e13f76b1481c236fd8498dfec9647aadfbe120f19762983c7207138087198 |
|
MD5 | bbc1615fbe4b57174fb453b124fbe3c1 |
|
BLAKE2b-256 | fe8cce6a8971633525c177f4030496884f6ddef6f29130cfe9d0773005f63c47 |
Hashes for css_inline-0.14.0-cp37-abi3-macosx_10_12_x86_64.whl
Algorithm | Hash digest | |
---|---|---|
SHA256 | 3c5603865a6dfcfeb19039f9e28a2349e101a32d9a6dce05683779ace4fdbad3 |
|
MD5 | f3828892a33bfb3ac1158c5bbcd116ee |
|
BLAKE2b-256 | fc6d09ec2a363fd9b6fae17c0a85b9b418d471ebf5f50e827f44016774b0d4d0 |
Hashes for css_inline-0.14.0-cp37-abi3-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl
Algorithm | Hash digest | |
---|---|---|
SHA256 | 9c4622c41391e151f716e4350365b801696c17e1ef6611226bd239ba5b88eec3 |
|
MD5 | 96004e796aa79d2bc14f8b606021db08 |
|
BLAKE2b-256 | a98cbfb71511f6039360f3f9d8a3154dea1988a783af9ef28dc7c577f576877e |