css-inline 0.22.0__tar.gz → 0.22.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- css_inline-0.22.1/PKG-INFO +355 -0
- css_inline-0.22.1/README.md +327 -0
- {css_inline-0.22.0 → css_inline-0.22.1}/bindings/python/CHANGELOG.md +8 -1
- {css_inline-0.22.0 → css_inline-0.22.1}/bindings/python/Cargo.lock +37 -38
- {css_inline-0.22.0 → css_inline-0.22.1}/bindings/python/Cargo.toml +1 -1
- css_inline-0.22.1/bindings/python/README.md +327 -0
- {css_inline-0.22.0 → css_inline-0.22.1}/css-inline/Cargo.toml +1 -1
- css_inline-0.22.1/css-inline/README.md +377 -0
- {css_inline-0.22.0 → css_inline-0.22.1}/css-inline/src/lib.rs +54 -35
- {css_inline-0.22.0 → css_inline-0.22.1}/css-inline/src/parser.rs +3 -4
- css_inline-0.22.0/PKG-INFO +0 -331
- css_inline-0.22.0/README.md +0 -303
- css_inline-0.22.0/bindings/python/README.md +0 -303
- css_inline-0.22.0/css-inline/README.md +0 -322
- {css_inline-0.22.0 → css_inline-0.22.1}/LICENSE +0 -0
- {css_inline-0.22.0 → css_inline-0.22.1}/bindings/python/build.rs +0 -0
- {css_inline-0.22.0 → css_inline-0.22.1}/bindings/python/src/lib.rs +0 -0
- {css_inline-0.22.0 → css_inline-0.22.1}/css-inline/benches/inliner.rs +0 -0
- {css_inline-0.22.0 → css_inline-0.22.1}/css-inline/rustfmt.toml +0 -0
- {css_inline-0.22.0 → css_inline-0.22.1}/css-inline/src/error.rs +0 -0
- {css_inline-0.22.0 → css_inline-0.22.1}/css-inline/src/html/attributes.rs +0 -0
- {css_inline-0.22.0 → css_inline-0.22.1}/css-inline/src/html/document.rs +0 -0
- {css_inline-0.22.0 → css_inline-0.22.1}/css-inline/src/html/element.rs +0 -0
- {css_inline-0.22.0 → css_inline-0.22.1}/css-inline/src/html/iter.rs +0 -0
- {css_inline-0.22.0 → css_inline-0.22.1}/css-inline/src/html/mod.rs +0 -0
- {css_inline-0.22.0 → css_inline-0.22.1}/css-inline/src/html/node.rs +0 -0
- {css_inline-0.22.0 → css_inline-0.22.1}/css-inline/src/html/parser.rs +0 -0
- {css_inline-0.22.0 → css_inline-0.22.1}/css-inline/src/html/selectors/attr_value.rs +0 -0
- {css_inline-0.22.0 → css_inline-0.22.1}/css-inline/src/html/selectors/local_name.rs +0 -0
- {css_inline-0.22.0 → css_inline-0.22.1}/css-inline/src/html/selectors/mod.rs +0 -0
- {css_inline-0.22.0 → css_inline-0.22.1}/css-inline/src/html/selectors/parser.rs +0 -0
- {css_inline-0.22.0 → css_inline-0.22.1}/css-inline/src/html/selectors/pseudo_classes.rs +0 -0
- {css_inline-0.22.0 → css_inline-0.22.1}/css-inline/src/html/selectors/pseudo_elements.rs +0 -0
- {css_inline-0.22.0 → css_inline-0.22.1}/css-inline/src/html/selectors/selector_impl.rs +0 -0
- {css_inline-0.22.0 → css_inline-0.22.1}/css-inline/src/html/serializer.rs +0 -0
- {css_inline-0.22.0 → css_inline-0.22.1}/css-inline/src/main.rs +0 -0
- {css_inline-0.22.0 → css_inline-0.22.1}/css-inline/src/resolver.rs +0 -0
- {css_inline-0.22.0 → css_inline-0.22.1}/pyproject.toml +0 -0
|
@@ -0,0 +1,355 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: css-inline
|
|
3
|
+
Version: 0.22.1
|
|
4
|
+
Classifier: Development Status :: 4 - Beta
|
|
5
|
+
Classifier: Intended Audience :: Developers
|
|
6
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
7
|
+
Classifier: Operating System :: MacOS :: MacOS X
|
|
8
|
+
Classifier: Operating System :: Microsoft :: Windows
|
|
9
|
+
Classifier: Operating System :: POSIX
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
16
|
+
Classifier: Programming Language :: Python :: Implementation :: CPython
|
|
17
|
+
Classifier: Programming Language :: Python :: Implementation :: PyPy
|
|
18
|
+
Classifier: Programming Language :: Rust
|
|
19
|
+
License-File: LICENSE
|
|
20
|
+
Summary: High-performance library for inlining CSS into HTML 'style' attributes
|
|
21
|
+
Keywords: css,html,email,stylesheet,inlining
|
|
22
|
+
Author-email: Dmitry Dygalo <dmitry@dygalo.dev>
|
|
23
|
+
Requires-Python: >=3.10
|
|
24
|
+
Description-Content-Type: text/markdown; charset=UTF-8; variant=GFM
|
|
25
|
+
Project-URL: homepage, https://github.com/Stranger6667/css-inline/tree/master/bindings/python
|
|
26
|
+
Project-URL: repository, https://github.com/Stranger6667/css-inline
|
|
27
|
+
|
|
28
|
+
# css_inline
|
|
29
|
+
|
|
30
|
+
[<img alt="build status" src="https://img.shields.io/github/actions/workflow/status/Stranger6667/css-inline/build.yml?style=flat-square&labelColor=555555&logo=github" height="20">](https://github.com/Stranger6667/css-inline/actions/workflows/build.yml)
|
|
31
|
+
[<img alt="pypi" src="https://img.shields.io/pypi/v/css_inline.svg?style=flat-square" height="20">](https://pypi.org/project/css_inline/)
|
|
32
|
+
[<img alt="versions" src="https://img.shields.io/pypi/pyversions/css_inline.svg?style=flat-square" height="20">](https://pypi.org/project/css_inline/)
|
|
33
|
+
[<img alt="license" src="https://img.shields.io/pypi/l/css_inline.svg?style=flat-square" height="20">](https://opensource.org/licenses/MIT)
|
|
34
|
+
[<img alt="codecov.io" src="https://img.shields.io/codecov/c/gh/Stranger6667/css-inline?logo=codecov&style=flat-square&token=tOzvV4kDY0" height="20">](https://app.codecov.io/github/Stranger6667/css-inline)
|
|
35
|
+
[<img alt="gitter" src="https://img.shields.io/gitter/room/Stranger6667/css-inline?style=flat-square" height="20">](https://gitter.im/Stranger6667/css-inline)
|
|
36
|
+
|
|
37
|
+
`css_inline` is a high-performance library for inlining CSS into HTML 'style' attributes.
|
|
38
|
+
|
|
39
|
+
Use it to prepare HTML emails or to embed HTML into third-party web pages.
|
|
40
|
+
|
|
41
|
+
It turns this HTML:
|
|
42
|
+
|
|
43
|
+
```html
|
|
44
|
+
<html>
|
|
45
|
+
<head>
|
|
46
|
+
<style>h1 { color:blue; }</style>
|
|
47
|
+
</head>
|
|
48
|
+
<body>
|
|
49
|
+
<h1>Big Text</h1>
|
|
50
|
+
</body>
|
|
51
|
+
</html>
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
into:
|
|
55
|
+
|
|
56
|
+
```html
|
|
57
|
+
<html><head>
|
|
58
|
+
|
|
59
|
+
</head>
|
|
60
|
+
<body>
|
|
61
|
+
<h1 style="color: blue;">Big Text</h1>
|
|
62
|
+
|
|
63
|
+
</body></html>
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
- Uses components from Mozilla's Servo project
|
|
67
|
+
- 14x to 680x faster than `premailer`, the fastest Python alternative (see [Performance](#performance))
|
|
68
|
+
- Inlines CSS from `style` and `link` tags
|
|
69
|
+
- Removes `style` and `link` tags
|
|
70
|
+
- Resolves external stylesheets (including local files)
|
|
71
|
+
- Caches external stylesheets (opt-in)
|
|
72
|
+
- Processes multiple documents in parallel
|
|
73
|
+
- Works on Linux, Windows, macOS and in the browser via Pyodide
|
|
74
|
+
- Supports HTML5 & CSS3
|
|
75
|
+
- Tested on CPython 3.10, 3.11, 3.12, 3.13, 3.14 and PyPy 3.11.
|
|
76
|
+
|
|
77
|
+
## Playground
|
|
78
|
+
|
|
79
|
+
Try `css-inline` in the WebAssembly-powered [playground](https://css-inline.org/).
|
|
80
|
+
|
|
81
|
+
## Installation
|
|
82
|
+
|
|
83
|
+
Install with `pip`:
|
|
84
|
+
|
|
85
|
+
```shell
|
|
86
|
+
pip install css_inline
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
The PyPI name is `css-inline`; `pip` accepts `css_inline` too. Import it as `css_inline`.
|
|
90
|
+
|
|
91
|
+
PyPI ships pre-compiled wheels for:
|
|
92
|
+
|
|
93
|
+
- Linux (glibc and musl): x86_64, aarch64, armv7; glibc also i686
|
|
94
|
+
- macOS: x86_64, arm64
|
|
95
|
+
- Windows: x64, x86
|
|
96
|
+
- PyPy 3.11: Linux x86_64 and aarch64, macOS x86_64
|
|
97
|
+
|
|
98
|
+
On other platforms, `pip` builds the package from source, which requires Rust 1.85 or newer.
|
|
99
|
+
|
|
100
|
+
## Usage
|
|
101
|
+
|
|
102
|
+
```python
|
|
103
|
+
import css_inline
|
|
104
|
+
|
|
105
|
+
HTML = """<html>
|
|
106
|
+
<head>
|
|
107
|
+
<style>h1 { color:blue; }</style>
|
|
108
|
+
</head>
|
|
109
|
+
<body>
|
|
110
|
+
<h1>Big Text</h1>
|
|
111
|
+
</body>
|
|
112
|
+
</html>"""
|
|
113
|
+
|
|
114
|
+
inlined = css_inline.inline(HTML)
|
|
115
|
+
# <html><head>
|
|
116
|
+
#
|
|
117
|
+
# </head>
|
|
118
|
+
# <body>
|
|
119
|
+
# <h1 style="color: blue;">Big Text</h1>
|
|
120
|
+
#
|
|
121
|
+
# </body></html>
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
`inline` adds missing `html`, `head` and `body` tags, so the output is a complete HTML document.
|
|
125
|
+
|
|
126
|
+
To inline CSS into an HTML fragment, use `inline_fragment(html, css)`. Both arguments are required; `css` holds the stylesheet to apply. The output stays a fragment: no `<html>`, `<head>` or `<body>` wrapper. Use `inline` for full documents:
|
|
127
|
+
|
|
128
|
+
```python
|
|
129
|
+
FRAGMENT = """<main>
|
|
130
|
+
<h1>Hello</h1>
|
|
131
|
+
<section>
|
|
132
|
+
<p>who am i</p>
|
|
133
|
+
</section>
|
|
134
|
+
</main>"""
|
|
135
|
+
|
|
136
|
+
CSS = """
|
|
137
|
+
p {
|
|
138
|
+
color: red;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
h1 {
|
|
142
|
+
color: blue;
|
|
143
|
+
}
|
|
144
|
+
"""
|
|
145
|
+
|
|
146
|
+
inlined = css_inline.inline_fragment(FRAGMENT, CSS)
|
|
147
|
+
# <main>
|
|
148
|
+
# <h1 style="color: blue;">Hello</h1>
|
|
149
|
+
# <section>
|
|
150
|
+
# <p style="color: red;">who am i</p>
|
|
151
|
+
# </section>
|
|
152
|
+
# </main>
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
To inline many documents at once, use `inline_many` or `inline_many_fragments`.
|
|
156
|
+
Both process inputs in parallel and return a list of strings in input order.
|
|
157
|
+
`inline_many_fragments` pairs `htmls[i]` with `css[i]`.
|
|
158
|
+
|
|
159
|
+
**Warning**: if the lists differ in length, `inline_many_fragments` silently drops the extra items from the longer list.
|
|
160
|
+
`CSSInliner` has the same two methods.
|
|
161
|
+
|
|
162
|
+
```python
|
|
163
|
+
import css_inline
|
|
164
|
+
|
|
165
|
+
css_inline.inline_many(["<...>", "<...>"])
|
|
166
|
+
css_inline.inline_many_fragments(["<p>a</p>", "<p>b</p>"], ["p { color: red; }", "p { color: blue; }"])
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
`inline_many` processes inputs in parallel on a Rust thread pool ([Rayon](https://github.com/rayon-rs/rayon)), so you don't need `multiprocessing`.
|
|
170
|
+
The speedup scales with the number of CPU cores.
|
|
171
|
+
|
|
172
|
+
### Configuration
|
|
173
|
+
|
|
174
|
+
Pass options to the `CSSInliner` class:
|
|
175
|
+
|
|
176
|
+
```python
|
|
177
|
+
import css_inline
|
|
178
|
+
|
|
179
|
+
inliner = css_inline.CSSInliner(keep_style_tags=True)
|
|
180
|
+
inliner.inline("...")
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
- `inline_style_tags`. Inline CSS from `style` tags. Default: `True`
|
|
184
|
+
- `keep_style_tags`. Keep `style` tags after inlining. Default: `False`
|
|
185
|
+
- `keep_link_tags`. Keep `link` tags after inlining. Default: `False`
|
|
186
|
+
- `keep_at_rules`. Keep at-rules (starting with `@`) after inlining. Default: `False`
|
|
187
|
+
- `minify_css`. Remove trailing semicolons and spaces between properties and values. Default: `False`
|
|
188
|
+
- `base_url`. Base URL for resolving relative URLs. Use the `file://` scheme to load stylesheets from the filesystem. Default: `None`
|
|
189
|
+
- `load_remote_stylesheets`. Load stylesheets from `link` tags, over the network or from `file://` paths. Default: `True`
|
|
190
|
+
- `cache`. Cache for external stylesheets, for example `StylesheetCache(size=5)`. Default: `None`
|
|
191
|
+
- `extra_css`. Extra CSS to inline. Default: `None`
|
|
192
|
+
- `preallocate_node_capacity`. **Advanced**. Number of HTML nodes to preallocate during parsing. Set it to your expected node count to avoid reallocations. Default: `32`
|
|
193
|
+
- `remove_inlined_selectors`. Remove inlined selectors from `<style>` blocks. Default: `False`
|
|
194
|
+
- `apply_width_attributes`. Add `width` HTML attributes from CSS `width` properties on supported elements (`table`, `td`, `th`, `img`). Default: `False`
|
|
195
|
+
- `apply_height_attributes`. Add `height` HTML attributes from CSS `height` properties on supported elements (`table`, `td`, `th`, `img`). Default: `False`
|
|
196
|
+
|
|
197
|
+
To skip CSS inlining for an HTML tag, add the `data-css-inline="ignore"` attribute:
|
|
198
|
+
|
|
199
|
+
```html
|
|
200
|
+
<head>
|
|
201
|
+
<style>h1 { color:blue; }</style>
|
|
202
|
+
</head>
|
|
203
|
+
<body>
|
|
204
|
+
<!-- The tag below won't receive additional styles -->
|
|
205
|
+
<h1 data-css-inline="ignore">Big Text</h1>
|
|
206
|
+
</body>
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
The same attribute on a `link` or `style` tag makes the inliner skip that stylesheet:
|
|
210
|
+
|
|
211
|
+
```html
|
|
212
|
+
<head>
|
|
213
|
+
<!-- Styles below are ignored -->
|
|
214
|
+
<style data-css-inline="ignore">h1 { color:blue; }</style>
|
|
215
|
+
</head>
|
|
216
|
+
<body>
|
|
217
|
+
<h1>Big Text</h1>
|
|
218
|
+
</body>
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
To keep a `style` tag in the output, add `data-css-inline="keep"`.
|
|
222
|
+
Use it for `@media` queries in responsive emails.
|
|
223
|
+
The tag stays even with `keep_style_tags=False`.
|
|
224
|
+
|
|
225
|
+
```html
|
|
226
|
+
<head>
|
|
227
|
+
<!-- Styles below are not removed -->
|
|
228
|
+
<style data-css-inline="keep">h1 { color:blue; }</style>
|
|
229
|
+
</head>
|
|
230
|
+
<body>
|
|
231
|
+
<h1>Big Text</h1>
|
|
232
|
+
</body>
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
At-rules can't go into `style` attributes, so the inliner removes them by default.
|
|
236
|
+
Set `keep_at_rules=True` to keep them, for example `@media` queries for responsive emails, in a `style` tag while inlining everything else.
|
|
237
|
+
The tag stays even with `keep_style_tags=False`.
|
|
238
|
+
|
|
239
|
+
```html
|
|
240
|
+
<head>
|
|
241
|
+
<!-- With keep_at_rules=True, "color: blue" goes into <h1> and @media stays in <style> -->
|
|
242
|
+
<style>h1 { color: blue; } @media (max-width: 600px) { h1 { font-size: 18px; } }</style>
|
|
243
|
+
</head>
|
|
244
|
+
<body>
|
|
245
|
+
<h1>Big Text</h1>
|
|
246
|
+
</body>
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
`minify_css=True` removes trailing semicolons and spaces between properties and values in inlined styles.
|
|
250
|
+
|
|
251
|
+
```html
|
|
252
|
+
<head>
|
|
253
|
+
<!-- With minify_css=True, the <h1> will have `style="color:blue;font-weight:bold"` -->
|
|
254
|
+
<style>h1 { color: blue; font-weight: bold; }</style>
|
|
255
|
+
</head>
|
|
256
|
+
<body>
|
|
257
|
+
<h1>Big Text</h1>
|
|
258
|
+
</body>
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
To load stylesheets from the filesystem, use the `file://` scheme.
|
|
262
|
+
Standard URL parsing reads `file://styles/email/` as host `styles` and path `/email/`.
|
|
263
|
+
`css-inline` deviates from the standard here: for a `file://` URL with a host, it drops the `file://` prefix and treats `styles/email/` as a path relative to the current directory.
|
|
264
|
+
With either form, `<link href="main.css">` resolves against the base, so keep the trailing slash: without it, `main.css` replaces the last segment (`styles/main.css`).
|
|
265
|
+
|
|
266
|
+
```python
|
|
267
|
+
import css_inline
|
|
268
|
+
|
|
269
|
+
# Absolute directory: three slashes
|
|
270
|
+
inliner = css_inline.CSSInliner(base_url="file:///srv/app/styles/email/")
|
|
271
|
+
inliner.inline("...")
|
|
272
|
+
|
|
273
|
+
# Relative to the current directory: two slashes
|
|
274
|
+
inliner = css_inline.CSSInliner(base_url="file://styles/email/")
|
|
275
|
+
inliner.inline("...")
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
To avoid repeated network requests, cache external stylesheets:
|
|
279
|
+
|
|
280
|
+
```python
|
|
281
|
+
import css_inline
|
|
282
|
+
|
|
283
|
+
inliner = css_inline.CSSInliner(
|
|
284
|
+
cache=css_inline.StylesheetCache(size=5)
|
|
285
|
+
)
|
|
286
|
+
inliner.inline("...")
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
`size` is the maximum number of stylesheets in the cache and must be greater than zero; otherwise `StylesheetCache` raises `InlineError`. When it is full, the least recently used entry is evicted. Caching is off by default.
|
|
290
|
+
|
|
291
|
+
### Errors
|
|
292
|
+
|
|
293
|
+
Inlining raises `css_inline.InlineError`, a `ValueError` subclass, when a stylesheet fails to load (missing file, network error) or to parse.
|
|
294
|
+
An invalid `base_url` raises a plain `ValueError`.
|
|
295
|
+
|
|
296
|
+
```python
|
|
297
|
+
import css_inline
|
|
298
|
+
|
|
299
|
+
try:
|
|
300
|
+
css_inline.inline('<link href="missing.css" rel="stylesheet">')
|
|
301
|
+
except css_inline.InlineError as exc:
|
|
302
|
+
print(exc) # Missing stylesheet file: missing.css
|
|
303
|
+
```
|
|
304
|
+
|
|
305
|
+
## XHTML compatibility
|
|
306
|
+
|
|
307
|
+
`css-inline` outputs HTML5, so void tags stay unclosed (`<hr>`, not `<hr/>`). For XHTML output, re-serialize with `lxml`:
|
|
308
|
+
|
|
309
|
+
```python
|
|
310
|
+
import css_inline
|
|
311
|
+
from lxml import html, etree
|
|
312
|
+
|
|
313
|
+
document = "..." # Your HTML document
|
|
314
|
+
inlined = css_inline.inline(document)
|
|
315
|
+
tree = html.fromstring(inlined)
|
|
316
|
+
inlined = etree.tostring(tree).decode(encoding="utf-8")
|
|
317
|
+
```
|
|
318
|
+
|
|
319
|
+
## Performance
|
|
320
|
+
|
|
321
|
+
`css-inline` is 14x to 680x faster than the next fastest Python inliner, `premailer`, on the inputs below:
|
|
322
|
+
|
|
323
|
+
| | Size | `css_inline 0.22.0` | `premailer 3.10.0` | `toronado 0.1.0` | `pynliner 0.8.0` |
|
|
324
|
+
|-------------|---------|---------------------|------------------------|-------------------------|-------------------------|
|
|
325
|
+
| Basic | 230 B | 4.19 µs | 90.77 µs (**21.66x**) | 538.65 µs (**128.56x**) | 951.89 µs (**227.18x**) |
|
|
326
|
+
| Realistic-1 | 8.58 KB | 78.15 µs | 1.12 ms (**14.32x**) | 12.25 ms (**156.77x**) | 12.92 ms (**165.31x**) |
|
|
327
|
+
| Realistic-2 | 4.30 KB | 42.70 µs | 1.47 ms (**34.38x**) | ERROR | ERROR |
|
|
328
|
+
| GitHub page | 1.81 MB | 16.79 ms | 11.42 s (**680.16x**) | ERROR | ERROR |
|
|
329
|
+
|
|
330
|
+
`toronado` and `pynliner` fail on Realistic-2 and the GitHub page because those inputs contain CSS they do not support: `toronado` raises `ExpressionError: Pseudo-elements are not supported.`, `pynliner` raises `Exception: No match was found. We're done or something is broken`.
|
|
331
|
+
`inlinestyler` 0.2.5 is not in the table: it crashes with current `lxml` (`'CSSSelector' object has no attribute 'evaluate'`).
|
|
332
|
+
|
|
333
|
+
Inputs live in [`benchmarks/benchmarks.json`](https://github.com/Stranger6667/css-inline/blob/master/benchmarks/benchmarks.json): Basic is `simple`, Realistic-1 is `big_email_1`, Realistic-2 is `big_email_2`, GitHub page is `big_page`.
|
|
334
|
+
Benchmark code: [`benches/bench.py`](https://github.com/Stranger6667/css-inline/blob/master/bindings/python/benches/bench.py) (`pytest-benchmark`, mean time). Environment: `rustc 1.99` (stable), Python `3.14.5`, Ryzen 9 9950X.
|
|
335
|
+
|
|
336
|
+
## Comparison with other libraries
|
|
337
|
+
|
|
338
|
+
Compared to other Python inliners, `css-inline`:
|
|
339
|
+
|
|
340
|
+
- Supports more CSS features. For example, `toronado` and `pynliner` don't support pseudo-elements.
|
|
341
|
+
- Has fewer configuration options than `premailer`.
|
|
342
|
+
- Has no debug logs.
|
|
343
|
+
- Supports only HTML5.
|
|
344
|
+
|
|
345
|
+
## Further reading
|
|
346
|
+
|
|
347
|
+
How the library works internally:
|
|
348
|
+
|
|
349
|
+
- [Rust crate](https://dygalo.dev/blog/rust-for-a-pythonista-2/)
|
|
350
|
+
- [Python bindings](https://dygalo.dev/blog/rust-for-a-pythonista-3/)
|
|
351
|
+
|
|
352
|
+
## License
|
|
353
|
+
|
|
354
|
+
This project is licensed under the terms of the [MIT license](https://opensource.org/licenses/MIT).
|
|
355
|
+
|
|
@@ -0,0 +1,327 @@
|
|
|
1
|
+
# css_inline
|
|
2
|
+
|
|
3
|
+
[<img alt="build status" src="https://img.shields.io/github/actions/workflow/status/Stranger6667/css-inline/build.yml?style=flat-square&labelColor=555555&logo=github" height="20">](https://github.com/Stranger6667/css-inline/actions/workflows/build.yml)
|
|
4
|
+
[<img alt="pypi" src="https://img.shields.io/pypi/v/css_inline.svg?style=flat-square" height="20">](https://pypi.org/project/css_inline/)
|
|
5
|
+
[<img alt="versions" src="https://img.shields.io/pypi/pyversions/css_inline.svg?style=flat-square" height="20">](https://pypi.org/project/css_inline/)
|
|
6
|
+
[<img alt="license" src="https://img.shields.io/pypi/l/css_inline.svg?style=flat-square" height="20">](https://opensource.org/licenses/MIT)
|
|
7
|
+
[<img alt="codecov.io" src="https://img.shields.io/codecov/c/gh/Stranger6667/css-inline?logo=codecov&style=flat-square&token=tOzvV4kDY0" height="20">](https://app.codecov.io/github/Stranger6667/css-inline)
|
|
8
|
+
[<img alt="gitter" src="https://img.shields.io/gitter/room/Stranger6667/css-inline?style=flat-square" height="20">](https://gitter.im/Stranger6667/css-inline)
|
|
9
|
+
|
|
10
|
+
`css_inline` is a high-performance library for inlining CSS into HTML 'style' attributes.
|
|
11
|
+
|
|
12
|
+
Use it to prepare HTML emails or to embed HTML into third-party web pages.
|
|
13
|
+
|
|
14
|
+
It turns this HTML:
|
|
15
|
+
|
|
16
|
+
```html
|
|
17
|
+
<html>
|
|
18
|
+
<head>
|
|
19
|
+
<style>h1 { color:blue; }</style>
|
|
20
|
+
</head>
|
|
21
|
+
<body>
|
|
22
|
+
<h1>Big Text</h1>
|
|
23
|
+
</body>
|
|
24
|
+
</html>
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
into:
|
|
28
|
+
|
|
29
|
+
```html
|
|
30
|
+
<html><head>
|
|
31
|
+
|
|
32
|
+
</head>
|
|
33
|
+
<body>
|
|
34
|
+
<h1 style="color: blue;">Big Text</h1>
|
|
35
|
+
|
|
36
|
+
</body></html>
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
- Uses components from Mozilla's Servo project
|
|
40
|
+
- 14x to 680x faster than `premailer`, the fastest Python alternative (see [Performance](#performance))
|
|
41
|
+
- Inlines CSS from `style` and `link` tags
|
|
42
|
+
- Removes `style` and `link` tags
|
|
43
|
+
- Resolves external stylesheets (including local files)
|
|
44
|
+
- Caches external stylesheets (opt-in)
|
|
45
|
+
- Processes multiple documents in parallel
|
|
46
|
+
- Works on Linux, Windows, macOS and in the browser via Pyodide
|
|
47
|
+
- Supports HTML5 & CSS3
|
|
48
|
+
- Tested on CPython 3.10, 3.11, 3.12, 3.13, 3.14 and PyPy 3.11.
|
|
49
|
+
|
|
50
|
+
## Playground
|
|
51
|
+
|
|
52
|
+
Try `css-inline` in the WebAssembly-powered [playground](https://css-inline.org/).
|
|
53
|
+
|
|
54
|
+
## Installation
|
|
55
|
+
|
|
56
|
+
Install with `pip`:
|
|
57
|
+
|
|
58
|
+
```shell
|
|
59
|
+
pip install css_inline
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
The PyPI name is `css-inline`; `pip` accepts `css_inline` too. Import it as `css_inline`.
|
|
63
|
+
|
|
64
|
+
PyPI ships pre-compiled wheels for:
|
|
65
|
+
|
|
66
|
+
- Linux (glibc and musl): x86_64, aarch64, armv7; glibc also i686
|
|
67
|
+
- macOS: x86_64, arm64
|
|
68
|
+
- Windows: x64, x86
|
|
69
|
+
- PyPy 3.11: Linux x86_64 and aarch64, macOS x86_64
|
|
70
|
+
|
|
71
|
+
On other platforms, `pip` builds the package from source, which requires Rust 1.85 or newer.
|
|
72
|
+
|
|
73
|
+
## Usage
|
|
74
|
+
|
|
75
|
+
```python
|
|
76
|
+
import css_inline
|
|
77
|
+
|
|
78
|
+
HTML = """<html>
|
|
79
|
+
<head>
|
|
80
|
+
<style>h1 { color:blue; }</style>
|
|
81
|
+
</head>
|
|
82
|
+
<body>
|
|
83
|
+
<h1>Big Text</h1>
|
|
84
|
+
</body>
|
|
85
|
+
</html>"""
|
|
86
|
+
|
|
87
|
+
inlined = css_inline.inline(HTML)
|
|
88
|
+
# <html><head>
|
|
89
|
+
#
|
|
90
|
+
# </head>
|
|
91
|
+
# <body>
|
|
92
|
+
# <h1 style="color: blue;">Big Text</h1>
|
|
93
|
+
#
|
|
94
|
+
# </body></html>
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
`inline` adds missing `html`, `head` and `body` tags, so the output is a complete HTML document.
|
|
98
|
+
|
|
99
|
+
To inline CSS into an HTML fragment, use `inline_fragment(html, css)`. Both arguments are required; `css` holds the stylesheet to apply. The output stays a fragment: no `<html>`, `<head>` or `<body>` wrapper. Use `inline` for full documents:
|
|
100
|
+
|
|
101
|
+
```python
|
|
102
|
+
FRAGMENT = """<main>
|
|
103
|
+
<h1>Hello</h1>
|
|
104
|
+
<section>
|
|
105
|
+
<p>who am i</p>
|
|
106
|
+
</section>
|
|
107
|
+
</main>"""
|
|
108
|
+
|
|
109
|
+
CSS = """
|
|
110
|
+
p {
|
|
111
|
+
color: red;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
h1 {
|
|
115
|
+
color: blue;
|
|
116
|
+
}
|
|
117
|
+
"""
|
|
118
|
+
|
|
119
|
+
inlined = css_inline.inline_fragment(FRAGMENT, CSS)
|
|
120
|
+
# <main>
|
|
121
|
+
# <h1 style="color: blue;">Hello</h1>
|
|
122
|
+
# <section>
|
|
123
|
+
# <p style="color: red;">who am i</p>
|
|
124
|
+
# </section>
|
|
125
|
+
# </main>
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
To inline many documents at once, use `inline_many` or `inline_many_fragments`.
|
|
129
|
+
Both process inputs in parallel and return a list of strings in input order.
|
|
130
|
+
`inline_many_fragments` pairs `htmls[i]` with `css[i]`.
|
|
131
|
+
|
|
132
|
+
**Warning**: if the lists differ in length, `inline_many_fragments` silently drops the extra items from the longer list.
|
|
133
|
+
`CSSInliner` has the same two methods.
|
|
134
|
+
|
|
135
|
+
```python
|
|
136
|
+
import css_inline
|
|
137
|
+
|
|
138
|
+
css_inline.inline_many(["<...>", "<...>"])
|
|
139
|
+
css_inline.inline_many_fragments(["<p>a</p>", "<p>b</p>"], ["p { color: red; }", "p { color: blue; }"])
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
`inline_many` processes inputs in parallel on a Rust thread pool ([Rayon](https://github.com/rayon-rs/rayon)), so you don't need `multiprocessing`.
|
|
143
|
+
The speedup scales with the number of CPU cores.
|
|
144
|
+
|
|
145
|
+
### Configuration
|
|
146
|
+
|
|
147
|
+
Pass options to the `CSSInliner` class:
|
|
148
|
+
|
|
149
|
+
```python
|
|
150
|
+
import css_inline
|
|
151
|
+
|
|
152
|
+
inliner = css_inline.CSSInliner(keep_style_tags=True)
|
|
153
|
+
inliner.inline("...")
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
- `inline_style_tags`. Inline CSS from `style` tags. Default: `True`
|
|
157
|
+
- `keep_style_tags`. Keep `style` tags after inlining. Default: `False`
|
|
158
|
+
- `keep_link_tags`. Keep `link` tags after inlining. Default: `False`
|
|
159
|
+
- `keep_at_rules`. Keep at-rules (starting with `@`) after inlining. Default: `False`
|
|
160
|
+
- `minify_css`. Remove trailing semicolons and spaces between properties and values. Default: `False`
|
|
161
|
+
- `base_url`. Base URL for resolving relative URLs. Use the `file://` scheme to load stylesheets from the filesystem. Default: `None`
|
|
162
|
+
- `load_remote_stylesheets`. Load stylesheets from `link` tags, over the network or from `file://` paths. Default: `True`
|
|
163
|
+
- `cache`. Cache for external stylesheets, for example `StylesheetCache(size=5)`. Default: `None`
|
|
164
|
+
- `extra_css`. Extra CSS to inline. Default: `None`
|
|
165
|
+
- `preallocate_node_capacity`. **Advanced**. Number of HTML nodes to preallocate during parsing. Set it to your expected node count to avoid reallocations. Default: `32`
|
|
166
|
+
- `remove_inlined_selectors`. Remove inlined selectors from `<style>` blocks. Default: `False`
|
|
167
|
+
- `apply_width_attributes`. Add `width` HTML attributes from CSS `width` properties on supported elements (`table`, `td`, `th`, `img`). Default: `False`
|
|
168
|
+
- `apply_height_attributes`. Add `height` HTML attributes from CSS `height` properties on supported elements (`table`, `td`, `th`, `img`). Default: `False`
|
|
169
|
+
|
|
170
|
+
To skip CSS inlining for an HTML tag, add the `data-css-inline="ignore"` attribute:
|
|
171
|
+
|
|
172
|
+
```html
|
|
173
|
+
<head>
|
|
174
|
+
<style>h1 { color:blue; }</style>
|
|
175
|
+
</head>
|
|
176
|
+
<body>
|
|
177
|
+
<!-- The tag below won't receive additional styles -->
|
|
178
|
+
<h1 data-css-inline="ignore">Big Text</h1>
|
|
179
|
+
</body>
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
The same attribute on a `link` or `style` tag makes the inliner skip that stylesheet:
|
|
183
|
+
|
|
184
|
+
```html
|
|
185
|
+
<head>
|
|
186
|
+
<!-- Styles below are ignored -->
|
|
187
|
+
<style data-css-inline="ignore">h1 { color:blue; }</style>
|
|
188
|
+
</head>
|
|
189
|
+
<body>
|
|
190
|
+
<h1>Big Text</h1>
|
|
191
|
+
</body>
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
To keep a `style` tag in the output, add `data-css-inline="keep"`.
|
|
195
|
+
Use it for `@media` queries in responsive emails.
|
|
196
|
+
The tag stays even with `keep_style_tags=False`.
|
|
197
|
+
|
|
198
|
+
```html
|
|
199
|
+
<head>
|
|
200
|
+
<!-- Styles below are not removed -->
|
|
201
|
+
<style data-css-inline="keep">h1 { color:blue; }</style>
|
|
202
|
+
</head>
|
|
203
|
+
<body>
|
|
204
|
+
<h1>Big Text</h1>
|
|
205
|
+
</body>
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
At-rules can't go into `style` attributes, so the inliner removes them by default.
|
|
209
|
+
Set `keep_at_rules=True` to keep them, for example `@media` queries for responsive emails, in a `style` tag while inlining everything else.
|
|
210
|
+
The tag stays even with `keep_style_tags=False`.
|
|
211
|
+
|
|
212
|
+
```html
|
|
213
|
+
<head>
|
|
214
|
+
<!-- With keep_at_rules=True, "color: blue" goes into <h1> and @media stays in <style> -->
|
|
215
|
+
<style>h1 { color: blue; } @media (max-width: 600px) { h1 { font-size: 18px; } }</style>
|
|
216
|
+
</head>
|
|
217
|
+
<body>
|
|
218
|
+
<h1>Big Text</h1>
|
|
219
|
+
</body>
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
`minify_css=True` removes trailing semicolons and spaces between properties and values in inlined styles.
|
|
223
|
+
|
|
224
|
+
```html
|
|
225
|
+
<head>
|
|
226
|
+
<!-- With minify_css=True, the <h1> will have `style="color:blue;font-weight:bold"` -->
|
|
227
|
+
<style>h1 { color: blue; font-weight: bold; }</style>
|
|
228
|
+
</head>
|
|
229
|
+
<body>
|
|
230
|
+
<h1>Big Text</h1>
|
|
231
|
+
</body>
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
To load stylesheets from the filesystem, use the `file://` scheme.
|
|
235
|
+
Standard URL parsing reads `file://styles/email/` as host `styles` and path `/email/`.
|
|
236
|
+
`css-inline` deviates from the standard here: for a `file://` URL with a host, it drops the `file://` prefix and treats `styles/email/` as a path relative to the current directory.
|
|
237
|
+
With either form, `<link href="main.css">` resolves against the base, so keep the trailing slash: without it, `main.css` replaces the last segment (`styles/main.css`).
|
|
238
|
+
|
|
239
|
+
```python
|
|
240
|
+
import css_inline
|
|
241
|
+
|
|
242
|
+
# Absolute directory: three slashes
|
|
243
|
+
inliner = css_inline.CSSInliner(base_url="file:///srv/app/styles/email/")
|
|
244
|
+
inliner.inline("...")
|
|
245
|
+
|
|
246
|
+
# Relative to the current directory: two slashes
|
|
247
|
+
inliner = css_inline.CSSInliner(base_url="file://styles/email/")
|
|
248
|
+
inliner.inline("...")
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
To avoid repeated network requests, cache external stylesheets:
|
|
252
|
+
|
|
253
|
+
```python
|
|
254
|
+
import css_inline
|
|
255
|
+
|
|
256
|
+
inliner = css_inline.CSSInliner(
|
|
257
|
+
cache=css_inline.StylesheetCache(size=5)
|
|
258
|
+
)
|
|
259
|
+
inliner.inline("...")
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
`size` is the maximum number of stylesheets in the cache and must be greater than zero; otherwise `StylesheetCache` raises `InlineError`. When it is full, the least recently used entry is evicted. Caching is off by default.
|
|
263
|
+
|
|
264
|
+
### Errors
|
|
265
|
+
|
|
266
|
+
Inlining raises `css_inline.InlineError`, a `ValueError` subclass, when a stylesheet fails to load (missing file, network error) or to parse.
|
|
267
|
+
An invalid `base_url` raises a plain `ValueError`.
|
|
268
|
+
|
|
269
|
+
```python
|
|
270
|
+
import css_inline
|
|
271
|
+
|
|
272
|
+
try:
|
|
273
|
+
css_inline.inline('<link href="missing.css" rel="stylesheet">')
|
|
274
|
+
except css_inline.InlineError as exc:
|
|
275
|
+
print(exc) # Missing stylesheet file: missing.css
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
## XHTML compatibility
|
|
279
|
+
|
|
280
|
+
`css-inline` outputs HTML5, so void tags stay unclosed (`<hr>`, not `<hr/>`). For XHTML output, re-serialize with `lxml`:
|
|
281
|
+
|
|
282
|
+
```python
|
|
283
|
+
import css_inline
|
|
284
|
+
from lxml import html, etree
|
|
285
|
+
|
|
286
|
+
document = "..." # Your HTML document
|
|
287
|
+
inlined = css_inline.inline(document)
|
|
288
|
+
tree = html.fromstring(inlined)
|
|
289
|
+
inlined = etree.tostring(tree).decode(encoding="utf-8")
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
## Performance
|
|
293
|
+
|
|
294
|
+
`css-inline` is 14x to 680x faster than the next fastest Python inliner, `premailer`, on the inputs below:
|
|
295
|
+
|
|
296
|
+
| | Size | `css_inline 0.22.0` | `premailer 3.10.0` | `toronado 0.1.0` | `pynliner 0.8.0` |
|
|
297
|
+
|-------------|---------|---------------------|------------------------|-------------------------|-------------------------|
|
|
298
|
+
| Basic | 230 B | 4.19 µs | 90.77 µs (**21.66x**) | 538.65 µs (**128.56x**) | 951.89 µs (**227.18x**) |
|
|
299
|
+
| Realistic-1 | 8.58 KB | 78.15 µs | 1.12 ms (**14.32x**) | 12.25 ms (**156.77x**) | 12.92 ms (**165.31x**) |
|
|
300
|
+
| Realistic-2 | 4.30 KB | 42.70 µs | 1.47 ms (**34.38x**) | ERROR | ERROR |
|
|
301
|
+
| GitHub page | 1.81 MB | 16.79 ms | 11.42 s (**680.16x**) | ERROR | ERROR |
|
|
302
|
+
|
|
303
|
+
`toronado` and `pynliner` fail on Realistic-2 and the GitHub page because those inputs contain CSS they do not support: `toronado` raises `ExpressionError: Pseudo-elements are not supported.`, `pynliner` raises `Exception: No match was found. We're done or something is broken`.
|
|
304
|
+
`inlinestyler` 0.2.5 is not in the table: it crashes with current `lxml` (`'CSSSelector' object has no attribute 'evaluate'`).
|
|
305
|
+
|
|
306
|
+
Inputs live in [`benchmarks/benchmarks.json`](https://github.com/Stranger6667/css-inline/blob/master/benchmarks/benchmarks.json): Basic is `simple`, Realistic-1 is `big_email_1`, Realistic-2 is `big_email_2`, GitHub page is `big_page`.
|
|
307
|
+
Benchmark code: [`benches/bench.py`](https://github.com/Stranger6667/css-inline/blob/master/bindings/python/benches/bench.py) (`pytest-benchmark`, mean time). Environment: `rustc 1.99` (stable), Python `3.14.5`, Ryzen 9 9950X.
|
|
308
|
+
|
|
309
|
+
## Comparison with other libraries
|
|
310
|
+
|
|
311
|
+
Compared to other Python inliners, `css-inline`:
|
|
312
|
+
|
|
313
|
+
- Supports more CSS features. For example, `toronado` and `pynliner` don't support pseudo-elements.
|
|
314
|
+
- Has fewer configuration options than `premailer`.
|
|
315
|
+
- Has no debug logs.
|
|
316
|
+
- Supports only HTML5.
|
|
317
|
+
|
|
318
|
+
## Further reading
|
|
319
|
+
|
|
320
|
+
How the library works internally:
|
|
321
|
+
|
|
322
|
+
- [Rust crate](https://dygalo.dev/blog/rust-for-a-pythonista-2/)
|
|
323
|
+
- [Python bindings](https://dygalo.dev/blog/rust-for-a-pythonista-3/)
|
|
324
|
+
|
|
325
|
+
## License
|
|
326
|
+
|
|
327
|
+
This project is licensed under the terms of the [MIT license](https://opensource.org/licenses/MIT).
|