1use crate::{CloverExtensions, ComponentImports, OutputFormat};
2use oxc_diagnostics::OxcDiagnostic;
3use serde::Serialize;
4use wasm_bindgen::prelude::*;
5
6#[derive(Serialize)]
7struct TransformResult {
8 text: Option<String>,
9 success: bool,
10 format: Option<OutputFormat>,
11 errors: Vec<WasmDiagnostic>,
12}
13
14#[derive(Serialize)]
15struct WasmDiagnostic {
16 message: String,
17 notes: Vec<WasmLabel>,
18 line: u32,
19 column: u32,
20}
21
22#[derive(Serialize)]
23struct WasmLabel {
24 message: String,
25 line: u32,
26 column: u32,
27 width: u32,
28}
29
30/// Convert an OxcDiagnostic to a WasmDiagnostic.
31/// - Takes the first label and absorbs its position into the root error.
32/// - If there are multiple labels or labels with text, forward them.
33fn convert_diagnostic(src: &str, diag: &OxcDiagnostic) -> WasmDiagnostic {
34 let labels = diag.labels.as_deref().unwrap_or(&[]);
35
36 // Determine root line/column from first label, or default to 1:1
37 let (line, column) = if let Some(first) = labels.first() {
38 crate::offset_to_line_col(src, first.offset())
39 } else {
40 (1, 1)
41 };
42
43 // Determine which labels to forward:
44 // - Skip the first label if it has no message (it's absorbed into root)
45 // - Forward all labels if first has a message, or if there are multiple
46 let forward_labels = if labels.is_empty() {
47 vec![]
48 } else {
49 let first_has_message = labels[0].label().is_some();
50 let has_multiple = labels.len() > 1;
51
52 if first_has_message || has_multiple {
53 // Forward all labels
54 labels
55 .iter()
56 .map(|label| {
57 let (l, c) = crate::offset_to_line_col(src, label.offset());
58 WasmLabel {
59 message: label.label().unwrap_or_default().to_string(),
60 line: l,
61 column: c,
62 width: label.len() as u32,
63 }
64 })
65 .collect()
66 } else {
67 // First label has no message and is the only one - absorbed
68 vec![]
69 }
70 };
71
72 WasmDiagnostic {
73 message: diag.message.to_string(),
74 notes: forward_labels,
75 line,
76 column,
77 }
78}
79
80#[wasm_bindgen]
81pub fn transform(
82 src: &str,
83 force_format: Option<OutputFormat>,
84 layout_import: Option<String>,
85 component_imports: JsValue,
86 self_path: Option<String>,
87 markdown_only: Option<bool>,
88 clover_extensions: JsValue,
89) -> JsValue {
90 // Deserialize component_imports from JsValue (can be undefined/null)
91 let component_imports: Option<ComponentImports> =
92 if component_imports.is_undefined() || component_imports.is_null() {
93 None
94 } else {
95 serde_wasm_bindgen::from_value(component_imports).ok()
96 };
97
98 // Deserialize clover_extensions from JsValue (can be undefined/null)
99 let clover_extensions: Option<CloverExtensions> =
100 if clover_extensions.is_undefined() || clover_extensions.is_null() {
101 None
102 } else {
103 serde_wasm_bindgen::from_value(clover_extensions).ok()
104 };
105
106 match crate::transform(
107 src,
108 force_format,
109 layout_import,
110 component_imports,
111 self_path,
112 markdown_only.unwrap_or(false),
113 clover_extensions,
114 ) {
115 Ok(output) => serde_wasm_bindgen::to_value(&TransformResult {
116 text: Some(output.text),
117 success: true,
118 format: Some(output.format),
119 errors: vec![],
120 })
121 .expect("failed serialization"),
122 Err(errors) => serde_wasm_bindgen::to_value(&TransformResult {
123 text: None,
124 success: false,
125 format: None,
126 errors: errors.iter().map(|e| convert_diagnostic(src, e)).collect(),
127 })
128 .expect("failed serialization"),
129 }
130}