1mod i18n;
2mod resolve;
3pub mod render;
4
5use std::collections::HashMap;
6
7pub use render::PageAssets;
8pub use resolve::ResolvedComponent;
9pub use resolve::resolve_single;
10pub use resolve::resolve_with_files;
11pub use resolve::resolve_with_files_debug;
12
13pub fn compile_page(
18 entry_path: &str,
19 files: &HashMap<String, String>,
20 data_json: &str,
21) -> Result<String, String> {
22 compile_page_with_debug(entry_path, files, data_json, false, &HashMap::new(), "Van")
23}
24
25pub fn compile_page_debug(
29 entry_path: &str,
30 files: &HashMap<String, String>,
31 data_json: &str,
32 file_origins: &HashMap<String, String>,
33) -> Result<String, String> {
34 compile_page_with_debug(entry_path, files, data_json, true, file_origins, "Van")
35}
36
37fn compile_page_with_debug(
38 entry_path: &str,
39 files: &HashMap<String, String>,
40 data_json: &str,
41 debug: bool,
42 file_origins: &HashMap<String, String>,
43 global_name: &str,
44) -> Result<String, String> {
45 let data: serde_json::Value = serde_json::from_str(data_json)
46 .map_err(|e| format!("Invalid JSON: {e}"))?;
47 let resolved = if debug {
48 resolve::resolve_with_files_debug(entry_path, files, &data, file_origins)?
49 } else {
50 resolve::resolve_with_files(entry_path, files, &data)?
51 };
52 render::render_page(&resolved, &data, global_name)
53}
54
55pub fn compile_page_assets(
60 entry_path: &str,
61 files: &HashMap<String, String>,
62 data_json: &str,
63 asset_prefix: &str,
64) -> Result<PageAssets, String> {
65 compile_page_assets_with_debug(entry_path, files, data_json, asset_prefix, false, &HashMap::new(), "Van")
66}
67
68pub fn compile_page_assets_debug(
72 entry_path: &str,
73 files: &HashMap<String, String>,
74 data_json: &str,
75 asset_prefix: &str,
76 file_origins: &HashMap<String, String>,
77) -> Result<PageAssets, String> {
78 compile_page_assets_with_debug(entry_path, files, data_json, asset_prefix, true, file_origins, "Van")
79}
80
81fn compile_page_assets_with_debug(
82 entry_path: &str,
83 files: &HashMap<String, String>,
84 data_json: &str,
85 asset_prefix: &str,
86 debug: bool,
87 file_origins: &HashMap<String, String>,
88 global_name: &str,
89) -> Result<PageAssets, String> {
90 let data: serde_json::Value = serde_json::from_str(data_json)
91 .map_err(|e| format!("Invalid JSON: {e}"))?;
92 let resolved = if debug {
93 resolve::resolve_with_files_debug(entry_path, files, &data, file_origins)?
94 } else {
95 resolve::resolve_with_files(entry_path, files, &data)?
96 };
97
98 let page_name = entry_path.trim_end_matches(".van");
100
101 render::render_page_assets(&resolved, &data, page_name, asset_prefix, global_name)
102}
103
104pub fn compile_page_full(
106 entry_path: &str,
107 files: &HashMap<String, String>,
108 data_json: &str,
109 debug: bool,
110 file_origins: &HashMap<String, String>,
111 global_name: &str,
112) -> Result<String, String> {
113 compile_page_with_debug(entry_path, files, data_json, debug, file_origins, global_name)
114}
115
116pub fn compile_page_assets_full(
118 entry_path: &str,
119 files: &HashMap<String, String>,
120 data_json: &str,
121 asset_prefix: &str,
122 debug: bool,
123 file_origins: &HashMap<String, String>,
124 global_name: &str,
125) -> Result<PageAssets, String> {
126 compile_page_assets_with_debug(entry_path, files, data_json, asset_prefix, debug, file_origins, global_name)
127}
128
129pub fn compile_single(source: &str, data_json: &str) -> Result<String, String> {
133 let mut files = HashMap::new();
134 files.insert("main.van".to_string(), source.to_string());
135 compile_page("main.van", &files, data_json)
136}
137
138#[cfg(feature = "wasm")]
139use wasm_bindgen::prelude::*;
140
141#[cfg(feature = "wasm")]
142#[wasm_bindgen]
143pub fn compile_van(
144 entry_path: &str,
145 files_json: &str,
146 data_json: &str,
147) -> Result<String, JsValue> {
148 let files_value: serde_json::Value = serde_json::from_str(files_json)
150 .map_err(|e| JsValue::from_str(&format!("Invalid files JSON: {e}")))?;
151
152 let files_obj = files_value
153 .as_object()
154 .ok_or_else(|| JsValue::from_str("files_json must be a JSON object"))?;
155
156 let mut files = HashMap::new();
157 for (key, val) in files_obj {
158 let content = val
159 .as_str()
160 .ok_or_else(|| JsValue::from_str(&format!("File '{}' content must be a string", key)))?;
161 files.insert(key.clone(), content.to_string());
162 }
163
164 compile_page(entry_path, &files, data_json).map_err(|e| JsValue::from_str(&e))
165}
166
167#[cfg(test)]
168mod tests {
169 use super::*;
170
171 #[test]
172 fn test_compile_single_basic() {
173 let source = r#"
174<template>
175 <h1>{{ title }}</h1>
176</template>
177"#;
178 let data = r#"{"title": "Hello World"}"#;
179 let html = compile_single(source, data).unwrap();
180 assert!(html.contains("<h1>Hello World</h1>"));
181 assert!(html.contains("<!DOCTYPE html>"));
182 }
183
184 #[test]
185 fn test_compile_single_with_signals() {
186 let source = r#"
187<template>
188 <div>
189 <p>Count: {{ count }}</p>
190 <button @click="increment">+1</button>
191 </div>
192</template>
193
194<script setup>
195const count = ref(0)
196function increment() { count.value++ }
197</script>
198"#;
199 let data = r#"{}"#;
200 let html = compile_single(source, data).unwrap();
201 assert!(html.contains("Van"));
202 assert!(!html.contains("@click"));
203 assert!(html.contains("effect"));
204 }
205
206 #[test]
207 fn test_compile_page_invalid_json() {
208 let mut files = HashMap::new();
209 files.insert("main.van".to_string(), "<template><p>Hi</p></template>".to_string());
210 let result = compile_page("main.van", &files, "not json");
211 assert!(result.is_err());
212 assert!(result.unwrap_err().contains("Invalid JSON"));
213 }
214
215 #[test]
216 fn test_compile_page_with_style() {
217 let source = r#"
218<template>
219 <h1>Hello</h1>
220</template>
221
222<style>
223h1 { color: blue; }
224</style>
225"#;
226 let html = compile_single(source, "{}").unwrap();
227 assert!(html.contains("color: blue"));
228 }
229
230 #[test]
231 fn test_compile_page_multi_file() {
232 let mut files = HashMap::new();
233 files.insert(
234 "index.van".to_string(),
235 r#"
236<template>
237 <hello :name="title" />
238</template>
239
240<script setup>
241import Hello from './hello.van'
242</script>
243"#
244 .to_string(),
245 );
246 files.insert(
247 "hello.van".to_string(),
248 r#"
249<template>
250 <h1>Hello, {{ name }}!</h1>
251</template>
252
253<style>
254h1 { color: green; }
255</style>
256"#
257 .to_string(),
258 );
259
260 let data = r#"{"title": "Van"}"#;
261 let html = compile_page("index.van", &files, data).unwrap();
262 assert!(html.contains("<h1>Hello, Van!</h1>"));
263 assert!(html.contains("color: green"));
264 }
265
266 #[test]
267 fn test_compile_page_with_ts_import() {
268 let mut files = HashMap::new();
269 files.insert(
270 "index.van".to_string(),
271 r#"
272<template>
273 <div>
274 <p>Count: {{ count }}</p>
275 <button @click="increment">+1</button>
276 </div>
277</template>
278
279<script setup lang="ts">
280import { formatDate } from './utils/format.ts'
281import type { User } from './types.ts'
282const count = ref(0)
283function increment() { count.value++ }
284</script>
285"#
286 .to_string(),
287 );
288 files.insert(
289 "utils/format.ts".to_string(),
290 r#"function formatDate(d) { return d.toISOString(); }
291return { formatDate: formatDate };"#
292 .to_string(),
293 );
294 let data = r#"{}"#;
297 let html = compile_page("index.van", &files, data).unwrap();
298 assert!(html.contains("__mod_0"));
300 assert!(html.contains("formatDate"));
301 assert!(html.contains("Van"));
303 assert!(html.contains("effect"));
304 assert!(!html.contains("__mod_1"));
306 }
307
308 #[test]
309 fn test_compile_single_i18n_basic() {
310 let source = r#"
311<template>
312 <h1>{{ $t('title') }}</h1>
313 <p>{{ $t('greeting', { name: userName }) }}</p>
314</template>
315"#;
316 let data = r#"{"userName": "Alice", "$i18n": {"title": "欢迎", "greeting": "你好,{name}!"}}"#;
317 let html = compile_single(source, data).unwrap();
318 assert!(html.contains("<h1>欢迎</h1>"));
319 assert!(html.contains("<p>你好,Alice!</p>"));
320 }
321
322 #[test]
323 fn test_compile_i18n_child_component_inherits() {
324 let mut files = HashMap::new();
325 files.insert(
326 "index.van".to_string(),
327 r#"
328<template>
329 <greeting :name="userName" />
330</template>
331
332<script setup>
333import Greeting from './greeting.van'
334</script>
335"#
336 .to_string(),
337 );
338 files.insert(
339 "greeting.van".to_string(),
340 r#"
341<template>
342 <p>{{ $t('hello') }}, {{ name }}!</p>
343</template>
344"#
345 .to_string(),
346 );
347
348 let data = r#"{"userName": "Bob", "$i18n": {"hello": "你好"}}"#;
349 let html = compile_page("index.van", &files, data).unwrap();
350 assert!(html.contains("你好, Bob!"));
351 }
352
353 #[test]
354 fn test_compile_i18n_prop_binding() {
355 let mut files = HashMap::new();
356 files.insert(
357 "index.van".to_string(),
358 r#"
359<template>
360 <img-comp :alt="$t('logo.alt')" />
361</template>
362
363<script setup>
364import ImgComp from './img-comp.van'
365</script>
366"#
367 .to_string(),
368 );
369 files.insert(
370 "img-comp.van".to_string(),
371 r#"
372<template>
373 <img alt="{{ alt }}" />
374</template>
375"#
376 .to_string(),
377 );
378
379 let data = r#"{"$i18n": {"logo": {"alt": "Logo图片"}}}"#;
380 let html = compile_page("index.van", &files, data).unwrap();
381 assert!(html.contains("Logo图片"));
382 }
383
384 #[test]
385 fn test_compile_page_type_only_import_erased() {
386 let mut files = HashMap::new();
387 files.insert(
388 "index.van".to_string(),
389 r#"
390<template>
391 <div><p>{{ count }}</p></div>
392</template>
393
394<script setup lang="ts">
395import type { Config } from './config.ts'
396const count = ref(0)
397</script>
398"#
399 .to_string(),
400 );
401
402 let data = r#"{}"#;
403 let html = compile_page("index.van", &files, data).unwrap();
404 assert!(!html.contains("__mod_"));
406 assert!(html.contains("V.signal(0)"));
408 }
409}