#[cfg(feature = "wasm")]
use wasm_bindgen::prelude::*;
#[cfg(feature = "wasm")]
use serde::{Deserialize, Serialize};
#[cfg(feature = "wasm")]
#[derive(Serialize, Deserialize, Default)]
pub struct L2TConvertOptions {
#[serde(default)]
pub pretty: bool,
#[serde(default)]
pub full_document: bool,
#[serde(default = "default_true")]
pub prefer_shorthands: bool,
#[serde(default = "default_true")]
pub frac_to_slash: bool,
#[serde(default)]
pub infty_to_oo: bool,
#[serde(default = "default_true")]
pub non_strict: bool,
#[serde(default = "default_true")]
pub optimize: bool,
}
#[cfg(feature = "wasm")]
#[derive(Serialize, Deserialize, Default)]
pub struct T2LConvertOptions {
#[serde(default)]
pub full_document: bool,
#[serde(default = "default_true")]
pub block_math_mode: bool,
#[serde(default = "default_true")]
pub expand_macros: bool,
}
#[cfg(feature = "wasm")]
#[derive(Serialize, Deserialize, Default)]
pub struct ConvertOptions {
#[serde(default)]
pub pretty: bool,
#[serde(default)]
pub preserve_comments: bool,
#[serde(default)]
pub full_document: bool,
}
#[cfg(feature = "wasm")]
fn default_true() -> bool {
true
}
#[cfg(feature = "wasm")]
fn to_js_value<T: Serialize>(value: &T) -> JsValue {
serde_wasm_bindgen::to_value(value).unwrap_or_else(|e| {
let error_obj = ConvertResult {
output: String::new(),
success: false,
error: Some(format!("Serialization error: {}", e)),
warnings: vec![],
};
serde_wasm_bindgen::to_value(&error_obj).unwrap_or(JsValue::NULL)
})
}
#[cfg(feature = "wasm")]
#[derive(Serialize, Deserialize)]
pub struct ConvertResult {
pub output: String,
pub success: bool,
pub error: Option<String>,
pub warnings: Vec<String>,
}
#[cfg(feature = "wasm")]
#[wasm_bindgen(start)]
pub fn init() {
console_error_panic_hook::set_once();
}
#[cfg(feature = "wasm")]
#[wasm_bindgen(js_name = "latexToTypst")]
pub fn latex_to_typst_wasm(input: &str) -> String {
crate::latex_to_typst(input)
}
#[cfg(feature = "wasm")]
#[wasm_bindgen(js_name = "typstToLatex")]
pub fn typst_to_latex_wasm(input: &str) -> String {
crate::typst_to_latex_with_options(
input,
&crate::T2LOptions {
math_only: true,
..Default::default()
},
)
}
#[cfg(feature = "wasm")]
#[wasm_bindgen(js_name = "latexDocumentToTypst")]
pub fn latex_document_to_typst_wasm(input: &str) -> String {
crate::latex_document_to_typst(input)
}
#[cfg(feature = "wasm")]
#[wasm_bindgen(js_name = "typstDocumentToLatex")]
pub fn typst_document_to_latex_wasm(input: &str) -> String {
crate::typst_to_latex_with_eval(input, &crate::T2LOptions::full_document())
}
#[cfg(feature = "wasm")]
#[wasm_bindgen(js_name = "latexToTypstWithOptions")]
pub fn latex_to_typst_with_options_wasm(input: &str, options: JsValue) -> JsValue {
let opts: L2TConvertOptions = serde_wasm_bindgen::from_value(options).unwrap_or_default();
let l2t_opts = crate::L2TOptions {
prefer_shorthands: opts.prefer_shorthands,
frac_to_slash: opts.frac_to_slash,
infty_to_oo: opts.infty_to_oo,
non_strict: opts.non_strict,
optimize: opts.optimize,
..Default::default()
};
let result = match std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
if opts.full_document {
crate::latex_document_to_typst_with_options(input, &l2t_opts)
} else {
let mut out = crate::latex_to_typst_with_options(input, &l2t_opts);
if opts.pretty {
out = format_typst_output(&out);
}
out
}
})) {
Ok(output) => ConvertResult {
output,
success: true,
error: None,
warnings: vec![],
},
Err(e) => {
let error_msg = if let Some(s) = e.downcast_ref::<&str>() {
format!("Conversion failed: {}", s)
} else if let Some(s) = e.downcast_ref::<String>() {
format!("Conversion failed: {}", s)
} else {
"Conversion failed: unknown error (check browser console for details)".to_string()
};
ConvertResult {
output: String::new(),
success: false,
error: Some(error_msg),
warnings: vec![],
}
}
};
to_js_value(&result)
}
#[cfg(feature = "wasm")]
#[wasm_bindgen(js_name = "typstToLatexWithOptions")]
pub fn typst_to_latex_with_options_wasm(input: &str, options: JsValue) -> JsValue {
let opts: T2LConvertOptions = serde_wasm_bindgen::from_value(options).unwrap_or_default();
let t2l_opts = crate::T2LOptions {
full_document: opts.full_document,
document_class: "article".to_string(),
title: None,
author: None,
math_only: !opts.full_document,
block_math_mode: opts.block_math_mode,
};
let result = match std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
if opts.full_document && opts.expand_macros {
crate::typst_to_latex_with_eval(input, &t2l_opts)
} else {
crate::typst_to_latex_with_options(input, &t2l_opts)
}
})) {
Ok(output) => ConvertResult {
output,
success: true,
error: None,
warnings: vec![],
},
Err(e) => {
let error_msg = if let Some(s) = e.downcast_ref::<&str>() {
format!("Conversion failed: {}", s)
} else if let Some(s) = e.downcast_ref::<String>() {
format!("Conversion failed: {}", s)
} else {
"Conversion failed: unknown error (check browser console for details)".to_string()
};
ConvertResult {
output: String::new(),
success: false,
error: Some(error_msg),
warnings: vec![],
}
}
};
to_js_value(&result)
}
#[cfg(feature = "wasm")]
#[wasm_bindgen(js_name = "detectFormat")]
pub fn detect_format_wasm(input: &str) -> String {
crate::detect_format(input).to_string()
}
#[cfg(feature = "wasm")]
#[wasm_bindgen(js_name = "getVersion")]
pub fn get_version() -> String {
env!("CARGO_PKG_VERSION").to_string()
}
#[cfg(feature = "wasm")]
#[wasm_bindgen(js_name = "tikzToCetz")]
pub fn tikz_to_cetz_wasm(input: &str) -> String {
crate::tikz::convert_tikz_to_cetz(input)
}
#[cfg(feature = "wasm")]
#[wasm_bindgen(js_name = "cetzToTikz")]
pub fn cetz_to_tikz_wasm(input: &str) -> String {
crate::tikz::convert_cetz_to_tikz(input)
}
#[cfg(feature = "wasm")]
#[wasm_bindgen(js_name = "isCetzCode")]
pub fn is_cetz_code_wasm(input: &str) -> bool {
crate::tikz::is_cetz_code(input)
}
#[cfg(feature = "wasm")]
#[wasm_bindgen(js_name = "checkLatex")]
pub fn check_latex_wasm(input: &str) -> JsValue {
use crate::diagnostics::DiagnosticLevel;
let result = crate::diagnostics::check_latex(input);
let mut errors = Vec::new();
let mut warnings = Vec::new();
let mut infos = Vec::new();
for d in &result.diagnostics {
match d.level {
DiagnosticLevel::Error => errors.push(d.message.clone()),
DiagnosticLevel::Warning => warnings.push(d.message.clone()),
DiagnosticLevel::Info => infos.push(d.message.clone()),
}
}
let summary = CheckSummary {
errors,
warnings,
infos,
has_errors: result.has_errors(),
};
to_js_value(&summary)
}
#[cfg(feature = "wasm")]
#[derive(Serialize, Deserialize)]
pub struct CheckSummary {
pub errors: Vec<String>,
pub warnings: Vec<String>,
pub infos: Vec<String>,
pub has_errors: bool,
}
#[cfg(feature = "wasm")]
#[derive(Serialize, Deserialize, Clone, Debug, Default)]
pub enum PreviewCellAlign {
Left,
#[default]
Center,
Right,
}
#[cfg(feature = "wasm")]
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct PreviewCell {
pub content: String,
#[serde(default = "default_one")]
pub colspan: usize,
#[serde(default = "default_one")]
pub rowspan: usize,
#[serde(default)]
pub align: PreviewCellAlign,
#[serde(default)]
pub is_header: bool,
}
#[cfg(feature = "wasm")]
fn default_one() -> usize {
1
}
#[cfg(feature = "wasm")]
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct PreviewRow {
pub cells: Vec<PreviewCell>,
#[serde(default)]
pub has_bottom_border: bool,
}
#[cfg(feature = "wasm")]
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct TablePreviewData {
pub rows: Vec<PreviewRow>,
#[serde(default)]
pub has_header: bool,
pub column_count: usize,
pub default_alignments: Vec<PreviewCellAlign>,
}
#[cfg(feature = "wasm")]
fn format_typst_output(input: &str) -> String {
let mut output = input.to_string();
output = output.replace(" ", " ");
output.trim().to_string()
}
#[cfg(feature = "wasm")]
#[wasm_bindgen(js_name = "previewTable")]
pub fn preview_table_wasm(input: &str, format: &str) -> JsValue {
use crate::features::tables::{parse_latex_table, parse_typst_table};
let result = match format {
"latex" => {
if let Some(table) = parse_latex_table(input) {
table_to_preview_data(&table)
} else {
return to_js_value(&TablePreviewError {
error: "Failed to parse LaTeX table".to_string(),
});
}
}
"typst" => {
if let Some(table) = parse_typst_table(input) {
table_to_preview_data(&table)
} else {
return to_js_value(&TablePreviewError {
error: "Failed to parse Typst table".to_string(),
});
}
}
_ => {
return to_js_value(&TablePreviewError {
error: format!("Unknown format: {}", format),
});
}
};
to_js_value(&result)
}
#[cfg(feature = "wasm")]
#[derive(Serialize, Deserialize)]
pub struct TablePreviewError {
pub error: String,
}
#[cfg(feature = "wasm")]
fn table_to_preview_data(table: &crate::features::tables::Table) -> TablePreviewData {
let mut rows = Vec::new();
let has_header = !table.header.is_empty();
for row in &table.header {
let preview_row = row_to_preview_row(row, true);
rows.push(preview_row);
}
for row in &table.body {
let preview_row = row_to_preview_row(row, false);
rows.push(preview_row);
}
for row in &table.footer {
let preview_row = row_to_preview_row(row, false);
rows.push(preview_row);
}
let default_alignments: Vec<PreviewCellAlign> = table
.colspecs
.iter()
.map(|spec| alignment_to_preview(&spec.alignment))
.collect();
TablePreviewData {
rows,
has_header,
column_count: table.num_cols(),
default_alignments,
}
}
#[cfg(feature = "wasm")]
fn row_to_preview_row(row: &crate::features::tables::Row, is_header: bool) -> PreviewRow {
let cells: Vec<PreviewCell> = row
.cells
.iter()
.map(|cell| cell_to_preview_cell(cell, is_header))
.collect();
PreviewRow {
cells,
has_bottom_border: row.has_bottom_border,
}
}
#[cfg(feature = "wasm")]
fn cell_to_preview_cell(cell: &crate::features::tables::Cell, is_header: bool) -> PreviewCell {
PreviewCell {
content: cell.content.clone(),
colspan: cell.colspan as usize,
rowspan: cell.rowspan as usize,
align: cell
.alignment
.map(|a| alignment_to_preview(&a))
.unwrap_or_default(),
is_header,
}
}
#[cfg(feature = "wasm")]
fn alignment_to_preview(align: &crate::features::tables::Alignment) -> PreviewCellAlign {
use crate::features::tables::Alignment;
match align {
Alignment::Left | Alignment::Default => PreviewCellAlign::Left,
Alignment::Center => PreviewCellAlign::Center,
Alignment::Right => PreviewCellAlign::Right,
}
}