A human-friendly DSL for ATProto Lexicons
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

Rust, Typescript, and Go codegen

+1181 -51
+54
Cargo.lock
··· 541 541 checksum = "f4c7245a08504955605670dbf141fceab975f15ca21570696aebe9d2e71576bd" 542 542 543 543 [[package]] 544 + name = "inventory" 545 + version = "0.3.21" 546 + source = "registry+https://github.com/rust-lang/crates.io-index" 547 + checksum = "bc61209c082fbeb19919bee74b176221b27223e27b65d781eb91af24eb1fb46e" 548 + dependencies = [ 549 + "rustversion", 550 + ] 551 + 552 + [[package]] 544 553 name = "is_ci" 545 554 version = "1.2.0" 546 555 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 683 692 name = "mlf-codegen" 684 693 version = "0.1.0" 685 694 dependencies = [ 695 + "inventory", 696 + "mlf-codegen-go", 697 + "mlf-codegen-rust", 698 + "mlf-codegen-typescript", 686 699 "mlf-lang", 687 700 "serde_json", 688 701 ] 689 702 690 703 [[package]] 704 + name = "mlf-codegen-go" 705 + version = "0.1.0" 706 + dependencies = [ 707 + "mlf-codegen", 708 + "mlf-lang", 709 + ] 710 + 711 + [[package]] 712 + name = "mlf-codegen-java" 713 + version = "0.1.0" 714 + 715 + [[package]] 716 + name = "mlf-codegen-rust" 717 + version = "0.1.0" 718 + dependencies = [ 719 + "mlf-codegen", 720 + "mlf-lang", 721 + ] 722 + 723 + [[package]] 724 + name = "mlf-codegen-typescript" 725 + version = "0.1.0" 726 + dependencies = [ 727 + "mlf-codegen", 728 + "mlf-lang", 729 + ] 730 + 731 + [[package]] 691 732 name = "mlf-diagnostics" 692 733 version = "0.1.0" 693 734 dependencies = [ ··· 704 745 ] 705 746 706 747 [[package]] 748 + name = "mlf-playground-wasm" 749 + version = "0.1.0" 750 + dependencies = [ 751 + "mlf-codegen", 752 + "mlf-codegen-go", 753 + "mlf-codegen-rust", 754 + "mlf-codegen-typescript", 755 + "mlf-wasm", 756 + "wasm-bindgen", 757 + ] 758 + 759 + [[package]] 707 760 name = "mlf-validation" 708 761 version = "0.1.0" 709 762 dependencies = [ ··· 728 781 "serde_json", 729 782 "wasm-bindgen", 730 783 "wasm-bindgen-test", 784 + "web-sys", 731 785 ] 732 786 733 787 [[package]]
+3 -3
Cargo.toml
··· 1 1 [workspace] 2 2 resolver = "3" 3 - members = [ 3 + members = ["codegen-plugins/mlf-codegen-go", "codegen-plugins/mlf-codegen-java", "codegen-plugins/mlf-codegen-rust","codegen-plugins/mlf-codegen-typescript", 4 4 "mlf-cli", "mlf-codegen", "mlf-diagnostics", 5 5 "mlf-lang", 6 6 "mlf-validation", "mlf-wasm", 7 - "tree-sitter-mlf" 8 - ] 7 + "tree-sitter-mlf", 8 + "website/mlf-playground-wasm"] 9 9 10 10 default-members = [ 11 11 "mlf-cli"
+10
codegen-plugins/mlf-codegen-go/Cargo.toml
··· 1 + [package] 2 + name = "mlf-codegen-go" 3 + version = "0.1.0" 4 + edition = "2024" 5 + license = "MIT" 6 + description = "Go code generator plugin for MLF" 7 + 8 + [dependencies] 9 + mlf-lang = { path = "../../mlf-lang" } 10 + mlf-codegen = { path = "../../mlf-codegen" }
+217
codegen-plugins/mlf-codegen-go/src/lib.rs
··· 1 + use mlf_codegen::{register_generator, CodeGenerator, GeneratorContext}; 2 + use mlf_lang::ast::*; 3 + use std::fmt::Write; 4 + 5 + pub struct GoGenerator; 6 + 7 + impl GoGenerator { 8 + pub const NAME: &'static str = "go"; 9 + 10 + fn to_snake_case(&self, s: &str) -> String { 11 + // Simple camelCase to snake_case conversion 12 + let mut result = String::new(); 13 + for (i, ch) in s.chars().enumerate() { 14 + if ch.is_uppercase() && i > 0 { 15 + result.push('_'); 16 + } 17 + result.push(ch.to_lowercase().next().unwrap()); 18 + } 19 + result 20 + } 21 + 22 + fn generate_type(&self, ty: &Type, optional: bool, ctx: &GeneratorContext) -> Result<String, String> { 23 + let base_type = match ty { 24 + Type::Primitive { kind, .. } => match kind { 25 + PrimitiveType::Null => "interface{}", 26 + PrimitiveType::Boolean => "bool", 27 + PrimitiveType::Integer => "int64", 28 + PrimitiveType::String => "string", 29 + PrimitiveType::Bytes => "[]byte", 30 + PrimitiveType::Blob => "[]byte", // Annotation idea: @goType("custom.BlobType") 31 + }.to_string(), 32 + Type::Reference { path, .. } => { 33 + let path_str = path.to_string(); 34 + match path_str.as_str() { 35 + // Map standard library types 36 + "Datetime" => "string".to_string(), // ISO 8601 string 37 + "Did" | "AtUri" | "Cid" | "AtIdentifier" | "Handle" | "Nsid" | "Tid" | "RecordKey" | "Uri" | "Language" => { 38 + "string".to_string() 39 + } 40 + _ => { 41 + // Local reference 42 + path.segments.last().unwrap().name.clone() 43 + } 44 + } 45 + } 46 + Type::Array { inner, .. } => { 47 + let inner_type = self.generate_type(inner, false, ctx)?; 48 + format!("[]{}", inner_type) 49 + } 50 + Type::Union { .. } => { 51 + // Go doesn't have union types, use interface{} 52 + // Annotation idea: @goUnion to generate type switch helpers 53 + "interface{}".to_string() 54 + } 55 + Type::Object { fields, .. } => { 56 + let mut obj = String::from("struct {\n"); 57 + for field in fields { 58 + let field_name = self.capitalize(&field.name.name); 59 + let field_type = self.generate_type(&field.ty, field.optional, ctx)?; 60 + let json_name = field.name.name.clone(); 61 + 62 + if !field.docs.is_empty() { 63 + write!(obj, "\t\t// {}\n", field.docs[0].text).unwrap(); 64 + } 65 + write!(obj, "\t\t{} {} `json:\"{}", field_name, field_type, json_name).unwrap(); 66 + if field.optional { 67 + write!(obj, ",omitempty").unwrap(); 68 + } 69 + writeln!(obj, "\"`").unwrap(); 70 + } 71 + obj.push_str("\t}"); 72 + obj 73 + } 74 + Type::Parenthesized { inner, .. } => { 75 + return self.generate_type(inner, optional, ctx); 76 + } 77 + Type::Constrained { base, .. } => { 78 + return self.generate_type(base, optional, ctx); 79 + } 80 + Type::Unknown { .. } => "interface{}".to_string(), 81 + }; 82 + 83 + // For optional fields, use pointer types 84 + if optional { 85 + Ok(format!("*{}", base_type)) 86 + } else { 87 + Ok(base_type) 88 + } 89 + } 90 + 91 + fn capitalize(&self, s: &str) -> String { 92 + let mut chars = s.chars(); 93 + match chars.next() { 94 + None => String::new(), 95 + Some(f) => f.to_uppercase().collect::<String>() + chars.as_str(), 96 + } 97 + } 98 + 99 + fn generate_doc_comment(&self, docs: &[DocComment]) -> String { 100 + if docs.is_empty() { 101 + return String::new(); 102 + } 103 + 104 + let mut result = String::new(); 105 + for doc in docs { 106 + result.push_str("// "); 107 + result.push_str(&doc.text); 108 + result.push('\n'); 109 + } 110 + result 111 + } 112 + } 113 + 114 + impl CodeGenerator for GoGenerator { 115 + fn name(&self) -> &'static str { 116 + Self::NAME 117 + } 118 + 119 + fn description(&self) -> &'static str { 120 + "Generate Go structs and client code" 121 + } 122 + 123 + fn file_extension(&self) -> &'static str { 124 + ".go" 125 + } 126 + 127 + fn generate(&self, ctx: &GeneratorContext) -> Result<String, String> { 128 + let mut output = String::new(); 129 + 130 + // Header comment 131 + writeln!(output, "// Generated from {}", ctx.namespace).unwrap(); 132 + writeln!(output, "// Do not edit manually").unwrap(); 133 + writeln!(output).unwrap(); 134 + 135 + // Package name (use last segment of namespace) 136 + let package_name = ctx.namespace.split('.').last().unwrap_or("lexicon"); 137 + writeln!(output, "package {}\n", package_name).unwrap(); 138 + 139 + // Generate code for each item 140 + for item in &ctx.lexicon.items { 141 + match item { 142 + Item::Record(record) => { 143 + output.push_str(&self.generate_doc_comment(&record.docs)); 144 + writeln!(output, "type {} struct {{", self.capitalize(&record.name.name)).unwrap(); 145 + 146 + for field in &record.fields { 147 + let field_name = self.capitalize(&field.name.name); 148 + let field_type = self.generate_type(&field.ty, field.optional, ctx)?; 149 + let json_name = &field.name.name; 150 + 151 + if !field.docs.is_empty() { 152 + writeln!(output, "\t// {}", field.docs[0].text).unwrap(); 153 + } 154 + write!(output, "\t{} {} `json:\"{}\"", field_name, field_type, json_name).unwrap(); 155 + if field.optional { 156 + write!(output, ",omitempty").unwrap(); 157 + } 158 + writeln!(output, "`").unwrap(); 159 + } 160 + 161 + writeln!(output, "}}\n").unwrap(); 162 + } 163 + Item::DefType(def) => { 164 + output.push_str(&self.generate_doc_comment(&def.docs)); 165 + let type_name = self.capitalize(&def.name.name); 166 + 167 + match &def.ty { 168 + Type::Object { .. } => { 169 + // Object types become structs 170 + writeln!( 171 + output, 172 + "type {} {}\n", 173 + type_name, 174 + self.generate_type(&def.ty, false, ctx)? 175 + ).unwrap(); 176 + } 177 + _ => { 178 + // Other types become type aliases 179 + writeln!( 180 + output, 181 + "type {} {}\n", 182 + type_name, 183 + self.generate_type(&def.ty, false, ctx)? 184 + ).unwrap(); 185 + } 186 + } 187 + } 188 + Item::InlineType(inline) => { 189 + output.push_str(&self.generate_doc_comment(&inline.docs)); 190 + writeln!( 191 + output, 192 + "type {} {}\n", 193 + self.capitalize(&inline.name.name), 194 + self.generate_type(&inline.ty, false, ctx)? 195 + ).unwrap(); 196 + } 197 + Item::Token(token) => { 198 + output.push_str(&self.generate_doc_comment(&token.docs)); 199 + let const_name = token.name.name.to_uppercase(); 200 + writeln!(output, "const {} = \"{}\"\n", const_name, token.name.name).unwrap(); 201 + } 202 + Item::Query(_) | Item::Procedure(_) | Item::Subscription(_) => { 203 + // TODO: Generate client methods 204 + } 205 + Item::Use(_) => { 206 + // Skip use statements 207 + } 208 + } 209 + } 210 + 211 + Ok(output) 212 + } 213 + } 214 + 215 + // Register the Go generator 216 + pub static GO_GENERATOR: GoGenerator = GoGenerator; 217 + register_generator!(GO_GENERATOR);
+6
codegen-plugins/mlf-codegen-java/Cargo.toml
··· 1 + [package] 2 + name = "mlf-codegen-java" 3 + version = "0.1.0" 4 + edition = "2024" 5 + 6 + [dependencies]
+14
codegen-plugins/mlf-codegen-java/src/lib.rs
··· 1 + pub fn add(left: u64, right: u64) -> u64 { 2 + left + right 3 + } 4 + 5 + #[cfg(test)] 6 + mod tests { 7 + use super::*; 8 + 9 + #[test] 10 + fn it_works() { 11 + let result = add(2, 2); 12 + assert_eq!(result, 4); 13 + } 14 + }
+10
codegen-plugins/mlf-codegen-rust/Cargo.toml
··· 1 + [package] 2 + name = "mlf-codegen-rust" 3 + version = "0.1.0" 4 + edition = "2024" 5 + license = "MIT" 6 + description = "Rust code generator plugin for MLF" 7 + 8 + [dependencies] 9 + mlf-lang = { path = "../../mlf-lang" } 10 + mlf-codegen = { path = "../../mlf-codegen" }
+231
codegen-plugins/mlf-codegen-rust/src/lib.rs
··· 1 + use mlf_codegen::{register_generator, CodeGenerator, GeneratorContext}; 2 + use mlf_lang::ast::*; 3 + use std::fmt::Write; 4 + 5 + pub struct RustGenerator; 6 + 7 + impl RustGenerator { 8 + pub const NAME: &'static str = "rust"; 9 + 10 + fn to_snake_case(&self, s: &str) -> String { 11 + let mut result = String::new(); 12 + for (i, ch) in s.chars().enumerate() { 13 + if ch.is_uppercase() && i > 0 { 14 + result.push('_'); 15 + } 16 + result.push(ch.to_lowercase().next().unwrap()); 17 + } 18 + result 19 + } 20 + 21 + fn to_pascal_case(&self, s: &str) -> String { 22 + let mut chars = s.chars(); 23 + match chars.next() { 24 + None => String::new(), 25 + Some(f) => f.to_uppercase().collect::<String>() + chars.as_str(), 26 + } 27 + } 28 + 29 + fn generate_type(&self, ty: &Type, optional: bool, ctx: &GeneratorContext) -> Result<String, String> { 30 + let base_type = match ty { 31 + Type::Primitive { kind, .. } => match kind { 32 + PrimitiveType::Null => "()".to_string(), // Unit type for null 33 + PrimitiveType::Boolean => "bool".to_string(), 34 + PrimitiveType::Integer => "i64".to_string(), 35 + PrimitiveType::String => "String".to_string(), 36 + PrimitiveType::Bytes => "Vec<u8>".to_string(), 37 + PrimitiveType::Blob => "Vec<u8>".to_string(), // Annotation idea: @rustType("custom::BlobType") 38 + }, 39 + Type::Reference { path, .. } => { 40 + let path_str = path.to_string(); 41 + match path_str.as_str() { 42 + // Map standard library types 43 + "Datetime" => "String".to_string(), // ISO 8601 string, could use chrono::DateTime 44 + "Did" | "AtUri" | "Cid" | "AtIdentifier" | "Handle" | "Nsid" | "Tid" | "RecordKey" | "Uri" | "Language" => { 45 + "String".to_string() 46 + } 47 + _ => { 48 + // Local reference - convert to PascalCase 49 + self.to_pascal_case(&path.segments.last().unwrap().name) 50 + } 51 + } 52 + } 53 + Type::Array { inner, .. } => { 54 + let inner_type = self.generate_type(inner, false, ctx)?; 55 + format!("Vec<{}>", inner_type) 56 + } 57 + Type::Union { types, .. } => { 58 + // Rust doesn't have direct union types, use an enum 59 + // For now, generate a simple representation 60 + // Annotation idea: @rustEnum to customize enum generation 61 + if types.len() == 2 && matches!(types[0], Type::Primitive { kind: PrimitiveType::Null, .. }) { 62 + // Special case: null | T becomes Option<T> 63 + return self.generate_type(&types[1], true, ctx); 64 + } else if types.len() == 2 && matches!(types[1], Type::Primitive { kind: PrimitiveType::Null, .. }) { 65 + return self.generate_type(&types[0], true, ctx); 66 + } 67 + // Otherwise use serde_json::Value for flexibility 68 + "serde_json::Value".to_string() 69 + } 70 + Type::Object { fields, .. } => { 71 + // Inline struct types aren't idiomatic in Rust 72 + // We'd need to generate a named type 73 + // For now, use serde_json::Value 74 + // Annotation idea: @rustInlineStruct to force inline generation 75 + "serde_json::Value".to_string() 76 + } 77 + Type::Parenthesized { inner, .. } => { 78 + return self.generate_type(inner, optional, ctx); 79 + } 80 + Type::Constrained { base, .. } => { 81 + return self.generate_type(base, optional, ctx); 82 + } 83 + Type::Unknown { .. } => "serde_json::Value".to_string(), 84 + }; 85 + 86 + if optional { 87 + Ok(format!("Option<{}>", base_type)) 88 + } else { 89 + Ok(base_type) 90 + } 91 + } 92 + 93 + fn generate_doc_comment(&self, docs: &[DocComment]) -> String { 94 + if docs.is_empty() { 95 + return String::new(); 96 + } 97 + 98 + let mut result = String::new(); 99 + for doc in docs { 100 + result.push_str("/// "); 101 + result.push_str(&doc.text); 102 + result.push('\n'); 103 + } 104 + result 105 + } 106 + } 107 + 108 + impl CodeGenerator for RustGenerator { 109 + fn name(&self) -> &'static str { 110 + Self::NAME 111 + } 112 + 113 + fn description(&self) -> &'static str { 114 + "Generate Rust structs and client code" 115 + } 116 + 117 + fn file_extension(&self) -> &'static str { 118 + ".rs" 119 + } 120 + 121 + fn generate(&self, ctx: &GeneratorContext) -> Result<String, String> { 122 + let mut output = String::new(); 123 + 124 + // Header comment 125 + writeln!(output, "// Generated from {}", ctx.namespace).unwrap(); 126 + writeln!(output, "// Do not edit manually").unwrap(); 127 + writeln!(output).unwrap(); 128 + 129 + // Common imports 130 + writeln!(output, "use serde::{{Deserialize, Serialize}};\n").unwrap(); 131 + 132 + // Generate code for each item 133 + for item in &ctx.lexicon.items { 134 + match item { 135 + Item::Record(record) => { 136 + output.push_str(&self.generate_doc_comment(&record.docs)); 137 + writeln!(output, "#[derive(Debug, Clone, Serialize, Deserialize)]").unwrap(); 138 + writeln!(output, "pub struct {} {{", self.to_pascal_case(&record.name.name)).unwrap(); 139 + 140 + for field in &record.fields { 141 + if !field.docs.is_empty() { 142 + writeln!(output, " /// {}", field.docs[0].text).unwrap(); 143 + } 144 + 145 + // Use serde rename for camelCase fields 146 + if field.name.name != self.to_snake_case(&field.name.name) { 147 + writeln!(output, " #[serde(rename = \"{}\")]", field.name.name).unwrap(); 148 + } 149 + 150 + // Skip serializing None values for optional fields 151 + if field.optional { 152 + writeln!(output, " #[serde(skip_serializing_if = \"Option::is_none\")]").unwrap(); 153 + } 154 + 155 + let field_type = self.generate_type(&field.ty, field.optional, ctx)?; 156 + writeln!(output, " pub {}: {},", self.to_snake_case(&field.name.name), field_type).unwrap(); 157 + } 158 + 159 + writeln!(output, "}}\n").unwrap(); 160 + } 161 + Item::DefType(def) => { 162 + output.push_str(&self.generate_doc_comment(&def.docs)); 163 + 164 + match &def.ty { 165 + Type::Object { fields, .. } => { 166 + // Generate a struct for object types 167 + writeln!(output, "#[derive(Debug, Clone, Serialize, Deserialize)]").unwrap(); 168 + writeln!(output, "pub struct {} {{", self.to_pascal_case(&def.name.name)).unwrap(); 169 + 170 + for field in fields { 171 + if !field.docs.is_empty() { 172 + writeln!(output, " /// {}", field.docs[0].text).unwrap(); 173 + } 174 + 175 + if field.name.name != self.to_snake_case(&field.name.name) { 176 + writeln!(output, " #[serde(rename = \"{}\")]", field.name.name).unwrap(); 177 + } 178 + 179 + if field.optional { 180 + writeln!(output, " #[serde(skip_serializing_if = \"Option::is_none\")]").unwrap(); 181 + } 182 + 183 + let field_type = self.generate_type(&field.ty, field.optional, ctx)?; 184 + writeln!(output, " pub {}: {},", self.to_snake_case(&field.name.name), field_type).unwrap(); 185 + } 186 + 187 + writeln!(output, "}}\n").unwrap(); 188 + } 189 + _ => { 190 + // Type alias 191 + writeln!( 192 + output, 193 + "pub type {} = {};\n", 194 + self.to_pascal_case(&def.name.name), 195 + self.generate_type(&def.ty, false, ctx)? 196 + ).unwrap(); 197 + } 198 + } 199 + } 200 + Item::InlineType(inline) => { 201 + output.push_str(&self.generate_doc_comment(&inline.docs)); 202 + writeln!( 203 + output, 204 + "pub type {} = {};\n", 205 + self.to_pascal_case(&inline.name.name), 206 + self.generate_type(&inline.ty, false, ctx)? 207 + ).unwrap(); 208 + } 209 + Item::Token(token) => { 210 + output.push_str(&self.generate_doc_comment(&token.docs)); 211 + writeln!(output, "pub const {}: &str = \"{}\";\n", 212 + token.name.name.to_uppercase(), 213 + token.name.name 214 + ).unwrap(); 215 + } 216 + Item::Query(_) | Item::Procedure(_) | Item::Subscription(_) => { 217 + // TODO: Generate client methods 218 + } 219 + Item::Use(_) => { 220 + // Skip use statements 221 + } 222 + } 223 + } 224 + 225 + Ok(output) 226 + } 227 + } 228 + 229 + // Register the Rust generator 230 + pub static RUST_GENERATOR: RustGenerator = RustGenerator; 231 + register_generator!(RUST_GENERATOR);
+10
codegen-plugins/mlf-codegen-typescript/Cargo.toml
··· 1 + [package] 2 + name = "mlf-codegen-typescript" 3 + version = "0.1.0" 4 + edition = "2024" 5 + license = "MIT" 6 + description = "TypeScript code generator plugin for MLF" 7 + 8 + [dependencies] 9 + mlf-lang = { path = "../../mlf-lang" } 10 + mlf-codegen = { path = "../../mlf-codegen" }
+177
codegen-plugins/mlf-codegen-typescript/src/lib.rs
··· 1 + use mlf_codegen::{register_generator, CodeGenerator, GeneratorContext}; 2 + use mlf_lang::ast::*; 3 + use std::fmt::Write; 4 + 5 + pub struct TypeScriptGenerator; 6 + 7 + impl TypeScriptGenerator { 8 + pub const NAME: &'static str = "typescript"; 9 + 10 + fn generate_type(&self, ty: &Type, ctx: &GeneratorContext) -> Result<String, String> { 11 + match ty { 12 + Type::Primitive { kind, .. } => Ok(match kind { 13 + PrimitiveType::Null => "null".to_string(), 14 + PrimitiveType::Boolean => "boolean".to_string(), 15 + PrimitiveType::Integer => "number".to_string(), 16 + PrimitiveType::String => "string".to_string(), 17 + PrimitiveType::Bytes => "Uint8Array".to_string(), 18 + PrimitiveType::Blob => "Blob".to_string(), // Annotation idea: @tsType("CustomBlobType") 19 + }), 20 + Type::Reference { path, .. } => { 21 + // Check if it's from the standard library 22 + let path_str = path.to_string(); 23 + 24 + // Map standard library types to TypeScript types 25 + Ok(match path_str.as_str() { 26 + "Datetime" => "string".to_string(), // ISO 8601 27 + "Did" | "AtUri" | "Cid" | "AtIdentifier" | "Handle" | "Nsid" | "Tid" | "RecordKey" | "Uri" | "Language" => { 28 + "string".to_string() 29 + } 30 + _ => { 31 + // Local or cross-file reference 32 + path.segments.last().unwrap().name.clone() 33 + } 34 + }) 35 + } 36 + Type::Array { inner, .. } => { 37 + let inner_type = self.generate_type(inner, ctx)?; 38 + Ok(format!("{}[]", inner_type)) 39 + } 40 + Type::Union { types, .. } => { 41 + let type_strings: Result<Vec<_>, _> = types 42 + .iter() 43 + .map(|t| self.generate_type(t, ctx)) 44 + .collect(); 45 + Ok(type_strings?.join(" | ")) 46 + } 47 + Type::Object { fields, .. } => { 48 + let mut obj = String::from("{\n"); 49 + for field in fields { 50 + if !field.docs.is_empty() { 51 + obj.push_str(" /** "); 52 + obj.push_str(&field.docs[0].text); 53 + obj.push_str(" */\n"); 54 + } 55 + obj.push_str(" "); 56 + obj.push_str(&field.name.name); 57 + if field.optional { 58 + obj.push('?'); 59 + } 60 + obj.push_str(": "); 61 + obj.push_str(&self.generate_type(&field.ty, ctx)?); 62 + obj.push_str(";\n"); 63 + } 64 + obj.push('}'); 65 + Ok(obj) 66 + } 67 + Type::Parenthesized { inner, .. } => { 68 + let inner_type = self.generate_type(inner, ctx)?; 69 + Ok(format!("({})", inner_type)) 70 + } 71 + Type::Constrained { base, .. } => { 72 + // For constrained types, just use the base type 73 + // Annotations like @min, @max could be added for runtime validation libraries 74 + self.generate_type(base, ctx) 75 + } 76 + Type::Unknown { .. } => Ok("unknown".to_string()), 77 + } 78 + } 79 + 80 + fn generate_doc_comment(&self, docs: &[DocComment]) -> String { 81 + if docs.is_empty() { 82 + return String::new(); 83 + } 84 + 85 + let mut result = String::from("/**\n"); 86 + for doc in docs { 87 + result.push_str(" * "); 88 + result.push_str(&doc.text); 89 + result.push('\n'); 90 + } 91 + result.push_str(" */\n"); 92 + result 93 + } 94 + } 95 + 96 + impl CodeGenerator for TypeScriptGenerator { 97 + fn name(&self) -> &'static str { 98 + Self::NAME 99 + } 100 + 101 + fn description(&self) -> &'static str { 102 + "Generate TypeScript type definitions and client code" 103 + } 104 + 105 + fn file_extension(&self) -> &'static str { 106 + ".ts" 107 + } 108 + 109 + fn generate(&self, ctx: &GeneratorContext) -> Result<String, String> { 110 + let mut output = String::new(); 111 + 112 + // Header comment 113 + writeln!(output, "/**").unwrap(); 114 + writeln!(output, " * Generated from {}", ctx.namespace).unwrap(); 115 + writeln!(output, " * Do not edit manually").unwrap(); 116 + writeln!(output, " */").unwrap(); 117 + writeln!(output).unwrap(); 118 + 119 + // Generate code for each item 120 + for item in &ctx.lexicon.items { 121 + match item { 122 + Item::Record(record) => { 123 + output.push_str(&self.generate_doc_comment(&record.docs)); 124 + writeln!(output, "export interface {} {{", record.name.name).unwrap(); 125 + 126 + for field in &record.fields { 127 + if !field.docs.is_empty() { 128 + write!(output, " /** {} */\n", field.docs[0].text).unwrap(); 129 + } 130 + write!(output, " {}", field.name.name).unwrap(); 131 + if field.optional { 132 + write!(output, "?").unwrap(); 133 + } 134 + writeln!(output, ": {};", self.generate_type(&field.ty, ctx)?).unwrap(); 135 + } 136 + 137 + writeln!(output, "}}\n").unwrap(); 138 + } 139 + Item::DefType(def) => { 140 + output.push_str(&self.generate_doc_comment(&def.docs)); 141 + writeln!( 142 + output, 143 + "export type {} = {};\n", 144 + def.name.name, 145 + self.generate_type(&def.ty, ctx)? 146 + ).unwrap(); 147 + } 148 + Item::InlineType(inline) => { 149 + output.push_str(&self.generate_doc_comment(&inline.docs)); 150 + writeln!( 151 + output, 152 + "export type {} = {};\n", 153 + inline.name.name, 154 + self.generate_type(&inline.ty, ctx)? 155 + ).unwrap(); 156 + } 157 + Item::Token(token) => { 158 + output.push_str(&self.generate_doc_comment(&token.docs)); 159 + writeln!(output, "export const {} = Symbol('{}');\n", token.name.name, token.name.name).unwrap(); 160 + } 161 + Item::Query(_) | Item::Procedure(_) | Item::Subscription(_) => { 162 + // TODO: Generate client methods for these 163 + // Annotation idea: @clientMethod for custom generation 164 + } 165 + Item::Use(_) => { 166 + // Skip use statements in output 167 + } 168 + } 169 + } 170 + 171 + Ok(output) 172 + } 173 + } 174 + 175 + // Register the TypeScript generator 176 + pub static TYPESCRIPT_GENERATOR: TypeScriptGenerator = TypeScriptGenerator; 177 + register_generator!(TYPESCRIPT_GENERATOR);
+6
mlf-codegen/Cargo.toml
··· 7 7 [dependencies] 8 8 mlf-lang = { path = "../mlf-lang" } 9 9 serde_json = { version = "1", features = ["preserve_order"] } 10 + inventory = "0.3" 11 + 12 + [dev-dependencies] 13 + mlf-codegen-typescript = { path = "../codegen-plugins/mlf-codegen-typescript" } 14 + mlf-codegen-go = { path = "../codegen-plugins/mlf-codegen-go" } 15 + mlf-codegen-rust = { path = "../codegen-plugins/mlf-codegen-rust" }
+26
mlf-codegen/examples/all_generators.rs
··· 1 + // This example loads all codegen plugins and lists them 2 + // 3 + // Run with: cargo run --example all_generators -p mlf-codegen 4 + 5 + use mlf_codegen::plugin; 6 + 7 + // Import all the plugin crates to trigger their registration 8 + extern crate mlf_codegen_typescript; 9 + extern crate mlf_codegen_go; 10 + extern crate mlf_codegen_rust; 11 + 12 + fn main() { 13 + println!("MLF Code Generator Plugins (All Loaded)\n"); 14 + println!("========================================\n"); 15 + 16 + let generators = plugin::generators(); 17 + println!("Found {} generator(s):\n", generators.len()); 18 + 19 + for generator in generators { 20 + println!(" {} ({}):", 21 + generator.name(), 22 + generator.file_extension() 23 + ); 24 + println!(" {}\n", generator.description()); 25 + } 26 + }
+31
mlf-codegen/examples/list_generators.rs
··· 1 + // This example demonstrates how to use MLF codegen plugins 2 + // 3 + // To run with all plugins loaded: 4 + // cargo run --example list_generators --features="typescript,go,rust" 5 + 6 + use mlf_codegen::plugin; 7 + 8 + fn main() { 9 + println!("MLF Code Generator Plugins\n"); 10 + println!("===========================\n"); 11 + 12 + let generators = plugin::generators(); 13 + 14 + if generators.is_empty() { 15 + println!("No generators registered!"); 16 + println!("\nTo use plugins, depend on them in your Cargo.toml:"); 17 + println!(" mlf-codegen-typescript = {{ path = \"../codegen-plugins/mlf-codegen-typescript\" }}"); 18 + println!(" mlf-codegen-go = {{ path = \"../codegen-plugins/mlf-codegen-go\" }}"); 19 + println!(" mlf-codegen-rust = {{ path = \"../codegen-plugins/mlf-codegen-rust\" }}"); 20 + } else { 21 + println!("Found {} generator(s):\n", generators.len()); 22 + 23 + for generator in generators { 24 + println!(" {} ({}):", 25 + generator.name(), 26 + generator.file_extension() 27 + ); 28 + println!(" {}\n", generator.description()); 29 + } 30 + } 31 + }
+23
mlf-codegen/examples/plugin_test.rs
··· 1 + use mlf_codegen::plugin; 2 + 3 + fn main() { 4 + println!("Testing MLF Plugin System\n"); 5 + 6 + // List all registered generators 7 + println!("Registered generators:"); 8 + for generator in plugin::generators() { 9 + println!(" - {} ({}): {}", 10 + generator.name(), 11 + generator.file_extension(), 12 + generator.description() 13 + ); 14 + } 15 + 16 + // Try to find the JSON generator 17 + println!("\nLooking for 'json' generator..."); 18 + if let Some(generator) = plugin::find_generator("json") { 19 + println!("Found: {} - {}", generator.name(), generator.description()); 20 + } else { 21 + println!("Not found!"); 22 + } 23 + }
+92
mlf-codegen/src/lib.rs
··· 3 3 use serde_json::{json, Map, Value}; 4 4 use std::collections::HashMap; 5 5 6 + // Re-export inventory for macros 7 + #[doc(hidden)] 8 + pub use inventory; 9 + 10 + // Plugin system for code generators 11 + pub mod plugin { 12 + use super::*; 13 + 14 + /// Context passed to code generators 15 + pub struct GeneratorContext<'a> { 16 + pub namespace: &'a str, 17 + pub lexicon: &'a Lexicon, 18 + pub workspace: &'a Workspace, 19 + } 20 + 21 + /// Trait for code generators 22 + pub trait CodeGenerator: Send + Sync { 23 + /// Unique identifier for this generator (e.g., "typescript", "python", "rust") 24 + fn name(&self) -> &'static str; 25 + 26 + /// Human-readable description 27 + fn description(&self) -> &'static str; 28 + 29 + /// File extension for generated files (e.g., ".ts", ".py", ".rs") 30 + fn file_extension(&self) -> &'static str; 31 + 32 + /// Generate code from a lexicon 33 + fn generate(&self, ctx: &GeneratorContext) -> Result<String, String>; 34 + } 35 + 36 + // Registry of code generators using inventory 37 + inventory::collect!(&'static dyn CodeGenerator); 38 + 39 + /// Get all registered generators as a Vec 40 + pub fn generators() -> Vec<&'static dyn CodeGenerator> { 41 + let mut result = Vec::new(); 42 + for generator in inventory::iter::<&'static dyn CodeGenerator> { 43 + result.push(*generator); 44 + } 45 + result 46 + } 47 + 48 + /// Find a generator by name 49 + pub fn find_generator(name: &str) -> Option<&'static dyn CodeGenerator> { 50 + for generator in inventory::iter::<&'static dyn CodeGenerator> { 51 + if generator.name() == name { 52 + return Some(*generator); 53 + } 54 + } 55 + None 56 + } 57 + 58 + /// Macro to easily register a generator 59 + #[macro_export] 60 + macro_rules! register_generator { 61 + ($generator:expr) => { 62 + $crate::inventory::submit! { 63 + &$generator as &'static dyn $crate::plugin::CodeGenerator 64 + } 65 + }; 66 + } 67 + } 68 + 69 + pub use plugin::{CodeGenerator, GeneratorContext}; 70 + 6 71 pub fn generate_lexicon(namespace: &str, lexicon: &Lexicon, workspace: &Workspace) -> Value { 7 72 let usage_counts = analyze_type_usage(lexicon); 8 73 ··· 563 628 } 564 629 } 565 630 } 631 + 632 + // Example built-in generator: JSON Lexicon 633 + pub struct JsonLexiconGenerator; 634 + 635 + impl CodeGenerator for JsonLexiconGenerator { 636 + fn name(&self) -> &'static str { 637 + "json" 638 + } 639 + 640 + fn description(&self) -> &'static str { 641 + "Generate AT Protocol JSON lexicon format" 642 + } 643 + 644 + fn file_extension(&self) -> &'static str { 645 + ".json" 646 + } 647 + 648 + fn generate(&self, ctx: &GeneratorContext) -> Result<String, String> { 649 + let json = generate_lexicon(ctx.namespace, ctx.lexicon, ctx.workspace); 650 + serde_json::to_string_pretty(&json) 651 + .map_err(|e| format!("Failed to serialize JSON: {}", e)) 652 + } 653 + } 654 + 655 + // Register the JSON generator as a static instance 656 + static JSON_GENERATOR: JsonLexiconGenerator = JsonLexiconGenerator; 657 + register_generator!(JSON_GENERATOR);
+1
mlf-wasm/Cargo.toml
··· 15 15 serde = { version = "1", features = ["derive"] } 16 16 serde_json = "1" 17 17 serde-wasm-bindgen = "0.6" 18 + web-sys = { version = "0.3", features = ["console"] } 18 19 19 20 [dev-dependencies] 20 21 wasm-bindgen-test = "0.3"
+115
mlf-wasm/src/lib.rs
··· 216 216 } 217 217 } 218 218 219 + #[derive(Serialize, Deserialize)] 220 + pub struct GeneratorInfo { 221 + pub name: String, 222 + pub description: String, 223 + pub file_extension: String, 224 + } 225 + 226 + #[derive(Serialize, Deserialize)] 227 + pub struct ListGeneratorsResult { 228 + pub generators: Vec<GeneratorInfo>, 229 + } 230 + 231 + #[derive(Serialize, Deserialize)] 232 + pub struct GenerateCodeResult { 233 + pub success: bool, 234 + pub code: Option<String>, 235 + pub error: Option<String>, 236 + } 237 + 238 + /// List all available code generators 239 + #[wasm_bindgen] 240 + pub fn list_generators() -> JsValue { 241 + let generators = mlf_codegen::plugin::generators(); 242 + 243 + // Debug: log how many generators we found 244 + #[cfg(target_arch = "wasm32")] 245 + web_sys::console::log_1(&format!("Found {} generators", generators.len()).into()); 246 + 247 + let generator_infos: Vec<GeneratorInfo> = generators.iter().map(|generator| { 248 + #[cfg(target_arch = "wasm32")] 249 + web_sys::console::log_1(&format!(" - {}", generator.name()).into()); 250 + 251 + GeneratorInfo { 252 + name: generator.name().to_string(), 253 + description: generator.description().to_string(), 254 + file_extension: generator.file_extension().to_string(), 255 + } 256 + }).collect(); 257 + 258 + let result = ListGeneratorsResult { 259 + generators: generator_infos, 260 + }; 261 + 262 + serde_wasm_bindgen::to_value(&result).unwrap() 263 + } 264 + 265 + /// Generate code using a specific generator 266 + #[wasm_bindgen] 267 + pub fn generate_code(source: &str, namespace: &str, generator_name: &str) -> JsValue { 268 + // Load standard library 269 + let mut workspace = match mlf_lang::Workspace::with_std() { 270 + Ok(ws) => ws, 271 + Err(e) => { 272 + let result = GenerateCodeResult { 273 + success: false, 274 + code: None, 275 + error: Some(format!("Failed to load standard library: {:?}", e)), 276 + }; 277 + return serde_wasm_bindgen::to_value(&result).unwrap(); 278 + } 279 + }; 280 + 281 + // Parse the source 282 + let lexicon = match mlf_lang::parse_lexicon(source) { 283 + Ok(lex) => lex, 284 + Err(e) => { 285 + let result = GenerateCodeResult { 286 + success: false, 287 + code: None, 288 + error: Some(format!("Parse error: {:?}", e)), 289 + }; 290 + return serde_wasm_bindgen::to_value(&result).unwrap(); 291 + } 292 + }; 293 + 294 + // Find the generator 295 + let generator = match mlf_codegen::plugin::find_generator(generator_name) { 296 + Some(g) => g, 297 + None => { 298 + let result = GenerateCodeResult { 299 + success: false, 300 + code: None, 301 + error: Some(format!("Generator '{}' not found", generator_name)), 302 + }; 303 + return serde_wasm_bindgen::to_value(&result).unwrap(); 304 + } 305 + }; 306 + 307 + // Generate code 308 + let ctx = mlf_codegen::GeneratorContext { 309 + namespace, 310 + lexicon: &lexicon, 311 + workspace: &workspace, 312 + }; 313 + 314 + match generator.generate(&ctx) { 315 + Ok(code) => { 316 + let result = GenerateCodeResult { 317 + success: true, 318 + code: Some(code), 319 + error: None, 320 + }; 321 + serde_wasm_bindgen::to_value(&result).unwrap() 322 + } 323 + Err(e) => { 324 + let result = GenerateCodeResult { 325 + success: false, 326 + code: None, 327 + error: Some(e), 328 + }; 329 + serde_wasm_bindgen::to_value(&result).unwrap() 330 + } 331 + } 332 + } 333 + 219 334 #[cfg(test)] 220 335 mod tests { 221 336 use super::*;
+4 -4
website/justfile
··· 1 1 # Build development version (faster, no optimizations) 2 2 build-dev: 3 3 #!/usr/bin/env bash 4 + cd mlf-playground-wasm 5 + wasm-pack build --target web --out-name mlf_wasm --out-dir ../static/js/pkg --dev 4 6 cd .. 5 - wasm-pack build mlf-wasm --target web --out-dir ../website/static/js/pkg 6 - cd website 7 7 zola build 8 8 9 9 # Build release version (optimized) 10 10 build-release: 11 11 #!/usr/bin/env bash 12 + cd mlf-playground-wasm 13 + wasm-pack build --target web --out-name mlf_wasm --out-dir ../static/js/pkg --release 12 14 cd .. 13 - wasm-pack build mlf-wasm --target web --out-dir ../website/static/js/pkg --release 14 - cd website 15 15 if command -v wasm-opt >/dev/null 2>&1; then 16 16 wasm-opt -Oz static/js/pkg/mlf_wasm_bg.wasm -o static/js/pkg/mlf_wasm_bg.wasm 17 17 fi
+20
website/mlf-playground-wasm/Cargo.toml
··· 1 + [package] 2 + name = "mlf-playground-wasm" 3 + version = "0.1.0" 4 + edition = "2024" 5 + license = "MIT" 6 + 7 + [lib] 8 + crate-type = ["cdylib", "rlib"] 9 + 10 + [dependencies] 11 + mlf-wasm = { path = "../../mlf-wasm" } 12 + mlf-codegen = { path = "../../mlf-codegen" } 13 + mlf-codegen-typescript = { path = "../../codegen-plugins/mlf-codegen-typescript" } 14 + mlf-codegen-go = { path = "../../codegen-plugins/mlf-codegen-go" } 15 + mlf-codegen-rust = { path = "../../codegen-plugins/mlf-codegen-rust" } 16 + wasm-bindgen = "0.2" 17 + 18 + # Force inclusion of plugin crates 19 + [profile.release] 20 + lto = false
+17
website/mlf-playground-wasm/src/lib.rs
··· 1 + // Re-export everything from mlf-wasm 2 + pub use mlf_wasm::*; 3 + 4 + // Import the plugin crates and reference their static generators 5 + // This forces the linker to include them in the binary 6 + use mlf_codegen_typescript::TYPESCRIPT_GENERATOR; 7 + use mlf_codegen_go::GO_GENERATOR; 8 + use mlf_codegen_rust::RUST_GENERATOR; 9 + 10 + // Force the linker to keep the generator statics by referencing them 11 + // This function must never be optimized away 12 + #[used] 13 + static _KEEP_GENERATORS: &[&dyn mlf_codegen::plugin::CodeGenerator] = &[ 14 + &TYPESCRIPT_GENERATOR, 15 + &GO_GENERATOR, 16 + &RUST_GENERATOR, 17 + ];
+30
website/sass/style.scss
··· 474 474 border-color: var(--accent); 475 475 } 476 476 477 + .generator-selector select { 478 + padding: 0.375rem 0.75rem; 479 + background: transparent; 480 + border: 1px solid var(--border); 481 + border-radius: 0.25rem; 482 + color: var(--text-light); 483 + font-size: 0.813rem; 484 + font-family: inherit; 485 + cursor: pointer; 486 + transition: all 0.2s; 487 + appearance: none; 488 + -webkit-appearance: none; 489 + -moz-appearance: none; 490 + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cpath fill='%23c3c3c3' d='M6 9L1 4h10z'/%3E%3C/svg%3E"); 491 + background-repeat: no-repeat; 492 + background-position: right 0.5rem center; 493 + padding-right: 2rem; 494 + } 495 + 496 + .generator-selector select:hover { 497 + color: var(--text); 498 + border-color: var(--text-light); 499 + } 500 + 501 + .generator-selector select:focus { 502 + outline: none; 503 + border-color: var(--accent); 504 + color: var(--text); 505 + } 506 + 477 507 textarea { 478 508 width: 100%; 479 509 flex: 1;
+73 -41
website/static/js/app.js
··· 182 182 updateLineNumbers(initialCode); 183 183 updateHighlighting(initialCode); 184 184 185 - // Convert JSON output textarea to highlighted div with line numbers 186 - const jsonTextarea = document.getElementById('lexicon-result'); 185 + // Convert generated output textarea to highlighted div with line numbers 186 + const generateTextarea = document.getElementById('generate-result'); 187 187 188 - const jsonOuterContainer = document.createElement('div'); 189 - jsonOuterContainer.className = 'shiki-output-outer-container'; 190 - jsonOuterContainer.id = 'lexicon-result-outer-container'; 188 + const generateOuterContainer = document.createElement('div'); 189 + generateOuterContainer.className = 'shiki-output-outer-container'; 190 + generateOuterContainer.id = 'generate-result-outer-container'; 191 191 192 - const jsonLineNumbers = document.createElement('div'); 193 - jsonLineNumbers.className = 'line-numbers'; 194 - jsonLineNumbers.id = 'json-line-numbers'; 192 + const generateLineNumbers = document.createElement('div'); 193 + generateLineNumbers.className = 'line-numbers'; 194 + generateLineNumbers.id = 'generate-line-numbers'; 195 195 196 - const jsonWrapper = document.createElement('div'); 197 - jsonWrapper.className = 'output-wrapper'; 196 + const generateWrapper = document.createElement('div'); 197 + generateWrapper.className = 'output-wrapper'; 198 198 199 - const jsonContainer = document.createElement('div'); 200 - jsonContainer.className = 'shiki-output-container'; 201 - jsonContainer.id = 'lexicon-result-container'; 199 + const generateContainer = document.createElement('div'); 200 + generateContainer.className = 'shiki-output-container'; 201 + generateContainer.id = 'generate-result-container'; 202 202 203 - jsonWrapper.appendChild(jsonContainer); 204 - jsonOuterContainer.appendChild(jsonLineNumbers); 205 - jsonOuterContainer.appendChild(jsonWrapper); 203 + generateWrapper.appendChild(generateContainer); 204 + generateOuterContainer.appendChild(generateLineNumbers); 205 + generateOuterContainer.appendChild(generateWrapper); 206 206 207 - jsonTextarea.style.display = 'none'; 208 - jsonTextarea.parentNode.insertBefore(jsonOuterContainer, jsonTextarea); 207 + generateTextarea.style.display = 'none'; 208 + generateTextarea.parentNode.insertBefore(generateOuterContainer, generateTextarea); 209 209 } 210 210 211 211 function updateHighlighting(code) { ··· 299 299 return textarea ? textarea.value : ''; 300 300 } 301 301 302 - function updateJsonOutput(jsonString) { 303 - const container = document.getElementById('lexicon-result-container'); 302 + function updateGeneratedOutput(code, generatorType) { 303 + const container = document.getElementById('generate-result-container'); 304 304 if (!container || !highlighter) return; 305 305 306 - // Format JSON 307 - try { 308 - const formatted = JSON.stringify(JSON.parse(jsonString), null, 2); 306 + // Map generator types to Shiki language identifiers 307 + const langMap = { 308 + 'json': 'json', 309 + 'typescript': 'typescript', 310 + 'go': 'go', 311 + 'rust': 'rust' 312 + }; 313 + 314 + const lang = langMap[generatorType] || 'text'; 309 315 316 + // Format JSON if it's JSON output 317 + let formattedCode = code; 318 + if (generatorType === 'json') { 319 + try { 320 + formattedCode = JSON.stringify(JSON.parse(code), null, 2); 321 + } catch (e) { 322 + // If JSON parsing fails, use the original code 323 + formattedCode = code; 324 + } 325 + } 326 + 327 + try { 310 328 // Highlight with Shiki 311 - const html = highlighter.codeToHtml(formatted, { 312 - lang: 'json', 329 + const html = highlighter.codeToHtml(formattedCode, { 330 + lang: lang, 313 331 theme: 'dracula' 314 332 }); 315 333 ··· 321 339 if (codeElement) { 322 340 container.innerHTML = codeElement.innerHTML; 323 341 } else { 324 - container.textContent = formatted; 342 + container.textContent = formattedCode; 325 343 } 326 344 327 - // Update line numbers for JSON output 328 - updateJsonLineNumbers(formatted); 345 + // Update line numbers for output 346 + updateGenerateLineNumbers(formattedCode); 329 347 } catch (e) { 330 - container.textContent = jsonString; 331 - updateJsonLineNumbers(jsonString); 348 + // Fallback if highlighting fails 349 + container.textContent = formattedCode; 350 + updateGenerateLineNumbers(formattedCode); 332 351 } 333 352 } 334 353 335 - function updateJsonLineNumbers(code) { 336 - const lineNumbers = document.getElementById('json-line-numbers'); 354 + function updateGenerateLineNumbers(code) { 355 + const lineNumbers = document.getElementById('generate-line-numbers'); 337 356 if (!lineNumbers) return; 338 357 339 358 const lines = code.split('\n').length; ··· 394 413 } 395 414 } 396 415 397 - // Synchronize scroll between JSON output and line numbers 398 - const jsonWrapper = document.querySelector('.output-wrapper'); 399 - const jsonLineNumbers = document.getElementById('json-line-numbers'); 400 - if (jsonWrapper && jsonLineNumbers) { 401 - jsonWrapper.addEventListener('scroll', () => { 402 - jsonLineNumbers.scrollTop = jsonWrapper.scrollTop; 416 + // Synchronize scroll between generated output and line numbers 417 + const generateWrapper = document.querySelector('.output-wrapper'); 418 + const generateLineNumbers = document.getElementById('generate-line-numbers'); 419 + if (generateWrapper && generateLineNumbers) { 420 + generateWrapper.addEventListener('scroll', () => { 421 + generateLineNumbers.scrollTop = generateWrapper.scrollTop; 422 + }); 423 + } 424 + 425 + // Generator dropdown change listener 426 + const generatorSelect = document.getElementById('generator-select'); 427 + if (generatorSelect) { 428 + generatorSelect.addEventListener('change', () => { 429 + handleCheck(); 403 430 }); 404 431 } 405 432 ··· 488 515 if (checkResult.success) { 489 516 hideError(); 490 517 491 - const generateResult = wasm.generate_lexicon(source, namespace); 518 + // Get selected generator 519 + const generatorSelect = document.getElementById('generator-select'); 520 + const selectedGenerator = generatorSelect ? generatorSelect.value : 'json'; 521 + 522 + // Generate code with selected generator 523 + const generateResult = wasm.generate_code(source, namespace, selectedGenerator); 492 524 493 525 if (generateResult.success) { 494 - updateJsonOutput(generateResult.lexicon); 526 + updateGeneratedOutput(generateResult.code, selectedGenerator); 495 527 } else { 496 - showError(generateResult.error || 'Failed to generate lexicon'); 528 + showError(generateResult.error || 'Failed to generate code'); 497 529 } 498 530 } else { 499 531 const errors = checkResult.errors || ['Unknown error'];
+11 -3
website/templates/playground.html
··· 29 29 <div class="output-panel"> 30 30 <div class="panel-header"> 31 31 <div class="tabs"> 32 - <button class="tab active" data-tab="lexicon">Lexicon</button> 32 + <button class="tab active" data-tab="generate">Generate</button> 33 33 <button class="tab" data-tab="validate">Validate</button> 34 34 </div> 35 + <div class="generator-selector"> 36 + <select id="generator-select"> 37 + <option value="json">Lexicon (JSON)</option> 38 + <option value="typescript">TypeScript</option> 39 + <option value="go">Go</option> 40 + <option value="rust">Rust</option> 41 + </select> 42 + </div> 35 43 </div> 36 44 37 - <div id="lexicon-output" class="output-content active"> 38 - <textarea id="lexicon-result" readonly spellcheck="false" placeholder="Generated JSON lexicon will appear here..."></textarea> 45 + <div id="generate-output" class="output-content active"> 46 + <textarea id="generate-result" readonly spellcheck="false" placeholder="Generated code will appear here..."></textarea> 39 47 </div> 40 48 41 49 <div id="validate-output" class="output-content">