diff --git a/.vscode/settings.json b/.vscode/settings.json index 819ffec5d..ebd52887e 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -2,5 +2,6 @@ "editor.formatOnSave": true, "[toml]": { "editor.formatOnSave": false - } + }, + "rust-analyzer.checkOnSave.allTargets": false, } diff --git a/Cargo.toml b/Cargo.toml index 5955451f8..606aef6be 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,6 +18,7 @@ members = [ "packages/tui", "packages/native-core", "packages/native-core-macro", + "packages/rsx-rosetta", "docs/guide", ] diff --git a/packages/autofmt/src/lib.rs b/packages/autofmt/src/lib.rs index aca9f64b0..a5da71c9b 100644 --- a/packages/autofmt/src/lib.rs +++ b/packages/autofmt/src/lib.rs @@ -1,3 +1,5 @@ +use dioxus_rsx::CallBody; + use crate::buffer::*; use crate::util::*; @@ -31,6 +33,11 @@ pub struct FormattedBlock { /// Format a file into a list of `FormattedBlock`s to be applied by an IDE for autoformatting. /// /// This function expects a complete file, not just a block of code. To format individual rsx! blocks, use fmt_block instead. +/// +/// The point here is to provide precise modifications of a source file so an accompanying IDE tool can map these changes +/// back to the file precisely. +/// +/// Nested blocks of RSX will be handled automatically pub fn fmt_file(contents: &str) -> Vec { let mut formatted_blocks = Vec::new(); let mut last_bracket_end = 0; @@ -93,15 +100,32 @@ pub fn fmt_file(contents: &str) -> Vec { formatted_blocks } +pub fn write_block_out(body: CallBody) -> Option { + let mut buf = Buffer { + src: vec!["".to_string()], + indent: 0, + ..Buffer::default() + }; + + // Oneliner optimization + if buf.is_short_children(&body.roots).is_some() { + buf.write_ident(&body.roots[0]).unwrap(); + } else { + buf.write_body_indented(&body.roots).unwrap(); + } + + buf.consume() +} + pub fn fmt_block(block: &str, indent_level: usize) -> Option { + let body = syn::parse_str::(block).ok()?; + let mut buf = Buffer { src: block.lines().map(|f| f.to_string()).collect(), indent: indent_level, ..Buffer::default() }; - let body = syn::parse_str::(block).unwrap(); - // Oneliner optimization if buf.is_short_children(&body.roots).is_some() { buf.write_ident(&body.roots[0]).unwrap(); diff --git a/packages/rsx-rosetta/Cargo.toml b/packages/rsx-rosetta/Cargo.toml new file mode 100644 index 000000000..2def4e7c8 --- /dev/null +++ b/packages/rsx-rosetta/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "rsx-rosetta" +version = "0.0.0" +edition = "2018" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +dioxus-autofmt = { path = "../autofmt" } +dioxus-rsx = { path = "../rsx" } +html_parser = "0.6.3" +proc-macro2 = "1.0.49" +quote = "1.0.23" +syn = { version = "1.0.107", features = ["full"] } +convert_case = "0.5.0" + +# [features] +# default = ["html"] + +# eventually more output options diff --git a/packages/rsx-rosetta/README.md b/packages/rsx-rosetta/README.md new file mode 100644 index 000000000..35f86d00a --- /dev/null +++ b/packages/rsx-rosetta/README.md @@ -0,0 +1,19 @@ +# Rosetta for RSX +--- + +Dioxus sports its own templating language inspired by C#/Kotlin/RTMP, etc. It's pretty straightforward. + +However, it's NOT HTML. This is done since HTML is verbose and you'd need a dedicated LSP or IDE integration to get a good DX in .rs files. + +RSX is simple... It's similar enough to regular Rust code to trick most IDEs into automatically providing support for things like block selections, folding, highlighting, etc. + +To accomodate the transition from HTML to RSX, you might need to translate some existing code. + +This library provids a central AST that can accept a number of inputs: + +- HTML +- Syn (todo) +- Akama (todo) +- Jinja (todo) + +From there, you can convert directly to a string or into some other AST. diff --git a/packages/rsx-rosetta/examples/html.rs b/packages/rsx-rosetta/examples/html.rs new file mode 100644 index 000000000..ef3006f31 --- /dev/null +++ b/packages/rsx-rosetta/examples/html.rs @@ -0,0 +1,24 @@ +use html_parser::Dom; + +fn main() { + let html = r#" +
+
hello world!
+
hello world!
+
hello world!
+
hello world!
+
hello world!
+
hello world!
+ hello world! +
+ "# + .trim(); + + let dom = Dom::parse(html).unwrap(); + + let body = rsx_rosetta::rsx_from_html(dom); + + let out = dioxus_autofmt::write_block_out(body).unwrap(); + + println!("{}", out); +} diff --git a/packages/rsx-rosetta/src/lib.rs b/packages/rsx-rosetta/src/lib.rs new file mode 100644 index 000000000..430ab54dc --- /dev/null +++ b/packages/rsx-rosetta/src/lib.rs @@ -0,0 +1,87 @@ +use convert_case::{Case, Casing}; +use dioxus_rsx::{BodyNode, CallBody, Element, ElementAttr, ElementAttrNamed, IfmtInput}; +pub use html_parser::{Dom, Node}; +use proc_macro2::{Ident, Span}; +use syn::LitStr; + +/// Convert an HTML DOM tree into an RSX CallBody +pub fn rsx_from_html(dom: &Dom) -> CallBody { + CallBody { + roots: dom.children.iter().filter_map(rsx_node_from_html).collect(), + } +} + +/// Convert an HTML Node into an RSX BodyNode +/// +/// If the node is a comment, it will be ignored since RSX doesn't support comments +pub fn rsx_node_from_html(node: &Node) -> Option { + match node { + Node::Text(text) => Some(BodyNode::Text(ifmt_from_text(text))), + Node::Element(el) => { + let el_name = el.name.to_case(Case::Snake); + let el_name = Ident::new(el_name.as_str(), Span::call_site()); + + let mut attributes: Vec<_> = el + .attributes + .iter() + .map(|(name, value)| { + let ident = if matches!(name.as_str(), "for" | "async" | "type" | "as") { + Ident::new_raw(name.as_str(), Span::call_site()) + } else { + let new_name = name.to_case(Case::Snake); + Ident::new(new_name.as_str(), Span::call_site()) + }; + + ElementAttrNamed { + el_name: el_name.clone(), + attr: ElementAttr::AttrText { + value: ifmt_from_text(value.as_deref().unwrap_or("false")), + name: ident, + }, + } + }) + .collect(); + + let class = el.classes.join(" "); + if !class.is_empty() { + attributes.push(ElementAttrNamed { + el_name: el_name.clone(), + attr: ElementAttr::AttrText { + name: Ident::new("class", Span::call_site()), + value: ifmt_from_text(&class), + }, + }); + } + + if let Some(id) = &el.id { + attributes.push(ElementAttrNamed { + el_name: el_name.clone(), + attr: ElementAttr::AttrText { + name: Ident::new("id", Span::call_site()), + value: ifmt_from_text(id), + }, + }); + } + + let children = el.children.iter().filter_map(rsx_node_from_html).collect(); + + Some(BodyNode::Element(Element { + name: el_name, + children, + attributes, + _is_static: false, + key: None, + })) + } + + // We ignore comments + Node::Comment(_) => None, + } +} + +fn ifmt_from_text(text: &str) -> IfmtInput { + IfmtInput { + source: Some(LitStr::new(text, Span::call_site())), + segments: vec![], + } +}