Skip to main content

deckmint_wasm/
lib.rs

1use deckmint::objects::image::ImageOptionsBuilder;
2use deckmint::objects::shape::ShapeOptionsBuilder;
3use deckmint::objects::table::{TableCell, TableOptions, TableRow};
4use deckmint::objects::text::TextOptionsBuilder;
5use deckmint::{Presentation, ShapeType};
6use wasm_bindgen::prelude::*;
7
8#[wasm_bindgen(start)]
9pub fn start() {
10    console_error_panic_hook::set_once();
11}
12
13/// WASM-exposed Presentation builder
14#[wasm_bindgen]
15pub struct JsPresentation {
16    inner: Presentation,
17}
18
19#[wasm_bindgen]
20impl JsPresentation {
21    #[wasm_bindgen(constructor)]
22    pub fn new() -> Self {
23        JsPresentation {
24            inner: Presentation::new(),
25        }
26    }
27
28    /// Set presentation title
29    pub fn set_title(&mut self, title: &str) {
30        self.inner.title = title.to_string();
31    }
32
33    /// Set presentation author
34    pub fn set_author(&mut self, author: &str) {
35        self.inner.author = author.to_string();
36    }
37
38    /// Set presentation subject
39    pub fn set_subject(&mut self, subject: &str) {
40        self.inner.subject = subject.to_string();
41    }
42
43    /// Set presentation company
44    pub fn set_company(&mut self, company: &str) {
45        self.inner.company = company.to_string();
46    }
47
48    /// Add a blank slide and return its index (0-based)
49    pub fn add_slide(&mut self) -> usize {
50        self.inner.add_slide();
51        self.inner.slide_count() - 1
52    }
53
54    /// Add a text box to a slide
55    /// opts_json: JSON string with fields: x, y, w, h, fontSize, bold, italic, align, color, fill
56    pub fn add_text(&mut self, slide_idx: usize, text: &str, opts_json: &str) -> Result<(), JsValue> {
57        let opts = parse_text_opts(opts_json)?;
58        if let Some(slide) = self.inner.slide_mut(slide_idx) {
59            slide.add_text(text, opts);
60        }
61        Ok(())
62    }
63
64    /// Add a shape to a slide
65    /// shape_type: string like "rect", "ellipse", "triangle", etc.
66    /// opts_json: JSON string with fields: x, y, w, h, fill, line_color, line_width
67    pub fn add_shape(&mut self, slide_idx: usize, shape_type: &str, opts_json: &str) -> Result<(), JsValue> {
68        let shape = parse_shape_type(shape_type)?;
69        let opts = parse_shape_opts(opts_json)?;
70        if let Some(slide) = self.inner.slide_mut(slide_idx) {
71            slide.add_shape(shape, opts);
72        }
73        Ok(())
74    }
75
76    /// Add an image from a base64-encoded string
77    /// extension: "png", "jpg", "gif", "svg", etc.
78    /// opts_json: JSON string with fields: x, y, w, h, alt_text, transparency
79    pub fn add_image_base64(
80        &mut self,
81        slide_idx: usize,
82        b64: &str,
83        extension: &str,
84        opts_json: &str,
85    ) -> Result<(), JsValue> {
86        let opts = parse_image_opts(opts_json)?;
87        if let Some(slide) = self.inner.slide_mut(slide_idx) {
88            slide.add_image_base64(b64, extension, opts)
89                .map_err(|e| JsValue::from_str(&e.to_string()))?;
90        }
91        Ok(())
92    }
93
94    /// Add a table from a JSON array of rows
95    /// rows_json: JSON array of arrays of objects: [{text, bold, italic, colspan, rowspan, fill, color, align}, ...]
96    /// opts_json: JSON with: x, y, w, h, col_w (array of col widths)
97    pub fn add_table(
98        &mut self,
99        slide_idx: usize,
100        rows_json: &str,
101        opts_json: &str,
102    ) -> Result<(), JsValue> {
103        let rows = parse_table_rows(rows_json)?;
104        let opts = parse_table_opts(opts_json)?;
105        if let Some(slide) = self.inner.slide_mut(slide_idx) {
106            slide.add_table(rows, opts);
107        }
108        Ok(())
109    }
110
111    /// Set the background color of a slide (hex string, e.g. "FF0000")
112    pub fn set_background_color(&mut self, slide_idx: usize, color: &str) {
113        if let Some(slide) = self.inner.slide_mut(slide_idx) {
114            slide.set_background_color(color);
115        }
116    }
117
118    /// Add speaker notes to a slide
119    pub fn add_notes(&mut self, slide_idx: usize, notes: &str) {
120        if let Some(slide) = self.inner.slide_mut(slide_idx) {
121            slide.add_notes(notes);
122        }
123    }
124
125    /// Serialize the presentation to a Uint8Array (.pptx bytes)
126    pub fn write(&self) -> Result<js_sys::Uint8Array, JsValue> {
127        let bytes = self.inner.write()
128            .map_err(|e| JsValue::from_str(&e.to_string()))?;
129        Ok(js_sys::Uint8Array::from(bytes.as_slice()))
130    }
131}
132
133// ─── JSON parsing helpers ────────────────────────────────────────────────────
134
135fn get_f64(obj: &serde_json::Value, key: &str) -> Option<f64> {
136    obj.get(key)?.as_f64()
137}
138
139fn get_str<'a>(obj: &'a serde_json::Value, key: &str) -> Option<&'a str> {
140    obj.get(key)?.as_str()
141}
142
143fn get_bool(obj: &serde_json::Value, key: &str) -> Option<bool> {
144    obj.get(key)?.as_bool()
145}
146
147fn parse_text_opts(json: &str) -> Result<deckmint::objects::text::TextOptions, JsValue> {
148    let v: serde_json::Value = serde_json::from_str(json)
149        .unwrap_or(serde_json::Value::Object(serde_json::Map::new()));
150
151    let mut b = TextOptionsBuilder::new();
152    if let Some(x) = get_f64(&v, "x") { b = b.x(x); }
153    if let Some(y) = get_f64(&v, "y") { b = b.y(y); }
154    if let Some(w) = get_f64(&v, "w") { b = b.w(w); }
155    if let Some(h) = get_f64(&v, "h") { b = b.h(h); }
156    if let Some(fs) = get_f64(&v, "fontSize") { b = b.font_size(fs); }
157    if get_bool(&v, "bold") == Some(true) { b = b.bold(); }
158    if get_bool(&v, "italic") == Some(true) { b = b.italic(); }
159    if let Some(color) = get_str(&v, "color") { b = b.color(color); }
160    if let Some(fill) = get_str(&v, "fill") { b = b.fill(fill); }
161    if let Some(align) = get_str(&v, "align") {
162        let a = match align {
163            "center" | "ctr" => deckmint::AlignH::Center,
164            "right" | "r" => deckmint::AlignH::Right,
165            "justify" | "just" => deckmint::AlignH::Justify,
166            _ => deckmint::AlignH::Left,
167        };
168        b = b.align(a);
169    }
170    Ok(b.build())
171}
172
173fn parse_shape_type(s: &str) -> Result<ShapeType, JsValue> {
174    let st = match s.to_lowercase().as_str() {
175        "rect" | "rectangle" => ShapeType::Rect,
176        "ellipse" | "oval" => ShapeType::Ellipse,
177        "triangle" => ShapeType::Triangle,
178        "roundrect" | "round_rect" => ShapeType::RoundRect,
179        "diamond" => ShapeType::Diamond,
180        "pentagon" => ShapeType::Pentagon,
181        "hexagon" => ShapeType::Hexagon,
182        "heptagon" => ShapeType::Heptagon,
183        "octagon" => ShapeType::Octagon,
184        "star4" => ShapeType::Star4,
185        "star5" => ShapeType::Star5,
186        "star6" => ShapeType::Star6,
187        "star7" => ShapeType::Star7,
188        "star8" => ShapeType::Star8,
189        "star10" => ShapeType::Star10,
190        "star12" => ShapeType::Star12,
191        "star16" => ShapeType::Star16,
192        "star24" => ShapeType::Star24,
193        "star32" => ShapeType::Star32,
194        "arrow_right" | "rightarrow" => ShapeType::RightArrow,
195        "arrow_left" | "leftarrow" => ShapeType::LeftArrow,
196        "arrow_up" | "uparrow" => ShapeType::UpArrow,
197        "arrow_down" | "downarrow" => ShapeType::DownArrow,
198        "line" => ShapeType::Line,
199        _ => return Err(JsValue::from_str(&format!("Unknown shape type: {s}"))),
200    };
201    Ok(st)
202}
203
204fn parse_shape_opts(json: &str) -> Result<deckmint::objects::shape::ShapeOptions, JsValue> {
205    let v: serde_json::Value = serde_json::from_str(json)
206        .unwrap_or(serde_json::Value::Object(serde_json::Map::new()));
207
208    let mut b = ShapeOptionsBuilder::new();
209    if let Some(x) = get_f64(&v, "x") { b = b.x(x); }
210    if let Some(y) = get_f64(&v, "y") { b = b.y(y); }
211    if let Some(w) = get_f64(&v, "w") { b = b.w(w); }
212    if let Some(h) = get_f64(&v, "h") { b = b.h(h); }
213    if let Some(fill) = get_str(&v, "fill") { b = b.fill_color(fill); }
214    if get_bool(&v, "no_fill") == Some(true) { b = b.no_fill(); }
215    if let Some(lc) = get_str(&v, "line_color") { b = b.line_color(lc); }
216    if let Some(lw) = get_f64(&v, "line_width") { b = b.line_width(lw); }
217    if let Some(r) = get_f64(&v, "rotate") { b = b.rotate(r); }
218    Ok(b.build())
219}
220
221fn parse_image_opts(json: &str) -> Result<deckmint::objects::image::ImageOptions, JsValue> {
222    let v: serde_json::Value = serde_json::from_str(json)
223        .unwrap_or(serde_json::Value::Object(serde_json::Map::new()));
224
225    let mut b = ImageOptionsBuilder::new();
226    if let Some(x) = get_f64(&v, "x") { b = b.x(x); }
227    if let Some(y) = get_f64(&v, "y") { b = b.y(y); }
228    if let Some(w) = get_f64(&v, "w") { b = b.w(w); }
229    if let Some(h) = get_f64(&v, "h") { b = b.h(h); }
230    if let Some(alt) = get_str(&v, "alt_text") { b = b.alt_text(alt); }
231    if let Some(t) = get_f64(&v, "transparency") { b = b.transparency(t); }
232    if get_bool(&v, "rounding") == Some(true) { b = b.rounding(); }
233    let (opts, _, _, _) = b.build();
234    Ok(opts)
235}
236
237fn parse_table_rows(json: &str) -> Result<Vec<TableRow>, JsValue> {
238    let arr: serde_json::Value = serde_json::from_str(json)
239        .map_err(|e| JsValue::from_str(&format!("Invalid table rows JSON: {e}")))?;
240
241    let rows_arr = arr.as_array()
242        .ok_or_else(|| JsValue::from_str("rows_json must be an array"))?;
243
244    let mut rows: Vec<TableRow> = Vec::new();
245    for row_val in rows_arr {
246        let cells_arr = row_val.as_array()
247            .ok_or_else(|| JsValue::from_str("Each row must be an array of cells"))?;
248        let mut row: TableRow = Vec::new();
249        for cell_val in cells_arr {
250            if cell_val.get("merge").and_then(|v| v.as_bool()) == Some(true) {
251                row.push(TableCell::merged());
252                continue;
253            }
254            let text = cell_val.get("text")
255                .and_then(|v| v.as_str())
256                .unwrap_or("")
257                .to_string();
258            let mut cell = TableCell::new(text);
259            if let Some(colspan) = cell_val.get("colspan").and_then(|v| v.as_u64()) {
260                cell.options.colspan = Some(colspan as u32);
261            }
262            if let Some(rowspan) = cell_val.get("rowspan").and_then(|v| v.as_u64()) {
263                cell.options.rowspan = Some(rowspan as u32);
264            }
265            if let Some(fill) = cell_val.get("fill").and_then(|v| v.as_str()) {
266                cell.options.fill = Some(fill.to_string());
267            }
268            if let Some(color) = cell_val.get("color").and_then(|v| v.as_str()) {
269                cell.options.color = Some(color.to_string());
270            }
271            if let Some(bold) = cell_val.get("bold").and_then(|v| v.as_bool()) {
272                cell.options.bold = Some(bold);
273            }
274            if let Some(italic) = cell_val.get("italic").and_then(|v| v.as_bool()) {
275                cell.options.italic = Some(italic);
276            }
277            row.push(cell);
278        }
279        rows.push(row);
280    }
281    Ok(rows)
282}
283
284fn parse_table_opts(json: &str) -> Result<TableOptions, JsValue> {
285    let v: serde_json::Value = serde_json::from_str(json)
286        .unwrap_or(serde_json::Value::Object(serde_json::Map::new()));
287
288    let mut b = deckmint::objects::table::TableOptionsBuilder::new();
289    if let Some(x) = get_f64(&v, "x") { b = b.x(x); }
290    if let Some(y) = get_f64(&v, "y") { b = b.y(y); }
291    if let Some(w) = get_f64(&v, "w") { b = b.w(w); }
292    if let Some(h) = get_f64(&v, "h") { b = b.h(h); }
293    if let Some(col_w) = v.get("col_w").and_then(|v| v.as_array()) {
294        let col_widths: Vec<f64> = col_w.iter()
295            .filter_map(|v| v.as_f64())
296            .collect();
297        b = b.col_w(col_widths);
298    }
299    Ok(b.build())
300}