1pub mod ast;
2pub mod error;
3pub mod generator;
4pub mod render;
5pub mod text;
6pub mod visual;
7
8use std::fs;
9use std::path::Path;
10use std::path::PathBuf;
11
12pub use render::{
13 PageRenderer, PdfDocumentGenerator, PdfRenderer, PixmapDocumentGenerator, PixmapRenderer,
14 SvgDocumentGenerator, SvgRenderer,
15};
16
17pub use ast::PageConfig;
18
19use generator::{
20 Document, markdown_to_document, markdown_to_document_with_base_dir,
21 markdown_to_document_with_css_and_page_config,
22};
23
24#[derive(Debug, Clone)]
37pub struct ConvertOptions {
38 pub font_family: Vec<String>,
44 pub user_css: String,
46 pub css_file: Option<PathBuf>,
49 pub strict: bool,
51 pub auto_font: bool,
57 pub page_config: Option<PageConfig>,
62}
63
64impl ConvertOptions {
65 pub fn new() -> Self {
66 Self::default()
67 }
68
69 pub fn with_font_family(mut self, families: &[&str]) -> Self {
84 self.font_family = families.iter().map(|f| f.to_string()).collect();
85 self
86 }
87
88 pub fn with_css(mut self, css: &str) -> Self {
90 self.user_css = css.to_string();
91 self
92 }
93
94 pub fn with_css_file(mut self, path: PathBuf) -> Self {
96 self.css_file = Some(path);
97 self
98 }
99
100 pub fn with_strict(mut self, strict: bool) -> Self {
102 self.strict = strict;
103 self
104 }
105
106 pub fn with_auto_font(mut self, auto_font: bool) -> Self {
108 self.auto_font = auto_font;
109 self
110 }
111
112 pub fn with_page_config(mut self, config: PageConfig) -> Self {
128 self.page_config = Some(config);
129 self
130 }
131
132 pub fn with_header(mut self, header: &str) -> Self {
147 let config = self.page_config.get_or_insert_with(PageConfig::default);
148 config.header = Some(header.to_string());
149 self
150 }
151
152 pub fn with_footer(mut self, footer: &str) -> Self {
167 let config = self.page_config.get_or_insert_with(PageConfig::default);
168 config.footer = Some(footer.to_string());
169 self
170 }
171
172 pub fn with_header_font_size(mut self, size: f32) -> Self {
176 let config = self.page_config.get_or_insert_with(PageConfig::default);
177 config.header_font_size = Some(size);
178 self
179 }
180
181 pub fn with_footer_font_size(mut self, size: f32) -> Self {
185 let config = self.page_config.get_or_insert_with(PageConfig::default);
186 config.footer_font_size = Some(size);
187 self
188 }
189}
190
191impl Default for ConvertOptions {
192 fn default() -> Self {
193 Self {
194 font_family: Vec::new(),
195 user_css: String::new(),
196 css_file: None,
197 strict: false,
198 auto_font: true,
199 page_config: None,
200 }
201 }
202}
203
204fn render_pdf(document: &Document) -> crate::error::Result<Vec<u8>> {
207 let mut pdf_gen = PdfDocumentGenerator::new("output".to_string());
208 for page in &document.pages {
209 pdf_gen.render_page(page)?;
210 }
211 pdf_gen.finalize()
212}
213
214fn render_svg(document: &Document) -> Vec<String> {
215 let mut svgs = Vec::new();
216 for page in &document.pages {
217 let mut renderer = SvgRenderer::new(page.width, page.height);
218 renderer.render_elements(&page.elements);
219 svgs.push(renderer.finalize());
220 }
221 svgs
222}
223
224fn render_png(document: &Document) -> crate::error::Result<Vec<Vec<u8>>> {
225 let mut pngs = Vec::new();
226 for page in &document.pages {
227 let mut renderer = PixmapRenderer::new_default_dpi(page.width, page.height);
228 renderer.render_elements(&page.elements);
229 pngs.push(renderer.render_to_png()?);
230 }
231 Ok(pngs)
232}
233
234fn read_markdown_file(path: &Path) -> crate::error::Result<(String, Option<PathBuf>)> {
237 let markdown = fs::read_to_string(path)?;
238 let base_dir = path.parent().map(|p| p.to_path_buf());
239 Ok((markdown, base_dir))
240}
241
242#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
246enum ScriptRange {
247 Han,
248 Japanese,
249 Korean,
250 Latin,
251 Other,
252}
253
254impl ScriptRange {
255 fn from_char(c: char) -> Self {
256 let code = c as u32;
257 match code {
258 0x3040..=0x309F | 0x30A0..=0x30FF | 0x31F0..=0x31FF => ScriptRange::Japanese,
260 0x3400..=0x4DBF
271 | 0x4E00..=0x9FFF
272 | 0xF900..=0xFAFF
273 | 0x20000..=0x2A6DF
274 | 0x2A700..=0x2B73F
275 | 0x2B740..=0x2B81F
276 | 0x2B820..=0x2CEAF
277 | 0x2CEB0..=0x2EBE0
278 | 0x2F800..=0x2FA1F => ScriptRange::Han,
279 0xAC00..=0xD7AF => ScriptRange::Korean,
281 0x0000..=0x00FF | 0x2000..=0x206F => ScriptRange::Latin,
283 _ if c.is_alphabetic() => ScriptRange::Latin,
285 _ => ScriptRange::Other,
286 }
287 }
288}
289
290fn infer_font_family(markdown: &str) -> Vec<String> {
292 let mut counts = std::collections::HashMap::new();
293 let mut in_code = false;
294 let mut in_link = false;
295
296 for line in markdown.lines() {
297 if line.trim().starts_with("```") {
299 in_code = !in_code;
300 continue;
301 }
302 if in_code {
303 continue;
304 }
305
306 let content = line.trim_start().trim_start_matches('#').trim_start();
308
309 for c in content.chars() {
310 if c == '[' {
312 in_link = true;
313 continue;
314 }
315 if in_link && c == ']' {
316 in_link = false;
317 continue;
318 }
319 if in_link {
320 continue;
321 }
322 if c == '`' {
323 continue;
324 }
325
326 let range = ScriptRange::from_char(c);
327 if range != ScriptRange::Other {
328 *counts.entry(range).or_insert(0) += 1;
329 }
330 }
331 }
332
333 let total: usize = counts.values().sum();
334 if total == 0 {
335 return vec!["serif".to_string()];
336 }
337
338 let dominant = counts
340 .iter()
341 .max_by_key(|&(_, count)| *count)
342 .map(|(k, _)| *k)
343 .unwrap_or(ScriptRange::Other);
344
345 let chinese_serif_fonts = vec![
348 "Noto Serif SC".to_string(),
349 "Source Han Serif SC".to_string(),
350 "SimSun".to_string(),
351 "SimSun-ExtB".to_string(),
352 ];
353 let chinese_sans_fonts = vec![
354 "Noto Sans SC".to_string(),
355 "Source Han Sans SC".to_string(),
356 "Microsoft YaHei".to_string(),
357 "WenQuanYi Micro Hei".to_string(),
358 ];
359
360 match dominant {
361 ScriptRange::Han => {
362 let mut fonts = chinese_serif_fonts;
363 fonts.extend(chinese_sans_fonts);
364 fonts.push("serif".to_string());
365 fonts.push("sans-serif".to_string());
366 fonts
367 }
368 ScriptRange::Japanese => vec![
369 "Noto Serif CJK JP".to_string(),
370 "Noto Serif JP".to_string(),
371 "Noto Sans CJK JP".to_string(),
372 "Noto Sans JP".to_string(),
373 "serif".to_string(),
374 "sans-serif".to_string(),
375 ],
376 ScriptRange::Korean => vec![
377 "Noto Serif CJK KR".to_string(),
378 "Noto Serif KR".to_string(),
379 "Noto Sans CJK KR".to_string(),
380 "Noto Sans KR".to_string(),
381 "serif".to_string(),
382 "sans-serif".to_string(),
383 ],
384 ScriptRange::Latin => {
385 let mut fonts = vec![
387 "Noto Serif".to_string(),
388 "Georgia".to_string(),
389 "Times New Roman".to_string(),
390 ];
391 fonts.extend(chinese_serif_fonts);
392 fonts.extend(chinese_sans_fonts);
393 fonts.push("serif".to_string());
394 fonts.push("sans-serif".to_string());
395 fonts
396 }
397 ScriptRange::Other => {
398 let mut fonts = chinese_serif_fonts;
399 fonts.extend(chinese_sans_fonts);
400 fonts.push("serif".to_string());
401 fonts.push("sans-serif".to_string());
402 fonts
403 }
404 }
405}
406
407fn resolve_user_css(
410 options: &ConvertOptions,
411 markdown: Option<&str>,
412) -> crate::error::Result<String> {
413 let file_css = match &options.css_file {
414 Some(path) => fs::read_to_string(path)?,
415 None => String::new(),
416 };
417
418 let user_has_font_css =
422 file_css.contains("font-family") || options.user_css.contains("font-family");
423
424 let font_css = if user_has_font_css || !options.font_family.is_empty() {
426 if !options.font_family.is_empty() {
427 let families: Vec<String> = options
428 .font_family
429 .iter()
430 .map(|f| {
431 if f.contains(' ') {
432 format!("\"{}\"", f)
433 } else {
434 f.clone()
435 }
436 })
437 .collect();
438 format!("body {{ font-family: {}; }}\n", families.join(", "))
439 } else {
440 String::new()
441 }
442 } else if options.auto_font {
443 if let Some(md) = markdown {
444 let families = infer_font_family(md);
445 format!(
446 "body {{ font-family: {}; }}\n",
447 families
448 .iter()
449 .map(|f| {
450 if f.contains(' ') {
451 format!("\"{}\"", f)
452 } else {
453 f.clone()
454 }
455 })
456 .collect::<Vec<_>>()
457 .join(", ")
458 )
459 } else {
460 String::new()
461 }
462 } else {
463 String::new()
464 };
465
466 let parts: Vec<&str> = [
467 font_css.as_str(),
468 options.user_css.as_str(),
469 file_css.as_str(),
470 ]
471 .into_iter()
472 .filter(|s| !s.is_empty())
473 .collect();
474
475 if parts.is_empty() {
476 Ok(String::new())
477 } else {
478 Ok(parts.join("\n"))
479 }
480}
481
482pub fn markdown_to_pdf(markdown: &str) -> crate::error::Result<Vec<u8>> {
485 render_pdf(&markdown_to_document(markdown))
486}
487
488pub fn markdown_to_svg(markdown: &str) -> crate::error::Result<Vec<String>> {
489 Ok(render_svg(&markdown_to_document(markdown)))
490}
491
492pub fn markdown_to_png(markdown: &str) -> crate::error::Result<Vec<Vec<u8>>> {
493 render_png(&markdown_to_document(markdown))
494}
495
496pub fn markdown_file_to_pdf(path: &Path) -> crate::error::Result<Vec<u8>> {
497 let (markdown, base_dir) = read_markdown_file(path)?;
498 render_pdf(&markdown_to_document_with_base_dir(&markdown, base_dir))
499}
500
501pub fn markdown_file_to_svg(path: &Path) -> crate::error::Result<Vec<String>> {
502 let (markdown, base_dir) = read_markdown_file(path)?;
503 Ok(render_svg(&markdown_to_document_with_base_dir(
504 &markdown, base_dir,
505 )))
506}
507
508pub fn markdown_file_to_png(path: &Path) -> crate::error::Result<Vec<Vec<u8>>> {
509 let (markdown, base_dir) = read_markdown_file(path)?;
510 render_png(&markdown_to_document_with_base_dir(&markdown, base_dir))
511}
512
513pub fn markdown_to_pdf_with_options(
516 markdown: &str,
517 options: &ConvertOptions,
518) -> crate::error::Result<Vec<u8>> {
519 let user_css = resolve_user_css(options, Some(markdown))?;
520 let doc = (if options.strict {
521 markdown_to_document_with_css_and_page_config(
522 markdown,
523 &user_css,
524 options.page_config.clone(),
525 None,
526 true,
527 )
528 } else {
529 markdown_to_document_with_css_and_page_config(
530 markdown,
531 &user_css,
532 options.page_config.clone(),
533 None,
534 false,
535 )
536 })
537 .map_err(crate::error::Error::CssParseError)?;
538 render_pdf(&doc)
539}
540
541pub fn markdown_to_svg_with_options(
542 markdown: &str,
543 options: &ConvertOptions,
544) -> crate::error::Result<Vec<String>> {
545 let user_css = resolve_user_css(options, Some(markdown))?;
546 let doc = (if options.strict {
547 markdown_to_document_with_css_and_page_config(
548 markdown,
549 &user_css,
550 options.page_config.clone(),
551 None,
552 true,
553 )
554 } else {
555 markdown_to_document_with_css_and_page_config(
556 markdown,
557 &user_css,
558 options.page_config.clone(),
559 None,
560 false,
561 )
562 })
563 .map_err(crate::error::Error::CssParseError)?;
564 Ok(render_svg(&doc))
565}
566
567pub fn markdown_to_png_with_options(
568 markdown: &str,
569 options: &ConvertOptions,
570) -> crate::error::Result<Vec<Vec<u8>>> {
571 let user_css = resolve_user_css(options, Some(markdown))?;
572 let doc = (if options.strict {
573 markdown_to_document_with_css_and_page_config(
574 markdown,
575 &user_css,
576 options.page_config.clone(),
577 None,
578 true,
579 )
580 } else {
581 markdown_to_document_with_css_and_page_config(
582 markdown,
583 &user_css,
584 options.page_config.clone(),
585 None,
586 false,
587 )
588 })
589 .map_err(crate::error::Error::CssParseError)?;
590 render_png(&doc)
591}
592
593pub fn markdown_file_to_pdf_with_options(
594 path: &Path,
595 options: &ConvertOptions,
596) -> crate::error::Result<Vec<u8>> {
597 let (markdown, base_dir) = read_markdown_file(path)?;
598 let user_css = resolve_user_css(options, Some(&markdown))?;
599 let doc = (if options.strict {
600 markdown_to_document_with_css_and_page_config(
601 &markdown,
602 &user_css,
603 options.page_config.clone(),
604 base_dir,
605 true,
606 )
607 } else {
608 markdown_to_document_with_css_and_page_config(
609 &markdown,
610 &user_css,
611 options.page_config.clone(),
612 base_dir,
613 false,
614 )
615 })
616 .map_err(crate::error::Error::CssParseError)?;
617 render_pdf(&doc)
618}
619
620pub fn markdown_file_to_svg_with_options(
621 path: &Path,
622 options: &ConvertOptions,
623) -> crate::error::Result<Vec<String>> {
624 let (markdown, base_dir) = read_markdown_file(path)?;
625 let user_css = resolve_user_css(options, Some(&markdown))?;
626 let doc = (if options.strict {
627 markdown_to_document_with_css_and_page_config(
628 &markdown,
629 &user_css,
630 options.page_config.clone(),
631 base_dir,
632 true,
633 )
634 } else {
635 markdown_to_document_with_css_and_page_config(
636 &markdown,
637 &user_css,
638 options.page_config.clone(),
639 base_dir,
640 false,
641 )
642 })
643 .map_err(crate::error::Error::CssParseError)?;
644 Ok(render_svg(&doc))
645}
646
647pub fn markdown_file_to_png_with_options(
648 path: &Path,
649 options: &ConvertOptions,
650) -> crate::error::Result<Vec<Vec<u8>>> {
651 let (markdown, base_dir) = read_markdown_file(path)?;
652 let user_css = resolve_user_css(options, Some(&markdown))?;
653 let doc = (if options.strict {
654 markdown_to_document_with_css_and_page_config(
655 &markdown,
656 &user_css,
657 options.page_config.clone(),
658 base_dir,
659 true,
660 )
661 } else {
662 markdown_to_document_with_css_and_page_config(
663 &markdown,
664 &user_css,
665 options.page_config.clone(),
666 base_dir,
667 false,
668 )
669 })
670 .map_err(crate::error::Error::CssParseError)?;
671 render_png(&doc)
672}