Skip to main content

asciidoc_parser/blocks/
media.rs

1use crate::{
2    HasSpan, Parser, Span,
3    attributes::{Attrlist, AttrlistContext},
4    blocks::{ContentModel, IsBlock, metadata::BlockMetadata},
5    content::substitute_attributes_in_macro_target,
6    span::MatchedItem,
7    strings::CowStr,
8    warnings::{MatchAndWarnings, Warning, WarningType},
9};
10
11/// A media block is used to represent an image, video, or audio block macro.
12#[derive(Clone, Debug, Eq, PartialEq)]
13pub struct MediaBlock<'src> {
14    type_: MediaType,
15    target: Span<'src>,
16    resolved_target: CowStr<'src>,
17    macro_attrlist: Attrlist<'src>,
18    source: Span<'src>,
19    title_source: Option<Span<'src>>,
20    title: Option<String>,
21    anchor: Option<Span<'src>>,
22    anchor_reftext: Option<Span<'src>>,
23    attrlist: Option<Attrlist<'src>>,
24}
25
26/// Outcome of resolving attribute references in a media block's target via
27/// [`MediaBlock::resolve_target`].
28#[derive(Clone, Copy, Debug, Eq, PartialEq)]
29pub(crate) enum TargetResolution {
30    /// The target was resolved and stored; the block should be kept.
31    Keep,
32
33    /// The target referenced a missing attribute under
34    /// `attribute-missing=drop-line`, so the entire block should be dropped.
35    Drop,
36}
37
38/// A media type may be one of three different types.
39#[derive(Clone, Copy, Eq, PartialEq)]
40pub enum MediaType {
41    /// Still image
42    Image,
43
44    /// Video
45    Video,
46
47    /// Audio
48    Audio,
49}
50
51impl std::fmt::Debug for MediaType {
52    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
53        match self {
54            MediaType::Image => write!(f, "MediaType::Image"),
55            MediaType::Video => write!(f, "MediaType::Video"),
56            MediaType::Audio => write!(f, "MediaType::Audio"),
57        }
58    }
59}
60
61impl<'src> MediaBlock<'src> {
62    pub(crate) fn parse(
63        metadata: &BlockMetadata<'src>,
64        parser: &mut Parser,
65    ) -> MatchAndWarnings<'src, Option<MatchedItem<'src, Self>>> {
66        let line = metadata.block_start.take_normalized_line();
67
68        // Line must end with `]`; otherwise, it's not a block macro.
69        if !line.item.ends_with(']') {
70            return MatchAndWarnings {
71                item: None,
72                warnings: vec![],
73            };
74        }
75
76        let Some(name) = line.item.take_ident() else {
77            return MatchAndWarnings {
78                item: None,
79                warnings: vec![],
80            };
81        };
82
83        let type_ = match name.item.data() {
84            "image" => MediaType::Image,
85            "video" => MediaType::Video,
86            "audio" => MediaType::Audio,
87            _ => {
88                return MatchAndWarnings {
89                    item: None,
90                    warnings: vec![],
91                };
92            }
93        };
94
95        let Some(colons) = name.after.take_prefix("::") else {
96            return MatchAndWarnings {
97                item: None,
98                warnings: vec![Warning {
99                    source: name.after,
100                    warning: WarningType::MacroMissingDoubleColon,
101                }],
102            };
103        };
104
105        // The target field must exist and be non-empty.
106        let target = colons.after.take_while(|c| c != '[');
107
108        if target.item.is_empty() {
109            return MatchAndWarnings {
110                item: None,
111                warnings: vec![Warning {
112                    source: target.after,
113                    warning: WarningType::MediaMacroMissingTarget,
114                }],
115            };
116        }
117
118        let Some(open_brace) = target.after.take_prefix("[") else {
119            return MatchAndWarnings {
120                item: None,
121                warnings: vec![Warning {
122                    source: target.after,
123                    warning: WarningType::MacroMissingAttributeList,
124                }],
125            };
126        };
127
128        let attrlist = open_brace.after.slice(0..open_brace.after.len() - 1);
129        // Note that we already checked that this line ends with a close brace.
130
131        let macro_attrlist = Attrlist::parse(attrlist, parser, AttrlistContext::Inline);
132
133        let source: Span = metadata.source.trim_remainder(line.after);
134        let source = source.slice(0..source.trim().len());
135
136        MatchAndWarnings {
137            item: Some(MatchedItem {
138                item: Self {
139                    type_,
140                    target: target.item,
141                    // Attribute references in the target are resolved later, in
142                    // `resolve_target` (which also decides whether a missing
143                    // reference should drop the whole block); until then, the
144                    // resolved target mirrors the raw target verbatim.
145                    resolved_target: target.item.data().into(),
146                    macro_attrlist: macro_attrlist.item.item,
147                    source,
148                    title_source: metadata.title_source,
149                    title: metadata.title.clone(),
150                    anchor: metadata.anchor,
151                    anchor_reftext: metadata.anchor_reftext,
152                    attrlist: metadata.attrlist.clone(),
153                },
154
155                after: line.after.discard_empty_lines(),
156            }),
157            warnings: macro_attrlist.warnings,
158        }
159    }
160
161    /// Return a [`Span`] describing the macro name.
162    pub fn type_(&self) -> MediaType {
163        self.type_
164    }
165
166    /// Return a [`Span`] describing the macro target.
167    ///
168    /// This is the target exactly as written in the source, _before_ any
169    /// attribute references within it are resolved. See
170    /// [`resolved_target()`](Self::resolved_target) for the resolved form.
171    pub fn target(&'src self) -> Option<&'src Span<'src>> {
172        Some(&self.target)
173    }
174
175    /// Return the macro target after any attribute references within it have
176    /// been resolved (honoring the [`attribute-missing`] document attribute).
177    ///
178    /// For the common case of a target with no attribute references, this is
179    /// identical to the text of [`target()`](Self::target).
180    ///
181    /// [`attribute-missing`]: https://docs.asciidoctor.org/asciidoc/latest/attributes/unresolved-references/#missing
182    pub fn resolved_target(&self) -> &str {
183        self.resolved_target.as_ref()
184    }
185
186    /// Resolve attribute references in this block's target, honoring the
187    /// [`attribute-missing`] document attribute.
188    ///
189    /// On success the resolved target is stored (see
190    /// [`resolved_target()`](Self::resolved_target)) and
191    /// [`TargetResolution::Keep`] is returned. When the target references a
192    /// missing attribute and `attribute-missing=drop-line` is in effect,
193    /// [`TargetResolution::Drop`] is returned and the caller drops the
194    /// entire block.
195    ///
196    /// [`attribute-missing`]: https://docs.asciidoctor.org/asciidoc/latest/attributes/unresolved-references/#missing
197    pub(crate) fn resolve_target(&mut self, parser: &Parser) -> TargetResolution {
198        match substitute_attributes_in_macro_target(self.target, parser) {
199            Some(resolved) => {
200                self.resolved_target = resolved;
201                TargetResolution::Keep
202            }
203            None => TargetResolution::Drop,
204        }
205    }
206
207    /// Return the macro's attribute list.
208    ///
209    /// **IMPORTANT:** This is the list of attributes _within_ the macro block
210    /// definition itself.
211    ///
212    /// See also [`attrlist()`] for attributes that can be defined before the
213    /// macro invocation.
214    ///
215    /// [`attrlist()`]: Self::attrlist()
216    pub fn macro_attrlist(&'src self) -> &'src Attrlist<'src> {
217        &self.macro_attrlist
218    }
219}
220
221impl<'src> IsBlock<'src> for MediaBlock<'src> {
222    fn content_model(&self) -> ContentModel {
223        ContentModel::Empty
224    }
225
226    fn raw_context(&self) -> CowStr<'src> {
227        match self.type_ {
228            MediaType::Audio => "audio",
229            MediaType::Image => "image",
230            MediaType::Video => "video",
231        }
232        .into()
233    }
234
235    fn title_source(&'src self) -> Option<Span<'src>> {
236        self.title_source
237    }
238
239    fn title(&self) -> Option<&str> {
240        self.title.as_deref()
241    }
242
243    fn anchor(&'src self) -> Option<Span<'src>> {
244        self.anchor
245    }
246
247    fn anchor_reftext(&'src self) -> Option<Span<'src>> {
248        self.anchor_reftext
249    }
250
251    fn attrlist(&'src self) -> Option<&'src Attrlist<'src>> {
252        self.attrlist.as_ref()
253    }
254}
255
256impl<'src> HasSpan<'src> for MediaBlock<'src> {
257    fn span(&self) -> Span<'src> {
258        self.source
259    }
260}
261
262#[cfg(test)]
263mod tests {
264    #![allow(clippy::unwrap_used)]
265
266    use std::ops::Deref;
267
268    use crate::{
269        blocks::{ContentModel, MediaType, metadata::BlockMetadata},
270        tests::prelude::*,
271    };
272
273    #[test]
274    fn impl_clone() {
275        // Silly test to mark the #[derive(...)] line as covered.
276        let mut parser = Parser::default();
277
278        let b1 =
279            crate::blocks::MediaBlock::parse(&BlockMetadata::new("image::foo.jpg[]"), &mut parser)
280                .unwrap_if_no_warnings()
281                .unwrap()
282                .item;
283
284        let b2 = b1.clone();
285        assert_eq!(b1, b2);
286    }
287
288    #[test]
289    fn err_empty_source() {
290        let mut parser = Parser::default();
291        assert!(
292            crate::blocks::MediaBlock::parse(&BlockMetadata::new(""), &mut parser)
293                .unwrap_if_no_warnings()
294                .is_none()
295        );
296    }
297
298    #[test]
299    fn err_only_spaces() {
300        let mut parser = Parser::default();
301        assert!(
302            crate::blocks::MediaBlock::parse(&BlockMetadata::new("    "), &mut parser)
303                .unwrap_if_no_warnings()
304                .is_none()
305        );
306    }
307
308    #[test]
309    fn err_macro_name_not_ident() {
310        let mut parser = Parser::default();
311        let maw = crate::blocks::MediaBlock::parse(
312            &BlockMetadata::new("98xyz::bar[blah,blap]"),
313            &mut parser,
314        );
315
316        assert!(maw.item.is_none());
317        assert!(maw.warnings.is_empty());
318    }
319
320    #[test]
321    fn err_missing_double_colon() {
322        let mut parser = Parser::default();
323        let maw = crate::blocks::MediaBlock::parse(
324            &BlockMetadata::new("image:bar[blah,blap]"),
325            &mut parser,
326        );
327
328        assert!(maw.item.is_none());
329
330        assert_eq!(
331            maw.warnings,
332            vec![Warning {
333                source: Span {
334                    data: ":bar[blah,blap]",
335                    line: 1,
336                    col: 6,
337                    offset: 5,
338                },
339                warning: WarningType::MacroMissingDoubleColon,
340            }]
341        );
342    }
343
344    #[test]
345    fn err_missing_macro_attrlist() {
346        let mut parser = Parser::default();
347        let maw = crate::blocks::MediaBlock::parse(
348            &BlockMetadata::new("image::barblah,blap]"),
349            &mut parser,
350        );
351
352        assert!(maw.item.is_none());
353
354        assert_eq!(
355            maw.warnings,
356            vec![Warning {
357                source: Span {
358                    data: "",
359                    line: 1,
360                    col: 21,
361                    offset: 20,
362                },
363                warning: WarningType::MacroMissingAttributeList,
364            }]
365        );
366    }
367
368    #[test]
369    fn err_unknown_type() {
370        let mut parser = Parser::default();
371        assert!(
372            crate::blocks::MediaBlock::parse(&BlockMetadata::new("imagex::bar[]"), &mut parser)
373                .unwrap_if_no_warnings()
374                .is_none()
375        );
376    }
377
378    #[test]
379    fn err_no_attr_list() {
380        let mut parser = Parser::default();
381        assert!(
382            crate::blocks::MediaBlock::parse(&BlockMetadata::new("image::bar"), &mut parser)
383                .unwrap_if_no_warnings()
384                .is_none()
385        );
386    }
387
388    #[test]
389    fn err_attr_list_not_closed() {
390        let mut parser = Parser::default();
391        assert!(
392            crate::blocks::MediaBlock::parse(&BlockMetadata::new("image::bar[blah"), &mut parser)
393                .unwrap_if_no_warnings()
394                .is_none()
395        );
396    }
397
398    #[test]
399    fn err_unexpected_after_attr_list() {
400        let mut parser = Parser::default();
401        assert!(
402            crate::blocks::MediaBlock::parse(
403                &BlockMetadata::new("image::bar[blah]bonus"),
404                &mut parser
405            )
406            .unwrap_if_no_warnings()
407            .is_none()
408        );
409    }
410
411    #[test]
412    fn simplest_block_macro() {
413        let mut parser = Parser::default();
414
415        let mi = crate::blocks::MediaBlock::parse(&BlockMetadata::new("image::[]"), &mut parser);
416        assert!(mi.item.is_none());
417
418        assert_eq!(
419            mi.warnings,
420            vec![Warning {
421                source: Span {
422                    data: "[]",
423                    line: 1,
424                    col: 8,
425                    offset: 7,
426                },
427                warning: WarningType::MediaMacroMissingTarget,
428            }]
429        );
430    }
431
432    #[test]
433    fn has_target() {
434        let mut parser = Parser::default();
435
436        let mi = crate::blocks::MediaBlock::parse(&BlockMetadata::new("image::bar[]"), &mut parser)
437            .unwrap_if_no_warnings()
438            .unwrap();
439
440        assert_eq!(
441            mi.item,
442            MediaBlock {
443                type_: MediaType::Image,
444                target: Span {
445                    data: "bar",
446                    line: 1,
447                    col: 8,
448                    offset: 7,
449                },
450                macro_attrlist: Attrlist {
451                    attributes: &[],
452                    anchor: None,
453                    source: Span {
454                        data: "",
455                        line: 1,
456                        col: 12,
457                        offset: 11,
458                    }
459                },
460                source: Span {
461                    data: "image::bar[]",
462                    line: 1,
463                    col: 1,
464                    offset: 0,
465                },
466                title_source: None,
467                title: None,
468                anchor: None,
469                anchor_reftext: None,
470                attrlist: None,
471            }
472        );
473
474        assert_eq!(
475            mi.after,
476            Span {
477                data: "",
478                line: 1,
479                col: 13,
480                offset: 12
481            }
482        );
483
484        assert_eq!(mi.item.content_model(), ContentModel::Empty);
485        assert_eq!(mi.item.raw_context().deref(), "image");
486        assert!(mi.item.nested_blocks().next().is_none());
487        assert!(mi.item.title_source().is_none());
488        assert!(mi.item.title().is_none());
489        assert!(mi.item.anchor().is_none());
490        assert!(mi.item.anchor_reftext().is_none());
491        assert!(mi.item.attrlist().is_none());
492        assert_eq!(mi.item.substitution_group(), SubstitutionGroup::Normal);
493    }
494
495    #[test]
496    fn has_target_and_attrlist() {
497        let mut parser = Parser::default();
498
499        let mi =
500            crate::blocks::MediaBlock::parse(&BlockMetadata::new("image::bar[blah]"), &mut parser)
501                .unwrap_if_no_warnings()
502                .unwrap();
503
504        assert_eq!(
505            mi.item,
506            MediaBlock {
507                type_: MediaType::Image,
508                target: Span {
509                    data: "bar",
510                    line: 1,
511                    col: 8,
512                    offset: 7,
513                },
514                macro_attrlist: Attrlist {
515                    attributes: &[ElementAttribute {
516                        name: None,
517                        shorthand_items: &["blah"],
518                        value: "blah"
519                    }],
520                    anchor: None,
521                    source: Span {
522                        data: "blah",
523                        line: 1,
524                        col: 12,
525                        offset: 11,
526                    }
527                },
528                source: Span {
529                    data: "image::bar[blah]",
530                    line: 1,
531                    col: 1,
532                    offset: 0,
533                },
534                title_source: None,
535                title: None,
536                anchor: None,
537                anchor_reftext: None,
538                attrlist: None,
539            }
540        );
541
542        assert_eq!(
543            mi.after,
544            Span {
545                data: "",
546                line: 1,
547                col: 17,
548                offset: 16
549            }
550        );
551    }
552
553    #[test]
554    fn audio() {
555        let mut parser = Parser::default();
556
557        let mi = crate::blocks::MediaBlock::parse(&BlockMetadata::new("audio::bar[]"), &mut parser)
558            .unwrap_if_no_warnings()
559            .unwrap();
560
561        assert_eq!(
562            mi.item,
563            MediaBlock {
564                type_: MediaType::Audio,
565                target: Span {
566                    data: "bar",
567                    line: 1,
568                    col: 8,
569                    offset: 7,
570                },
571                macro_attrlist: Attrlist {
572                    attributes: &[],
573                    anchor: None,
574                    source: Span {
575                        data: "",
576                        line: 1,
577                        col: 12,
578                        offset: 11,
579                    }
580                },
581                source: Span {
582                    data: "audio::bar[]",
583                    line: 1,
584                    col: 1,
585                    offset: 0,
586                },
587                title_source: None,
588                title: None,
589                anchor: None,
590                anchor_reftext: None,
591                attrlist: None,
592            }
593        );
594
595        assert_eq!(
596            mi.after,
597            Span {
598                data: "",
599                line: 1,
600                col: 13,
601                offset: 12
602            }
603        );
604
605        assert_eq!(mi.item.content_model(), ContentModel::Empty);
606        assert_eq!(mi.item.raw_context().deref(), "audio");
607        assert!(mi.item.nested_blocks().next().is_none());
608        assert!(mi.item.title_source().is_none());
609        assert!(mi.item.title().is_none());
610        assert!(mi.item.anchor().is_none());
611        assert!(mi.item.anchor_reftext().is_none());
612        assert!(mi.item.attrlist().is_none());
613        assert_eq!(mi.item.substitution_group(), SubstitutionGroup::Normal);
614    }
615
616    #[test]
617    fn video() {
618        let mut parser = Parser::default();
619
620        let mi = crate::blocks::MediaBlock::parse(&BlockMetadata::new("video::bar[]"), &mut parser)
621            .unwrap_if_no_warnings()
622            .unwrap();
623
624        assert_eq!(
625            mi.item,
626            MediaBlock {
627                type_: MediaType::Video,
628                target: Span {
629                    data: "bar",
630                    line: 1,
631                    col: 8,
632                    offset: 7,
633                },
634                macro_attrlist: Attrlist {
635                    attributes: &[],
636                    anchor: None,
637                    source: Span {
638                        data: "",
639                        line: 1,
640                        col: 12,
641                        offset: 11,
642                    }
643                },
644                source: Span {
645                    data: "video::bar[]",
646                    line: 1,
647                    col: 1,
648                    offset: 0,
649                },
650                title_source: None,
651                title: None,
652                anchor: None,
653                anchor_reftext: None,
654                attrlist: None,
655            }
656        );
657
658        assert_eq!(
659            mi.after,
660            Span {
661                data: "",
662                line: 1,
663                col: 13,
664                offset: 12
665            }
666        );
667
668        assert_eq!(mi.item.content_model(), ContentModel::Empty);
669        assert_eq!(mi.item.raw_context().deref(), "video");
670        assert!(mi.item.nested_blocks().next().is_none());
671        assert!(mi.item.title_source().is_none());
672        assert!(mi.item.title().is_none());
673        assert!(mi.item.anchor().is_none());
674        assert!(mi.item.anchor_reftext().is_none());
675        assert!(mi.item.attrlist().is_none());
676        assert_eq!(mi.item.substitution_group(), SubstitutionGroup::Normal);
677    }
678
679    #[test]
680    fn err_duplicate_comma() {
681        let mut parser = Parser::default();
682        let maw = crate::blocks::MediaBlock::parse(
683            &BlockMetadata::new("image::bar[blah,,blap]"),
684            &mut parser,
685        );
686
687        let mi = maw.item.unwrap().clone();
688
689        assert_eq!(
690            mi.item,
691            MediaBlock {
692                type_: MediaType::Image,
693                target: Span {
694                    data: "bar",
695                    line: 1,
696                    col: 8,
697                    offset: 7,
698                },
699                macro_attrlist: Attrlist {
700                    attributes: &[
701                        ElementAttribute {
702                            name: None,
703                            shorthand_items: &["blah"],
704                            value: "blah"
705                        },
706                        ElementAttribute {
707                            name: None,
708                            shorthand_items: &[],
709                            value: "blap"
710                        }
711                    ],
712                    anchor: None,
713                    source: Span {
714                        data: "blah,,blap",
715                        line: 1,
716                        col: 12,
717                        offset: 11,
718                    }
719                },
720                source: Span {
721                    data: "image::bar[blah,,blap]",
722                    line: 1,
723                    col: 1,
724                    offset: 0,
725                },
726                title_source: None,
727                title: None,
728                anchor: None,
729                anchor_reftext: None,
730                attrlist: None,
731            }
732        );
733
734        assert_eq!(
735            mi.after,
736            Span {
737                data: "",
738                line: 1,
739                col: 23,
740                offset: 22
741            }
742        );
743
744        assert_eq!(
745            maw.warnings,
746            vec![Warning {
747                source: Span {
748                    data: "blah,,blap",
749                    line: 1,
750                    col: 12,
751                    offset: 11,
752                },
753                warning: WarningType::EmptyAttributeValue,
754            }]
755        );
756    }
757
758    mod target_resolution {
759        #![allow(clippy::indexing_slicing)]
760
761        use crate::{
762            blocks::{MediaBlock, media::TargetResolution, metadata::BlockMetadata},
763            parser::ModificationContext,
764            tests::prelude::*,
765            warnings::WarningType,
766        };
767
768        /// Parses `input` as a media block and resolves its target against
769        /// `parser`, returning the resolved [`MediaBlock`] (or `None` if the
770        /// block was dropped).
771        fn resolve<'i>(input: &'i str, parser: &mut Parser) -> Option<MediaBlock<'i>> {
772            let mut block = MediaBlock::parse(&BlockMetadata::new(input), parser)
773                .unwrap_if_no_warnings()
774                .unwrap()
775                .item;
776
777            match block.resolve_target(parser) {
778                TargetResolution::Keep => Some(block),
779                TargetResolution::Drop => None,
780            }
781        }
782
783        fn parser_with_mode(mode: &str) -> Parser {
784            Parser::default().with_intrinsic_attribute(
785                "attribute-missing",
786                mode,
787                ModificationContext::Anywhere,
788            )
789        }
790
791        #[test]
792        fn target_without_reference_is_unchanged() {
793            // The fast path (no `{`) returns the borrowed target verbatim.
794            let mut p = Parser::default();
795            let block = resolve("image::foo.png[]", &mut p).unwrap();
796            assert_eq!(block.resolved_target(), "foo.png");
797        }
798
799        #[test]
800        fn resolves_a_defined_reference() {
801            let mut p = Parser::default().with_intrinsic_attribute(
802                "name",
803                "bar",
804                ModificationContext::Anywhere,
805            );
806            let block = resolve("image::pre-{name}-post.png[]", &mut p).unwrap();
807            assert_eq!(block.resolved_target(), "pre-bar-post.png");
808        }
809
810        #[test]
811        fn skip_leaves_a_missing_reference_in_place() {
812            // `skip` is the default `attribute-missing` mode.
813            let mut p = Parser::default();
814            let block = resolve("image::a{missing}b.png[]", &mut p).unwrap();
815            assert_eq!(block.resolved_target(), "a{missing}b.png");
816            assert!(p.take_substitution_warnings().is_empty());
817        }
818
819        #[test]
820        fn drop_removes_only_the_missing_reference() {
821            let mut p = parser_with_mode("drop");
822            let block = resolve("image::a{missing}b.png[]", &mut p).unwrap();
823            assert_eq!(block.resolved_target(), "ab.png");
824        }
825
826        #[test]
827        fn warn_leaves_the_reference_and_records_a_warning() {
828            let mut p = parser_with_mode("warn");
829            let block = resolve("image::a{missing}b.png[]", &mut p).unwrap();
830            assert_eq!(block.resolved_target(), "a{missing}b.png");
831
832            let warnings = p.take_substitution_warnings();
833            assert_eq!(warnings.len(), 1);
834            assert_eq!(
835                warnings[0].warning,
836                WarningType::SkippingReferenceToMissingAttribute("missing".to_string())
837            );
838        }
839
840        #[test]
841        fn drop_line_drops_the_whole_block() {
842            let mut p = parser_with_mode("drop-line");
843            assert!(resolve("image::a{missing}b.png[]", &mut p).is_none());
844        }
845
846        #[test]
847        fn drop_line_keeps_a_block_whose_reference_resolves() {
848            let mut p = parser_with_mode("drop-line").with_intrinsic_attribute(
849                "name",
850                "bar",
851                ModificationContext::Anywhere,
852            );
853            let block = resolve("image::{name}.png[]", &mut p).unwrap();
854            assert_eq!(block.resolved_target(), "bar.png");
855        }
856
857        #[test]
858        fn drop_line_drops_a_top_level_block_but_keeps_following_blocks() {
859            // Exercises the drop at the document (non-list) level, which flows
860            // through `parse_blocks_until`.
861            let doc = Parser::default().parse(
862                ":attribute-missing: drop-line\n\nimage::{unresolved}[]\n\nparagraph after\n",
863            );
864
865            assert_css(&doc, ".imageblock", 0);
866            assert_css(&doc, ".paragraph", 1);
867        }
868
869        #[test]
870        fn drop_line_drops_a_top_level_audio_block() {
871            // Audio is a block macro too, so it honors `drop-line` just like an
872            // image block.
873            let doc = Parser::default().parse(
874                ":attribute-missing: drop-line\n\naudio::{unresolved}[]\n\nparagraph after\n",
875            );
876
877            assert_css(&doc, ".audioblock", 0);
878            assert_css(&doc, ".paragraph", 1);
879        }
880
881        #[test]
882        fn escaped_missing_reference_never_drops_the_block() {
883            // An escaped reference is not a missing reference, so even under
884            // `drop-line` the block survives. (As elsewhere in the crate, the
885            // escaping backslash is preserved verbatim by the attribute
886            // substitution.)
887            let mut p = parser_with_mode("drop-line");
888            let block = resolve("image::a\\{missing}b.png[]", &mut p).unwrap();
889            assert_eq!(block.resolved_target(), "a\\{missing}b.png");
890            assert!(p.take_substitution_warnings().is_empty());
891        }
892    }
893
894    mod media_type {
895        mod impl_debug {
896            use crate::blocks::MediaType;
897
898            #[test]
899            fn image() {
900                let media_type = MediaType::Image;
901                let debug_output = format!("{:?}", media_type);
902                assert_eq!(debug_output, "MediaType::Image");
903            }
904
905            #[test]
906            fn video() {
907                let media_type = MediaType::Video;
908                let debug_output = format!("{:?}", media_type);
909                assert_eq!(debug_output, "MediaType::Video");
910            }
911
912            #[test]
913            fn audio() {
914                let media_type = MediaType::Audio;
915                let debug_output = format!("{:?}", media_type);
916                assert_eq!(debug_output, "MediaType::Audio");
917            }
918        }
919    }
920}