Skip to content

Instantly share code, notes, and snippets.

@inxeoz
Created February 8, 2025 18:59
Show Gist options
  • Select an option

  • Save inxeoz/9716d832ee97a940016a56bb6fac4142 to your computer and use it in GitHub Desktop.

Select an option

Save inxeoz/9716d832ee97a940016a56bb6fac4142 to your computer and use it in GitHub Desktop.
combineHTMLandCSS.rs
use kuchiki::traits::*;
use std::collections::HashMap;
/// A simple structure to hold one CSS rule.
struct Rule {
/// One or more selectors (for example, "header nav ul" or "body")
selectors: Vec<String>,
/// A map of CSS property to value for this rule.
declarations: HashMap<String, String>,
}
/// Remove comments (/* … */) from the CSS source.
fn remove_css_comments(css: &str) -> String {
let mut result = String::with_capacity(css.len());
let mut chars = css.chars().peekable();
while let Some(ch) = chars.next() {
if ch == '/' && chars.peek() == Some(&'*') {
// Skip until end of comment.
chars.next(); // skip '*'
while let Some(ch) = chars.next() {
if ch == '*' && chars.peek() == Some(&'/') {
chars.next(); // skip '/'
break;
}
}
} else {
result.push(ch);
}
}
result
}
/// A very simple CSS parser that returns a list of rules.
/// This parser assumes the CSS is in the form:
/// selector { property: value; property: value; }
fn parse_css(css: &str) -> Vec<Rule> {
let mut rules = Vec::new();
let css = remove_css_comments(css);
// Split by "}" to get each block.
for block in css.split('}') {
if let Some((selectors, declarations)) = block.split_once('{') {
// The selectors may be a comma-separated list.
let selectors = selectors
.split(',')
.map(|s| s.trim().to_string())
.filter(|s| !s.is_empty())
.collect::<Vec<_>>();
let mut decl_map = HashMap::new();
for decl in declarations.split(';') {
let decl = decl.trim();
if decl.is_empty() {
continue;
}
if let Some((prop, val)) = decl.split_once(':') {
decl_map.insert(prop.trim().to_string(), val.trim().to_string());
}
}
if !selectors.is_empty() && !decl_map.is_empty() {
rules.push(Rule {
selectors,
declarations: decl_map,
});
}
}
}
rules
}
/// Parse an inline style attribute (if any) into a HashMap.
fn parse_inline_style(style: Option<&str>) -> HashMap<String, String> {
let mut map = HashMap::new();
if let Some(style_str) = style {
for decl in style_str.split(';') {
let decl = decl.trim();
if decl.is_empty() {
continue;
}
if let Some((prop, val)) = decl.split_once(':') {
map.insert(prop.trim().to_string(), val.trim().to_string());
}
}
}
map
}
/// Main function that reads HTML and CSS, inlines the CSS rules,
/// and prints the resulting HTML.
fn main() {
let html = r##"<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Simple Demo</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<header>
<h1>Welcome!</h1>
<nav>
<ul>
<li><a href="#">Home</a></li>
<li><a href="#">About</a></li>
<li><a href="#">Services</a></li>
<li><a href="#">Contact</a></li>
</ul>
</nav>
</header>
<main>
<section id="hero">
<h2>This is the Hero Section</h2>
<p>A brief description or call to action here.</p>
<button>Learn More</button>
</section>
<section id="content">
<article>
<h3>Article Title</h3>
<p>Some sample content for the article.</p>
</article>
<article>
<h3>Another Article Title</h3>
<p>More sample content for the article.</p>
</article>
</section>
</main>
<footer>
<p>&copy; 2025 My Demo Site</p>
</footer>
</body>
</html>"##;
// Your CSS code.
let css = r#"/* General Styles */
body {
font-family: sans-serif;
margin: 0;
padding: 0;
background-color: #f4f4f4;
color: #333;
}
/* Header */
header {
background-color: #333;
color: #fff;
padding: 10px 0;
text-align: center;
}
header nav ul {
padding: 0;
list-style: none;
}
header nav ul li {
display: inline;
margin: 0 10px;
}
header nav a {
color: #fff;
text-decoration: none;
}
/* Main */
main {
padding: 20px;
}
#hero {
background-color: #e0e0e0;
padding: 20px;
text-align: center;
margin-bottom: 20px;
}
#hero button {
background-color: #333;
color: #fff;
padding: 10px 20px;
border: none;
cursor: pointer;
}
#content {
display: flex;
justify-content: space-around; /* Distribute articles evenly */
}
#content article {
width: 45%; /* Adjust as needed */
background-color: #fff;
padding: 15px;
border: 1px solid #ddd;
}
/* Footer */
footer {
background-color: #333;
color: #fff;
text-align: center;
padding: 10px 0;
position: fixed;
bottom: 0;
width: 100%;
}"#;
// Parse the CSS into rules.
let rules = parse_css(css);
// Parse the HTML document.
let document = kuchiki::parse_html().one(html);
// Optionally, remove the external CSS <link> tag.
for css_link in document.select("link[rel=stylesheet]").unwrap() {
css_link.as_node().detach();
}
// For each rule and each selector in that rule, select matching elements
// and merge the rule’s declarations into the element’s inline style.
for rule in &rules {
for selector in &rule.selectors {
if let Ok(mut nodes) = document.select(selector) {
for css_match in nodes {
// Get the element as a mutable NodeRef.
let as_node = css_match.as_node();
if let Some(element) = as_node.as_element() {
// Borrow the attributes.
let mut attrs = element.attributes.borrow_mut();
// Parse any existing inline style.
let mut style_map = parse_inline_style(attrs.get("style"));
// Merge/overwrite with the rule's declarations.
for (prop, val) in &rule.declarations {
style_map.insert(prop.clone(), val.clone());
}
// Reconstruct the inline style string.
let new_style = style_map
.into_iter()
.map(|(prop, val)| format!("{}: {};", prop, val))
.collect::<Vec<_>>()
.join(" ");
attrs.insert("style", new_style);
}
}
}
}
}
// Print the modified HTML.
println!("{}", document.to_string());
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment