Skip to content

Commit 64161f7

Browse files
author
kamil-zych
committed
feat: Add keep_at_rules option
1 parent d0ee769 commit 64161f7

File tree

7 files changed

+192
-12
lines changed

7 files changed

+192
-12
lines changed

css-inline/src/html/attributes.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,8 @@ pub(crate) fn hash_class_name(name: &[u8]) -> u64 {
123123
}
124124

125125
/// A collection of HTML attributes.
126-
#[derive(Debug)]
126+
// TODO there must be a way for not having to instantiate Attributes class, see serializer.rs
127+
#[derive(Debug, Default)]
127128
pub(crate) struct Attributes {
128129
pub(crate) attributes: Vec<html5ever::Attribute>,
129130
/// The 'class' attribute value is separated for performance reasons.

css-inline/src/html/document.rs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -288,9 +288,11 @@ impl Document {
288288
styles: DocumentStyleMap<'_>,
289289
keep_style_tags: bool,
290290
keep_link_tags: bool,
291+
keep_at_rules: bool,
292+
at_rules: Option<String>,
291293
mode: InliningMode,
292294
) -> Result<(), InlineError> {
293-
serialize_to(self, writer, styles, keep_style_tags, keep_link_tags, mode)
295+
serialize_to(self, writer, styles, keep_style_tags, keep_link_tags, keep_at_rules, at_rules, mode)
294296
}
295297

296298
/// Filter this node iterator to elements matching the given selectors.
@@ -341,6 +343,8 @@ mod tests {
341343
IndexMap::default(),
342344
false,
343345
false,
346+
false,
347+
None,
344348
InliningMode::Document,
345349
)
346350
.expect("Failed to serialize");

css-inline/src/html/serializer.rs

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,17 @@ pub(crate) fn serialize_to<W: Write>(
1515
styles: DocumentStyleMap<'_>,
1616
keep_style_tags: bool,
1717
keep_link_tags: bool,
18+
keep_at_rules: bool,
19+
at_rules: Option<String>,
1820
mode: InliningMode,
1921
) -> Result<(), InlineError> {
2022
let sink = Sink::new(
2123
document,
2224
NodeId::document_id(),
2325
keep_style_tags,
2426
keep_link_tags,
27+
keep_at_rules,
28+
at_rules,
2529
mode,
2630
);
2731
let mut ser = HtmlSerializer::new(writer, styles);
@@ -34,6 +38,9 @@ struct Sink<'a> {
3438
node: NodeId,
3539
keep_style_tags: bool,
3640
keep_link_tags: bool,
41+
// TODO this is not used here but I might need it for simplifying serialization logic below
42+
keep_at_rules: bool,
43+
at_rules: Option<String>,
3744
inlining_mode: InliningMode,
3845
}
3946

@@ -43,13 +50,17 @@ impl<'a> Sink<'a> {
4350
node: NodeId,
4451
keep_style_tags: bool,
4552
keep_link_tags: bool,
53+
keep_at_rules: bool,
54+
at_rules: Option<String>,
4655
inlining_mode: InliningMode,
4756
) -> Sink<'a> {
4857
Sink {
4958
document,
5059
node,
5160
keep_style_tags,
5261
keep_link_tags,
62+
keep_at_rules,
63+
at_rules,
5364
inlining_mode,
5465
}
5566
}
@@ -60,6 +71,8 @@ impl<'a> Sink<'a> {
6071
node,
6172
self.keep_style_tags,
6273
self.keep_link_tags,
74+
self.keep_at_rules,
75+
self.at_rules.clone(),
6376
self.inlining_mode,
6477
)
6578
}
@@ -114,6 +127,22 @@ impl<'a> Sink<'a> {
114127

115128
serializer.start_elem(&element.name, &element.attributes, style_node_id)?;
116129

130+
// TODO this part is the one that I don't like the most
131+
if element.name.local == local_name!("head") {
132+
if let Some(at_rules) = &self.at_rules {
133+
if !at_rules.is_empty() {
134+
serializer.start_elem(
135+
&QualName::new(None, ns!(html), local_name!("style")),
136+
&Attributes::default(),
137+
None,
138+
)?;
139+
serializer.write_text(at_rules)?;
140+
serializer
141+
.end_elem(&QualName::new(None, ns!(html), local_name!("style")))?;
142+
}
143+
}
144+
}
145+
117146
self.serialize_children(serializer)?;
118147

119148
serializer.end_elem(&element.name)?;
@@ -575,6 +604,8 @@ mod tests {
575604
IndexMap::default(),
576605
true,
577606
false,
607+
false,
608+
None,
578609
InliningMode::Document,
579610
)
580611
.expect("Should not fail");
@@ -594,6 +625,8 @@ mod tests {
594625
IndexMap::default(),
595626
false,
596627
false,
628+
false,
629+
None,
597630
InliningMode::Document,
598631
)
599632
.expect("Should not fail");
@@ -613,6 +646,8 @@ mod tests {
613646
IndexMap::default(),
614647
false,
615648
false,
649+
false,
650+
None,
616651
InliningMode::Document,
617652
)
618653
.expect("Should not fail");
@@ -632,6 +667,8 @@ mod tests {
632667
IndexMap::default(),
633668
false,
634669
false,
670+
false,
671+
None,
635672
InliningMode::Document,
636673
)
637674
.expect("Should not fail");
@@ -654,9 +691,13 @@ mod tests {
654691
IndexMap::default(),
655692
false,
656693
false,
694+
false,
695+
None,
657696
InliningMode::Document,
658697
)
659698
.expect("Should not fail");
660699
assert_eq!(buffer, b"<!DOCTYPE html><html><head></head><body data-foo=\"&amp; &nbsp; &quot;\"></body></html>");
661700
}
701+
702+
// TODO adding a test case for `keep_at_rules` would be nice
662703
}

css-inline/src/lib.rs

Lines changed: 35 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,8 @@ pub struct InlineOptions<'a> {
6262
pub keep_style_tags: bool,
6363
/// Keep "link" tags after inlining.
6464
pub keep_link_tags: bool,
65+
/// Keep "@..." and wrap them into a "style" tag.
66+
pub keep_at_rules: bool,
6567
/// Used for loading external stylesheets via relative URLs.
6668
pub base_url: Option<Url>,
6769
/// Whether remote stylesheets should be loaded or not.
@@ -123,6 +125,13 @@ impl<'a> InlineOptions<'a> {
123125
self
124126
}
125127

128+
/// Override whether "@..." rules should be kept after processing.
129+
#[must_use]
130+
pub fn keep_at_rules(mut self, keep_at_rules: bool) -> Self {
131+
self.keep_at_rules = keep_at_rules;
132+
self
133+
}
134+
126135
/// Set base URL that will be used for loading external stylesheets via relative URLs.
127136
#[must_use]
128137
pub fn base_url(mut self, base_url: Option<Url>) -> Self {
@@ -184,6 +193,7 @@ impl Default for InlineOptions<'_> {
184193
inline_style_tags: true,
185194
keep_style_tags: false,
186195
keep_link_tags: false,
196+
keep_at_rules: false,
187197
base_url: None,
188198
load_remote_stylesheets: true,
189199
#[cfg(feature = "stylesheet-cache")]
@@ -432,14 +442,29 @@ impl<'a> CSSInliner<'a> {
432442
.max(16),
433443
);
434444
let mut rule_list = Vec::with_capacity(declarations.capacity() / 3);
435-
for rule in cssparser::StyleSheetParser::new(
436-
&mut parser,
437-
&mut parser::CSSRuleListParser::new(&mut declarations),
438-
)
439-
.flatten()
440-
{
441-
rule_list.push(rule);
442-
}
445+
let at_rules = if self.options.keep_at_rules {
446+
let mut at_rules = String::new();
447+
for rule in cssparser::StyleSheetParser::new(
448+
&mut parser,
449+
&mut parser::AtRuleFilteringParser::new(&mut declarations, &mut at_rules),
450+
)
451+
.flatten()
452+
{
453+
rule_list.push(rule);
454+
}
455+
// TODO debug it
456+
Some(at_rules)
457+
} else {
458+
for rule in cssparser::StyleSheetParser::new(
459+
&mut parser,
460+
&mut parser::CSSRuleListParser::new(&mut declarations),
461+
)
462+
.flatten()
463+
{
464+
rule_list.push(rule);
465+
}
466+
None
467+
};
443468
// This cache is unused but required in the `selectors` API
444469
let mut caches = SelectorCaches::default();
445470
for (selectors, (start, end)) in &rule_list {
@@ -496,6 +521,8 @@ impl<'a> CSSInliner<'a> {
496521
styles,
497522
self.options.keep_style_tags,
498523
self.options.keep_link_tags,
524+
self.options.keep_at_rules,
525+
at_rules,
499526
mode,
500527
)?;
501528
Ok(())

css-inline/src/main.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
5353
inline_style_tags: bool,
5454
keep_style_tags: bool,
5555
keep_link_tags: bool,
56+
keep_at_rules: bool,
5657
base_url: Option<String>,
5758
extra_css: Option<String>,
5859
output_filename_prefix: Option<OsString>,
@@ -70,6 +71,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
7071
inline_style_tags: true,
7172
keep_style_tags: false,
7273
keep_link_tags: false,
74+
keep_at_rules: false,
7375
base_url: None,
7476
extra_css: None,
7577
output_filename_prefix: None,
@@ -199,6 +201,9 @@ OPTIONS:
199201
200202
--keep-link-tags
201203
Keep "link" tags after inlining.
204+
205+
--keep-at-rules
206+
Keep "@..." rules after inlining.
202207
203208
--base-url
204209
Used for loading external stylesheets via relative URLs.
@@ -284,6 +289,7 @@ OPTIONS:
284289
inline_style_tags: args.inline_style_tags,
285290
keep_style_tags: args.keep_style_tags,
286291
keep_link_tags: args.keep_link_tags,
292+
keep_at_rules: args.keep_at_rules,
287293
base_url,
288294
load_remote_stylesheets: args.load_remote_stylesheets,
289295
#[cfg(feature = "stylesheet-cache")]

css-inline/src/parser.rs

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,3 +98,81 @@ impl<'i> cssparser::QualifiedRuleParser<'i> for CSSDeclarationListParser {
9898
type QualifiedRule = Declaration<'i>;
9999
type Error = ();
100100
}
101+
102+
pub(crate) struct AtRuleFilteringParser<'d, 'i, 'o> {
103+
declarations: &'d mut Vec<Declaration<'i>>,
104+
at_rules: &'o mut String,
105+
}
106+
107+
impl<'d, 'i, 'o> AtRuleFilteringParser<'d, 'i, 'o> {
108+
#[inline]
109+
pub(crate) fn new(
110+
declarations: &'d mut Vec<Declaration<'i>>,
111+
at_rules: &'o mut String,
112+
) -> AtRuleFilteringParser<'d, 'i, 'o> {
113+
AtRuleFilteringParser {
114+
declarations,
115+
at_rules,
116+
}
117+
}
118+
}
119+
120+
impl<'i> cssparser::QualifiedRuleParser<'i> for AtRuleFilteringParser<'_, 'i, '_> {
121+
type Prelude = &'i str;
122+
type QualifiedRule = QualifiedRule<'i>;
123+
type Error = ();
124+
125+
fn parse_prelude<'t>(
126+
&mut self,
127+
input: &mut cssparser::Parser<'i, 't>,
128+
) -> Result<Self::Prelude, cssparser::ParseError<'i, Self::Error>> {
129+
Ok(exhaust(input))
130+
}
131+
132+
fn parse_block<'t>(
133+
&mut self,
134+
prelude: Self::Prelude,
135+
_: &ParserState,
136+
input: &mut cssparser::Parser<'i, 't>,
137+
) -> Result<Self::QualifiedRule, cssparser::ParseError<'i, Self::Error>> {
138+
let mut parser = CSSDeclarationListParser;
139+
let parser = cssparser::RuleBodyParser::new(input, &mut parser);
140+
let start = self.declarations.len();
141+
for item in parser.flatten() {
142+
self.declarations.push(item);
143+
}
144+
Ok((prelude, (start, self.declarations.len())))
145+
}
146+
}
147+
148+
impl<'i> cssparser::AtRuleParser<'i> for AtRuleFilteringParser<'_, 'i, '_> {
149+
type Prelude = &'i str;
150+
type AtRule = QualifiedRule<'i>;
151+
type Error = ();
152+
153+
fn parse_prelude<'t>(
154+
&mut self,
155+
name: cssparser::CowRcStr<'i>,
156+
input: &mut cssparser::Parser<'i, 't>,
157+
) -> Result<Self::Prelude, cssparser::ParseError<'i, Self::Error>> {
158+
// TODO pushing @ feels odd, there should be a less dumb way of doing it?
159+
self.at_rules.push_str("@");
160+
self.at_rules.push_str(&name);
161+
Ok(exhaust(input))
162+
}
163+
164+
fn parse_block<'t>(
165+
&mut self,
166+
prelude: Self::Prelude,
167+
_start: &ParserState,
168+
input: &mut cssparser::Parser<'i, 't>,
169+
) -> Result<Self::AtRule, cssparser::ParseError<'i, Self::Error>> {
170+
// TODO same here, pushing braces manually feels odd
171+
let start = self.declarations.len();
172+
self.at_rules.push_str(prelude);
173+
self.at_rules.push_str(" {");
174+
self.at_rules.push_str(exhaust(input));
175+
self.at_rules.push_str("}");
176+
Ok((prelude, (start, self.declarations.len())))
177+
}
178+
}

0 commit comments

Comments
 (0)