sable-markdown/src/render/block.rs@main
raw
1use heck::ToTitleCase as _;
2use pretty::{Arena, DocAllocator, DocBuilder};
3
4use crate::{
5 ast::{
6 Alignment, Block, Callout, CodeBlock, FootnoteDefinition, HeadingKind, List,
7 ListBulletKind, ListItem, ListKind, ListOrderedKindOptions, SetextHeading, Table, TableRow,
8 TaskState,
9 },
10 render::{Config, Context, ToDoc, highlighter, util::ArenaExt as _},
11};
12
13impl<'a> ToDoc<'a> for Vec<Block> {
14 fn to_doc(
15 &self,
16 config: &'a Config<'a>,
17 context: &'a Context,
18 arena: &'a Arena<'a>,
19 ) -> DocBuilder<'a, Arena<'a>, ()> {
20 arena.concat(
21 self.iter()
22 .map(|block| block.to_doc(config, context, arena)),
23 )
24 }
25}
26
27impl<'a> ToDoc<'a> for Vec<&Block> {
28 fn to_doc(
29 &self,
30 config: &'a Config<'a>,
31 context: &'a Context,
32 arena: &'a Arena<'a>,
33 ) -> DocBuilder<'a, Arena<'a>, ()> {
34 arena.concat(
35 self.iter()
36 .map(|block| block.to_doc(config, context, arena)),
37 )
38 }
39}
40
41impl<'a> ToDoc<'a> for Block {
42 fn to_doc(
43 &self,
44 config: &'a Config<'a>,
45 context: &'a Context,
46 arena: &'a Arena<'a>,
47 ) -> DocBuilder<'a, Arena<'a>, ()> {
48 match self {
49 Self::Paragraph(inlines) => {
50 let inner = arena.concat(
51 inlines
52 .iter()
53 .map(|inline| inline.to_doc(config, context, arena)),
54 );
55 arena.tag("p", vec![], inner)
56 }
57 Self::Heading(v) => {
59 let htag = match v.kind {
60 HeadingKind::Atx(1) | HeadingKind::Setext(SetextHeading::Level1) => "h1",
61 HeadingKind::Atx(2) | HeadingKind::Setext(SetextHeading::Level2) => "h2",
62 HeadingKind::Atx(3) => "h3",
63 HeadingKind::Atx(4) => "h4",
64 HeadingKind::Atx(5) => "h5",
65 HeadingKind::Atx(_) => "h6",
66 };
67 let inner = arena.concat(
68 v.content
69 .iter()
70 .map(|inline| inline.to_doc(config, context, arena)),
71 );
72 arena.tag(htag, vec![], inner)
73 }
74 Self::ThematicBreak => arena.tag("hr", vec![], arena.nil()),
75 Self::BlockQuote(inner) => {
76 let inner = arena.concat(
77 inner
78 .iter()
79 .map(|inline| inline.to_doc(config, context, arena)),
80 );
81 arena.tag("blockquote", vec![], inner)
82 }
83 Self::List(v) => v.to_doc(config, context, arena),
84 Self::CodeBlock(v) => v.to_doc(config, context, arena),
85 Self::HtmlBlock(html) => arena.text(html.clone()),
86 Self::Table(v) => v.to_doc(config, context, arena),
87 Self::FootnoteDefinition(def) => def.to_doc(config, context, arena),
88 Self::Callout(callout) => callout.to_doc(config, context, arena),
89 Self::Empty | Self::Definition(_) => arena.nil(),
90 }
91 }
92}
93
94impl<'a> ToDoc<'a> for List {
95 fn to_doc(
96 &self,
97 config: &'a Config<'a>,
98 context: &'a Context,
99 arena: &'a Arena<'a>,
100 ) -> DocBuilder<'a, Arena<'a>, ()> {
101 let items = arena.concat(
102 self.items
103 .iter()
104 .map(|item| item.to_doc(config, context, arena)),
105 );
106 match self.kind {
107 ListKind::Ordered(ListOrderedKindOptions { start }) => {
108 arena.tag("ol", vec![("start".to_owned(), format!("{start}"))], items)
109 }
110 ListKind::Bullet(kind) => {
111 let style = match kind {
112 ListBulletKind::Dash => "list-kind-dash",
113 ListBulletKind::Star => "list-kind-star",
114 ListBulletKind::Plus => "list-kind-plus",
115 };
116 arena.tag("ul", vec![("class".to_owned(), style.to_owned())], items)
117 }
118 }
119 }
120}
121
122impl<'a> ToDoc<'a> for ListItem {
123 fn to_doc(
124 &self,
125 config: &'a Config<'a>,
126 context: &'a Context,
127 arena: &'a Arena<'a>,
128 ) -> DocBuilder<'a, Arena<'a>, ()> {
129 let task = match self.task {
130 Some(TaskState::Complete) => arena.tag(
131 "span",
132 vec![("class".to_owned(), "list-task-complete".to_owned())],
133 arena.text("[X] "),
134 ),
135 Some(TaskState::Incomplete) => arena.tag(
136 "span",
137 vec![("class".to_owned(), "list-task-incomplete".to_owned())],
138 arena.text("[ ] "),
139 ),
140 None => arena.nil(),
141 };
142 let content = task.append(
143 arena.concat(
144 self.blocks
145 .iter()
146 .map(|block| block.to_doc(config, context, arena)),
147 ),
148 );
149
150 arena.tag("li", vec![], content)
151 }
152}
153
154impl<'a> ToDoc<'a> for CodeBlock {
155 fn to_doc(
156 &self,
157 _config: &'a Config<'a>,
158 _context: &'a Context,
159 arena: &'a Arena<'a>,
160 ) -> DocBuilder<'a, Arena<'a>, ()> {
161 let code = highlighter::highlight(self);
162
163 arena.tag("pre", vec![], arena.tag("code", vec![], arena.text(code)))
164 }
165}
166
167impl<'a> ToDoc<'a> for Table {
168 fn to_doc(
169 &self,
170 config: &'a Config<'a>,
171 context: &'a Context,
172 arena: &'a Arena<'a>,
173 ) -> DocBuilder<'a, Arena<'a>, ()> {
174 let first_row = table_row_to_doc(
175 config,
176 context,
177 arena,
178 self.rows.first().unwrap(),
179 "th",
180 &self.alignments,
181 );
182 let mut acc = arena.nil();
183 for row in self.rows.iter().skip(1) {
184 acc = acc.append(table_row_to_doc(
185 config,
186 context,
187 arena,
188 row,
189 "td",
190 &self.alignments,
191 ));
192 }
193
194 let content = arena
195 .tag("thead", vec![], first_row)
196 .append(arena.tag("tbody", vec![], acc));
197
198 arena.tag("table", vec![], content)
199 }
200}
201
202fn table_row_to_doc<'a>(
203 config: &'a Config<'a>,
204 context: &'a Context,
205 arena: &'a Arena<'a>,
206 row: &TableRow,
207 row_tag: &'static str,
208 alignments: &[Alignment],
209) -> DocBuilder<'a, Arena<'a>, ()> {
210 let mut acc = arena.nil();
211 for (i, cell) in row.iter().enumerate() {
212 let alignment = match alignments.get(i) {
213 None | Some(Alignment::Left | Alignment::None) => "left",
214 Some(Alignment::Right) => "right",
215 Some(Alignment::Center) => "center",
216 };
217 let alignment_class = format!("table-align-{alignment}");
218 let attributes = vec![("class".to_owned(), alignment_class)];
219 acc = acc.append(arena.tag(row_tag, attributes, cell.to_doc(config, context, arena)));
220 }
221
222 arena.tag("tr", vec![], acc)
223}
224
225impl<'a> ToDoc<'a> for FootnoteDefinition {
226 fn to_doc(
227 &self,
228 config: &'a Config<'a>,
229 context: &'a Context,
230 arena: &'a Arena<'a>,
231 ) -> DocBuilder<'a, Arena<'a>, ()> {
232 let Some(index) = context.get_footnote_index(&self.label) else {
233 return arena.nil();
234 };
235
236 arena.tag(
237 "div",
238 vec![
239 ("class".to_owned(), "footnote-definition".to_owned()),
240 ("id".to_owned(), format!("footnote-{index}")),
241 ],
242 arena
243 .tag(
244 "span",
245 vec![("class".to_owned(), "footnote-definition-index".to_owned())],
246 arena.text(format!("{index}. ")),
247 )
248 .append(arena.tag(
249 "span",
250 vec![("class".to_owned(), "footnote-definition-content".to_owned())],
251 self.blocks.to_doc(config, context, arena),
252 )),
253 )
254 }
255}
256
257impl<'a> ToDoc<'a> for Callout {
258 fn to_doc(
259 &self,
260 config: &'a Config<'a>,
261 context: &'a Context,
262 arena: &'a Arena<'a>,
263 ) -> DocBuilder<'a, Arena<'a>, ()> {
264 arena.tag(
265 "div",
266 vec![
267 ("class".to_owned(), "callout".to_owned()),
268 ("data-callout".to_owned(), "info".to_owned()),
269 ],
270 arena
271 .tag(
272 "div",
273 vec![("class".to_owned(), "callout-title".to_owned())],
274 arena
275 .tag(
276 "div",
277 vec![("class".to_owned(), "callout-title-icon".to_owned())],
278 arena.nil(),
279 )
280 .append(
281 arena.tag(
282 "div",
283 vec![("class".to_owned(), "callout-title-inner".to_owned())],
284 arena.text(
285 self.title
286 .clone()
287 .unwrap_or_else(|| self.level.to_title_case()),
288 ),
289 ),
290 ),
291 )
292 .append(arena.tag(
293 "div",
294 vec![("class".to_owned(), "callout-content".to_owned())],
295 self.blocks.to_doc(config, context, arena),
296 )),
297 )
298 }
299}
300