Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
144 changes: 141 additions & 3 deletions packages/autofmt/src/writer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -109,9 +109,20 @@ impl<'a> Writer<'a> {
children,
spreads,
brace,
tag_syntax,
..
} = el;

if *tag_syntax {
return self.write_jsx_tag(
&name.to_string(),
attributes,
spreads,
children,
&brace.unwrap_or_default(),
);
}

write!(self.out, "{name} ")?;
self.write_rsx_block(attributes, spreads, children, &brace.unwrap_or_default())?;

Expand All @@ -127,27 +138,154 @@ impl<'a> Writer<'a> {
generics,
spreads,
brace,
tag_syntax,
..
}: &Component,
) -> Result {
// Write the path by to_tokensing it and then removing all whitespace
let mut name = name.to_token_stream().to_string();
name.retain(|c| !c.is_whitespace());
write!(self.out, "{name}")?;

// Same idea with generics, write those via the to_tokens method and then remove all whitespace
if let Some(generics) = generics {
let mut written = generics.to_token_stream().to_string();
written.retain(|c| !c.is_whitespace());
write!(self.out, "{written}")?;

// The tag syntax doesn't use the turbofish: `<Outlet<R>>`
if *tag_syntax {
written = written.trim_start_matches("::").to_string();
}

name.push_str(&written);
}

write!(self.out, " ")?;
if *tag_syntax {
return self.write_jsx_tag(
&name,
fields,
spreads,
&children.roots,
&brace.unwrap_or_default(),
);
}

write!(self.out, "{name} ")?;
self.write_rsx_block(fields, spreads, &children.roots, &brace.unwrap_or_default())?;

Ok(())
}

/// Write an element or component that was written using the JSX/XML-like tag syntax,
/// preserving the tag style:
///
/// `<div class="asd">"hello"</div>` or `<img src="..." />`
fn write_jsx_tag(
&mut self,
name: &str,
attributes: &[Attribute],
spreads: &[Spread],
children: &[BodyNode],
brace: &Brace,
) -> Result {
write!(self.out, "<{name}")?;

// Decide if the attributes fit in the opening tag or need to be split across lines
let attr_len = self.is_short_attrs(brace, attributes, spreads);
let is_short_attr_list = (attr_len + self.out.indent_level * 4) < 80;

if is_short_attr_list {
for attr in attributes {
write!(self.out, " ")?;
self.write_jsx_attribute(attr)?;
}
for spread in spreads {
write!(self.out, " {{")?;
self.write_spread_attribute(&spread.expr)?;
write!(self.out, "}}")?;
}
} else {
self.out.indent_level += 1;
for attr in attributes {
self.out.tabbed_line()?;
self.write_jsx_attribute(attr)?;
}
for spread in spreads {
self.out.tabbed_line()?;
write!(self.out, "{{")?;
self.write_spread_attribute(&spread.expr)?;
write!(self.out, "}}")?;
}
self.out.indent_level -= 1;
self.out.tabbed_line()?;
}

// Self-closing tags
if children.is_empty() {
if is_short_attr_list {
write!(self.out, " ")?;
}
write!(self.out, "/>")?;
return Ok(());
}

write!(self.out, ">")?;

// Inline a single short child: `<h1>"hello"</h1>`
let children_len = self
.is_short_children(children)
.map_err(|_| std::fmt::Error)?;
let is_small_children = children_len.is_some_and(|len| {
is_short_attr_list && len + attr_len + self.out.indent_level * 4 < 100
});

if is_small_children {
for child in children {
self.write_ident(child)?;
}
} else {
self.out.new_line()?;
self.write_body_indented(children)?;
self.out.tabbed_line()?;
}

write!(self.out, "</{name}>")?;

Ok(())
}

/// Write an attribute in the JSX style: `name`, `name="literal"` or `name={expr}`
fn write_jsx_attribute(&mut self, attr: &Attribute) -> Result {
match &attr.name {
// Dashed custom attributes don't need to be quoted in the tag syntax: `data-count="1"`
AttributeName::Custom(name)
if name
.value()
.split('-')
.all(|seg| syn::parse_str::<syn::Ident>(seg).is_ok()) =>
{
write!(self.out, "{}", name.value())?
}
name => self.write_attribute_name(name)?,
}

if attr.can_be_shorthand() {
return Ok(());
}

write!(self.out, "=")?;

match &attr.value {
AttributeValue::AttrLiteral(value) => write!(self.out, "{value}")?,
value => {
write!(self.out, "{{")?;
self.write_attribute_value(value)?;
write!(self.out, "}}")?;
}
}

Ok(())
}

fn write_text_node(&mut self, text: &TextNode) -> Result {
self.out.write_text(&text.input)
}
Expand Down
1 change: 1 addition & 0 deletions packages/autofmt/tests/samples.rs
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ twoway![
long_if_else_attr,
empty_component_body,
empty_braces_oneliner,
jsx,
];

fn assert_idempotent(src: &str) {
Expand Down
40 changes: 40 additions & 0 deletions packages/autofmt/tests/samples/jsx.rsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
use dioxus::prelude::*;

pub fn app() -> Element {
rsx! {
<div class="container">
<h1>"Hello, world!"</h1>
<img src="image.png" />
<button disabled onclick={move |_| println!("clicked")}>"Click me"</button>
<my-web-component data-count="1" "custom^attr"="value" />
<div {..attrs} />
<MyComponent prop="value">"children"</MyComponent>
<some::cool::Component />
<Outlet<R>>"child"</Outlet<R>>

// The regular syntax can be mixed in freely
div { class: "inner", "More content" }
for item in items {
<span>"{item}"</span>
}
if cond {
<span>"conditional"</span>
}
<section>
p { class: "regular", "regular child" }
{some_expr}
<ul>
<li>"one"</li>
<li>"two"</li>
</ul>
</section>
<div
class="a-very-long-class-name-that-pushes-this-over-the-line-length-limit"
id="some-long-id-attribute-value"
onclick={move |_| println!("clicked")}
>
"Attributes split across lines"
</div>
</div>
}
}
1 change: 1 addition & 0 deletions packages/autofmt/tests/wrong.rs
Original file line number Diff line number Diff line change
Expand Up @@ -42,3 +42,4 @@ twoway!("skipfail" => skipfail (IndentOptions::new(IndentType::Spaces, 4, false)
twoway!("comments-inline-4sp" => comments_inline_4sp (IndentOptions::new(IndentType::Spaces, 4, false)));
twoway!("comments-attributes-4sp" => comments_attributes_4sp (IndentOptions::new(IndentType::Spaces, 4, false)));
twoway!("comments-big" => comments_big (IndentOptions::new(IndentType::Spaces, 4, false)));
twoway!("jsx-4sp" => jsx_4sp (IndentOptions::new(IndentType::Spaces, 4, false)));
12 changes: 12 additions & 0 deletions packages/autofmt/tests/wrong/jsx-4sp.rsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
use dioxus::prelude::*;

pub fn app() -> Element {
rsx! {
<div class="container">
<h1>"Hello, world!"</h1>
<img src="image.png" />
<button disabled onclick={move |_| println!("clicked")}>"Click me"</button>
<MyComponent prop="value">"children"</MyComponent>
</div>
}
}
13 changes: 13 additions & 0 deletions packages/autofmt/tests/wrong/jsx-4sp.wrong.rsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
use dioxus::prelude::*;

pub fn app() -> Element {
rsx! {
<div class="container" >
<h1 >"Hello, world!"</h1>
<img src="image.png"/>
<button disabled={disabled} onclick={move |_| println!("clicked")}>"Click me"</button>
<MyComponent
prop="value" >"children"</MyComponent>
</div>
}
}
2 changes: 2 additions & 0 deletions packages/rsx-rosetta/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,7 @@ pub fn rsx_node_from_html(node: &Node) -> Option<BodyNode> {
diagnostics: Default::default(),
spreads: Default::default(),
brace: Default::default(),
tag_syntax: false,
}))
}

Expand Down Expand Up @@ -144,6 +145,7 @@ pub fn collect_svgs(children: &mut [BodyNode], out: &mut Vec<BodyNode>) {
fields: vec![],
children: TemplateBody::new(vec![]),
brace: Some(Default::default()),
tag_syntax: false,
});

std::mem::swap(child, &mut new_comp);
Expand Down
7 changes: 7 additions & 0 deletions packages/rsx/src/component.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,11 @@ pub struct Component {
pub brace: Option<token::Brace>,
pub children: TemplateBody,
pub diagnostics: Diagnostics,

/// Whether this component was written using the JSX/XML-like tag syntax (`<MyComponent />`)
///
/// Used by autofmt to print the component back out in the same style it was written in
pub tag_syntax: bool,
}

impl Parse for Component {
Expand Down Expand Up @@ -131,6 +136,7 @@ impl Component {
brace,
spreads,
diagnostics,
tag_syntax: false,
};

// We've received a valid rsx block, but it's not necessarily a valid component
Expand Down Expand Up @@ -392,6 +398,7 @@ impl Component {
spreads: vec![],
children: TemplateBody::new(vec![]),
diagnostics,
tag_syntax: false,
}
}
}
Expand Down
6 changes: 6 additions & 0 deletions packages/rsx/src/element.rs
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,11 @@ pub struct Element {
/// but not technically a valid element - these diagnostics tell us what's wrong and then are used
/// when rendering
pub diagnostics: Diagnostics,

/// Whether this element was written using the JSX/XML-like tag syntax (`<div />`)
///
/// Used by autofmt to print the element back out in the same style it was written in
pub tag_syntax: bool,
}

impl Parse for Element {
Expand Down Expand Up @@ -119,6 +124,7 @@ impl Element {
diagnostics,
spreads: spreads.clone(),
merged_attributes: Vec::new(),
tag_syntax: false,
};

// And then merge the various attributes together
Expand Down
14 changes: 10 additions & 4 deletions packages/rsx/src/jsx.rs
Original file line number Diff line number Diff line change
Expand Up @@ -93,14 +93,17 @@ fn parse_element(stream: ParseStream) -> syn::Result<BodyNode> {
Ok(())
})?;

Ok(BodyNode::Element(Element::from_parts(
let mut element = Element::from_parts(
name,
attributes,
spreads,
children,
Some(brace),
Diagnostics::new(),
)))
);
element.tag_syntax = true;

Ok(BodyNode::Element(element))
}

fn parse_component(stream: ParseStream) -> syn::Result<BodyNode> {
Expand All @@ -126,15 +129,18 @@ fn parse_component(stream: ParseStream) -> syn::Result<BodyNode> {
Ok(())
})?;

Ok(BodyNode::Component(Component::from_parts(
let mut component = Component::from_parts(
name,
generics,
fields,
spreads,
children,
Some(brace),
Diagnostics::new(),
)))
);
component.tag_syntax = true;

Ok(BodyNode::Component(component))
}

/// Parse the attributes of an open tag, stopping at `/>` or `>`
Expand Down
Loading