readme_sync/
sync.rs

1use core::fmt::Display;
2use std::borrow::ToOwned;
3use std::path::Path;
4use std::string::String;
5
6use pulldown_cmark::CowStr;
7use thiserror::Error;
8
9use crate::{CMarkDocs, CMarkReadme};
10
11/// Asserts that the given readme and docs are the same.
12pub fn assert_sync<M1, M2>(readme: &CMarkReadme<&Path, M1>, docs: &CMarkDocs<&Path, M2>) {
13    match check_sync(readme, docs) {
14        Ok(()) => {}
15        Err(CheckSyncError::MatchFailed(err)) => {
16            err.emit_to_stderr_colored();
17            panic!();
18        }
19    }
20}
21
22/// Returns `Ok(())` if the given readme and docs are the same, and `Err(CheckSyncError)` otherwise.
23pub fn check_sync<P1, P2, M1, M2>(
24    readme: &CMarkReadme<P1, M1>,
25    docs: &CMarkDocs<P2, M2>,
26) -> Result<(), CheckSyncError> {
27    use std::vec::Vec;
28
29    let mut readme_iter = readme.iter();
30    let mut docs_iter = docs.iter();
31    let mut matched_events = Vec::new();
32
33    loop {
34        let NextItem {
35            node: readme_node,
36            event: readme_event,
37            removed: readme_removed_nodes,
38        } = next_node(&mut readme_iter);
39
40        let NextItem {
41            node: docs_node,
42            event: docs_event,
43            removed: docs_removed_nodes,
44        } = next_node(&mut docs_iter);
45
46        if readme_node.is_none() && docs_node.is_none() {
47            break;
48        }
49
50        if readme_event == docs_event {
51            matched_events.push(readme_event.unwrap());
52        } else {
53            use crate::CodemapFiles;
54            use std::sync::Arc;
55
56            let mut codemap_files = CodemapFiles::new();
57            let mut diags = std::vec![node_not_mached_diagnostic(
58                &mut codemap_files,
59                &readme_node,
60                &docs_node,
61            )];
62
63            diags.extend(removed_nodes_note(
64                &mut codemap_files,
65                &readme_removed_nodes,
66                "readme",
67            ));
68
69            diags.extend(removed_nodes_note(
70                &mut codemap_files,
71                &docs_removed_nodes,
72                "docs",
73            ));
74
75            if let (Some(readme_event), Some(docs_event)) = (readme_event, docs_event) {
76                diags.append(&mut event_diff_notes(&readme_event, &docs_event));
77            }
78
79            diags.push(previous_events_notes(&matched_events));
80
81            let codemap_files = Arc::new(codemap_files);
82            return Err(CheckSyncError::MatchFailed(MatchFailed {
83                diags,
84                codemap_files,
85            }));
86        }
87    }
88    Ok(())
89}
90
91/// An error which can occur when checking readme and docs for equality.
92#[derive(Clone, Debug, Error)]
93pub enum CheckSyncError {
94    /// Readme and docs are not the same.
95    #[error(
96        "CMarkReadme and CMarkDocs nodes are not the same. \
97         Use `MatchFailed::emit_to_stderr` for details."
98    )]
99    MatchFailed(MatchFailed),
100}
101
102/// Readme and docs match failed diagnostics and codemap files.
103#[derive(Clone, Debug)]
104pub struct MatchFailed {
105    diags: std::vec::Vec<codemap_diagnostic::Diagnostic>,
106    codemap_files: std::sync::Arc<crate::CodemapFiles>,
107}
108
109impl MatchFailed {
110    /// Print dianostic messages to console with colors.
111    pub fn emit_to_stderr_colored(&self) {
112        use codemap_diagnostic::{ColorConfig, Emitter};
113
114        let mut emitter = Emitter::stderr(ColorConfig::Always, Some(self.codemap_files.codemap()));
115        emitter.emit(&self.diags);
116    }
117}
118
119impl Display for MatchFailed {
120    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
121        use codemap_diagnostic::Emitter;
122        use std::vec::Vec;
123
124        let mut raw = Vec::new();
125        {
126            let mut emitter = Emitter::vec(&mut raw, Some(self.codemap_files.codemap()));
127            emitter.emit(&self.diags);
128        }
129        let msg = String::from_utf8_lossy(&raw);
130        write!(f, "{}", msg)
131    }
132}
133
134struct NextItem<'a> {
135    node: Option<std::sync::Arc<crate::CMarkItem>>,
136    event: Option<pulldown_cmark::Event<'a>>,
137    removed: std::vec::Vec<std::sync::Arc<crate::CMarkItem>>,
138}
139
140fn next_node<'a>(iter: &mut crate::CMarkDataIter<'a>) -> NextItem<'a> {
141    use std::sync::Arc;
142    use std::vec::Vec;
143
144    let mut removed = Vec::new();
145    loop {
146        if let Some(node) = iter.next() {
147            if let Some(event) = node.event() {
148                return NextItem {
149                    node: Some(Arc::clone(node)),
150                    event: Some(event.clone()),
151                    removed,
152                };
153            } else {
154                removed.push(Arc::clone(node));
155            }
156        } else {
157            return NextItem {
158                node: None,
159                event: None,
160                removed,
161            };
162        }
163    }
164}
165
166fn node_not_mached_diagnostic(
167    codemap_files: &mut crate::CodemapFiles,
168    readme_node: &Option<std::sync::Arc<crate::CMarkItem>>,
169    docs_node: &Option<std::sync::Arc<crate::CMarkItem>>,
170) -> codemap_diagnostic::Diagnostic {
171    use crate::CodemapSpans;
172    use codemap_diagnostic::{Diagnostic, Level};
173    use std::format;
174
175    let nodes = [readme_node, docs_node];
176    let spans = nodes
177        .iter()
178        .filter_map(|node| node.as_ref())
179        .flat_map(|node| node.spans());
180    let span_labels = CodemapSpans::span_labels_from(codemap_files, spans);
181    let readme_event = readme_node.as_ref().and_then(|node| node.event());
182    let docs_event = docs_node.as_ref().and_then(|node| node.event());
183
184    let message = match (readme_event, docs_event) {
185        (Some(readme_event), Some(docs_event)) => format!(
186            "readme node\n`{}`\n does not match docs node\n`{}`",
187            FmtPrint(readme_event),
188            FmtPrint(docs_event)
189        ),
190        (Some(readme_event), None) => format!(
191            "readme node\n`{}`\n does not match any docs node",
192            FmtPrint(readme_event)
193        ),
194        (None, Some(docs_event)) => format!(
195            "docs node\n`{}`\n does not match any readme node",
196            FmtPrint(docs_event)
197        ),
198        (None, None) => unreachable!(),
199    };
200
201    Diagnostic {
202        level: Level::Error,
203        message,
204        code: None,
205        spans: span_labels,
206    }
207}
208
209fn removed_nodes_note(
210    codemap_files: &mut crate::CodemapFiles,
211    nodes: &[std::sync::Arc<crate::CMarkItem>],
212    node_type: &str,
213) -> Option<codemap_diagnostic::Diagnostic> {
214    use crate::CodemapSpans;
215    use codemap_diagnostic::{Diagnostic, Level};
216    use std::format;
217
218    if nodes.is_empty() {
219        None
220    } else {
221        let spans = nodes.iter().flat_map(|node| node.spans());
222        let span_labels = CodemapSpans::span_labels_from(codemap_files, spans);
223        Some(Diagnostic {
224            level: Level::Note,
225            message: format!("some {} nodes were removed before these", node_type),
226            code: None,
227            spans: span_labels,
228        })
229    }
230}
231
232fn event_diff_notes(
233    readme_event: &pulldown_cmark::Event<'_>,
234    docs_event: &pulldown_cmark::Event<'_>,
235) -> std::vec::Vec<codemap_diagnostic::Diagnostic> {
236    use std::iter::repeat;
237    use std::string::ToString;
238    use std::{format, vec};
239
240    use pulldown_cmark::{CodeBlockKind, Event, Tag};
241
242    let readme_event_name = get_event_name(readme_event);
243    let docs_event_name = get_event_name(docs_event);
244    if readme_event_name != docs_event_name {
245        return vec![
246            text_note(std::format!(
247                "readme node event name is \"{}\"",
248                readme_event_name
249            )),
250            text_note(std::format!(
251                "docs   node event name is \"{}\"",
252                docs_event_name
253            )),
254        ];
255    }
256
257    let readme_tag = get_event_start_tag(readme_event);
258    let docs_tag = get_event_start_tag(docs_event);
259    if let (Some(readme_tag), Some(docs_tag)) = (readme_tag, docs_tag) {
260        let readme_tag_name = get_start_tag_name(readme_tag);
261        let docs_tag_name = get_start_tag_name(docs_tag);
262        if readme_tag_name != docs_tag_name {
263            let mut notes = vec![
264                text_note(std::format!(
265                    "readme node event start tag name is \"{}\"",
266                    readme_tag_name
267                )),
268                text_note(std::format!(
269                    "docs   node event start tag name is \"{}\"",
270                    docs_tag_name
271                )),
272            ];
273            if let Event::Start(Tag::CodeBlock(CodeBlockKind::Indented)) = docs_event {
274                notes.push(text_note(
275                    concat!(
276                        "Possible issue: ",
277                        "Rustdoc ignore indents in the consecutive ",
278                        "doc-comments and doc-attributes. ",
279                        "However, the four-space indents should be ",
280                        "interpreted as Indented code blocks in CMark. ",
281                        "Issue: https://github.com/rust-lang/rust/issues/70732",
282                    )
283                    .to_string(),
284                ));
285            }
286            return notes;
287        }
288    }
289
290    let readme_tag = get_event_end_tag(readme_event);
291    let docs_tag = get_event_end_tag(docs_event);
292    if let (Some(readme_tag), Some(docs_tag)) = (readme_tag, docs_tag) {
293        let readme_tag_name = get_end_tag_name(readme_tag);
294        let docs_tag_name = get_end_tag_name(docs_tag);
295        if readme_tag_name != docs_tag_name {
296            let notes = vec![
297                text_note(std::format!(
298                    "readme node event end tag name is \"{}\"",
299                    readme_tag_name
300                )),
301                text_note(std::format!(
302                    "docs   node event end tag name is \"{}\"",
303                    docs_tag_name
304                )),
305            ];
306            return notes;
307        }
308    }
309
310    let readme_text = get_event_text(readme_event);
311    let docs_text = get_event_text(docs_event);
312    if let (Some(readme_text), Some(docs_text)) = (readme_text, docs_text) {
313        if readme_text != docs_text {
314            const OFFSET: usize = 32;
315            const LEN: usize = 32;
316
317            let readme_chars = readme_text.char_indices().map(Some).chain(repeat(None));
318            let docs_chars = docs_text.char_indices().map(Some).chain(repeat(None));
319            let mut chars = readme_chars.zip(docs_chars);
320            let pos = chars
321                .find_map(|pair| match pair {
322                    (Some(lhs), Some(rhs)) => {
323                        if lhs.1 != rhs.1 {
324                            assert_eq!(lhs.0, rhs.0);
325                            Some(lhs.0)
326                        } else {
327                            None
328                        }
329                    }
330                    (Some(lhs), _) => Some(lhs.0),
331                    (_, Some(rhs)) => Some(rhs.0),
332                    (None, None) => unreachable!(),
333                })
334                .unwrap();
335            let start = pos.saturating_sub(OFFSET);
336            let end = pos + LEN;
337
338            return vec![
339                text_note(std::format!(
340                    "readme node text part: \"{}\"",
341                    formatted_subslice(readme_text, start, end)
342                )),
343                text_note(std::format!(
344                    "docs   node text part: \"{}\"",
345                    formatted_subslice(docs_text, start, end)
346                )),
347            ];
348        }
349    }
350
351    vec![
352        text_note(format!("readme node: {}", FmtPrint(readme_event))),
353        text_note(format!("docs   node: {}", FmtPrint(docs_event))),
354    ]
355}
356
357fn previous_events_notes(events: &[pulldown_cmark::Event<'_>]) -> codemap_diagnostic::Diagnostic {
358    use std::format;
359    use std::string::ToString;
360
361    const MAX_EVENTS_SHOWN: usize = 16;
362
363    if events.is_empty() {
364        text_note("match failed on first events".to_string())
365    } else {
366        let from = events.len().saturating_sub(MAX_EVENTS_SHOWN);
367        let mut note = "previous events: [\n".to_owned();
368        if from != 0 {
369            note += "    ...\n";
370        }
371        for event in &events[from..] {
372            note += &format!("    {}\n", FmtPrint(event));
373        }
374        note += "]";
375        text_note(note)
376    }
377}
378
379fn text_note(message: String) -> codemap_diagnostic::Diagnostic {
380    use codemap_diagnostic::{Diagnostic, Level};
381    use std::vec::Vec;
382
383    Diagnostic {
384        level: Level::Note,
385        message,
386        code: None,
387        spans: Vec::new(),
388    }
389}
390
391fn formatted_subslice(text: &str, start: usize, end: usize) -> String {
392    use std::format;
393
394    let skip_before = start > 3;
395    let start = if skip_before { start } else { 0 };
396    let skip_after = text.len().saturating_sub(end) > 3;
397    let end = if skip_after { end } else { text.len() };
398
399    format!(
400        "{}{}{}",
401        if skip_before { "..." } else { "" },
402        &text[start..end],
403        if skip_after { "..." } else { "" }
404    )
405}
406
407fn get_event_start_tag<'a>(
408    event: &'a pulldown_cmark::Event<'_>,
409) -> Option<&'a pulldown_cmark::Tag<'a>> {
410    use pulldown_cmark::Event;
411    match event {
412        Event::Start(tag) => Some(tag),
413        _ => None,
414    }
415}
416
417fn get_event_end_tag<'a>(
418    event: &'a pulldown_cmark::Event<'_>,
419) -> Option<&'a pulldown_cmark::TagEnd> {
420    use pulldown_cmark::Event;
421    match event {
422        Event::End(tag) => Some(tag),
423        _ => None,
424    }
425}
426
427fn get_event_text<'a>(event: &'a pulldown_cmark::Event<'_>) -> Option<&'a str> {
428    use pulldown_cmark::Event;
429    match event {
430        Event::Text(text) => Some(text),
431        Event::Code(text) => Some(text),
432        Event::Html(text) => Some(text),
433        Event::FootnoteReference(text) => Some(text),
434        _ => None,
435    }
436}
437
438fn get_event_name<'a>(event: &pulldown_cmark::Event<'_>) -> &'a str {
439    use pulldown_cmark::Event;
440    match event {
441        Event::Start(..) => "Start",
442        Event::End(..) => "End",
443        Event::Text(..) => "Text",
444        Event::Code(..) => "Code",
445        Event::InlineMath(..) => "InlineMath",
446        Event::DisplayMath(..) => "DisplayMath",
447        Event::Html(..) => "Html",
448        Event::InlineHtml(..) => "InlineHtml",
449        Event::FootnoteReference(..) => "FootnoteReference",
450        Event::SoftBreak => "SoftBreak",
451        Event::HardBreak => "HardBreak",
452        Event::Rule => "Rule",
453        Event::TaskListMarker(..) => "TaskListMarker",
454    }
455}
456
457fn get_start_tag_name<'a>(tag: &'a pulldown_cmark::Tag<'_>) -> &'a str {
458    use pulldown_cmark::Tag;
459    match tag {
460        Tag::Paragraph => "Paragraph",
461        Tag::Heading { .. } => "Heading",
462        Tag::BlockQuote(..) => "BlockQuote",
463        Tag::CodeBlock(..) => "CodeBlock",
464        Tag::HtmlBlock { .. } => "HtmlBlock",
465        Tag::List(..) => "List",
466        Tag::Item => "Item",
467        Tag::FootnoteDefinition(..) => "FootnoteDefinition",
468        Tag::DefinitionList => "DefinitionList",
469        Tag::DefinitionListTitle => "DefinitionListTitle",
470        Tag::DefinitionListDefinition => "DefinitionListDefinition",
471        Tag::Table(..) => "Table",
472        Tag::TableHead => "TableHead",
473        Tag::TableRow => "TableRow",
474        Tag::TableCell => "TableCell",
475        Tag::Emphasis => "Emphasis",
476        Tag::Strong => "Strong",
477        Tag::Strikethrough => "Strikethrough",
478        Tag::Link { .. } => "Link",
479        Tag::Image { .. } => "Image",
480        Tag::MetadataBlock(..) => "MetadataBlock",
481    }
482}
483
484fn get_end_tag_name(tag: &pulldown_cmark::TagEnd) -> &str {
485    use pulldown_cmark::TagEnd;
486    match tag {
487        TagEnd::Paragraph => "Paragraph",
488        TagEnd::Heading { .. } => "Heading",
489        TagEnd::BlockQuote(..) => "BlockQuote",
490        TagEnd::CodeBlock => "CodeBlock",
491        TagEnd::HtmlBlock { .. } => "HtmlBlock",
492        TagEnd::List(..) => "List",
493        TagEnd::Item => "Item",
494        TagEnd::FootnoteDefinition => "FootnoteDefinition",
495        TagEnd::DefinitionList => "DefinitionList",
496        TagEnd::DefinitionListTitle => "DefinitionListTitle",
497        TagEnd::DefinitionListDefinition => "DefinitionListDefinition",
498        TagEnd::Table => "Table",
499        TagEnd::TableHead => "TableHead",
500        TagEnd::TableRow => "TableRow",
501        TagEnd::TableCell => "TableCell",
502        TagEnd::Emphasis => "Emphasis",
503        TagEnd::Strong => "Strong",
504        TagEnd::Strikethrough => "Strikethrough",
505        TagEnd::Link { .. } => "Link",
506        TagEnd::Image { .. } => "Image",
507        TagEnd::MetadataBlock(..) => "MetadataBlock",
508    }
509}
510
511pub trait Print {
512    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result;
513}
514
515#[derive(Clone, Debug)]
516pub struct FmtPrint<T>(T);
517
518impl<T> Display for FmtPrint<T>
519where
520    T: Print,
521{
522    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
523        T::fmt(&self.0, f)
524    }
525}
526
527impl<T> Print for &T
528where
529    T: ?Sized + Print,
530{
531    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
532        T::fmt(self, f)
533    }
534}
535
536impl<T> Print for Option<T>
537where
538    T: Print,
539{
540    fn fmt(&self, fmt: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
541        match self {
542            Some(value) => write!(fmt, "Some({})", FmtPrint(value)),
543            None => write!(fmt, "None"),
544        }
545    }
546}
547
548impl<T> Print for [T]
549where
550    T: Print,
551{
552    fn fmt(&self, fmt: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
553        let mut iter = self.iter();
554        writeln!(fmt, "[")?;
555        if let Some(first) = iter.next() {
556            writeln!(fmt, "{}", FmtPrint(first))?;
557            for item in iter {
558                writeln!(fmt, ", {}", FmtPrint(item))?;
559            }
560        }
561        writeln!(fmt, "]")?;
562        Ok(())
563    }
564}
565
566impl Print for str {
567    fn fmt(&self, fmt: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
568        write!(fmt, "{}", self)
569    }
570}
571
572impl Print for CowStr<'_> {
573    fn fmt(&self, fmt: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
574        write!(fmt, "{}", self)
575    }
576}
577
578impl<T1, T2> Print for (T1, T2)
579where
580    T1: Print,
581    T2: Print,
582{
583    fn fmt(&self, fmt: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
584        write!(fmt, "({}, {})", FmtPrint(&self.0), FmtPrint(&self.1))
585    }
586}
587
588impl Print for pulldown_cmark::Event<'_> {
589    fn fmt(&self, fmt: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
590        use pulldown_cmark::Event;
591
592        let event_name = get_event_name(self);
593        write!(fmt, "{}", event_name)?;
594
595        match self {
596            Event::Start(tag) => write!(fmt, "({})", FmtPrint(tag)),
597            Event::End(tag) => write!(fmt, "({})", FmtPrint(tag)),
598            Event::Text(text)
599            | Event::Code(text)
600            | Event::InlineMath(text)
601            | Event::DisplayMath(text)
602            | Event::Html(text)
603            | Event::InlineHtml(text)
604            | Event::FootnoteReference(text) => write!(fmt, "(\"{}\")", &text),
605            Event::SoftBreak => Ok(()),
606            Event::HardBreak => Ok(()),
607            Event::Rule => Ok(()),
608            Event::TaskListMarker(ch) => write!(fmt, "({})", ch),
609        }
610    }
611}
612
613impl Print for pulldown_cmark::Tag<'_> {
614    fn fmt(&self, fmt: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
615        use pulldown_cmark::{MetadataBlockKind, Tag};
616
617        let tag_name = get_start_tag_name(self);
618        write!(fmt, "{}", tag_name)?;
619
620        match self {
621            Tag::Paragraph => Ok(()),
622            Tag::Heading {
623                level,
624                id,
625                classes,
626                attrs,
627            } => write!(
628                fmt,
629                "({}, \"{}\", {}, {})",
630                level,
631                FmtPrint(id.as_deref()),
632                FmtPrint(classes.as_slice()),
633                FmtPrint(attrs.as_slice())
634            ),
635            Tag::BlockQuote(kind) => write!(fmt, "({})", FmtPrint(kind)),
636            Tag::CodeBlock(kind) => write!(fmt, "({})", FmtPrint(kind)),
637            Tag::HtmlBlock => Ok(()),
638            Tag::List(Some(first)) => write!(fmt, "(Some({}))", first),
639            Tag::List(None) => write!(fmt, "(None)"),
640            Tag::Item => Ok(()),
641            Tag::FootnoteDefinition(label) => write!(fmt, "(\"{}\")", &label),
642            Tag::DefinitionList => Ok(()),
643            Tag::DefinitionListTitle => Ok(()),
644            Tag::DefinitionListDefinition => Ok(()),
645            Tag::Table(alignment) => write!(fmt, "({})", FmtPrint(&alignment[..])),
646            Tag::TableHead => Ok(()),
647            Tag::TableRow => Ok(()),
648            Tag::TableCell => Ok(()),
649            Tag::Emphasis => Ok(()),
650            Tag::Strong => Ok(()),
651            Tag::Strikethrough => Ok(()),
652            Tag::Link {
653                link_type,
654                dest_url,
655                title,
656                id,
657            } => {
658                write!(
659                    fmt,
660                    "({}, \"{}\", \"{}\", \"{}\")",
661                    FmtPrint(link_type),
662                    dest_url,
663                    title,
664                    id
665                )
666            }
667            Tag::Image {
668                link_type,
669                dest_url,
670                title,
671                id,
672            } => {
673                write!(
674                    fmt,
675                    "({}, \"{}\", \"{}\", \"{}\")",
676                    FmtPrint(link_type),
677                    dest_url,
678                    title,
679                    id
680                )
681            }
682            Tag::MetadataBlock(MetadataBlockKind::YamlStyle) => {
683                write!(fmt, "(\"yaml style\")")
684            }
685            Tag::MetadataBlock(MetadataBlockKind::PlusesStyle) => {
686                write!(fmt, "(\"pluses style\")")
687            }
688        }
689    }
690}
691
692impl Print for pulldown_cmark::TagEnd {
693    fn fmt(&self, fmt: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
694        use pulldown_cmark::{MetadataBlockKind, TagEnd};
695
696        let tag_name = get_end_tag_name(self);
697        write!(fmt, "/{}", tag_name)?;
698
699        match self {
700            TagEnd::Paragraph => Ok(()),
701            TagEnd::Heading(level) => write!(fmt, "({})", level),
702            TagEnd::BlockQuote(kind) => write!(fmt, "({})", FmtPrint(kind)),
703            TagEnd::CodeBlock => Ok(()),
704            TagEnd::HtmlBlock => Ok(()),
705            TagEnd::List(is_ordered) => write!(fmt, "({})", &is_ordered),
706            TagEnd::Item => Ok(()),
707            TagEnd::FootnoteDefinition => Ok(()),
708            TagEnd::DefinitionList => Ok(()),
709            TagEnd::DefinitionListTitle => Ok(()),
710            TagEnd::DefinitionListDefinition => Ok(()),
711            TagEnd::Table => Ok(()),
712            TagEnd::TableHead => Ok(()),
713            TagEnd::TableRow => Ok(()),
714            TagEnd::TableCell => Ok(()),
715            TagEnd::Emphasis => Ok(()),
716            TagEnd::Strong => Ok(()),
717            TagEnd::Strikethrough => Ok(()),
718            TagEnd::Link => Ok(()),
719            TagEnd::Image => Ok(()),
720            TagEnd::MetadataBlock(MetadataBlockKind::YamlStyle) => {
721                write!(fmt, "(\"yaml style\")")
722            }
723            TagEnd::MetadataBlock(MetadataBlockKind::PlusesStyle) => {
724                write!(fmt, "(\"pluses style\")")
725            }
726        }
727    }
728}
729
730impl Print for pulldown_cmark::CodeBlockKind<'_> {
731    fn fmt(&self, fmt: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
732        use pulldown_cmark::CodeBlockKind;
733
734        match self {
735            CodeBlockKind::Indented => write!(fmt, "Indented"),
736            CodeBlockKind::Fenced(tag) => write!(fmt, "Fenced({})", tag),
737        }
738    }
739}
740
741impl Print for pulldown_cmark::Alignment {
742    fn fmt(&self, fmt: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
743        use pulldown_cmark::Alignment;
744
745        match self {
746            Alignment::None => write!(fmt, "None"),
747            Alignment::Left => write!(fmt, "Left"),
748            Alignment::Center => write!(fmt, "Center"),
749            Alignment::Right => write!(fmt, "Right"),
750        }
751    }
752}
753
754impl Print for pulldown_cmark::LinkType {
755    fn fmt(&self, fmt: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
756        use pulldown_cmark::LinkType;
757
758        match self {
759            LinkType::Inline => write!(fmt, "Inline"),
760            LinkType::Reference => write!(fmt, "Reference"),
761            LinkType::ReferenceUnknown => write!(fmt, "ReferenceUnknown"),
762            LinkType::Collapsed => write!(fmt, "Collapsed"),
763            LinkType::CollapsedUnknown => write!(fmt, "CollapsedUnknown"),
764            LinkType::Shortcut => write!(fmt, "Shortcut"),
765            LinkType::ShortcutUnknown => write!(fmt, "ShortcutUnknown"),
766            LinkType::Autolink => write!(fmt, "Autolink"),
767            LinkType::Email => write!(fmt, "Email"),
768        }
769    }
770}
771
772impl Print for pulldown_cmark::BlockQuoteKind {
773    fn fmt(&self, fmt: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
774        use pulldown_cmark::BlockQuoteKind;
775
776        match self {
777            BlockQuoteKind::Note => write!(fmt, "Note"),
778            BlockQuoteKind::Tip => write!(fmt, "Tip"),
779            BlockQuoteKind::Important => write!(fmt, "Important"),
780            BlockQuoteKind::Warning => write!(fmt, "Warning"),
781            BlockQuoteKind::Caution => write!(fmt, "Caution"),
782        }
783    }
784}