baobao_codegen_typescript/
code_file.rs1use baobao_codegen::builder::{CodeBuilder, CodeFragment, Indent, Renderable};
7
8use crate::ast::{Export, Import};
9
10#[derive(Default)]
25pub struct CodeFile {
26 imports: Vec<Import>,
27 body: Vec<Vec<CodeFragment>>,
28 exports: Vec<Export>,
29}
30
31impl CodeFile {
32 pub fn new() -> Self {
34 Self::default()
35 }
36
37 pub fn import(mut self, import: Import) -> Self {
39 self.imports.push(import);
40 self
41 }
42
43 pub fn imports(mut self, imports: impl IntoIterator<Item = Import>) -> Self {
45 self.imports.extend(imports);
46 self
47 }
48
49 #[allow(clippy::should_implement_trait)]
51 pub fn add<R: Renderable>(mut self, node: R) -> Self {
52 self.body.push(node.to_fragments());
53 self
54 }
55
56 pub fn add_all<R: Renderable>(mut self, nodes: impl IntoIterator<Item = R>) -> Self {
58 for node in nodes {
59 self.body.push(node.to_fragments());
60 }
61 self
62 }
63
64 pub fn export(mut self, export: Export) -> Self {
66 self.exports.push(export);
67 self
68 }
69
70 pub fn exports(mut self, exports: impl IntoIterator<Item = Export>) -> Self {
72 self.exports.extend(exports);
73 self
74 }
75
76 pub fn render(&self) -> String {
78 self.render_with_indent(Indent::TYPESCRIPT)
79 }
80
81 pub fn render_with_indent(&self, indent: Indent) -> String {
83 let mut builder = CodeBuilder::new(indent);
84
85 for import in &self.imports {
87 builder.emit(import);
88 }
89
90 if !self.imports.is_empty() && (!self.body.is_empty() || !self.exports.is_empty()) {
92 builder.push_blank();
93 }
94
95 for (i, fragments) in self.body.iter().enumerate() {
97 if i > 0 {
98 builder.push_blank();
99 }
100 for fragment in fragments {
101 builder.apply_fragment(fragment.clone());
102 }
103 }
104
105 if !self.body.is_empty() && !self.exports.is_empty() {
107 builder.push_blank();
108 }
109
110 for export in &self.exports {
112 builder.emit(export);
113 }
114
115 builder.build()
116 }
117
118 pub fn is_empty(&self) -> bool {
120 self.imports.is_empty() && self.body.is_empty() && self.exports.is_empty()
121 }
122}
123
124#[derive(Debug, Clone)]
128pub struct RawCode(String);
129
130impl RawCode {
131 pub fn new(code: impl Into<String>) -> Self {
133 Self(code.into())
134 }
135
136 pub fn lines(lines: impl IntoIterator<Item = impl Into<String>>) -> Self {
138 Self(
139 lines
140 .into_iter()
141 .map(Into::into)
142 .collect::<Vec<_>>()
143 .join("\n"),
144 )
145 }
146}
147
148impl Renderable for RawCode {
149 fn to_fragments(&self) -> Vec<CodeFragment> {
150 self.0
151 .lines()
152 .map(|line| CodeFragment::Line(line.to_string()))
153 .collect()
154 }
155}
156
157#[cfg(test)]
158mod tests {
159 use super::*;
160
161 #[test]
162 fn test_empty_file() {
163 let file = CodeFile::new();
164 assert!(file.is_empty());
165 assert_eq!(file.render(), "");
166 }
167
168 #[test]
169 fn test_imports_only() {
170 let file = CodeFile::new().import(Import::new("boune").named("defineCommand"));
171 let code = file.render();
172 assert!(code.contains("import { defineCommand } from \"boune\";"));
173 }
174
175 #[test]
176 fn test_raw_code_body() {
177 let file = CodeFile::new().add(RawCode::new("const x = 1;"));
178 let code = file.render();
179 assert_eq!(code, "const x = 1;\n");
180 }
181
182 #[test]
183 fn test_full_file() {
184 let file = CodeFile::new()
185 .import(Import::new("boune").named("defineCommand"))
186 .add(RawCode::new("const cmd = defineCommand({});"))
187 .export(Export::new().named("cmd"));
188
189 let code = file.render();
190 assert!(code.contains("import { defineCommand }"));
191 assert!(code.contains("const cmd = defineCommand"));
192 assert!(code.contains("export { cmd }"));
193 }
194
195 #[test]
196 fn test_blank_lines_between_body() {
197 let file = CodeFile::new()
198 .add(RawCode::new("const a = 1;"))
199 .add(RawCode::new("const b = 2;"));
200
201 let code = file.render();
202 assert!(code.contains("const a = 1;\n\nconst b = 2;"));
203 }
204}