1pub mod common;
2pub mod compiler;
3pub mod converter;
4pub mod diagnostic;
5pub mod formatter;
6pub mod parser;
7pub mod validate;
8
9use serde::Serialize;
10use wasm_bindgen::prelude::*;
11
12fn parse_source(source: &str) -> Result<parser::RootNode, String> {
14 let span = parser::Span::new(source);
15 let (rest, ast) = parser::parse::parse(span).map_err(|e| format!("HSML parse error: {e}"))?;
16
17 if !rest.fragment().is_empty() {
18 return Err(format!(
19 "HSML parse error: unconsumed input at line {}, column {}",
20 rest.location_line(),
21 rest.get_column()
22 ));
23 }
24
25 Ok(ast)
26}
27
28pub fn compile_content_core(source: &str) -> Result<String, String> {
30 let ast = parse_source(source)?;
31 compiler::compile(&ast, &compiler::HsmlCompileOptions::default())
32}
33
34#[derive(Debug, Clone, PartialEq, Serialize)]
36pub struct CompileOutput {
37 pub html: String,
39 pub diagnostics: Vec<diagnostic::Diagnostic>,
41}
42
43pub fn check_content(source: &str) -> Vec<diagnostic::Diagnostic> {
46 let span = parser::Span::new(source);
47
48 let (rest, ast) = match parser::parse::parse(span) {
49 Ok(result) => result,
50 Err(e) => return vec![diagnostic::Diagnostic::from(&e)],
51 };
52
53 if !rest.fragment().is_empty() {
54 return vec![diagnostic::Diagnostic {
55 severity: diagnostic::Severity::Error,
56 message: "Unconsumed input".to_string(),
57 code: None,
58 location: Some(diagnostic::Location {
59 start: diagnostic::Position {
60 line: rest.location_line(),
61 column: rest.get_column() as u32,
62 },
63 end: diagnostic::Position {
64 line: rest.location_line(),
65 column: rest.get_column() as u32,
66 },
67 }),
68 file_path: None,
69 }];
70 }
71
72 validate::validate(&ast, source)
73}
74
75pub fn compile_content_diagnostics(
78 source: &str,
79) -> Result<CompileOutput, Vec<diagnostic::Diagnostic>> {
80 compile_content_diagnostics_with_options(source, &compiler::HsmlCompileOptions::default())
81}
82
83pub fn compile_content_diagnostics_with_options(
85 source: &str,
86 options: &compiler::HsmlCompileOptions,
87) -> Result<CompileOutput, Vec<diagnostic::Diagnostic>> {
88 let span = parser::Span::new(source);
89
90 let (rest, ast) =
91 parser::parse::parse(span).map_err(|e| vec![diagnostic::Diagnostic::from(&e)])?;
92
93 if !rest.fragment().is_empty() {
94 return Err(vec![diagnostic::Diagnostic {
95 severity: diagnostic::Severity::Error,
96 message: "Unconsumed input".to_string(),
97 code: None,
98 location: Some(diagnostic::Location {
99 start: diagnostic::Position {
100 line: rest.location_line(),
101 column: rest.get_column() as u32,
102 },
103 end: diagnostic::Position {
104 line: rest.location_line(),
105 column: rest.get_column() as u32,
106 },
107 }),
108 file_path: None,
109 }]);
110 }
111
112 let diagnostics = validate::validate(&ast, source);
114
115 let html = compiler::compile(&ast, options)
116 .map_err(|e| vec![diagnostic::Diagnostic::compiler_error(e)])?;
117
118 Ok(CompileOutput { html, diagnostics })
119}
120
121pub fn convert_html_core(html: &str) -> Result<String, String> {
123 converter::convert(html)
124}
125
126pub fn format_content_core(
128 source: &str,
129 options: &formatter::FormatOptions,
130) -> Result<String, String> {
131 let ast = parse_source(source)?;
132 Ok(formatter::format(&ast, options))
133}
134
135#[derive(serde::Deserialize)]
137#[serde(rename_all = "camelCase")]
138struct WasmFormatOptions {
139 #[serde(default = "default_indent_size")]
140 indent_size: usize,
141 #[serde(default = "default_print_width")]
142 print_width: usize,
143}
144
145fn default_indent_size() -> usize {
146 formatter::FormatOptions::default().indent_size
147}
148
149fn default_print_width() -> usize {
150 formatter::FormatOptions::default().print_width
151}
152
153impl From<WasmFormatOptions> for formatter::FormatOptions {
154 fn from(opts: WasmFormatOptions) -> Self {
155 Self {
156 indent_size: opts.indent_size,
157 print_width: opts.print_width,
158 }
159 }
160}
161
162#[wasm_bindgen(js_name = "formatContent")]
170pub fn format_content(source: &str, options: JsValue) -> Result<String, JsError> {
171 let options: formatter::FormatOptions = if options.is_undefined() || options.is_null() {
172 formatter::FormatOptions::default()
173 } else {
174 let wasm_opts: WasmFormatOptions = serde_wasm_bindgen::from_value(options)
175 .map_err(|e| JsError::new(&format!("Invalid formatContent options: {e}")))?;
176 wasm_opts.into()
177 };
178
179 format_content_core(source, &options).map_err(|e| JsError::new(&e))
180}
181
182#[wasm_bindgen(js_name = "convertHtml")]
186pub fn convert_html(html: &str) -> Result<String, JsError> {
187 convert_html_core(html).map_err(|e| JsError::new(&e))
188}
189
190#[wasm_bindgen(js_name = "compileContent")]
194pub fn compile_content(source: &str) -> Result<String, JsError> {
195 compile_content_core(source).map_err(|e| JsError::new(&e))
196}
197
198#[derive(Serialize)]
200struct WasmCompileResult {
201 success: bool,
202 html: Option<String>,
203 diagnostics: Vec<diagnostic::Diagnostic>,
204}
205
206#[derive(serde::Deserialize)]
208#[serde(rename_all = "camelCase")]
209struct WasmCompileOptions {
210 #[serde(default)]
211 pretty: bool,
212 #[serde(default = "default_compile_indent_size")]
213 indent_size: usize,
214}
215
216fn default_compile_indent_size() -> usize {
217 compiler::HsmlCompileOptions::default().indent_size
218}
219
220impl From<WasmCompileOptions> for compiler::HsmlCompileOptions {
221 fn from(opts: WasmCompileOptions) -> Self {
222 Self {
223 pretty: opts.pretty,
224 indent_size: opts.indent_size,
225 }
226 }
227}
228
229#[wasm_bindgen(js_name = "compileContentWithDiagnostics")]
237pub fn compile_content_with_diagnostics(source: &str, options: JsValue) -> JsValue {
238 let compile_options: compiler::HsmlCompileOptions =
239 if options.is_undefined() || options.is_null() {
240 compiler::HsmlCompileOptions::default()
241 } else {
242 let wasm_opts: WasmCompileOptions = match serde_wasm_bindgen::from_value(options) {
243 Ok(opts) => opts,
244 Err(_) => WasmCompileOptions {
245 pretty: false,
246 indent_size: default_compile_indent_size(),
247 },
248 };
249 wasm_opts.into()
250 };
251
252 let result = match compile_content_diagnostics_with_options(source, &compile_options) {
253 Ok(output) => WasmCompileResult {
254 success: true,
255 html: Some(output.html),
256 diagnostics: output.diagnostics,
257 },
258 Err(diagnostics) => WasmCompileResult {
259 success: false,
260 html: None,
261 diagnostics,
262 },
263 };
264
265 let serializer = serde_wasm_bindgen::Serializer::json_compatible();
266 use serde::Serialize;
267
268 result.serialize(&serializer).unwrap_or_else(|e| {
269 let fallback = WasmCompileResult {
270 success: false,
271 html: None,
272 diagnostics: vec![diagnostic::Diagnostic::compiler_error(format!(
273 "Failed to serialize compile result: {e}"
274 ))],
275 };
276 fallback
277 .serialize(&serializer)
278 .expect("fallback WasmCompileResult serialization should always succeed")
279 })
280}