@@ -444,7 +444,6 @@ pub struct CodegenOptions {
444 pub dev: bool,
444 pub dev: bool,
445 pub partial: bool,
445 pub partial: bool,
446 pub minify: bool,
446 pub minify: bool,
450/// Generate HTML from a parsed document using the given options.
449/// Generate HTML from a parsed document using the given options.
@@ -457,9 +456,6 @@ pub fn generate_with(doc: &Document, opts: &CodegenOptions) -> String {
457 if opts.minify {
456 if opts.minify {
458 html = minify_html(&html);
457 html = minify_html(&html);
461− html = add_vendor_prefixes(&html);
@@ -508,38 +504,6 @@ pub fn generate_minified(doc: &Document) -> String {
511−pub fn generate_compat(doc: &Document) -> String {
516− ..Default::default()
521−pub fn generate_dev_compat(doc: &Document) -> String {
527− ..Default::default()
532−pub fn generate_minified_compat(doc: &Document) -> String {
538− ..Default::default()
543fn minify_html(html: &str) -> String {
507fn minify_html(html: &str) -> String {
544 let mut result = String::with_capacity(html.len());
508 let mut result = String::with_capacity(html.len());
545 let mut in_pre = false;
509 let mut in_pre = false;
@@ -618,109 +582,6 @@ fn minify_html(html: &str) -> String {
621−/// Add vendor prefixes to CSS within <style> tags for broader browser compatibility.
622−fn add_vendor_prefixes(html: &str) -> String {
623− // Find CSS within <style>...</style> and add vendor prefixes
624− let mut result = String::with_capacity(html.len() + 512);
625− let mut rest = html;
626− while let Some(start) = rest.find("<style>") {
627− let after_tag = start + 7;
628− result.push_str(&rest[..after_tag]);
629− rest = &rest[after_tag..];
630− if let Some(end) = rest.find("</style>") {
631− let css = &rest[..end];
632− result.push_str(&prefix_css(css));
633− result.push_str("</style>");
634− rest = &rest[end + 8..];
639− result.push_str(rest);
643−fn prefix_css(css: &str) -> String {
644− let mut out = String::with_capacity(css.len() + 256);
646− let bytes = css.as_bytes();
647− while i < bytes.len() {
648− // Find property declarations
649− if let Some(pos) = css[i..].find('{') {
650− let brace = i + pos;
651− out.push_str(&css[i..=brace]);
653− // Process declarations within this block
654− if let Some(close) = css[i..].find('}') {
655− let block = &css[i..i + close];
656− out.push_str(&prefix_declarations(block));
661− out.push_str(&css[i..]);
668−fn prefix_declarations(block: &str) -> String {
669− let mut out = String::with_capacity(block.len() + 128);
670− for decl in block.split(';') {
671− let decl = decl.trim();
672− if decl.is_empty() {
675− if let Some((prop, val)) = decl.split_once(':') {
676− let prop = prop.trim();
677− let val = val.trim();
679− "backdrop-filter" => {
680− out.push_str(&format!("-webkit-backdrop-filter:{};", val));
681− out.push_str(&format!("backdrop-filter:{};", val));
684− out.push_str(&format!("-webkit-user-select:{};", val));
685− out.push_str(&format!("-moz-user-select:{};", val));
686− out.push_str(&format!("user-select:{};", val));
689− out.push_str(&format!("-webkit-appearance:{};", val));
690− out.push_str(&format!("-moz-appearance:{};", val));
691− out.push_str(&format!("appearance:{};", val));
693− "background-clip" if val.contains("text") => {
694− out.push_str(&format!("-webkit-background-clip:{};", val));
695− out.push_str(&format!("background-clip:{};", val));
698− out.push_str(&format!("-webkit-hyphens:{};", val));
699− out.push_str(&format!("-ms-hyphens:{};", val));
700− out.push_str(&format!("hyphens:{};", val));
702− "text-size-adjust" => {
703− out.push_str(&format!("-webkit-text-size-adjust:{};", val));
704− out.push_str(&format!("-ms-text-size-adjust:{};", val));
705− out.push_str(&format!("text-size-adjust:{};", val));
707− "mask-image" | "mask-size" | "mask-repeat" | "mask-position" => {
708− out.push_str(&format!("-webkit-{}:{};", prop, val));
709− out.push_str(&format!("{}:{};", prop, val));
724fn generate_full_inner(doc: &Document, dev: bool) -> String {
585fn generate_full_inner(doc: &Document, dev: bool) -> String {
725 let mut styles = StyleCollector::new();
586 let mut styles = StyleCollector::new();
726 let mut ctx = GenContext {
587 let mut ctx = GenContext {
@@ -748,8 +609,6 @@ fn generate_full_inner(doc: &Document, dev: bool) -> String {
748 generate_node(node, None, &mut body, &mut styles, &mut ctx);
609 generate_node(node, None, &mut body, &mut styles, &mut ctx);
751− // Collect external domains for DNS prefetch
752− let dns_prefetch_html = collect_dns_prefetch(&body, dev);
754 let element_css = build_element_css(doc, &styles, dev);
613 let element_css = build_element_css(doc, &styles, dev);
@@ -932,7 +791,7 @@ fn generate_full_inner(doc: &Document, dev: bool) -> String {
932<meta charset=\"utf-8\">
791<meta charset=\"utf-8\">
933<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">
792<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">
934<title>{title}</title>
793<title>{title}</title>
935−{base_html}{canonical_html}{preload_html}{dns_prefetch_html}{meta_html}{og_html}{favicon_html}{head_html}\
794+{base_html}{canonical_html}{preload_html}{meta_html}{og_html}{favicon_html}{head_html}\
937{reset_css}{element_css}\
796{reset_css}{element_css}\
@@ -947,7 +806,6 @@ fn generate_full_inner(doc: &Document, dev: bool) -> String {
947 base_html = base_html,
806 base_html = base_html,
948 canonical_html = canonical_html,
807 canonical_html = canonical_html,
949 preload_html = preload_html,
808 preload_html = preload_html,
950− dns_prefetch_html = dns_prefetch_html,
951 meta_html = meta_html,
809 meta_html = meta_html,
952 favicon_html = favicon_html,
810 favicon_html = favicon_html,
953 head_html = head_html,
811 head_html = head_html,
@@ -958,13 +816,12 @@ fn generate_full_inner(doc: &Document, dev: bool) -> String {
961− "<!DOCTYPE html><html{lang_attr}><head><meta charset=\"utf-8\"><meta name=\"viewport\" content=\"width=device-width, initial-scale=1\"><title>{title}</title>{base_html}{canonical_html}{preload_html}{dns_prefetch_html}{meta_html}{og_html}{favicon_html}{head_html}<style>{reset_css}{element_css}</style></head><body>{body}</body></html>",
819+ "<!DOCTYPE html><html{lang_attr}><head><meta charset=\"utf-8\"><meta name=\"viewport\" content=\"width=device-width, initial-scale=1\"><title>{title}</title>{base_html}{canonical_html}{preload_html}{meta_html}{og_html}{favicon_html}{head_html}<style>{reset_css}{element_css}</style></head><body>{body}</body></html>",
962 title = html_escape(title),
820 title = html_escape(title),
963 lang_attr = lang_attr,
821 lang_attr = lang_attr,
964 base_html = base_html,
822 base_html = base_html,
965 canonical_html = canonical_html,
823 canonical_html = canonical_html,
966 preload_html = preload_html,
824 preload_html = preload_html,
967− dns_prefetch_html = dns_prefetch_html,
968 meta_html = meta_html,
825 meta_html = meta_html,
969 og_html = og_html,
826 og_html = og_html,
970 favicon_html = favicon_html,
827 favicon_html = favicon_html,
@@ -1008,16 +865,6 @@ fn build_element_css(doc: &Document, styles: &StyleCollector, dev: bool) -> Stri
1008 // `var(--name)` references so the generated CSS actually uses the
865 // `var(--name)` references so the generated CSS actually uses the
1009 // custom properties emitted in `:root`.
866 // custom properties emitted in `:root`.
1010 let styles_css = substitute_css_vars(&styles_css, &root_vars);
867 let styles_css = substitute_css_vars(&styles_css, &root_vars);
1011− // Further compress the CSS by auto-extracting any remaining literal
1012− // values that appear often enough for `var(--hN)` references to come out
1013− // shorter overall. Disabled in dev mode to keep the CSS readable.
1014− let styles_css = if dev {
1017− let (new_css, auto_vars) = auto_extract_repeats(&styles_css, &root_vars);
1018− root_vars.extend(auto_vars);
1022 // Emit the :root block first so the cascade picks up the custom
869 // Emit the :root block first so the cascade picks up the custom
1023 // properties before the class rules consume them.
870 // properties before the class rules consume them.
@@ -2565,118 +2412,6 @@ fn substitute_css_vars(css: &str, vars: &[(String, String)]) -> String {
2568−/// Scan generated CSS for literal property values that repeat often enough
2569−/// that substituting them with an auto-named custom property would reduce
2570−/// total byte count. Returns the rewritten CSS and any new vars that should
2571−/// be appended to `:root`.
2573−/// The algorithm only promotes a value if it strictly saves bytes after
2574−/// accounting for the `--xN:value;` declaration overhead plus the
2575−/// `var(--xN)` reference cost at each call site. Values that are already
2576−/// `var(...)` references or contain `var(` calls are skipped so we never
2577−/// nest references. Names avoid collisions with `existing_vars`.
2578−fn auto_extract_repeats(
2580− existing_vars: &[(String, String)],
2581−) -> (String, Vec<(String, String)>) {
2582− if css.is_empty() {
2583− return (css.to_string(), Vec::new());
2585− // Count occurrences of each distinct property value (the text between
2586− // `:` and the next `;` or `}` at the top level of a declaration block,
2587− // ignoring balanced parentheses so that e.g. `rgba(...)` stays intact).
2588− let bytes = css.as_bytes();
2589− let mut counts: HashMap<String, usize> = HashMap::new();
2590− let mut depth: i32 = 0;
2592− while i < bytes.len() {
2608− if depth > 0 && bytes[i] == b':' {
2609− // Skip past `:` then walk to the terminator.
2611− let mut end = start;
2612− let mut pdepth = 0i32;
2613− while end < bytes.len() {
2615− b'(' => pdepth += 1,
2616− b')' if pdepth > 0 => pdepth -= 1,
2617− b';' | b'}' if pdepth == 0 => break,
2618− b'{' if pdepth == 0 => break,
2623− let val = css[start..end].trim();
2624− if !val.is_empty() && !val.starts_with("var(") && !val.contains("var(") {
2625− *counts.entry(val.to_string()).or_default() += 1;
2633− // Decide which values are worth extracting. A value of length L appearing
2634− // N times costs N*L bytes inline; promoting it costs (L + 6) for the
2635− // `--hK:V;` declaration plus N * ref_len for the call sites. We estimate
2636− // ref_len optimistically as `var(--h0)` (9 bytes) and fall back to 10 for
2637− // two-digit indices, which only affects very large extraction counts.
2638− let existing: std::collections::HashSet<&str> =
2639− existing_vars.iter().map(|(n, _)| n.as_str()).collect();
2640− let mut candidates: Vec<(String, usize)> =
2641− counts.into_iter().filter(|(_, n)| *n >= 2).collect();
2642− // Stable, deterministic ordering — longest values first, tiebreak by text.
2643− candidates.sort_by(|a, b| b.0.len().cmp(&a.0.len()).then_with(|| a.0.cmp(&b.0)));
2645− let mut extracted: Vec<(String, String)> = Vec::new();
2646− let mut next_idx: usize = 0;
2647− for (value, n) in candidates {
2648− let l = value.len();
2649− // Allocate the next unused `--hN` name.
2650− let (name, ref_len) = loop {
2651− let candidate = format!("--h{}", next_idx);
2653− let rl = candidate.len() + 6; // `var(` + name + `)`
2654− if !existing.contains(candidate.as_str())
2655− && !extracted.iter().any(|(n, _)| *n == candidate)
2657− break (candidate, rl);
2660− // Skip if promotion would not strictly reduce byte count.
2661− // Before: n * l bytes. After: (l + name.len() + 2) for the :root
2662− // declaration (`<name>:<value>;`) plus n * ref_len for the sites.
2663− let decl_overhead = l + name.len() + 2;
2664− let before = n * l;
2665− let after = decl_overhead + n * ref_len;
2666− if after < before {
2667− extracted.push((name, value));
2669− // Rewind the index so the next value can reuse this slot.
2674− if extracted.is_empty() {
2675− return (css.to_string(), Vec::new());
2677− let new_css = substitute_css_vars(css, &extracted);
2678− (new_css, extracted)
2681/// Format a `line-height` value. CSS accepts either a unitless multiplier
2416/// Format a `line-height` value. CSS accepts either a unitless multiplier
2682/// (e.g. `1.5`) or a length (e.g. `24px`). Plain integers in htmlang source
2417/// (e.g. `1.5`) or a length (e.g. `24px`). Plain integers in htmlang source
@@ -2990,34 +2725,4 @@ fn has_tag(nodes: &[Node], name: &str) -> bool {
2993−/// Collect unique external domains from generated HTML for DNS prefetch hints.
2728+
2994−fn collect_dns_prefetch(html: &str, dev: bool) -> String {
2995− let mut domains = Vec::new();
2996− let mut rest = html;
2997− while let Some(pos) = rest.find("https://") {
2998− let start = pos + 8; // skip "https://"
2999− rest = &rest[start..];
3001− .find(['/', '"', '\'', ' ', '>', ')'])
3002− .unwrap_or(rest.len());
3003− let domain = &rest[..end];
3004− if !domain.is_empty() && domain.contains('.') && !domains.contains(&domain.to_string()) {
3005− domains.push(domain.to_string());
3008− let mut out = String::new();
3009− for domain in &domains {
3011− out.push_str(&format!(
3012− "<link rel=\"dns-prefetch\" href=\"//{}\">\n",
3016− out.push_str(&format!(
3017− "<link rel=\"dns-prefetch\" href=\"//{}\">",