on master help sign in
Minify produced HTML assets

Remove all unnecessary whitespace from generated HTML output: - Collapse CSS rules to single line - Remove newlines between HTML elements - Minify boilerplate template

commit2b496b9ff1b7dc411bc87d1fbb51622f6695fa47 authoriko <mail@iko.soy> dateTue 14 Apr 2026, 14:48 (6 months ago) parent1540416
2 files changed+10−85
examples/demo.html+1−57 src/codegen.rs+9−28
examples/demo.html+1−57
@@ -1,57 +1 @@
1−<!DOCTYPE html>
2−<html>
3−<head>
4−<meta charset="utf-8">
5−<meta name="viewport" content="width=device-width, initial-scale=1">
6−<title>htmlang Demo</title>
7−<style>
8−*, *::before, *::after { box-sizing: border-box; }
9−body { margin: 0; font-family: system-ui, -apple-system, sans-serif; }
10−img { display: block; }
11−._0{display:flex;flex-direction:column;max-width:800px;margin-left:auto;margin-right:auto;padding:40px;gap:20px;}
12−._1{display:flex;flex-direction:row;gap:20px;}
13−._2{display:flex;flex-direction:column;flex:1;min-width:0;gap:10px;}
14−._3{font-weight:bold;font-size:32px;}
15−._4{margin:0;}
16−._5{font-weight:bold;color:#3b82f6;}
17−._6{width:80px;height:80px;border-radius:40px;}
18−._7{display:flex;flex-direction:column;gap:10px;}
19−._8{font-weight:bold;font-size:24px;color:#333;}
20−._9{display:flex;flex-direction:row;flex-wrap:wrap;gap:10px;}
21−._10{display:flex;flex-direction:column;padding:20px;background:white;border-radius:8px;border:1px solid #e5e7eb;transition:all 0.15s ease;cursor:pointer;}
22−._10:hover{border:1px solid #3b82f6;}
23−._11{font-weight:bold;}
24−._12{display:flex;flex-direction:row;gap:10px;}
25−._13{display:flex;flex-direction:column;padding:16px;background:#3b82f6;border-radius:8px;transition:all 0.15s ease;cursor:pointer;}
26−._13:hover{background:#2563eb;}
27−._13:active{background:#1d4ed8;}
28−._14{color:white;font-weight:bold;}
29−._15{display:flex;flex-direction:column;flex:1;min-width:0;}
30−._16{color:#888;}
31−</style>
32−</head>
33−<body>
34−<div class="_0"><div class="_1"><div class="_2"><span class="_3">Welcome to htmlang</span>
35−<p class="_4">A minimalist layout language that compiles to
36−<span class="_5">static HTML</span>.</p>
37−</div>
38−<img src="https://picsum.photos/80" class="_6">
39−</div>
40−<div class="_7"><span class="_8">Features</span>
41−<div class="_9"><div class="_10"><span class="_11">Simple</span>
42−<span>Write layouts without CSS</span></div>
43−<div class="_10"><span class="_11">Fast</span>
44−<span>Compiles to a single HTML file</span></div>
45−<div class="_10"><span class="_11">Minimal</span>
46−<span>No JavaScript required</span></div>
47−</div>
48−</div>
49−<div class="_12"><div class="_13"><a href="https://github.com"><span class="_14">Get Started</span>
50−</a>
51−</div>
52−<div class="_15"></div>
53−<span class="_16">© 2026</span>
54−</div>
55−</div>
56−</body>
57−</html>
1+<!DOCTYPE html><html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><title>htmlang Demo</title><style>*,*::before,*::after{box-sizing:border-box}body{margin:0;font-family:system-ui,-apple-system,sans-serif}img{display:block}._0{display:flex;flex-direction:column;max-width:800px;margin-left:auto;margin-right:auto;padding:40px;gap:20px;}._1{display:flex;flex-direction:row;gap:20px;}._2{display:flex;flex-direction:column;flex:1;min-width:0;gap:10px;}._3{font-weight:bold;font-size:32px;}._4{margin:0;}._5{font-weight:bold;color:#3b82f6;}._6{width:80px;height:80px;border-radius:40px;}._7{display:flex;flex-direction:column;gap:10px;}._8{font-weight:bold;font-size:24px;color:#333;}._9{display:flex;flex-direction:row;flex-wrap:wrap;gap:10px;}._10{display:flex;flex-direction:column;padding:20px;background:white;border-radius:8px;border:1px solid #e5e7eb;transition:all 0.15s ease;cursor:pointer;}._10:hover{border:1px solid #3b82f6;}._11{font-weight:bold;}._12{display:flex;flex-direction:row;gap:10px;}._13{display:flex;flex-direction:column;padding:16px;background:#3b82f6;border-radius:8px;transition:all 0.15s ease;cursor:pointer;}._13:hover{background:#2563eb;}._13:active{background:#1d4ed8;}._14{color:white;font-weight:bold;}._15{display:flex;flex-direction:column;flex:1;min-width:0;}._16{color:#888;}</style></head><body><div class="_0"><div class="_1"><div class="_2"><span class="_3">Welcome to htmlang</span><p class="_4">A minimalist layout language that compiles to <span class="_5">static HTML</span>.</p></div><img src="https://picsum.photos/80" class="_6"></div><div class="_7"><span class="_8">Features</span><div class="_9"><div class="_10"><span class="_11">Simple</span><span>Write layouts without CSS</span></div><div class="_10"><span class="_11">Fast</span><span>Compiles to a single HTML file</span></div><div class="_10"><span class="_11">Minimal</span><span>No JavaScript required</span></div></div></div><div class="_12"><div class="_13"><a href="https://github.com"><span class="_14">Get Started</span></a></div><div class="_15"></div><span class="_16">© 2026</span></div></div></body></html>
src/codegen.rs+9−28
@@ -59,16 +59,16 @@ impl StyleCollector {
5959 let mut css = String::new();
6060 for e in &self.entries {
6161 if !e.base.is_empty() {
62− css.push_str(&format!(".{}{{{}}}\n", e.class_name, e.base));
62+ css.push_str(&format!(".{}{{{}}}", e.class_name, e.base));
6363 }
6464 if !e.hover.is_empty() {
65− css.push_str(&format!(".{}:hover{{{}}}\n", e.class_name, e.hover));
65+ css.push_str(&format!(".{}:hover{{{}}}", e.class_name, e.hover));
6666 }
6767 if !e.active.is_empty() {
68− css.push_str(&format!(".{}:active{{{}}}\n", e.class_name, e.active));
68+ css.push_str(&format!(".{}:active{{{}}}", e.class_name, e.active));
6969 }
7070 if !e.focus.is_empty() {
71− css.push_str(&format!(".{}:focus{{{}}}\n", e.class_name, e.focus));
71+ css.push_str(&format!(".{}:focus{{{}}}", e.class_name, e.focus));
7272 }
7373 }
7474 css
@@ -91,25 +91,7 @@ pub fn generate(doc: &Document) -> String {
9191
9292 match &doc.page_title {
9393 Some(title) => format!(
94− "\
95−<!DOCTYPE html>
96−<html>
97−<head>
98−<meta charset=\"utf-8\">
99−<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">
100−<title>{title}</title>
101−<style>
102−*, *::before, *::after {{ box-sizing: border-box; }}
103−body {{ margin: 0; font-family: system-ui, -apple-system, sans-serif; }}
104−img {{ display: block; }}
105−{element_css}\
106−</style>
107−</head>
108−<body>
109−{body}\
110−</body>
111−</html>
112−",
94+ "<!DOCTYPE html><html><head><meta charset=\"utf-8\"><meta name=\"viewport\" content=\"width=device-width, initial-scale=1\"><title>{title}</title><style>*,*::before,*::after{{box-sizing:border-box}}body{{margin:0;font-family:system-ui,-apple-system,sans-serif}}img{{display:block}}{element_css}</style></head><body>{body}</body></html>",
11395 title = html_escape(title),
11496 element_css = element_css,
11597 body = body,
@@ -118,7 +100,7 @@ img {{ display: block; }}
118100 if element_css.is_empty() {
119101 body
120102 } else {
121− format!("<style>\n{}</style>\n{}", element_css, body)
103+ format!("<style>{}</style>{}", element_css, body)
122104 }
123105 }
124106 }
@@ -151,7 +133,6 @@ fn generate_node(
151133 }
152134 Node::Raw(content) => {
153135 out.push_str(content);
154− out.push('\n');
155136 }
156137 }
157138}
@@ -214,13 +195,13 @@ fn generate_element(
214195 for (i, child) in elem.children.iter().enumerate() {
215196 generate_node(child, Some(&elem.kind), out, styles);
216197 if is_paragraph && i < elem.children.len() - 1 {
217− out.push('\n');
198+ out.push(' ');
218199 }
219200 }
220201
221202 out.push_str("</");
222203 out.push_str(tag);
223− out.push_str(">\n");
204+ out.push('>');
224205}
225206
226207fn generate_image(
@@ -244,7 +225,7 @@ fn generate_image(
244225 out.push_str(&html_escape(&id));
245226 out.push('"');
246227 }
247− out.push_str(">\n");
228+ out.push('>');
248229}
249230
250231fn generate_text_segments(