1#[cfg(target_arch = "wasm32")]
30use wasm_bindgen::prelude::*;
31
32#[cfg(feature = "cli")]
33use std::fs::{self, File};
34#[cfg(feature = "cli")]
35use std::io::{BufReader, IsTerminal, Read};
36
37use std::io::{BufRead, Cursor, Write};
38
39#[cfg(feature = "cli")]
40use tempfile::NamedTempFile;
41
42#[cfg(feature = "cli")]
43pub mod cli;
44mod constants;
45mod context;
46mod document;
47mod elements;
48mod errors;
49mod expr;
50mod geometry;
51#[cfg(feature = "server")]
52pub mod server;
53mod style;
54mod transform;
55mod types;
56
57pub use errors::{Error, Result};
58pub use style::{AutoStyleMode, ThemeType};
59use transform::Transformer;
60
61pub const VERSION: &str = env!("CARGO_PKG_VERSION");
63
64#[derive(Clone, Debug)]
70pub struct TransformConfig {
71 pub debug: bool,
73 pub scale: f32,
75 pub border: u16,
77 pub auto_style_mode: AutoStyleMode,
79 pub background: String, pub seed: u64,
83 pub loop_limit: u32,
85 pub var_limit: u32,
87 pub depth_limit: u32,
89 pub path_repeat_limit: u32,
91 pub add_metadata: bool,
93 pub font_size: f32,
95 pub font_family: String,
97 pub theme: ThemeType,
99 pub use_local_styles: bool,
101 pub svg_style: Option<String>,
103 pub error_mode: ErrorMode,
105}
106
107impl Default for TransformConfig {
108 fn default() -> Self {
109 Self {
110 debug: false,
111 scale: 1.0,
112 border: 5,
113 auto_style_mode: AutoStyleMode::default(),
114 background: "default".to_owned(),
115 seed: 0,
116 loop_limit: 1000,
117 var_limit: 1024,
118 depth_limit: 100,
119 path_repeat_limit: 10000,
120 add_metadata: false,
121 font_size: 3.0,
122 font_family: "sans-serif".to_owned(),
123 theme: ThemeType::default(),
124 use_local_styles: false,
125 svg_style: None,
126 error_mode: ErrorMode::default(),
127 }
128 }
129}
130
131#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
132#[cfg_attr(feature = "cli", derive(clap::ValueEnum))]
133pub enum ErrorMode {
134 #[default]
136 Strict,
137 Warn,
139 Ignore,
141}
142
143impl std::str::FromStr for ErrorMode {
144 type Err = Error;
145
146 fn from_str(s: &str) -> Result<Self> {
147 match s {
148 "strict" => Ok(ErrorMode::Strict),
149 "warn" => Ok(ErrorMode::Warn),
150 "ignore" => Ok(ErrorMode::Ignore),
151 _ => Err(Error::InvalidValue(
152 "error-mode must be 'strict', 'warn', or 'ignore'".to_string(),
153 s.to_string(),
154 )),
155 }
156 }
157}
158
159pub fn transform_stream(
165 reader: &mut dyn BufRead,
166 writer: &mut dyn Write,
167 config: &TransformConfig,
168) -> Result<()> {
169 let mut t = Transformer::from_config(config);
170 t.transform(reader, writer)
171}
172
173#[cfg(feature = "cli")]
178pub fn transform_file(input: &str, output: &str, cfg: &TransformConfig) -> Result<()> {
179 let mut in_reader = if input == "-" {
180 let mut stdin = std::io::stdin().lock();
181 if stdin.is_terminal() {
182 let mut buf = Vec::new();
188 stdin
189 .read_to_end(&mut buf)
190 .expect("stdin should be readable to EOF");
191 Box::new(BufReader::new(Cursor::new(buf))) as Box<dyn BufRead>
192 } else {
193 Box::new(stdin) as Box<dyn BufRead>
194 }
195 } else {
196 Box::new(BufReader::new(File::open(input).map_err(Error::Io)?)) as Box<dyn BufRead>
197 };
198
199 if output == "-" {
200 transform_stream(&mut in_reader, &mut std::io::stdout(), cfg)?;
201 } else {
202 let mut out_temp = NamedTempFile::new().map_err(Error::Io)?;
203 transform_stream(&mut in_reader, &mut out_temp, cfg)?;
204 fs::copy(out_temp.path(), output).map_err(Error::Io)?;
208 }
209
210 Ok(())
211}
212
213#[cfg_attr(target_arch = "wasm32", wasm_bindgen)]
217pub fn transform_string(input: String, add_metadata: bool) -> core::result::Result<String, String> {
218 let cfg = TransformConfig {
219 add_metadata,
220 ..Default::default()
221 };
222 transform_str(input, &cfg).map_err(|e| e.to_string())
223}
224
225pub fn transform_str<T: Into<String>>(input: T, cfg: &TransformConfig) -> Result<String> {
226 let input = input.into();
227
228 let mut input = Cursor::new(input);
229 let mut output: Vec<u8> = vec![];
230
231 transform_stream(&mut input, &mut output, cfg)?;
232
233 Ok(String::from_utf8(output).expect("Non-UTF8 output generated"))
234}
235
236pub fn transform_str_default<T: Into<String>>(input: T) -> Result<String> {
240 transform_str(input, &TransformConfig::default())
241}
242
243#[cfg(feature = "json")]
245pub mod json_api {
246 use super::{TransformConfig, transform_str};
247 use serde_derive::{Deserialize, Serialize};
248
249 pub const JSON_API_VERSION: u32 = 1;
250
251 #[derive(Debug, Deserialize)]
252 pub struct TransformRequest {
253 pub version: u32,
254 pub input: String,
255 #[serde(default)]
256 pub config: RequestConfig,
257 }
258
259 #[derive(Debug, Default, Deserialize)]
260 pub struct RequestConfig {
261 #[serde(default)]
262 pub add_metadata: bool,
263 }
264
265 impl From<RequestConfig> for TransformConfig {
266 fn from(config: RequestConfig) -> Self {
267 TransformConfig {
268 add_metadata: config.add_metadata,
269 ..Default::default()
270 }
271 }
272 }
273
274 #[derive(Debug, Serialize)]
275 pub struct TransformResponse {
276 pub version: u32,
277 #[serde(skip_serializing_if = "Option::is_none")]
278 pub svg: Option<String>,
279 #[serde(skip_serializing_if = "Option::is_none")]
280 pub error: Option<String>,
281 #[serde(skip_serializing_if = "Vec::is_empty")]
282 pub warnings: Vec<String>,
283 }
284
285 impl TransformResponse {
286 pub fn success(svg: String) -> Self {
287 Self {
288 version: JSON_API_VERSION,
289 svg: Some(svg),
290 error: None,
291 warnings: vec![],
292 }
293 }
294
295 pub fn error(message: String) -> Self {
296 Self {
297 version: JSON_API_VERSION,
298 svg: None,
299 error: Some(message),
300 warnings: vec![],
301 }
302 }
303 }
304
305 pub fn transform_json_impl(input: &str) -> String {
310 let result: TransformResponse = match serde_json::from_str::<TransformRequest>(input) {
311 Ok(request) => {
312 if request.version != JSON_API_VERSION {
313 TransformResponse::error(format!(
314 "Unsupported API version: {} (expected {})",
315 request.version, JSON_API_VERSION
316 ))
317 } else {
318 let config: TransformConfig = request.config.into();
319 match transform_str(request.input, &config) {
320 Ok(svg) => TransformResponse::success(svg),
321 Err(e) => TransformResponse::error(e.to_string()),
322 }
323 }
324 }
325 Err(e) => TransformResponse::error(format!("Invalid JSON request: {e}")),
326 };
327 serde_json::to_string(&result).expect("Failed to serialize response")
328 }
329}
330
331#[cfg(all(feature = "json", target_arch = "wasm32"))]
338#[wasm_bindgen]
339pub fn transform_json(input: String) -> String {
340 json_api::transform_json_impl(&input)
341}
342
343#[cfg(all(test, feature = "json"))]
344mod json_tests {
345 use super::json_api::*;
346
347 #[test]
348 fn test_json_transform_success() {
349 let request = r#"{"version": 1, "input": "<svg><rect wh=\"10\"/></svg>", "config": {}}"#;
350 let response = transform_json_impl(request);
351 let parsed: serde_json::Value = serde_json::from_str(&response).unwrap();
352
353 assert_eq!(parsed["version"], 1);
354 assert!(parsed["svg"].as_str().unwrap().contains("<svg"));
355 assert!(parsed["error"].is_null());
356 }
357
358 #[test]
359 fn test_json_transform_error() {
360 let request = r#"{"version": 1, "input": "<svg><invalid", "config": {}}"#;
361 let response = transform_json_impl(request);
362 let parsed: serde_json::Value = serde_json::from_str(&response).unwrap();
363
364 assert_eq!(parsed["version"], 1);
365 assert!(parsed["svg"].is_null());
366 assert!(parsed["error"].as_str().is_some());
367 }
368
369 #[test]
370 fn test_json_invalid_version() {
371 let request = r#"{"version": 999, "input": "<svg/>", "config": {}}"#;
372 let response = transform_json_impl(request);
373 let parsed: serde_json::Value = serde_json::from_str(&response).unwrap();
374
375 assert_eq!(parsed["version"], 1);
376 assert!(
377 parsed["error"]
378 .as_str()
379 .unwrap()
380 .contains("Unsupported API version")
381 );
382 }
383
384 #[test]
385 fn test_json_invalid_request() {
386 let request = r#"not valid json"#;
387 let response = transform_json_impl(request);
388 let parsed: serde_json::Value = serde_json::from_str(&response).unwrap();
389
390 assert_eq!(parsed["version"], 1);
391 assert!(
392 parsed["error"]
393 .as_str()
394 .unwrap()
395 .contains("Invalid JSON request")
396 );
397 }
398}