Skip to content

Commit 549e351

Browse files
authored
feat: add compact html (#8)
1 parent c49b415 commit 549e351

45 files changed

Lines changed: 878 additions & 140 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

crates/astro_codegen/src/lib.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ mod printer;
2525
pub mod scanner;
2626

2727
pub use diagnostic::{Diagnostic, DiagnosticLabel, DiagnosticSeverity};
28-
pub use options::{ScopedStyleStrategy, SourcemapOption, TransformOptions};
28+
pub use options::{CompactMode, ScopedStyleStrategy, SourcemapOption, TransformOptions};
2929
pub use printer::{
3030
AstroCodegen, HoistedScriptType, StyleBlock, TransformResult, TransformResultHoistedScript,
3131
TransformResultHydratedComponent, extract_styles, transform,

crates/astro_codegen/src/options.rs

Lines changed: 33 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
//! Options for Astro codegen.
22
//!
3-
//! Some fields (such as `compact`, `sourcemap`, CSS scoping) are accepted but
3+
//! Some fields (such as `sourcemap`, CSS scoping) are accepted but
44
//! stubbed for API compatibility.
55
66
/// Controls whether and how source maps are emitted.
@@ -25,6 +25,26 @@ impl SourcemapOption {
2525
}
2626
}
2727

28+
/// Controls how whitespace is collapsed in the HTML output.
29+
///
30+
/// - `Disabled` (default): no whitespace modification.
31+
/// - `Html`: HTML-aware whitespace collapsing, following the same rules as a browser
32+
/// (preserves significant whitespace, collapses runs of whitespace to a single
33+
/// space or newline, removes whitespace-only text nodes in insensitive contexts).
34+
/// Matches the Go compiler's `compact: true` behavior.
35+
/// - `Jsx`: removes all whitespace-only text nodes and strips leading/trailing
36+
/// whitespace from text content. Similar to how JSX transformers handle whitespace.
37+
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
38+
pub enum CompactMode {
39+
/// No whitespace modification (default).
40+
#[default]
41+
Disabled,
42+
/// HTML-aware whitespace collapsing (Go compiler `compact: true` behavior).
43+
Html,
44+
/// Strip all whitespace-only text nodes and leading/trailing whitespace.
45+
Jsx,
46+
}
47+
2848
/// Scoped style strategy for CSS scoping.
2949
///
3050
/// Determines how Astro scopes CSS selectors to components.
@@ -66,11 +86,12 @@ pub struct TransformOptions {
6686
/// Defaults to `"https://astro.build"`.
6787
pub astro_global_args: Option<String>,
6888

69-
/// Whether to collapse whitespace in the HTML output.
89+
/// Controls how whitespace is collapsed in the HTML output.
7090
///
71-
/// **Stub**: compact mode is not yet implemented; this field is accepted
72-
/// for API compatibility.
73-
pub compact: bool,
91+
/// - `Disabled` (default): no whitespace modification.
92+
/// - `Html`: HTML-aware collapsing following browser whitespace rules.
93+
/// - `Jsx`: strip all whitespace-only text nodes and leading/trailing whitespace.
94+
pub compact: CompactMode,
7495

7596
/// Enable scoped slot result handling.
7697
///
@@ -149,7 +170,7 @@ impl Default for TransformOptions {
149170
internal_url: None,
150171
sourcemap: SourcemapOption::default(),
151172
astro_global_args: None,
152-
compact: false,
173+
compact: CompactMode::Disabled,
153174
result_scoped_slot: false,
154175
scoped_style_strategy: ScopedStyleStrategy::default(),
155176
transitions_animation_url: None,
@@ -226,9 +247,13 @@ impl TransformOptions {
226247
self
227248
}
228249

229-
/// Enable or disable compact mode (stub).
250+
/// Set the compact whitespace collapsing mode.
251+
///
252+
/// - `CompactMode::Disabled`: no whitespace modification (default).
253+
/// - `CompactMode::Html`: HTML-aware collapsing (Go compiler behavior).
254+
/// - `CompactMode::Jsx`: strip all whitespace-only text nodes.
230255
#[must_use]
231-
pub fn with_compact(mut self, compact: bool) -> Self {
256+
pub fn with_compact(mut self, compact: CompactMode) -> Self {
232257
self.compact = compact;
233258
self
234259
}

crates/astro_codegen/src/printer/components.rs

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,9 @@ use super::elements::ScopeId;
99
use super::escape::{decode_html_entities, escape_double_quotes};
1010
use super::expr_to_string;
1111
use super::runtime;
12+
use super::whitespace::has_is_raw_attr;
1213
use crate::css_scoping;
13-
use crate::options::ScopedStyleStrategy;
14+
use crate::options::{CompactMode, ScopedStyleStrategy};
1415
use crate::scanner::{get_jsx_attribute_name, is_custom_element};
1516
use oxc_ast::ast::*;
1617

@@ -102,6 +103,14 @@ impl<'a> AstroCodegen<'a> {
102103
// Check if this is a custom element (has dash in name)
103104
let is_custom = is_custom_element(name);
104105

106+
// Track raw element depth for compact whitespace collapsing.
107+
// A component with `is:raw` has its slot children treated as raw text.
108+
let is_raw = self.options.compact != CompactMode::Disabled
109+
&& has_is_raw_attr(&el.opening_element.attributes);
110+
if is_raw {
111+
self.raw_element_depth += 1;
112+
}
113+
105114
// Check for server:defer directive
106115
let mut server_defer_info = if Self::has_server_defer(&el.opening_element.attributes) {
107116
Some(ServerDeferInfo {
@@ -270,6 +279,10 @@ impl<'a> AstroCodegen<'a> {
270279
self.add_source_mapping_for_span(closing.span);
271280
}
272281
self.print(")}");
282+
283+
if is_raw {
284+
self.raw_element_depth -= 1;
285+
}
273286
}
274287

275288
/// Extract `set:html` or `set:text` value from component attributes.

crates/astro_codegen/src/printer/elements.rs

Lines changed: 18 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,10 @@
88
99
use super::escape::{escape_double_quotes, escape_html_attribute};
1010
use super::runtime;
11+
use super::whitespace::{has_is_raw_attr, is_raw_element_name};
1112
use super::{AstroCodegen, expr_to_string};
1213
use crate::css_scoping;
13-
use crate::options::ScopedStyleStrategy;
14+
use crate::options::{CompactMode, ScopedStyleStrategy};
1415
use crate::scanner::get_jsx_attribute_name;
1516
use oxc_ast::ast::*;
1617

@@ -114,6 +115,15 @@ impl<'a> AstroCodegen<'a> {
114115
self.has_explicit_head = true;
115116
}
116117

118+
// Track raw element depth for compact whitespace collapsing.
119+
// Raw elements are those whose text content must never be modified:
120+
// <pre>, <textarea>, <script>, <style>, etc., and any element with `is:raw`.
121+
let is_raw = self.options.compact != CompactMode::Disabled
122+
&& (is_raw_element_name(name) || has_is_raw_attr(&el.opening_element.attributes));
123+
if is_raw {
124+
self.raw_element_depth += 1;
125+
}
126+
117127
// Insert $$maybeRenderHead before the first body HTML element
118128
self.maybe_insert_render_head(name);
119129

@@ -150,10 +160,8 @@ impl<'a> AstroCodegen<'a> {
150160

151161
// Handle special head insertion
152162
if is_head {
153-
// Children
154-
for child in &el.children {
155-
self.print_jsx_child(child);
156-
}
163+
// Children (use compact-aware printing)
164+
self.print_jsx_children_compact(&el.children);
157165
// Insert renderHead before closing head tag
158166
self.print(&format!(
159167
"${{{}({})}}",
@@ -178,10 +186,8 @@ impl<'a> AstroCodegen<'a> {
178186
self.print(&format!("${{{value}}}"));
179187
}
180188
} else {
181-
// Regular children
182-
for child in &el.children {
183-
self.print_jsx_child(child);
184-
}
189+
// Regular children with compact-aware printing
190+
self.print_jsx_children_compact(&el.children);
185191
}
186192

187193
// Closing tag (skip for void elements like <meta>, <input>, <br>, etc.)
@@ -195,6 +201,9 @@ impl<'a> AstroCodegen<'a> {
195201
self.print(">");
196202
}
197203

204+
if is_raw {
205+
self.raw_element_depth -= 1;
206+
}
198207
if is_head {
199208
self.in_head = was_in_head;
200209
}

crates/astro_codegen/src/printer/expressions.rs

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -27,9 +27,7 @@ impl<'a> AstroCodegen<'a> {
2727
self.print(&format!(",{{}},{{\"default\": {async_prefix}{slot_params}"));
2828
self.print(runtime::RENDER);
2929
self.print("`");
30-
for child in &frag.children {
31-
self.print_jsx_child(child);
32-
}
30+
self.print_jsx_children_compact(&frag.children);
3331
// Map the closing fragment tag (</>) to the `)` that closes
3432
// $$renderComponent(...) — the semantic equivalent in generated code.
3533
if !frag.closing_fragment.span.is_empty() {
@@ -118,9 +116,7 @@ impl<'a> AstroCodegen<'a> {
118116
));
119117
self.print(runtime::RENDER);
120118
self.print("`");
121-
for child in &frag.children {
122-
self.print_jsx_child(child);
123-
}
119+
self.print_jsx_children_compact(&frag.children);
124120
// Map closing fragment tag (</>) before the closing boilerplate.
125121
if !frag.closing_fragment.span.is_empty() {
126122
self.add_source_mapping_for_span(frag.closing_fragment.span);
@@ -130,9 +126,7 @@ impl<'a> AstroCodegen<'a> {
130126
// Implicit fragments (multiple JSX siblings) are just wrapped in $$render`...`
131127
self.print(runtime::RENDER);
132128
self.print("`");
133-
for child in &frag.children {
134-
self.print_jsx_child(child);
135-
}
129+
self.print_jsx_children_compact(&frag.children);
136130
self.print("`");
137131
}
138132
}

0 commit comments

Comments
 (0)