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.
Files changed (38) hide show
  1. css_inline-0.22.1/PKG-INFO +355 -0
  2. css_inline-0.22.1/README.md +327 -0
  3. {css_inline-0.22.0 → css_inline-0.22.1}/bindings/python/CHANGELOG.md +8 -1
  4. {css_inline-0.22.0 → css_inline-0.22.1}/bindings/python/Cargo.lock +37 -38
  5. {css_inline-0.22.0 → css_inline-0.22.1}/bindings/python/Cargo.toml +1 -1
  6. css_inline-0.22.1/bindings/python/README.md +327 -0
  7. {css_inline-0.22.0 → css_inline-0.22.1}/css-inline/Cargo.toml +1 -1
  8. css_inline-0.22.1/css-inline/README.md +377 -0
  9. {css_inline-0.22.0 → css_inline-0.22.1}/css-inline/src/lib.rs +54 -35
  10. {css_inline-0.22.0 → css_inline-0.22.1}/css-inline/src/parser.rs +3 -4
  11. css_inline-0.22.0/PKG-INFO +0 -331
  12. css_inline-0.22.0/README.md +0 -303
  13. css_inline-0.22.0/bindings/python/README.md +0 -303
  14. css_inline-0.22.0/css-inline/README.md +0 -322
  15. {css_inline-0.22.0 → css_inline-0.22.1}/LICENSE +0 -0
  16. {css_inline-0.22.0 → css_inline-0.22.1}/bindings/python/build.rs +0 -0
  17. {css_inline-0.22.0 → css_inline-0.22.1}/bindings/python/src/lib.rs +0 -0
  18. {css_inline-0.22.0 → css_inline-0.22.1}/css-inline/benches/inliner.rs +0 -0
  19. {css_inline-0.22.0 → css_inline-0.22.1}/css-inline/rustfmt.toml +0 -0
  20. {css_inline-0.22.0 → css_inline-0.22.1}/css-inline/src/error.rs +0 -0
  21. {css_inline-0.22.0 → css_inline-0.22.1}/css-inline/src/html/attributes.rs +0 -0
  22. {css_inline-0.22.0 → css_inline-0.22.1}/css-inline/src/html/document.rs +0 -0
  23. {css_inline-0.22.0 → css_inline-0.22.1}/css-inline/src/html/element.rs +0 -0
  24. {css_inline-0.22.0 → css_inline-0.22.1}/css-inline/src/html/iter.rs +0 -0
  25. {css_inline-0.22.0 → css_inline-0.22.1}/css-inline/src/html/mod.rs +0 -0
  26. {css_inline-0.22.0 → css_inline-0.22.1}/css-inline/src/html/node.rs +0 -0
  27. {css_inline-0.22.0 → css_inline-0.22.1}/css-inline/src/html/parser.rs +0 -0
  28. {css_inline-0.22.0 → css_inline-0.22.1}/css-inline/src/html/selectors/attr_value.rs +0 -0
  29. {css_inline-0.22.0 → css_inline-0.22.1}/css-inline/src/html/selectors/local_name.rs +0 -0
  30. {css_inline-0.22.0 → css_inline-0.22.1}/css-inline/src/html/selectors/mod.rs +0 -0
  31. {css_inline-0.22.0 → css_inline-0.22.1}/css-inline/src/html/selectors/parser.rs +0 -0
  32. {css_inline-0.22.0 → css_inline-0.22.1}/css-inline/src/html/selectors/pseudo_classes.rs +0 -0
  33. {css_inline-0.22.0 → css_inline-0.22.1}/css-inline/src/html/selectors/pseudo_elements.rs +0 -0
  34. {css_inline-0.22.0 → css_inline-0.22.1}/css-inline/src/html/selectors/selector_impl.rs +0 -0
  35. {css_inline-0.22.0 → css_inline-0.22.1}/css-inline/src/html/serializer.rs +0 -0
  36. {css_inline-0.22.0 → css_inline-0.22.1}/css-inline/src/main.rs +0 -0
  37. {css_inline-0.22.0 → css_inline-0.22.1}/css-inline/src/resolver.rs +0 -0
  38. {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).