diff --git a/Cargo.lock b/Cargo.lock index 2f2918225..33fa71e1a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -715,6 +715,7 @@ dependencies = [ "proc-macro2", "regex", "reqwest", + "rsx-rosetta", "serde", "serde_json", "syn", @@ -2268,6 +2269,20 @@ dependencies = [ "winapi", ] +[[package]] +name = "rsx-rosetta" +version = "0.0.0" +source = "git+https://github.com/dioxuslabs/dioxus#04ca284704b3aa7e18cf7b4f2e573e25df3d47eb" +dependencies = [ + "convert_case", + "dioxus-autofmt", + "dioxus-rsx", + "html_parser", + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "rust-argon2" version = "0.8.3" diff --git a/Cargo.toml b/Cargo.toml index 1970fb8c3..0c7349ba4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -82,6 +82,7 @@ dioxus-core = { git = "https://github.com/DioxusLabs/dioxus", features = [ ] } dioxus-autofmt = { git = "https://github.com/dioxuslabs/dioxus" } gitignore = "1.0.7" +rsx-rosetta = { git = "https://github.com/dioxuslabs/dioxus" } [[bin]] path = "src/main.rs" diff --git a/src/builder.rs b/src/builder.rs index 2caa20b90..c6e850b58 100644 --- a/src/builder.rs +++ b/src/builder.rs @@ -385,9 +385,9 @@ fn prettier_build(mut cmd: Command) -> anyhow::Result> { let message = msg.message; match message.level { cargo_metadata::diagnostic::DiagnosticLevel::Error => { - return Err(anyhow::anyhow!(message - .rendered - .unwrap_or("Unknown".into()))); + return Err(anyhow::anyhow!( + message.rendered.unwrap_or("Unknown".into()) + )); } cargo_metadata::diagnostic::DiagnosticLevel::Warning => { warning_messages.push(message.clone()); diff --git a/src/cli/mod.rs b/src/cli/mod.rs index 44888ac57..a27b1b794 100644 --- a/src/cli/mod.rs +++ b/src/cli/mod.rs @@ -16,11 +16,10 @@ use crate::{ gen_page, server, CrateConfig, Error, }; use clap::{Parser, Subcommand}; -use html_parser::{Dom, Element, Node}; +use html_parser::Dom; use regex::Regex; use serde::Deserialize; use std::{ - fmt::{Display, Formatter}, fs::{remove_dir_all, File}, io::{Read, Write}, path::PathBuf, diff --git a/src/cli/translate/mod.rs b/src/cli/translate/mod.rs index bf519c4f9..20ca724fd 100644 --- a/src/cli/translate/mod.rs +++ b/src/cli/translate/mod.rs @@ -1,5 +1,9 @@ use std::process::exit; +use dioxus_rsx::{BodyNode, CallBody, Component}; +use proc_macro2::{Ident, Span}; +use syn::punctuated::Punctuated; + use super::*; /// Build the Rust WASM app and all of its assets. @@ -26,291 +30,111 @@ pub struct Translate { impl Translate { pub fn translate(self) -> Result<()> { - let Translate { - component, - output, - file, - raw, - } = self; + // Get the right input for the translation + let contents = determine_input(self.file, self.raw)?; - let contents = match (file, raw) { - (Some(input), None) => std::fs::read_to_string(&input).unwrap_or_else(|e| { - log::error!("Cloud not read input file: {}.", e); - exit(0); - }), - (None, Some(raw)) => raw, - (Some(_), Some(_)) => { - log::error!("Only one of --file or --raw can be specified."); - exit(0); - } - (None, None) => { - if atty::is(atty::Stream::Stdin) { - return custom_error!("No input file, source, or stdin to translate from."); - } + // Ensure we're loading valid HTML + let dom = html_parser::Dom::parse(&contents)?; - let mut buffer = String::new(); - std::io::stdin().read_to_string(&mut buffer).unwrap(); + // Convert the HTML to RSX + let out = convert_html_to_formatted_rsx(&dom, self.component); - buffer.trim().to_string() - } - }; - - let out_buf = match component { - true => format!("{}", convert_html_to_component(&contents).unwrap()), - false => { - let mut buf = String::new(); - let dom = Dom::parse(&contents).unwrap(); - for child in &dom.children { - simple_render_child(&mut buf, child, 0)?; - } - buf - } - }; - - if let Some(output) = output { - std::fs::write(&output, out_buf)?; - } else { - print!("{}", out_buf); + // Write the output + match self.output { + Some(output) => std::fs::write(&output, out)?, + None => print!("{}", out), } Ok(()) } } -/// render without handling svgs or anything -pub fn simple_render_child( - f: &mut impl std::fmt::Write, - child: &Node, - il: u32, -) -> std::fmt::Result { - write_tabs(f, il)?; - match child { - Node::Text(t) => writeln!(f, "\"{}\"", t)?, - Node::Comment(e) => writeln!(f, "/* {} */", e)?, - Node::Element(el) => { - // open the tag - write!(f, "{} {{ ", &el.name)?; +pub fn convert_html_to_formatted_rsx(dom: &Dom, component: bool) -> String { + let callbody = rsx_rosetta::rsx_from_html(&dom); - // todo: dioxus will eventually support classnames - // for now, just write them with a space between each - let class_iter = &mut el.classes.iter(); - if let Some(first_class) = class_iter.next() { - write!(f, "class: \"{}", first_class)?; - for next_class in class_iter { - write!(f, " {}", next_class)?; - } - write!(f, "\",")?; - } - writeln!(f)?; - - // write the attributes - if let Some(id) = &el.id { - write_tabs(f, il + 1)?; - writeln!(f, "id: \"{}\",", id)?; - } - - for (name, value) in &el.attributes { - write_tabs(f, il + 1)?; - - use convert_case::{Case, Casing}; - if name.chars().any(|ch| ch.is_ascii_uppercase() || ch == '-') { - let new_name = name.to_case(Case::Snake); - match value { - Some(val) => writeln!(f, "{}: \"{}\",", new_name, val)?, - None => writeln!(f, "{}: \"\",", new_name)?, - } - } else { - if matches!(name.as_str(), "for" | "async" | "type" | "as") { - write!(f, "r#")? - } - - match value { - Some(val) => writeln!(f, "{}: \"{}\",", name, val)?, - None => writeln!(f, "{}: \"\",", name)?, - } - } - } - - // now the children - for child in &el.children { - simple_render_child(f, child, il + 1)?; - } - - // close the tag - write_tabs(f, il)?; - writeln!(f, "}}")?; - } - }; - Ok(()) -} - -pub fn convert_html_to_component(html: &str) -> Result { - Ok(ComponentRenderer { - dom: Dom::parse(html)?, - icon_index: 0, - }) -} - -#[allow(dead_code)] -pub struct ComponentRenderer { - dom: Dom, - icon_index: usize, -} - -impl Display for ComponentRenderer { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - writeln!( - f, - r##" -fn component(cx: Scope) -> Element {{ - cx.render(rsx!("## - )?; - let mut svg_nodes = vec![]; - - let mut svg_idx = 0; - for child in &self.dom.children { - render_child(f, child, 2, &mut svg_nodes, true, &mut svg_idx)?; - } - write!( - f, - r##" )) -}}"## - )?; - - if svg_idx == 0 { - return Ok(()); - } - - writeln!(f, "\n\nmod icons {{")?; - writeln!(f, "\tuse super::*;")?; - - let mut id = 0; - while let Some(svg) = svg_nodes.pop() { - writeln!( - f, - r##" pub(super) fn icon_{}(cx: Scope) -> Element {{ - cx.render(rsx!("##, - id - )?; - write_tabs(f, 3)?; - - render_element(f, svg, 3, &mut svg_nodes, false, &mut 0)?; - - writeln!(f, "\t\t))\n\t}}")?; - id += 1; - } - - writeln!(f, "}}")?; - - Ok(()) + match component { + true => write_callbody_with_icon_section(callbody), + false => dioxus_autofmt::write_block_out(callbody).unwrap(), } } -fn render_child<'a>( - f: &mut Formatter<'_>, - child: &'a Node, - il: u32, - svg_buffer: &mut Vec<&'a Element>, - skip_svg: bool, - svg_idx: &mut usize, -) -> std::fmt::Result { - write_tabs(f, il)?; - match child { - Node::Text(t) => writeln!(f, "\"{}\"", t)?, - Node::Comment(e) => writeln!(f, "/* {} */", e)?, - Node::Element(el) => render_element(f, el, il, svg_buffer, skip_svg, svg_idx)?, - }; - Ok(()) +fn write_callbody_with_icon_section(mut callbody: CallBody) -> String { + let mut svgs = vec![]; + + rsx_rosetta::collect_svgs(&mut callbody.roots, &mut svgs); + + let mut out = write_component_body(dioxus_autofmt::write_block_out(callbody).unwrap()); + + if !svgs.is_empty() { + write_svg_section(&mut out, svgs); + } + + out } -fn render_element<'a>( - f: &mut Formatter<'_>, - el: &'a Element, - il: u32, - svg_buffer: &mut Vec<&'a Element>, - skip_svg: bool, - svg_idx: &mut usize, -) -> std::fmt::Result { - if el.name == "svg" && skip_svg { - svg_buffer.push(el); - // todo: attach the right icon ID - writeln!(f, "icons::icon_{} {{}}", svg_idx)?; - *svg_idx += 1; - return Ok(()); - } - - // open the tag - write!(f, "{} {{ ", &el.name)?; - - // todo: dioxus will eventually support classnames - // for now, just write them with a space between each - let class_iter = &mut el.classes.iter(); - if let Some(first_class) = class_iter.next() { - write!(f, "class: \"{}", first_class)?; - for next_class in class_iter { - write!(f, " {}", next_class)?; - } - write!(f, "\",")?; - } - writeln!(f)?; - - // write the attributes - if let Some(id) = &el.id { - write_tabs(f, il + 1)?; - writeln!(f, "id: \"{}\",", id)?; - } - - for (name, value) in &el.attributes { - write_tabs(f, il + 1)?; - - use convert_case::{Case, Casing}; - if name.chars().any(|ch| ch.is_ascii_uppercase() || ch == '-') { - let new_name = name.to_case(Case::Snake); - match value { - Some(val) => writeln!(f, "{}: \"{}\",", new_name, val)?, - None => writeln!(f, "{}: \"\",", new_name)?, - } - } else { - if matches!(name.as_str(), "for" | "async" | "type" | "as") { - write!(f, "r#")? - } - - match value { - Some(val) => writeln!(f, "{}: \"{}\",", name, val)?, - None => writeln!(f, "{}: \"\",", name)?, - } - } - } - - // now the children - for child in &el.children { - render_child(f, child, il + 1, svg_buffer, skip_svg, svg_idx)?; - } - - // close the tag - write_tabs(f, il)?; - writeln!(f, "}}")?; - Ok(()) +fn write_component_body(raw: String) -> String { + let mut out = String::from("fn component(cx: Scope) -> Element {\n cx.render(rsx! {"); + indent_and_write(&raw, 1, &mut out); + out.push_str(" })\n}"); + out } -fn write_tabs(f: &mut impl std::fmt::Write, num: u32) -> std::fmt::Result { - for _ in 0..num { - write!(f, " ")? +fn write_svg_section(out: &mut String, svgs: Vec) { + out.push_str("\n\nmod icons {"); + out.push_str("\n use super::*;"); + for (idx, icon) in svgs.into_iter().enumerate() { + let raw = dioxus_autofmt::write_block_out(CallBody { roots: vec![icon] }).unwrap(); + out.push_str("\n\n pub fn icon_"); + out.push_str(&idx.to_string()); + out.push_str("(cx: Scope) -> Element {\n cx.render(rsx! {"); + indent_and_write(&raw, 2, out); + out.push_str(" })\n }"); } - Ok(()) + + out.push_str("\n}"); +} + +fn indent_and_write(raw: &str, idx: usize, out: &mut String) { + for line in raw.lines() { + for _ in 0..idx { + out.push_str(" "); + } + out.push_str(line); + out.push('\n'); + } +} + +fn determine_input(file: Option, raw: Option) -> Result { + // Make sure not both are specified + if file.is_some() && raw.is_some() { + log::error!("Only one of --file or --raw should be specified."); + exit(0); + } + + if let Some(raw) = raw { + return Ok(raw); + } + + if let Some(file) = file { + return Ok(std::fs::read_to_string(&file)?); + } + + // If neither exist, we try to read from stdin + if atty::is(atty::Stream::Stdin) { + return custom_error!("No input file, source, or stdin to translate from."); + } + + let mut buffer = String::new(); + std::io::stdin().read_to_string(&mut buffer).unwrap(); + + Ok(buffer.trim().to_string()) } #[test] fn generates_svgs() { - use std::io::Write; - let st = include_str!("../../../tests/svg.html"); - let out = format!("{:}", convert_html_to_component(st).unwrap()); - dbg!(&out); + let out = convert_html_to_formatted_rsx(&html_parser::Dom::parse(st).unwrap(), true); - std::fs::File::create("svg_rsx.rs") - .unwrap() - .write_all(out.as_bytes()) - .unwrap(); + println!("{}", out); } diff --git a/src/config.rs b/src/config.rs index 5170f8756..99c487fa2 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,10 +1,11 @@ use crate::error::Result; use serde::{Deserialize, Serialize}; -use std::{collections::HashMap, fs::File, io::Read, path::PathBuf}; +use std::{collections::HashMap, path::PathBuf}; #[derive(Debug, Clone, Serialize, Deserialize)] pub struct DioxusConfig { pub application: ApplicationConfig, + pub web: WebConfig, #[serde(default = "default_plugin")] @@ -16,23 +17,34 @@ fn default_plugin() -> toml::Value { } impl DioxusConfig { - pub fn load() -> crate::error::Result { + pub fn load() -> crate::error::Result> { let crate_dir = crate::cargo::crate_root()?; - if !crate_dir.join("Dioxus.toml").is_file() { - log::warn!("Config file: `Dioxus.toml` not found; using default config."); - return Ok(DioxusConfig::default()); - } + // we support either `Dioxus.toml` or `Cargo.toml` + let Some(dioxus_conf_file) = acquire_dioxus_toml(crate_dir) else { + return Ok(None); + }; - let mut dioxus_conf_file = File::open(crate_dir.join("Dioxus.toml"))?; - let mut meta_str = String::new(); - dioxus_conf_file.read_to_string(&mut meta_str)?; - - toml::from_str::(&meta_str) - .map_err(|_| crate::Error::Unique("Dioxus.toml parse failed".into())) + toml::from_str::(&std::fs::read_to_string(dioxus_conf_file)?) + .map_err(|_| crate::Error::Unique("Dioxus.toml parse failed".into())) + .map(Some) } } +fn acquire_dioxus_toml(dir: PathBuf) -> Option { + // prefer uppercase + if dir.join("Dioxus.toml").is_file() { + return Some(dir.join("Dioxus.toml")); + } + + // lowercase is fine too + if dir.join("dioxus.toml").is_file() { + return Some(dir.join("Dioxus.toml")); + } + + None +} + impl Default for DioxusConfig { fn default() -> Self { Self { @@ -141,7 +153,7 @@ pub enum ExecutableType { impl CrateConfig { pub fn new() -> Result { - let dioxus_config = DioxusConfig::load()?; + let dioxus_config = DioxusConfig::load()?.unwrap_or_default(); let crate_dir = if let Some(package) = &dioxus_config.application.sub_package { crate::cargo::crate_root()?.join(package) diff --git a/src/main.rs b/src/main.rs index 2a9d89859..278fd43b6 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,81 +1,65 @@ +use anyhow::anyhow; use clap::Parser; use dioxus_cli::{plugin::PluginManager, *}; -use std::process::exit; +use Commands::*; #[tokio::main] -async fn main() -> Result<()> { +async fn main() -> anyhow::Result<()> { let args = Cli::parse(); + set_up_logging(); - let dioxus_config = DioxusConfig::load().unwrap_or(DioxusConfig::default()); + let dioxus_config = DioxusConfig::load() + .map_err(|e| anyhow!("Failed to load `dioxus.toml` because: {e}"))? + .unwrap_or_else(|| { + log::warn!("Your `dioxus.toml` could not be found. Using the default config. To set up this crate with dioxus, use `dioxus init`."); + DioxusConfig::default() + }); - let plugin_state = PluginManager::init(dioxus_config.plugin); - - if let Err(e) = plugin_state { - log::error!("🚫 Plugin system initialization failed: {e}"); - exit(1); - } + PluginManager::init(dioxus_config.plugin) + .map_err(|e| anyhow!("🚫 Plugin system initialization failed: {e}"))?; match args.action { - Commands::Translate(opts) => { - if let Err(e) = opts.translate() { - log::error!("🚫 Translate failed: {}", e); - exit(1); - } - } + Translate(opts) => opts + .translate() + .map_err(|e| anyhow!("🚫 Translation of HTML into RSX failed: {}", e)), - Commands::Build(opts) => { - if let Err(e) = opts.build() { - log::error!("🚫 Build project failed: {}", e); - exit(1); - } - } + Build(opts) => opts + .build() + .map_err(|e| anyhow!("🚫 Building project failed: {}", e)), - Commands::Clean(opts) => { - if let Err(e) = opts.clean() { - log::error!("🚫 Clean project failed: {}", e); - exit(1); - } - } + Clean(opts) => opts + .clean() + .map_err(|e| anyhow!("🚫 Cleaning project failed: {}", e)), - Commands::Serve(opts) => { - if let Err(e) = opts.serve().await { - log::error!("🚫 Serve startup failed: {}", e); - exit(1); - } - } + Serve(opts) => opts + .serve() + .await + .map_err(|e| anyhow!("🚫 Serving project failed: {}", e)), - Commands::Create(opts) => { - if let Err(e) = opts.create() { - log::error!("🚫 Create project failed: {}", e); - exit(1); - } - } + Create(opts) => opts + .create() + .map_err(|e| anyhow!("🚫 Creating new project failed: {}", e)), - Commands::Config(opts) => { - if let Err(e) = opts.config() { - log::error!("config error: {}", e); - exit(1); - } - } + Config(opts) => opts + .config() + .map_err(|e| anyhow!("🚫 Configuring new project failed: {}", e)), - Commands::Plugin(opts) => { - if let Err(e) = opts.plugin().await { - log::error!("tool error: {}", e); - } - } + Plugin(opts) => opts + .plugin() + .await + .map_err(|e| anyhow!("🚫 Error with plugin: {}", e)), - Commands::Autoformat(opts) => { - if let Err(e) = opts.autoformat().await { - log::error!("format error: {}", e); - } - } + Autoformat(opts) => opts + .autoformat() + .await + .map_err(|e| anyhow!("🚫 Error autoformatting RSX: {}", e)), - Commands::Version(opt) => { + Version(opt) => { let version = opt.version(); println!("{}", version); + + Ok(()) } } - - Ok(()) } diff --git a/src/plugin/interface/command.rs b/src/plugin/interface/command.rs index 34b6f9082..373689f83 100644 --- a/src/plugin/interface/command.rs +++ b/src/plugin/interface/command.rs @@ -37,7 +37,6 @@ impl UserData for PluginCommander { methods.add_function( "exec", |_, args: (Vec, StdioFromString, StdioFromString)| { - let cmd = args.0; let stdout = args.1; let stderr = args.2; diff --git a/src/plugin/interface/dirs.rs b/src/plugin/interface/dirs.rs index 7837482dc..afce56b5c 100644 --- a/src/plugin/interface/dirs.rs +++ b/src/plugin/interface/dirs.rs @@ -10,4 +10,4 @@ impl UserData for PluginDirs { Ok(path.to_str().unwrap().to_string()) }); } -} \ No newline at end of file +} diff --git a/src/plugin/interface/fs.rs b/src/plugin/interface/fs.rs index d72e41375..a2e169ae2 100644 --- a/src/plugin/interface/fs.rs +++ b/src/plugin/interface/fs.rs @@ -1,13 +1,13 @@ use std::{ - fs::{create_dir, create_dir_all, remove_dir_all}, - path::PathBuf, io::{Read, Write}, + fs::{create_dir, create_dir_all, remove_dir_all, File}, + io::{Read, Write}, + path::PathBuf, }; -use std::fs::File; -use mlua::UserData; -use flate2::read::GzDecoder; -use tar::Archive; use crate::tools::extract_zip; +use flate2::read::GzDecoder; +use mlua::UserData; +use tar::Archive; pub struct PluginFileSystem; impl UserData for PluginFileSystem { @@ -49,9 +49,9 @@ impl UserData for PluginFileSystem { } if file.unwrap().write_all(content.as_bytes()).is_err() { - return Ok(false) + return Ok(false); } - + Ok(true) }); methods.add_function("unzip_file", |_, args: (String, String)| { @@ -64,7 +64,6 @@ impl UserData for PluginFileSystem { Ok(true) }); methods.add_function("untar_gz_file", |_, args: (String, String)| { - let file = PathBuf::from(args.0); let target = PathBuf::from(args.1); @@ -80,7 +79,6 @@ impl UserData for PluginFileSystem { return Ok(false); } - Ok(true) }); } diff --git a/src/plugin/interface/log.rs b/src/plugin/interface/log.rs index d67ae0316..bed8e2cbb 100644 --- a/src/plugin/interface/log.rs +++ b/src/plugin/interface/log.rs @@ -25,4 +25,4 @@ impl UserData for PluginLogger { Ok(()) }); } -} \ No newline at end of file +} diff --git a/src/plugin/interface/os.rs b/src/plugin/interface/os.rs index 33368f38a..73295d930 100644 --- a/src/plugin/interface/os.rs +++ b/src/plugin/interface/os.rs @@ -15,4 +15,4 @@ impl UserData for PluginOS { } }); } -} \ No newline at end of file +} diff --git a/src/plugin/interface/path.rs b/src/plugin/interface/path.rs index f0d36bd78..510ead409 100644 --- a/src/plugin/interface/path.rs +++ b/src/plugin/interface/path.rs @@ -12,7 +12,7 @@ impl UserData for PluginPath { path = path.join(i); } Ok(path.to_str().unwrap().to_string()) - }); + }); // parent function methods.add_function("parent", |_, path: String| { diff --git a/src/plugin/mod.rs b/src/plugin/mod.rs index c77764fd4..c75e96444 100644 --- a/src/plugin/mod.rs +++ b/src/plugin/mod.rs @@ -13,10 +13,9 @@ use crate::{ }; use self::{ - interface::PluginInfo, interface::{ command::PluginCommander, dirs::PluginDirs, fs::PluginFileSystem, log::PluginLogger, - network::PluginNetwork, os::PluginOS, path::PluginPath, + network::PluginNetwork, os::PluginOS, path::PluginPath, PluginInfo, }, types::PluginConfig, }; @@ -143,7 +142,9 @@ impl PluginManager { } } Ok(false) => { - log::warn!("Plugin init function result is `false`, init failed."); + log::warn!( + "Plugin init function result is `false`, init failed." + ); } Err(e) => { log::warn!("Plugin init failed: {e}"); diff --git a/src/tools.rs b/src/tools.rs index de762d364..704104de6 100644 --- a/src/tools.rs +++ b/src/tools.rs @@ -252,8 +252,8 @@ impl Tool { if self.target_platform() == "linux" { // This code does not update permissions idk why - /*let mut perms = final_file.metadata()?.permissions(); - perms.set_mode(0o744);*/ + // let mut perms = final_file.metadata()?.permissions(); + // perms.set_mode(0o744); // Adding to the binary execution rights with "chmod" let mut command = Command::new("chmod");