notes/architecture/03-RSX.md
The dioxus-rsx crate parses JSX-like syntax into Rust code, while dioxus-autofmt provides formatting capabilities.
Root struct for rsx! {} macro contents:
CallBody
├── TemplateBody (list of BodyNode roots)
├── template_idx: Cell<usize>
└── span: Option<Span>
CallBody::new() initializes template indices and cascades hotreload info through nested structures.
Six variants representing RSX content:
Element(Element) - HTML elements (div, span)Component(Component) - User componentsText(TextNode) - String literals with interpolationRawExpr(ExprNode) - Braced expressions {expr}ForLoop(ForLoop) - for pat in expr { body }IfChain(IfChain) - if cond { } else { }for → ForLoopif → IfChainmatch → RawExpr- → ElementElement
├── name: ElementName (Ident or Custom)
├── raw_attributes: Vec<Attribute>
├── merged_attributes: Vec<Attribute> // After combining duplicates
├── spreads: Vec<Spread>
├── children: Vec<BodyNode>
├── brace: Option<Brace>
└── diagnostics: Diagnostics
merge_attributes() collapses duplicate attribute names using IfmtInput with space delimiter.
Attribute
├── name: AttributeName (BuiltIn, Custom, or Spread)
├── value: AttributeValue
├── colon: Option<Token![:]>
├── dyn_idx: DynIdx
└── el_name: Option<ElementName>
Shorthand(Ident) - attribute without valueAttrLiteral(HotLiteral) - hotreloadable literalEventTokens(PartialClosure) - event handlersIfExpr(IfAttributeValue) - conditional attributesAttrExpr(PartialExpr) - arbitrary expressionsHotLiteral
├── Fmted(HotReloadFormattedSegment) // "{expr}" interpolation
├── Float(LitFloat)
├── Int(LitInt)
└── Bool(LitBool)
Parses string contents into segments:
Segment::Literal(String) - plain textSegment::Formatted(FormattedSegment) - {expr} interpolationParsing rules:
{{ → literal {}} → literal }{expr} → formatted segment{expr:format_args} → formatted with format specComponent
├── name: syn::Path
├── generics: Option<AngleBracketedGenericArguments>
├── fields: Vec<Attribute>
├── component_literal_dyn_idx: Vec<DynIdx>
├── spreads: Vec<Spread>
├── children: TemplateBody
├── dyn_idx: DynIdx
└── diagnostics: Diagnostics
ForLoop
├── for_token, pat, in_token
├── expr: Box<Expr>
├── body: TemplateBody
└── dyn_idx: DynIdx
Generates: (expr).into_iter().map(|pat| { body })
IfChain
├── if_token, cond: Box<Expr>
├── then_branch: TemplateBody
├── else_if_branch: Option<Box<IfChain>>
├── else_branch: Option<TemplateBody>
└── dyn_idx: DynIdx
Cell<Option<usize>> for tracking dynamic indices. Transparent in PartialEq/Eq/Hash. Used for hot-reload mapping.
Generated output structure:
__TEMPLATE_ROOTS - Static array of TemplateNodedioxus_core::Element::Ok({
#[cfg(debug_assertions)]
fn __original_template() -> &'static HotReloadedTemplate { ... }
let __dynamic_nodes: [DynamicNode; N] = [ ... ];
let __dynamic_attributes: [Box<[Attribute]>; M] = [ ... ];
static __TEMPLATE_ROOTS: &[TemplateNode] = &[ ... ];
// Template reference and rendering
})
Element { tag, namespace, attrs, children } - Static elementText { text } - Static textDynamic { id } - References dynamic node poolTemplateBody
├── roots: Vec<BodyNode>
├── template_idx: DynIdx
├── node_paths: Vec<Vec<u8>> // Path to each dynamic node
├── attr_paths: Vec<(Vec<u8>, usize)> // Path and attribute index
├── dynamic_text_segments: Vec<FormattedSegment>
└── diagnostics: Diagnostics
CallBody::next_template_idx() generates sequential IDsfile!(), line!(), column!() for source locationWraps IfmtInput with dynamic_node_indexes: Vec<DynIdx>:
Segment::Formatted entrytry_fmt_file(contents, &syn::File, IndentOptions) → Vec<FormattedBlock>fmt_block(block_str, indent_level, IndentOptions) → Option<String>write_block_out(body) → Option<String>FormattedBlock
├── formatted: String
├── start: usize (byte offset)
└── end: usize (byte offset)
Writer
├── raw_src: &str
├── src: Vec<&str> // Lines
├── out: Buffer
├── cached_formats: HashMap<LineColumn, String>
└── invalid_exprs: Vec<Span>
Buffer
├── buf: String
├── indent_level: usize
└── indent: IndentOptions
div {} (no space inside)div { class: "x", child {} } (single line)if formatted.len() <= 80
&& !formatted.contains('\n')
&& !body_is_solo_expr
&& !formatted.trim().is_empty()
{
formatted = format!(" {formatted} "); // Collapse to single line
}
Whitespace is significant in RSX:
write_comments() accumulates full-line comments before spanswrite_inline_comments() preserves end-of-line commentsLineColumn from Spanprettier_please to unparse expressionsunparse_expr() visits macro calls:
Uses marker "𝕣𝕤𝕩" to replace macros during unparse, avoiding conflicts with actual code.
IndentOptions
├── width: usize
├── indent_string: String ("\t" or spaces)
└── split_line_attributes: bool
indent_str() → returns indent stringcount_indents(line) → estimates indent levelline_length(line) → estimates visible lengthtrue: attrs on one line with spacesfalse: each attr on new line, indentedShorthand → just identAttrLiteral → uses Display implEventTokens → write_partial_expr()IfExpr → write_attribute_if_chain()AttrExpr → write_partial_expr()