407 lines
13 KiB
Rust
407 lines
13 KiB
Rust
use crate::{ifmt_to_string, Writer};
|
|
use dioxus_rsx::*;
|
|
use proc_macro2::Span;
|
|
use quote::ToTokens;
|
|
use std::{
|
|
fmt::Result,
|
|
fmt::{self, Write},
|
|
};
|
|
use syn::{spanned::Spanned, token::Brace, Expr};
|
|
|
|
#[derive(Debug)]
|
|
enum ShortOptimization {
|
|
/// Special because we want to print the closing bracket immediately
|
|
///
|
|
/// IE
|
|
/// `div {}` instead of `div { }`
|
|
Empty,
|
|
|
|
/// Special optimization to put everything on the same line and add some buffer spaces
|
|
///
|
|
/// IE
|
|
///
|
|
/// `div { "asdasd" }` instead of a multiline variant
|
|
Oneliner,
|
|
|
|
/// Optimization where children flow but props remain fixed on top
|
|
PropsOnTop,
|
|
|
|
/// The noisiest optimization where everything flows
|
|
NoOpt,
|
|
}
|
|
|
|
/*
|
|
// whitespace
|
|
div {
|
|
// some whitespace
|
|
class: "asdasd"
|
|
|
|
// whjiot
|
|
asdasd // whitespace
|
|
}
|
|
*/
|
|
|
|
impl Writer<'_> {
|
|
pub fn write_element(&mut self, el: &Element) -> Result {
|
|
let Element {
|
|
name,
|
|
key,
|
|
attributes,
|
|
children,
|
|
_is_static,
|
|
brace,
|
|
} = el;
|
|
|
|
/*
|
|
1. Write the tag
|
|
2. Write the key
|
|
3. Write the attributes
|
|
4. Write the children
|
|
*/
|
|
|
|
write!(self.out, "{name} {{")?;
|
|
|
|
// decide if we have any special optimizations
|
|
// Default with none, opt the cases in one-by-one
|
|
let mut opt_level = ShortOptimization::NoOpt;
|
|
|
|
// check if we have a lot of attributes
|
|
let attr_len = self.is_short_attrs(attributes);
|
|
let is_short_attr_list = (attr_len + self.out.indent * 4) < 80;
|
|
let children_len = self.is_short_children(children);
|
|
let is_small_children = children_len.is_some();
|
|
|
|
// if we have few attributes and a lot of children, place the attrs on top
|
|
if is_short_attr_list && !is_small_children {
|
|
opt_level = ShortOptimization::PropsOnTop;
|
|
}
|
|
|
|
// even if the attr is long, it should be put on one line
|
|
if !is_short_attr_list && attributes.len() <= 1 {
|
|
if children.is_empty() {
|
|
opt_level = ShortOptimization::Oneliner;
|
|
} else {
|
|
opt_level = ShortOptimization::PropsOnTop;
|
|
}
|
|
}
|
|
|
|
// if we have few children and few attributes, make it a one-liner
|
|
if is_short_attr_list && is_small_children {
|
|
if children_len.unwrap() + attr_len + self.out.indent * 4 < 100 {
|
|
opt_level = ShortOptimization::Oneliner;
|
|
} else {
|
|
opt_level = ShortOptimization::PropsOnTop;
|
|
}
|
|
}
|
|
|
|
// If there's nothing at all, empty optimization
|
|
if attributes.is_empty() && children.is_empty() && key.is_none() {
|
|
opt_level = ShortOptimization::Empty;
|
|
|
|
// Write comments if they exist
|
|
self.write_todo_body(brace)?;
|
|
}
|
|
|
|
// multiline handlers bump everything down
|
|
if attr_len > 1000 {
|
|
opt_level = ShortOptimization::NoOpt;
|
|
}
|
|
|
|
match opt_level {
|
|
ShortOptimization::Empty => {}
|
|
ShortOptimization::Oneliner => {
|
|
write!(self.out, " ")?;
|
|
|
|
self.write_attributes(attributes, key, true)?;
|
|
|
|
if !children.is_empty() && (!attributes.is_empty() || key.is_some()) {
|
|
write!(self.out, ", ")?;
|
|
}
|
|
|
|
for (id, child) in children.iter().enumerate() {
|
|
self.write_ident(child)?;
|
|
if id != children.len() - 1 && children.len() > 1 {
|
|
write!(self.out, ", ")?;
|
|
}
|
|
}
|
|
|
|
write!(self.out, " ")?;
|
|
}
|
|
|
|
ShortOptimization::PropsOnTop => {
|
|
if !attributes.is_empty() || key.is_some() {
|
|
write!(self.out, " ")?;
|
|
}
|
|
self.write_attributes(attributes, key, true)?;
|
|
|
|
if !children.is_empty() && (!attributes.is_empty() || key.is_some()) {
|
|
write!(self.out, ",")?;
|
|
}
|
|
|
|
if !children.is_empty() {
|
|
self.write_body_indented(children)?;
|
|
}
|
|
self.out.tabbed_line()?;
|
|
}
|
|
|
|
ShortOptimization::NoOpt => {
|
|
self.write_attributes(attributes, key, false)?;
|
|
|
|
if !children.is_empty() && (!attributes.is_empty() || key.is_some()) {
|
|
write!(self.out, ",")?;
|
|
}
|
|
|
|
if !children.is_empty() {
|
|
self.write_body_indented(children)?;
|
|
}
|
|
|
|
self.out.tabbed_line()?;
|
|
}
|
|
}
|
|
|
|
write!(self.out, "}}")?;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
fn write_attributes(
|
|
&mut self,
|
|
attributes: &[ElementAttrNamed],
|
|
key: &Option<IfmtInput>,
|
|
sameline: bool,
|
|
) -> Result {
|
|
let mut attr_iter = attributes.iter().peekable();
|
|
|
|
if let Some(key) = key {
|
|
if !sameline {
|
|
self.out.indented_tabbed_line()?;
|
|
}
|
|
write!(self.out, "key: {}", ifmt_to_string(key))?;
|
|
if !attributes.is_empty() {
|
|
write!(self.out, ",")?;
|
|
if sameline {
|
|
write!(self.out, " ")?;
|
|
}
|
|
}
|
|
}
|
|
|
|
while let Some(attr) = attr_iter.next() {
|
|
self.out.indent += 1;
|
|
if !sameline {
|
|
self.write_comments(attr.attr.start())?;
|
|
}
|
|
self.out.indent -= 1;
|
|
|
|
if !sameline {
|
|
self.out.indented_tabbed_line()?;
|
|
}
|
|
|
|
self.write_attribute(attr)?;
|
|
|
|
if attr_iter.peek().is_some() {
|
|
write!(self.out, ",")?;
|
|
|
|
if sameline {
|
|
write!(self.out, " ")?;
|
|
}
|
|
}
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
fn write_attribute(&mut self, attr: &ElementAttrNamed) -> Result {
|
|
match &attr.attr {
|
|
ElementAttr::AttrText { name, value } => {
|
|
write!(self.out, "{name}: {value}", value = ifmt_to_string(value))?;
|
|
}
|
|
ElementAttr::AttrExpression { name, value } => {
|
|
let out = prettyplease::unparse_expr(value);
|
|
write!(self.out, "{name}: {out}")?;
|
|
}
|
|
|
|
ElementAttr::CustomAttrText { name, value } => {
|
|
write!(
|
|
self.out,
|
|
"{name}: {value}",
|
|
name = name.to_token_stream(),
|
|
value = ifmt_to_string(value)
|
|
)?;
|
|
}
|
|
|
|
ElementAttr::CustomAttrExpression { name, value } => {
|
|
let out = prettyplease::unparse_expr(value);
|
|
write!(self.out, "{}: {}", name.to_token_stream(), out)?;
|
|
}
|
|
|
|
ElementAttr::EventTokens { name, tokens } => {
|
|
let out = self.retrieve_formatted_expr(tokens).to_string();
|
|
|
|
let mut lines = out.split('\n').peekable();
|
|
let first = lines.next().unwrap();
|
|
|
|
// a one-liner for whatever reason
|
|
// Does not need a new line
|
|
if lines.peek().is_none() {
|
|
write!(self.out, "{name}: {first}")?;
|
|
} else {
|
|
writeln!(self.out, "{name}: {first}")?;
|
|
|
|
while let Some(line) = lines.next() {
|
|
self.out.indented_tab()?;
|
|
write!(self.out, "{line}")?;
|
|
if lines.peek().is_none() {
|
|
write!(self.out, "")?;
|
|
} else {
|
|
writeln!(self.out)?;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
// make sure the comments are actually relevant to this element.
|
|
// test by making sure this element is the primary element on this line
|
|
pub fn current_span_is_primary(&self, location: Span) -> bool {
|
|
let start = location.start();
|
|
let line_start = start.line - 1;
|
|
|
|
let beginning = self
|
|
.src
|
|
.get(line_start)
|
|
.filter(|this_line| this_line.len() > start.column)
|
|
.map(|this_line| this_line[..start.column].trim())
|
|
.unwrap_or_default();
|
|
|
|
beginning.is_empty()
|
|
}
|
|
|
|
// check if the children are short enough to be on the same line
|
|
// We don't have the notion of current line depth - each line tries to be < 80 total
|
|
// returns the total line length if it's short
|
|
// returns none if the length exceeds the limit
|
|
// I think this eventually becomes quadratic :(
|
|
pub fn is_short_children(&mut self, children: &[BodyNode]) -> Option<usize> {
|
|
if children.is_empty() {
|
|
// todo: allow elements with comments but no children
|
|
// like div { /* comment */ }
|
|
// or
|
|
// div {
|
|
// // some helpful
|
|
// }
|
|
return Some(0);
|
|
}
|
|
|
|
for child in children {
|
|
if self.current_span_is_primary(child.span()) {
|
|
'line: for line in self.src[..child.span().start().line - 1].iter().rev() {
|
|
match (line.trim().starts_with("//"), line.is_empty()) {
|
|
(true, _) => return None,
|
|
(_, true) => continue 'line,
|
|
_ => break 'line,
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
match children {
|
|
[BodyNode::Text(ref text)] => Some(ifmt_to_string(text).len()),
|
|
[BodyNode::Component(ref comp)] => {
|
|
let attr_len = self.field_len(&comp.fields, &comp.manual_props);
|
|
|
|
if attr_len > 80 {
|
|
None
|
|
} else if comp.children.is_empty() {
|
|
Some(attr_len)
|
|
} else {
|
|
None
|
|
}
|
|
}
|
|
// TODO: let rawexprs to be inlined
|
|
[BodyNode::RawExpr(ref expr)] => get_expr_length(expr),
|
|
[BodyNode::Element(ref el)] => {
|
|
let attr_len = self.is_short_attrs(&el.attributes);
|
|
|
|
if el.children.is_empty() && attr_len < 80 {
|
|
return Some(el.name.to_string().len());
|
|
}
|
|
|
|
if el.children.len() == 1 {
|
|
if let BodyNode::Text(ref text) = el.children[0] {
|
|
let value = ifmt_to_string(text);
|
|
|
|
if value.len() + el.name.to_string().len() + attr_len < 80 {
|
|
return Some(value.len() + el.name.to_string().len() + attr_len);
|
|
}
|
|
}
|
|
}
|
|
|
|
None
|
|
}
|
|
// todo, allow non-elements to be on the same line
|
|
items => {
|
|
let mut total_count = 0;
|
|
|
|
for item in items {
|
|
match item {
|
|
BodyNode::Component(_) | BodyNode::Element(_) => return None,
|
|
BodyNode::Text(text) => {
|
|
total_count += ifmt_to_string(text).len();
|
|
}
|
|
BodyNode::RawExpr(expr) => match get_expr_length(expr) {
|
|
Some(len) => total_count += len,
|
|
None => return None,
|
|
},
|
|
BodyNode::ForLoop(_forloop) => return None,
|
|
BodyNode::IfChain(_chain) => return None,
|
|
}
|
|
}
|
|
|
|
Some(total_count)
|
|
}
|
|
}
|
|
}
|
|
|
|
/// empty everything except for some comments
|
|
fn write_todo_body(&mut self, brace: &Brace) -> fmt::Result {
|
|
let span = brace.span.span();
|
|
let start = span.start();
|
|
let end = span.end();
|
|
|
|
if start.line == end.line {
|
|
return Ok(());
|
|
}
|
|
|
|
writeln!(self.out)?;
|
|
|
|
for idx in start.line..end.line {
|
|
let line = &self.src[idx];
|
|
if line.trim().starts_with("//") {
|
|
for _ in 0..self.out.indent + 1 {
|
|
write!(self.out, " ")?
|
|
}
|
|
writeln!(self.out, "{}", line.trim()).unwrap();
|
|
}
|
|
}
|
|
|
|
for _ in 0..self.out.indent {
|
|
write!(self.out, " ")?
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
fn get_expr_length(expr: &Expr) -> Option<usize> {
|
|
let span = expr.span();
|
|
let (start, end) = (span.start(), span.end());
|
|
if start.line == end.line {
|
|
Some(end.column - start.column)
|
|
} else {
|
|
None
|
|
}
|
|
}
|