Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions lib/css_inline.ex
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,10 @@ defmodule CSSInline do
* `:load_remote_stylesheets` - Whether to load remote stylesheets referenced in `<link>` tags.
Defaults to `true`. Set to `false` to skip external stylesheets.
* `:minify_css` - Whether to minify the inlined CSS. Defaults to `true`.
* `:remove_inlined_selectors` - Whether to remove selectors from `<style>` tags after they've
been successfully inlined. Useful with `:keep_style_tags` to avoid conflicts between retained
`<style>` rules and inlined styles (e.g. `!important` specificity issues in email clients).
Defaults to `false`.
* `:check_depth` - Whether to check HTML nesting depth before inlining. Defaults to `true`.
* `:max_depth` - Maximum allowed HTML nesting depth. Documents exceeding this return
`{:error, :nesting_depth_exceeded}`. Only applies when `:check_depth` is `true`. Defaults to `128`.
Expand All @@ -51,6 +55,7 @@ defmodule CSSInline do
keep_link_tags: false,
load_remote_stylesheets: true,
minify_css: true,
remove_inlined_selectors: false,
check_depth: true,
max_depth: 128

Expand All @@ -60,6 +65,7 @@ defmodule CSSInline do
keep_link_tags: boolean(),
load_remote_stylesheets: boolean(),
minify_css: boolean(),
remove_inlined_selectors: boolean(),
check_depth: boolean(),
max_depth: pos_integer()
}
Expand All @@ -71,6 +77,7 @@ defmodule CSSInline do
| {:keep_link_tags, boolean()}
| {:load_remote_stylesheets, boolean()}
| {:minify_css, boolean()}
| {:remove_inlined_selectors, boolean()}
| {:check_depth, boolean()}
| {:max_depth, pos_integer()}

Expand All @@ -86,6 +93,8 @@ defmodule CSSInline do
* `:keep_link_tags` - Whether to keep `<link>` tags after processing. Defaults to `false`.
* `:load_remote_stylesheets` - Whether to load remote stylesheets. Defaults to `true`.
* `:minify_css` - Whether to minify the inlined CSS. Defaults to `true`.
* `:remove_inlined_selectors` - Whether to remove selectors from `<style>` tags after they've
been successfully inlined. Defaults to `false`.
* `:check_depth` - Whether to check HTML nesting depth before inlining. Defaults to `true`.
* `:max_depth` - Maximum allowed HTML nesting depth. Defaults to `128`.
"""
Expand Down Expand Up @@ -116,6 +125,8 @@ defmodule CSSInline do
* `:keep_link_tags` - Whether to keep `<link>` tags after processing. Defaults to `false`.
* `:load_remote_stylesheets` - Whether to load remote stylesheets. Defaults to `true`.
* `:minify_css` - Whether to minify the inlined CSS. Defaults to `true`.
* `:remove_inlined_selectors` - Whether to remove selectors from `<style>` tags after they've
been successfully inlined. Defaults to `false`.
* `:check_depth` - Whether to check HTML nesting depth before inlining. Defaults to `true`.
* `:max_depth` - Maximum allowed HTML nesting depth. Defaults to `128`.
"""
Expand Down
2 changes: 2 additions & 0 deletions native/css_inline_nif/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ struct Options {
keep_link_tags: bool,
load_remote_stylesheets: bool,
minify_css: bool,
remove_inlined_selectors: bool,
check_depth: bool,
max_depth: usize,
}
Expand Down Expand Up @@ -97,6 +98,7 @@ fn inline_css(html: &str, opts: Options) -> Result<Vec<u8>, RustlerError> {
.keep_link_tags(opts.keep_link_tags)
.load_remote_stylesheets(opts.load_remote_stylesheets)
.minify_css(opts.minify_css)
.remove_inlined_selectors(opts.remove_inlined_selectors)
.build();

inliner
Expand Down
74 changes: 74 additions & 0 deletions test/css_inline_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -240,6 +240,80 @@ defmodule CSSInlineTest do
end
end

describe "!important handling" do
@email_html """
<html>
<head>
<style>
a[x-apple-data-detectors],
u + #body a {
color: inherit !important;
text-decoration: none !important;
font-size: inherit !important;
font-family: inherit !important;
font-weight: inherit !important;
line-height: inherit !important;
}
.button a {
color: #ffffff !important;
font-size: 16px !important;
font-weight: bold !important;
}
</style>
</head>
<body id="body">
<u>
<div class="button">
<a href="https://example.com">Click Me</a>
</div>
</u>
</body>
</html>
"""

test "preserves !important when inlining styles" do
assert {:ok, result} = CSSInline.inline(@email_html)
assert result =~ ~r/style="[^"]*!important/
end

@production_opts [
load_remote_stylesheets: false,
keep_link_tags: true,
keep_style_tags: true
]

test "production settings: inlined selectors remain in <style> without remove_inlined_selectors" do
assert {:ok, result} = CSSInline.inline(@email_html, @production_opts)

assert result =~ ~r/style="[^"]*!important/,
"Inlined styles should preserve !important"

[_, style_content] = Regex.run(~r/<style[^>]*>(.*?)<\/style>/s, result)

assert style_content =~ "u + #body a",
"Non-inlinable selectors should remain in <style> tag"

assert style_content =~ ".button a",
"Without remove_inlined_selectors, inlined selectors remain in <style>"
end

test "production settings + remove_inlined_selectors strips inlined rules from <style>" do
{:ok, result} =
CSSInline.inline(@email_html, [remove_inlined_selectors: true] ++ @production_opts)

assert result =~ ~r/style="[^"]*color:[^"]*!important/,
"Inlined styles should preserve !important"

[_, style_content] = Regex.run(~r/<style[^>]*>(.*?)<\/style>/s, result)

assert style_content =~ "u + #body a",
"Non-inlinable selectors (complex email client overrides) must remain"

refute style_content =~ ".button a",
"Inlined selectors should be removed from <style> to prevent conflicts"
end
end

describe "regression tests" do
test "returns error for deeply nested HTML" do
html = File.read!("test/fixtures/deeply_nested.html")
Expand Down
Loading