1use std::collections::BTreeMap;
4use std::mem;
5use std::ops::Range;
6
7use pulldown_cmark::{CodeBlockKind, Event, HeadingLevel, Options, Parser, Tag, TagEnd};
8
9use crate::backend::{
10 MarkupBackend, MarkupDecodeOptions, MarkupEncodeOptions, MarkupError, MarkupFidelity,
11 MarkupLoss,
12};
13use crate::markdown_writer::MarkdownEncoder;
14use crate::markup::{BackendId, Inline, MarkupBlock, MarkupDoc, MathSource, SourceDoc, Span};
15
16type MarkdownEvent = (Event<'static>, Range<usize>);
17
18#[derive(Clone, Debug, Default)]
20pub struct MarkdownBackend;
21
22impl MarkupBackend for MarkdownBackend {
23 fn id(&self) -> BackendId {
24 markdown_id()
25 }
26
27 fn decode(
28 &self,
29 input: &str,
30 opts: &MarkupDecodeOptions,
31 ) -> Result<(MarkupDoc, MarkupFidelity), MarkupError> {
32 let events = Parser::new_ext(input, markdown_options())
33 .into_offset_iter()
34 .map(|(event, range)| (event.into_static(), range))
35 .collect();
36 let mut parser = MarkdownParser::new(input, events, opts);
37 let blocks = parser.parse_blocks_until(|_| false);
38 let title = blocks.iter().find_map(|block| match block {
39 MarkupBlock::Heading { level: 1, text, .. } => Some(inline_plain_text(text)),
40 _ => None,
41 });
42 let source = opts.preserve_source.then(|| SourceDoc {
43 backend: markdown_id(),
44 text: input.to_owned(),
45 });
46 Ok((
47 MarkupDoc {
48 title,
49 blocks,
50 attrs: BTreeMap::new(),
51 source,
52 },
53 parser.fidelity,
54 ))
55 }
56
57 fn encode(
58 &self,
59 doc: &MarkupDoc,
60 opts: &MarkupEncodeOptions,
61 ) -> Result<(String, MarkupFidelity), MarkupError> {
62 let mut encoder = MarkdownEncoder::new(opts);
63 let source = encoder.write_doc(doc);
64 if opts.fail_on_loss && !encoder.fidelity.dropped.is_empty() {
65 return Err(MarkupError::Encode(format!(
66 "markdown encode dropped {} raw fragment(s)",
67 encoder.fidelity.dropped.len()
68 )));
69 }
70 Ok((source, encoder.fidelity))
71 }
72}
73
74struct MarkdownParser<'a> {
75 input: &'a str,
76 events: Vec<MarkdownEvent>,
77 index: usize,
78 preserve_raw: bool,
79 fidelity: MarkupFidelity,
80}
81
82impl<'a> MarkdownParser<'a> {
83 fn new(input: &'a str, events: Vec<MarkdownEvent>, opts: &MarkupDecodeOptions) -> Self {
84 Self {
85 input,
86 events,
87 index: 0,
88 preserve_raw: opts.preserve_raw,
89 fidelity: MarkupFidelity::exact(markdown_id()),
90 }
91 }
92
93 fn parse_blocks_until<F>(&mut self, stop: F) -> Vec<MarkupBlock>
94 where
95 F: Fn(&TagEnd) -> bool + Copy,
96 {
97 let mut blocks = Vec::new();
98 let mut loose = Vec::new();
99 while let Some((event, range)) = self.next() {
100 match event {
101 Event::End(end) if stop(&end) => {
102 self.index -= 1;
103 break;
104 }
105 Event::End(_) => self.flush_loose(&mut blocks, &mut loose),
106 Event::Start(tag) => {
107 self.flush_loose(&mut blocks, &mut loose);
108 self.push_block(tag, range, &mut blocks);
109 }
110 Event::DisplayMath(text) => {
111 self.flush_loose(&mut blocks, &mut loose);
112 blocks.push(MarkupBlock::MathBlock {
113 source: tex_math(text),
114 span: Some(span(range.start, range.end)),
115 });
116 }
117 Event::Rule => {
118 self.flush_loose(&mut blocks, &mut loose);
119 if let Some(raw) = self.raw_block(self.slice(&range), "rule", &range) {
120 blocks.push(raw);
121 }
122 }
123 other => self.push_inline_event(other, range, &mut loose),
124 }
125 }
126 self.flush_loose(&mut blocks, &mut loose);
127 blocks
128 }
129
130 fn push_block(
131 &mut self,
132 tag: Tag<'static>,
133 range: Range<usize>,
134 blocks: &mut Vec<MarkupBlock>,
135 ) {
136 match tag {
137 Tag::Paragraph => blocks.push(self.parse_paragraph(range, Vec::new())),
138 Tag::Heading { level, id, .. } => blocks.push(self.parse_heading(level, id, range)),
139 Tag::CodeBlock(kind) => blocks.push(self.parse_code_block(kind, range)),
140 Tag::BlockQuote(_) => blocks.push(self.parse_quote(range)),
141 Tag::List(start) => blocks.push(self.parse_list(start.is_some(), range)),
142 Tag::Table(_) => blocks.push(self.parse_table(range)),
143 Tag::HtmlBlock => {
144 if let Some(raw) = self.raw_container(range, TagEnd::HtmlBlock, "html-block") {
145 blocks.push(raw);
146 }
147 }
148 Tag::FootnoteDefinition(_) => {
149 if let Some(raw) =
150 self.raw_container(range, TagEnd::FootnoteDefinition, "footnote-definition")
151 {
152 blocks.push(raw);
153 }
154 }
155 other => {
156 let end = other.to_end();
157 if let Some(raw) = self.raw_container(range, end, "unsupported-block") {
158 blocks.push(raw);
159 }
160 }
161 }
162 }
163
164 fn parse_paragraph(&mut self, start: Range<usize>, mut prefix: Vec<Inline>) -> MarkupBlock {
165 let (mut content, end) = self.collect_inlines_until(|end| *end == TagEnd::Paragraph);
166 prefix.append(&mut content);
167 if prefix.len() == 1
168 && matches!(prefix[0], Inline::Math(_))
169 && self
170 .input
171 .get(start.start..end.end)
172 .map(str::trim)
173 .is_some_and(|source| source.starts_with("$$") && source.ends_with("$$"))
174 {
175 let Inline::Math(source) = prefix.remove(0) else {
176 unreachable!();
177 };
178 return MarkupBlock::MathBlock {
179 source,
180 span: Some(span(start.start, end.end)),
181 };
182 }
183 MarkupBlock::Paragraph {
184 content: prefix,
185 span: Some(span(start.start, end.end)),
186 }
187 }
188
189 fn parse_heading(
190 &mut self,
191 level: HeadingLevel,
192 id: Option<pulldown_cmark::CowStr<'static>>,
193 start: Range<usize>,
194 ) -> MarkupBlock {
195 let (text, end) = self.collect_inlines_until(|end| matches!(end, TagEnd::Heading(_)));
196 MarkupBlock::Heading {
197 level: heading_level(level),
198 text,
199 id: id.map(|value| value.to_string()),
200 span: Some(span(start.start, end.end)),
201 }
202 }
203
204 fn parse_code_block(
205 &mut self,
206 kind: CodeBlockKind<'static>,
207 start: Range<usize>,
208 ) -> MarkupBlock {
209 let mut code = String::new();
210 let mut end = start.clone();
211 while let Some((event, range)) = self.next() {
212 end = range.clone();
213 match event {
214 Event::End(TagEnd::CodeBlock) => break,
215 Event::Text(text) => code.push_str(&text),
216 Event::Code(text) => code.push_str(&text),
217 Event::SoftBreak | Event::HardBreak => code.push('\n'),
218 _ => {}
219 }
220 }
221 let lang = match kind {
222 CodeBlockKind::Fenced(info) => info.split_whitespace().next().map(str::to_owned),
223 CodeBlockKind::Indented => None,
224 };
225 if matches!(lang.as_deref(), Some("math" | "tex")) {
226 MarkupBlock::MathBlock {
227 source: MathSource {
228 notation: "tex".to_owned(),
229 text: code,
230 },
231 span: Some(span(start.start, end.end)),
232 }
233 } else {
234 MarkupBlock::CodeBlock {
235 lang,
236 code,
237 span: Some(span(start.start, end.end)),
238 }
239 }
240 }
241
242 fn parse_quote(&mut self, start: Range<usize>) -> MarkupBlock {
243 let blocks = self.parse_blocks_until(|end| matches!(end, TagEnd::BlockQuote(_)));
244 let end = self.consume_end(|end| matches!(end, TagEnd::BlockQuote(_)), &start);
245 MarkupBlock::Quote {
246 blocks,
247 span: Some(span(start.start, end.end)),
248 }
249 }
250
251 fn parse_list(&mut self, ordered: bool, start: Range<usize>) -> MarkupBlock {
252 let mut items = Vec::new();
253 let mut end = start.clone();
254 while let Some((event, range)) = self.next() {
255 end = range.clone();
256 match event {
257 Event::End(TagEnd::List(_)) => break,
258 Event::Start(Tag::Item) => {
259 let item = self.parse_blocks_until(|end| *end == TagEnd::Item);
260 end = self.consume_end(|end| *end == TagEnd::Item, &range);
261 items.push(item);
262 }
263 _ => {}
264 }
265 }
266 MarkupBlock::List {
267 ordered,
268 items,
269 span: Some(span(start.start, end.end)),
270 }
271 }
272
273 fn parse_table(&mut self, start: Range<usize>) -> MarkupBlock {
274 let mut header = Vec::new();
275 let mut rows = Vec::new();
276 let mut current_row: Option<Vec<Vec<Inline>>> = None;
277 let mut in_head = false;
278 let mut end = start.clone();
279 while let Some((event, range)) = self.next() {
280 end = range.clone();
281 match event {
282 Event::End(TagEnd::Table) => break,
283 Event::Start(Tag::TableHead) => {
284 in_head = true;
285 current_row = Some(Vec::new());
286 }
287 Event::End(TagEnd::TableHead) => {
288 if let Some(row) = current_row.take() {
289 header = row;
290 }
291 in_head = false;
292 }
293 Event::Start(Tag::TableRow) => current_row = Some(Vec::new()),
294 Event::End(TagEnd::TableRow) => {
295 if let Some(row) = current_row.take() {
296 if in_head {
297 header = row;
298 } else {
299 rows.push(row);
300 }
301 }
302 }
303 Event::Start(Tag::TableCell) => {
304 let (cell, cell_end) =
305 self.collect_inlines_until(|end| *end == TagEnd::TableCell);
306 end = cell_end;
307 current_row.get_or_insert_with(Vec::new).push(cell);
308 }
309 _ => {}
310 }
311 }
312 MarkupBlock::Table {
313 header,
314 rows,
315 span: Some(span(start.start, end.end)),
316 }
317 }
318
319 fn collect_inlines_until<F>(&mut self, stop: F) -> (Vec<Inline>, Range<usize>)
320 where
321 F: Fn(&TagEnd) -> bool + Copy,
322 {
323 let mut items = Vec::new();
324 let mut end = self.current_end();
325 while let Some((event, range)) = self.next() {
326 end = range.clone();
327 match event {
328 Event::End(tag_end) if stop(&tag_end) => break,
329 other => self.push_inline_event(other, range, &mut items),
330 }
331 }
332 (items, end)
333 }
334
335 fn push_inline_event(
336 &mut self,
337 event: Event<'static>,
338 range: Range<usize>,
339 items: &mut Vec<Inline>,
340 ) {
341 match event {
342 Event::Text(text) => items.push(Inline::Text(text.to_string())),
343 Event::Code(text) => items.push(Inline::Code(text.to_string())),
344 Event::InlineMath(text) | Event::DisplayMath(text) => {
345 items.push(Inline::Math(tex_math(text)));
346 }
347 Event::SoftBreak | Event::HardBreak => items.push(Inline::Text("\n".to_owned())),
348 Event::Html(text) | Event::InlineHtml(text) => {
349 if let Some(raw) = self.raw_inline(text.to_string(), "html") {
350 items.push(raw);
351 }
352 }
353 Event::FootnoteReference(label) => {
354 if let Some(raw) = self.raw_inline(format!("[^{label}]"), "footnote-reference") {
355 items.push(raw);
356 }
357 }
358 Event::TaskListMarker(checked) => {
359 let marker = if checked { "[x] " } else { "[ ] " };
360 if let Some(raw) = self.raw_inline(marker.to_owned(), "task-list-marker") {
361 items.push(raw);
362 }
363 }
364 Event::Rule => {
365 if let Some(raw) = self.raw_inline(self.slice(&range), "rule") {
366 items.push(raw);
367 }
368 }
369 Event::Start(Tag::Emphasis) => {
370 let (children, _) = self.collect_inlines_until(|end| *end == TagEnd::Emphasis);
371 items.push(Inline::Emph(children));
372 }
373 Event::Start(Tag::Strong) => {
374 let (children, _) = self.collect_inlines_until(|end| *end == TagEnd::Strong);
375 items.push(Inline::Strong(children));
376 }
377 Event::Start(Tag::Link { dest_url, .. }) => {
378 let (label, _) = self.collect_inlines_until(|end| *end == TagEnd::Link);
379 items.push(Inline::Link {
380 label,
381 target: dest_url.to_string(),
382 });
383 }
384 Event::Start(tag) => {
385 let end = tag.to_end();
386 if let Some(raw) = self.raw_inline_container(range, end, "unsupported-inline") {
387 items.push(raw);
388 }
389 }
390 Event::End(_) => {}
391 }
392 }
393
394 fn raw_container(
395 &mut self,
396 start: Range<usize>,
397 target: TagEnd,
398 path: &str,
399 ) -> Option<MarkupBlock> {
400 let (raw, end) = self.consume_raw_container(start.clone(), target);
401 self.raw_block(raw, path, &(start.start..end))
402 }
403
404 fn raw_inline_container(
405 &mut self,
406 start: Range<usize>,
407 target: TagEnd,
408 path: &str,
409 ) -> Option<Inline> {
410 let (raw, _) = self.consume_raw_container(start, target);
411 self.raw_inline(raw, path)
412 }
413
414 fn consume_raw_container(&mut self, start: Range<usize>, target: TagEnd) -> (String, usize) {
415 let mut depth = 1usize;
416 let mut end = start.end;
417 while let Some((event, range)) = self.next() {
418 end = range.end;
419 match event {
420 Event::Start(tag) if tag.to_end() == target => depth += 1,
421 Event::End(tag_end) if tag_end == target => {
422 depth -= 1;
423 if depth == 0 {
424 break;
425 }
426 }
427 _ => {}
428 }
429 }
430 (
431 self.input.get(start.start..end).unwrap_or("").to_owned(),
432 end,
433 )
434 }
435
436 fn raw_block(&mut self, raw: String, path: &str, range: &Range<usize>) -> Option<MarkupBlock> {
437 if self.preserve_raw {
438 self.fidelity.preserved_raw.push(raw.clone());
439 Some(MarkupBlock::Raw {
440 backend: markdown_id(),
441 text: raw,
442 span: Some(span(range.start, range.end)),
443 })
444 } else {
445 self.drop_raw(path, "unsupported markdown block");
446 None
447 }
448 }
449
450 fn raw_inline(&mut self, raw: String, path: &str) -> Option<Inline> {
451 if self.preserve_raw {
452 self.fidelity.preserved_raw.push(raw.clone());
453 Some(Inline::Raw {
454 backend: markdown_id(),
455 text: raw,
456 })
457 } else {
458 self.drop_raw(path, "unsupported markdown inline");
459 None
460 }
461 }
462
463 fn drop_raw(&mut self, path: &str, reason: &str) {
464 self.fidelity.dropped.push(MarkupLoss {
465 path: path.to_owned(),
466 reason: reason.to_owned(),
467 });
468 }
469
470 fn flush_loose(&mut self, blocks: &mut Vec<MarkupBlock>, loose: &mut Vec<Inline>) {
471 if !loose.is_empty() {
472 blocks.push(MarkupBlock::Paragraph {
473 content: mem::take(loose),
474 span: None,
475 });
476 }
477 }
478
479 fn consume_end<F>(&mut self, stop: F, fallback: &Range<usize>) -> Range<usize>
480 where
481 F: Fn(&TagEnd) -> bool,
482 {
483 match self.next() {
484 Some((Event::End(end), range)) if stop(&end) => range,
485 Some(_) => fallback.clone(),
486 None => fallback.clone(),
487 }
488 }
489
490 fn next(&mut self) -> Option<MarkdownEvent> {
491 let event = self.events.get(self.index).cloned();
492 if event.is_some() {
493 self.index += 1;
494 }
495 event
496 }
497
498 fn current_end(&self) -> Range<usize> {
499 self.events
500 .get(self.index.saturating_sub(1))
501 .map(|(_, range)| range.clone())
502 .unwrap_or(0..0)
503 }
504
505 fn slice(&self, range: &Range<usize>) -> String {
506 self.input.get(range.clone()).unwrap_or("").to_owned()
507 }
508}
509
510fn markdown_options() -> Options {
511 let mut options = Options::empty();
512 options.insert(Options::ENABLE_TABLES);
513 options.insert(Options::ENABLE_FOOTNOTES);
514 options.insert(Options::ENABLE_TASKLISTS);
515 options.insert(Options::ENABLE_HEADING_ATTRIBUTES);
516 options.insert(Options::ENABLE_MATH);
517 options
518}
519
520fn markdown_id() -> BackendId {
521 BackendId::new("markdown")
522}
523
524fn heading_level(level: HeadingLevel) -> u8 {
525 match level {
526 HeadingLevel::H1 => 1,
527 HeadingLevel::H2 => 2,
528 HeadingLevel::H3 => 3,
529 HeadingLevel::H4 => 4,
530 HeadingLevel::H5 => 5,
531 HeadingLevel::H6 => 6,
532 }
533}
534
535fn tex_math(text: pulldown_cmark::CowStr<'static>) -> MathSource {
536 MathSource {
537 notation: "tex".to_owned(),
538 text: text.trim_matches('\n').to_owned(),
539 }
540}
541
542fn span(start: usize, end: usize) -> Span {
543 Span {
544 start,
545 end,
546 state: crate::SpanState::Preserved,
547 }
548}
549
550fn inline_plain_text(items: &[Inline]) -> String {
551 let mut text = String::new();
552 for item in items {
553 match item {
554 Inline::Text(value) | Inline::Code(value) => text.push_str(value),
555 Inline::Emph(children) | Inline::Strong(children) => {
556 text.push_str(&inline_plain_text(children));
557 }
558 Inline::Link { label, .. } => text.push_str(&inline_plain_text(label)),
559 Inline::Math(source) => text.push_str(&source.text),
560 Inline::Raw { text: raw, .. } => text.push_str(raw),
561 }
562 }
563 text
564}