diff --git a/CHANGELOG.md b/CHANGELOG.md index 4688d7d9..cdf15f22 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ ### Added - A way to customize resolving remote stylesheets. +- Non-blocking stylesheet resolving by default. ### Changed diff --git a/README.md b/README.md index 10325964..746db8c7 100644 --- a/README.md +++ b/README.md @@ -70,8 +70,21 @@ const HTML: &str = r#"<html> </body> </html>"#; +#[tokio::main] +async fn main() -> css_inline::Result<()> { + let inlined = css_inline::inline(HTML).await?; + // Do something with inlined HTML, e.g. send an email + Ok(()) +} +``` + +There is also a "blocking" API for inlining: + +```rust +const HTML: &str = "..."; + fn main() -> css_inline::Result<()> { - let inlined = css_inline::inline(HTML)?; + let inlined = css_inline::blocking::inline(HTML)?; // Do something with inlined HTML, e.g. send an email Ok(()) } @@ -84,11 +97,12 @@ fn main() -> css_inline::Result<()> { ```rust const HTML: &str = "..."; -fn main() -> css_inline::Result<()> { +#[tokio::main] +async fn main() -> css_inline::Result<()> { let inliner = css_inline::CSSInliner::options() .load_remote_stylesheets(false) .build(); - let inlined = inliner.inline(HTML)?; + let inlined = inliner.inline(HTML).await?; // Do something with inlined HTML, e.g. send an email Ok(()) } @@ -131,12 +145,28 @@ If you'd like to load stylesheets from your filesystem, use the `file://` scheme ```rust const HTML: &str = "..."; -fn main() -> css_inline::Result<()> { +#[tokio::main] +async fn main() -> css_inline::Result<()> { let base_url = css_inline::Url::parse("file://styles/email/").expect("Invalid URL"); let inliner = css_inline::CSSInliner::options() .base_url(Some(base_url)) .build(); - let inlined = inliner.inline(HTML); + let inlined = inliner.inline(HTML).await?; + // Do something with inlined HTML, e.g. send an email + Ok(()) +} +``` + +The blocking version is available as well: + +```rust +const HTML: &str = "..."; + +fn main() -> css_inline::Result<()> { + let inliner = css_inline::blocking::CSSInliner::options() + .load_remote_stylesheets(false) + .build(); + let inlined = inliner.inline(HTML)?; // Do something with inlined HTML, e.g. send an email Ok(()) } @@ -149,7 +179,7 @@ For resolving remote stylesheets it is possible to implement a custom resolver: pub struct CustomStylesheetResolver; impl css_inline::StylesheetResolver for CustomStylesheetResolver { - fn retrieve(&self, location: &str) -> css_inline::Result<String> { + fn retrieve_blocking(&self, location: &str) -> css_inline::Result<String> { Err(self.unsupported("External stylesheets are not supported")) } } diff --git a/bindings/c/Cargo.toml b/bindings/c/Cargo.toml index 4d012086..f087d814 100644 --- a/bindings/c/Cargo.toml +++ b/bindings/c/Cargo.toml @@ -18,4 +18,4 @@ cbindgen = "0.26" path = "../../css-inline" version = "*" default-features = false -features = ["http", "file"] +features = ["http-blocking", "file"] diff --git a/bindings/c/src/lib.rs b/bindings/c/src/lib.rs index b40a5fde..490fbc60 100644 --- a/bindings/c/src/lib.rs +++ b/bindings/c/src/lib.rs @@ -1,4 +1,7 @@ -use css_inline::{CSSInliner, DefaultStylesheetResolver, InlineError, InlineOptions, Url}; +use css_inline::{ + blocking::{CSSInliner, InlineOptions}, + DefaultStylesheetResolver, InlineError, Url, +}; use libc::{c_char, size_t}; use std::{borrow::Cow, cmp, ffi::CStr, io::Write, ptr, sync::Arc}; diff --git a/bindings/javascript/Cargo.toml b/bindings/javascript/Cargo.toml index fbb0e945..fa109511 100644 --- a/bindings/javascript/Cargo.toml +++ b/bindings/javascript/Cargo.toml @@ -32,7 +32,7 @@ serde = { version = "1", features = ["derive"], default-features = false } path = "../../css-inline" version = "*" default-features = false -features = ["http", "file"] +features = ["http-blocking", "file"] [target.'cfg(target_arch = "wasm32")'.dependencies.css-inline] path = "../../css-inline" diff --git a/bindings/javascript/src/lib.rs b/bindings/javascript/src/lib.rs index d96e95d8..987853ea 100644 --- a/bindings/javascript/src/lib.rs +++ b/bindings/javascript/src/lib.rs @@ -60,6 +60,6 @@ export function inline(html: string, options?: InlineOptions): string; export function version(): string;"#; fn inline_inner(html: String, options: Options) -> std::result::Result<String, errors::JsError> { - let inliner = css_inline::CSSInliner::new(options.try_into()?); + let inliner = css_inline::blocking::CSSInliner::new(options.try_into()?); Ok(inliner.inline(&html).map_err(errors::InlineError)?) } diff --git a/bindings/javascript/src/options.rs b/bindings/javascript/src/options.rs index 2691df49..a07c57a9 100644 --- a/bindings/javascript/src/options.rs +++ b/bindings/javascript/src/options.rs @@ -42,11 +42,11 @@ pub struct Options { pub preallocate_node_capacity: Option<u32>, } -impl TryFrom<Options> for css_inline::InlineOptions<'_> { +impl TryFrom<Options> for css_inline::blocking::InlineOptions<'_> { type Error = JsError; fn try_from(value: Options) -> std::result::Result<Self, Self::Error> { - Ok(css_inline::InlineOptions { + Ok(css_inline::blocking::InlineOptions { inline_style_tags: value.inline_style_tags.unwrap_or(true), keep_style_tags: value.keep_style_tags.unwrap_or(false), keep_link_tags: value.keep_link_tags.unwrap_or(false), @@ -75,7 +75,7 @@ impl TryFrom<Options> for css_inline::InlineOptions<'_> { pub struct UnsupportedResolver; impl css_inline::StylesheetResolver for UnsupportedResolver { - fn retrieve(&self, location: &str) -> css_inline::Result<String> { + fn retrieve_blocking(&self, location: &str) -> css_inline::Result<String> { let message = if location.starts_with("https") | location.starts_with("http") { diff --git a/bindings/javascript/wasm/index.js b/bindings/javascript/wasm/index.js index a1d5a90c..5e3c0a2e 100644 --- a/bindings/javascript/wasm/index.js +++ b/bindings/javascript/wasm/index.js @@ -33,6 +33,18 @@ heap.push(void 0, null, true, false); function getObject(idx) { return heap[idx]; } +var heap_next = heap.length; +function dropObject(idx) { + if (idx < 132) + return; + heap[idx] = heap_next; + heap_next = idx; +} +function takeObject(idx) { + const ret = getObject(idx); + dropObject(idx); + return ret; +} var WASM_VECTOR_LEN = 0; var cachedUint8Memory0 = null; function getUint8Memory0() { @@ -104,7 +116,6 @@ function getStringFromWasm0(ptr, len) { ptr = ptr >>> 0; return cachedTextDecoder.decode(getUint8Memory0().subarray(ptr, ptr + len)); } -var heap_next = heap.length; function addHeapObject(obj) { if (heap_next === heap.length) heap.push(heap.length + 1); @@ -113,17 +124,6 @@ function addHeapObject(obj) { heap[idx] = obj; return idx; } -function dropObject(idx) { - if (idx < 132) - return; - heap[idx] = heap_next; - heap_next = idx; -} -function takeObject(idx) { - const ret = getObject(idx); - dropObject(idx); - return ret; -} var cachedFloat64Memory0 = null; function getFloat64Memory0() { if (cachedFloat64Memory0 === null || cachedFloat64Memory0.byteLength === 0) { @@ -261,6 +261,9 @@ function __wbg_get_imports() { const ret = getObject(arg0) === void 0; return ret; }; + imports.wbg.__wbindgen_object_drop_ref = function(arg0) { + takeObject(arg0); + }; imports.wbg.__wbindgen_string_get = function(arg0, arg1) { const obj = getObject(arg1); const ret = typeof obj === "string" ? obj : void 0; @@ -303,9 +306,6 @@ function __wbg_get_imports() { const ret = +getObject(arg0); return ret; }; - imports.wbg.__wbindgen_object_drop_ref = function(arg0) { - takeObject(arg0); - }; imports.wbg.__wbg_length_1d25fa9e4ac21ce7 = function(arg0) { const ret = getObject(arg0).length; return ret; diff --git a/bindings/javascript/wasm/index.min.js b/bindings/javascript/wasm/index.min.js index eaf8dad1..a440b878 100644 --- a/bindings/javascript/wasm/index.min.js +++ b/bindings/javascript/wasm/index.min.js @@ -1,2 +1,2 @@ -"use strict";var cssInline=(()=>{var S=Object.defineProperty;var $=Object.getOwnPropertyDescriptor;var D=Object.getOwnPropertyNames;var N=Object.prototype.hasOwnProperty;var B=(e,n)=>{for(var t in n)S(e,t,{get:n[t],enumerable:!0})},q=(e,n,t,r)=>{if(n&&typeof n=="object"||typeof n=="function")for(let i of D(n))!N.call(e,i)&&i!==t&&S(e,i,{get:()=>n[i],enumerable:!(r=$(n,i))||r.enumerable});return e};var z=e=>q(S({},"__esModule",{value:!0}),e);var Y={};B(Y,{initWasm:()=>K,inline:()=>Q,version:()=>X});var c,b=new Array(128).fill(void 0);b.push(void 0,null,!0,!1);function o(e){return b[e]}var h=0,g=null;function A(){return(g===null||g.byteLength===0)&&(g=new Uint8Array(c.memory.buffer)),g}var I=typeof TextEncoder<"u"?new TextEncoder("utf-8"):{encode:()=>{throw Error("TextEncoder not available")}},C=typeof I.encodeInto=="function"?function(e,n){return I.encodeInto(e,n)}:function(e,n){let t=I.encode(e);return n.set(t),{read:e.length,written:t.length}};function W(e,n,t){if(t===void 0){let _=I.encode(e),a=n(_.length,1)>>>0;return A().subarray(a,a+_.length).set(_),h=_.length,a}let r=e.length,i=n(r,1)>>>0,f=A(),s=0;for(;s<r;s++){let _=e.charCodeAt(s);if(_>127)break;f[i+s]=_}if(s!==r){s!==0&&(e=e.slice(s)),i=t(i,r,r=s+e.length*3,1)>>>0;let _=A().subarray(i+s,i+r),a=C(e,_);s+=a.written}return h=s,i}function j(e){return e==null}var w=null;function u(){return(w===null||w.byteLength===0)&&(w=new Int32Array(c.memory.buffer)),w}var M=typeof TextDecoder<"u"?new TextDecoder("utf-8",{ignoreBOM:!0,fatal:!0}):{decode:()=>{throw Error("TextDecoder not available")}};typeof TextDecoder<"u"&&M.decode();function m(e,n){return e=e>>>0,M.decode(A().subarray(e,e+n))}var p=b.length;function l(e){p===b.length&&b.push(b.length+1);let n=p;return p=b[n],b[n]=e,n}function P(e){e<132||(b[e]=p,p=e)}function O(e){let n=o(e);return P(e),n}var y=null;function H(){return(y===null||y.byteLength===0)&&(y=new Float64Array(c.memory.buffer)),y}function E(e){let n=typeof e;if(n=="number"||n=="boolean"||e==null)return`${e}`;if(n=="string")return`"${e}"`;if(n=="symbol"){let i=e.description;return i==null?"Symbol":`Symbol(${i})`}if(n=="function"){let i=e.name;return typeof i=="string"&&i.length>0?`Function(${i})`:"Function"}if(Array.isArray(e)){let i=e.length,f="[";i>0&&(f+=E(e[0]));for(let s=1;s<i;s++)f+=", "+E(e[s]);return f+="]",f}let t=/\[object ([^\]]+)\]/.exec(toString.call(e)),r;if(t.length>1)r=t[1];else return toString.call(e);if(r=="Object")try{return"Object("+JSON.stringify(e)+")"}catch{return"Object"}return e instanceof Error?`${e.name}: ${e.message} -${e.stack}`:r}function T(e,n){let t,r;try{let d=c.__wbindgen_add_to_stack_pointer(-16),L=W(e,c.__wbindgen_malloc,c.__wbindgen_realloc),R=h;c.inline(d,L,R,l(n));var i=u()[d/4+0],f=u()[d/4+1],s=u()[d/4+2],_=u()[d/4+3],a=i,x=f;if(_)throw a=0,x=0,O(s);return t=a,r=x,m(a,x)}finally{c.__wbindgen_add_to_stack_pointer(16),c.__wbindgen_free(t,r,1)}}function k(){let e,n;try{let i=c.__wbindgen_add_to_stack_pointer(-16);c.version(i);var t=u()[i/4+0],r=u()[i/4+1];return e=t,n=r,m(t,r)}finally{c.__wbindgen_add_to_stack_pointer(16),c.__wbindgen_free(e,n,1)}}async function J(e,n){if(typeof Response=="function"&&e instanceof Response){if(typeof WebAssembly.instantiateStreaming=="function")try{return await WebAssembly.instantiateStreaming(e,n)}catch(r){if(e.headers.get("Content-Type")!="application/wasm")console.warn("`WebAssembly.instantiateStreaming` failed because your server does not serve wasm with `application/wasm` MIME type. Falling back to `WebAssembly.instantiate` which is slower. Original error:\n",r);else throw r}let t=await e.arrayBuffer();return await WebAssembly.instantiate(t,n)}else{let t=await WebAssembly.instantiate(e,n);return t instanceof WebAssembly.Instance?{instance:t,module:e}:t}}function V(){let e={};return e.wbg={},e.wbg.__wbindgen_is_undefined=function(n){return o(n)===void 0},e.wbg.__wbindgen_string_get=function(n,t){let r=o(t),i=typeof r=="string"?r:void 0;var f=j(i)?0:W(i,c.__wbindgen_malloc,c.__wbindgen_realloc),s=h;u()[n/4+1]=s,u()[n/4+0]=f},e.wbg.__wbindgen_boolean_get=function(n){let t=o(n);return typeof t=="boolean"?t?1:0:2},e.wbg.__wbindgen_is_object=function(n){let t=o(n);return typeof t=="object"&&t!==null},e.wbg.__wbindgen_string_new=function(n,t){let r=m(n,t);return l(r)},e.wbg.__wbindgen_object_clone_ref=function(n){let t=o(n);return l(t)},e.wbg.__wbg_getwithrefkey_4a92a5eca60879b9=function(n,t){let r=o(n)[o(t)];return l(r)},e.wbg.__wbindgen_in=function(n,t){return o(n)in o(t)},e.wbg.__wbg_isSafeInteger_f93fde0dca9820f8=function(n){return Number.isSafeInteger(o(n))},e.wbg.__wbindgen_as_number=function(n){return+o(n)},e.wbg.__wbindgen_object_drop_ref=function(n){O(n)},e.wbg.__wbg_length_1d25fa9e4ac21ce7=function(n){return o(n).length},e.wbg.__wbindgen_memory=function(){let n=c.memory;return l(n)},e.wbg.__wbg_buffer_a448f833075b71ba=function(n){let t=o(n).buffer;return l(t)},e.wbg.__wbg_new_8f67e318f15d7254=function(n){let t=new Uint8Array(o(n));return l(t)},e.wbg.__wbg_set_2357bf09366ee480=function(n,t,r){o(n).set(o(t),r>>>0)},e.wbg.__wbindgen_error_new=function(n,t){let r=new Error(m(n,t));return l(r)},e.wbg.__wbindgen_jsval_loose_eq=function(n,t){return o(n)==o(t)},e.wbg.__wbindgen_number_get=function(n,t){let r=o(t),i=typeof r=="number"?r:void 0;H()[n/8+1]=j(i)?0:i,u()[n/4+0]=!j(i)},e.wbg.__wbg_instanceof_Uint8Array_bced6f43aed8c1aa=function(n){let t;try{t=o(n)instanceof Uint8Array}catch{t=!1}return t},e.wbg.__wbg_instanceof_ArrayBuffer_e7d53d51371448e2=function(n){let t;try{t=o(n)instanceof ArrayBuffer}catch{t=!1}return t},e.wbg.__wbindgen_debug_string=function(n,t){let r=E(o(t)),i=W(r,c.__wbindgen_malloc,c.__wbindgen_realloc),f=h;u()[n/4+1]=f,u()[n/4+0]=i},e.wbg.__wbindgen_throw=function(n,t){throw new Error(m(n,t))},e}function G(e,n){return c=e.exports,U.__wbindgen_wasm_module=n,y=null,w=null,g=null,c}async function U(e){if(c!==void 0)return c;typeof e>"u"&&(e=new URL("index_bg.wasm",void 0));let n=V();(typeof e=="string"||typeof Request=="function"&&e instanceof Request||typeof URL=="function"&&e instanceof URL)&&(e=fetch(e));let{instance:t,module:r}=await J(await e,n);return G(t,r)}var v=U;var F=!1,K=async e=>{if(F)throw new Error("Already initialized. The `initWasm()` function can be used only once.");await v(await e),F=!0};function Q(e,n){return T(e,n)}function X(){return k()}return z(Y);})(); +"use strict";var cssInline=(()=>{var S=Object.defineProperty;var $=Object.getOwnPropertyDescriptor;var D=Object.getOwnPropertyNames;var N=Object.prototype.hasOwnProperty;var B=(e,n)=>{for(var t in n)S(e,t,{get:n[t],enumerable:!0})},q=(e,n,t,r)=>{if(n&&typeof n=="object"||typeof n=="function")for(let i of D(n))!N.call(e,i)&&i!==t&&S(e,i,{get:()=>n[i],enumerable:!(r=$(n,i))||r.enumerable});return e};var z=e=>q(S({},"__esModule",{value:!0}),e);var Y={};B(Y,{initWasm:()=>K,inline:()=>Q,version:()=>X});var c,b=new Array(128).fill(void 0);b.push(void 0,null,!0,!1);function o(e){return b[e]}var m=b.length;function C(e){e<132||(b[e]=m,m=e)}function M(e){let n=o(e);return C(e),n}var h=0,g=null;function A(){return(g===null||g.byteLength===0)&&(g=new Uint8Array(c.memory.buffer)),g}var I=typeof TextEncoder<"u"?new TextEncoder("utf-8"):{encode:()=>{throw Error("TextEncoder not available")}},P=typeof I.encodeInto=="function"?function(e,n){return I.encodeInto(e,n)}:function(e,n){let t=I.encode(e);return n.set(t),{read:e.length,written:t.length}};function W(e,n,t){if(t===void 0){let _=I.encode(e),a=n(_.length,1)>>>0;return A().subarray(a,a+_.length).set(_),h=_.length,a}let r=e.length,i=n(r,1)>>>0,f=A(),s=0;for(;s<r;s++){let _=e.charCodeAt(s);if(_>127)break;f[i+s]=_}if(s!==r){s!==0&&(e=e.slice(s)),i=t(i,r,r=s+e.length*3,1)>>>0;let _=A().subarray(i+s,i+r),a=P(e,_);s+=a.written}return h=s,i}function j(e){return e==null}var w=null;function u(){return(w===null||w.byteLength===0)&&(w=new Int32Array(c.memory.buffer)),w}var O=typeof TextDecoder<"u"?new TextDecoder("utf-8",{ignoreBOM:!0,fatal:!0}):{decode:()=>{throw Error("TextDecoder not available")}};typeof TextDecoder<"u"&&O.decode();function p(e,n){return e=e>>>0,O.decode(A().subarray(e,e+n))}function l(e){m===b.length&&b.push(b.length+1);let n=m;return m=b[n],b[n]=e,n}var y=null;function H(){return(y===null||y.byteLength===0)&&(y=new Float64Array(c.memory.buffer)),y}function E(e){let n=typeof e;if(n=="number"||n=="boolean"||e==null)return`${e}`;if(n=="string")return`"${e}"`;if(n=="symbol"){let i=e.description;return i==null?"Symbol":`Symbol(${i})`}if(n=="function"){let i=e.name;return typeof i=="string"&&i.length>0?`Function(${i})`:"Function"}if(Array.isArray(e)){let i=e.length,f="[";i>0&&(f+=E(e[0]));for(let s=1;s<i;s++)f+=", "+E(e[s]);return f+="]",f}let t=/\[object ([^\]]+)\]/.exec(toString.call(e)),r;if(t.length>1)r=t[1];else return toString.call(e);if(r=="Object")try{return"Object("+JSON.stringify(e)+")"}catch{return"Object"}return e instanceof Error?`${e.name}: ${e.message} +${e.stack}`:r}function T(e,n){let t,r;try{let d=c.__wbindgen_add_to_stack_pointer(-16),L=W(e,c.__wbindgen_malloc,c.__wbindgen_realloc),R=h;c.inline(d,L,R,l(n));var i=u()[d/4+0],f=u()[d/4+1],s=u()[d/4+2],_=u()[d/4+3],a=i,x=f;if(_)throw a=0,x=0,M(s);return t=a,r=x,p(a,x)}finally{c.__wbindgen_add_to_stack_pointer(16),c.__wbindgen_free(t,r,1)}}function k(){let e,n;try{let i=c.__wbindgen_add_to_stack_pointer(-16);c.version(i);var t=u()[i/4+0],r=u()[i/4+1];return e=t,n=r,p(t,r)}finally{c.__wbindgen_add_to_stack_pointer(16),c.__wbindgen_free(e,n,1)}}async function J(e,n){if(typeof Response=="function"&&e instanceof Response){if(typeof WebAssembly.instantiateStreaming=="function")try{return await WebAssembly.instantiateStreaming(e,n)}catch(r){if(e.headers.get("Content-Type")!="application/wasm")console.warn("`WebAssembly.instantiateStreaming` failed because your server does not serve wasm with `application/wasm` MIME type. Falling back to `WebAssembly.instantiate` which is slower. Original error:\n",r);else throw r}let t=await e.arrayBuffer();return await WebAssembly.instantiate(t,n)}else{let t=await WebAssembly.instantiate(e,n);return t instanceof WebAssembly.Instance?{instance:t,module:e}:t}}function V(){let e={};return e.wbg={},e.wbg.__wbindgen_is_undefined=function(n){return o(n)===void 0},e.wbg.__wbindgen_object_drop_ref=function(n){M(n)},e.wbg.__wbindgen_string_get=function(n,t){let r=o(t),i=typeof r=="string"?r:void 0;var f=j(i)?0:W(i,c.__wbindgen_malloc,c.__wbindgen_realloc),s=h;u()[n/4+1]=s,u()[n/4+0]=f},e.wbg.__wbindgen_boolean_get=function(n){let t=o(n);return typeof t=="boolean"?t?1:0:2},e.wbg.__wbindgen_is_object=function(n){let t=o(n);return typeof t=="object"&&t!==null},e.wbg.__wbindgen_string_new=function(n,t){let r=p(n,t);return l(r)},e.wbg.__wbindgen_object_clone_ref=function(n){let t=o(n);return l(t)},e.wbg.__wbg_getwithrefkey_4a92a5eca60879b9=function(n,t){let r=o(n)[o(t)];return l(r)},e.wbg.__wbindgen_in=function(n,t){return o(n)in o(t)},e.wbg.__wbg_isSafeInteger_f93fde0dca9820f8=function(n){return Number.isSafeInteger(o(n))},e.wbg.__wbindgen_as_number=function(n){return+o(n)},e.wbg.__wbg_length_1d25fa9e4ac21ce7=function(n){return o(n).length},e.wbg.__wbindgen_memory=function(){let n=c.memory;return l(n)},e.wbg.__wbg_buffer_a448f833075b71ba=function(n){let t=o(n).buffer;return l(t)},e.wbg.__wbg_new_8f67e318f15d7254=function(n){let t=new Uint8Array(o(n));return l(t)},e.wbg.__wbg_set_2357bf09366ee480=function(n,t,r){o(n).set(o(t),r>>>0)},e.wbg.__wbindgen_error_new=function(n,t){let r=new Error(p(n,t));return l(r)},e.wbg.__wbindgen_jsval_loose_eq=function(n,t){return o(n)==o(t)},e.wbg.__wbindgen_number_get=function(n,t){let r=o(t),i=typeof r=="number"?r:void 0;H()[n/8+1]=j(i)?0:i,u()[n/4+0]=!j(i)},e.wbg.__wbg_instanceof_Uint8Array_bced6f43aed8c1aa=function(n){let t;try{t=o(n)instanceof Uint8Array}catch{t=!1}return t},e.wbg.__wbg_instanceof_ArrayBuffer_e7d53d51371448e2=function(n){let t;try{t=o(n)instanceof ArrayBuffer}catch{t=!1}return t},e.wbg.__wbindgen_debug_string=function(n,t){let r=E(o(t)),i=W(r,c.__wbindgen_malloc,c.__wbindgen_realloc),f=h;u()[n/4+1]=f,u()[n/4+0]=i},e.wbg.__wbindgen_throw=function(n,t){throw new Error(p(n,t))},e}function G(e,n){return c=e.exports,U.__wbindgen_wasm_module=n,y=null,w=null,g=null,c}async function U(e){if(c!==void 0)return c;typeof e>"u"&&(e=new URL("index_bg.wasm",void 0));let n=V();(typeof e=="string"||typeof Request=="function"&&e instanceof Request||typeof URL=="function"&&e instanceof URL)&&(e=fetch(e));let{instance:t,module:r}=await J(await e,n);return G(t,r)}var v=U;var F=!1,K=async e=>{if(F)throw new Error("Already initialized. The `initWasm()` function can be used only once.");await v(await e),F=!0};function Q(e,n){return T(e,n)}function X(){return k()}return z(Y);})(); diff --git a/bindings/javascript/wasm/index.mjs b/bindings/javascript/wasm/index.mjs index 008fbb26..6ec93e90 100644 --- a/bindings/javascript/wasm/index.mjs +++ b/bindings/javascript/wasm/index.mjs @@ -5,6 +5,18 @@ heap.push(void 0, null, true, false); function getObject(idx) { return heap[idx]; } +var heap_next = heap.length; +function dropObject(idx) { + if (idx < 132) + return; + heap[idx] = heap_next; + heap_next = idx; +} +function takeObject(idx) { + const ret = getObject(idx); + dropObject(idx); + return ret; +} var WASM_VECTOR_LEN = 0; var cachedUint8Memory0 = null; function getUint8Memory0() { @@ -76,7 +88,6 @@ function getStringFromWasm0(ptr, len) { ptr = ptr >>> 0; return cachedTextDecoder.decode(getUint8Memory0().subarray(ptr, ptr + len)); } -var heap_next = heap.length; function addHeapObject(obj) { if (heap_next === heap.length) heap.push(heap.length + 1); @@ -85,17 +96,6 @@ function addHeapObject(obj) { heap[idx] = obj; return idx; } -function dropObject(idx) { - if (idx < 132) - return; - heap[idx] = heap_next; - heap_next = idx; -} -function takeObject(idx) { - const ret = getObject(idx); - dropObject(idx); - return ret; -} var cachedFloat64Memory0 = null; function getFloat64Memory0() { if (cachedFloat64Memory0 === null || cachedFloat64Memory0.byteLength === 0) { @@ -233,6 +233,9 @@ function __wbg_get_imports() { const ret = getObject(arg0) === void 0; return ret; }; + imports.wbg.__wbindgen_object_drop_ref = function(arg0) { + takeObject(arg0); + }; imports.wbg.__wbindgen_string_get = function(arg0, arg1) { const obj = getObject(arg1); const ret = typeof obj === "string" ? obj : void 0; @@ -275,9 +278,6 @@ function __wbg_get_imports() { const ret = +getObject(arg0); return ret; }; - imports.wbg.__wbindgen_object_drop_ref = function(arg0) { - takeObject(arg0); - }; imports.wbg.__wbg_length_1d25fa9e4ac21ce7 = function(arg0) { const ret = getObject(arg0).length; return ret; diff --git a/bindings/javascript/wasm/index_bg.wasm b/bindings/javascript/wasm/index_bg.wasm index a61fbf8e..8fb999b9 100644 Binary files a/bindings/javascript/wasm/index_bg.wasm and b/bindings/javascript/wasm/index_bg.wasm differ diff --git a/bindings/python/Cargo.toml b/bindings/python/Cargo.toml index d64ceafc..98c6131e 100644 --- a/bindings/python/Cargo.toml +++ b/bindings/python/Cargo.toml @@ -23,7 +23,7 @@ url = "2" path = "../../css-inline" version = "*" default-features = false -features = ["http", "file"] +features = ["http-blocking", "file"] [profile.release] codegen-units = 1 diff --git a/bindings/python/src/lib.rs b/bindings/python/src/lib.rs index b2a86651..d7a232dd 100644 --- a/bindings/python/src/lib.rs +++ b/bindings/python/src/lib.rs @@ -74,12 +74,12 @@ fn parse_url(url: Option<String>) -> PyResult<Option<rust_inline::Url>> { /// Customizable CSS inliner. #[pyclass] struct CSSInliner { - inner: rust_inline::CSSInliner<'static>, + inner: rust_inline::blocking::CSSInliner<'static>, } macro_rules! inliner { ($inline_style_tags:expr, $keep_style_tags:expr, $keep_link_tags:expr, $base_url:expr, $load_remote_stylesheets:expr, $extra_css:expr, $preallocate_node_capacity:expr) => {{ - let options = rust_inline::InlineOptions { + rust_inline::blocking::InlineOptions { inline_style_tags: $inline_style_tags.unwrap_or(true), keep_style_tags: $keep_style_tags.unwrap_or(false), keep_link_tags: $keep_link_tags.unwrap_or(false), @@ -88,8 +88,8 @@ macro_rules! inliner { extra_css: $extra_css.map(Into::into), preallocate_node_capacity: $preallocate_node_capacity.unwrap_or(32), resolver: std::sync::Arc::new(rust_inline::DefaultStylesheetResolver), - }; - rust_inline::CSSInliner::new(options) + } + .build() }}; } @@ -198,7 +198,7 @@ fn inline_many( } fn inline_many_impl( - inliner: &rust_inline::CSSInliner<'_>, + inliner: &rust_inline::blocking::CSSInliner<'_>, htmls: &PyList, ) -> PyResult<Vec<String>> { // Extract strings from the list. It will fail if there is any non-string value diff --git a/bindings/ruby/ext/css_inline/Cargo.toml b/bindings/ruby/ext/css_inline/Cargo.toml index a71fb898..97fc8e02 100644 --- a/bindings/ruby/ext/css_inline/Cargo.toml +++ b/bindings/ruby/ext/css_inline/Cargo.toml @@ -23,4 +23,4 @@ rayon = "1" path = "../../../../css-inline" version = "*" default-features = false -features = ["http", "file"] +features = ["http-blocking", "file"] diff --git a/bindings/ruby/ext/css_inline/src/lib.rs b/bindings/ruby/ext/css_inline/src/lib.rs index f5395eba..e2ced6c9 100644 --- a/bindings/ruby/ext/css_inline/src/lib.rs +++ b/bindings/ruby/ext/css_inline/src/lib.rs @@ -39,7 +39,7 @@ type RubyResult<T> = Result<T, magnus::Error>; fn parse_options<Req>( args: &Args<Req, (), (), (), RHash, ()>, -) -> RubyResult<rust_inline::InlineOptions<'static>> { +) -> RubyResult<rust_inline::blocking::InlineOptions<'static>> { let kwargs = get_kwargs::< _, (), @@ -67,7 +67,7 @@ fn parse_options<Req>( ], )?; let kwargs = kwargs.optional; - Ok(rust_inline::InlineOptions { + Ok(rust_inline::blocking::InlineOptions { inline_style_tags: kwargs.0.unwrap_or(true), keep_style_tags: kwargs.1.unwrap_or(false), keep_link_tags: kwargs.2.unwrap_or(false), @@ -81,7 +81,7 @@ fn parse_options<Req>( #[magnus::wrap(class = "CSSInline::CSSInliner")] struct CSSInliner { - inner: rust_inline::CSSInliner<'static>, + inner: rust_inline::blocking::CSSInliner<'static>, } struct InlineErrorWrapper(rust_inline::InlineError); @@ -133,7 +133,7 @@ impl CSSInliner { let args = scan_args::<(), _, _, _, _, _>(args)?; let options = parse_options(&args)?; Ok(CSSInliner { - inner: rust_inline::CSSInliner::new(options), + inner: rust_inline::blocking::CSSInliner::new(options), }) } @@ -152,7 +152,7 @@ fn inline(args: &[Value]) -> RubyResult<String> { let args = scan_args::<(String,), _, _, _, _, _>(args)?; let options = parse_options(&args)?; let html = args.required.0; - Ok(rust_inline::CSSInliner::new(options) + Ok(rust_inline::blocking::CSSInliner::new(options) .inline(&html) .map_err(InlineErrorWrapper)?) } @@ -160,13 +160,13 @@ fn inline(args: &[Value]) -> RubyResult<String> { fn inline_many(args: &[Value]) -> RubyResult<Vec<String>> { let args = scan_args::<(Vec<String>,), _, _, _, _, _>(args)?; let options = parse_options(&args)?; - let inliner = rust_inline::CSSInliner::new(options); + let inliner = rust_inline::blocking::CSSInliner::new(options); inline_many_impl(&args.required.0, &inliner) } fn inline_many_impl( htmls: &[String], - inliner: &rust_inline::CSSInliner<'static>, + inliner: &rust_inline::blocking::CSSInliner<'static>, ) -> RubyResult<Vec<String>> { let output: Result<Vec<_>, _> = htmls.par_iter().map(|html| inliner.inline(html)).collect(); Ok(output.map_err(InlineErrorWrapper)?) diff --git a/css-inline/Cargo.toml b/css-inline/Cargo.toml index 22cae200..a4e29a2e 100644 --- a/css-inline/Cargo.toml +++ b/css-inline/Cargo.toml @@ -22,18 +22,20 @@ rust-version = "1.65" name = "css-inline" [features] -default = ["cli", "http", "file"] +default = ["cli", "http", "http-blocking", "file"] cli = ["pico-args", "rayon"] http = ["reqwest"] +http-blocking = ["reqwest/blocking"] file = [] [dependencies] cssparser = "0.31.2" +futures-util = "0.3.30" html5ever = "0.26.0" indexmap = "2.1" pico-args = { version = "0.3", optional = true } rayon = { version = "1.7", optional = true } -reqwest = { version = "0.11.23", optional = true, default-features = false, features = ["rustls-tls", "blocking"] } +reqwest = { version = "0.11.23", optional = true, default-features = false, features = ["rustls-tls"] } rustc-hash = "1.1.0" selectors = "0.25.0" smallvec = "1" @@ -46,6 +48,7 @@ criterion = { version = "0.5.1", features = [], default-features = false } serde = { version = "1", features = ["derive"] } serde_json = "1" test-case = "3.3" +tokio = { version = "1.32", features = ["macros", "rt"] } [[bench]] name = "inliner" diff --git a/css-inline/benches/inliner.rs b/css-inline/benches/inliner.rs index e009b328..76ab8c3f 100644 --- a/css-inline/benches/inliner.rs +++ b/css-inline/benches/inliner.rs @@ -1,5 +1,5 @@ use codspeed_criterion_compat::{black_box, criterion_group, criterion_main, Criterion}; -use css_inline::inline_to; +use css_inline::blocking::inline_to; use std::fs; #[derive(serde::Deserialize, Debug)] diff --git a/css-inline/src/error.rs b/css-inline/src/error.rs index 5f21deb3..1d7ce97c 100644 --- a/css-inline/src/error.rs +++ b/css-inline/src/error.rs @@ -20,7 +20,7 @@ pub enum InlineError { /// from the filesystem. IO(io::Error), /// Network-related problem. E.g. resource is not available. - #[cfg(feature = "http")] + #[cfg(any(feature = "http", feature = "http-blocking"))] Network { /// Original network error. error: reqwest::Error, @@ -41,7 +41,7 @@ impl Error for InlineError { fn source(&self) -> Option<&(dyn Error + 'static)> { match self { InlineError::IO(error) => Some(error), - #[cfg(feature = "http")] + #[cfg(any(feature = "http", feature = "http-blocking"))] InlineError::Network { error, .. } => Some(error), InlineError::MissingStyleSheet { .. } | InlineError::ParseError(_) => None, } @@ -52,7 +52,7 @@ impl Display for InlineError { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { match self { Self::IO(error) => error.fmt(f), - #[cfg(feature = "http")] + #[cfg(any(feature = "http", feature = "http-blocking"))] Self::Network { error, location } => f.write_fmt(format_args!("{error}: {location}")), Self::ParseError(error) => f.write_str(error), Self::MissingStyleSheet { path } => { diff --git a/css-inline/src/lib.rs b/css-inline/src/lib.rs index 04aeeb63..56d4d253 100644 --- a/css-inline/src/lib.rs +++ b/css-inline/src/lib.rs @@ -34,8 +34,7 @@ mod parser; mod resolver; pub use error::InlineError; -use indexmap::IndexMap; -use std::{borrow::Cow, fmt::Formatter, hash::BuildHasherDefault, io::Write, sync::Arc}; +use std::{hash::BuildHasherDefault, io::Write}; use crate::html::ElementStyleMap; use hasher::BuildNoHashHasher; @@ -43,214 +42,32 @@ use html::Document; pub use resolver::{DefaultStylesheetResolver, StylesheetResolver}; pub use url::{ParseError, Url}; -/// Configuration options for CSS inlining process. -#[allow(clippy::struct_excessive_bools)] -pub struct InlineOptions<'a> { - /// Whether to inline CSS from "style" tags. - /// - /// Sometimes HTML may include a lot of boilerplate styles, that are not applicable in every - /// scenario and it is useful to ignore them and use `extra_css` instead. - pub inline_style_tags: bool, - /// Keep "style" tags after inlining. - pub keep_style_tags: bool, - /// Keep "link" tags after inlining. - pub keep_link_tags: bool, - /// Used for loading external stylesheets via relative URLs. - pub base_url: Option<Url>, - /// Whether remote stylesheets should be loaded or not. - pub load_remote_stylesheets: bool, - // The point of using `Cow` here is Python bindings, where it is problematic to pass a reference - // without dealing with memory leaks & unsafe. With `Cow` we can use moved values as `String` in - // Python wrapper for `CSSInliner` and `&str` in Rust & simple functions on the Python side - /// Additional CSS to inline. - pub extra_css: Option<Cow<'a, str>>, - /// Pre-allocate capacity for HTML nodes during parsing. - /// It can improve performance when you have an estimate of the number of nodes in your HTML document. - pub preallocate_node_capacity: usize, - /// A way to resolve stylesheets from various sources. - pub resolver: Arc<dyn StylesheetResolver>, -} - -impl<'a> std::fmt::Debug for InlineOptions<'a> { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - f.debug_struct("InlineOptions") - .field("inline_style_tags", &self.inline_style_tags) - .field("keep_style_tags", &self.keep_style_tags) - .field("keep_link_tags", &self.keep_link_tags) - .field("base_url", &self.base_url) - .field("load_remote_stylesheets", &self.load_remote_stylesheets) - .field("extra_css", &self.extra_css) - .field("preallocate_node_capacity", &self.preallocate_node_capacity) - .finish_non_exhaustive() - } -} - -impl<'a> InlineOptions<'a> { - /// Override whether "style" tags should be inlined. - #[must_use] - pub fn inline_style_tags(mut self, inline_style_tags: bool) -> Self { - self.inline_style_tags = inline_style_tags; - self - } - - /// Override whether "style" tags should be kept after processing. - #[must_use] - pub fn keep_style_tags(mut self, keep_style_tags: bool) -> Self { - self.keep_style_tags = keep_style_tags; - self - } - - /// Override whether "link" tags should be kept after processing. - #[must_use] - pub fn keep_link_tags(mut self, keep_link_tags: bool) -> Self { - self.keep_link_tags = keep_link_tags; - self - } - - /// Set base URL that will be used for loading external stylesheets via relative URLs. - #[must_use] - pub fn base_url(mut self, base_url: Option<Url>) -> Self { - self.base_url = base_url; - self - } - - /// Override whether remote stylesheets should be loaded. - #[must_use] - pub fn load_remote_stylesheets(mut self, load_remote_stylesheets: bool) -> Self { - self.load_remote_stylesheets = load_remote_stylesheets; - self - } - - /// Set additional CSS to inline. - #[must_use] - pub fn extra_css(mut self, extra_css: Option<Cow<'a, str>>) -> Self { - self.extra_css = extra_css; - self - } - - /// Set the initial node capacity for HTML tree. - #[must_use] - pub fn preallocate_node_capacity(mut self, preallocate_node_capacity: usize) -> Self { - self.preallocate_node_capacity = preallocate_node_capacity; - self - } - - /// Set the way to resolve stylesheets from various sources. - #[must_use] - pub fn resolver(mut self, resolver: Arc<dyn StylesheetResolver>) -> Self { - self.resolver = resolver; - self - } - - /// Create a new `CSSInliner` instance from this options. - #[must_use] - pub const fn build(self) -> CSSInliner<'a> { - CSSInliner::new(self) - } -} - -impl Default for InlineOptions<'_> { - #[inline] - fn default() -> Self { - InlineOptions { - inline_style_tags: true, - keep_style_tags: false, - keep_link_tags: false, - base_url: None, - load_remote_stylesheets: true, - extra_css: None, - preallocate_node_capacity: 32, - resolver: Arc::new(DefaultStylesheetResolver), - } - } -} - /// A specialized `Result` type for CSS inlining operations. pub type Result<T> = std::result::Result<T, InlineError>; -/// Customizable CSS inliner. -#[derive(Debug)] -pub struct CSSInliner<'a> { - options: InlineOptions<'a>, -} - const GROWTH_COEFFICIENT: f64 = 1.5; // A rough coefficient to calculate the number of individual declarations based on the total CSS size. const DECLARATION_SIZE_COEFFICIENT: f64 = 30.0; -impl<'a> CSSInliner<'a> { - /// Create a new `CSSInliner` instance with given options. - #[must_use] - #[inline] - pub const fn new(options: InlineOptions<'a>) -> Self { - CSSInliner { options } - } - - /// Return a default `InlineOptions` that can fully configure the CSS inliner. - /// - /// # Examples - /// - /// Get default `InlineOptions`, then change base url - /// - /// ```rust - /// use css_inline::{CSSInliner, Url}; - /// # use url::ParseError; - /// # fn run() -> Result<(), ParseError> { - /// let url = Url::parse("https://api.example.com")?; - /// let inliner = CSSInliner::options() - /// .base_url(Some(url)) - /// .build(); - /// # Ok(()) - /// # } - /// # run().unwrap(); - /// ``` - #[must_use] - #[inline] - pub fn options() -> InlineOptions<'a> { - InlineOptions::default() - } - - /// Inline CSS styles from <style> tags to matching elements in the HTML tree and return a - /// string. - /// - /// # Errors - /// - /// Inlining might fail for the following reasons: - /// - Missing stylesheet file; - /// - Remote stylesheet is not available; - /// - IO errors; - /// - Internal CSS selector parsing error; - #[inline] - pub fn inline(&self, html: &str) -> Result<String> { - // Allocating more memory than the input HTML, as the inlined version is usually bigger - #[allow( - clippy::cast_precision_loss, - clippy::cast_sign_loss, - clippy::cast_possible_truncation - )] - let mut out = Vec::with_capacity( - (html.len() as f64 * GROWTH_COEFFICIENT) - .min(usize::MAX as f64) - .round() as usize, - ); - self.inline_to(html, &mut out)?; - Ok(String::from_utf8_lossy(&out).to_string()) - } +#[inline] +fn build_output_buffer(input_length: usize) -> Vec<u8> { + // Allocating more memory than the input HTML, as the inlined version is usually bigger + #[allow( + clippy::cast_precision_loss, + clippy::cast_sign_loss, + clippy::cast_possible_truncation + )] + Vec::with_capacity( + (input_length as f64 * GROWTH_COEFFICIENT) + .min(usize::MAX as f64) + .round() as usize, + ) +} - /// Inline CSS & write the result to a generic writer. Use it if you want to write - /// the inlined document to a file. - /// - /// # Errors - /// - /// Inlining might fail for the following reasons: - /// - Missing stylesheet file; - /// - Remote stylesheet is not available; - /// - IO errors; - /// - Internal CSS selector parsing error; - #[inline] - pub fn inline_to<W: Write>(&self, html: &str, target: &mut W) -> Result<()> { +macro_rules! inline_to_impl { + ($self:expr, $html:expr, $target:expr, $retrieve:expr, $($_await:tt)*) => {{ let document = - Document::parse_with_options(html.as_bytes(), self.options.preallocate_node_capacity); + $crate::Document::parse_with_options($html.as_bytes(), $self.options.preallocate_node_capacity); // CSS rules may overlap, and the final set of rules applied to an element depend on // selectors' specificity - selectors with higher specificity have more priority. // Inlining happens in two major steps: @@ -258,7 +75,7 @@ impl<'a> CSSInliner<'a> { // selector's specificity. When two rules overlap on the same declaration, then // the one with higher specificity replaces another. // 2. Resulting styles are merged into existing "style" tags. - let mut size_estimate: usize = if self.options.inline_style_tags { + let mut size_estimate: usize = if $self.options.inline_style_tags { document .styles() .map(|s| { @@ -269,31 +86,32 @@ impl<'a> CSSInliner<'a> { } else { 0 }; - if let Some(extra_css) = &self.options.extra_css { + if let Some(extra_css) = &$self.options.extra_css { size_estimate = size_estimate.saturating_add(extra_css.len()); } let mut raw_styles = String::with_capacity(size_estimate); - if self.options.inline_style_tags { + if $self.options.inline_style_tags { for style in document.styles() { raw_styles.push_str(style); raw_styles.push('\n'); } } - if self.options.load_remote_stylesheets { + if $self.options.load_remote_stylesheets { let mut links = document.stylesheets().collect::<Vec<&str>>(); links.sort_unstable(); links.dedup(); for href in &links { - let url = self.get_full_url(href); - let css = self.options.resolver.retrieve(url.as_ref())?; - raw_styles.push_str(&css); + let url = $self.get_full_url(href); + #[allow(clippy::redundant_closure_call)] + let css = $retrieve(url.as_ref())$($_await)*; + raw_styles.push_str(css.as_str()); raw_styles.push('\n'); } } - if let Some(extra_css) = &self.options.extra_css { + if let Some(extra_css) = &$self.options.extra_css { raw_styles.push_str(extra_css); } - let mut styles = IndexMap::with_capacity_and_hasher(128, BuildNoHashHasher::default()); + let mut styles = indexmap::IndexMap::with_capacity_and_hasher(128, $crate::BuildNoHashHasher::default()); let mut parse_input = cssparser::ParserInput::new(&raw_styles); let mut parser = cssparser::Parser::new(&mut parse_input); // Allocating some memory for all the parsed declarations @@ -303,7 +121,7 @@ impl<'a> CSSInliner<'a> { clippy::cast_possible_truncation )] let mut declarations = Vec::with_capacity( - ((raw_styles.len() as f64 / DECLARATION_SIZE_COEFFICIENT) + ((raw_styles.len() as f64 / $crate::DECLARATION_SIZE_COEFFICIENT) .min(usize::MAX as f64) .round() as usize) .max(16), @@ -311,7 +129,7 @@ impl<'a> CSSInliner<'a> { let mut rule_list = Vec::with_capacity(declarations.capacity() / 3); for rule in cssparser::StyleSheetParser::new( &mut parser, - &mut parser::CSSRuleListParser::new(&mut declarations), + &mut $crate::parser::CSSRuleListParser::new(&mut declarations), ) .flatten() { @@ -327,9 +145,9 @@ impl<'a> CSSInliner<'a> { for matching_element in matching_elements { let element_styles = styles.entry(matching_element.node_id).or_insert_with(|| { - ElementStyleMap::with_capacity_and_hasher( + $crate::ElementStyleMap::with_capacity_and_hasher( end.saturating_sub(*start).saturating_add(4), - BuildHasherDefault::default(), + $crate::BuildHasherDefault::default(), ) }); // Iterate over pairs of property name & value @@ -353,38 +171,242 @@ impl<'a> CSSInliner<'a> { } } document.serialize( - target, + $target, styles, - self.options.keep_style_tags, - self.options.keep_link_tags, + $self.options.keep_style_tags, + $self.options.keep_link_tags, )?; Ok(()) - } + }}; +} - fn get_full_url<'u>(&self, href: &'u str) -> Cow<'u, str> { - // Valid absolute URL - if Url::parse(href).is_ok() { - return Cow::Borrowed(href); - }; - if let Some(base_url) = &self.options.base_url { - // Use the same scheme as the base URL - if href.starts_with("//") { - return Cow::Owned(format!("{}:{}", base_url.scheme(), href)); +macro_rules! inliner_impl { + () => { + /// Configuration options for CSS inlining process. + #[allow(clippy::struct_excessive_bools)] + pub struct InlineOptions<'a> { + /// Whether to inline CSS from "style" tags. + /// + /// Sometimes HTML may include a lot of boilerplate styles, that are not applicable in every + /// scenario and it is useful to ignore them and use `extra_css` instead. + pub inline_style_tags: bool, + /// Keep "style" tags after inlining. + pub keep_style_tags: bool, + /// Keep "link" tags after inlining. + pub keep_link_tags: bool, + /// Used for loading external stylesheets via relative URLs. + #[allow(unused_qualifications)] + pub base_url: Option<url::Url>, + /// Whether remote stylesheets should be loaded or not. + pub load_remote_stylesheets: bool, + // The point of using `Cow` here is Python bindings, where it is problematic to pass a reference + // without dealing with memory leaks & unsafe. With `Cow` we can use moved values as `String` in + // Python wrapper for `CSSInliner` and `&str` in Rust & simple functions on the Python side + /// Additional CSS to inline. + pub extra_css: Option<std::borrow::Cow<'a, str>>, + /// Pre-allocate capacity for HTML nodes during parsing. + /// It can improve performance when you have an estimate of the number of nodes in your HTML document. + pub preallocate_node_capacity: usize, + /// A way to resolve stylesheets from various sources. + pub resolver: std::sync::Arc<dyn $crate::resolver::StylesheetResolver>, + } + + impl<'a> std::fmt::Debug for InlineOptions<'a> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("InlineOptions") + .field("inline_style_tags", &self.inline_style_tags) + .field("keep_style_tags", &self.keep_style_tags) + .field("keep_link_tags", &self.keep_link_tags) + .field("base_url", &self.base_url) + .field("load_remote_stylesheets", &self.load_remote_stylesheets) + .field("extra_css", &self.extra_css) + .field("preallocate_node_capacity", &self.preallocate_node_capacity) + .finish_non_exhaustive() + } + } + + impl<'a> InlineOptions<'a> { + /// Override whether "style" tags should be inlined. + #[must_use] + pub fn inline_style_tags(mut self, inline_style_tags: bool) -> Self { + self.inline_style_tags = inline_style_tags; + self } - // Not a URL, then it is a relative URL - if let Ok(new_url) = base_url.join(href) { - return Cow::Owned(new_url.into()); + + /// Override whether "style" tags should be kept after processing. + #[must_use] + pub fn keep_style_tags(mut self, keep_style_tags: bool) -> Self { + self.keep_style_tags = keep_style_tags; + self } - }; - // If it is not a valid URL and there is no base URL specified, we assume a local path - Cow::Borrowed(href) - } + + /// Override whether "link" tags should be kept after processing. + #[must_use] + pub fn keep_link_tags(mut self, keep_link_tags: bool) -> Self { + self.keep_link_tags = keep_link_tags; + self + } + + /// Set base URL that will be used for loading external stylesheets via relative URLs. + #[must_use] + #[allow(unused_qualifications)] + pub fn base_url(mut self, base_url: Option<url::Url>) -> Self { + self.base_url = base_url; + self + } + + /// Override whether remote stylesheets should be loaded. + #[must_use] + pub fn load_remote_stylesheets(mut self, load_remote_stylesheets: bool) -> Self { + self.load_remote_stylesheets = load_remote_stylesheets; + self + } + + /// Set additional CSS to inline. + #[must_use] + pub fn extra_css(mut self, extra_css: Option<std::borrow::Cow<'a, str>>) -> Self { + self.extra_css = extra_css; + self + } + + /// Set the initial node capacity for HTML tree. + #[must_use] + pub fn preallocate_node_capacity(mut self, preallocate_node_capacity: usize) -> Self { + self.preallocate_node_capacity = preallocate_node_capacity; + self + } + + /// Set the way to resolve stylesheets from various sources. + #[must_use] + pub fn resolver( + mut self, + resolver: std::sync::Arc<dyn $crate::resolver::StylesheetResolver>, + ) -> Self { + self.resolver = resolver; + self + } + + /// Create a new `CSSInliner` instance from this options. + #[must_use] + pub const fn build(self) -> CSSInliner<'a> { + CSSInliner::new(self) + } + } + + impl Default for InlineOptions<'_> { + #[inline] + fn default() -> Self { + InlineOptions { + inline_style_tags: true, + keep_style_tags: false, + keep_link_tags: false, + base_url: None, + load_remote_stylesheets: true, + extra_css: None, + preallocate_node_capacity: 32, + resolver: std::sync::Arc::new($crate::DefaultStylesheetResolver), + } + } + } + /// Customizable CSS inliner. + #[derive(Debug)] + pub struct CSSInliner<'a> { + options: InlineOptions<'a>, + } + + impl<'a> CSSInliner<'a> { + /// Create a new `CSSInliner` instance with given options. + #[must_use] + #[inline] + pub const fn new(options: InlineOptions<'a>) -> Self { + CSSInliner { options } + } + + /// Return a default `InlineOptions` that can fully configure the CSS inliner. + /// + /// # Examples + /// + /// Get default `InlineOptions`, then change base url + /// + /// ```rust + /// use css_inline::{CSSInliner, Url}; + /// # use url::ParseError; + /// # fn run() -> Result<(), ParseError> { + /// let url = Url::parse("https://api.example.com")?; + /// let inliner = CSSInliner::options() + /// .base_url(Some(url)) + /// .build(); + /// # Ok(()) + /// # } + /// # run().unwrap(); + /// ``` + #[must_use] + #[inline] + pub fn options() -> InlineOptions<'a> { + InlineOptions::default() + } + + fn get_full_url<'u>(&self, href: &'u str) -> std::borrow::Cow<'u, str> { + // Valid absolute URL + if url::Url::parse(href).is_ok() { + return std::borrow::Cow::Borrowed(href); + }; + if let Some(base_url) = &self.options.base_url { + // Use the same scheme as the base URL + if href.starts_with("//") { + return std::borrow::Cow::Owned(format!("{}:{}", base_url.scheme(), href)); + } + // Not a URL, then it is a relative URL + if let Ok(new_url) = base_url.join(href) { + return std::borrow::Cow::Owned(new_url.into()); + } + }; + // If it is not a valid URL and there is no base URL specified, we assume a local path + std::borrow::Cow::Borrowed(href) + } + } + + impl Default for CSSInliner<'_> { + #[inline] + fn default() -> Self { + CSSInliner::new(InlineOptions::default()) + } + } + }; } -impl Default for CSSInliner<'_> { +inliner_impl!(); + +impl<'a> CSSInliner<'a> { + /// Inline CSS styles from <style> tags to matching elements in the HTML tree and return a + /// string. + /// + /// # Errors + /// + /// Inlining might fail for the following reasons: + /// - Missing stylesheet file; + /// - Remote stylesheet is not available; + /// - IO errors; + /// - Internal CSS selector parsing error; #[inline] - fn default() -> Self { - CSSInliner::new(InlineOptions::default()) + pub async fn inline(&self, html: &str) -> Result<String> { + let mut out = build_output_buffer(html.len()); + self.inline_to(html, &mut out).await?; + Ok(String::from_utf8_lossy(&out).to_string()) + } + /// Inline CSS & write the result to a generic writer. Use it if you want to write + /// the inlined document to a file. + /// + /// # Errors + /// + /// Inlining might fail for the following reasons: + /// - Missing stylesheet file; + /// - Remote stylesheet is not available; + /// - IO errors; + /// - Internal CSS selector parsing error; + #[inline] + pub async fn inline_to<W: Write>(&self, html: &str, target: &mut W) -> Result<()> { + inline_to_impl!(self, html, target, |location| async move { self.options.resolver.retrieve(location).await}, .await?) } } @@ -398,8 +420,8 @@ impl Default for CSSInliner<'_> { /// - IO errors; /// - Internal CSS selector parsing error; #[inline] -pub fn inline(html: &str) -> Result<String> { - CSSInliner::default().inline(html) +pub async fn inline(html: &str) -> Result<String> { + CSSInliner::default().inline(html).await } /// Shortcut for inlining CSS with default parameters and writing the output to a generic writer. @@ -412,6 +434,74 @@ pub fn inline(html: &str) -> Result<String> { /// - IO errors; /// - Internal CSS selector parsing error; #[inline] -pub fn inline_to<W: Write>(html: &str, target: &mut W) -> Result<()> { - CSSInliner::default().inline_to(html, target) +pub async fn inline_to<W: Write>(html: &str, target: &mut W) -> Result<()> { + CSSInliner::default().inline_to(html, target).await +} + +/// Blocking API for CSS inlining. +pub mod blocking { + use super::{build_output_buffer, Result}; + use std::io::Write; + + inliner_impl!(); + + impl<'a> CSSInliner<'a> { + /// Inline CSS styles from <style> tags to matching elements in the HTML tree and return a + /// string. + /// + /// # Errors + /// + /// Inlining might fail for the following reasons: + /// - Missing stylesheet file; + /// - Remote stylesheet is not available; + /// - IO errors; + /// - Internal CSS selector parsing error; + #[inline] + pub fn inline(&self, html: &str) -> Result<String> { + let mut out = build_output_buffer(html.len()); + self.inline_to(html, &mut out)?; + Ok(String::from_utf8_lossy(&out).to_string()) + } + /// Inline CSS & write the result to a generic writer. Use it if you want to write + /// the inlined document to a file. + /// + /// # Errors + /// + /// Inlining might fail for the following reasons: + /// - Missing stylesheet file; + /// - Remote stylesheet is not available; + /// - IO errors; + /// - Internal CSS selector parsing error; + #[inline] + pub fn inline_to<W: Write>(&self, html: &str, target: &mut W) -> Result<()> { + inline_to_impl!(self, html, target, |location| { self.options.resolver.retrieve_blocking(location)}, ?) + } + } + /// Shortcut for inlining CSS with default parameters. + /// + /// # Errors + /// + /// Inlining might fail for the following reasons: + /// - Missing stylesheet file; + /// - Remote stylesheet is not available; + /// - IO errors; + /// - Internal CSS selector parsing error; + #[inline] + pub fn inline(html: &str) -> Result<String> { + CSSInliner::default().inline(html) + } + + /// Shortcut for inlining CSS with default parameters and writing the output to a generic writer. + /// + /// # Errors + /// + /// Inlining might fail for the following reasons: + /// - Missing stylesheet file; + /// - Remote stylesheet is not available; + /// - IO errors; + /// - Internal CSS selector parsing error; + #[inline] + pub fn inline_to<W: Write>(html: &str, target: &mut W) -> Result<()> { + CSSInliner::default().inline_to(html, target) + } } diff --git a/css-inline/src/main.rs b/css-inline/src/main.rs index 699f1a04..22789250 100644 --- a/css-inline/src/main.rs +++ b/css-inline/src/main.rs @@ -6,7 +6,10 @@ fn main() { #[cfg(feature = "cli")] fn main() -> Result<(), Box<dyn std::error::Error>> { - use css_inline::{CSSInliner, DefaultStylesheetResolver, InlineOptions}; + use css_inline::{ + blocking::{CSSInliner, InlineOptions}, + DefaultStylesheetResolver, + }; use rayon::prelude::*; use std::{ borrow::Cow, diff --git a/css-inline/src/resolver.rs b/css-inline/src/resolver.rs index c40eecc0..4f26e12a 100644 --- a/css-inline/src/resolver.rs +++ b/css-inline/src/resolver.rs @@ -1,25 +1,27 @@ use crate::{InlineError, Result}; +use futures_util::FutureExt; use std::io::ErrorKind; -/// Blocking way of resolving stylesheets from various sources. +type AsyncResult<'a, T> = std::pin::Pin<Box<dyn futures_util::Future<Output = Result<T>> + 'a>>; + +/// Resolving stylesheets from various sources. pub trait StylesheetResolver: Send + Sync { - /// Retrieve a stylesheet from a network or local filesystem location. + /// Retrieve a stylesheet from a network or local filesystem location in a blocking way. /// /// # Errors /// /// Any network or filesystem related error, or an error during response parsing. - fn retrieve(&self, location: &str) -> Result<String> { + fn retrieve_blocking(&self, location: &str) -> Result<String> { if location.starts_with("https") | location.starts_with("http") { - #[cfg(feature = "http")] + #[cfg(feature = "http-blocking")] { - self.retrieve_from_url(location) + self.retrieve_from_url_blocking(location) } - - #[cfg(not(feature = "http"))] + #[cfg(not(feature = "http-blocking"))] { Err(InlineError::IO(std::io::Error::new( ErrorKind::Unsupported, - "Loading external URLs requires the `http` feature", + "Loading external URLs requires the `http-blocking` feature", ))) } } else { @@ -36,14 +38,60 @@ pub trait StylesheetResolver: Send + Sync { } } } - /// Retrieve a stylesheet from a network location. + /// Retrieve a stylesheet from a network location in a blocking way. /// /// # Errors /// /// Any network-related error, or an error during response parsing. - fn retrieve_from_url(&self, url: &str) -> Result<String> { + fn retrieve_from_url_blocking(&self, url: &str) -> Result<String> { Err(self.unsupported(&format!("Loading external URLs is not supported: {url}"))) } + /// Retrieve a stylesheet from a network or local filesystem location in a non-blocking way. + /// + /// # Errors + /// + /// Any network or filesystem related error, or an error during response parsing. + fn retrieve<'a>(&'a self, location: &'a str) -> AsyncResult<'_, String> { + if location.starts_with("https") | location.starts_with("http") { + #[cfg(feature = "http")] + { + self.retrieve_from_url(location) + } + #[cfg(not(feature = "http"))] + { + async move { + Err(InlineError::IO(std::io::Error::new( + ErrorKind::Unsupported, + "Loading external URLs requires the `http` feature", + ))) + } + .boxed_local() + } + } else { + async move { + #[cfg(feature = "file")] + { + self.retrieve_from_path(location) + } + #[cfg(not(feature = "file"))] + { + Err(InlineError::IO(std::io::Error::new( + ErrorKind::Unsupported, + "Loading local files requires the `file` feature", + ))) + } + } + .boxed_local() + } + } + /// Retrieve a stylesheet from a network location in a non-blocking way. + /// + /// # Errors + /// + /// Any network-related error, or an error during response parsing. + fn retrieve_from_url<'a>(&'a self, url: &'a str) -> AsyncResult<'_, String> { + async move { Err(self.unsupported(&format!("Loading external URLs is not supported: {url}"))) }.boxed_local() + } /// Retrieve a stylesheet from the local filesystem. /// /// # Errors @@ -74,7 +122,23 @@ pub struct DefaultStylesheetResolver; impl StylesheetResolver for DefaultStylesheetResolver { #[cfg(feature = "http")] - fn retrieve_from_url(&self, url: &str) -> Result<String> { + fn retrieve_from_url<'a>(&'a self, url: &'a str) -> AsyncResult<'_, String> { + let into_error = |error| InlineError::Network { + error, + location: url.to_string(), + }; + async move { + reqwest::get(url) + .await + .map_err(into_error)? + .text() + .await + .map_err(into_error) + } + .boxed_local() + } + #[cfg(feature = "http-blocking")] + fn retrieve_from_url_blocking(&self, url: &str) -> Result<String> { let into_error = |error| InlineError::Network { error, location: url.to_string(), diff --git a/css-inline/tests/test_inlining.rs b/css-inline/tests/test_inlining.rs index 7e5a59a8..09c97bd8 100644 --- a/css-inline/tests/test_inlining.rs +++ b/css-inline/tests/test_inlining.rs @@ -38,8 +38,8 @@ fn assert_http(inlined: Result<String, css_inline::InlineError>, expected: &str) } } -#[test] -fn no_existing_style() { +#[tokio::test] +async fn no_existing_style() { // When no "style" attributes exist assert_inlined!( style = r#"h1, h2 { color:red; } @@ -56,8 +56,8 @@ p.footer { font-size: 1px}"#, ) } -#[test] -fn ignore_inlining_attribute_tag() { +#[tokio::test] +async fn ignore_inlining_attribute_tag() { // When an HTML tag contains `data-css-inline="ignore"` assert_inlined!( style = "h1 { color:blue; }", @@ -67,8 +67,8 @@ fn ignore_inlining_attribute_tag() { ) } -#[test] -fn ignore_inlining_attribute_style() { +#[tokio::test] +async fn ignore_inlining_attribute_style() { // When a `style` tag contains `data-css-inline="ignore"` let html = r#" <html> @@ -81,7 +81,7 @@ h1 { color: blue; } <h1>Big Text</h1> </body> </html>"#; - let result = inline(html).unwrap(); + let result = inline(html).await.unwrap(); // Then it should be skipped assert!(result.ends_with( r#"<body> @@ -91,8 +91,8 @@ h1 { color: blue; } )) } -#[test] -fn ignore_inlining_attribute_link() { +#[tokio::test] +async fn ignore_inlining_attribute_link() { // When a `link` tag contains `data-css-inline="ignore"` let html = r#" <html> @@ -103,7 +103,7 @@ fn ignore_inlining_attribute_link() { <h1>Big Text</h1> </body> </html>"#; - let result = inline(html).unwrap(); + let result = inline(html).await.unwrap(); // Then it should be skipped assert!(result.ends_with( r#"<body> @@ -113,8 +113,8 @@ fn ignore_inlining_attribute_link() { )) } -#[test] -fn specificity_same_selector() { +#[tokio::test] +async fn specificity_same_selector() { assert_inlined!( style = r#" .test-class { @@ -128,8 +128,8 @@ fn specificity_same_selector() { ) } -#[test] -fn specificity_different_selectors() { +#[tokio::test] +async fn specificity_different_selectors() { assert_inlined!( style = r#" .test { padding-left: 16px; } @@ -139,8 +139,8 @@ h1 { padding: 0; }"#, ) } -#[test] -fn specificity_different_selectors_existing_style() { +#[tokio::test] +async fn specificity_different_selectors_existing_style() { assert_inlined!( style = r#" .test { padding-left: 16px; } @@ -150,8 +150,8 @@ h1 { padding: 0; }"#, ) } -#[test] -fn overlap_styles() { +#[tokio::test] +async fn overlap_styles() { // When two selectors match the same element assert_inlined!( style = r#" @@ -168,12 +168,12 @@ a { ) } -#[test] -fn simple_merge() { +#[tokio::test] +async fn simple_merge() { // When "style" attributes exist and collides with values defined in "style" tag let style = "h1 { color:red; }"; let html = html!(style, r#"<h1 style="font-size: 1px">Big Text</h1>"#); - let inlined = inline(&html).unwrap(); + let inlined = inline(&html).await.unwrap(); // Then new styles should be merged with the existing ones let option_1 = html!(r#"<h1 style="font-size: 1px;color: red">Big Text</h1>"#); let option_2 = html!(r#"<h1 style="color: red;font-size: 1px">Big Text</h1>"#); @@ -181,8 +181,8 @@ fn simple_merge() { assert!(valid, "{}", inlined); } -#[test] -fn overloaded_styles() { +#[tokio::test] +async fn overloaded_styles() { // When there is a style, applied to an ID assert_inlined!( style = "h1 { color: red; } #test { color: blue; }", @@ -192,8 +192,8 @@ fn overloaded_styles() { ) } -#[test] -fn important() { +#[tokio::test] +async fn important() { // `!important` rules should override existing inline styles assert_inlined!( style = "h1 { color: blue !important; }", @@ -202,8 +202,8 @@ fn important() { ) } -#[test] -fn important_no_rule_exists() { +#[tokio::test] +async fn important_no_rule_exists() { // `!important` rules should override existing inline styles assert_inlined!( style = "h1 { color: blue !important; }", @@ -212,8 +212,8 @@ fn important_no_rule_exists() { ) } -#[test] -fn font_family_quoted() { +#[tokio::test] +async fn font_family_quoted() { // When property value contains double quotes assert_inlined!( style = r#"h1 { font-family: "Open Sans", sans-serif; }"#, @@ -223,8 +223,8 @@ fn font_family_quoted() { ) } -#[test] -fn other_property_quoted() { +#[tokio::test] +async fn other_property_quoted() { // When property value contains double quotes assert_inlined!( style = r#"h1 { --bs-font-sant-serif: system-ui,-applie-system,"helvetica neue"; }"#, @@ -234,8 +234,8 @@ fn other_property_quoted() { ) } -#[test] -fn href_attribute_unchanged() { +#[tokio::test] +async fn href_attribute_unchanged() { // All HTML attributes should be serialized as is let html = r#"<html> <head> @@ -246,7 +246,7 @@ fn href_attribute_unchanged() { <a href="https://example.org/test?a=b&c=d">Link</a> </body> </html>"#; - let inlined = inline(html).unwrap(); + let inlined = inline(html).await.unwrap(); assert_eq!( inlined, r#"<html><head> @@ -260,8 +260,8 @@ fn href_attribute_unchanged() { ); } -#[test] -fn complex_child_selector() { +#[tokio::test] +async fn complex_child_selector() { let html = r#"<html> <head> <style>.parent { @@ -288,7 +288,7 @@ fn complex_child_selector() { </tbody> </table> </div></body></html>"#; - let inlined = inline(html).unwrap(); + let inlined = inline(html).await.unwrap(); assert_eq!( inlined, r#"<html><head> @@ -311,8 +311,8 @@ fn complex_child_selector() { ); } -#[test] -fn existing_styles() { +#[tokio::test] +async fn existing_styles() { // When there is a `style` attribute on a tag that contains a rule // And the `style` tag contains the same rule applicable to that tag assert_inlined!( @@ -323,8 +323,8 @@ fn existing_styles() { ) } -#[test] -fn existing_styles_multiple_tags() { +#[tokio::test] +async fn existing_styles_multiple_tags() { // When there are `style` attribute on tags that contains rules // And the `style` tag contains the same rule applicable to those tags assert_inlined!( @@ -337,8 +337,8 @@ fn existing_styles_multiple_tags() { ) } -#[test] -fn existing_styles_with_merge() { +#[tokio::test] +async fn existing_styles_with_merge() { // When there is a `style` attribute on a tag that contains a rule // And the `style` tag contains the same rule applicable to that tag // And there is a new rule in the `style` tag @@ -351,8 +351,8 @@ fn existing_styles_with_merge() { ) } -#[test] -fn existing_styles_with_merge_multiple_tags() { +#[tokio::test] +async fn existing_styles_with_merge_multiple_tags() { // When there are non-empty `style` attributes on tags // And the `style` tag contains the same rule applicable to those tags // And there is a new rule in the `style` tag @@ -366,8 +366,8 @@ fn existing_styles_with_merge_multiple_tags() { ) } -#[test] -fn remove_multiple_style_tags_without_inlining() { +#[tokio::test] +async fn remove_multiple_style_tags_without_inlining() { let html = r#" <html> <head> @@ -395,7 +395,7 @@ a { .keep_style_tags(false) .inline_style_tags(false) .build(); - let result = inliner.inline(html).unwrap(); + let result = inliner.inline(html).await.unwrap(); assert_eq!( result, r#"<html><head> @@ -411,8 +411,8 @@ a { ) } -#[test] -fn do_not_process_style_tag() { +#[tokio::test] +async fn do_not_process_style_tag() { let html = html!("h1 {background-color: blue;}", "<h1>Hello world!</h1>"); let options = InlineOptions { inline_style_tags: false, @@ -420,15 +420,15 @@ fn do_not_process_style_tag() { ..Default::default() }; let inliner = CSSInliner::new(options); - let result = inliner.inline(&html).unwrap(); + let result = inliner.inline(&html).await.unwrap(); assert_eq!( result, "<html><head><style>h1 {background-color: blue;}</style></head><body><h1>Hello world!</h1></body></html>" ) } -#[test] -fn do_not_process_style_tag_and_remove() { +#[tokio::test] +async fn do_not_process_style_tag_and_remove() { let html = html!("h1 {background-color: blue;}", "<h1>Hello world!</h1>"); let options = InlineOptions { keep_style_tags: false, @@ -436,15 +436,15 @@ fn do_not_process_style_tag_and_remove() { ..Default::default() }; let inliner = CSSInliner::new(options); - let result = inliner.inline(&html).unwrap(); + let result = inliner.inline(&html).await.unwrap(); assert_eq!( result, "<html><head></head><body><h1>Hello world!</h1></body></html>" ) } -#[test] -fn empty_style() { +#[tokio::test] +async fn empty_style() { // When the style tag is empty assert_inlined!( style = "", @@ -454,8 +454,8 @@ fn empty_style() { ) } -#[test] -fn media_query_ignore() { +#[tokio::test] +async fn media_query_ignore() { // When the style value includes @media query assert_inlined!( style = r#"@media screen and (max-width: 992px) { @@ -471,25 +471,26 @@ fn media_query_ignore() { #[test_case("@wrong { color: --- }", "Invalid @ rule: wrong")] #[test_case("ttt { 123 }", "Unexpected token: CurlyBracketBlock")] #[test_case("----", "End of input")] -fn invalid_rule(style: &str, expected: &str) { +#[tokio::test] +async fn invalid_rule(style: &str, expected: &str) { let html = html!( "h1 {background-color: blue;}", format!(r#"<h1 style="{}">Hello world!</h1>"#, style) ); - let result = inline(&html); + let result = inline(&html).await; assert!(result.is_err()); assert_eq!(result.unwrap_err().to_string(), expected); } -#[test] -fn remove_style_tag() { +#[tokio::test] +async fn remove_style_tag() { let html = html!("h1 {background-color: blue;}", "<h1>Hello world!</h1>"); - let result = inline(&html).unwrap(); + let result = inline(&html).await.unwrap(); assert_eq!(result, "<html><head></head><body><h1 style=\"background-color: blue;\">Hello world!</h1></body></html>") } -#[test] -fn remove_multiple_style_tags() { +#[tokio::test] +async fn remove_multiple_style_tags() { let html = r#" <html> <head> @@ -513,7 +514,7 @@ a { </body> </html> "#; - let result = inline(html).unwrap(); + let result = inline(html).await.unwrap(); assert_eq!( result, r#"<html><head> @@ -529,8 +530,8 @@ a { ) } -#[test] -fn extra_css() { +#[tokio::test] +async fn extra_css() { let html = html!("h1 {background-color: blue;}", "<h1>Hello world!</h1>"); let options = InlineOptions { inline_style_tags: false, @@ -538,15 +539,15 @@ fn extra_css() { ..Default::default() }; let inliner = CSSInliner::new(options); - let result = inliner.inline(&html).unwrap(); + let result = inliner.inline(&html).await.unwrap(); assert_eq!( result, "<html><head></head><body><h1 style=\"background-color: green;\">Hello world!</h1></body></html>" ) } -#[test] -fn remote_file_stylesheet() { +#[tokio::test] +async fn remote_file_stylesheet() { let html = r#" <html> <head> @@ -561,7 +562,7 @@ h2 { color: red; } <h2>Smaller Text</h2> </body> </html>"#; - let inlined = inline(html); + let inlined = inline(html).await; assert_file( inlined, r#"<body> @@ -572,8 +573,8 @@ h2 { color: red; } ); } -#[test] -fn missing_stylesheet() { +#[tokio::test] +async fn missing_stylesheet() { let html = r#" <html> <head> @@ -583,7 +584,7 @@ fn missing_stylesheet() { <h1>Big Text</h1> </body> </html>"#; - let inlined = inline(html); + let inlined = inline(html).await; #[cfg(feature = "file")] { assert_eq!( @@ -597,8 +598,8 @@ fn missing_stylesheet() { } } -#[test] -fn remote_file_stylesheet_disable() { +#[tokio::test] +async fn remote_file_stylesheet_disable() { let html = r#" <html> <head> @@ -613,7 +614,7 @@ h2 { color: red; } <h2>Smaller Text</h2> </body> </html>"#; - let inlined = inline(html); + let inlined = inline(html).await; assert_file( inlined, r#"<body> @@ -624,8 +625,8 @@ h2 { color: red; } ); } -#[test] -fn remote_network_stylesheet() { +#[tokio::test] +async fn remote_network_stylesheet() { let html = r#" <html> <head> @@ -640,7 +641,7 @@ h2 { color: red; } <h2>Smaller Text</h2> </body> </html>"#; - let inlined = inline(html); + let inlined = inline(html).await; assert_http( inlined, r#"<body> @@ -651,8 +652,8 @@ h2 { color: red; } ); } -#[test] -fn remote_network_stylesheet_invalid_url() { +#[tokio::test] +async fn remote_network_stylesheet_invalid_url() { let html = r#" <html> <head> @@ -661,7 +662,7 @@ fn remote_network_stylesheet_invalid_url() { <body> </body> </html>"#; - let error = inline(html).expect_err("Should fail"); + let error = inline(html).await.expect_err("Should fail"); #[cfg(feature = "http")] let expected = "builder error: empty host: http:"; #[cfg(not(feature = "http"))] @@ -669,8 +670,8 @@ fn remote_network_stylesheet_invalid_url() { assert_eq!(error.to_string(), expected); } -#[test] -fn remote_network_stylesheet_same_scheme() { +#[tokio::test] +async fn remote_network_stylesheet_same_scheme() { let html = r#" <html> <head> @@ -688,7 +689,7 @@ h2 { color: red; } let inliner = CSSInliner::options() .base_url(Some(Url::parse("http://127.0.0.1:1234").unwrap())) .build(); - let inlined = inliner.inline(html); + let inlined = inliner.inline(html).await; assert_http( inlined, r#"<body> @@ -699,8 +700,8 @@ h2 { color: red; } ); } -#[test] -fn remote_network_relative_stylesheet() { +#[tokio::test] +async fn remote_network_relative_stylesheet() { let html = r#" <html> <head> @@ -718,7 +719,7 @@ h2 { color: red; } let inliner = CSSInliner::options() .base_url(Some(Url::parse("http://127.0.0.1:1234").unwrap())) .build(); - let inlined = inliner.inline(html); + let inlined = inliner.inline(html).await; assert_http( inlined, r#"<body> @@ -729,8 +730,8 @@ h2 { color: red; } ); } -#[test] -fn file_scheme() { +#[tokio::test] +async fn file_scheme() { let html = r#" <html> <head> @@ -750,7 +751,7 @@ h2 { color: red; } ..Default::default() }; let inliner = CSSInliner::new(options); - let inlined = inliner.inline(html); + let inlined = inliner.inline(html).await; assert_file( inlined, r#"<body> @@ -761,8 +762,8 @@ h2 { color: red; } ); } -#[test] -fn customize_inliner() { +#[tokio::test] +async fn customize_inliner() { let options = InlineOptions { load_remote_stylesheets: false, ..Default::default() @@ -774,8 +775,8 @@ fn customize_inliner() { assert_eq!(options.preallocate_node_capacity, 25); } -#[test] -fn use_builder() { +#[tokio::test] +async fn use_builder() { let url = Url::parse("https://api.example.com").unwrap(); let _ = CSSInliner::options() .keep_style_tags(true) @@ -785,19 +786,19 @@ fn use_builder() { .build(); } -#[test] -fn inline_to() { +#[tokio::test] +async fn inline_to() { let html = html!("h1 { color: blue }", r#"<h1>Big Text</h1>"#); let mut out = Vec::new(); - css_inline::inline_to(&html, &mut out).unwrap(); + css_inline::inline_to(&html, &mut out).await.unwrap(); assert_eq!( String::from_utf8_lossy(&out), "<html><head></head><body><h1 style=\"color: blue;\">Big Text</h1></body></html>" ) } -#[test] -fn keep_style_tags() { +#[tokio::test] +async fn keep_style_tags() { let inliner = CSSInliner::options().keep_style_tags(true).build(); let html = r#" <html> @@ -810,12 +811,12 @@ h2 { color: red; } <h2></h2> </body> </html>"#; - let inlined = inliner.inline(html).unwrap(); + let inlined = inliner.inline(html).await.unwrap(); assert_eq!(inlined, "<html><head>\n<style>\nh2 { color: red; }\n</style>\n</head>\n<body>\n<h2 style=\"color: red;\"></h2>\n\n</body></html>"); } -#[test] -fn keep_link_tags() { +#[tokio::test] +async fn keep_link_tags() { let inliner = CSSInliner::options() .base_url(Some(Url::parse("http://127.0.0.1:1234").unwrap())) .keep_link_tags(true) @@ -829,7 +830,7 @@ fn keep_link_tags() { <h1></h1> </body> </html>"#; - let inlined = inliner.inline(html); + let inlined = inliner.inline(html).await; assert_http( inlined, "<html><head>\n<link href=\"external.css\" rel=\"stylesheet\">\n</head>\n<body>\n<h1 style=\"color: blue;\"></h1>\n\n</body></html>", diff --git a/css-inline/tests/test_selectors.rs b/css-inline/tests/test_selectors.rs index 75cd2f2d..f3e144ca 100644 --- a/css-inline/tests/test_selectors.rs +++ b/css-inline/tests/test_selectors.rs @@ -2,8 +2,8 @@ mod utils; // Most of the following tests are ported to Rust from https://github.com/rennat/pynliner -#[test] -fn identical_element() { +#[tokio::test] +async fn identical_element() { assert_inlined!( style = r#" .text-right { @@ -18,8 +18,8 @@ fn identical_element() { ) } -#[test] -fn is_or_prefixed_by() { +#[tokio::test] +async fn is_or_prefixed_by() { assert_inlined!( style = r#"[data-type|="thing"] {color: red;}"#, body = r#"<span data-type="thing">1</span>"#, @@ -32,8 +32,8 @@ fn is_or_prefixed_by() { ) } -#[test] -fn contains() { +#[tokio::test] +async fn contains() { assert_inlined!( style = r#"[data-type*="i"] {color: red;}"#, body = r#"<span data-type="thing">1</span>"#, @@ -41,8 +41,8 @@ fn contains() { ) } -#[test] -fn has_class_unicode() { +#[tokio::test] +async fn has_class_unicode() { assert_inlined!( style = r#".тест {color: red}"#, body = r#"<span class="тест">1</span>"#, @@ -50,8 +50,8 @@ fn has_class_unicode() { ) } -#[test] -fn has_class_short() { +#[tokio::test] +async fn has_class_short() { assert_inlined!( style = r#".t {color: red}"#, body = r#"<span class="test">1</span>"#, @@ -59,8 +59,8 @@ fn has_class_short() { ) } -#[test] -fn has_class_multiple() { +#[tokio::test] +async fn has_class_multiple() { assert_inlined!( style = r#".t {color: red}"#, body = r#"<span class="t e s t">1</span>"#, @@ -68,24 +68,24 @@ fn has_class_multiple() { ) } -#[test] -fn ends_with() { +#[tokio::test] +async fn ends_with() { assert_inlined!( style = r#"[data-type$="ng"] {color: red;}"#, body = r#"<span data-type="thing">1</span>"#, expected = r#"<span data-type="thing" style="color: red;">1</span>"# ) } -#[test] -fn starts_with() { +#[tokio::test] +async fn starts_with() { assert_inlined!( style = r#"[data-type^="th"] {color: red;}"#, body = r#"<span data-type="thing">1</span>"#, expected = r#"<span data-type="thing" style="color: red;">1</span>"# ) } -#[test] -fn one_of() { +#[tokio::test] +async fn one_of() { assert_inlined!( style = r#"[data-type~="thing1"] {color: red;}"#, body = r#"<span data-type="thing1 thing2">1</span>"#, @@ -97,8 +97,8 @@ fn one_of() { expected = r#"<span data-type="thing1 thing2" style="color: red;">1</span>"# ) } -#[test] -fn equals() { +#[tokio::test] +async fn equals() { assert_inlined!( style = r#"[data-type="thing"] {color: red;}"#, body = r#"<span data-type="thing">1</span>"#, @@ -110,16 +110,16 @@ fn equals() { expected = r#"<span data-type="thing" style="color: red;">1</span>"# ) } -#[test] -fn exists() { +#[tokio::test] +async fn exists() { assert_inlined!( style = r#"[data-type] {color: red;}"#, body = r#"<span data-type="thing">1</span>"#, expected = r#"<span data-type="thing" style="color: red;">1</span>"# ) } -#[test] -fn specificity() { +#[tokio::test] +async fn specificity() { assert_inlined!( style = r#"div,a,b,c,d,e,f,g,h,i,j { color: red; } .foo { color: blue; }"#, body = r#"<div class="foo"></div>"#, @@ -127,88 +127,88 @@ fn specificity() { ) } -#[test] -fn first_child_descendant_selector_complex_dom() { +#[tokio::test] +async fn first_child_descendant_selector_complex_dom() { assert_inlined!( style = r#"h1 :first-child { color: red; }"#, body = r#"<h1><div><span>Hello World!</span></div><p>foo</p><div class="barclass"><span>baz</span>bar</div></h1>"#, expected = r#"<h1><div style="color: red;"><span style="color: red;">Hello World!</span></div><p>foo</p><div class="barclass"><span style="color: red;">baz</span>bar</div></h1>"# ) } -#[test] -fn last_child_descendant_selector() { +#[tokio::test] +async fn last_child_descendant_selector() { assert_inlined!( style = r#"h1 :last-child { color: red; }"#, body = r#"<h1><div><span>Hello World!</span></div></h1>"#, expected = r#"<h1><div style="color: red;"><span style="color: red;">Hello World!</span></div></h1>"# ) } -#[test] -fn first_child_descendant_selector() { +#[tokio::test] +async fn first_child_descendant_selector() { assert_inlined!( style = r#"h1 :first-child { color: red; }"#, body = r#"<h1><div><span>Hello World!</span></div></h1>"#, expected = r#"<h1><div style="color: red;"><span style="color: red;">Hello World!</span></div></h1>"# ) } -#[test] -fn child_with_first_child_and_unmatched_class_selector_complex_dom() { +#[tokio::test] +async fn child_with_first_child_and_unmatched_class_selector_complex_dom() { assert_inlined!( style = r#"h1 > .hello:first-child { color: green; }"#, body = r#"<h1><span>Hello World!</span><p>foo</p><div class="barclass"><span>baz</span>bar</div></h1>"#, expected = r#"<h1><span>Hello World!</span><p>foo</p><div class="barclass"><span>baz</span>bar</div></h1>"# ) } -#[test] -fn child_with_first_child_and_class_selector_complex_dom() { +#[tokio::test] +async fn child_with_first_child_and_class_selector_complex_dom() { assert_inlined!( style = r#"h1 > .hello:first-child { color: green; }"#, body = r#"<h1><span class="hello">Hello World!</span><p>foo</p><div class="barclass"><span>baz</span>bar</div></h1>"#, expected = r#"<h1><span class="hello" style="color: green;">Hello World!</span><p>foo</p><div class="barclass"><span>baz</span>bar</div></h1>"# ) } -#[test] -fn nested_child_with_first_child_override_selector_complex_dom() { +#[tokio::test] +async fn nested_child_with_first_child_override_selector_complex_dom() { assert_inlined!( style = r#"div > div > * { color: green; } div > div > :first-child { color: red; }"#, body = r#"<div><div><span>Hello World!</span><p>foo</p><div class="barclass"><span>baz</span>bar</div></div></div>"#, expected = r#"<div><div><span style="color: red;">Hello World!</span><p style="color: green;">foo</p><div class="barclass" style="color: green;"><span style="color: red;">baz</span>bar</div></div></div>"# ) } -#[test] -fn child_with_first_and_last_child_override_selector() { +#[tokio::test] +async fn child_with_first_and_last_child_override_selector() { assert_inlined!( style = r#"p > * { color: green; } p > :first-child:last-child { color: red; }"#, body = r#"<p><span>Hello World!</span></p>"#, expected = r#"<p><span style="color: red;">Hello World!</span></p>"# ) } -#[test] -fn id_el_child_with_first_child_override_selector_complex_dom() { +#[tokio::test] +async fn id_el_child_with_first_child_override_selector_complex_dom() { assert_inlined!( style = r#"#abc > * { color: green; } #abc > :first-child { color: red; }"#, body = r#"<div id="abc"><span class="cde">Hello World!</span><p>foo</p><div class="barclass"><span>baz</span>bar</div></div>"#, expected = r#"<div id="abc"><span class="cde" style="color: red;">Hello World!</span><p style="color: green;">foo</p><div class="barclass" style="color: green;"><span>baz</span>bar</div></div>"# ) } -#[test] -fn child_with_first_child_override_selector_complex_dom() { +#[tokio::test] +async fn child_with_first_child_override_selector_complex_dom() { assert_inlined!( style = r#"div > * { color: green; } div > :first-child { color: red; }"#, body = r#"<div><span>Hello World!</span><p>foo</p><div class="barclass"><span>baz</span>bar</div></div>"#, expected = r#"<div><span style="color: red;">Hello World!</span><p style="color: green;">foo</p><div class="barclass" style="color: green;"><span style="color: red;">baz</span>bar</div></div>"# ) } -#[test] -fn child_follow_by_last_child_selector_complex_dom() { +#[tokio::test] +async fn child_follow_by_last_child_selector_complex_dom() { assert_inlined!( style = r#"h1 > :last-child { color: red; }"#, body = r#"<h1><span>Hello World!</span><p>foo</p><div class="barclass"><span>baz</span>bar</div></h1>"#, expected = r#"<h1><span>Hello World!</span><p>foo</p><div class="barclass" style="color: red;"><span>baz</span>bar</div></h1>"# ) } -#[test] -fn parent_pseudo_selector() { +#[tokio::test] +async fn parent_pseudo_selector() { assert_inlined!( style = r#"span:last-child span { color: red; }"#, body = r#"<h1><span><span>Hello World!</span></span></h1>"#, @@ -225,8 +225,8 @@ fn parent_pseudo_selector() { expected = r#"<h1><span><span>Hello World!</span></span><span>nope</span></h1>"# ) } -#[test] -fn multiple_pseudo_selectors() { +#[tokio::test] +async fn multiple_pseudo_selectors() { assert_inlined!( style = r#"span:first-child:last-child { color: red; }"#, body = r#"<h1><span>Hello World!</span></h1>"#, @@ -238,32 +238,32 @@ fn multiple_pseudo_selectors() { expected = r#"<h1><span>Hello World!</span><span>again!</span></h1>"# ) } -#[test] -fn last_child_selector() { +#[tokio::test] +async fn last_child_selector() { assert_inlined!( style = r#"h1 > :last-child { color: red; }"#, body = r#"<h1><span>Hello World!</span></h1>"#, expected = r#"<h1><span style="color: red;">Hello World!</span></h1>"# ) } -#[test] -fn child_follow_by_first_child_selector_complex_dom() { +#[tokio::test] +async fn child_follow_by_first_child_selector_complex_dom() { assert_inlined!( style = r#"h1 > :first-child { color: red; }"#, body = r#"<h1><span>Hello World!</span><p>foo</p><div class="barclass"><span>baz</span>bar</div></h1>"#, expected = r#"<h1><span style="color: red;">Hello World!</span><p>foo</p><div class="barclass"><span>baz</span>bar</div></h1>"# ) } -#[test] -fn child_follow_by_first_child_selector_with_comments() { +#[tokio::test] +async fn child_follow_by_first_child_selector_with_comments() { assert_inlined!( style = r#"h1 > :first-child { color: red; }"#, body = r#"<h1> <!-- enough said --><span>Hello World!</span><p>foo</p><div class="barclass"><span>baz</span>bar</div></h1>"#, expected = r#"<h1> <!-- enough said --><span style="color: red;">Hello World!</span><p>foo</p><div class="barclass"><span>baz</span>bar</div></h1>"# ) } -#[test] -fn child_follow_by_first_child_selector_with_white_spaces() { +#[tokio::test] +async fn child_follow_by_first_child_selector_with_white_spaces() { assert_inlined!( style = r#"h1 > :first-child { color: red; }"#, body = r#"<h1> <span>Hello World!</span><p>foo</p><div class="barclass"><span>baz</span>bar</div></h1>"#, @@ -271,8 +271,8 @@ fn child_follow_by_first_child_selector_with_white_spaces() { ) } -#[test] -fn child_follow_by_adjacent_selector_complex_dom() { +#[tokio::test] +async fn child_follow_by_adjacent_selector_complex_dom() { assert_inlined!( style = r#"h1 > span + p { color: red; }"#, body = r#"<h1><span>Hello World!</span><p>foo</p><div class="barclass"><span>baz</span>bar</div></h1>"#, @@ -280,88 +280,88 @@ fn child_follow_by_adjacent_selector_complex_dom() { ) } -#[test] -fn unknown_pseudo_selector() { +#[tokio::test] +async fn unknown_pseudo_selector() { assert_inlined!( style = r#"h1 > span:css4-selector { color: red; }"#, body = r#"<h1><span>Hello World!</span><p>foo</p><div class="barclass"><span>baz</span>bar</div></h1>"#, expected = r#"<h1><span>Hello World!</span><p>foo</p><div class="barclass"><span>baz</span>bar</div></h1>"# ) } -#[test] -fn adjacent_selector() { +#[tokio::test] +async fn adjacent_selector() { assert_inlined!( style = r#"h1 + h2 { color: red; }"#, body = r#"<h1>Hello World!</h1><h2>How are you?</h2>"#, expected = r#"<h1>Hello World!</h1><h2 style="color: red;">How are you?</h2>"# ) } -#[test] -fn child_all_selector_complex_dom() { +#[tokio::test] +async fn child_all_selector_complex_dom() { assert_inlined!( style = r#"h1 > * { color: red; }"#, body = r#"<h1><span>Hello World!</span><p>foo</p><div class="barclass"><span>baz</span>bar</div></h1>"#, expected = r#"<h1><span style="color: red;">Hello World!</span><p style="color: red;">foo</p><div class="barclass" style="color: red;"><span>baz</span>bar</div></h1>"# ) } -#[test] -fn child_selector_complex_dom() { +#[tokio::test] +async fn child_selector_complex_dom() { assert_inlined!( style = r#"h1 > span { color: red; }"#, body = r#"<h1><span>Hello World!</span><p>foo</p><div class="barclass"><span>baz</span>bar</div></h1>"#, expected = r#"<h1><span style="color: red;">Hello World!</span><p>foo</p><div class="barclass"><span>baz</span>bar</div></h1>"# ) } -#[test] -fn nested_child_selector() { +#[tokio::test] +async fn nested_child_selector() { assert_inlined!( style = r#"div > h1 > span { color: red; }""#, body = r#"<div><h1><span>Hello World!</span></h1></div>"#, expected = r#"<div><h1><span style="color: red;">Hello World!</span></h1></div>"# ) } -#[test] -fn child_selector() { +#[tokio::test] +async fn child_selector() { assert_inlined!( style = r#"h1 > span { color: red; }"#, body = r#"<h1><span>Hello World!</span></h1>"#, expected = r#"<h1><span style="color: red;">Hello World!</span></h1>"# ) } -#[test] -fn descendant_selector() { +#[tokio::test] +async fn descendant_selector() { assert_inlined!( style = r#"h1 span { color: red; }"#, body = r#"<h1><span>Hello World!</span></h1>"#, expected = r#"<h1><span style="color: red;">Hello World!</span></h1>"# ) } -#[test] -fn combination_selector() { +#[tokio::test] +async fn combination_selector() { assert_inlined!( style = r#"h1#a.b { color: red; }"#, body = r#"<h1 id="a" class="b">Hello World!</h1>"#, expected = r#"<h1 class="b" id="a" style="color: red;">Hello World!</h1>"# ) } -#[test] -fn conflicting_multiple_class_selector() { +#[tokio::test] +async fn conflicting_multiple_class_selector() { assert_inlined!( style = r#"h1.a.b { color: red; }"#, body = r#"<h1 class="a b">Hello World!</h1><h1 class="a">I should not be changed</h1>"#, expected = r#"<h1 class="a b" style="color: red;">Hello World!</h1><h1 class="a">I should not be changed</h1>"# ) } -#[test] -fn multiple_class_selector() { +#[tokio::test] +async fn multiple_class_selector() { assert_inlined!( style = r#"h1.a.b { color: red; }"#, body = r#"<h1 class="a b">Hello World!</h1>"#, expected = r#"<h1 class="a b" style="color: red;">Hello World!</h1>"# ) } -#[test] -fn missing_link_descendant_selector() { +#[tokio::test] +async fn missing_link_descendant_selector() { assert_inlined!( style = r#"#a b i { color: red }"#, body = r#"<div id="a"><i>x</i></div>"#, @@ -369,8 +369,8 @@ fn missing_link_descendant_selector() { ) } -#[test] -fn comma_specificity() { +#[tokio::test] +async fn comma_specificity() { assert_inlined!( style = r#"i, i { color: red; } i { color: blue; }"#, body = r#"<i>howdy</i>"#, @@ -378,8 +378,8 @@ fn comma_specificity() { ) } -#[test] -fn overwrite_comma() { +#[tokio::test] +async fn overwrite_comma() { assert_inlined!( style = r#"h1,h2,h3 {color: #000;}"#, body = r#"<h1 style="color: #fff">Foo</h1><h3 style="color: #fff">Foo</h3>"#, diff --git a/css-inline/tests/utils.rs b/css-inline/tests/utils.rs index bebc768b..4fa4358e 100644 --- a/css-inline/tests/utils.rs +++ b/css-inline/tests/utils.rs @@ -15,7 +15,7 @@ macro_rules! html { macro_rules! assert_inlined { (style = $style: expr, body = $body: expr, expected = $expected: expr) => {{ let html = html!($style, $body); - let inlined = css_inline::inline(&html).unwrap(); + let inlined = css_inline::inline(&html).await.unwrap(); assert_eq!(inlined, html!($expected)) }}; } diff --git a/profiler/src/main.rs b/profiler/src/main.rs index 79eac16b..c709dacc 100644 --- a/profiler/src/main.rs +++ b/profiler/src/main.rs @@ -35,7 +35,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> { for _ in 0..args.iterations { #[cfg(feature = "dhat-heap")] let _profiler = dhat::Profiler::new_heap(); - css_inline::inline_to(&benchmark.html, &mut output).expect("Inlining failed"); + css_inline::blocking::inline_to(&benchmark.html, &mut output).expect("Inlining failed"); output.clear(); } } else {