Skip to content

Commit ea74258

Browse files
committed
Initial commit
0 parents  commit ea74258

File tree

7 files changed

+281
-0
lines changed

7 files changed

+281
-0
lines changed

.gitignore

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
# Generated by Cargo
2+
# will have compiled files and executables
3+
/target/
4+
5+
# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries
6+
# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html
7+
Cargo.lock
8+
9+
# These are backup files generated by rustfmt
10+
**/*.rs.bk
11+
12+
.idea

Cargo.toml

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
[package]
2+
name = "css-inline"
3+
version = "0.1.0"
4+
authors = ["Dmitry Dygalo <[email protected]>"]
5+
edition = "2018"
6+
7+
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
8+
9+
[dependencies]
10+
html5ever = "*"
11+
cssparser = "*"
12+
kuchiki = "0.8"

LICENSE

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
MIT License
2+
3+
Copyright (c) 2020 Dmitry Dygalo
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy
6+
of this software and associated documentation files (the "Software"), to deal
7+
in the Software without restriction, including without limitation the rights
8+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
copies of the Software, and to permit persons to whom the Software is
10+
furnished to do so, subject to the following conditions:
11+
12+
The above copyright notice and this permission notice shall be included in all
13+
copies or substantial portions of the Software.
14+
15+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
SOFTWARE.

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
# css-inline
2+
Inline CSS into style attributes

src/lib.rs

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
use crate::parse::Declaration;
2+
use kuchiki::traits::TendrilSink;
3+
use kuchiki::{parse_html, ElementData, NodeDataRef, Selectors};
4+
use std::io;
5+
6+
mod parse;
7+
8+
#[derive(Debug)]
9+
struct Rule {
10+
selectors: kuchiki::Selectors,
11+
declarations: Vec<Declaration>,
12+
}
13+
14+
impl Rule {
15+
pub fn new(selectors: &str, declarations: Vec<Declaration>) -> Result<Rule, ()> {
16+
Ok(Rule {
17+
selectors: Selectors::compile(selectors)?,
18+
declarations,
19+
})
20+
}
21+
}
22+
23+
fn process_style_node(node: NodeDataRef<ElementData>) -> Vec<Rule> {
24+
let css = node.text_contents();
25+
let mut parse_input = cssparser::ParserInput::new(css.as_str());
26+
let mut parser = parse::CSSParser::new(&mut parse_input);
27+
parser
28+
.parse()
29+
.filter_map(|r| {
30+
r.map(|(selector, declarations)| Rule::new(&selector, declarations))
31+
.ok()
32+
})
33+
.collect::<Result<Vec<_>, _>>()
34+
.unwrap()
35+
}
36+
37+
/// Inline CSS styles from <style> tags to matching elements in the HTML tree.
38+
pub fn inline(html: &str) -> Result<String, io::Error> {
39+
let document = parse_html().one(html);
40+
let rules = document
41+
.select("style")
42+
.unwrap()
43+
.map(process_style_node)
44+
.flatten();
45+
46+
for rule in rules {
47+
let matching_elements = document
48+
.inclusive_descendants()
49+
.filter_map(|node| node.into_element_ref())
50+
.filter(|element| rule.selectors.matches(element));
51+
for matching_element in matching_elements {
52+
let style = rule
53+
.declarations
54+
.iter()
55+
.map(|&(ref key, ref value)| format!("{}:{};", key, value));
56+
matching_element
57+
.attributes
58+
.borrow_mut()
59+
.insert("style", style.collect());
60+
}
61+
}
62+
63+
let mut out = vec![];
64+
document
65+
.select("html")
66+
.unwrap()
67+
.next()
68+
.unwrap()
69+
.as_node()
70+
.serialize(&mut out)?;
71+
Ok(String::from_utf8_lossy(&out).to_string())
72+
}
73+
74+
#[cfg(test)]
75+
mod tests {
76+
use crate::*;
77+
78+
const HTML: &str = r#"<html>
79+
<head>
80+
<title>Test</title>
81+
<style>
82+
h1, h2 { color:red; }
83+
strong {
84+
text-decoration:none
85+
}
86+
p { font-size:2px }
87+
p.footer { font-size: 1px}
88+
</style>
89+
</head>
90+
<body>
91+
<h1>Hi!</h1>
92+
<p><strong>Yes!</strong></p>
93+
<p class="footer">Feetnuts</p>
94+
</body>
95+
</html>"#;
96+
97+
#[test]
98+
fn test_inline() {
99+
let inlined = inline(HTML).unwrap();
100+
assert_eq!(
101+
inlined,
102+
r#"<html><head>
103+
<title>Test</title>
104+
<style>
105+
h1, h2 { color:red; }
106+
strong {
107+
text-decoration:none
108+
}
109+
p { font-size:2px }
110+
p.footer { font-size: 1px}
111+
</style>
112+
</head>
113+
<body>
114+
<h1 style="color:red;">Hi!</h1>
115+
<p style="font-size:2px ;"><strong style="text-decoration:none
116+
;">Yes!</strong></p>
117+
<p class="footer" style="font-size: 1px;">Feetnuts</p>
118+
119+
</body></html>"#
120+
)
121+
}
122+
}

src/main.rs

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
use css_inline::inline;
2+
3+
const HTML: &str = r#"<html>
4+
<head>
5+
<title>Test</title>
6+
<style>
7+
h1, h2 { color:red; }
8+
strong {
9+
text-decoration:none
10+
}
11+
p { font-size:2px }
12+
p.footer { font-size: 1px}
13+
</style>
14+
</head>
15+
<body>
16+
<h1>Hi!</h1>
17+
<p><strong>Yes!</strong></p>
18+
<p class="footer">Feetnuts</p>
19+
</body>
20+
</html>"#;
21+
22+
fn main() {
23+
for _ in 0..1000 {
24+
let _ = inline(HTML);
25+
}
26+
}

src/parse.rs

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
pub struct CSSRuleListParser;
2+
struct CSSDeclarationListParser;
3+
4+
pub type Declaration = (String, String);
5+
pub type QualifiedRule = (String, Vec<Declaration>);
6+
7+
fn exhaust(input: &mut cssparser::Parser) -> String {
8+
let start = input.position();
9+
while input.next().is_ok() {}
10+
input.slice_from(start).to_string()
11+
}
12+
13+
impl<'i> cssparser::QualifiedRuleParser<'i> for CSSRuleListParser {
14+
type Prelude = String;
15+
type QualifiedRule = QualifiedRule;
16+
type Error = ();
17+
18+
fn parse_prelude<'t>(
19+
&mut self,
20+
input: &mut cssparser::Parser<'i, 't>,
21+
) -> Result<Self::Prelude, cssparser::ParseError<'i, Self::Error>> {
22+
let _ = input;
23+
Ok(exhaust(input))
24+
}
25+
26+
fn parse_block<'t>(
27+
&mut self,
28+
prelude: Self::Prelude,
29+
_: cssparser::SourceLocation,
30+
input: &mut cssparser::Parser<'i, 't>,
31+
) -> Result<Self::QualifiedRule, cssparser::ParseError<'i, Self::Error>> {
32+
let parser = cssparser::DeclarationListParser::new(input, CSSDeclarationListParser);
33+
let mut declarations = vec![];
34+
35+
for item in parser {
36+
if let Ok(declaration) = item {
37+
declarations.push(declaration);
38+
}
39+
}
40+
41+
Ok((prelude, declarations))
42+
}
43+
}
44+
45+
impl<'i> cssparser::DeclarationParser<'i> for CSSDeclarationListParser {
46+
type Declaration = Declaration;
47+
type Error = ();
48+
49+
fn parse_value<'t>(
50+
&mut self,
51+
name: cssparser::CowRcStr<'i>,
52+
input: &mut cssparser::Parser<'i, 't>,
53+
) -> Result<Self::Declaration, cssparser::ParseError<'i, Self::Error>> {
54+
Ok((name.to_string(), exhaust(input)))
55+
}
56+
}
57+
58+
impl cssparser::AtRuleParser<'_> for CSSRuleListParser {
59+
type PreludeNoBlock = String;
60+
type PreludeBlock = String;
61+
type AtRule = QualifiedRule;
62+
type Error = ();
63+
}
64+
65+
impl cssparser::AtRuleParser<'_> for CSSDeclarationListParser {
66+
type PreludeNoBlock = String;
67+
type PreludeBlock = String;
68+
type AtRule = Declaration;
69+
type Error = ();
70+
}
71+
72+
pub struct CSSParser<'i, 't> {
73+
input: cssparser::Parser<'i, 't>,
74+
}
75+
76+
impl<'i: 't, 't> CSSParser<'i, 't> {
77+
pub fn new(css: &'t mut cssparser::ParserInput<'i>) -> CSSParser<'i, 't> {
78+
CSSParser {
79+
input: cssparser::Parser::new(css),
80+
}
81+
}
82+
83+
pub fn parse<'a>(&'a mut self) -> cssparser::RuleListParser<'i, 't, 'a, CSSRuleListParser> {
84+
cssparser::RuleListParser::new_for_stylesheet(&mut self.input, CSSRuleListParser)
85+
}
86+
}

0 commit comments

Comments
 (0)