1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
use crate::Pdf;
use pulldown_cmark::{Event, Parser, Tag, TagEnd};
use std::io::Write;
use crate::Color;
pub struct MarkdownRenderer<'a, W: Write> {
pdf: &'a mut Pdf<W>,
markdown: String,
base_font_size: f64,
font_name: Option<String>,
}
impl<'a, W: Write> MarkdownRenderer<'a, W> {
pub fn new(pdf: &'a mut Pdf<W>, markdown: &str) -> Self {
Self {
pdf,
markdown: markdown.to_string(),
base_font_size: 12.0,
font_name: None,
}
}
/// Sets the base font size for the markdown text
pub fn size(mut self, size: f64) -> Self {
self.base_font_size = size;
self
}
/// Sets the font to be used for the entire markdown block
pub fn font(mut self, name: &str) -> Self {
self.font_name = Some(name.to_string());
self
}
pub fn render(self) -> std::io::Result<()> {
let parser = Parser::new(&self.markdown);
let mut current_text = String::new();
let mut in_list = false;
let mut current_heading_level = None;
for event in parser {
match event {
Event::Start(tag) => match tag {
Tag::Paragraph => current_text.clear(),
Tag::Heading { level, .. } => {
current_text.clear();
current_heading_level = Some(level as usize);
}
Tag::List(_) => {
in_list = true;
}
Tag::Item => {
current_text.clear();
}
Tag::CodeBlock(_) => {
current_text.clear();
}
_ => {}
},
Event::Text(t) | Event::Code(t) => {
current_text.push_str(&t);
}
Event::SoftBreak | Event::HardBreak => {
current_text.push(' ');
}
Event::End(tag) => match tag {
TagEnd::Paragraph => {
if !current_text.is_empty() && !in_list {
let mut txt = self.pdf.text(¤t_text)
.size(self.base_font_size)
.margin_bottom(self.base_font_size * 0.8);
if let Some(f) = &self.font_name {
txt = txt.font(f);
}
let _ = txt;
}
}
TagEnd::Heading(_level) => {
let level = current_heading_level.unwrap_or(1);
let size = self.base_font_size + ((6 - level.min(6)) as f64 * 3.0);
let mut txt = self.pdf.text(¤t_text)
.size(size)
.margin_bottom(self.base_font_size * 0.8);
if let Some(f) = &self.font_name {
txt = txt.font(f);
}
let _ = txt;
current_heading_level = None;
}
TagEnd::Item => {
let bullet = format!("- {}", current_text.trim());
let margin = self.base_font_size * 1.5;
// Setup inner properties we can use in the closure
let fs = self.base_font_size;
let fnm = self.font_name.clone();
self.pdf.row(move |row| {
let fnm1 = fnm.clone();
row.col(margin, move |p| {
let mut txt = p.text("").size(fs);
if let Some(f) = &fnm1 {
txt = txt.font(f);
}
let _ = txt;
Ok(())
});
let fnm2 = fnm.clone();
row.col(crate::pct(95.0), move |p| {
let mut txt = p.text(&bullet).size(fs).margin_bottom(fs * 0.5);
if let Some(f) = &fnm2 {
txt = txt.font(f);
}
let _ = txt;
Ok(())
});
})?;
}
TagEnd::List(_) => {
in_list = false;
}
TagEnd::CodeBlock => {
let mut txt = self.pdf.text(¤t_text)
.size(self.base_font_size * 0.9)
.color(Color::Rgb(100, 100, 100))
.margin_top(self.base_font_size * 0.5)
.margin_bottom(self.base_font_size);
if let Some(f) = &self.font_name {
txt = txt.font(f);
}
let _ = txt;
}
_ => {}
},
_ => {}
}
}
Ok(())
}
}