@@ -444,7 +444,6 @@ pub struct CodegenOptions {
444444 pub dev: bool,
445445 pub partial: bool,
446446 pub minify: bool,
447− pub compat: bool,
448447}
449448
450449/// Generate HTML from a parsed document using the given options.
@@ -457,9 +456,6 @@ pub fn generate_with(doc: &Document, opts: &CodegenOptions) -> String {
457456 if opts.minify {
458457 html = minify_html(&html);
459458 }
460− if opts.compat {
461− html = add_vendor_prefixes(&html);
462− }
463459 html
464460}
465461
@@ -508,38 +504,6 @@ pub fn generate_minified(doc: &Document) -> String {
508504 )
509505}
510506
511−pub fn generate_compat(doc: &Document) -> String {
512− generate_with(
513− doc,
514− &CodegenOptions {
515− compat: true,
516− ..Default::default()
517− },
518− )
519−}
520−
521−pub fn generate_dev_compat(doc: &Document) -> String {
522− generate_with(
523− doc,
524− &CodegenOptions {
525− dev: true,
526− compat: true,
527− ..Default::default()
528− },
529− )
530−}
531−
532−pub fn generate_minified_compat(doc: &Document) -> String {
533− generate_with(
534− doc,
535− &CodegenOptions {
536− minify: true,
537− compat: true,
538− ..Default::default()
539− },
540− )
541−}
542−
543507fn minify_html(html: &str) -> String {
544508 let mut result = String::with_capacity(html.len());
545509 let mut in_pre = false;
@@ -618,109 +582,6 @@ fn minify_html(html: &str) -> String {
618582 result
619583}
620584
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..];
635− } else {
636− break;
637− }
638− }
639− result.push_str(rest);
640− result
641−}
642−
643−fn prefix_css(css: &str) -> String {
644− let mut out = String::with_capacity(css.len() + 256);
645− let mut i = 0;
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]);
652− i = brace + 1;
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));
657− out.push('}');
658− i = i + close + 1;
659− }
660− } else {
661− out.push_str(&css[i..]);
662− break;
663− }
664− }
665− out
666−}
667−
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() {
673− continue;
674− }
675− if let Some((prop, val)) = decl.split_once(':') {
676− let prop = prop.trim();
677− let val = val.trim();
678− match prop {
679− "backdrop-filter" => {
680− out.push_str(&format!("-webkit-backdrop-filter:{};", val));
681− out.push_str(&format!("backdrop-filter:{};", val));
682− }
683− "user-select" => {
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));
687− }
688− "appearance" => {
689− out.push_str(&format!("-webkit-appearance:{};", val));
690− out.push_str(&format!("-moz-appearance:{};", val));
691− out.push_str(&format!("appearance:{};", val));
692− }
693− "background-clip" if val.contains("text") => {
694− out.push_str(&format!("-webkit-background-clip:{};", val));
695− out.push_str(&format!("background-clip:{};", val));
696− }
697− "hyphens" => {
698− out.push_str(&format!("-webkit-hyphens:{};", val));
699− out.push_str(&format!("-ms-hyphens:{};", val));
700− out.push_str(&format!("hyphens:{};", val));
701− }
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));
706− }
707− "mask-image" | "mask-size" | "mask-repeat" | "mask-position" => {
708− out.push_str(&format!("-webkit-{}:{};", prop, val));
709− out.push_str(&format!("{}:{};", prop, val));
710− }
711− _ => {
712− out.push_str(decl);
713− out.push(';');
714− }
715− }
716− } else {
717− out.push_str(decl);
718− out.push(';');
719− }
720− }
721− out
722−}
723−
724585fn generate_full_inner(doc: &Document, dev: bool) -> String {
725586 let mut styles = StyleCollector::new();
726587 let mut ctx = GenContext {
@@ -748,8 +609,6 @@ fn generate_full_inner(doc: &Document, dev: bool) -> String {
748609 generate_node(node, None, &mut body, &mut styles, &mut ctx);
749610 }
750611
751− // Collect external domains for DNS prefetch
752− let dns_prefetch_html = collect_dns_prefetch(&body, dev);
753612
754613 let element_css = build_element_css(doc, &styles, dev);
755614
@@ -932,7 +791,7 @@ fn generate_full_inner(doc: &Document, dev: bool) -> String {
932791<meta charset=\"utf-8\">
933792<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">
934793<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}\
936795<style>
937796{reset_css}{element_css}\
938797</style>
@@ -947,7 +806,6 @@ fn generate_full_inner(doc: &Document, dev: bool) -> String {
947806 base_html = base_html,
948807 canonical_html = canonical_html,
949808 preload_html = preload_html,
950− dns_prefetch_html = dns_prefetch_html,
951809 meta_html = meta_html,
952810 favicon_html = favicon_html,
953811 head_html = head_html,
@@ -958,13 +816,12 @@ fn generate_full_inner(doc: &Document, dev: bool) -> String {
958816 )
959817 } else {
960818 format!(
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>",
962820 title = html_escape(title),
963821 lang_attr = lang_attr,
964822 base_html = base_html,
965823 canonical_html = canonical_html,
966824 preload_html = preload_html,
967− dns_prefetch_html = dns_prefetch_html,
968825 meta_html = meta_html,
969826 og_html = og_html,
970827 favicon_html = favicon_html,
@@ -1008,16 +865,6 @@ fn build_element_css(doc: &Document, styles: &StyleCollector, dev: bool) -> Stri
1008865 // `var(--name)` references so the generated CSS actually uses the
1009866 // custom properties emitted in `:root`.
1010867 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 {
1015− styles_css
1016− } else {
1017− let (new_css, auto_vars) = auto_extract_repeats(&styles_css, &root_vars);
1018− root_vars.extend(auto_vars);
1019− new_css
1020− };
1021868
1022869 // Emit the :root block first so the cascade picks up the custom
1023870 // properties before the class rules consume them.
@@ -2565,118 +2412,6 @@ fn substitute_css_vars(css: &str, vars: &[(String, String)]) -> String {
25652412 out
25662413}
25672414
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`.
2572−///
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(
2579− css: &str,
2580− existing_vars: &[(String, String)],
2581−) -> (String, Vec<(String, String)>) {
2582− if css.is_empty() {
2583− return (css.to_string(), Vec::new());
2584− }
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;
2591− let mut i = 0;
2592− while i < bytes.len() {
2593− match bytes[i] {
2594− b'{' => {
2595− depth += 1;
2596− i += 1;
2597− continue;
2598− }
2599− b'}' => {
2600− if depth > 0 {
2601− depth -= 1;
2602− }
2603− i += 1;
2604− continue;
2605− }
2606− _ => {}
2607− }
2608− if depth > 0 && bytes[i] == b':' {
2609− // Skip past `:` then walk to the terminator.
2610− let start = i + 1;
2611− let mut end = start;
2612− let mut pdepth = 0i32;
2613− while end < bytes.len() {
2614− match bytes[end] {
2615− b'(' => pdepth += 1,
2616− b')' if pdepth > 0 => pdepth -= 1,
2617− b';' | b'}' if pdepth == 0 => break,
2618− b'{' if pdepth == 0 => break,
2619− _ => {}
2620− }
2621− end += 1;
2622− }
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;
2626− }
2627− i = end;
2628− continue;
2629− }
2630− i += 1;
2631− }
2632−
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)));
2644−
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);
2652− next_idx += 1;
2653− let rl = candidate.len() + 6; // `var(` + name + `)`
2654− if !existing.contains(candidate.as_str())
2655− && !extracted.iter().any(|(n, _)| *n == candidate)
2656− {
2657− break (candidate, rl);
2658− }
2659− };
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));
2668− } else {
2669− // Rewind the index so the next value can reuse this slot.
2670− next_idx -= 1;
2671− }
2672− }
2673−
2674− if extracted.is_empty() {
2675− return (css.to_string(), Vec::new());
2676− }
2677− let new_css = substitute_css_vars(css, &extracted);
2678− (new_css, extracted)
2679−}
26802415
26812416/// Format a `line-height` value. CSS accepts either a unitless multiplier
26822417/// (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 {
29902725 })
29912726}
29922727
2993−/// Collect unique external domains from generated HTML for DNS prefetch hints.
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..];
3000− let end = rest
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());
3006− }
3007− }
3008− let mut out = String::new();
3009− for domain in &domains {
3010− if dev {
3011− out.push_str(&format!(
3012− "<link rel=\"dns-prefetch\" href=\"//{}\">\n",
3013− domain
3014− ));
3015− } else {
3016− out.push_str(&format!(
3017− "<link rel=\"dns-prefetch\" href=\"//{}\">",
3018− domain
3019− ));
3020− }
3021− }
3022− out
3023−}
2728+