diff --git a/Cargo.lock b/Cargo.lock index 547444e..a98bc7d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -131,6 +131,7 @@ version = "0.1.0" dependencies = [ "cogs_ast", "eyre", + "intern-arc", ] [[package]] @@ -142,6 +143,10 @@ dependencies = [ "tracing", ] +[[package]] +name = "cogs_runtime" +version = "0.1.0" + [[package]] name = "color-eyre" version = "0.6.3" @@ -267,6 +272,17 @@ dependencies = [ "similar", ] +[[package]] +name = "intern-arc" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "328e5c9569f7aa3ee795c58e1e7da8875c1eb506fba10341e912d892e6d0a193" +dependencies = [ + "loom", + "once_cell", + "parking_lot", +] + [[package]] name = "lazy_static" version = "1.5.0" @@ -297,6 +313,16 @@ version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" +[[package]] +name = "lock_api" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" +dependencies = [ + "autocfg", + "scopeguard", +] + [[package]] name = "log" version = "0.4.22" @@ -390,6 +416,9 @@ name = "once_cell" version = "1.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" +dependencies = [ + "parking_lot_core", +] [[package]] name = "overload" @@ -403,6 +432,29 @@ version = "3.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c1b04fb49957986fdce4d6ee7a65027d55d4b6d2265e5848bbb507b58ccfdb6f" +[[package]] +name = "parking_lot" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-targets", +] + [[package]] name = "pin-project-lite" version = "0.2.15" @@ -501,6 +553,15 @@ dependencies = [ "rand_core", ] +[[package]] +name = "redox_syscall" +version = "0.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b6dfecf2c74bce2466cabf93f6664d6998a69eb21e39f4207930065b27b771f" +dependencies = [ + "bitflags", +] + [[package]] name = "regex" version = "1.11.1" @@ -588,6 +649,12 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294" +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + [[package]] name = "sharded-slab" version = "0.1.7" diff --git a/Cargo.toml b/Cargo.toml index f04ed00..fee0d5e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [workspace] members = [ - "crates/ast", + "crates/ast", "crates/runtime", "crates/codegen", "crates/parser", "integrations/axum" diff --git a/crates/codegen/Cargo.toml b/crates/codegen/Cargo.toml index 647fcaf..797a395 100644 --- a/crates/codegen/Cargo.toml +++ b/crates/codegen/Cargo.toml @@ -6,3 +6,4 @@ edition = "2021" [dependencies] cogs_ast = { path = "../ast" } eyre.workspace = true +intern-arc = "0.6.1" diff --git a/crates/codegen/src/generate.rs b/crates/codegen/src/generate.rs new file mode 100644 index 0000000..02f3255 --- /dev/null +++ b/crates/codegen/src/generate.rs @@ -0,0 +1,177 @@ +use super::*; + +pub struct Generator { + pub trees: Vec, + pub intern_str: StrInterner, +} + +macro_rules! push { + (@noquote $cx:expr, $fmt:expr, $($arg:expr),+$(,)?) => {{ + $cx.format.push_str($fmt); + $cx.arguments.extend([$($arg),+]); + }}; + ($cx:expr, $fmt:expr, $($arg:expr),+$(,)?) => {{ + $cx.format.push_str($fmt); + $cx.arguments.extend([$(quoted(&$arg)),+]); + }}; +} + +struct AppendContext { + pre: String, + format: String, + arguments: Vec, +} + +impl AppendContext { + #[inline] + fn push(&mut self, format: &str) { + self.format.push_str(format); + } + + #[inline] + fn push_arg(&mut self, value: String) { + self.push("{}"); + self.arguments.push(value); + } +} + +fn quoted(s: &str) -> String { + format!("r#\"{}\"#", s) +} + +impl Expression { + fn append(&self, cx: &mut AppendContext) { + match self { + Expression::Literal(literal) => push!(cx, "\"{}\"", literal), + Expression::Code(code) => cx.push_arg(code.trim().to_string()), + } + } +} + +impl HtmlTag { + fn append(&self, cx: &mut AppendContext) { + push!( + cx, + // add space after, if there are attributes + if self.attributes.is_empty() { + "<{}" + } else { + "<{} " + }, + self.tag, + ); + + for attr in &self.attributes { + push!( + cx, + if attr.value.is_some() { "{}=" } else { "{}" }, + attr.name + ); + if let Some(value) = &attr.value { + value.append(cx); + } + } + if self.content.is_empty() { + cx.push("/>"); + return; + } + + cx.push(">"); + + for tree in &self.content { + tree.append(cx); + } + + push!(cx, "", self.tag); + } +} + +impl CodeTree { + fn append(&self, cx: &mut AppendContext) { + match self { + CodeTree::Code(code) => { + let trimmed = code.trim(); + if trimmed.is_empty() { + return; + } + cx.pre.push_str(code.trim()); + } + CodeTree::HtmlTag(html_tag) => html_tag.append(cx), + } + } +} + +impl CodeBlock { + fn append(&self, cx: &mut AppendContext) { + let mut my_cx = AppendContext { + pre: String::new(), + format: String::new(), + arguments: Vec::new(), + }; + for code in self.content.iter() { + code.append(&mut my_cx); + } + + let fmt = if my_cx.format.is_empty() { + my_cx.pre + } else { + format!( + r##"{{{pre} +format!(r#"{format}"#, {arguments}) +}}"##, + pre = my_cx.pre, + format = my_cx.format, + arguments = my_cx.arguments.join(","), + ) + }; + + push!(@noquote cx, "{}", fmt); + } +} + +impl Tree { + fn append(&self, cx: &mut AppendContext) { + match self { + Tree::HtmlText(text) => { + // this adds {} to the format string and adds "{text}" to the args + cx.push_arg(quoted(text.trim())); + } + Tree::HtmlTag(html_tag) => html_tag.append(cx), + Tree::CodeBlock(code_block) => code_block.append(cx), + } + } +} + +impl Generator { + pub fn to_format(&self) -> String { + let mut cx = AppendContext { + pre: String::new(), + format: String::new(), + arguments: Vec::new(), + }; + for tree in self.trees.iter() { + if let Tree::CodeBlock(code_block) = tree { + if !code_block.has_html { + for code in code_block.content.iter() { + let CodeTree::Code(code) = code else { + panic!("has_html = false, but got CodeTree::HtmlTag") + }; + let trimmed = code.trim(); + if trimmed.is_empty() { + continue; + } + cx.pre.push_str(code.trim()); + } + } + } else { + tree.append(&mut cx); + } + } + format!( + "{pre}let __rendered = format!(r#\"{format}\"#, {arguments});", + pre = cx.pre, + format = cx.format, + arguments = cx.arguments.join(","), + ) + } +} diff --git a/crates/codegen/src/ir.rs b/crates/codegen/src/ir.rs new file mode 100644 index 0000000..cb0f537 --- /dev/null +++ b/crates/codegen/src/ir.rs @@ -0,0 +1,122 @@ +use super::*; + +pub enum Expression { + Literal(InternedStr), + Code(InternedStr), +} + +pub struct HtmlAttribute { + pub name: InternedStr, + pub value: Option, +} + +pub struct HtmlTag { + pub tag: InternedStr, + pub attributes: Vec, + pub content: Vec, +} + +pub enum CodeTree { + HtmlTag(HtmlTag), + Code(InternedStr), +} + +pub struct CodeBlock { + pub has_html: bool, + pub content: Vec, +} + +pub enum Tree { + HtmlText(InternedStr), + HtmlTag(HtmlTag), + CodeBlock(CodeBlock), +} + +impl Tree { + pub fn from_ast(value: &ast::Element, intern: &StrInterner) -> Self { + match value { + ast::Element::Text(text) => Tree::HtmlText(intern.intern_ref(text)), + ast::Element::Html(html) => Tree::HtmlTag(HtmlTag::from_ast(html, intern)), + ast::Element::Block(block) => Tree::CodeBlock(CodeBlock::from_ast(block, intern)), + } + } +} + +impl HtmlTag { + pub fn from_ast(value: &ast::HtmlTag, intern: &StrInterner) -> Self { + let tag = intern.intern_ref(&value.tag); + let attributes = value + .attributes + .iter() + .map(|attr| HtmlAttribute::from_ast(attr, intern)) + .collect(); + let content = value + .content + .iter() + .map(|elem| Tree::from_ast(elem, intern)) + .collect(); + HtmlTag { + tag, + attributes, + content, + } + } +} + +impl HtmlAttribute { + pub fn from_ast(value: &ast::Attribute, intern: &StrInterner) -> Self { + let ast::Element::Text(name) = &value.name else { + panic!("attribute name should be a string") + }; + let name = intern.intern_ref(name); + let value = value + .value + .as_ref() + .map(|v| Expression::from_ast(v, intern)); + HtmlAttribute { name, value } + } +} + +impl CodeBlock { + pub fn from_ast(value: &ast::CodeBlock, intern: &StrInterner) -> Self { + let mut has_html = false; + let content = value + .content + .iter() + .map(|elem| { + if matches!(elem, ast::Element::Html(_)) { + has_html = true + }; + CodeTree::from_ast(elem, intern) + }) + .collect(); + CodeBlock { content, has_html } + } +} + +impl Expression { + pub fn from_ast(value: &ast::Element, intern: &StrInterner) -> Self { + // TODO expression in parser + match value { + ast::Element::Text(text) => Expression::Literal(intern.intern_ref(text)), + ast::Element::Html(_html) => { + panic!("ast::Element::Html should not be used as attribute value") + } // this is the only case where expression is used so we can mention that in the panic message + ast::Element::Block(_block) => { + todo!("code blocks are not supported as attribute values until Expression is implemented in the parser") + } + } + } +} + +impl CodeTree { + pub fn from_ast(value: &ast::Element, intern: &StrInterner) -> Self { + match value { + ast::Element::Text(text) => CodeTree::Code(intern.intern_ref(text)), + ast::Element::Html(html) => CodeTree::HtmlTag(HtmlTag::from_ast(html, intern)), + ast::Element::Block(_) => { + panic!("nested code block detected (how the hell did the parser do this?)") + } + } + } +} diff --git a/crates/codegen/src/lib.rs b/crates/codegen/src/lib.rs index 141eb8d..3063ba9 100644 --- a/crates/codegen/src/lib.rs +++ b/crates/codegen/src/lib.rs @@ -1,5 +1,43 @@ extern crate cogs_ast as ast; +mod generate; +use generate::*; +mod ir; +use ir::*; + +// type aliases for: +// 1. convenience +// 2. easy swapping to InternedOrd if i ever find it better +type InternedStr = intern_arc::InternedHash; +type StrInterner = intern_arc::HashInterner; + pub fn generate(ast: &ast::Component) -> eyre::Result { - Ok(format!("not yet implemented")) + let mut generator = Generator { + trees: Vec::new(), + intern_str: StrInterner::new(), + }; + let mut elements = ast.elements.iter(); + for element in elements { + generator + .trees + .push(Tree::from_ast(element, &generator.intern_str)); + } + + let render = generator.to_format(); + Ok(format!( + r#" +pub struct Cog; + +impl cogs_runtime::Component for Cog {{ + type Props = (); + type Error = core::convert::Infallible; + fn render(&self, props: Self::Props) -> impl core::future::Future> + core::marker::Send + '_ {{ + async move {{ + {render} + Ok(__rendered) + }} + }} +}} +"# + )) } diff --git a/crates/parser/src/lib.rs b/crates/parser/src/lib.rs index 4506cf8..6053a1f 100644 --- a/crates/parser/src/lib.rs +++ b/crates/parser/src/lib.rs @@ -204,7 +204,7 @@ fn parse_inside_code_block(input: &str) -> IResult<&str, Vec> { debug!("Attempting inside code block {input}"); let (input, elems) = parse_consecutive_proper_elements(input)?; - dbg!(&elems); + debug!(?elems, "parsed inside code block"); Ok((input, elems)) } diff --git a/crates/runtime/Cargo.toml b/crates/runtime/Cargo.toml new file mode 100644 index 0000000..a53d4e4 --- /dev/null +++ b/crates/runtime/Cargo.toml @@ -0,0 +1,6 @@ +[package] +name = "cogs_runtime" +version = "0.1.0" +edition = "2021" + +[dependencies] diff --git a/crates/runtime/src/lib.rs b/crates/runtime/src/lib.rs new file mode 100644 index 0000000..0e713cc --- /dev/null +++ b/crates/runtime/src/lib.rs @@ -0,0 +1,43 @@ +use core::fmt; +use std::future::Future; + +pub trait Component { + type Props; + type Error; + + fn render<'a>(&'a self, props: Self::Props) -> impl Future> + Send + 'a; +} + +pub trait Render { + fn render(&self) -> String; +} + +impl Render for () { + fn render(&self) -> String { + String::new() + } +} + +impl Render for &str { + fn render(&self) -> String { + format!("{}", self) + } +} + +impl Render for String { + fn render(&self) -> String { + format!("{}", self) + } +} + +impl Render for &T { + fn render(&self) -> String { + (**self).render() + } +} + +impl Render for Box { + fn render(&self) -> String { + (**self).render() + } +} diff --git a/src/snapshots/cogs__tests__codegen@1.snap b/src/snapshots/cogs__tests__codegen@1.snap index 5e04bdb..4c7888c 100644 --- a/src/snapshots/cogs__tests__codegen@1.snap +++ b/src/snapshots/cogs__tests__codegen@1.snap @@ -3,4 +3,17 @@ source: src/tests.rs expression: "cogs_codegen::generate(&ast).unwrap()" snapshot_kind: text --- -not yet implemented +pub struct Cog; + +impl cogs_runtime::Component for Cog { + type Props = (); + type Error = core::convert::Infallible; + fn render(&self, props: Self::Props) -> impl core::future::Future> + core::marker::Send + '_ { + async move { + let x = 1;let __rendered = format!(r#"<{}><{}>{}<{} {}="{}">{}{}{}{}{}"#, r#"body"#,r#"h1"#,r#"Yo."#,r#"h1"#,r#"a"#,r#"src"#,r#"https://www.youtube.com/watch?v=dQw4w9WgXcQ"#,r#"Click this"#,x,r#"a"#,{println!("test"); +format!(r#"<{}>{}"#, r#"p"#,r#"More Html"#,r#"p"#) +},r#""#,r#"body"#,r#""#); + Ok(__rendered) + } + } +} diff --git a/src/snapshots/cogs__tests__cogs@1.snap b/src/snapshots/cogs__tests__cogs@1.snap index 70d72a1..06f18b5 100644 --- a/src/snapshots/cogs__tests__cogs@1.snap +++ b/src/snapshots/cogs__tests__cogs@1.snap @@ -5,6 +5,15 @@ snapshot_kind: text --- Component { elements: [ + Block( + CodeBlock { + content: [ + Text( + "let x = 1;\n", + ), + ], + }, + ), Html( HtmlTag { tag: "body", @@ -40,6 +49,15 @@ Component { Text( "Click this", ), + Block( + CodeBlock { + content: [ + Text( + "x", + ), + ], + }, + ), ], }, ), diff --git a/tests/1.cog b/tests/1.cog index aa6af23..10b414a 100644 --- a/tests/1.cog +++ b/tests/1.cog @@ -1,6 +1,9 @@ +{ + let x = 1; +}

Yo.

- Click this + Click this {x} { println!("test");

More Html